@worca/app 1.0.0 → 1.1.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.
- package/README.md +22 -9
- package/agents/clarify.meta.json +4 -4
- package/agents/decomposer.meta.json +5 -5
- package/agents/implementer.meta.json +15 -5
- package/agents/manualTestsChecklist.meta.json +5 -4
- package/agents/manualWebUiTesting.meta.json +9 -4
- package/agents/planReviewer.meta.json +12 -4
- package/agents/planner.meta.json +12 -5
- package/agents/refiner.meta.json +15 -4
- package/agents/reviewer.meta.json +14 -4
- package/agents/worca-cc-clarify.md +7 -0
- package/agents/worca-cc-code-reviewer.md +11 -6
- package/agents/worca-cc-decomposer.md +7 -0
- package/agents/worca-cc-implementer.md +9 -0
- package/agents/worca-cc-manual-tests-checklist.md +8 -5
- package/agents/worca-cc-manual-web-ui-testing.md +10 -6
- package/agents/worca-cc-plan-refiner.md +11 -6
- package/agents/worca-cc-plan-reviewer.md +10 -7
- package/agents/worca-cc-planner.md +9 -0
- package/agents/worca-cc-workspace-reviewer.md +11 -4
- package/agents/worca-cc-workspace-scanner.md +8 -4
- package/agents/workspaceReviewer.meta.json +15 -4
- package/agents/workspaceScanner.meta.json +5 -4
- package/package.json +8 -2
- package/skills/worca/SKILL.md +5 -5
- package/src/cli/render.mjs +148 -0
- package/src/cli/worca-cc.mjs +319 -45
- package/src/core/agent-gen.mjs +69 -31
- package/src/core/agent-registry.mjs +124 -144
- package/src/core/agent-store.mjs +164 -4
- package/src/core/artifacts.mjs +189 -21
- package/src/core/ask/catalog.mjs +111 -0
- package/src/core/ask/comment-deps.mjs +55 -0
- package/src/core/ask/events.mjs +506 -0
- package/src/core/ask/follow.mjs +107 -0
- package/src/core/ask/git-allowlist.mjs +226 -0
- package/src/core/ask/limits.mjs +54 -0
- package/src/core/ask/mcp-stdio.mjs +135 -0
- package/src/core/ask/models.mjs +125 -0
- package/src/core/ask/prompt.mjs +261 -0
- package/src/core/ask/proposal.mjs +170 -0
- package/src/core/ask/redact.mjs +30 -0
- package/src/core/ask/spawn.mjs +153 -0
- package/src/core/ask/store.mjs +360 -0
- package/src/core/ask/tool-deps.mjs +63 -0
- package/src/core/ask/tools.mjs +848 -0
- package/src/core/ask/turn.mjs +416 -0
- package/src/core/ask/worktree-deps.mjs +27 -0
- package/src/core/ask/worktrees.mjs +285 -0
- package/src/core/chat/command-router.mjs +20 -3
- package/src/core/claude-runner.mjs +434 -57
- package/src/core/config.mjs +264 -41
- package/src/core/cost-budget.mjs +29 -2
- package/src/core/db.mjs +684 -47
- package/src/core/diff-anchor.mjs +213 -0
- package/src/core/diff-comments.mjs +273 -0
- package/src/core/engine-select.mjs +32 -0
- package/src/core/git-info.mjs +49 -10
- package/src/core/graph/builtin-workflows.mjs +51 -0
- package/src/core/graph/executor.mjs +894 -0
- package/src/core/graph/registry-ports.mjs +12 -0
- package/src/core/graph/scheduler.mjs +1065 -0
- package/src/core/graph/seed-templates.mjs +318 -0
- package/src/core/model-env.mjs +112 -8
- package/src/core/model-test.mjs +79 -0
- package/src/core/orchestrator.mjs +902 -4098
- package/src/core/overview-agent.mjs +15 -3
- package/src/core/phases.mjs +208 -537
- package/src/core/pipeline-delete.mjs +13 -2
- package/src/core/plugin-api.mjs +8 -3
- package/src/core/plugin-config.mjs +178 -28
- package/src/core/plugin-inventory.mjs +6 -2
- package/src/core/plugin-manifest.mjs +199 -11
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-repo.mjs +16 -4
- package/src/core/plugin-shim-child.mjs +9 -3
- package/src/core/plugin-shim.mjs +77 -14
- package/src/core/plugin-store.mjs +236 -29
- package/src/core/plugin-workflows.mjs +90 -41
- package/src/core/preflight.mjs +135 -3
- package/src/core/projects.mjs +7 -5
- package/src/core/protocol.mjs +8 -35
- package/src/core/recoverable-error.mjs +1 -1
- package/src/core/run-harness.mjs +3585 -0
- package/src/core/run-manifest.mjs +5 -1
- package/src/core/settings.mjs +109 -13
- package/src/core/skills.mjs +10 -3
- package/src/core/source-bindings.mjs +175 -0
- package/src/core/sources.mjs +87 -25
- package/src/core/stats.mjs +25 -6
- package/src/core/title.mjs +51 -4
- package/src/core/workflows.mjs +358 -259
- package/src/core/workspace-scan.mjs +4 -0
- package/src/core/worktree.mjs +98 -7
- package/src/shared/graph/agent-meta.mjs +278 -0
- package/src/shared/graph/constants.mjs +105 -0
- package/src/shared/graph/geometry.mjs +157 -0
- package/src/shared/graph/layout.mjs +134 -0
- package/src/shared/graph/loops.mjs +130 -0
- package/src/shared/graph/manifest.mjs +257 -0
- package/src/shared/graph/ports.mjs +153 -0
- package/src/shared/graph/route.mjs +397 -0
- package/src/shared/graph/template.mjs +165 -0
- package/src/shared/graph/thumbnail.mjs +67 -0
- package/src/shared/graph/validate.mjs +491 -0
- package/src/shared/graph/verdict.mjs +41 -0
- package/ui/public/app.js +4008 -1670
- package/ui/public/ask-markdown.mjs +145 -0
- package/ui/public/ask-model.mjs +264 -0
- package/ui/public/ask-panel.mjs +1880 -0
- package/ui/public/chat-settings-view.mjs +6 -2
- package/ui/public/diff-view.mjs +66 -11
- package/ui/public/file-tree.mjs +305 -0
- package/ui/public/graph/composer.mjs +889 -0
- package/ui/public/graph/inspector.mjs +183 -0
- package/ui/public/graph/model.mjs +37 -0
- package/ui/public/graph/palette.mjs +144 -0
- package/ui/public/graph/run-decor.mjs +410 -0
- package/ui/public/graph/run-hosts.mjs +201 -0
- package/ui/public/graph/save-dialog.mjs +56 -0
- package/ui/public/graph/view.mjs +858 -0
- package/ui/public/guardrails-view.mjs +4 -2
- package/ui/public/hljs-loader.mjs +180 -0
- package/ui/public/index.html +269 -265
- package/ui/public/log-filter.mjs +22 -4
- package/ui/public/log-line.mjs +45 -19
- package/ui/public/models-view.mjs +171 -9
- package/ui/public/plugins-view.mjs +106 -4
- package/ui/public/source-pane.mjs +190 -8
- package/ui/public/stats-view.mjs +81 -1
- package/ui/public/style.css +1459 -229
- package/ui/public/syntax-highlight.mjs +270 -0
- package/ui/public/thinking-orb.mjs +110 -0
- package/ui/server.mjs +1667 -98
- package/src/core/channels.mjs +0 -302
- package/src/core/runners.mjs +0 -167
- package/src/core/workflow-validator.mjs +0 -185
- package/ui/public/composer-core.mjs +0 -211
package/src/cli/worca-cc.mjs
CHANGED
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
import { createInterface } from 'node:readline';
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
|
+
import { fstatSync } from 'node:fs';
|
|
13
14
|
import { fileURLToPath } from 'node:url';
|
|
14
15
|
import { dirname, resolve, join, basename } from 'node:path';
|
|
15
16
|
import process from 'node:process';
|
|
16
17
|
|
|
17
18
|
import { preflightNode } from '../core/preflight-node.mjs';
|
|
18
|
-
import {
|
|
19
|
+
import { createOrchestratorFor } from '../core/engine-select.mjs';
|
|
19
20
|
import {
|
|
20
21
|
addProject,
|
|
21
22
|
listProjects,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
normalizeProjectPath,
|
|
24
25
|
} from '../core/projects.mjs';
|
|
25
26
|
import { projectKey } from '../core/store.mjs';
|
|
27
|
+
import { formatExecLine, formatGateHeader, formatRunSummary } from './render.mjs';
|
|
26
28
|
|
|
27
29
|
// ── node:sqlite runtime guard + warning filter ──────────────────────────────────
|
|
28
30
|
// Drop ONLY the one-time ExperimentalWarning emitted by node:sqlite (the module is
|
|
@@ -46,6 +48,14 @@ const REPO_ROOT = resolve(__dirname, '..', '..');
|
|
|
46
48
|
|
|
47
49
|
// ── arg parsing ────────────────────────────────────────────────────────────────
|
|
48
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The permission modes a pipeline run may be launched with. Deliberately NOT the
|
|
53
|
+
* full set claude accepts: `dontAsk` belongs to the Ask Worca runner alone
|
|
54
|
+
* (core/ask/spawn.mjs), which owns its own spawn options and never comes through
|
|
55
|
+
* here.
|
|
56
|
+
*/
|
|
57
|
+
const PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'bypassPermissions', 'dontAsk'];
|
|
58
|
+
|
|
49
59
|
/**
|
|
50
60
|
* Parse argv into a flags object. Supports "--flag value" and "--flag=value", plus the
|
|
51
61
|
* boolean flags --mock, --yes/--non-interactive, --ui, -h/--help.
|
|
@@ -134,6 +144,12 @@ function parseArgs(argv) {
|
|
|
134
144
|
const p = part.trim();
|
|
135
145
|
if (p) out.extras.push(p);
|
|
136
146
|
}
|
|
147
|
+
} else if (key === 'permissionMode' && !PERMISSION_MODES.includes(String(value))) {
|
|
148
|
+
// A pipeline run's mode reaches claude-runner as-is. `dontAsk` is a legitimate
|
|
149
|
+
// headless mode for a REAL run (allowedTools decide what runs); only the
|
|
150
|
+
// MOCK runner treats it as the Ask Worca recipe — that pair is refused below,
|
|
151
|
+
// after --mock/WORCA_MOCK are known (review of PR #376).
|
|
152
|
+
fail(`--permission-mode must be one of ${PERMISSION_MODES.join(', ')}, got: ${value}`);
|
|
137
153
|
} else {
|
|
138
154
|
out[key] = value;
|
|
139
155
|
}
|
|
@@ -165,12 +181,13 @@ function budgetRefusalDetail(b) {
|
|
|
165
181
|
+ `resets ${when} (in ${days}d ${hours}h)`;
|
|
166
182
|
}
|
|
167
183
|
|
|
168
|
-
const HELP = `worca —
|
|
184
|
+
const HELP = `worca — node-graph multi-agent pipelines
|
|
169
185
|
|
|
170
186
|
Usage:
|
|
171
187
|
worca <subcommand> [args]
|
|
172
188
|
worca --prompt "<task>" [--project <dir>] [options]
|
|
173
189
|
worca --file <task.md> [--project <dir>] [options]
|
|
190
|
+
worca "<task>" [--project <dir>] [options] (bare prompt; quote it)
|
|
174
191
|
worca --ui
|
|
175
192
|
worca --install <targetDir> [--force]
|
|
176
193
|
|
|
@@ -182,9 +199,10 @@ Subcommands:
|
|
|
182
199
|
[--ignore-cost-cap] Resume past this pipeline's cost cap (persists on the run).
|
|
183
200
|
doctor Reconcile crashed runs and sweep leftover run roots.
|
|
184
201
|
plugin <cmd> [...] Manage plugins: add|install|list|update|remove|purge|enable|
|
|
185
|
-
disable|doctor|link|init|validate|exec. See: worca plugin help
|
|
202
|
+
disable|doctor|link|reimport|init|validate|exec. See: worca plugin help
|
|
186
203
|
marketplace <cmd> [...] Manage plugin marketplaces: add|list|refresh|remove. See: worca marketplace help
|
|
187
204
|
config [get|set|unset] Budget & cost-limit settings
|
|
205
|
+
help Print this help (same as --help).
|
|
188
206
|
|
|
189
207
|
Options:
|
|
190
208
|
--project <dir> Target project directory (default: cwd)
|
|
@@ -194,8 +212,9 @@ Options:
|
|
|
194
212
|
--extras <paths> Extra files copied into the pipeline's extras/ folder
|
|
195
213
|
(comma-separated; repeatable)
|
|
196
214
|
--model <m> Claude model id
|
|
197
|
-
--permission-mode <m> Claude permission mode
|
|
198
|
-
|
|
215
|
+
--permission-mode <m> Claude permission mode: default | acceptEdits | plan |
|
|
216
|
+
bypassPermissions (default acceptEdits)
|
|
217
|
+
--workflow <id> Saved pipeline template to run (default: wf_default — the built-in graph)
|
|
199
218
|
--source-branch <name> Branch to fork the per-run worktree from (default: current HEAD)
|
|
200
219
|
--branch <name> Feature branch name (default: claude proposes one)
|
|
201
220
|
--mock Offline mock mode (no claude, no tokens)
|
|
@@ -227,18 +246,7 @@ function out(s) {
|
|
|
227
246
|
process.stdout.write(s + '\n');
|
|
228
247
|
}
|
|
229
248
|
|
|
230
|
-
function phaseLabel(phase, cycle) {
|
|
231
|
-
if (cycle && (phase === 'refine' || phase === 'review' || phase === 'implement' || phase === 'clarify')) {
|
|
232
|
-
return `${phase} #${cycle}`;
|
|
233
|
-
}
|
|
234
|
-
return phase;
|
|
235
|
-
}
|
|
236
249
|
|
|
237
|
-
function statusMark(status) {
|
|
238
|
-
if (status === 'done') return c('green', '✓');
|
|
239
|
-
if (status === 'start') return c('cyan', '▶');
|
|
240
|
-
return c('gray', '•');
|
|
241
|
-
}
|
|
242
250
|
|
|
243
251
|
const LEVEL_COLOR = { info: 'reset', debug: 'gray', warn: 'yellow', error: 'red' };
|
|
244
252
|
|
|
@@ -300,9 +308,10 @@ async function askClarify(rl, questions) {
|
|
|
300
308
|
* Ask a loop gate interactively. Shows the open blocking issues and the two choices.
|
|
301
309
|
* Returns { decision: "continue" | "another" }.
|
|
302
310
|
*/
|
|
303
|
-
async function askGate(rl, issues) {
|
|
311
|
+
async function askGate(rl, issues, header) {
|
|
304
312
|
out('');
|
|
305
|
-
|
|
313
|
+
// A graph run names the wire and its budget (`? Loop gate · Reviewer → Implementer 3/3 cycles used`).
|
|
314
|
+
out(c('yellow', c('bold', header || 'Loop gate — maximum cycles reached.')));
|
|
306
315
|
out(c('yellow', 'Open critical/major issues:'));
|
|
307
316
|
if (!issues || issues.length === 0) {
|
|
308
317
|
out(' (none reported)');
|
|
@@ -347,18 +356,109 @@ async function askRecovery(rl, recovery) {
|
|
|
347
356
|
|
|
348
357
|
// ── shared drive loop ────────────────────────────────────────────────────────────
|
|
349
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Whether stdin could ever deliver an interactive answer.
|
|
361
|
+
*
|
|
362
|
+
* A TTY always can. A pipe, socket or file redirect MAY — a wrapper script that
|
|
363
|
+
* feeds answers is legitimate — so those pass. `/dev/null` never can: it EOFs on
|
|
364
|
+
* the first read, so the run would start, spend a real agent call, and then be
|
|
365
|
+
* abandoned mid-question. Measured on darwin (and matching linux): `< /dev/null`,
|
|
366
|
+
* a closed fd 0 (Node reopens it on /dev/null) and `spawn(…, {stdio:['ignore',…]})`
|
|
367
|
+
* are all non-TTY CHARACTER devices, while a pipe is a fifo/socket and a redirect
|
|
368
|
+
* is a regular file. An fstat that throws answers "yes" — never refuse a run on a
|
|
369
|
+
* guess.
|
|
370
|
+
*/
|
|
371
|
+
function stdinCanAnswer() {
|
|
372
|
+
if (process.stdin.isTTY) return true;
|
|
373
|
+
try {
|
|
374
|
+
return !fstatSync(0).isCharacterDevice();
|
|
375
|
+
} catch {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
350
380
|
/**
|
|
351
381
|
* Wire readline Q&A, log/phase rendering, and SIGINT pause/stop onto an
|
|
352
382
|
* orchestrator, then drive it. `start` launches run() or resume(). Returns the
|
|
353
383
|
* process exit code (0 for done/paused, 1 otherwise).
|
|
354
384
|
*/
|
|
355
385
|
async function attachAndDrive(orch, flags, start) {
|
|
386
|
+
// Refuse an unanswerable interactive run BEFORE start(). The orchestrator
|
|
387
|
+
// constructor is pure (createPipeline runs inside run()), so nothing exists yet:
|
|
388
|
+
// no pipelines row, no run root, no worktree, no spend. Without this, a
|
|
389
|
+
// `worca --prompt … < /dev/null` reached the first clarify question, printed
|
|
390
|
+
// `Failed to read answer: readline was closed` and exited 0 with the row left
|
|
391
|
+
// `running` — a CI job read success on an abandoned run.
|
|
392
|
+
if (!flags.auto && !stdinCanAnswer()) {
|
|
393
|
+
fail('stdin cannot answer prompts (it is /dev/null or closed) — pass --yes for a non-interactive run.');
|
|
394
|
+
}
|
|
356
395
|
const rl = flags.auto ? null : makeRl();
|
|
357
396
|
let answering = false; // serialize interactive prompts vs. log rendering
|
|
397
|
+
let answerFailure = null; // a question we could not answer -> non-zero exit
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Abandon a prompt we cannot answer: stop the run and force a non-zero exit, so
|
|
401
|
+
* the row never stays `running` and no caller reads success off an abandoned run.
|
|
402
|
+
* A pause/stop already in flight owns the outcome — Ctrl+C must still end
|
|
403
|
+
* `paused`, never `stopped`.
|
|
404
|
+
*/
|
|
405
|
+
const abandonAnswer = (err) => {
|
|
406
|
+
if (orch.pauseRequested || !orch.state || orch.state.status !== 'running') return;
|
|
407
|
+
answerFailure = err;
|
|
408
|
+
process.stderr.write('worca: cannot continue without an answer — stopping the run. '
|
|
409
|
+
+ 'Use --yes for a non-interactive run.\n');
|
|
410
|
+
// Deferred by ONE microtask, for the same reason answers are: _ask emits
|
|
411
|
+
// `question` BEFORE it parks pendingQuestion, so a SYNCHRONOUS throw in the
|
|
412
|
+
// handler below would reach stop() with nothing parked to reject — and the ask
|
|
413
|
+
// parked a moment later would then hang the run forever, which is the very
|
|
414
|
+
// outcome this guard exists to prevent.
|
|
415
|
+
queueMicrotask(() => {
|
|
416
|
+
if (orch.pauseRequested || !orch.state || orch.state.status !== 'running') return;
|
|
417
|
+
orch.stop();
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// stdin reaching EOF *while a question is open* does not throw: readline simply
|
|
422
|
+
// closes and never invokes the question callback, so the awaited answer never
|
|
423
|
+
// settles, the event loop drains and node exits 0 with the run abandoned. Treat
|
|
424
|
+
// it as a failed answer — but ONLY while the run is still running: our own
|
|
425
|
+
// rl.close() in the finally below also fires with `answering` still true when a
|
|
426
|
+
// prompt was interrupted by Ctrl+C (the pause settles start() first, and the
|
|
427
|
+
// parked rl.question never resolves), and that is not a lost answer.
|
|
428
|
+
if (rl) {
|
|
429
|
+
rl.on('close', () => {
|
|
430
|
+
if (!answering) return;
|
|
431
|
+
if (orch.pauseRequested || !orch.state || orch.state.status !== 'running') return;
|
|
432
|
+
process.stderr.write('Failed to read answer: stdin closed\n');
|
|
433
|
+
abandonAnswer(new Error('stdin closed'));
|
|
434
|
+
});
|
|
435
|
+
}
|
|
358
436
|
|
|
359
437
|
// ── event wiring ──────────────────────────────────────────────────────────────
|
|
360
|
-
|
|
361
|
-
|
|
438
|
+
// The run renders its `exec` stream and nothing else: the v1 `phase` event and
|
|
439
|
+
// its renderer died with the v1 engine, and the preflight/done BOOKENDS are
|
|
440
|
+
// exec rows now (x:preflight:1 / x:done:1) that render nothing.
|
|
441
|
+
orch.on('exec', (ev) => {
|
|
442
|
+
// A user STOP surfaces as `exec error 'aborted'` on the in-flight execution
|
|
443
|
+
// while its ledger row is 'stopped'; the harness prints the stop line itself.
|
|
444
|
+
if (ev.status === 'error' && orch.state && orch.state.status === 'stopped') return;
|
|
445
|
+
// The exec payload carries no durationMs (spec §5.7) — the ledger rows do,
|
|
446
|
+
// and they are final by the time a terminal exec arrives. A composite
|
|
447
|
+
// parent has no row of its own: its slices (parentExecutionId) are summed,
|
|
448
|
+
// time and spend alike; a slice's own event keeps its own cost.
|
|
449
|
+
let e = ev;
|
|
450
|
+
if (ev.status !== 'start') {
|
|
451
|
+
const rows = Array.isArray(orch.state && orch.state.steps) ? orch.state.steps : [];
|
|
452
|
+
// By `executionId`, never by `key` — Task 14's rule for step rows: today's
|
|
453
|
+
// bookends are key-only, and a v1 row's key is a phase name.
|
|
454
|
+
const mine = rows.filter((s) => s && (s.executionId === ev.executionId || s.parentExecutionId === ev.executionId));
|
|
455
|
+
if (mine.length) {
|
|
456
|
+
e = { ...ev, durationMs: mine.reduce((a, s) => a + (s.activeMs || 0), 0) };
|
|
457
|
+
if (ev.kind !== 'task') e.costUsd = mine.reduce((a, s) => a + (s.costUsd || 0), 0);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const line = formatExecLine(e, orch.state && orch.state.stepper, { color: c });
|
|
461
|
+
if (line) out(line);
|
|
362
462
|
});
|
|
363
463
|
|
|
364
464
|
orch.on('log', ({ source, level, text }) => {
|
|
@@ -375,16 +475,24 @@ async function attachAndDrive(orch, flags, start) {
|
|
|
375
475
|
process.stderr.write(c('red', `Error: ${message}`) + '\n');
|
|
376
476
|
});
|
|
377
477
|
|
|
378
|
-
|
|
478
|
+
// The WHOLE payload is kept: a graph run's gate question carries `wireId`,
|
|
479
|
+
// which the header formatter resolves against the manifest.
|
|
480
|
+
orch.on('question', async (payload) => {
|
|
481
|
+
const { id, kind, questions, issues, recovery, agent } = payload;
|
|
379
482
|
if (flags.auto || !rl) return; // auto mode resolves internally
|
|
380
483
|
answering = true;
|
|
381
484
|
try {
|
|
382
485
|
if (kind === 'clarify') {
|
|
383
|
-
const
|
|
384
|
-
orch.answer(id,
|
|
486
|
+
const answer = await askClarify(rl, questions || []);
|
|
487
|
+
orch.answer(id, answer);
|
|
385
488
|
} else if (kind === 'gate') {
|
|
386
|
-
|
|
387
|
-
|
|
489
|
+
// One engine: every gate question is a graph question, so the header is
|
|
490
|
+
// built unconditionally. (The `graphRun()` gate that used to guard this
|
|
491
|
+
// died with the phase listener — leaving the call was a ReferenceError
|
|
492
|
+
// waiting for the first interactive gate.)
|
|
493
|
+
const answer = await askGate(rl, issues || [],
|
|
494
|
+
formatGateHeader(payload, orch.state && orch.state.stepper));
|
|
495
|
+
orch.answer(id, answer);
|
|
388
496
|
} else if (kind === 'recovery') {
|
|
389
497
|
const payload = await askRecovery(rl, recovery);
|
|
390
498
|
orch.answer(id, payload);
|
|
@@ -395,6 +503,13 @@ async function attachAndDrive(orch, flags, start) {
|
|
|
395
503
|
}
|
|
396
504
|
} catch (err) {
|
|
397
505
|
process.stderr.write(`Failed to read answer: ${err?.message || err}\n`);
|
|
506
|
+
// Never swallow: orch.answer() was not called, so the ask stays open and the
|
|
507
|
+
// run would hang on it (or be abandoned at EOF with its row left `running`
|
|
508
|
+
// while node exits 0). This is also the arm any THROW inside askClarify /
|
|
509
|
+
// askGate / askRecovery lands in — the shape the P6 graphRun() ReferenceError
|
|
510
|
+
// took — so failing loudly here is what turns that class of bug into a
|
|
511
|
+
// visible failure instead of a silent hang.
|
|
512
|
+
abandonAnswer(err);
|
|
398
513
|
} finally {
|
|
399
514
|
answering = false;
|
|
400
515
|
}
|
|
@@ -434,6 +549,13 @@ async function attachAndDrive(orch, flags, start) {
|
|
|
434
549
|
out('');
|
|
435
550
|
if (result?.status === 'done') {
|
|
436
551
|
out(c('green', c('bold', 'Pipeline complete.')));
|
|
552
|
+
// v2 runs: `Result: <path|value>` (or the amber quiescence line), then
|
|
553
|
+
// `N executions · <active> active · $<cost>`; [] on a v1 run.
|
|
554
|
+
const summary = formatRunSummary(orch.state);
|
|
555
|
+
if (summary.length) {
|
|
556
|
+
out(summary[0].startsWith('Finished at quiescence') ? c('yellow', summary[0]) : summary[0]);
|
|
557
|
+
for (const line of summary.slice(1)) out(line);
|
|
558
|
+
}
|
|
437
559
|
} else if (result?.status === 'paused') {
|
|
438
560
|
out(c('yellow', result?.reason ? `Pipeline paused: ${result.reason}` : 'Pipeline paused.'));
|
|
439
561
|
out(`Resume with: ${c('bold', `worca resume ${orch.state.id}`)}`);
|
|
@@ -445,6 +567,8 @@ async function attachAndDrive(orch, flags, start) {
|
|
|
445
567
|
if (result?.pipelineDir) {
|
|
446
568
|
out(`Pipeline directory: ${c('bold', result.pipelineDir)}`);
|
|
447
569
|
}
|
|
570
|
+
// An unanswered question is a failure even if the run somehow settled `done`.
|
|
571
|
+
if (answerFailure) return 1;
|
|
448
572
|
return result?.status === 'done' || result?.status === 'paused' ? 0 : 1;
|
|
449
573
|
}
|
|
450
574
|
|
|
@@ -570,6 +694,13 @@ async function cmdDoctor() {
|
|
|
570
694
|
} catch (err) {
|
|
571
695
|
process.stderr.write(`worca doctor: reconcile failed: ${err?.message || err}\n`);
|
|
572
696
|
}
|
|
697
|
+
try {
|
|
698
|
+
const { sweepV1Runs } = await import('../core/db.mjs');
|
|
699
|
+
const swept = sweepV1Runs();
|
|
700
|
+
if (swept.length) out(`retired ${swept.length} run(s) paused on the v1 engine`);
|
|
701
|
+
} catch (err) {
|
|
702
|
+
process.stderr.write(`worca doctor: v1-run sweep failed: ${err?.message || err}\n`);
|
|
703
|
+
}
|
|
573
704
|
try {
|
|
574
705
|
// The injected callbacks THROW on a DB failure instead of reporting "no row"
|
|
575
706
|
// (artifacts.mjs#runRootSweepLookups); the sweep records each throw in `failed`
|
|
@@ -589,6 +720,16 @@ async function cmdDoctor() {
|
|
|
589
720
|
} catch (err) {
|
|
590
721
|
process.stderr.write(`worca doctor: run-root sweep failed: ${err?.message || err}\n`);
|
|
591
722
|
}
|
|
723
|
+
// P4: BEFORE the legacy return 0 — that block short-circuits the whole function
|
|
724
|
+
// whenever the effective mode is not `detached` (the default), so an ask-worktree
|
|
725
|
+
// sweep appended after it would never run for most users.
|
|
726
|
+
try {
|
|
727
|
+
const { sweepAskWorktrees } = await import('../core/ask/worktrees.mjs');
|
|
728
|
+
const res = await sweepAskWorktrees({ log: (level, msg) => out(level === 'warn' ? c('yellow', msg) : msg) });
|
|
729
|
+
out(`ask worktrees: removed ${res.removedDirs} orphan dir(s), dropped ${res.prunedRows} stale row(s), skipped ${res.failed}`);
|
|
730
|
+
} catch (err) {
|
|
731
|
+
process.stderr.write(`worca doctor: ask-worktree sweep failed: ${err?.message || err}\n`);
|
|
732
|
+
}
|
|
592
733
|
try {
|
|
593
734
|
// A TOTAL no-op while the effective mode is `legacy`: those paths hold every live
|
|
594
735
|
// and every paused run, so sweeping them would make the documented §10 rollback
|
|
@@ -725,6 +866,19 @@ async function cmdResume(argv) {
|
|
|
725
866
|
process.stderr.write(`pipeline ${id} has no resume point\n`);
|
|
726
867
|
return 1;
|
|
727
868
|
}
|
|
869
|
+
if (saved.resumePoint.version !== 2) {
|
|
870
|
+
const { V1_RUN_RETIRED } = await import('../core/db.mjs');
|
|
871
|
+
process.stderr.write(`worca resume: ${V1_RUN_RETIRED}\n`);
|
|
872
|
+
return 2;
|
|
873
|
+
}
|
|
874
|
+
// The v1 sweep runs AFTER this run's own guards: sweeping FIRST would NULL the
|
|
875
|
+
// point under test, so the caller would read "has no resume point" instead of
|
|
876
|
+
// the honest retirement message above.
|
|
877
|
+
try {
|
|
878
|
+
const { sweepV1Runs } = await import('../core/db.mjs');
|
|
879
|
+
const swept = sweepV1Runs();
|
|
880
|
+
if (swept.length) out(`retired ${swept.length} run(s) paused on the v1 engine`);
|
|
881
|
+
} catch { /* best-effort: resume still works if the sweep fails */ }
|
|
728
882
|
if (saved.row.archived_at) {
|
|
729
883
|
process.stderr.write('worca resume: pipeline is archived\n');
|
|
730
884
|
return 1;
|
|
@@ -781,7 +935,7 @@ async function cmdResume(argv) {
|
|
|
781
935
|
return 1;
|
|
782
936
|
}
|
|
783
937
|
|
|
784
|
-
const orch =
|
|
938
|
+
const orch = await createOrchestratorFor({
|
|
785
939
|
projectDir,
|
|
786
940
|
...(workspace ? { workspace } : {}),
|
|
787
941
|
claude: { mock },
|
|
@@ -808,9 +962,10 @@ Usage:
|
|
|
808
962
|
worca plugin enable <name> | disable <name> Toggle without removing files
|
|
809
963
|
worca plugin doctor [name] [--fix] Health checks (--fix re-runs deterministic setup on failure)
|
|
810
964
|
worca plugin link <dir> Dev mode: use a local dir as "current"
|
|
965
|
+
worca plugin reimport <name> Re-read the plugin's pipeline templates (a linked dir is live-edited)
|
|
811
966
|
worca plugin init <name> [--dir <D>] [--with task-source,agents,skills,workflows]
|
|
812
967
|
worca plugin validate <dir> [--strict] Lint a plugin dir (--strict: unknown fields error)
|
|
813
|
-
worca plugin exec <name> <sourceId> <op> [--args '<json>'] [--inspect] Debug one connector op
|
|
968
|
+
worca plugin exec <name> <sourceId> <op> [--args '<json>'] [--profile <id>] [--inspect] Debug one connector op
|
|
814
969
|
worca plugin channel <name> <channelId> [--check] [--inspect] Run a chat channel worker in the
|
|
815
970
|
foreground (typed lines = simulated inbound); --check runs
|
|
816
971
|
the module's validateConfig once and exits
|
|
@@ -907,6 +1062,15 @@ function printInventory(inv) {
|
|
|
907
1062
|
for (const cmd of i.setupCommands || []) out(` setup: ${cmd}`);
|
|
908
1063
|
}
|
|
909
1064
|
|
|
1065
|
+
/** Contributions worca refused to load (spec §9.3): one yellow line each, so a
|
|
1066
|
+
* receipt or a list never claims an agent/template that exists nowhere. */
|
|
1067
|
+
function printIgnored(ignored) {
|
|
1068
|
+
const list = Array.isArray(ignored) ? ignored : [];
|
|
1069
|
+
if (!list.length) return;
|
|
1070
|
+
out(c('yellow', ` ${list.length} contribution${list.length > 1 ? 's' : ''} ignored:`));
|
|
1071
|
+
for (const i of list) out(c('yellow', ` ${i.file} — ${i.reason}`));
|
|
1072
|
+
}
|
|
1073
|
+
|
|
910
1074
|
/** kebab plugin name -> camelCase stem for the scaffolded example agent key. */
|
|
911
1075
|
function camelizePluginName(name) {
|
|
912
1076
|
return name.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase());
|
|
@@ -942,8 +1106,8 @@ async function pluginInit(rest) {
|
|
|
942
1106
|
const manifestObj = {
|
|
943
1107
|
name,
|
|
944
1108
|
version: '0.1.0',
|
|
945
|
-
description: 'Scaffolded worca
|
|
946
|
-
engines: { 'worca-cc-api': '>=
|
|
1109
|
+
description: 'Scaffolded worca plugin — edit me',
|
|
1110
|
+
engines: { 'worca-cc-api': '>=3 <4' },
|
|
947
1111
|
};
|
|
948
1112
|
if (withParts.includes('task-source')) {
|
|
949
1113
|
manifestObj.taskSources = [{
|
|
@@ -994,14 +1158,15 @@ async function pluginInit(rest) {
|
|
|
994
1158
|
}
|
|
995
1159
|
if (withParts.includes('agents')) {
|
|
996
1160
|
files.set(`agents/${agentKey}.meta.json`, JSON.stringify({
|
|
1161
|
+
metaVersion: 2,
|
|
997
1162
|
key: agentKey,
|
|
998
1163
|
displayName: 'Example Helper',
|
|
999
1164
|
description: `Example agent installed by the ${name} plugin`,
|
|
1000
1165
|
color: 'amber',
|
|
1001
1166
|
agentFile: `${agentKey}.md`,
|
|
1002
1167
|
runnerType: 'producer',
|
|
1003
|
-
|
|
1004
|
-
|
|
1168
|
+
inputs: [{ id: 'task', type: 'md', required: true }],
|
|
1169
|
+
outputs: [{ id: 'notes', type: 'md', filename: 'notes.md', store: 'run' }],
|
|
1005
1170
|
...(withParts.includes('skills') ? { requiresSkills: ['example-skill'] } : {}),
|
|
1006
1171
|
order: 900,
|
|
1007
1172
|
}, null, 2) + '\n');
|
|
@@ -1013,7 +1178,7 @@ async function pluginInit(rest) {
|
|
|
1013
1178
|
'model: inherit',
|
|
1014
1179
|
'---',
|
|
1015
1180
|
'',
|
|
1016
|
-
`You are an example agent shipped by the "${name}" worca
|
|
1181
|
+
`You are an example agent shipped by the "${name}" worca plugin.`,
|
|
1017
1182
|
'Acknowledge the task you were given and describe what a real agent would do here.',
|
|
1018
1183
|
'',
|
|
1019
1184
|
].join('\n'));
|
|
@@ -1033,12 +1198,21 @@ async function pluginInit(rest) {
|
|
|
1033
1198
|
files.set('skills/example-skill/helper.sh', '#!/bin/sh\necho "example-skill helper ok"\n');
|
|
1034
1199
|
}
|
|
1035
1200
|
if (withParts.includes('workflows')) {
|
|
1201
|
+
// A v2 graph: the Task and End cards are mandatory (V20/V21) and every input
|
|
1202
|
+
// takes exactly one wire (V7). Ports come from the sidecar above.
|
|
1036
1203
|
files.set('workflows/example-flow.json', JSON.stringify({
|
|
1037
1204
|
name: `${name} example flow`,
|
|
1038
|
-
version:
|
|
1205
|
+
version: 2,
|
|
1039
1206
|
domain: 'general',
|
|
1040
|
-
|
|
1041
|
-
|
|
1207
|
+
nodes: [
|
|
1208
|
+
{ id: 'n_task', kind: 'task', x: 40, y: 200, config: {} },
|
|
1209
|
+
{ id: 'n_helper', kind: 'agent', key: agentKey, x: 320, y: 200, config: {} },
|
|
1210
|
+
{ id: 'n_end', kind: 'end', x: 600, y: 200, config: {} },
|
|
1211
|
+
],
|
|
1212
|
+
wires: [
|
|
1213
|
+
{ id: 'w1', from: { node: 'n_task', port: 'task' }, to: { node: 'n_helper', port: 'task' } },
|
|
1214
|
+
{ id: 'w2', from: { node: 'n_helper', port: 'notes' }, to: { node: 'n_end', port: 'result' } },
|
|
1215
|
+
],
|
|
1042
1216
|
}, null, 2) + '\n');
|
|
1043
1217
|
}
|
|
1044
1218
|
files.set('worca-cc-plugin.json', JSON.stringify(manifestObj, null, 2) + '\n');
|
|
@@ -1147,6 +1321,7 @@ async function cmdPlugin(argv) {
|
|
|
1147
1321
|
const res = await store.installPlugin({ repoUrl, subdir: entry.subdir, name, sha, ...(marketplace ? { marketplace } : {}) });
|
|
1148
1322
|
out('installed:');
|
|
1149
1323
|
printInventory(res.inventory);
|
|
1324
|
+
printIgnored(res.ignored);
|
|
1150
1325
|
return 0;
|
|
1151
1326
|
}
|
|
1152
1327
|
|
|
@@ -1160,6 +1335,8 @@ async function cmdPlugin(argv) {
|
|
|
1160
1335
|
const version = p.linked ? 'linked' : p.version || (p.pinnedSha || '').slice(0, 7);
|
|
1161
1336
|
const flags = [p.enabled ? 'enabled' : 'disabled', ...(p.linked ? ['linked'] : [])].join(', ');
|
|
1162
1337
|
out(`${p.name}\t${version}\t${flags}\t${contribSummary(p.contributions)}`);
|
|
1338
|
+
if (p.apiMismatch) out(c('yellow', ` ${p.apiMismatch.message}`));
|
|
1339
|
+
printIgnored(p.ignored);
|
|
1163
1340
|
}
|
|
1164
1341
|
return 0;
|
|
1165
1342
|
}
|
|
@@ -1259,12 +1436,27 @@ async function cmdPlugin(argv) {
|
|
|
1259
1436
|
if (!dir) fail('Usage: worca plugin link <dir>');
|
|
1260
1437
|
const abs = resolve(process.cwd(), dir);
|
|
1261
1438
|
const v = manifestMod.validatePluginDir(abs);
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1439
|
+
// Print EVERY level, pass or fail: a link that SUCCEEDS with warnings is
|
|
1440
|
+
// the mid-migration case the author most needs to read (MAJ-12) — an
|
|
1441
|
+
// API-1 plugin keeps linking, and now says why its agent is ignored.
|
|
1442
|
+
for (const p of v.problems) process.stderr.write(`${p.level}: ${p.message}\n`);
|
|
1443
|
+
if (!v.ok) return 2;
|
|
1444
|
+
const linked = await store.linkPlugin(v.manifest.name, abs);
|
|
1267
1445
|
out(`linked ${v.manifest.name} -> ${abs} (dev mode; doctor will warn)`);
|
|
1446
|
+
const n = linked.workflows.imported.length;
|
|
1447
|
+
if (n) out(` imported ${n} pipeline template${n === 1 ? '' : 's'} — edits to them need: worca plugin reimport ${v.manifest.name}`);
|
|
1448
|
+
printIgnored(store.ignoredContributions(v.manifest.name, abs, { workflowSkips: linked.workflows.skipped }));
|
|
1449
|
+
return 0;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
case 'reimport': {
|
|
1453
|
+
const a = pluginArgs(rest);
|
|
1454
|
+
const name = a._[0];
|
|
1455
|
+
if (!name) fail('Usage: worca plugin reimport <name>');
|
|
1456
|
+
const r = await store.reimportPlugin(name);
|
|
1457
|
+
const n = r.workflows.imported.length;
|
|
1458
|
+
out(`reimported ${name}: ${n} pipeline template${n === 1 ? '' : 's'}`);
|
|
1459
|
+
printIgnored(r.ignored);
|
|
1268
1460
|
return 0;
|
|
1269
1461
|
}
|
|
1270
1462
|
|
|
@@ -1286,9 +1478,9 @@ async function cmdPlugin(argv) {
|
|
|
1286
1478
|
}
|
|
1287
1479
|
|
|
1288
1480
|
case 'exec': {
|
|
1289
|
-
const a = pluginArgs(rest, ['--args'], ['--inspect']);
|
|
1481
|
+
const a = pluginArgs(rest, ['--args', '--profile'], ['--inspect']);
|
|
1290
1482
|
const [name, sourceId, op] = a._;
|
|
1291
|
-
if (!name || !sourceId || !op) fail("Usage: worca plugin exec <name> <sourceId> <op> [--args '<json>'] [--inspect]");
|
|
1483
|
+
if (!name || !sourceId || !op) fail("Usage: worca plugin exec <name> <sourceId> <op> [--args '<json>'] [--profile <id>] [--inspect]");
|
|
1292
1484
|
if (a.inspect) process.env.WORCA_PLUGIN_INSPECT = '1'; // shim spawns the child with --inspect-brk
|
|
1293
1485
|
let args = {};
|
|
1294
1486
|
if (a.args) {
|
|
@@ -1299,7 +1491,9 @@ async function cmdPlugin(argv) {
|
|
|
1299
1491
|
}
|
|
1300
1492
|
}
|
|
1301
1493
|
const { callSource } = await import('../core/plugin-shim.mjs');
|
|
1302
|
-
|
|
1494
|
+
// --profile targets one instance of a multi-profile source; absent, the
|
|
1495
|
+
// shim falls back to the implicit default bucket (single-profile case).
|
|
1496
|
+
const result = await callSource({ plugin: name, sourceId, op, args, profile: a.profile || undefined });
|
|
1303
1497
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n'); // stdout = result ONLY
|
|
1304
1498
|
return 0;
|
|
1305
1499
|
}
|
|
@@ -1431,8 +1625,51 @@ async function cmdMarketplace(argv) {
|
|
|
1431
1625
|
|
|
1432
1626
|
const SUBCOMMANDS = new Set(['add', 'list', 'remove', 'resume', 'doctor', 'plugin', 'marketplace', 'config']);
|
|
1433
1627
|
|
|
1628
|
+
/** Levenshtein distance, two-row. Only ever called on short argv tokens. */
|
|
1629
|
+
function editDistance(a, b) {
|
|
1630
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
1631
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1632
|
+
const row = [i];
|
|
1633
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1634
|
+
row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1635
|
+
}
|
|
1636
|
+
prev = row;
|
|
1637
|
+
}
|
|
1638
|
+
return prev[b.length];
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* The subcommand a lone positional was probably meant to be, or null.
|
|
1643
|
+
*
|
|
1644
|
+
* A bare positional is a legal prompt (`worca "do the thing"`), so only a SINGLE
|
|
1645
|
+
* whitespace-free token that near-misses a real subcommand counts as a typo: edit
|
|
1646
|
+
* distance <= 2, or a strict prefix of at least 3 characters (`plug` -> `plugin`).
|
|
1647
|
+
* Refusing costs one retry with --prompt; running `worca reusme run-abc123` cuts a
|
|
1648
|
+
* worktree + feature branch and spends real tokens on a task named "reusme".
|
|
1649
|
+
*/
|
|
1650
|
+
function nearestSubcommand(token) {
|
|
1651
|
+
if (!token || /\s/.test(token)) return null;
|
|
1652
|
+
// 'help' is spliced into both loops: it is a real CLI arm (the head of main())
|
|
1653
|
+
// but deliberately absent from the dispatch table, so without it a typo of help
|
|
1654
|
+
// itself (`worca hlep`) is distance >= 4 from everything and runs as a PROMPT.
|
|
1655
|
+
for (const name of [...SUBCOMMANDS, 'help']) {
|
|
1656
|
+
if (token.length >= 3 && name.length > token.length && name.startsWith(token)) return name;
|
|
1657
|
+
}
|
|
1658
|
+
let best = null;
|
|
1659
|
+
let bestD = 3; // strictly less than 3 == distance <= 2
|
|
1660
|
+
for (const name of [...SUBCOMMANDS, 'help']) {
|
|
1661
|
+
const d = editDistance(token, name);
|
|
1662
|
+
if (d < bestD) { bestD = d; best = name; }
|
|
1663
|
+
}
|
|
1664
|
+
return best;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1434
1667
|
async function main() {
|
|
1435
1668
|
const sub = process.argv[2];
|
|
1669
|
+
// `worca help` is what every CLI user types first; it is not a subcommand and
|
|
1670
|
+
// not a near-miss of one, so without this line it became a PROMPT and ran a
|
|
1671
|
+
// pipeline named "help" (MIN-51).
|
|
1672
|
+
if (sub === 'help') { process.stdout.write(HELP); return 0; }
|
|
1436
1673
|
if (SUBCOMMANDS.has(sub)) {
|
|
1437
1674
|
const rest = process.argv.slice(3);
|
|
1438
1675
|
if (sub === 'add') return cmdAdd(rest);
|
|
@@ -1466,10 +1703,23 @@ async function main() {
|
|
|
1466
1703
|
if (flags.mock) {
|
|
1467
1704
|
process.env.WORCA_MOCK = '1';
|
|
1468
1705
|
}
|
|
1706
|
+
// The mock runner routes EVERY dontAsk spawn to the Ask Worca mock (claude-runner.mjs
|
|
1707
|
+
// runMock), which writes no pipeline artifact — a mock pipeline under dontAsk dies
|
|
1708
|
+
// at its first artifact read with no hint why. Refuse the PAIR, not the mode.
|
|
1709
|
+
if (flags.permissionMode === 'dontAsk' && /^(1|true|yes|on)$/i.test(String(process.env.WORCA_MOCK ?? process.env.ORCH_MOCK ?? ''))) {
|
|
1710
|
+
fail('--permission-mode dontAsk cannot be combined with --mock: the mock runner reserves it for the Ask Worca assistant.');
|
|
1711
|
+
}
|
|
1469
1712
|
|
|
1470
1713
|
if (!flags.prompt && !flags.file) {
|
|
1471
|
-
// Allow a bare positional prompt: `worca "do the thing"`.
|
|
1714
|
+
// Allow a bare positional prompt: `worca "do the thing"`. A lone token that
|
|
1715
|
+
// near-misses a subcommand is a typo, not a task — refuse it here, before a
|
|
1716
|
+
// pipeline row, a worktree or a feature branch exists.
|
|
1472
1717
|
if (flags._.length) {
|
|
1718
|
+
const meant = nearestSubcommand(flags._[0]);
|
|
1719
|
+
if (meant) {
|
|
1720
|
+
if (meant === flags._[0]) fail(`"${meant}" is a subcommand and must come first: worca ${meant} [args] (to run a prompt with that word, use --prompt "\u2026")`);
|
|
1721
|
+
fail(`unknown subcommand "${flags._[0]}" — did you mean "${meant}"? (to run a prompt, use --prompt "\u2026")`);
|
|
1722
|
+
}
|
|
1473
1723
|
flags.prompt = flags._.join(' ');
|
|
1474
1724
|
} else {
|
|
1475
1725
|
fail('Provide a task with --prompt "<text>" or --file <markdown>. See --help.');
|
|
@@ -1477,6 +1727,19 @@ async function main() {
|
|
|
1477
1727
|
}
|
|
1478
1728
|
|
|
1479
1729
|
const projectDir = resolve(flags.project);
|
|
1730
|
+
// A NAMED --file must be readable BEFORE anything starts. The readers used to
|
|
1731
|
+
// swallow the failure and run the whole pipeline on an empty prompt with exit 0
|
|
1732
|
+
// — in real mode that spends tokens and cuts a worktree + feature branch for
|
|
1733
|
+
// nothing. Relative paths resolve against the PROJECT dir, exactly as the
|
|
1734
|
+
// orchestrator's own read does.
|
|
1735
|
+
if (flags.file) {
|
|
1736
|
+
const { readPromptFile } = await import('../core/artifacts.mjs');
|
|
1737
|
+
try {
|
|
1738
|
+
await readPromptFile(projectDir, flags.file);
|
|
1739
|
+
} catch (err) {
|
|
1740
|
+
fail(err && err.message ? err.message : String(err));
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1480
1743
|
// Resolve extras against the shell cwd so relative paths are unambiguous.
|
|
1481
1744
|
const extras = (flags.extras || []).map((p) => resolve(process.cwd(), p));
|
|
1482
1745
|
|
|
@@ -1490,13 +1753,24 @@ async function main() {
|
|
|
1490
1753
|
return 1;
|
|
1491
1754
|
}
|
|
1492
1755
|
|
|
1493
|
-
|
|
1756
|
+
// Validate --workflow before spawning anything: an unknown or archived template
|
|
1757
|
+
// must fail with one line, not a stack trace half-way through a run. The read row
|
|
1758
|
+
// doubles as createOrchestratorFor's routing hint (it skips a second row read).
|
|
1759
|
+
let row;
|
|
1760
|
+
if (flags.workflow) {
|
|
1761
|
+
const { assertRunnableWorkflow } = await import('../core/workflows.mjs');
|
|
1762
|
+
try { row = await assertRunnableWorkflow(flags.workflow); }
|
|
1763
|
+
catch (err) { fail(`${err && err.message ? err.message : String(err)}`); }
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const orch = await createOrchestratorFor({
|
|
1494
1767
|
projectDir,
|
|
1495
1768
|
prompt: flags.prompt || undefined,
|
|
1496
1769
|
promptFile: flags.file || undefined,
|
|
1497
1770
|
title: flags.title || undefined,
|
|
1498
1771
|
extras,
|
|
1499
1772
|
workflowId: flags.workflow || undefined,
|
|
1773
|
+
template: row,
|
|
1500
1774
|
branch: { source: flags.sourceBranch, feature: flags.featureBranch },
|
|
1501
1775
|
claude: {
|
|
1502
1776
|
permissionMode: flags.permissionMode,
|