@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.
- package/README.md +30 -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 +386 -56
- 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 +199 -23
- package/src/core/ask/attachment-kind.mjs +95 -0
- package/src/core/ask/catalog.mjs +111 -0
- package/src/core/ask/comment-deps.mjs +55 -0
- package/src/core/ask/events.mjs +545 -0
- package/src/core/ask/follow.mjs +113 -0
- package/src/core/ask/git-allowlist.mjs +226 -0
- package/src/core/ask/limits.mjs +57 -0
- package/src/core/ask/mcp-stdio.mjs +135 -0
- package/src/core/ask/models.mjs +125 -0
- package/src/core/ask/prompt.mjs +286 -0
- package/src/core/ask/proposal.mjs +170 -0
- package/src/core/ask/redact.mjs +30 -0
- package/src/core/ask/spawn.mjs +156 -0
- package/src/core/ask/store.mjs +438 -0
- package/src/core/ask/tool-deps.mjs +87 -0
- package/src/core/ask/tools.mjs +879 -0
- package/src/core/ask/turn.mjs +462 -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 +28 -7
- package/src/core/chat/notifier.mjs +6 -1
- package/src/core/chat/renderers.mjs +15 -8
- package/src/core/claude-runner.mjs +541 -62
- package/src/core/config.mjs +310 -44
- package/src/core/cost-budget.mjs +29 -2
- package/src/core/db.mjs +773 -53
- 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/failure-policy.mjs +201 -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 +1072 -0
- package/src/core/graph/seed-templates.mjs +318 -0
- package/src/core/host-guard.mjs +271 -0
- package/src/core/model-env.mjs +180 -8
- package/src/core/model-test.mjs +79 -0
- package/src/core/orchestrator.mjs +994 -4097
- 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 +80 -17
- 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 +3934 -0
- package/src/core/run-manifest.mjs +5 -1
- package/src/core/settings.mjs +184 -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 +4240 -1682
- package/ui/public/ask-markdown.mjs +145 -0
- package/ui/public/ask-model.mjs +317 -0
- package/ui/public/ask-panel.mjs +2129 -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 +311 -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 +1487 -229
- package/ui/public/syntax-highlight.mjs +270 -0
- package/ui/public/thinking-orb.mjs +110 -0
- package/ui/server.mjs +1894 -104
- 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
|
@@ -4,18 +4,21 @@
|
|
|
4
4
|
// CLI entry point. Parses flags, creates a core orchestrator, subscribes to its events,
|
|
5
5
|
// renders a phase tracker + streamed agent logs to the terminal, and drives interactive
|
|
6
6
|
// Q&A (clarify) and loop gates via node:readline. Supports --yes (auto), --mock,
|
|
7
|
-
// --install <dir> (delegates to scripts/install.mjs),
|
|
7
|
+
// --install <dir> (delegates to scripts/install.mjs), --ui (spawns ui/server.mjs),
|
|
8
|
+
// and -v/-V/--version (also the bare word `version`).
|
|
8
9
|
//
|
|
9
10
|
// ESM, no external dependencies.
|
|
10
11
|
|
|
11
12
|
import { createInterface } from 'node:readline';
|
|
12
13
|
import { spawn } from 'node:child_process';
|
|
14
|
+
import { fstatSync } from 'node:fs';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
13
16
|
import { fileURLToPath } from 'node:url';
|
|
14
17
|
import { dirname, resolve, join, basename } from 'node:path';
|
|
15
18
|
import process from 'node:process';
|
|
16
19
|
|
|
17
20
|
import { preflightNode } from '../core/preflight-node.mjs';
|
|
18
|
-
import {
|
|
21
|
+
import { createOrchestratorFor } from '../core/engine-select.mjs';
|
|
19
22
|
import {
|
|
20
23
|
addProject,
|
|
21
24
|
listProjects,
|
|
@@ -23,6 +26,8 @@ import {
|
|
|
23
26
|
normalizeProjectPath,
|
|
24
27
|
} from '../core/projects.mjs';
|
|
25
28
|
import { projectKey } from '../core/store.mjs';
|
|
29
|
+
import { formatExecLine, formatGateHeader, formatRunSummary } from './render.mjs';
|
|
30
|
+
import { pauseExitCode, describePauseReason, promptOptions, REASON } from '../core/failure-policy.mjs';
|
|
26
31
|
|
|
27
32
|
// ── node:sqlite runtime guard + warning filter ──────────────────────────────────
|
|
28
33
|
// Drop ONLY the one-time ExperimentalWarning emitted by node:sqlite (the module is
|
|
@@ -37,6 +42,18 @@ process.on('warning', (w) => {
|
|
|
37
42
|
if (w && w.name === 'ExperimentalWarning' && /SQLite/i.test(w.message)) return;
|
|
38
43
|
process.stderr.write(`${w?.stack || w?.message || w}\n`);
|
|
39
44
|
});
|
|
45
|
+
// ── --version ──────────────────────────────────────────────────────────────────
|
|
46
|
+
// Answered BEFORE the Node preflight and before any flag validation: "which worca is
|
|
47
|
+
// this?" is the first question asked when something else is broken, so it must work
|
|
48
|
+
// on an unsupported Node and alongside an otherwise-bad command line. The bare word
|
|
49
|
+
// `version` is only honoured in the subcommand slot (like `help`); the flags anywhere.
|
|
50
|
+
// Output is the GNU/gh/go form, `<prog> <semver>`, on stdout, exit 0.
|
|
51
|
+
const PKG_VERSION = createRequire(import.meta.url)('../../package.json').version;
|
|
52
|
+
const VERSION_FLAGS = new Set(['-v', '-V', '--version']);
|
|
53
|
+
if (process.argv[2] === 'version' || process.argv.slice(2).some((a) => VERSION_FLAGS.has(a))) {
|
|
54
|
+
process.stdout.write(`worca ${PKG_VERSION}\n`);
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
40
57
|
// Fail fast on an unsupported Node / missing node:sqlite BEFORE any DB is opened.
|
|
41
58
|
preflightNode();
|
|
42
59
|
|
|
@@ -46,9 +63,18 @@ const REPO_ROOT = resolve(__dirname, '..', '..');
|
|
|
46
63
|
|
|
47
64
|
// ── arg parsing ────────────────────────────────────────────────────────────────
|
|
48
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The permission modes a pipeline run may be launched with. Deliberately NOT the
|
|
68
|
+
* full set claude accepts: `dontAsk` belongs to the Ask Worca runner alone
|
|
69
|
+
* (core/ask/spawn.mjs), which owns its own spawn options and never comes through
|
|
70
|
+
* here.
|
|
71
|
+
*/
|
|
72
|
+
const PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'bypassPermissions', 'dontAsk'];
|
|
73
|
+
|
|
49
74
|
/**
|
|
50
75
|
* Parse argv into a flags object. Supports "--flag value" and "--flag=value", plus the
|
|
51
|
-
* boolean flags --mock, --yes/--non-interactive, --ui, -h/--help.
|
|
76
|
+
* boolean flags --mock, --yes/--non-interactive, --ui, -h/--help. (-v/-V/--version
|
|
77
|
+
* never reach here: they are answered at module top, before the Node preflight.)
|
|
52
78
|
*/
|
|
53
79
|
function parseArgs(argv) {
|
|
54
80
|
const out = {
|
|
@@ -134,6 +160,12 @@ function parseArgs(argv) {
|
|
|
134
160
|
const p = part.trim();
|
|
135
161
|
if (p) out.extras.push(p);
|
|
136
162
|
}
|
|
163
|
+
} else if (key === 'permissionMode' && !PERMISSION_MODES.includes(String(value))) {
|
|
164
|
+
// A pipeline run's mode reaches claude-runner as-is. `dontAsk` is a legitimate
|
|
165
|
+
// headless mode for a REAL run (allowedTools decide what runs); only the
|
|
166
|
+
// MOCK runner treats it as the Ask Worca recipe — that pair is refused below,
|
|
167
|
+
// after --mock/WORCA_MOCK are known (review of PR #376).
|
|
168
|
+
fail(`--permission-mode must be one of ${PERMISSION_MODES.join(', ')}, got: ${value}`);
|
|
137
169
|
} else {
|
|
138
170
|
out[key] = value;
|
|
139
171
|
}
|
|
@@ -165,12 +197,13 @@ function budgetRefusalDetail(b) {
|
|
|
165
197
|
+ `resets ${when} (in ${days}d ${hours}h)`;
|
|
166
198
|
}
|
|
167
199
|
|
|
168
|
-
const HELP = `worca —
|
|
200
|
+
const HELP = `worca — node-graph multi-agent pipelines
|
|
169
201
|
|
|
170
202
|
Usage:
|
|
171
203
|
worca <subcommand> [args]
|
|
172
204
|
worca --prompt "<task>" [--project <dir>] [options]
|
|
173
205
|
worca --file <task.md> [--project <dir>] [options]
|
|
206
|
+
worca "<task>" [--project <dir>] [options] (bare prompt; quote it)
|
|
174
207
|
worca --ui
|
|
175
208
|
worca --install <targetDir> [--force]
|
|
176
209
|
|
|
@@ -182,9 +215,11 @@ Subcommands:
|
|
|
182
215
|
[--ignore-cost-cap] Resume past this pipeline's cost cap (persists on the run).
|
|
183
216
|
doctor Reconcile crashed runs and sweep leftover run roots.
|
|
184
217
|
plugin <cmd> [...] Manage plugins: add|install|list|update|remove|purge|enable|
|
|
185
|
-
disable|doctor|link|init|validate|exec. See: worca plugin help
|
|
218
|
+
disable|doctor|link|reimport|init|validate|exec. See: worca plugin help
|
|
186
219
|
marketplace <cmd> [...] Manage plugin marketplaces: add|list|refresh|remove. See: worca marketplace help
|
|
187
220
|
config [get|set|unset] Budget & cost-limit settings
|
|
221
|
+
help Print this help (same as --help).
|
|
222
|
+
version Print the version (same as --version).
|
|
188
223
|
|
|
189
224
|
Options:
|
|
190
225
|
--project <dir> Target project directory (default: cwd)
|
|
@@ -194,8 +229,9 @@ Options:
|
|
|
194
229
|
--extras <paths> Extra files copied into the pipeline's extras/ folder
|
|
195
230
|
(comma-separated; repeatable)
|
|
196
231
|
--model <m> Claude model id
|
|
197
|
-
--permission-mode <m> Claude permission mode
|
|
198
|
-
|
|
232
|
+
--permission-mode <m> Claude permission mode: default | acceptEdits | plan |
|
|
233
|
+
bypassPermissions (default acceptEdits)
|
|
234
|
+
--workflow <id> Saved pipeline template to run (default: wf_default — the built-in graph)
|
|
199
235
|
--source-branch <name> Branch to fork the per-run worktree from (default: current HEAD)
|
|
200
236
|
--branch <name> Feature branch name (default: claude proposes one)
|
|
201
237
|
--mock Offline mock mode (no claude, no tokens)
|
|
@@ -203,6 +239,7 @@ Options:
|
|
|
203
239
|
--ui Launch the web UI (ui/server.mjs) and exit
|
|
204
240
|
--install <targetDir> Copy agents + /worca skill into <targetDir>/.claude
|
|
205
241
|
-h, --help Show this help
|
|
242
|
+
-v, -V, --version Print the version (worca <semver>) and exit
|
|
206
243
|
`;
|
|
207
244
|
|
|
208
245
|
// ── terminal rendering ───────────────────────────────────────────────────────────
|
|
@@ -227,18 +264,7 @@ function out(s) {
|
|
|
227
264
|
process.stdout.write(s + '\n');
|
|
228
265
|
}
|
|
229
266
|
|
|
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
267
|
|
|
237
|
-
function statusMark(status) {
|
|
238
|
-
if (status === 'done') return c('green', '✓');
|
|
239
|
-
if (status === 'start') return c('cyan', '▶');
|
|
240
|
-
return c('gray', '•');
|
|
241
|
-
}
|
|
242
268
|
|
|
243
269
|
const LEVEL_COLOR = { info: 'reset', debug: 'gray', warn: 'yellow', error: 'red' };
|
|
244
270
|
|
|
@@ -300,9 +326,10 @@ async function askClarify(rl, questions) {
|
|
|
300
326
|
* Ask a loop gate interactively. Shows the open blocking issues and the two choices.
|
|
301
327
|
* Returns { decision: "continue" | "another" }.
|
|
302
328
|
*/
|
|
303
|
-
async function askGate(rl, issues) {
|
|
329
|
+
async function askGate(rl, issues, header) {
|
|
304
330
|
out('');
|
|
305
|
-
|
|
331
|
+
// A graph run names the wire and its budget (`? Loop gate · Reviewer → Implementer 3/3 cycles used`).
|
|
332
|
+
out(c('yellow', c('bold', header || 'Loop gate — maximum cycles reached.')));
|
|
306
333
|
out(c('yellow', 'Open critical/major issues:'));
|
|
307
334
|
if (!issues || issues.length === 0) {
|
|
308
335
|
out(' (none reported)');
|
|
@@ -325,40 +352,145 @@ async function askGate(rl, issues) {
|
|
|
325
352
|
|
|
326
353
|
/**
|
|
327
354
|
* Ask the user how to handle a recoverable error (auth / rate-limit / quota /
|
|
328
|
-
* network). Shows the cause and
|
|
355
|
+
* network). Shows the cause and the row's options (failure-policy.mjs: Retry, plus
|
|
356
|
+
* what giving up does — pause or abort). Returns { decision } with the chosen
|
|
357
|
+
* option's id as the wire value.
|
|
329
358
|
*/
|
|
330
359
|
async function askRecovery(rl, recovery) {
|
|
331
360
|
const rec = recovery || {};
|
|
361
|
+
const options = Array.isArray(rec.options) && rec.options.length ? rec.options : promptOptions({ outcome: 'pause' });
|
|
332
362
|
out('');
|
|
333
363
|
out(c('yellow', c('bold', `Recoverable ${String(rec.cls || 'error').replace('_', ' ')} error — the pipeline could not reach the model.`)));
|
|
334
364
|
if (rec.message) out(c('gray', ` ${rec.message}`));
|
|
335
365
|
if (rec.cls === 'auth') out(c('gray', ' Fix: re-authenticate (claude setup-token or /login) in another terminal, then retry.'));
|
|
336
366
|
else out(c('gray', ' Fix: wait out the limit / restore connectivity / top up credit, then retry.'));
|
|
337
|
-
out(
|
|
338
|
-
out(' 2) Abort the run');
|
|
367
|
+
options.forEach((o, i) => out(` ${i + 1}) ${o.label}`));
|
|
339
368
|
let decision = '';
|
|
340
369
|
while (!decision) {
|
|
341
|
-
const raw = (await question(rl, c('cyan',
|
|
342
|
-
|
|
343
|
-
|
|
370
|
+
const raw = (await question(rl, c('cyan', `Choose [1-${options.length}]: `))).trim();
|
|
371
|
+
const byNumber = options[Number(raw) - 1];
|
|
372
|
+
if (byNumber) decision = byNumber.id;
|
|
373
|
+
else if (/^retry/i.test(raw)) decision = 'retry';
|
|
374
|
+
// 'pause' and 'abort' both mean give up; the option offered names the verdict.
|
|
375
|
+
else if (/^(pause|abort)/i.test(raw)) decision = options.find((o) => o.id !== 'retry')?.id || 'pause';
|
|
344
376
|
}
|
|
345
377
|
return { decision };
|
|
346
378
|
}
|
|
347
379
|
|
|
348
380
|
// ── shared drive loop ────────────────────────────────────────────────────────────
|
|
349
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Whether stdin could ever deliver an interactive answer.
|
|
384
|
+
*
|
|
385
|
+
* A TTY always can. A pipe, socket or file redirect MAY — a wrapper script that
|
|
386
|
+
* feeds answers is legitimate — so those pass. `/dev/null` never can: it EOFs on
|
|
387
|
+
* the first read, so the run would start, spend a real agent call, and then be
|
|
388
|
+
* abandoned mid-question. Measured on darwin (and matching linux): `< /dev/null`,
|
|
389
|
+
* a closed fd 0 (Node reopens it on /dev/null) and `spawn(…, {stdio:['ignore',…]})`
|
|
390
|
+
* are all non-TTY CHARACTER devices, while a pipe is a fifo/socket and a redirect
|
|
391
|
+
* is a regular file. An fstat that throws answers "yes" — never refuse a run on a
|
|
392
|
+
* guess.
|
|
393
|
+
*/
|
|
394
|
+
function stdinCanAnswer() {
|
|
395
|
+
if (process.stdin.isTTY) return true;
|
|
396
|
+
try {
|
|
397
|
+
return !fstatSync(0).isCharacterDevice();
|
|
398
|
+
} catch {
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
350
403
|
/**
|
|
351
404
|
* Wire readline Q&A, log/phase rendering, and SIGINT pause/stop onto an
|
|
352
405
|
* orchestrator, then drive it. `start` launches run() or resume(). Returns the
|
|
353
|
-
* process exit code (
|
|
406
|
+
* process exit code (pauseExitCode, failure-policy.mjs):
|
|
407
|
+
* 0 done — and an INTERACTIVE pause the user chose or a limit/cap forced (they
|
|
408
|
+
* witnessed it and can resume);
|
|
409
|
+
* 1 a terminal error (a launch failure, an unrecoverable resume, a stop) and an
|
|
410
|
+
* INTERACTIVE pause an error forced;
|
|
411
|
+
* 2 a usage error (fail());
|
|
412
|
+
* 3 any pause under --yes — the run parked itself (auth/quota/usage limit,
|
|
413
|
+
* exhausted retries, an error) with nobody attached to resume it, so a
|
|
414
|
+
* wrapper must not read success. Under --yes a parked run's cause prints
|
|
415
|
+
* on STDOUT with the pause block; only a terminal error reaches stderr.
|
|
354
416
|
*/
|
|
355
417
|
async function attachAndDrive(orch, flags, start) {
|
|
418
|
+
// Refuse an unanswerable interactive run BEFORE start(). The orchestrator
|
|
419
|
+
// constructor is pure (createPipeline runs inside run()), so nothing exists yet:
|
|
420
|
+
// no pipelines row, no run root, no worktree, no spend. Without this, a
|
|
421
|
+
// `worca --prompt … < /dev/null` reached the first clarify question, printed
|
|
422
|
+
// `Failed to read answer: readline was closed` and exited 0 with the row left
|
|
423
|
+
// `running` — a CI job read success on an abandoned run.
|
|
424
|
+
if (!flags.auto && !stdinCanAnswer()) {
|
|
425
|
+
fail('stdin cannot answer prompts (it is /dev/null or closed) — pass --yes for a non-interactive run.');
|
|
426
|
+
}
|
|
356
427
|
const rl = flags.auto ? null : makeRl();
|
|
357
428
|
let answering = false; // serialize interactive prompts vs. log rendering
|
|
429
|
+
let answerFailure = null; // a question we could not answer -> non-zero exit
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Abandon a prompt we cannot answer: stop the run and force a non-zero exit, so
|
|
433
|
+
* the row never stays `running` and no caller reads success off an abandoned run.
|
|
434
|
+
* A pause/stop already in flight owns the outcome — Ctrl+C must still end
|
|
435
|
+
* `paused`, never `stopped`.
|
|
436
|
+
*/
|
|
437
|
+
const abandonAnswer = (err) => {
|
|
438
|
+
if (orch.pauseRequested || !orch.state || orch.state.status !== 'running') return;
|
|
439
|
+
answerFailure = err;
|
|
440
|
+
process.stderr.write('worca: cannot continue without an answer — stopping the run. '
|
|
441
|
+
+ 'Use --yes for a non-interactive run.\n');
|
|
442
|
+
// Deferred by ONE microtask, for the same reason answers are: _ask emits
|
|
443
|
+
// `question` BEFORE it parks pendingQuestion, so a SYNCHRONOUS throw in the
|
|
444
|
+
// handler below would reach stop() with nothing parked to reject — and the ask
|
|
445
|
+
// parked a moment later would then hang the run forever, which is the very
|
|
446
|
+
// outcome this guard exists to prevent.
|
|
447
|
+
queueMicrotask(() => {
|
|
448
|
+
if (orch.pauseRequested || !orch.state || orch.state.status !== 'running') return;
|
|
449
|
+
orch.stop();
|
|
450
|
+
});
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// stdin reaching EOF *while a question is open* does not throw: readline simply
|
|
454
|
+
// closes and never invokes the question callback, so the awaited answer never
|
|
455
|
+
// settles, the event loop drains and node exits 0 with the run abandoned. Treat
|
|
456
|
+
// it as a failed answer — but ONLY while the run is still running: our own
|
|
457
|
+
// rl.close() in the finally below also fires with `answering` still true when a
|
|
458
|
+
// prompt was interrupted by Ctrl+C (the pause settles start() first, and the
|
|
459
|
+
// parked rl.question never resolves), and that is not a lost answer.
|
|
460
|
+
if (rl) {
|
|
461
|
+
rl.on('close', () => {
|
|
462
|
+
if (!answering) return;
|
|
463
|
+
if (orch.pauseRequested || !orch.state || orch.state.status !== 'running') return;
|
|
464
|
+
process.stderr.write('Failed to read answer: stdin closed\n');
|
|
465
|
+
abandonAnswer(new Error('stdin closed'));
|
|
466
|
+
});
|
|
467
|
+
}
|
|
358
468
|
|
|
359
469
|
// ── event wiring ──────────────────────────────────────────────────────────────
|
|
360
|
-
|
|
361
|
-
|
|
470
|
+
// The run renders its `exec` stream and nothing else: the v1 `phase` event and
|
|
471
|
+
// its renderer died with the v1 engine, and the preflight/done BOOKENDS are
|
|
472
|
+
// exec rows now (x:preflight:1 / x:done:1) that render nothing.
|
|
473
|
+
orch.on('exec', (ev) => {
|
|
474
|
+
// A user STOP surfaces as `exec error 'aborted'` on the in-flight execution
|
|
475
|
+
// while its ledger row is 'stopped'; the harness prints the stop line itself.
|
|
476
|
+
if (ev.status === 'error' && orch.state && orch.state.status === 'stopped') return;
|
|
477
|
+
// The exec payload carries no durationMs (spec §5.7) — the ledger rows do,
|
|
478
|
+
// and they are final by the time a terminal exec arrives. A composite
|
|
479
|
+
// parent has no row of its own: its slices (parentExecutionId) are summed,
|
|
480
|
+
// time and spend alike; a slice's own event keeps its own cost.
|
|
481
|
+
let e = ev;
|
|
482
|
+
if (ev.status !== 'start') {
|
|
483
|
+
const rows = Array.isArray(orch.state && orch.state.steps) ? orch.state.steps : [];
|
|
484
|
+
// By `executionId`, never by `key` — Task 14's rule for step rows: today's
|
|
485
|
+
// bookends are key-only, and a v1 row's key is a phase name.
|
|
486
|
+
const mine = rows.filter((s) => s && (s.executionId === ev.executionId || s.parentExecutionId === ev.executionId));
|
|
487
|
+
if (mine.length) {
|
|
488
|
+
e = { ...ev, durationMs: mine.reduce((a, s) => a + (s.activeMs || 0), 0) };
|
|
489
|
+
if (ev.kind !== 'task') e.costUsd = mine.reduce((a, s) => a + (s.costUsd || 0), 0);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const line = formatExecLine(e, orch.state && orch.state.stepper, { color: c });
|
|
493
|
+
if (line) out(line);
|
|
362
494
|
});
|
|
363
495
|
|
|
364
496
|
orch.on('log', ({ source, level, text }) => {
|
|
@@ -375,16 +507,24 @@ async function attachAndDrive(orch, flags, start) {
|
|
|
375
507
|
process.stderr.write(c('red', `Error: ${message}`) + '\n');
|
|
376
508
|
});
|
|
377
509
|
|
|
378
|
-
|
|
510
|
+
// The WHOLE payload is kept: a graph run's gate question carries `wireId`,
|
|
511
|
+
// which the header formatter resolves against the manifest.
|
|
512
|
+
orch.on('question', async (payload) => {
|
|
513
|
+
const { id, kind, questions, issues, recovery, agent } = payload;
|
|
379
514
|
if (flags.auto || !rl) return; // auto mode resolves internally
|
|
380
515
|
answering = true;
|
|
381
516
|
try {
|
|
382
517
|
if (kind === 'clarify') {
|
|
383
|
-
const
|
|
384
|
-
orch.answer(id,
|
|
518
|
+
const answer = await askClarify(rl, questions || []);
|
|
519
|
+
orch.answer(id, answer);
|
|
385
520
|
} else if (kind === 'gate') {
|
|
386
|
-
|
|
387
|
-
|
|
521
|
+
// One engine: every gate question is a graph question, so the header is
|
|
522
|
+
// built unconditionally. (The `graphRun()` gate that used to guard this
|
|
523
|
+
// died with the phase listener — leaving the call was a ReferenceError
|
|
524
|
+
// waiting for the first interactive gate.)
|
|
525
|
+
const answer = await askGate(rl, issues || [],
|
|
526
|
+
formatGateHeader(payload, orch.state && orch.state.stepper));
|
|
527
|
+
orch.answer(id, answer);
|
|
388
528
|
} else if (kind === 'recovery') {
|
|
389
529
|
const payload = await askRecovery(rl, recovery);
|
|
390
530
|
orch.answer(id, payload);
|
|
@@ -395,6 +535,13 @@ async function attachAndDrive(orch, flags, start) {
|
|
|
395
535
|
}
|
|
396
536
|
} catch (err) {
|
|
397
537
|
process.stderr.write(`Failed to read answer: ${err?.message || err}\n`);
|
|
538
|
+
// Never swallow: orch.answer() was not called, so the ask stays open and the
|
|
539
|
+
// run would hang on it (or be abandoned at EOF with its row left `running`
|
|
540
|
+
// while node exits 0). This is also the arm any THROW inside askClarify /
|
|
541
|
+
// askGate / askRecovery lands in — the shape the P6 graphRun() ReferenceError
|
|
542
|
+
// took — so failing loudly here is what turns that class of bug into a
|
|
543
|
+
// visible failure instead of a silent hang.
|
|
544
|
+
abandonAnswer(err);
|
|
398
545
|
} finally {
|
|
399
546
|
answering = false;
|
|
400
547
|
}
|
|
@@ -434,8 +581,30 @@ async function attachAndDrive(orch, flags, start) {
|
|
|
434
581
|
out('');
|
|
435
582
|
if (result?.status === 'done') {
|
|
436
583
|
out(c('green', c('bold', 'Pipeline complete.')));
|
|
584
|
+
// v2 runs: `Result: <path|value>` (or the amber quiescence line), then
|
|
585
|
+
// `N executions · <active> active · $<cost>`; [] on a v1 run.
|
|
586
|
+
const summary = formatRunSummary(orch.state);
|
|
587
|
+
if (summary.length) {
|
|
588
|
+
out(summary[0].startsWith('Finished at quiescence') ? c('yellow', summary[0]) : summary[0]);
|
|
589
|
+
for (const line of summary.slice(1)) out(line);
|
|
590
|
+
}
|
|
437
591
|
} else if (result?.status === 'paused') {
|
|
438
|
-
|
|
592
|
+
// An error-pause reads as a failure the user can pick up again: the cause on
|
|
593
|
+
// its own line, then the reassurance that nothing was thrown away.
|
|
594
|
+
if (result.reason === REASON.ERROR) {
|
|
595
|
+
out(c('red', c('bold', 'Pipeline paused after an error.')));
|
|
596
|
+
if (result.detail) out(c('red', ` ${result.detail}`));
|
|
597
|
+
out(c('yellow', 'Nothing was discarded: the worktree and the run position are kept.'));
|
|
598
|
+
} else if (result.reason === REASON.RECOVERABLE) {
|
|
599
|
+
out(c('yellow', c('bold', 'Pipeline paused on a recoverable error — resume once it clears.')));
|
|
600
|
+
if (result.detail) out(c('yellow', ` ${result.detail}`));
|
|
601
|
+
out(c('yellow', 'Nothing was discarded: the worktree and the run position are kept.'));
|
|
602
|
+
} else if (result?.reason) {
|
|
603
|
+
const label = describePauseReason(result.reason) || result.reason;
|
|
604
|
+
out(c('yellow', `Pipeline paused: ${label}${result.detail ? ` — ${result.detail}` : ''}`));
|
|
605
|
+
} else {
|
|
606
|
+
out(c('yellow', 'Pipeline paused.'));
|
|
607
|
+
}
|
|
439
608
|
out(`Resume with: ${c('bold', `worca resume ${orch.state.id}`)}`);
|
|
440
609
|
} else if (result?.status === 'stopped') {
|
|
441
610
|
out(c('yellow', 'Pipeline stopped.'));
|
|
@@ -445,7 +614,17 @@ async function attachAndDrive(orch, flags, start) {
|
|
|
445
614
|
if (result?.pipelineDir) {
|
|
446
615
|
out(`Pipeline directory: ${c('bold', result.pipelineDir)}`);
|
|
447
616
|
}
|
|
448
|
-
|
|
617
|
+
// An unanswered question is a failure even if the run somehow settled `done`.
|
|
618
|
+
if (answerFailure) return 1;
|
|
619
|
+
if (result?.status === 'done') return 0;
|
|
620
|
+
// The exit code for a pause is a consequence of its reason (failure-policy.mjs):
|
|
621
|
+
// 0 only when someone is attached to resume it (interactive — pinned by the
|
|
622
|
+
// MAJ-7 Ctrl+C pitfall test) and no error forced it; 1 for an interactive
|
|
623
|
+
// error-pause; 3 under --yes, where every pause is the run parking ITSELF with
|
|
624
|
+
// nobody left to resume (0 would let a CI job go green on a run that did no
|
|
625
|
+
// work; 2 is fail()'s usage-error code).
|
|
626
|
+
if (result?.status === 'paused') return pauseExitCode(result.reason, flags.auto);
|
|
627
|
+
return 1;
|
|
449
628
|
}
|
|
450
629
|
|
|
451
630
|
// ── subcommands ──────────────────────────────────────────────────────────────────
|
|
@@ -570,6 +749,13 @@ async function cmdDoctor() {
|
|
|
570
749
|
} catch (err) {
|
|
571
750
|
process.stderr.write(`worca doctor: reconcile failed: ${err?.message || err}\n`);
|
|
572
751
|
}
|
|
752
|
+
try {
|
|
753
|
+
const { sweepV1Runs } = await import('../core/db.mjs');
|
|
754
|
+
const swept = sweepV1Runs();
|
|
755
|
+
if (swept.length) out(`retired ${swept.length} run(s) paused on the v1 engine`);
|
|
756
|
+
} catch (err) {
|
|
757
|
+
process.stderr.write(`worca doctor: v1-run sweep failed: ${err?.message || err}\n`);
|
|
758
|
+
}
|
|
573
759
|
try {
|
|
574
760
|
// The injected callbacks THROW on a DB failure instead of reporting "no row"
|
|
575
761
|
// (artifacts.mjs#runRootSweepLookups); the sweep records each throw in `failed`
|
|
@@ -589,6 +775,16 @@ async function cmdDoctor() {
|
|
|
589
775
|
} catch (err) {
|
|
590
776
|
process.stderr.write(`worca doctor: run-root sweep failed: ${err?.message || err}\n`);
|
|
591
777
|
}
|
|
778
|
+
// P4: BEFORE the legacy return 0 — that block short-circuits the whole function
|
|
779
|
+
// whenever the effective mode is not `detached` (the default), so an ask-worktree
|
|
780
|
+
// sweep appended after it would never run for most users.
|
|
781
|
+
try {
|
|
782
|
+
const { sweepAskWorktrees } = await import('../core/ask/worktrees.mjs');
|
|
783
|
+
const res = await sweepAskWorktrees({ log: (level, msg) => out(level === 'warn' ? c('yellow', msg) : msg) });
|
|
784
|
+
out(`ask worktrees: removed ${res.removedDirs} orphan dir(s), dropped ${res.prunedRows} stale row(s), skipped ${res.failed}`);
|
|
785
|
+
} catch (err) {
|
|
786
|
+
process.stderr.write(`worca doctor: ask-worktree sweep failed: ${err?.message || err}\n`);
|
|
787
|
+
}
|
|
592
788
|
try {
|
|
593
789
|
// A TOTAL no-op while the effective mode is `legacy`: those paths hold every live
|
|
594
790
|
// and every paused run, so sweeping them would make the documented §10 rollback
|
|
@@ -725,6 +921,19 @@ async function cmdResume(argv) {
|
|
|
725
921
|
process.stderr.write(`pipeline ${id} has no resume point\n`);
|
|
726
922
|
return 1;
|
|
727
923
|
}
|
|
924
|
+
if (saved.resumePoint.version !== 2) {
|
|
925
|
+
const { V1_RUN_RETIRED } = await import('../core/db.mjs');
|
|
926
|
+
process.stderr.write(`worca resume: ${V1_RUN_RETIRED}\n`);
|
|
927
|
+
return 2;
|
|
928
|
+
}
|
|
929
|
+
// The v1 sweep runs AFTER this run's own guards: sweeping FIRST would NULL the
|
|
930
|
+
// point under test, so the caller would read "has no resume point" instead of
|
|
931
|
+
// the honest retirement message above.
|
|
932
|
+
try {
|
|
933
|
+
const { sweepV1Runs } = await import('../core/db.mjs');
|
|
934
|
+
const swept = sweepV1Runs();
|
|
935
|
+
if (swept.length) out(`retired ${swept.length} run(s) paused on the v1 engine`);
|
|
936
|
+
} catch { /* best-effort: resume still works if the sweep fails */ }
|
|
728
937
|
if (saved.row.archived_at) {
|
|
729
938
|
process.stderr.write('worca resume: pipeline is archived\n');
|
|
730
939
|
return 1;
|
|
@@ -781,7 +990,7 @@ async function cmdResume(argv) {
|
|
|
781
990
|
return 1;
|
|
782
991
|
}
|
|
783
992
|
|
|
784
|
-
const orch =
|
|
993
|
+
const orch = await createOrchestratorFor({
|
|
785
994
|
projectDir,
|
|
786
995
|
...(workspace ? { workspace } : {}),
|
|
787
996
|
claude: { mock },
|
|
@@ -808,9 +1017,10 @@ Usage:
|
|
|
808
1017
|
worca plugin enable <name> | disable <name> Toggle without removing files
|
|
809
1018
|
worca plugin doctor [name] [--fix] Health checks (--fix re-runs deterministic setup on failure)
|
|
810
1019
|
worca plugin link <dir> Dev mode: use a local dir as "current"
|
|
1020
|
+
worca plugin reimport <name> Re-read the plugin's pipeline templates (a linked dir is live-edited)
|
|
811
1021
|
worca plugin init <name> [--dir <D>] [--with task-source,agents,skills,workflows]
|
|
812
1022
|
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
|
|
1023
|
+
worca plugin exec <name> <sourceId> <op> [--args '<json>'] [--profile <id>] [--inspect] Debug one connector op
|
|
814
1024
|
worca plugin channel <name> <channelId> [--check] [--inspect] Run a chat channel worker in the
|
|
815
1025
|
foreground (typed lines = simulated inbound); --check runs
|
|
816
1026
|
the module's validateConfig once and exits
|
|
@@ -907,6 +1117,15 @@ function printInventory(inv) {
|
|
|
907
1117
|
for (const cmd of i.setupCommands || []) out(` setup: ${cmd}`);
|
|
908
1118
|
}
|
|
909
1119
|
|
|
1120
|
+
/** Contributions worca refused to load (spec §9.3): one yellow line each, so a
|
|
1121
|
+
* receipt or a list never claims an agent/template that exists nowhere. */
|
|
1122
|
+
function printIgnored(ignored) {
|
|
1123
|
+
const list = Array.isArray(ignored) ? ignored : [];
|
|
1124
|
+
if (!list.length) return;
|
|
1125
|
+
out(c('yellow', ` ${list.length} contribution${list.length > 1 ? 's' : ''} ignored:`));
|
|
1126
|
+
for (const i of list) out(c('yellow', ` ${i.file} — ${i.reason}`));
|
|
1127
|
+
}
|
|
1128
|
+
|
|
910
1129
|
/** kebab plugin name -> camelCase stem for the scaffolded example agent key. */
|
|
911
1130
|
function camelizePluginName(name) {
|
|
912
1131
|
return name.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase());
|
|
@@ -942,8 +1161,8 @@ async function pluginInit(rest) {
|
|
|
942
1161
|
const manifestObj = {
|
|
943
1162
|
name,
|
|
944
1163
|
version: '0.1.0',
|
|
945
|
-
description: 'Scaffolded worca
|
|
946
|
-
engines: { 'worca-cc-api': '>=
|
|
1164
|
+
description: 'Scaffolded worca plugin — edit me',
|
|
1165
|
+
engines: { 'worca-cc-api': '>=3 <4' },
|
|
947
1166
|
};
|
|
948
1167
|
if (withParts.includes('task-source')) {
|
|
949
1168
|
manifestObj.taskSources = [{
|
|
@@ -994,14 +1213,15 @@ async function pluginInit(rest) {
|
|
|
994
1213
|
}
|
|
995
1214
|
if (withParts.includes('agents')) {
|
|
996
1215
|
files.set(`agents/${agentKey}.meta.json`, JSON.stringify({
|
|
1216
|
+
metaVersion: 2,
|
|
997
1217
|
key: agentKey,
|
|
998
1218
|
displayName: 'Example Helper',
|
|
999
1219
|
description: `Example agent installed by the ${name} plugin`,
|
|
1000
1220
|
color: 'amber',
|
|
1001
1221
|
agentFile: `${agentKey}.md`,
|
|
1002
1222
|
runnerType: 'producer',
|
|
1003
|
-
|
|
1004
|
-
|
|
1223
|
+
inputs: [{ id: 'task', type: 'md', required: true }],
|
|
1224
|
+
outputs: [{ id: 'notes', type: 'md', filename: 'notes.md', store: 'run' }],
|
|
1005
1225
|
...(withParts.includes('skills') ? { requiresSkills: ['example-skill'] } : {}),
|
|
1006
1226
|
order: 900,
|
|
1007
1227
|
}, null, 2) + '\n');
|
|
@@ -1013,7 +1233,7 @@ async function pluginInit(rest) {
|
|
|
1013
1233
|
'model: inherit',
|
|
1014
1234
|
'---',
|
|
1015
1235
|
'',
|
|
1016
|
-
`You are an example agent shipped by the "${name}" worca
|
|
1236
|
+
`You are an example agent shipped by the "${name}" worca plugin.`,
|
|
1017
1237
|
'Acknowledge the task you were given and describe what a real agent would do here.',
|
|
1018
1238
|
'',
|
|
1019
1239
|
].join('\n'));
|
|
@@ -1033,12 +1253,21 @@ async function pluginInit(rest) {
|
|
|
1033
1253
|
files.set('skills/example-skill/helper.sh', '#!/bin/sh\necho "example-skill helper ok"\n');
|
|
1034
1254
|
}
|
|
1035
1255
|
if (withParts.includes('workflows')) {
|
|
1256
|
+
// A v2 graph: the Task and End cards are mandatory (V20/V21) and every input
|
|
1257
|
+
// takes exactly one wire (V7). Ports come from the sidecar above.
|
|
1036
1258
|
files.set('workflows/example-flow.json', JSON.stringify({
|
|
1037
1259
|
name: `${name} example flow`,
|
|
1038
|
-
version:
|
|
1260
|
+
version: 2,
|
|
1039
1261
|
domain: 'general',
|
|
1040
|
-
|
|
1041
|
-
|
|
1262
|
+
nodes: [
|
|
1263
|
+
{ id: 'n_task', kind: 'task', x: 40, y: 200, config: {} },
|
|
1264
|
+
{ id: 'n_helper', kind: 'agent', key: agentKey, x: 320, y: 200, config: {} },
|
|
1265
|
+
{ id: 'n_end', kind: 'end', x: 600, y: 200, config: {} },
|
|
1266
|
+
],
|
|
1267
|
+
wires: [
|
|
1268
|
+
{ id: 'w1', from: { node: 'n_task', port: 'task' }, to: { node: 'n_helper', port: 'task' } },
|
|
1269
|
+
{ id: 'w2', from: { node: 'n_helper', port: 'notes' }, to: { node: 'n_end', port: 'result' } },
|
|
1270
|
+
],
|
|
1042
1271
|
}, null, 2) + '\n');
|
|
1043
1272
|
}
|
|
1044
1273
|
files.set('worca-cc-plugin.json', JSON.stringify(manifestObj, null, 2) + '\n');
|
|
@@ -1147,6 +1376,7 @@ async function cmdPlugin(argv) {
|
|
|
1147
1376
|
const res = await store.installPlugin({ repoUrl, subdir: entry.subdir, name, sha, ...(marketplace ? { marketplace } : {}) });
|
|
1148
1377
|
out('installed:');
|
|
1149
1378
|
printInventory(res.inventory);
|
|
1379
|
+
printIgnored(res.ignored);
|
|
1150
1380
|
return 0;
|
|
1151
1381
|
}
|
|
1152
1382
|
|
|
@@ -1160,6 +1390,8 @@ async function cmdPlugin(argv) {
|
|
|
1160
1390
|
const version = p.linked ? 'linked' : p.version || (p.pinnedSha || '').slice(0, 7);
|
|
1161
1391
|
const flags = [p.enabled ? 'enabled' : 'disabled', ...(p.linked ? ['linked'] : [])].join(', ');
|
|
1162
1392
|
out(`${p.name}\t${version}\t${flags}\t${contribSummary(p.contributions)}`);
|
|
1393
|
+
if (p.apiMismatch) out(c('yellow', ` ${p.apiMismatch.message}`));
|
|
1394
|
+
printIgnored(p.ignored);
|
|
1163
1395
|
}
|
|
1164
1396
|
return 0;
|
|
1165
1397
|
}
|
|
@@ -1259,12 +1491,27 @@ async function cmdPlugin(argv) {
|
|
|
1259
1491
|
if (!dir) fail('Usage: worca plugin link <dir>');
|
|
1260
1492
|
const abs = resolve(process.cwd(), dir);
|
|
1261
1493
|
const v = manifestMod.validatePluginDir(abs);
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1494
|
+
// Print EVERY level, pass or fail: a link that SUCCEEDS with warnings is
|
|
1495
|
+
// the mid-migration case the author most needs to read (MAJ-12) — an
|
|
1496
|
+
// API-1 plugin keeps linking, and now says why its agent is ignored.
|
|
1497
|
+
for (const p of v.problems) process.stderr.write(`${p.level}: ${p.message}\n`);
|
|
1498
|
+
if (!v.ok) return 2;
|
|
1499
|
+
const linked = await store.linkPlugin(v.manifest.name, abs);
|
|
1267
1500
|
out(`linked ${v.manifest.name} -> ${abs} (dev mode; doctor will warn)`);
|
|
1501
|
+
const n = linked.workflows.imported.length;
|
|
1502
|
+
if (n) out(` imported ${n} pipeline template${n === 1 ? '' : 's'} — edits to them need: worca plugin reimport ${v.manifest.name}`);
|
|
1503
|
+
printIgnored(store.ignoredContributions(v.manifest.name, abs, { workflowSkips: linked.workflows.skipped }));
|
|
1504
|
+
return 0;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
case 'reimport': {
|
|
1508
|
+
const a = pluginArgs(rest);
|
|
1509
|
+
const name = a._[0];
|
|
1510
|
+
if (!name) fail('Usage: worca plugin reimport <name>');
|
|
1511
|
+
const r = await store.reimportPlugin(name);
|
|
1512
|
+
const n = r.workflows.imported.length;
|
|
1513
|
+
out(`reimported ${name}: ${n} pipeline template${n === 1 ? '' : 's'}`);
|
|
1514
|
+
printIgnored(r.ignored);
|
|
1268
1515
|
return 0;
|
|
1269
1516
|
}
|
|
1270
1517
|
|
|
@@ -1286,9 +1533,9 @@ async function cmdPlugin(argv) {
|
|
|
1286
1533
|
}
|
|
1287
1534
|
|
|
1288
1535
|
case 'exec': {
|
|
1289
|
-
const a = pluginArgs(rest, ['--args'], ['--inspect']);
|
|
1536
|
+
const a = pluginArgs(rest, ['--args', '--profile'], ['--inspect']);
|
|
1290
1537
|
const [name, sourceId, op] = a._;
|
|
1291
|
-
if (!name || !sourceId || !op) fail("Usage: worca plugin exec <name> <sourceId> <op> [--args '<json>'] [--inspect]");
|
|
1538
|
+
if (!name || !sourceId || !op) fail("Usage: worca plugin exec <name> <sourceId> <op> [--args '<json>'] [--profile <id>] [--inspect]");
|
|
1292
1539
|
if (a.inspect) process.env.WORCA_PLUGIN_INSPECT = '1'; // shim spawns the child with --inspect-brk
|
|
1293
1540
|
let args = {};
|
|
1294
1541
|
if (a.args) {
|
|
@@ -1299,7 +1546,9 @@ async function cmdPlugin(argv) {
|
|
|
1299
1546
|
}
|
|
1300
1547
|
}
|
|
1301
1548
|
const { callSource } = await import('../core/plugin-shim.mjs');
|
|
1302
|
-
|
|
1549
|
+
// --profile targets one instance of a multi-profile source; absent, the
|
|
1550
|
+
// shim falls back to the implicit default bucket (single-profile case).
|
|
1551
|
+
const result = await callSource({ plugin: name, sourceId, op, args, profile: a.profile || undefined });
|
|
1303
1552
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n'); // stdout = result ONLY
|
|
1304
1553
|
return 0;
|
|
1305
1554
|
}
|
|
@@ -1431,8 +1680,52 @@ async function cmdMarketplace(argv) {
|
|
|
1431
1680
|
|
|
1432
1681
|
const SUBCOMMANDS = new Set(['add', 'list', 'remove', 'resume', 'doctor', 'plugin', 'marketplace', 'config']);
|
|
1433
1682
|
|
|
1683
|
+
/** Levenshtein distance, two-row. Only ever called on short argv tokens. */
|
|
1684
|
+
function editDistance(a, b) {
|
|
1685
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
1686
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1687
|
+
const row = [i];
|
|
1688
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1689
|
+
row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1690
|
+
}
|
|
1691
|
+
prev = row;
|
|
1692
|
+
}
|
|
1693
|
+
return prev[b.length];
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* The subcommand a lone positional was probably meant to be, or null.
|
|
1698
|
+
*
|
|
1699
|
+
* A bare positional is a legal prompt (`worca "do the thing"`), so only a SINGLE
|
|
1700
|
+
* whitespace-free token that near-misses a real subcommand counts as a typo: edit
|
|
1701
|
+
* distance <= 2, or a strict prefix of at least 3 characters (`plug` -> `plugin`).
|
|
1702
|
+
* Refusing costs one retry with --prompt; running `worca reusme run-abc123` cuts a
|
|
1703
|
+
* worktree + feature branch and spends real tokens on a task named "reusme".
|
|
1704
|
+
*/
|
|
1705
|
+
function nearestSubcommand(token) {
|
|
1706
|
+
if (!token || /\s/.test(token)) return null;
|
|
1707
|
+
// 'help' and 'version' are spliced into both loops: they are real CLI arms (the
|
|
1708
|
+
// head of main() / the module top) but deliberately absent from the dispatch
|
|
1709
|
+
// table, so without them a typo of either (`worca hlep`, `worca versoin`) is
|
|
1710
|
+
// distance >= 3 from everything and runs as a PROMPT.
|
|
1711
|
+
for (const name of [...SUBCOMMANDS, 'help', 'version']) {
|
|
1712
|
+
if (token.length >= 3 && name.length > token.length && name.startsWith(token)) return name;
|
|
1713
|
+
}
|
|
1714
|
+
let best = null;
|
|
1715
|
+
let bestD = 3; // strictly less than 3 == distance <= 2
|
|
1716
|
+
for (const name of [...SUBCOMMANDS, 'help', 'version']) {
|
|
1717
|
+
const d = editDistance(token, name);
|
|
1718
|
+
if (d < bestD) { bestD = d; best = name; }
|
|
1719
|
+
}
|
|
1720
|
+
return best;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1434
1723
|
async function main() {
|
|
1435
1724
|
const sub = process.argv[2];
|
|
1725
|
+
// `worca help` is what every CLI user types first; it is not a subcommand and
|
|
1726
|
+
// not a near-miss of one, so without this line it became a PROMPT and ran a
|
|
1727
|
+
// pipeline named "help" (MIN-51).
|
|
1728
|
+
if (sub === 'help') { process.stdout.write(HELP); return 0; }
|
|
1436
1729
|
if (SUBCOMMANDS.has(sub)) {
|
|
1437
1730
|
const rest = process.argv.slice(3);
|
|
1438
1731
|
if (sub === 'add') return cmdAdd(rest);
|
|
@@ -1466,10 +1759,23 @@ async function main() {
|
|
|
1466
1759
|
if (flags.mock) {
|
|
1467
1760
|
process.env.WORCA_MOCK = '1';
|
|
1468
1761
|
}
|
|
1762
|
+
// The mock runner routes EVERY dontAsk spawn to the Ask Worca mock (claude-runner.mjs
|
|
1763
|
+
// runMock), which writes no pipeline artifact — a mock pipeline under dontAsk dies
|
|
1764
|
+
// at its first artifact read with no hint why. Refuse the PAIR, not the mode.
|
|
1765
|
+
if (flags.permissionMode === 'dontAsk' && /^(1|true|yes|on)$/i.test(String(process.env.WORCA_MOCK ?? process.env.ORCH_MOCK ?? ''))) {
|
|
1766
|
+
fail('--permission-mode dontAsk cannot be combined with --mock: the mock runner reserves it for the Ask Worca assistant.');
|
|
1767
|
+
}
|
|
1469
1768
|
|
|
1470
1769
|
if (!flags.prompt && !flags.file) {
|
|
1471
|
-
// Allow a bare positional prompt: `worca "do the thing"`.
|
|
1770
|
+
// Allow a bare positional prompt: `worca "do the thing"`. A lone token that
|
|
1771
|
+
// near-misses a subcommand is a typo, not a task — refuse it here, before a
|
|
1772
|
+
// pipeline row, a worktree or a feature branch exists.
|
|
1472
1773
|
if (flags._.length) {
|
|
1774
|
+
const meant = nearestSubcommand(flags._[0]);
|
|
1775
|
+
if (meant) {
|
|
1776
|
+
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")`);
|
|
1777
|
+
fail(`unknown subcommand "${flags._[0]}" — did you mean "${meant}"? (to run a prompt, use --prompt "\u2026")`);
|
|
1778
|
+
}
|
|
1473
1779
|
flags.prompt = flags._.join(' ');
|
|
1474
1780
|
} else {
|
|
1475
1781
|
fail('Provide a task with --prompt "<text>" or --file <markdown>. See --help.');
|
|
@@ -1477,6 +1783,19 @@ async function main() {
|
|
|
1477
1783
|
}
|
|
1478
1784
|
|
|
1479
1785
|
const projectDir = resolve(flags.project);
|
|
1786
|
+
// A NAMED --file must be readable BEFORE anything starts. The readers used to
|
|
1787
|
+
// swallow the failure and run the whole pipeline on an empty prompt with exit 0
|
|
1788
|
+
// — in real mode that spends tokens and cuts a worktree + feature branch for
|
|
1789
|
+
// nothing. Relative paths resolve against the PROJECT dir, exactly as the
|
|
1790
|
+
// orchestrator's own read does.
|
|
1791
|
+
if (flags.file) {
|
|
1792
|
+
const { readPromptFile } = await import('../core/artifacts.mjs');
|
|
1793
|
+
try {
|
|
1794
|
+
await readPromptFile(projectDir, flags.file);
|
|
1795
|
+
} catch (err) {
|
|
1796
|
+
fail(err && err.message ? err.message : String(err));
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1480
1799
|
// Resolve extras against the shell cwd so relative paths are unambiguous.
|
|
1481
1800
|
const extras = (flags.extras || []).map((p) => resolve(process.cwd(), p));
|
|
1482
1801
|
|
|
@@ -1490,13 +1809,24 @@ async function main() {
|
|
|
1490
1809
|
return 1;
|
|
1491
1810
|
}
|
|
1492
1811
|
|
|
1493
|
-
|
|
1812
|
+
// Validate --workflow before spawning anything: an unknown or archived template
|
|
1813
|
+
// must fail with one line, not a stack trace half-way through a run. The read row
|
|
1814
|
+
// doubles as createOrchestratorFor's routing hint (it skips a second row read).
|
|
1815
|
+
let row;
|
|
1816
|
+
if (flags.workflow) {
|
|
1817
|
+
const { assertRunnableWorkflow } = await import('../core/workflows.mjs');
|
|
1818
|
+
try { row = await assertRunnableWorkflow(flags.workflow); }
|
|
1819
|
+
catch (err) { fail(`${err && err.message ? err.message : String(err)}`); }
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
const orch = await createOrchestratorFor({
|
|
1494
1823
|
projectDir,
|
|
1495
1824
|
prompt: flags.prompt || undefined,
|
|
1496
1825
|
promptFile: flags.file || undefined,
|
|
1497
1826
|
title: flags.title || undefined,
|
|
1498
1827
|
extras,
|
|
1499
1828
|
workflowId: flags.workflow || undefined,
|
|
1829
|
+
template: row,
|
|
1500
1830
|
branch: { source: flags.sourceBranch, feature: flags.featureBranch },
|
|
1501
1831
|
claude: {
|
|
1502
1832
|
permissionMode: flags.permissionMode,
|