Skip to content

feat(conductor): workflow template platform v2 (#478)#484

Merged
Weegy merged 11 commits into
feat/issue-429-workflow-templatesfrom
feat/issue-478-templates-v2
Jul 10, 2026
Merged

feat(conductor): workflow template platform v2 (#478)#484
Weegy merged 11 commits into
feat/issue-429-workflow-templatesfrom
feat/issue-478-templates-v2

Conversation

@Weegy

@Weegy Weegy commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

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 canonical TemplateManifest for bundled JSON, DB rows, and plugin-borne templates, all through the same checkTemplateManifest + validate() pipeline, GET /templates extended additively only (#330).

Core (@omadia/conductor-core)

  • Manifest v2: integer version (absent = 1, read via templateManifestVersion), localizable metadata/labels (LocalizedText, resolveLocalizedText).
  • Text slots: 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 in slots.text with optional default.
  • Slot inference (inferTemplateManifest): reverses the extractSlotRefs walk 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 import gate ({ 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.
  • Malformed-input hardening: checkTemplateManifest never throws on arbitrary JSON — a manifest with missing/mis-shaped slots, non-array kind lists, or keyless entries yields ordinary validation errors.

Middleware

  • Migration 0006_templates.sql (conductor chain): conductor_templates, conductor_template_versions (immutable JSONB manifests), conductor_template_instantiations (append-only, anonymous — no per-user tracking), plus template_id/template_version provenance columns on conductor_workflows. No CHECK constraints on growable enumerations.
  • Composite catalog (bundled + DB + plugin sources) with viewer-scoped visibility: a user template is visible when status='shared' OR createdBy = viewer OR status='pending' — pending templates are visible to every operator so the review gate is reachable by non-author reviewers.
  • CRUD + versioning routes: POST /templates (creates private, 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.
  • Review gate (Make-style team model): private → pending → shared via submit/approve/reject; approve/reject by any operator, reviewed_by recorded; self-approval permitted so single-operator installs cannot deadlock (separation of duties deferred).
  • Plugin-borne templates: plugins declare manifests in their package; install-time validation (strict gate) registers them as read-only catalog entries (source plugin), removed on uninstall. Data-only artifacts — no parallel serving protocol, no registration API.
  • Telemetry: anonymous per-template instantiation counter (modeled on the MCP call log), surfaced as instantiationCount.
  • Builder-chat awareness: the builder agent sees the catalog and returns additive templateProposals (template id, version, reason, slot prefill).

Web UI (Lume-conformant, en+de)

  • Save-as-template dialog: edits the inference draft (metadata, slot labels en/de), declares text slots, and places their tokens via a "Place text-slot tokens" section that edits the designated step texts with per-field insert buttons — publish is blocked until every declared token is placed. Owner-aware primary action: an id owned by the viewer switches to "Publish as v{n+1}" (PUT); a 409 race re-checks ownership and flips into version mode instead of dead-ending.
  • Gallery v2: source facets (all/bundled/mine/shared/pending review), search, manage actions (submit/approve/reject/delete), version + instantiation-count metadata.
  • Instantiate form v2: text-slot inputs with defaults, read-only graph preview (React Flow, manifest placeholders shown as declared labels), version pinning, and a "Template updated (v{n} → v{m})" hint with deliberate re-instantiation.
  • Chat proposal cards: up to 3 template proposals under the assistant reply with one "Use template" action handing off to the instantiate form (prefilled, version-pinned) — chat never auto-instantiates.
  • Step-kind colors moved from hardcoded hex to Lume --step-kind-* tokens; oversized components split per the 500-line rule.

Review round 2 (codex) — addressed

  1. POST /conductors/templates with {} / {"manifest":{"id":"x"}} 500'd → the manifest gate is now shape-defensive and the route answers 400 conductor.template_invalid (core + route regression tests).
  2. Text-slot authoring was undeliverable (declared slots without a placed token were rejected server-side as template_text_slot_unused) → the dialog now provides the token-placement UI described above, with vitest coverage for placement, blocking, and the submitted graph.
  3. Trailing whitespace in conductorTemplateRoutes.test.ts / conductorTemplateStore.test.ts stripped; git diff --check is clean over the full range.

Verification

  • middleware: npm run typecheck clean; npm test — 4129 pass, 0 fail (4 skipped); @omadia/conductor-core vitest — 96 pass.
  • web-ui: npm run typecheck clean; 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-v2 clean.

Review round 3 (codex) — addressed

The cross-vendor reviewer's final finding: SaveAsTemplateDialog conflated "getAuthMe still pending" with "resolved anonymous", so an owner opening the dialog before auth resolved dead-ended an owned template id in taken instead of being offered "Publish as v{n+1}". Fixed in c56e837: viewerPending is threaded from the page, publishMode gains a live-derived pending state (submit gated, Lume busy-dots "Checking ownership" note, no spinner), flipping to version/taken the 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.

Stacked PR: base is feat/issue-429-workflow-templates (PR #479). Merge #479 first; this PR then retargets/merges cleanly onto main.

Closes #478

Weegy added 11 commits July 10, 2026 11:58
…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 Weegy merged commit 18cddae into feat/issue-429-workflow-templates Jul 10, 2026
5 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant