feat(conductor): workflow template platform v2 (#478)#484
Merged
Weegy merged 11 commits intoJul 10, 2026
Conversation
…erence, strict gate (#478) Additive extension of the #429 template contract, pure functions only: TemplateManifest.version (absent = 1, read via templateManifestVersion), declared text slots substituted as slot:text:<key> tokens in the two designated prose fields (step.prompt, human.message; disjoint from {{...}} run-context interpolation, structural walk, never string-replace over JSON), TemplateSlotMapping.text, kind:'text' missing-slot entries, checkTemplateManifest text-slot coverage codes plus a { strict } mode that rejects concrete refs in distributed (plugin/hub) manifests, and inferTemplateManifest reversing the extractSlotRefs walk for the save-as-template path (slugified de-duplicated keys, pre-existing placeholders preserved, round-trip tested). Text machinery split into textSlots.ts and v2 tests into templateV2.test.ts to keep every file under the 500-line budget; the v1 test file stays byte-identical as the contract pin for #330 consumers.
…ing routes (#478) User-authored workflow templates move into the conductor DB: migration 0006_templates.sql (conductor chain) adds conductor_templates, conductor_template_versions (immutable JSONB manifests), conductor_template_instantiations (append-only anonymous telemetry) and template provenance columns on conductor_workflows. A composite catalog merges bundled files, DB user templates and a plugin registration seam behind one viewer-scoped interface whose visibility rule makes 'pending' templates reachable by every operator — every operator on the single-tier operator API is a potential reviewer, so the review gate cannot collapse into author-only self-approval. New CRUD + versioning routes (split into templateRoutes.ts for file size) keep the GET /templates wire shape additive over v1 so the #330 consumer contract holds; resolve/instantiate accept an explicit version and instantiate stamps provenance in the same transaction as the publish. Conductor migrations are now also mirrored into dist by copy-build-assets.mjs, which previously relied on the Dockerfile COPY alone.
…e templates, update hint (#478) Unit B3 of the templates-v2 plan: - POST /:slug/save-as-template returns an inferTemplateManifest DRAFT (one declared slot per distinct concrete ref) from the workflow's active published version; nothing persisted, UI publishes via POST/PUT /templates. Default id derives from the slug with a -template suffix on collision. - Review state machine private -> pending -> shared: submit (author-only, 409 template_status_conflict otherwise), approve/reject (ANY operator via the viewer-scoped catalog get - reachable because pending is visible install-wide; reviewed_by recorded; self-approval permitted and auditable). - Workflow list/detail additively report template?: { id, version, latestVersion, updateAvailable } from the template_id/ template_version provenance columns (viewer-scoped, no existence leak); workflowStore now selects those columns. - Plugin-borne templates (trust boundary in docs/security-architecture section 4): permissions.templates declares package-relative JSON manifests; fail-closed install gate in src/plugins/pluginTemplates.ts (path confinement after symlink unwrapping, plugin:<id>:<name> namespacing, checkTemplateManifest strict mode, isValidCron); any violation fails the install with install.template_invalid. Accepted manifests register as read-only source 'plugin' catalog entries, unregister on uninstall, and re-register at boot (fail-open sweep). Templates are data, never code - no runtime template API. Tests: test/pluginTemplates.test.ts (new) + extended test/conductorTemplateRoutes.test.ts (48 route cases incl. non-author approve, inference round-trip, update hint, plugin source read-only).
The conversational builder can now see the workflow-template catalog and suggest templates in chat: its system prompt carries a viewer-scoped catalog digest (capped at 30 entries) and the reply protocol accepts a templateProposals block that POST /builder/turn returns additively (the key is absent without proposals, keeping the v1 wire shape byte-identical). Proposals are vetted server-side inside the agent seam so the LLM cannot surface templates the viewer must not see: unknown/invisible ids dropped against the composite catalog, max 3 deduped proposals, version taken from the catalog rather than the model, and prefill guesses kept only for declared slot keys whose ref values resolve against the live KnownRefs sets (shared with the resolve/instantiate routes via a hoisted templateKnownRefs so the two gates cannot drift). Chat proposes and prefills only - instantiation stays on the deliberate form flow.
#478) Authoring UX in the Conductor admin UI over the backend's slot-inference draft (POST /:slug/save-as-template): edit metadata and inferred ref-slot labels (en/de LocalizedText, en required), declare text slots with the paste-able slot:text:<key> token, and publish. The primary action is owner-aware: fresh id posts a new template, an id owned by the viewer switches to 'Publish as v{latestVersion+1}' via PUT, a foreign/bundled id shows an inline error, and a 409 race on POST re-checks ownership and flips the dialog into the PUT state instead of dead-ending. This makes publisher maintenance (v>1) reachable from the UI, not just the raw API.
… actions (#478) TemplateGallery renders the composite catalog with provenance facets (All / Bundled / My templates / Shared / Plugins / Pending review), client-side text search over the locale-resolved name/description/useCase, and secondary use-case chips. The Pending review facet is the reviewer queue: every pending user template is listed for EVERY operator (the install-wide pending-visibility rule makes the review gate reachable by non-author reviewers), with the submitter shown and Approve/Reject actions directly on the card, plus a waiting-count badge on the facet label. Cards gain a provenance/status badge (text + edge color only, per Lume), a v{n} tag, an instantiation count, and author manage actions on own user templates: Submit for review (private only) and Delete behind an inline confirm. Mutations refetch the catalog via the page (onCatalogChanged -> reload); errors surface inline as text with the server's message. API client gains deleteConductorTemplate, submitConductorTemplate, approveConductorTemplate, rejectConductorTemplate. Vitest covers facet filtering (viewer-owned only under My templates), the non-author pending queue with a working Approve (and the card surfacing under Shared after refresh), locale-fallback search, author-only manage actions, the delete confirm, and absence of review actions on non-pending cards.
…n pin, update hint (#478) Text-slot inputs (required-fill unless a default exists; kind:'text' server errors mapped inline; mapping carries the additive text record), a collapsed- by-default read-only graph preview rendered from the manifest (TemplatePreview on @xyflow/react — the designer's actual stack, not the plan's Cytoscape), version display + explicit-version resolve/instantiate, an initialMapping prefill seam for the chat proposals (F4), and the opt-in update path: workflows carrying template.updateAvailable render 'Template updated (v{n} -> v{m})' with a re-instantiate action opening the form pinned to the latest version (TemplateUpdateHint; copy-not-reference — the old workflow stays untouched). API client: mapping.text + version on resolve/instantiate, fetchConductorTemplateVersions, template hint on the workflow wire type.
Render the builder turn's templateProposals (B4) as up to 3 compact cards
under the assistant reply: locale-resolved template name, v{n} tag, one-line
reason, and a slot-coverage line counted against declared slots only. The
single 'Use template' action hands off to the instantiate form through the
page's existing state plumbing (prefill analog of the chat->canvas
setChatGraphRequest hand-off): the form opens pinned to the proposed version
with the proposal's prefill as initialMapping, re-keyed per hand-off. Chat
never auto-instantiates. Unknown template ids degrade to plain text; turns
without proposals render exactly as before (API client type is additive).
i18n en+de; 5 vitest cases (cards, hand-off payload, no-proposal regression,
>3 cap, degrade).
Review fixups for the templates-v2 branch: - Lume gate: the step-kind palette (agent/action/human + badge text) was hardcoded hex twice (ConductorCanvas, TemplatePreview). It now lives as --step-kind-* tokens in theme.css, consumed by both components through the shared stepKindColors.ts map — no hex in component code, no keep-in-sync comment. - 500-line rule: SaveAsTemplateDialog (527) extracted its slot editor sections into SaveAsTemplateSlotEditors.tsx (388/197); conductor/page.tsx (604) extracted the Roles and emit-event sections into ConductorRolesSection.tsx and ConductorEmitSection.tsx (464). Behavior-preserving: role/emit errors and refetches still route through the page, and the double-fire guard is shared via a guard callback (prop-object mutation trips react-hooks/immutability).
Codex review round 2 on the templates-v2 branch found three blockers:
- POST /conductors/templates with {} (or any manifest whose slots field /
kind lists / entries have the wrong shape) crashed checkTemplateManifest
and surfaced as a 500. The gate now shape-checks slots before walking and
reports malformed input as ordinary validation errors, so the route
answers 400 conductor.template_invalid. The localized-text helpers moved
to localizedText.ts to keep template.ts within the 500-line budget; the
@omadia/conductor-core export surface is unchanged.
- Save-as-template text slots were undeliverable: the dialog declared them
but never put a slot:text:<key> token into the graph, so the backend
rejected every such manifest as template_text_slot_unused. The dialog now
edits the graph's designated step texts (step.prompt, human.message) with
per-field token insert buttons and blocks publish until every declared
slot's token is placed; the edited texts travel in the manifest graph.
- Committed trailing whitespace in the conductor template test files broke
git diff --check hygiene; stripped.
#478) An owned template id no longer dead-ends on 'id taken' when the dialog opens before getAuthMe settles. The page now tracks viewerPending, and the dialog derives a fourth publishMode 'pending' from live props: a user-sourced collision holds a gated 'Checking ownership' state (Lume busy-dots, submit disabled) and flips to 'Publish as v{n+1}' or the id-taken error once the viewer is known. Bundled/plugin collisions stay terminal, and the 409-race re-check is now pending-aware too.
Weegy
added a commit
that referenced
this pull request
Jul 10, 2026
…pping, guided instantiation (#429) (#479) * feat(conductor-core): workflow-template contract with pure slot substitution (#429) TemplateManifest/TemplateSlots/TemplateSlotMapping types plus pure, I/O-free helpers: extractSlotRefs (structural walk of the five ref fields), missingSlotMappings, applyTemplateSlots (deep-clone, field-targeted slot:<kind>:<key> substitution, {{ctx.*}} interpolation untouched) and checkTemplateManifest (metadata, structural validate, bidirectional slot coverage, duplicate keys, malformed refs). Foundation for the file-based template catalog and /templates routes. * feat(conductor): bundled workflow-template catalog with loader and build plumbing (#429) Four curated TemplateManifest JSONs (expense-approval, notify-and-escalate, weekly-report, onboarding-checklist) in middleware/src/conductor/templates/, a dirname-relative loadTemplateCatalog() that skips invalid assets with a log line instead of failing boot, a CI gate test proving every bundled template is valid and instantiable end-to-end (synthetic mapping + validate() with KnownRefs + cron validity), and build plumbing so both plain npm run build (copy-build-assets.mjs) and the Docker image carry the JSON assets. File-based catalog, no DB migration. * feat(conductor): workflow-template routes — list, resolve, instantiate (#429) Expose the bundled template catalog on the operator conductor API and add the two instantiation paths, both validating with live KnownRefs (agent slugs, action ids, role keys, event catalog) — deliberately stricter than POST / so a template instance is runnable, not merely well-formed. Instantiate publishes through the ordinary createOrPublish path with the atomic cron-schedule reconcile and 409s on slug collision instead of upserting. Resolve is the ephemeral seam for #330 and the UI's open-in- designer flow. Routes registered before the /:slug catch-all; stub-dep route tests cover every status code, the createOrPublish call shape, and a route-order regression. API documented in the middleware handoff doc. * feat(web-ui): workflow-template gallery on /conductor (#429) Card grid above the workflows list rendering the bundled template catalog from GET /templates: name, useCase tag, description, pluralized slot-mapping summary and a text/edge schedule badge for cron templates. API client mirrors the TemplateManifest wire shape locally and ships the resolve/instantiate fetchers for the follow-up mapping-form unit. i18n en+de; Vitest component tests. * feat(web-ui): template slot-mapping form on /conductor (#429) Completes the guided instantiation flow the f1 gallery opened: one upfront mapping form per template instead of per-node walking, because declared slots make a single form sufficient and the server's completeness check already names every gap. Create publishes via POST /templates/:id/ instantiate; Open in designer resolves ephemerally and reuses the existing chat-to-canvas hydration so the canvas needs no changes. The enable toggle defaults to off with a persistent schedule notice for cron templates — schedule transparency is a must-have, never hover-hidden. Channel slots prefill 'teams' so the mapping state never diverges from what the shared ChannelSelect displays. * fix(conductor): template designer handoff, no Dockerfile COPY (#429) Review fixups: - The Dockerfile COPY for the template JSON assets was redundant (the builder stage's npm run build already mirrors them into dist via copy-build-assets.mjs, and the runtime stage copies dist wholesale) and the review gate forbids touching deployment config — reverted to base. - "Open in designer" now hands the form's instance slug/name to the canvas alongside the resolved graph (CanvasGraphRequest gains optional slug/name), so Save is armed on a fresh page and never republishes the template graph over a previously loaded workflow's slug. Chat-pane draft pushes omit the fields and leave the canvas identity untouched. * fix(web-ui): carry the template form's enable choice into the designer save path (#429) The 'Open in designer' handoff passed only graph/slug/name, and ConductorCanvas.handleSave hardcoded publishConductorWorkflow enable: true — so a cron template left on the default-off toggle was created enabled on Save and its schedule started without the form's schedule notice applying. onOpenInDesigner and CanvasGraphRequest now carry the form's enable flag; the canvas publishes with it and resets to the historical enabled-on-save behaviour for chat drafts and loaded workflows (the store only applies enable on first create anyway). Regression tests: new ConductorCanvas.test.tsx (stubbed @xyflow/react) plus extended form handoff tests. * docs: restore #453 SkillSpector changelog heading dropped by #429 unit b1 edit * fix(conductor): localize template metadata in the manifest, ship German for bundled templates (#429) Template name, description, useCase, and slot labels/help texts rendered raw English in the German UI. Templates are data (v2 distributes them outside the repo), so localization travels in the manifest: these fields now accept a plain string or an { en, de?, ... } record with en required as the universal fallback. - conductor-core: LocalizedText/LocalizedTextMap types, resolveLocalizedText, checkTemplateManifest validates the localized shape (new template_invalid_localized_text code); missing-slot envelopes keep flat English labels (clients localize by kind+key from the manifest they hold) - bundled manifests: proper German translations for all operator-facing texts; the catalog CI gate now asserts bundled en/de parity - routes: GET /templates keeps returning full, unresolved manifests (#330); the instantiate route resolves its manifest-borne name/description fallbacks to the en base before persisting - web-ui: gallery + slot-mapping form resolve the active locale via next-intl useLocale() with en fallback (resolveConductorText); component tests cover German rendering under de and the en fallback * fix(conductor): make template instantiation's slug 409 atomic via create-only publish (#429) Two concurrent POST /templates/:id/instantiate with the same not-yet-existing slug both passed the getBySlug pre-check; the loser fell into createOrPublish's ON CONFLICT DO UPDATE upsert and silently published a second version over the just-created workflow, answering 201 — violating the route's create-new contract. - ConductorWorkflowStore.createOrPublish gains expectNew: the conflict clause flips to INSERT ... ON CONFLICT (slug) DO NOTHING; zero returned rows aborts the publish transaction with the new WorkflowSlugExistsError - the instantiate route drops the racy pre-check entirely and maps the error to the existing 409 conductor.slug_exists envelope — of two racing instantiates exactly one INSERT wins - POST / and the canvas save path keep their idempotent upsert untouched (regression-tested: no expectNew leaks into the ordinary publish) - store-level tests (fake pool, SQL-shape scripted) prove the create-only conflict semantics incl. rollback; route tests prove the 409 mapping * feat(conductor): workflow template platform v2 (#478) (#484) * feat(conductor-core): template manifest v2 — version, text slots, inference, strict gate (#478) Additive extension of the #429 template contract, pure functions only: TemplateManifest.version (absent = 1, read via templateManifestVersion), declared text slots substituted as slot:text:<key> tokens in the two designated prose fields (step.prompt, human.message; disjoint from {{...}} run-context interpolation, structural walk, never string-replace over JSON), TemplateSlotMapping.text, kind:'text' missing-slot entries, checkTemplateManifest text-slot coverage codes plus a { strict } mode that rejects concrete refs in distributed (plugin/hub) manifests, and inferTemplateManifest reversing the extractSlotRefs walk for the save-as-template path (slugified de-duplicated keys, pre-existing placeholders preserved, round-trip tested). Text machinery split into textSlots.ts and v2 tests into templateV2.test.ts to keep every file under the 500-line budget; the v1 test file stays byte-identical as the contract pin for #330 consumers. * feat(conductor): DB template store, composite catalog, CRUD + versioning routes (#478) User-authored workflow templates move into the conductor DB: migration 0006_templates.sql (conductor chain) adds conductor_templates, conductor_template_versions (immutable JSONB manifests), conductor_template_instantiations (append-only anonymous telemetry) and template provenance columns on conductor_workflows. A composite catalog merges bundled files, DB user templates and a plugin registration seam behind one viewer-scoped interface whose visibility rule makes 'pending' templates reachable by every operator — every operator on the single-tier operator API is a potential reviewer, so the review gate cannot collapse into author-only self-approval. New CRUD + versioning routes (split into templateRoutes.ts for file size) keep the GET /templates wire shape additive over v1 so the #330 consumer contract holds; resolve/instantiate accept an explicit version and instantiate stamps provenance in the same transaction as the publish. Conductor migrations are now also mirrored into dist by copy-build-assets.mjs, which previously relied on the Dockerfile COPY alone. * feat(conductor): save-as-template inference, review gate, plugin-borne templates, update hint (#478) Unit B3 of the templates-v2 plan: - POST /:slug/save-as-template returns an inferTemplateManifest DRAFT (one declared slot per distinct concrete ref) from the workflow's active published version; nothing persisted, UI publishes via POST/PUT /templates. Default id derives from the slug with a -template suffix on collision. - Review state machine private -> pending -> shared: submit (author-only, 409 template_status_conflict otherwise), approve/reject (ANY operator via the viewer-scoped catalog get - reachable because pending is visible install-wide; reviewed_by recorded; self-approval permitted and auditable). - Workflow list/detail additively report template?: { id, version, latestVersion, updateAvailable } from the template_id/ template_version provenance columns (viewer-scoped, no existence leak); workflowStore now selects those columns. - Plugin-borne templates (trust boundary in docs/security-architecture section 4): permissions.templates declares package-relative JSON manifests; fail-closed install gate in src/plugins/pluginTemplates.ts (path confinement after symlink unwrapping, plugin:<id>:<name> namespacing, checkTemplateManifest strict mode, isValidCron); any violation fails the install with install.template_invalid. Accepted manifests register as read-only source 'plugin' catalog entries, unregister on uninstall, and re-register at boot (fail-open sweep). Templates are data, never code - no runtime template API. Tests: test/pluginTemplates.test.ts (new) + extended test/conductorTemplateRoutes.test.ts (48 route cases incl. non-author approve, inference round-trip, update hint, plugin source read-only). * feat(conductor): builder-chat template awareness (#478) The conversational builder can now see the workflow-template catalog and suggest templates in chat: its system prompt carries a viewer-scoped catalog digest (capped at 30 entries) and the reply protocol accepts a templateProposals block that POST /builder/turn returns additively (the key is absent without proposals, keeping the v1 wire shape byte-identical). Proposals are vetted server-side inside the agent seam so the LLM cannot surface templates the viewer must not see: unknown/invisible ids dropped against the composite catalog, max 3 deduped proposals, version taken from the catalog rather than the model, and prefill guesses kept only for declared slot keys whose ref values resolve against the live KnownRefs sets (shared with the resolve/instantiate routes via a hoisted templateKnownRefs so the two gates cannot drift). Chat proposes and prefills only - instantiation stays on the deliberate form flow. * feat(web-ui): save-as-template dialog with owner-aware version publish (#478) Authoring UX in the Conductor admin UI over the backend's slot-inference draft (POST /:slug/save-as-template): edit metadata and inferred ref-slot labels (en/de LocalizedText, en required), declare text slots with the paste-able slot:text:<key> token, and publish. The primary action is owner-aware: fresh id posts a new template, an id owned by the viewer switches to 'Publish as v{latestVersion+1}' via PUT, a foreign/bundled id shows an inline error, and a 409 race on POST re-checks ownership and flips the dialog into the PUT state instead of dead-ending. This makes publisher maintenance (v>1) reachable from the UI, not just the raw API. * feat(web-ui): gallery v2 facets, pending-review queue, search, manage actions (#478) TemplateGallery renders the composite catalog with provenance facets (All / Bundled / My templates / Shared / Plugins / Pending review), client-side text search over the locale-resolved name/description/useCase, and secondary use-case chips. The Pending review facet is the reviewer queue: every pending user template is listed for EVERY operator (the install-wide pending-visibility rule makes the review gate reachable by non-author reviewers), with the submitter shown and Approve/Reject actions directly on the card, plus a waiting-count badge on the facet label. Cards gain a provenance/status badge (text + edge color only, per Lume), a v{n} tag, an instantiation count, and author manage actions on own user templates: Submit for review (private only) and Delete behind an inline confirm. Mutations refetch the catalog via the page (onCatalogChanged -> reload); errors surface inline as text with the server's message. API client gains deleteConductorTemplate, submitConductorTemplate, approveConductorTemplate, rejectConductorTemplate. Vitest covers facet filtering (viewer-owned only under My templates), the non-author pending queue with a working Approve (and the card surfacing under Shared after refresh), locale-fallback search, author-only manage actions, the delete confirm, and absence of review actions on non-pending cards. * feat(web-ui): instantiate form v2 — text slots, graph preview, version pin, update hint (#478) Text-slot inputs (required-fill unless a default exists; kind:'text' server errors mapped inline; mapping carries the additive text record), a collapsed- by-default read-only graph preview rendered from the manifest (TemplatePreview on @xyflow/react — the designer's actual stack, not the plan's Cytoscape), version display + explicit-version resolve/instantiate, an initialMapping prefill seam for the chat proposals (F4), and the opt-in update path: workflows carrying template.updateAvailable render 'Template updated (v{n} -> v{m})' with a re-instantiate action opening the form pinned to the latest version (TemplateUpdateHint; copy-not-reference — the old workflow stays untouched). API client: mapping.text + version on resolve/instantiate, fetchConductorTemplateVersions, template hint on the workflow wire type. * feat(web-ui): chat template proposal cards in the builder pane (#478) Render the builder turn's templateProposals (B4) as up to 3 compact cards under the assistant reply: locale-resolved template name, v{n} tag, one-line reason, and a slot-coverage line counted against declared slots only. The single 'Use template' action hands off to the instantiate form through the page's existing state plumbing (prefill analog of the chat->canvas setChatGraphRequest hand-off): the form opens pinned to the proposed version with the proposal's prefill as initialMapping, re-keyed per hand-off. Chat never auto-instantiates. Unknown template ids degrade to plain text; turns without proposals render exactly as before (API client type is additive). i18n en+de; 5 vitest cases (cards, hand-off payload, no-proposal regression, >3 cap, degrade). * refactor(web-ui): step-kind Lume tokens + 500-line splits (#478) Review fixups for the templates-v2 branch: - Lume gate: the step-kind palette (agent/action/human + badge text) was hardcoded hex twice (ConductorCanvas, TemplatePreview). It now lives as --step-kind-* tokens in theme.css, consumed by both components through the shared stepKindColors.ts map — no hex in component code, no keep-in-sync comment. - 500-line rule: SaveAsTemplateDialog (527) extracted its slot editor sections into SaveAsTemplateSlotEditors.tsx (388/197); conductor/page.tsx (604) extracted the Roles and emit-event sections into ConductorRolesSection.tsx and ConductorEmitSection.tsx (464). Behavior-preserving: role/emit errors and refetches still route through the page, and the double-fire guard is shared via a guard callback (prop-object mutation trips react-hooks/immutability). * fix(conductor): harden manifest gate, place text-slot tokens (#478) Codex review round 2 on the templates-v2 branch found three blockers: - POST /conductors/templates with {} (or any manifest whose slots field / kind lists / entries have the wrong shape) crashed checkTemplateManifest and surfaced as a 500. The gate now shape-checks slots before walking and reports malformed input as ordinary validation errors, so the route answers 400 conductor.template_invalid. The localized-text helpers moved to localizedText.ts to keep template.ts within the 500-line budget; the @omadia/conductor-core export surface is unchanged. - Save-as-template text slots were undeliverable: the dialog declared them but never put a slot:text:<key> token into the graph, so the backend rejected every such manifest as template_text_slot_unused. The dialog now edits the graph's designated step texts (step.prompt, human.message) with per-field token insert buttons and blocks publish until every declared slot's token is placed; the edited texts travel in the manifest graph. - Committed trailing whitespace in the conductor template test files broke git diff --check hygiene; stripped. * fix(web-ui): gate save-as-template ownership check while auth resolves (#478) An owned template id no longer dead-ends on 'id taken' when the dialog opens before getAuthMe settles. The page now tracks viewerPending, and the dialog derives a fourth publishMode 'pending' from live props: a user-sourced collision holds a gated 'Checking ownership' state (Lume busy-dots, submit disabled) and flips to 'Publish as v{n+1}' or the id-taken error once the viewer is known. Bundled/plugin collisions stay terminal, and the 409-race re-check is now pending-aware too.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Workflow template platform v2 — authoring, distribution, persistence, discovery, chat integration, telemetry, text slots, and versioning. Stacks on the #429 groundwork (
feat/issue-429-workflow-templates, PR #479); adapts the researched platform patterns (n8n, Make, Zapier, Power Automate) to our contract instead of cloning them: one canonicalTemplateManifestfor bundled JSON, DB rows, and plugin-borne templates, all through the samecheckTemplateManifest+validate()pipeline,GET /templatesextended additively only (#330).Core (
@omadia/conductor-core)version(absent = 1, read viatemplateManifestVersion), localizable metadata/labels (LocalizedText,resolveLocalizedText).slot:text:<key>tokens inside the two designated text fields (step.prompt,human.message) — structural walk, never string-replace over JSON; explicitly disjoint from{{...}}run-context interpolation. Declared inslots.textwith optionaldefault.inferTemplateManifest): reverses theextractSlotRefswalk over a concrete graph — each distinct concrete ref in the five ref fields becomes a declared slot with the ref as proposed label; pre-existing placeholders pass through.{ strict: true }): a distributed (plugin/hub) manifest with any remaining concrete ref is rejected — undeclared install-local refs are confusion/exfiltration vectors. Bundled and user-authored templates stay non-strict.checkTemplateManifestnever throws on arbitrary JSON — a manifest with missing/mis-shapedslots, non-array kind lists, or keyless entries yields ordinary validation errors.Middleware
0006_templates.sql(conductor chain):conductor_templates,conductor_template_versions(immutable JSONB manifests),conductor_template_instantiations(append-only, anonymous — no per-user tracking), plustemplate_id/template_versionprovenance columns onconductor_workflows. No CHECK constraints on growable enumerations.status='shared'ORcreatedBy = viewerORstatus='pending'— pending templates are visible to every operator so the review gate is reachable by non-author reviewers.POST /templates(createsprivate, owned by viewer),PUT /templates/:id(author-only next-version publish),DELETE,GET /templates/:id/versions. Copy-not-reference stays the contract: instances record{templateId, version}at creation; template updates surface as an opt-in hint, never silent propagation.private → pending → sharedviasubmit/approve/reject; approve/reject by any operator,reviewed_byrecorded; self-approval permitted so single-operator installs cannot deadlock (separation of duties deferred).plugin), removed on uninstall. Data-only artifacts — no parallel serving protocol, no registration API.instantiationCount.templateProposals(template id, version, reason, slot prefill).Web UI (Lume-conformant, en+de)
--step-kind-*tokens; oversized components split per the 500-line rule.Review round 2 (codex) — addressed
POST /conductors/templateswith{}/{"manifest":{"id":"x"}}500'd → the manifest gate is now shape-defensive and the route answers 400conductor.template_invalid(core + route regression tests).template_text_slot_unused) → the dialog now provides the token-placement UI described above, with vitest coverage for placement, blocking, and the submitted graph.conductorTemplateRoutes.test.ts/conductorTemplateStore.test.tsstripped;git diff --checkis clean over the full range.Verification
middleware:npm run typecheckclean;npm test— 4129 pass, 0 fail (4 skipped);@omadia/conductor-corevitest — 96 pass.web-ui:npm run typecheckclean;npm run test— 272 pass (40 files);npm run i18n:check— OK, en/de parity (2840 keys).git diff --check 8f25c15...feat/issue-478-templates-v2clean.Review round 3 (codex) — addressed
The cross-vendor reviewer's final finding:
SaveAsTemplateDialogconflated "getAuthMe still pending" with "resolved anonymous", so an owner opening the dialog before auth resolved dead-ended an owned template id intakeninstead of being offered "Publish as v{n+1}". Fixed inc56e837:viewerPendingis threaded from the page,publishModegains a live-derivedpendingstate (submit gated, Lume busy-dots "Checking ownership" note, no spinner), flipping toversion/takenthe moment the viewer resolves; the 409-race re-check is pending-aware; resolved-anonymous stays terminal. Covered by the reviewer's exact scenario as a vitest case. Both reviewers (same-family adversarial + codex) re-approved the branch afterwards.Closes #478