@worca/app 1.0.0 → 1.2.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +30 -9
  2. package/agents/clarify.meta.json +4 -4
  3. package/agents/decomposer.meta.json +5 -5
  4. package/agents/implementer.meta.json +15 -5
  5. package/agents/manualTestsChecklist.meta.json +5 -4
  6. package/agents/manualWebUiTesting.meta.json +9 -4
  7. package/agents/planReviewer.meta.json +12 -4
  8. package/agents/planner.meta.json +12 -5
  9. package/agents/refiner.meta.json +15 -4
  10. package/agents/reviewer.meta.json +14 -4
  11. package/agents/worca-cc-clarify.md +7 -0
  12. package/agents/worca-cc-code-reviewer.md +11 -6
  13. package/agents/worca-cc-decomposer.md +7 -0
  14. package/agents/worca-cc-implementer.md +9 -0
  15. package/agents/worca-cc-manual-tests-checklist.md +8 -5
  16. package/agents/worca-cc-manual-web-ui-testing.md +10 -6
  17. package/agents/worca-cc-plan-refiner.md +11 -6
  18. package/agents/worca-cc-plan-reviewer.md +10 -7
  19. package/agents/worca-cc-planner.md +9 -0
  20. package/agents/worca-cc-workspace-reviewer.md +11 -4
  21. package/agents/worca-cc-workspace-scanner.md +8 -4
  22. package/agents/workspaceReviewer.meta.json +15 -4
  23. package/agents/workspaceScanner.meta.json +5 -4
  24. package/package.json +8 -2
  25. package/skills/worca/SKILL.md +5 -5
  26. package/src/cli/render.mjs +148 -0
  27. package/src/cli/worca-cc.mjs +386 -56
  28. package/src/core/agent-gen.mjs +69 -31
  29. package/src/core/agent-registry.mjs +124 -144
  30. package/src/core/agent-store.mjs +164 -4
  31. package/src/core/artifacts.mjs +199 -23
  32. package/src/core/ask/attachment-kind.mjs +95 -0
  33. package/src/core/ask/catalog.mjs +111 -0
  34. package/src/core/ask/comment-deps.mjs +55 -0
  35. package/src/core/ask/events.mjs +545 -0
  36. package/src/core/ask/follow.mjs +113 -0
  37. package/src/core/ask/git-allowlist.mjs +226 -0
  38. package/src/core/ask/limits.mjs +57 -0
  39. package/src/core/ask/mcp-stdio.mjs +135 -0
  40. package/src/core/ask/models.mjs +125 -0
  41. package/src/core/ask/prompt.mjs +286 -0
  42. package/src/core/ask/proposal.mjs +170 -0
  43. package/src/core/ask/redact.mjs +30 -0
  44. package/src/core/ask/spawn.mjs +156 -0
  45. package/src/core/ask/store.mjs +438 -0
  46. package/src/core/ask/tool-deps.mjs +87 -0
  47. package/src/core/ask/tools.mjs +879 -0
  48. package/src/core/ask/turn.mjs +462 -0
  49. package/src/core/ask/worktree-deps.mjs +27 -0
  50. package/src/core/ask/worktrees.mjs +285 -0
  51. package/src/core/chat/command-router.mjs +28 -7
  52. package/src/core/chat/notifier.mjs +6 -1
  53. package/src/core/chat/renderers.mjs +15 -8
  54. package/src/core/claude-runner.mjs +541 -62
  55. package/src/core/config.mjs +310 -44
  56. package/src/core/cost-budget.mjs +29 -2
  57. package/src/core/db.mjs +773 -53
  58. package/src/core/diff-anchor.mjs +213 -0
  59. package/src/core/diff-comments.mjs +273 -0
  60. package/src/core/engine-select.mjs +32 -0
  61. package/src/core/failure-policy.mjs +201 -0
  62. package/src/core/git-info.mjs +49 -10
  63. package/src/core/graph/builtin-workflows.mjs +51 -0
  64. package/src/core/graph/executor.mjs +894 -0
  65. package/src/core/graph/registry-ports.mjs +12 -0
  66. package/src/core/graph/scheduler.mjs +1072 -0
  67. package/src/core/graph/seed-templates.mjs +318 -0
  68. package/src/core/host-guard.mjs +271 -0
  69. package/src/core/model-env.mjs +180 -8
  70. package/src/core/model-test.mjs +79 -0
  71. package/src/core/orchestrator.mjs +994 -4097
  72. package/src/core/overview-agent.mjs +15 -3
  73. package/src/core/phases.mjs +208 -537
  74. package/src/core/pipeline-delete.mjs +13 -2
  75. package/src/core/plugin-api.mjs +8 -3
  76. package/src/core/plugin-config.mjs +178 -28
  77. package/src/core/plugin-inventory.mjs +6 -2
  78. package/src/core/plugin-manifest.mjs +199 -11
  79. package/src/core/plugin-models.mjs +1 -0
  80. package/src/core/plugin-repo.mjs +16 -4
  81. package/src/core/plugin-shim-child.mjs +9 -3
  82. package/src/core/plugin-shim.mjs +80 -17
  83. package/src/core/plugin-store.mjs +236 -29
  84. package/src/core/plugin-workflows.mjs +90 -41
  85. package/src/core/preflight.mjs +135 -3
  86. package/src/core/projects.mjs +7 -5
  87. package/src/core/protocol.mjs +8 -35
  88. package/src/core/recoverable-error.mjs +1 -1
  89. package/src/core/run-harness.mjs +3934 -0
  90. package/src/core/run-manifest.mjs +5 -1
  91. package/src/core/settings.mjs +184 -13
  92. package/src/core/skills.mjs +10 -3
  93. package/src/core/source-bindings.mjs +175 -0
  94. package/src/core/sources.mjs +87 -25
  95. package/src/core/stats.mjs +25 -6
  96. package/src/core/title.mjs +51 -4
  97. package/src/core/workflows.mjs +358 -259
  98. package/src/core/workspace-scan.mjs +4 -0
  99. package/src/core/worktree.mjs +98 -7
  100. package/src/shared/graph/agent-meta.mjs +278 -0
  101. package/src/shared/graph/constants.mjs +105 -0
  102. package/src/shared/graph/geometry.mjs +157 -0
  103. package/src/shared/graph/layout.mjs +134 -0
  104. package/src/shared/graph/loops.mjs +130 -0
  105. package/src/shared/graph/manifest.mjs +257 -0
  106. package/src/shared/graph/ports.mjs +153 -0
  107. package/src/shared/graph/route.mjs +397 -0
  108. package/src/shared/graph/template.mjs +165 -0
  109. package/src/shared/graph/thumbnail.mjs +67 -0
  110. package/src/shared/graph/validate.mjs +491 -0
  111. package/src/shared/graph/verdict.mjs +41 -0
  112. package/ui/public/app.js +4240 -1682
  113. package/ui/public/ask-markdown.mjs +145 -0
  114. package/ui/public/ask-model.mjs +317 -0
  115. package/ui/public/ask-panel.mjs +2129 -0
  116. package/ui/public/chat-settings-view.mjs +6 -2
  117. package/ui/public/diff-view.mjs +66 -11
  118. package/ui/public/file-tree.mjs +305 -0
  119. package/ui/public/graph/composer.mjs +889 -0
  120. package/ui/public/graph/inspector.mjs +183 -0
  121. package/ui/public/graph/model.mjs +37 -0
  122. package/ui/public/graph/palette.mjs +144 -0
  123. package/ui/public/graph/run-decor.mjs +410 -0
  124. package/ui/public/graph/run-hosts.mjs +201 -0
  125. package/ui/public/graph/save-dialog.mjs +56 -0
  126. package/ui/public/graph/view.mjs +858 -0
  127. package/ui/public/guardrails-view.mjs +4 -2
  128. package/ui/public/hljs-loader.mjs +180 -0
  129. package/ui/public/index.html +311 -265
  130. package/ui/public/log-filter.mjs +22 -4
  131. package/ui/public/log-line.mjs +45 -19
  132. package/ui/public/models-view.mjs +171 -9
  133. package/ui/public/plugins-view.mjs +106 -4
  134. package/ui/public/source-pane.mjs +190 -8
  135. package/ui/public/stats-view.mjs +81 -1
  136. package/ui/public/style.css +1487 -229
  137. package/ui/public/syntax-highlight.mjs +270 -0
  138. package/ui/public/thinking-orb.mjs +110 -0
  139. package/ui/server.mjs +1894 -104
  140. package/src/core/channels.mjs +0 -302
  141. package/src/core/runners.mjs +0 -167
  142. package/src/core/workflow-validator.mjs +0 -185
  143. package/ui/public/composer-core.mjs +0 -211
@@ -1,302 +0,0 @@
1
- // src/core/channels.mjs
2
- // Typed channel vocabulary + the dispatch-time binder. Pure: the only IO is the
3
- // path STRINGS it computes via artifacts.mjs (no reads/writes). Replaces the
4
- // role-coupled _nodeIo/_afterNode switch with data-driven channel wiring.
5
-
6
- import { join } from 'node:path';
7
- import { planPath, reviewPath } from './artifacts.mjs';
8
-
9
- /** The closed set of channel ids (single source; imported by registry + validator).
10
- * `workspace` (M3) is a read-only metadata channel carrying the frozen workspace
11
- * description + member set; it is seeded once and never re-published (CONV-6). */
12
- export const CHANNEL_IDS = ['userPrompt', 'plan', 'review', 'checklist', 'code', 'workspace', 'clarify', 'decomposition'];
13
-
14
- /** Channels the dispatcher pre-seeds WITH VALUES on the bus at run start
15
- * (orchestrator.mjs _dispatch bus literal): userPrompt IS the prompt,
16
- * plan/checklist get default destinations, code is the standing worktree.
17
- * The bus literal also carries workspace (metadata or null) and clarify/
18
- * decomposition seeded as null — those are deliberately NOT listed here so the
19
- * validator keeps warning when they are consumed with no upstream producer
20
- * (current behavior). Single source for the validator's reachability pass —
21
- * keep in sync with the _dispatch bus literal. */
22
- export const PRESEEDED_CHANNELS = ['userPrompt', 'plan', 'checklist', 'code'];
23
-
24
- /**
25
- * Mint the concrete OUTPUT handle for a channel the node produces. This is the
26
- * relocated output half of the old _nodeIo switch; paths are byte-identical so
27
- * history/state.json/MOCK markers do not move.
28
- * @param {string} channel
29
- * @param {{projectDir,pipelineDir,baseName,datePrefix,cycle,key?}} ctx
30
- */
31
- export function allocate(channel, ctx) {
32
- const { projectDir, pipelineDir, baseName, datePrefix, cycle, key, workspaceKey } = ctx;
33
- switch (channel) {
34
- case 'plan': {
35
- // The planner is cycle-aware so a plan-review -> planner loop writes a fresh
36
- // -vN on each replan instead of clobbering v1 (cycle is 1 on the first pass,
37
- // so v1 has NO -v suffix, matching today's seeded io.planPath). The refiner
38
- // versions up to cycle+1 (a self-loop never produces v1). workspaceKey (when
39
- // present) routes the unified plan to the workspace store; absent it is byte-
40
- // identical to the single-project path.
41
- const version = key === 'planner' ? cycle : cycle + 1;
42
- return { kind: 'artifact', path: planPath(projectDir, baseName, version, datePrefix, workspaceKey) };
43
- }
44
- case 'review': {
45
- // Review json basename differs by producing role (keeps existing filenames).
46
- // A CUSTOM verifier key gets its own `<key>-review` basename so it can never
47
- // clobber the built-in reviewer's impl-review files.
48
- const k = key || 'reviewer';
49
- const BESPOKE_BASE = {
50
- reviewer: 'impl-review',
51
- refiner: 'refine-review',
52
- manualWebUiTesting: 'webui-review',
53
- planReviewer: 'plan-review',
54
- workspaceReviewer: 'ws-review',
55
- };
56
- let base = BESPOKE_BASE[k] || `${k}-review`;
57
- // A custom key like "impl" would mint impl-review-* and clobber the built-in
58
- // reviewer's files AND its DB verdict (reviewKind upsert); divert it.
59
- if (!BESPOKE_BASE[k] && Object.values(BESPOKE_BASE).includes(base)) base = `${k}-agent-review`;
60
- // ▲ C2: the refiner emits ONLY a json verdict (its md is null => private to its
61
- // self-loop). Every other verifier carries a review md so publish() folds it
62
- // onto the shared `review` channel its loop target consumes. workspaceKey routes
63
- // the review md to the workspace store (the json stays per-cycle in the pipeline
64
- // dir, store-root independent); absent it is byte-identical. Custom verifiers
65
- // keep their md pipeline-dir-local (like webui-review).
66
- const mdPath = k === 'refiner'
67
- ? null
68
- : k === 'manualWebUiTesting'
69
- ? join(pipelineDir, `webui-review-cycle${cycle}.md`)
70
- : k === 'planReviewer'
71
- ? reviewPath(projectDir, baseName, datePrefix, 'plan-review', workspaceKey)
72
- : k === 'workspaceReviewer'
73
- ? reviewPath(projectDir, baseName, datePrefix, 'ws-review', workspaceKey)
74
- : k === 'reviewer'
75
- ? reviewPath(projectDir, baseName, datePrefix, 'impl-review', workspaceKey)
76
- : join(pipelineDir, `${base}-cycle${cycle}.md`);
77
- return { kind: 'review', mdPath, jsonPath: join(pipelineDir, `${base}-cycle${cycle}.json`), reviewKind: base };
78
- }
79
- case 'checklist':
80
- return { kind: 'artifact', path: join(pipelineDir, 'manual-tests-checklist.md') };
81
- case 'code':
82
- return { kind: 'worktree' }; // standing channel; nothing to allocate
83
- case 'workspace':
84
- // Read-only metadata handle: the frozen workspace description snapshot. The
85
- // member set + description are seeded onto the bus directly by the orchestrator
86
- // (this just names the on-disk path of the frozen snapshot).
87
- return { kind: 'metadata', path: join(pipelineDir, 'workspace-description.md') };
88
- case 'decomposition':
89
- // The decomposer writes decomposition.json (phase->task index) into the run dir,
90
- // alongside a tasks/ subdir of self-contained task markdown files.
91
- return { kind: 'artifact', path: join(pipelineDir, 'decomposition.json') };
92
- case 'clarify':
93
- // The clarify agent writes clarify.json into the pipeline dir as scratch; the
94
- // DB clarify row is authoritative. Same path the legacy pre-step used.
95
- return { kind: 'clarify', path: join(pipelineDir, 'clarify.json') };
96
- default: {
97
- // Open vocabulary: any custom channel allocates a generic pipeline-dir
98
- // artifact. A registry channelDef (threaded via ctx.channelDefs) overrides
99
- // kind (md|json) and filename; absent a def the channel defaults to
100
- // markdown <id>.md. Loop re-runs suffix -cycle<N> so a rewind never
101
- // clobbers an earlier cycle's output.
102
- const def = (ctx.channelDefs && ctx.channelDefs[channel]) || null;
103
- const ext = def?.kind === 'json' ? 'json' : 'md';
104
- const stem = String(def?.filename || `${channel}.${ext}`).replace(/\.(md|json)$/i, '');
105
- const name = Number(cycle) > 1 ? `${stem}-cycle${cycle}.${ext}` : `${stem}.${ext}`;
106
- return { kind: 'artifact', path: join(pipelineDir, name), channel };
107
- }
108
- }
109
- }
110
-
111
- /**
112
- * Resolve a node's inputs from the bus (latest-writer-wins). Required channels that
113
- * are null still pass through (the agent/mock proceeds, e.g. a pre-seeded but
114
- * unwritten plan path). Optional channels that are null are omitted entirely.
115
- */
116
- export function bindInputs(consumes, optionalConsumes, bus) {
117
- const optional = new Set(optionalConsumes || []);
118
- const inputs = {};
119
- for (const c of consumes || []) {
120
- const v = bus[c];
121
- if (v == null && optional.has(c)) continue; // omit absent optional channel
122
- inputs[c] = v;
123
- }
124
- return inputs;
125
- }
126
-
127
- /**
128
- * Fold a node's result back onto the bus for each produced channel, in node order.
129
- * Clearing `review` on a `code` publish fixes the sticky fix-mode bug: a later
130
- * implementer no longer sees a stale review.
131
- */
132
- export function publish(produces, result, outputs, bus) {
133
- if (!result) return;
134
- for (const c of produces || []) {
135
- if (c === 'plan') {
136
- const path = result.outPlanPath || result.planPath || outputs.plan?.path;
137
- if (path) bus.plan = { kind: 'artifact', path };
138
- } else if (c === 'review') {
139
- // ▲ C2: only a review carrying an md (reviewer / manualWebUiTesting) is shared.
140
- // The refiner's md-less verdict stays private to its self-loop (read from the
141
- // results array by _reviewOf, never the bus), so it can't flip a later
142
- // implementer into `fix` mode.
143
- const mdPath = result.reviewMdPath ?? outputs.review?.mdPath;
144
- if (!mdPath) continue;
145
- bus.review = { kind: 'review', mdPath, jsonPath: outputs.review?.jsonPath, verdict: result.review, reviewKind: outputs.review?.reviewKind };
146
- } else if (c === 'checklist') {
147
- const path = result.checklistPath || outputs.checklist?.path;
148
- if (path) bus.checklist = { kind: 'artifact', path };
149
- } else if (c === 'code') {
150
- bus.review = null; // implementer superseded any pending review (fix-mode reset)
151
- } else if (c === 'workspace') {
152
- // Read-only metadata: seeded once by the orchestrator, NEVER re-published
153
- // (CONV-6: the frozen per-step snapshot must not change mid-run). No-op.
154
- } else if (c === 'clarify') {
155
- // The clarify node carries questions (from the agent) + answers (from the
156
- // interactive gate) in its result; fold both onto the bus so the planner reads
157
- // inputs.clarify.answers.
158
- bus.clarify = {
159
- kind: 'clarify',
160
- path: outputs.clarify?.path,
161
- questions: result.questions || [],
162
- answers: result.answers || [],
163
- };
164
- } else if (c === 'decomposition') {
165
- // Fold the decomposer's parsed phases onto the bus so _runStep's implement
166
- // step can fan out one implementer per task.
167
- const path = result.decompositionPath || outputs.decomposition?.path;
168
- bus.decomposition = {
169
- kind: 'artifact',
170
- path: path || null,
171
- phases: Array.isArray(result.decomposition?.phases) ? result.decomposition.phases : [],
172
- };
173
- } else {
174
- // Open vocabulary: a custom channel folds its allocated artifact handle onto
175
- // the bus (latest-writer-wins), mirroring plan/checklist.
176
- const path = outputs?.[c]?.path;
177
- if (path) bus[c] = { kind: 'artifact', path };
178
- }
179
- }
180
- }
181
-
182
- /**
183
- * Adapter: flatten typed inputs/outputs into the EXACT field names the phases.mjs
184
- * runners already read (verified against runners.mjs), so runners.mjs/phases.mjs and
185
- * the runner/dispatcher tests need ZERO changes. This is one of two role switches
186
- * (the other is runners.mjs's producer/verifier); it names fields, it does not
187
- * compute data flow.
188
- */
189
- export function legacyFields(node, inputs, outputs, cycle, baseName) {
190
- const fields = legacyRoleFields(node, inputs, outputs, cycle, baseName);
191
- // Workspace runs: surface the read-only metadata channel (description + member
192
- // set) onto the flattened ctx so phases.mjs runners read ctx.workspace. Absent
193
- // the channel (every single-project node) the field is never added, so the ctx
194
- // shape stays byte-identical.
195
- if (inputs && inputs.workspace) fields.workspace = inputs.workspace;
196
- return fields;
197
- }
198
-
199
- /** Per-role field naming (the original legacyFields body). */
200
- function legacyRoleFields(node, inputs, outputs, cycle, baseName) {
201
- switch (node.key) {
202
- case 'planner':
203
- // reviewPath is set only when a review is bound (a plan-review -> planner
204
- // rewind), which switches runPlannerPlan into its cold-replan branch.
205
- return { planFilePath: outputs.plan?.path, baseName, answers: inputs.clarify?.answers || inputs.userPrompt?.answers || [], reviewPath: inputs.review?.mdPath };
206
- case 'refiner':
207
- return { inPlanPath: inputs.plan?.path, outPlanPath: outputs.plan?.path, reviewJsonPath: outputs.review?.jsonPath, cycle };
208
- case 'decomposer':
209
- return { planPath: inputs.plan?.path, decompositionPath: outputs.decomposition?.path };
210
- case 'implementer':
211
- // The implementer fixes CODE reviews (impl-review / webui-review). A plan-review
212
- // left on the shared `review` bus by a planReviewer -> planner loop is NOT for the
213
- // implementer (it bounces to the planner), so it must not flip a first-pass
214
- // implementer into fix mode. Discriminate by review provenance (reviewKind), not
215
- // cycle — a linear `... -> reviewer -> implementer` forward edge legitimately fixes
216
- // a code review at cycle 1 (see saved-pipeline-parity).
217
- return { planPath: inputs.plan?.path, reviewPath: inputs.review?.mdPath, mode: (inputs.review?.mdPath && inputs.review?.reviewKind !== 'plan-review') ? 'fix' : 'implement', cycle };
218
- case 'planReviewer':
219
- return { planPath: inputs.plan?.path, reviewMdPath: outputs.review?.mdPath, reviewJsonPath: outputs.review?.jsonPath, cycle };
220
- case 'reviewer':
221
- return { planPath: inputs.plan?.path, reviewMdPath: outputs.review?.mdPath, reviewJsonPath: outputs.review?.jsonPath, cycle };
222
- case 'workspaceReviewer':
223
- // Identical field shape to `reviewer` — the workspace synthesizer reads the
224
- // same planPath/reviewMd/reviewJson/cycle and folds its merged verdict onto
225
- // the shared `review` channel its implementer loop target consumes.
226
- return { planPath: inputs.plan?.path, reviewMdPath: outputs.review?.mdPath, reviewJsonPath: outputs.review?.jsonPath, cycle };
227
- case 'manualTestsChecklist':
228
- return { planPath: inputs.plan?.path, checklistPath: outputs.checklist?.path };
229
- case 'manualWebUiTesting':
230
- return { checklistPath: inputs.checklist?.path, reviewMdPath: outputs.review?.mdPath, reviewJsonPath: outputs.review?.jsonPath, cycle };
231
- default:
232
- // Generic agents: no bespoke field names. The generic runners consume the
233
- // typed inputs/outputs maps directly (same references _bindNodeIo spreads).
234
- return { cycle, inputs, outputs };
235
- }
236
- }
237
-
238
- /** Channels materializable from plain prompt text (markdown artifacts). userPrompt
239
- * is already the prompt; code is the standing worktree; review is not pre-seeded and
240
- * is optional for its only consumer — so none of those ever seed. */
241
- const SEEDABLE = new Set(['plan', 'checklist']);
242
-
243
- /**
244
- * Decide which materializable channels must be seeded from the user prompt because
245
- * the topology REQUIRES them before any step produces them (a pipeline that starts
246
- * mid-stream — implementer/refiner/... first). Mirrors the validator's step-ordered
247
- * reachability walk (workflow-validator.mjs:112-144) but returns the channels to fill
248
- * rather than warnings. Reads the channel spec carried on each resolved node, so no
249
- * registry argument is needed (resolveWorkflow already stamped consumes/produces).
250
- * @param {Array<Array<{key,consumes,optionalConsumes,produces}>>} steps
251
- * @returns {string[]} channel ids to seed (subset of SEEDABLE), in first-needed order
252
- */
253
- export function entrySeedChannels(steps) {
254
- const seeded = new Set();
255
- const produced = new Set();
256
- for (const group of steps || []) {
257
- for (const node of group || []) {
258
- const optional = new Set(node?.optionalConsumes || []);
259
- for (const c of node?.consumes || []) {
260
- if (!SEEDABLE.has(c) || optional.has(c) || produced.has(c) || seeded.has(c)) continue;
261
- seeded.add(c);
262
- }
263
- }
264
- // Producers fold in AFTER the step (matches the validator's frozen-snapshot model),
265
- // so a node that both consumes AND produces the same channel (the refiner) still
266
- // triggers a seed at its own step.
267
- for (const node of group || []) for (const c of node?.produces || []) produced.add(c);
268
- }
269
- return [...seeded];
270
- }
271
-
272
- /**
273
- * Render the `## Attached files` block listing each attachment by path + name.
274
- * Single source of truth shared by renderPromptArtifact (the seeded file body) and
275
- * phases.mjs taskHeader (the entry agent's inline header) so the two cannot drift.
276
- * Returns '' when there are no attachments.
277
- * @param {Array<{name:string,path:string}>} [extras]
278
- */
279
- export function renderAttachmentsBlock(extras = []) {
280
- if (!Array.isArray(extras) || extras.length === 0) return '';
281
- return (
282
- `\n## Attached files\n\nThe user attached these files; read any that are relevant:\n\n` +
283
- extras.map((e) => `- \`${e.path}\` (${e.name})`).join('\n') +
284
- '\n'
285
- );
286
- }
287
-
288
- /**
289
- * Render the markdown a seeded artifact channel holds: the user's request stands in
290
- * for the missing upstream artifact, with any attached files listed by path.
291
- * @param {string} promptText
292
- * @param {Array<{name:string,path:string}>} [extras]
293
- */
294
- export function renderPromptArtifact(promptText, extras = []) {
295
- const body = (promptText || '').trim() || '(no prompt text)';
296
- return (
297
- `# Task (from the user prompt)\n\n` +
298
- `No upstream agent produced this artifact, so the user's request below stands in for it.\n\n` +
299
- `## Original request\n\n${body}\n` +
300
- renderAttachmentsBlock(extras)
301
- );
302
- }
@@ -1,167 +0,0 @@
1
- // src/core/runners.mjs
2
- // Runner registry: maps an agent's runnerType -> a function the dispatcher calls.
3
- //
4
- // There are exactly two runner types (CONTRACT):
5
- // - producer : generates artifacts/code (Plan, Refine, Implement, Manual Tests
6
- // Checklist). Always returns status "ok"; may carry a review.
7
- // - verifier : emits a protocol.mjs review verdict (Review, Manual web UI
8
- // testing). status is "blocked" iff the review has blocking
9
- // (critical/major) issues; eligible as a loopSource.
10
- //
11
- // Each runner receives the orchestrator's node ctx (see Orchestrator._nodeCtx):
12
- // { projectDir, pipelineDir, taskPrompt, toolInstruction, agentPrompts,
13
- // checkpointRef, signal, onEvent, claudeOpts:{model,effort,mock,...},
14
- // node:{nodeId,key,runnerType,loopSource,...}, nodeId, stepIndex, cycle,
15
- // ...per-call fields the dispatcher threads in (planPath, planFilePath,
16
- // reviewMdPath, reviewJsonPath, outPlanPath, inPlanPath, baseName,
17
- // answers, reviewPath, mode) }
18
- //
19
- // New agents pick an existing runnerType and need NO engine code; a genuinely new
20
- // behavior = add one branch (or one runner) here.
21
-
22
- import {
23
- runPlannerPlan,
24
- runRefiner,
25
- runDecomposer,
26
- runImplementer,
27
- runReviewer,
28
- runPlanReviewer,
29
- runWorkspaceReviewer,
30
- runManualTestsChecklist,
31
- runManualWebUiTesting,
32
- runGenericProducer,
33
- runGenericVerifier,
34
- } from './phases.mjs';
35
- import { hasBlocking, blockingIssues } from './protocol.mjs';
36
-
37
- /** Normalize a protocol review into the RunnerResult verdict fields. */
38
- function verdict(review) {
39
- return {
40
- status: hasBlocking(review) ? 'blocked' : 'ok',
41
- issues: blockingIssues(review),
42
- review,
43
- summary: review?.summary || '',
44
- };
45
- }
46
-
47
- /**
48
- * producer — generates artifacts/code. Dispatches on the canonical agent key.
49
- * Always status "ok" (producers do not gate); the refiner additionally surfaces
50
- * its review so a workflow MAY hang a loop off it, but default routing does not.
51
- * @param {object} ctx node ctx from the orchestrator
52
- * @returns {Promise<{status:'ok', summary?:string, planPath?:string, outPlanPath?:string, review?:object}>}
53
- */
54
- async function producer(ctx) {
55
- const key = ctx?.node?.key;
56
- switch (key) {
57
- case 'planner': {
58
- const { planPath } = await runPlannerPlan(ctx, {
59
- answers: ctx.answers || [],
60
- planFilePath: ctx.planFilePath,
61
- baseName: ctx.baseName,
62
- reviewPath: ctx.reviewPath,
63
- });
64
- return { status: 'ok', planPath, summary: 'Plan written.' };
65
- }
66
- case 'refiner': {
67
- const { outPlanPath, review } = await runRefiner(ctx, {
68
- inPlanPath: ctx.inPlanPath,
69
- outPlanPath: ctx.outPlanPath,
70
- cycle: ctx.cycle,
71
- reviewJsonPath: ctx.reviewJsonPath,
72
- });
73
- // A producer never blocks; expose the review (+ issues) for loop wiring.
74
- return { status: 'ok', outPlanPath, review, issues: blockingIssues(review), summary: review?.summary || '' };
75
- }
76
- case 'decomposer': {
77
- const { decompositionPath, decomposition } = await runDecomposer(ctx, {
78
- planPath: ctx.planPath,
79
- decompositionPath: ctx.decompositionPath,
80
- });
81
- return { status: 'ok', decompositionPath, decomposition, summary: 'Plan decomposed.' };
82
- }
83
- case 'implementer': {
84
- const { summary } = await runImplementer(ctx, {
85
- planPath: ctx.planPath,
86
- reviewPath: ctx.reviewPath,
87
- taskPath: ctx.node?.taskPath,
88
- siblings: ctx.node?.siblings,
89
- mode: ctx.mode || 'implement',
90
- });
91
- return { status: 'ok', summary };
92
- }
93
- case 'manualTestsChecklist': {
94
- const { checklistPath, summary } = await runManualTestsChecklist(ctx, {
95
- planPath: ctx.planPath,
96
- checklistPath: ctx.checklistPath,
97
- });
98
- return { status: 'ok', checklistPath, summary };
99
- }
100
- default: {
101
- // Generic branch: a metadata-declared agent runs with ZERO core edits.
102
- const { summary } = await runGenericProducer(ctx);
103
- return { status: 'ok', summary };
104
- }
105
- }
106
- }
107
-
108
- /**
109
- * verifier — emits a protocol review verdict. status "blocked" iff the review has
110
- * blocking issues. Eligible as a loopSource.
111
- * @param {object} ctx node ctx from the orchestrator
112
- * @returns {Promise<{status:'ok'|'blocked', issues:Array, review:object, summary:string}>}
113
- */
114
- async function verifier(ctx) {
115
- const key = ctx?.node?.key;
116
- switch (key) {
117
- case 'reviewer': {
118
- const { review } = await runReviewer(ctx, {
119
- planPath: ctx.planPath,
120
- reviewMdPath: ctx.reviewMdPath,
121
- reviewJsonPath: ctx.reviewJsonPath,
122
- cycle: ctx.cycle,
123
- });
124
- // CONV-5: thread the review markdown path so a loop rewind runs the implementer in `fix` mode.
125
- return { ...verdict(review), reviewMdPath: ctx.reviewMdPath };
126
- }
127
- case 'planReviewer': {
128
- const { review } = await runPlanReviewer(ctx, {
129
- planPath: ctx.planPath,
130
- reviewMdPath: ctx.reviewMdPath,
131
- reviewJsonPath: ctx.reviewJsonPath,
132
- cycle: ctx.cycle,
133
- });
134
- // Thread the review md path so a loop rewind to the planner reads the review.
135
- return { ...verdict(review), reviewMdPath: ctx.reviewMdPath };
136
- }
137
- case 'workspaceReviewer': {
138
- const { review } = await runWorkspaceReviewer(ctx, {
139
- planPath: ctx.planPath,
140
- reviewMdPath: ctx.reviewMdPath,
141
- reviewJsonPath: ctx.reviewJsonPath,
142
- cycle: ctx.cycle,
143
- });
144
- // CONV-5: thread the review markdown path so a loop rewind runs the implementer in `fix` mode.
145
- return { ...verdict(review), reviewMdPath: ctx.reviewMdPath };
146
- }
147
- case 'manualWebUiTesting': {
148
- const { review } = await runManualWebUiTesting(ctx, {
149
- checklistPath: ctx.checklistPath,
150
- reviewMdPath: ctx.reviewMdPath,
151
- reviewJsonPath: ctx.reviewJsonPath,
152
- cycle: ctx.cycle,
153
- });
154
- // CONV-5: thread the review markdown path (web-UI loop source → implementer fix mode).
155
- return { ...verdict(review), reviewMdPath: ctx.reviewMdPath };
156
- }
157
- default: {
158
- // Generic branch: a metadata-declared verifier emits the standard protocol
159
- // verdict; thread the md path so a loop rewind reads the review (CONV-5).
160
- const { review, reviewMdPath } = await runGenericVerifier(ctx);
161
- return { ...verdict(review), reviewMdPath };
162
- }
163
- }
164
- }
165
-
166
- /** The runner registry: runnerType -> async (ctx) => RunnerResult. */
167
- export const runners = { producer, verifier };
@@ -1,185 +0,0 @@
1
- // src/core/workflow-validator.mjs
2
- // Pure validator (imports only the channel constants from channels.mjs) for a
3
- // WorkflowTemplate against an agent registry. Collects ALL violations (does not
4
- // short-circuit) so the UI/API can show every problem at once. Returns
5
- // { ok, errors:string[], warnings:string[] } (warnings are soft: reachability/
6
- // governance/multi-producer hints that never set ok=false, so saved
7
- // topology-only pipelines stay valid).
8
- //
9
- // Rules (CONTRACT §workflow-validator):
10
- // 1. template is a non-null object with a non-empty steps array;
11
- // 2. no empty steps (every step has >= 1 node);
12
- // 3. every node has a non-blank string id, and ids are unique workflow-wide;
13
- // 4. every node.key exists in the registry;
14
- // 5. feedback from/to reference existing node ids;
15
- // 6. a feedback's target step index < its source step index
16
- // (a same-node self-loop, from===to, is allowed; the forward graph is
17
- // otherwise acyclic so only back-edges are legal feedbacks);
18
- // 7. feedback ids are unique.
19
-
20
- import { CHANNEL_IDS, PRESEEDED_CHANNELS } from './channels.mjs';
21
-
22
- /**
23
- * @param {object} tpl WorkflowTemplate { steps:[[{id,key}]], feedbacks:[{id,from,to}] }
24
- * @param {Record<string,{key:string}>} registry loadAgentRegistry() output
25
- * @returns {{ok:boolean, errors:string[], warnings:string[]}}
26
- */
27
- export function validateWorkflow(tpl, registry) {
28
- const errors = [];
29
- const reg = registry && typeof registry === 'object' ? registry : {};
30
-
31
- if (!tpl || typeof tpl !== 'object' || !Array.isArray(tpl.steps)) {
32
- return { ok: false, errors: ['workflow must be an object with a steps array'], warnings: [] };
33
- }
34
- if (tpl.steps.length === 0) {
35
- errors.push('workflow must have at least one step');
36
- }
37
-
38
- // Pass 1: nodes — shape, unique ids, known keys. Build id -> stepIndex map.
39
- const stepOfNode = new Map(); // nodeId -> step index
40
- const seenIds = new Set();
41
- for (let i = 0; i < tpl.steps.length; i++) {
42
- const group = tpl.steps[i];
43
- if (!Array.isArray(group) || group.length === 0) {
44
- errors.push(`step ${i} is empty (a step must contain at least one node)`);
45
- continue;
46
- }
47
- for (const node of group) {
48
- if (!node || typeof node !== 'object') {
49
- errors.push(`step ${i} contains a non-object node`);
50
- continue;
51
- }
52
- const id = typeof node.id === 'string' ? node.id.trim() : '';
53
- if (!id) {
54
- errors.push(`step ${i} has a node with a missing or blank id`);
55
- continue;
56
- }
57
- if (seenIds.has(id)) {
58
- errors.push(`duplicate node id "${id}"`);
59
- } else {
60
- seenIds.add(id);
61
- stepOfNode.set(id, i);
62
- }
63
- const key = typeof node.key === 'string' ? node.key.trim() : '';
64
- if (!key) {
65
- errors.push(`node "${id}" has a missing or blank key`);
66
- } else if (!Object.prototype.hasOwnProperty.call(reg, key)) {
67
- errors.push(`node "${id}" has key "${key}" which is not in the agent registry`);
68
- }
69
- }
70
- }
71
-
72
- // Pass 2: feedbacks — unique ids, endpoints exist, target precedes source.
73
- const feedbacks = Array.isArray(tpl.feedbacks) ? tpl.feedbacks : [];
74
- const seenFb = new Set();
75
- for (const fb of feedbacks) {
76
- if (!fb || typeof fb !== 'object') {
77
- errors.push('feedbacks contains a non-object entry');
78
- continue;
79
- }
80
- const fid = typeof fb.id === 'string' ? fb.id.trim() : '';
81
- if (!fid) {
82
- errors.push('a feedback has a missing or blank id');
83
- } else if (seenFb.has(fid)) {
84
- errors.push(`duplicate feedback id "${fid}"`);
85
- } else {
86
- seenFb.add(fid);
87
- }
88
- const from = typeof fb.from === 'string' ? fb.from.trim() : '';
89
- const to = typeof fb.to === 'string' ? fb.to.trim() : '';
90
- const hasFrom = stepOfNode.has(from);
91
- const hasTo = stepOfNode.has(to);
92
- if (!hasFrom) errors.push(`feedback "${fid || '?'}" from "${from}" does not exist`);
93
- if (!hasTo) errors.push(`feedback "${fid || '?'}" to "${to}" does not exist`);
94
- if (hasFrom && hasTo) {
95
- const sFrom = stepOfNode.get(from);
96
- const sTo = stepOfNode.get(to);
97
- // A same-node self-loop (from === to) is legal (the refine loop). Otherwise
98
- // the target step must strictly precede the source step (a back-edge).
99
- if (from !== to && sTo >= sFrom) {
100
- errors.push(
101
- `feedback "${fid || '?'}" target step (${sTo}) must precede its source step (${sFrom})`,
102
- );
103
- }
104
- }
105
- }
106
-
107
- // Pass 3: WARNINGS (never block saves, so existing topology-only pipelines stay
108
- // valid). Forward order = step order.
109
- const warnings = [];
110
- // Pre-seeded channels (derived from channels.mjs — the dispatcher's bus literal)
111
- // are always reachable and never warn.
112
- const PRESEEDED = new Set(PRESEEDED_CHANNELS);
113
- const NON_MULTIPLEXABLE = new Set(['code', 'plan']); // one producer per step
114
- const produced = new Set();
115
- for (let i = 0; i < tpl.steps.length; i++) {
116
- const group = Array.isArray(tpl.steps[i]) ? tpl.steps[i] : [];
117
- // (a) reachability: a required, non-pre-seeded channel must be produced earlier
118
- for (const node of group) {
119
- const meta = reg[node?.key] || {};
120
- const optional = new Set(meta.optionalConsumes || []);
121
- for (const c of meta.consumes || []) {
122
- if (optional.has(c) || PRESEEDED.has(c) || produced.has(c)) continue;
123
- warnings.push(`node "${node.id}" consumes "${c}" but no upstream step produces it`);
124
- }
125
- }
126
- // (b) multi-producer (D2) + stale-sibling (D3): scan within the step
127
- const stepProducers = new Map(); // channel -> count
128
- for (const node of group) for (const c of (reg[node?.key]?.produces || [])) {
129
- stepProducers.set(c, (stepProducers.get(c) || 0) + 1);
130
- }
131
- for (const [c, n] of stepProducers) {
132
- if (n > 1 && NON_MULTIPLEXABLE.has(c)) {
133
- warnings.push(`step ${i} has ${n} producers of "${c}" (only one producer per step is well-defined)`);
134
- }
135
- }
136
- for (const node of group) {
137
- const meta = reg[node?.key] || {};
138
- const optional = new Set(meta.optionalConsumes || []);
139
- for (const c of meta.consumes || []) {
140
- // a channel produced ONLY by a same-step sibling is read stale (pre-step snapshot)
141
- if (!optional.has(c) && !produced.has(c) && !PRESEEDED.has(c) && stepProducers.has(c)) {
142
- warnings.push(`node "${node.id}" consumes "${c}" produced only by a same-step sibling; it reads the pre-step value`);
143
- }
144
- }
145
- }
146
- // commit this step's production AFTER the step (matches the frozen-snapshot model)
147
- for (const [c] of stepProducers) produced.add(c);
148
- }
149
- // (d) custom-channel hygiene: a PRODUCED channel that is neither built-in nor
150
- // declared by any registry channelDef silently defaults to a generic markdown
151
- // artifact — surface that once per channel so a wizard/meta typo is visible.
152
- const builtinChannels = new Set(CHANNEL_IDS);
153
- const definedCustom = new Set();
154
- for (const m of Object.values(reg)) {
155
- for (const d of m?.channelDefs || []) if (d?.id) definedCustom.add(d.id);
156
- }
157
- const flaggedDefless = new Set();
158
- for (const group of tpl.steps) {
159
- for (const node of Array.isArray(group) ? group : []) {
160
- for (const c of reg[node?.key]?.produces || []) {
161
- if (builtinChannels.has(c) || definedCustom.has(c) || flaggedDefless.has(c)) continue;
162
- flaggedDefless.add(c);
163
- warnings.push(`custom channel "${c}" (produced by "${node.id}") has no channelDef; it defaults to markdown "${c}.md"`);
164
- }
165
- }
166
- }
167
- // (c) governance: every adjacent forward edge + every feedback edge must be allowed
168
- const keyOf = new Map();
169
- tpl.steps.forEach((g) => (Array.isArray(g) ? g : []).forEach((n) => keyOf.set(n.id, n.key)));
170
- const allows = (fromKey, toKey) => {
171
- const ct = reg[fromKey]?.connectsTo;
172
- return ct === '*' || ct === undefined || !Array.isArray(ct) || ct.includes(toKey);
173
- };
174
- for (let i = 0; i < tpl.steps.length - 1; i++) {
175
- for (const a of tpl.steps[i] || []) for (const b of tpl.steps[i + 1] || []) {
176
- if (!allows(a.key, b.key)) warnings.push(`"${a.key}" is not allowed to connect to "${b.key}" (connectsTo)`);
177
- }
178
- }
179
- for (const fb of Array.isArray(tpl.feedbacks) ? tpl.feedbacks : []) {
180
- const fk = keyOf.get(fb?.from), tk = keyOf.get(fb?.to);
181
- if (fk && tk && !allows(fk, tk)) warnings.push(`feedback "${fb.id || '?'}": "${fk}" is not allowed to connect to "${tk}" (connectsTo)`);
182
- }
183
-
184
- return { ok: errors.length === 0, errors, warnings };
185
- }