@khoavu882/doflow 2.0.0 → 2.0.2
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 +4 -5
- package/bin/doflow.js +1 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/claude/hooks/session-end.sh +21 -1
- package/core/harnesses/claude/settings/settings.json +8 -9
- package/core/harnesses/codex/hooks/hooks.json +0 -10
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/codex/hooks/session-end.impl.sh +21 -1
- package/core/harnesses/copilot/hooks/doflow-lib.sh +4 -3
- package/core/harnesses/copilot/hooks/doflow.json +0 -6
- package/core/harnesses/kiro/agents/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +7 -3
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +7 -3
- package/core/harnesses/pi/extensions/doflow-guard.js +3 -2
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -1
- package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +35 -25
- package/core/harnesses/shared/hooks/bridge/envelope.jq +1 -1
- package/core/harnesses/shared/hooks/bridge/respond.sh +6 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +1 -1
- package/core/harnesses/shared/hooks/policies/compact-text.sh +8 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +23 -1
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +4 -6
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +20 -21
- package/core/harnesses/shared/hooks/policies/stop-check.sh +7 -6
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +26 -2
- package/core/harnesses/shared/hooks/stream-hook-runner.js +11 -12
- package/core/registry/contracts.json +1 -1
- package/core/registry/contribution-points.json +5 -2
- package/core/registry/gates.json +5 -5
- package/core/registry/harnesses.json +9 -9
- package/core/registry/intake.json +53 -0
- package/core/registry/lifecycle.json +176 -503
- package/core/registry/package-templates/intake-category/files/category.json +8 -0
- package/core/registry/package-templates/intake-category/template.json +4 -0
- package/core/registry/packages/doflow/doflow-package.json +2 -2
- package/core/registry/rules.json +8 -8
- package/core/registry/settings.json +12 -16
- package/core/registry/storage.json +9 -2
- package/core/registry/telemetry.json +1 -1
- package/core/registry/verification.json +0 -5
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +7 -0
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +4 -4
- package/core/shared/guidance/references/ORCHESTRATION.md +2 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
- package/core/shared/guidance/rules/RULES.md +2 -2
- package/core/shared/scripts/doflow/bash/do-git-state.sh +12 -4
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +86 -33
- package/core/shared/scripts/doflow/bash/do-review-package.sh +36 -2
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +1 -1
- package/core/shared/scripts/doflow/bin/doflow-run +23 -2
- package/core/shared/skills/do/SKILL.md +3 -3
- package/core/shared/skills/do-brainstorm/SKILL.md +26 -24
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +2 -1
- package/core/shared/skills/do-code-review/SKILL.md +7 -4
- package/core/shared/skills/do-design/SKILL.md +11 -5
- package/core/shared/skills/do-design/references/data_model_example.md +167 -0
- package/core/shared/skills/do-diagnose/SKILL.md +4 -2
- package/core/shared/skills/do-document/SKILL.md +1 -0
- package/core/shared/skills/do-document/references/embedded-research.md +1 -1
- package/core/shared/skills/do-execute-plan/SKILL.md +4 -2
- package/core/shared/skills/do-execute-plan/references/run_position.md +3 -1
- package/core/shared/skills/do-execute-plan/references/scaffold.md +1 -1
- package/core/shared/skills/do-flow/SKILL.md +4 -4
- package/core/shared/skills/do-flow/references/task_classes.md +5 -5
- package/core/shared/skills/do-implement/SKILL.md +9 -7
- package/core/shared/skills/do-plan/SKILL.md +9 -6
- package/core/shared/templates/doflow/data-model-template.md +136 -102
- package/core/shared/workflows/do-flow-chain.md +2 -0
- package/package.json +1 -1
- package/src/adapters/antigravity/index.js +54 -12
- package/src/adapters/claude/index.js +23 -2
- package/src/adapters/codex/agents.js +15 -1
- package/src/adapters/codex/config.js +5 -2
- package/src/adapters/codex/hooks.js +39 -3
- package/src/adapters/codex/index.js +13 -0
- package/src/adapters/copilot/index.js +11 -4
- package/src/adapters/copy-tree.js +8 -2
- package/src/adapters/created-file.js +41 -0
- package/src/adapters/mcp-entries.js +11 -1
- package/src/adapters/opencode/index.js +32 -5
- package/src/adapters/pi/index.js +15 -4
- package/src/adapters/pi/mcp.js +44 -18
- package/src/cli/commands/remove.js +3 -1
- package/src/cli/commands/status.js +1 -1
- package/src/cli/commands/update.js +7 -3
- package/src/cli/index.js +43 -19
- package/src/cli/runtime-commands.js +13 -5
- package/src/cli/shared.js +5 -3
- package/src/helper/marker-merge.js +11 -7
- package/src/install/backup-set.js +7 -3
- package/src/install/backup.js +13 -7
- package/src/install/context.js +2 -1
- package/src/install/manifest.js +35 -11
- package/src/install/paths.js +38 -0
- package/src/install/targets.js +11 -2
- package/src/lifecycle/coverage.js +1 -30
- package/src/lifecycle/hook-tier.js +2 -2
- package/src/lifecycle/index.js +80 -3
- package/src/lifecycle/retired-harnesses.js +16 -1
- package/src/lifecycle/view.js +3 -2
- package/src/registry/index.js +21 -4
- package/src/release/docs-blocks.js +37 -53
- package/src/runtime/advice/doctor.js +2 -2
- package/src/runtime/brief-tools.js +2 -1
- package/src/runtime/capability-router.js +26 -3
- package/src/runtime/context/pointer.js +84 -17
- package/src/runtime/context-pack.js +52 -9
- package/src/runtime/dispatch/integrate.js +27 -8
- package/src/runtime/dispatch/loop.js +7 -0
- package/src/runtime/dispatch/verify-run.js +9 -3
- package/src/runtime/evidence-ledger.js +18 -0
- package/src/runtime/feature-pointer.js +10 -1
- package/src/runtime/health.js +68 -5
- package/src/runtime/implementation-gate.js +14 -6
- package/src/runtime/intake/engine.js +679 -0
- package/src/runtime/intake/index.js +136 -0
- package/src/runtime/intake/manifest.js +140 -0
- package/src/runtime/intake/taxonomy.js +206 -0
- package/src/runtime/knowledge/code-retrieval.js +11 -5
- package/src/runtime/knowledge/semble-provider.js +12 -2
- package/src/runtime/lifecycle/cli.js +76 -6
- package/src/runtime/lifecycle/followup.js +32 -6
- package/src/runtime/lifecycle/overview.js +34 -3
- package/src/runtime/lifecycle/release.js +16 -3
- package/src/runtime/lifecycle/retention.js +12 -15
- package/src/runtime/lifecycle/store-upkeep.js +13 -3
- package/src/runtime/mask.js +3 -3
- package/src/runtime/mcp-consultations.js +11 -5
- package/src/runtime/packages/points.js +10 -0
- package/src/runtime/research-request.js +3 -1
- package/src/runtime/review/command.js +3 -7
- package/src/runtime/review/start.js +24 -10
- package/src/runtime/roadmap/attendance.js +137 -0
- package/src/runtime/roadmap/gates.js +6 -3
- package/src/runtime/roadmap/loop.js +127 -33
- package/src/runtime/roadmap/merge.js +17 -8
- package/src/runtime/roadmap/rows.js +15 -5
- package/src/runtime/roadmap/seed.js +153 -41
- package/src/runtime/roadmap/worktree-cleanup.js +147 -0
- package/src/runtime/settings/files.js +3 -1
- package/src/runtime/settings/prepared-run.js +12 -2
- package/src/runtime/settings/registry.js +1 -1
- package/src/runtime/settings/resolve.js +13 -1
- package/src/runtime/settings/write.js +73 -2
- package/src/runtime/slots.js +1 -1
- package/src/runtime/storage/registry.js +2 -2
- package/src/runtime/storage/roots.js +4 -18
- package/src/runtime/storage/rules.js +22 -46
- package/src/runtime/storage/work-status.js +14 -4
- package/src/runtime/task-classifier.js +39 -4
- package/src/runtime/task-scope.js +17 -2
- package/src/runtime/task-state.js +13 -7
- package/src/runtime/trace/analyze.js +4 -2
- package/src/runtime/trace/indicators.js +25 -20
- package/src/runtime/trace/spans.js +4 -5
- package/src/runtime/verb-view.js +268 -0
- package/src/runtime/verification/engine.js +0 -45
- package/src/runtime/worker/command.js +4 -2
- package/src/runtime/worker/launch.js +5 -37
- package/src/runtime/worker/readiness.js +10 -6
- package/src/runtime/worker/supervisor.js +18 -6
- package/src/runtime/workflow-orchestrator.js +39 -7
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +0 -9
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -25
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -105
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -579
package/src/cli/index.js
CHANGED
|
@@ -53,6 +53,16 @@ function assertNoBackupRequiresForce(o ) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Reports an argument error and exits. `review` answers in its own JSON usage shape when `--json` is anywhere in the
|
|
58
|
+
* arguments (before or after the bad one); every other verb prints the plain line on stderr.
|
|
59
|
+
*/
|
|
60
|
+
function argError(o , argv , message , code ) {
|
|
61
|
+
if (o.cmd === 'review' && argv.includes('--json')) console.log(JSON.stringify({ ok: false, error: 'usage', message }));
|
|
62
|
+
else console.error(`doflow: ${message}`);
|
|
63
|
+
process.exit(code);
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
function parseArgs(argv ) {
|
|
57
67
|
const o = { cmd: null, positional: [], targets: [], mcp: null, dryRun: false, force: false, adopt: false,
|
|
58
68
|
otlpHttp: false,
|
|
@@ -68,7 +78,7 @@ function parseArgs(argv ) {
|
|
|
68
78
|
if (a.startsWith('-')) o.flagsGiven.push(a);
|
|
69
79
|
// `--commit` is the roadmap verb's own flag: anywhere else it must reach the default branch and get
|
|
70
80
|
// that verb's unknown-flag answer, as it did before it was added. '' matches no case.
|
|
71
|
-
switch (a === '--commit' && o.cmd !== 'roadmap' ? '' : a) {
|
|
81
|
+
switch (a === '--commit' && o.cmd !== 'roadmap' ? '' : a.startsWith('--action=') ? '--action' : a) {
|
|
72
82
|
case '-h': case '--help': o.help = true; break;
|
|
73
83
|
case '-v': o.version = true; break;
|
|
74
84
|
case '--version':
|
|
@@ -92,7 +102,7 @@ function parseArgs(argv ) {
|
|
|
92
102
|
case '--':
|
|
93
103
|
if (o.cmd === 'slot') { o.slotCommand = argv.slice(i + 1); i = argv.length; break; }
|
|
94
104
|
if (o.cmd === 'plan-check') break;
|
|
95
|
-
|
|
105
|
+
argError(o, argv, unknownFlagMessage(a), unknownFlagExit(o.cmd));
|
|
96
106
|
break;
|
|
97
107
|
// worker --action run|describe|select: the isolated-edit recipe and baseline (082 IC-014).
|
|
98
108
|
case '--edit': o.edit = true; break;
|
|
@@ -148,10 +158,12 @@ function parseArgs(argv ) {
|
|
|
148
158
|
o.tools = val.split(',').map((s) => s.trim()).filter(Boolean); i++; break;
|
|
149
159
|
}
|
|
150
160
|
case '--action': {
|
|
151
|
-
|
|
161
|
+
// `--action <value>` or `--action=<value>`.
|
|
162
|
+
const inline = a.startsWith('--action=');
|
|
163
|
+
const val = inline ? a.slice('--action='.length) : argv[i + 1];
|
|
152
164
|
// `review`, `protocol`, `config`, `primary` and `package` answer a missing value as usage, exit 2; other verbs keep exit 1.
|
|
153
|
-
if (val === undefined || val.startsWith('-'))
|
|
154
|
-
o.action = val; o.actionGiven = true; i++; break;
|
|
165
|
+
if (val === undefined || val === '' || val.startsWith('-')) argError(o, argv, '--action requires a value', o.cmd === 'review' || o.cmd === 'protocol' || o.cmd === 'config' || o.cmd === 'primary' || o.cmd === 'fleet' || o.cmd === 'transcript' || o.cmd === 'package' || o.cmd === 'worktree' ? 2 : 1);
|
|
166
|
+
o.action = val; o.actionGiven = true; if (!inline) i++; break;
|
|
155
167
|
}
|
|
156
168
|
case '--task-class': {
|
|
157
169
|
const val = argv[i + 1];
|
|
@@ -236,7 +248,7 @@ function parseArgs(argv ) {
|
|
|
236
248
|
if (runtime !== null) { i = runtime; break; }
|
|
237
249
|
// slot, primary, fleet and transcript report an unknown flag as their own usage error (exit 2, 115 IC-008, 120 IC-008).
|
|
238
250
|
if (a.startsWith('-') && (o.cmd === 'slot' || o.cmd === 'primary' || o.cmd === 'fleet' || o.cmd === 'transcript')) (o.extraFlags = o.extraFlags || []).push(a);
|
|
239
|
-
else if (a.startsWith('-') && o.cmd !== 'plan-check')
|
|
251
|
+
else if (a.startsWith('-') && o.cmd !== 'plan-check') argError(o, argv, unknownFlagMessage(a), unknownFlagExit(o.cmd));
|
|
240
252
|
else if (!o.cmd) o.cmd = a;
|
|
241
253
|
else o.positional.push(a);
|
|
242
254
|
}
|
|
@@ -248,18 +260,18 @@ function parseArgs(argv ) {
|
|
|
248
260
|
|
|
249
261
|
/** Verbs that answer an unrecognised flag as a usage error (exit 2); every other verb keeps exit 1. */
|
|
250
262
|
function unknownFlagExit(cmd ) {
|
|
251
|
-
return cmd === 'protocol' || cmd === 'config' || cmd === 'rules' || cmd === 'review' ? 2 : 1;
|
|
263
|
+
return cmd === 'protocol' || cmd === 'config' || cmd === 'rules' || cmd === 'review' || cmd === 'worktree' || cmd === 'attendance' ? 2 : 1;
|
|
252
264
|
}
|
|
253
265
|
|
|
254
266
|
/** Longest part of an unrecognised argument echoed back; the rest is cut and marked. */
|
|
255
267
|
const UNKNOWN_FLAG_ECHO_CHARS = 80;
|
|
256
268
|
|
|
257
|
-
/** The unknown-flag
|
|
269
|
+
/** The unknown-flag message (without the `doflow: ` prefix): the argument is untrusted text, so it is made print-safe (no control
|
|
258
270
|
* characters, one line) and cut to a bounded length (FU-8eqytg). */
|
|
259
271
|
function unknownFlagMessage(arg ) {
|
|
260
272
|
const safe = printSafe(String(arg));
|
|
261
273
|
const shown = safe.length > UNKNOWN_FLAG_ECHO_CHARS ? `${safe.slice(0, UNKNOWN_FLAG_ECHO_CHARS)}...` : safe;
|
|
262
|
-
return `
|
|
274
|
+
return `unknown flag '${shown}'`;
|
|
263
275
|
}
|
|
264
276
|
|
|
265
277
|
/** Single-value arguments of the runtime verbs → the option key each one writes. */
|
|
@@ -277,6 +289,7 @@ const RUNTIME_STRING_FLAGS = new Map([
|
|
|
277
289
|
['--request', 'request'], // classify: request text for an advisory second opinion
|
|
278
290
|
['--request-file', 'requestFile'], // classify: request text read from a file
|
|
279
291
|
// decision --action add. `--statement`, `--stage`, `--slug` and `--batch` above are shared.
|
|
292
|
+
['--run-id', 'runId'], // intake --action undo: the sort run to undo (default: the latest)
|
|
280
293
|
['--topic', 'topic'], // the subject key a decision is live on
|
|
281
294
|
['--channel', 'channel'], // question | gate | prompt | default | resolution
|
|
282
295
|
['--supersedes', 'supersedes'], // comma-separated DEC-### ids this decision replaces
|
|
@@ -434,8 +447,9 @@ const RUNTIME_STRING_FLAGS = new Map([
|
|
|
434
447
|
['--dir', 'dir'], // package --action new: authored package target directory
|
|
435
448
|
['--name', 'name'], // slot: the slot's name, 1 to 64 characters of a-z, 0-9 and - (115 IC-008)
|
|
436
449
|
['--label', 'label'], // slot --action run: the holder's label, at most 80 characters (115 IC-008)
|
|
437
|
-
['--cursor', 'cursor'], // transcript --action read: the cursor an earlier read returned (093 IC-022)
|
|
438
|
-
['--limit', 'limit'], // transcript --action read: at most this many events, validated in the verb (093 IC-022)
|
|
450
|
+
['--cursor', 'cursor'], // transcript --action read: the cursor an earlier read returned (093 IC-022) · followup --action list: the nextCursor of an earlier page (137 IC-004)
|
|
451
|
+
['--limit', 'limit'], // transcript --action read: at most this many events, validated in the verb (093 IC-022) · followup --action list: the page size (137 IC-004)
|
|
452
|
+
['--view', 'view'], // followup --action list, lifecycle --action overview, classify: brief or full, validated in the verb (137 IC-001)
|
|
439
453
|
['--window', 'window'], // transcript --action stats: minutes, 1 to 60, validated in the verb (093 IC-023)
|
|
440
454
|
['--port', 'port'], // ui: the Console's port, 0 to 65535, checked in commands/ui.ts (092 IC-008)
|
|
441
455
|
]);
|
|
@@ -496,14 +510,14 @@ function parseRuntimeFlag(arg , argv , i , o )
|
|
|
496
510
|
// `--batch <file|->`. Rejecting it made the documented spelling exit 2 while only `--batch=-`
|
|
497
511
|
// worked — a help text endorsing a form the parser refuses.
|
|
498
512
|
const isStdin = value === '-';
|
|
499
|
-
if (value === undefined || (value.startsWith('-') && !isStdin))
|
|
513
|
+
if (value === undefined || (value.startsWith('-') && !isStdin)) argError(o, argv, `${name} requires a value`, 2);
|
|
500
514
|
next = i + 1;
|
|
501
515
|
}
|
|
502
|
-
if (value === '')
|
|
516
|
+
if (value === '') argError(o, argv, `${name} requires a value`, 2);
|
|
503
517
|
|
|
504
518
|
if (RUNTIME_INT_FLAGS.has(name)) {
|
|
505
519
|
const parsed = parseInt(value, 10);
|
|
506
|
-
if (!Number.isFinite(parsed) || parsed < 0)
|
|
520
|
+
if (!Number.isFinite(parsed) || parsed < 0) argError(o, argv, `${name} expects a non-negative integer, got '${value}'`, 2);
|
|
507
521
|
o[key] = parsed;
|
|
508
522
|
} else if (RUNTIME_LIST_FLAGS.has(name)) {
|
|
509
523
|
// --exclude and --exclude-family both accept comma lists or repeated flags. Keep --exclude's
|
|
@@ -546,7 +560,7 @@ Commands:
|
|
|
546
560
|
context-grant Grant one compacted summary to a launched run and lineage (--summary, --to-run, --to-lineage)
|
|
547
561
|
retrieval-plan Declare a stage's information needs, attempt retrieval, then report every declared item
|
|
548
562
|
outcome Record a task's terminal state with its basis, or show it (--task-id)
|
|
549
|
-
classify Validate a proposed task class and return its workflow (--task-class)
|
|
563
|
+
classify Validate a proposed task class and return its workflow (--task-class [--view brief|full])
|
|
550
564
|
workflow Resolve a task class to its stages, gates and readiness templates
|
|
551
565
|
orchestrate Start or resume a run: complete stages, decide gates (--action)
|
|
552
566
|
research-request Open, list or resolve a feature-stage research question (--task-id)
|
|
@@ -577,15 +591,16 @@ Commands:
|
|
|
577
591
|
config unset <key> --layer user|project|local [--json] remove the key from that layer's file
|
|
578
592
|
scaffold Emit the reviewable code scaffold the active feature's artifacts imply
|
|
579
593
|
decision Register, list or compact the active feature's decisions (--action init|add|list|compact)
|
|
580
|
-
|
|
581
|
-
|
|
594
|
+
intake Sort, preview, undo, list or file prompt text for the active feature's additional-context folder (--action sort|preview|undo|list|file-prompt)
|
|
595
|
+
followup Record, list, take, settle, promote or report what a feature left unfinished (--action add|list|take|settle|promote|report; list: [--view brief|full] [--cursor <c>] [--limit <n>] [--ids <FU-id,...>])
|
|
596
|
+
lifecycle Open follow-ups, feature tracking, releases and status for the project (--action overview|init|release|status|merged; overview: [--view brief|full])
|
|
582
597
|
goal Add, check, link, list or close a goal and its checklist (--action add|item|check|link|list|done)
|
|
583
598
|
failure List, settle or switch off the failures DoFlow captured on this machine (--action list|settle|capture)
|
|
584
599
|
(a value beginning with '-' must be written --flag=value)
|
|
585
600
|
roadmap Session-driven autopilot runner (--action render|status|next|audit|init-policy [--check] [--row <id>] [--run <id>]).
|
|
586
601
|
roadmap --action init-policy [--commit] writes the gate policy for review, or with --commit commits it onto local develop when develop is not checked out in a worktree (otherwise it prints the commands to run there; never pushes);
|
|
587
602
|
roadmap seed --action check|apply|amend --draft <path> [--json] validates and writes new rows;
|
|
588
|
-
rows written while autopilot is on land proposed; roadmap promote <id...> [--json] (person
|
|
603
|
+
rows written while autopilot is on land proposed; roadmap promote <id...> [--json] (person: asks y/N on the terminal) makes them ready
|
|
589
604
|
plan-check Check a plan's task lines the way the task-attempt ledger reads them: plan-check <plan path> [--json]
|
|
590
605
|
review Validate a feature's review/findings.json; print errors, advisories and counts (exit 0 valid, 1 invalid, 2 usage).
|
|
591
606
|
review --action check [--slug <name> | --path <findings.json>] [--json]
|
|
@@ -604,6 +619,10 @@ Commands:
|
|
|
604
619
|
transcript Read an agent's recent events, or the project's traffic (read-only, run in a project).
|
|
605
620
|
transcript --action read --agent <key> [--cursor <c>] [--limit <n>] [--json] masked events and DoFlow's record events (exit 0, 1 agent-not-in-project or session-not-in-project, 2 usage)
|
|
606
621
|
transcript --action stats [--window <minutes 1..60>] [--json] events per minute, tool calls, tokens, who works with whom (exit 0, 2 usage)
|
|
622
|
+
worktree Remove a merged feature's worktree and branch, never by force (--action cleanup).
|
|
623
|
+
worktree --action cleanup --slug <row slug> [--json] copies agent-docs/doflow/<slug> back to the main checkout first (exit 0 cleaned, 1 kept or lease-held with the reason, 2 usage)
|
|
624
|
+
attendance Say whether a person is watching a stage (read-only).
|
|
625
|
+
attendance --task-id <row slug> [--run <rl-id>] [--json] prints "unattended <source>" or "attended <reason>" (exit 0, 2 usage)
|
|
607
626
|
package List the contribution points a package may fill, check a package directory, show the packages, add and update a local package, or turn it on and off.
|
|
608
627
|
package --action points [--json]
|
|
609
628
|
package --action check --path <dir> [--json] (read-only; exit 0 clean, 1 findings, 2 usage)
|
|
@@ -673,6 +692,7 @@ Runtime verb arguments (accept --flag value or --flag=value):
|
|
|
673
692
|
failure: list|settle|capture
|
|
674
693
|
roadmap: render|status|next|audit|init-policy (seed: check|apply|amend; promote <id...>)
|
|
675
694
|
slot: run|list|peek|release
|
|
695
|
+
worktree: cleanup
|
|
676
696
|
models: sync|list|describe
|
|
677
697
|
review: check|start
|
|
678
698
|
protocol: list|resolve
|
|
@@ -700,7 +720,9 @@ Runtime verb arguments (accept --flag value or --flag=value):
|
|
|
700
720
|
disposition in the JSON; 2 usage or an unreadable store or plan)
|
|
701
721
|
--rationale, --proposed-by, --calling-skill classify
|
|
702
722
|
--request <text>, --request-file <file> classify (mutually exclusive; optional second opinion)
|
|
703
|
-
--
|
|
723
|
+
--view brief|full followup --action list (default brief), lifecycle --action overview, classify (default full); brief drops detail, full is the whole answer
|
|
724
|
+
--cursor <c>, --limit <n> followup --action list (the cursor is opaque: pass back a page's nextCursor); also transcript --action read
|
|
725
|
+
--intent, --query, --check, --harness route
|
|
704
726
|
--statement, --claim-id,
|
|
705
727
|
--evidence-id, --relation claim
|
|
706
728
|
--kind, --provenance, --provider,
|
|
@@ -772,6 +794,8 @@ Runtime verb arguments (accept --flag value or --flag=value):
|
|
|
772
794
|
--tag, --confirm, --feature,
|
|
773
795
|
--exclude (repeatable), --reason lifecycle (release, merged)
|
|
774
796
|
--fp, --as, --reason, --set, --all failure (list, settle, capture)
|
|
797
|
+
--run-id <id> intake --action undo (default: the latest sort run that has a move to undo)
|
|
798
|
+
--stdin | --file <path>, --stage intake --action file-prompt (the text, unchanged, from exactly one of --stdin or --file; --stage names the filing stage)
|
|
775
799
|
--harness, --need (repeatable),
|
|
776
800
|
--model-check, --budget-tokens,
|
|
777
801
|
--task, --token, --prompt-file,
|
|
@@ -54,6 +54,7 @@ const { handleLeakScanCommand } = require('../runtime/leak-scan.js');
|
|
|
54
54
|
const { handleRecoverCommand } = require('../runtime/recovery.js');
|
|
55
55
|
const { handleScaffoldCommand } = require('../runtime/scaffold/generate.js');
|
|
56
56
|
const { handleDecisionCommand, isReservedChannel, CHANNEL_RESERVED } = require('../runtime/decision-register.js');
|
|
57
|
+
const { handleIntakeCommand } = require('../runtime/intake/index.js');
|
|
57
58
|
const { handleFollowupCommand, handleLifecycleCommand, handleGoalCommand } = require('../runtime/lifecycle/cli.js');
|
|
58
59
|
const { projectRoot: lifecycleProjectRoot } = require('../runtime/lifecycle/root.js');
|
|
59
60
|
const { upkeepAfterVerb } = require('../runtime/storage/upkeep.js');
|
|
@@ -66,6 +67,8 @@ const { handleProtocolCommand } = require('../runtime/review/protocol-command.js
|
|
|
66
67
|
const { handleRulesCommand } = require('../runtime/rules-report.js');
|
|
67
68
|
const { handlePackageCommand } = require('../runtime/packages/command.js');
|
|
68
69
|
const { handleModelsCommand } = require('../runtime/models/command.js');
|
|
70
|
+
const { handleWorktreeCommand } = require('../runtime/roadmap/worktree-cleanup.js');
|
|
71
|
+
const { handleAttendanceCommand } = require('../runtime/roadmap/attendance.js');
|
|
69
72
|
// `plan-check` is read-only (115 IC-001): no upkeep, which would write `.gitignore` and `upkeep-due`.
|
|
70
73
|
// `review` writes only its one telemetry line (095 IC-003).
|
|
71
74
|
// `protocol` only prints the registry and the resolved protocol (119 IC-004), so it has nothing to keep up.
|
|
@@ -73,9 +76,10 @@ const { handleModelsCommand } = require('../runtime/models/command.js');
|
|
|
73
76
|
// `primary` only prints the resolved main session (120 IC-008) and writes nothing.
|
|
74
77
|
// `fleet` only lists the projects this machine knows from two read-only stores (093 IC-019) and writes nothing.
|
|
75
78
|
// `transcript` only reads agents' transcripts and DoFlow's own records and prints them (093 IC-022, IC-023); it writes nothing.
|
|
79
|
+
// `attendance` is read-only; `worktree --action cleanup` removes a merged feature's worktree under the roadmap lease and writes no DoFlow state, so neither runs upkeep.
|
|
76
80
|
// `package` stays here (122 IC-012): `new` writes only the target folder; `add --plan`, `update --plan` and `remove` write the store and lock and run upkeep (see `runsUpkeep`).
|
|
77
81
|
// `slot` is not here: `run`, `list` and `release` write the slot files and run upkeep; only `slot --action peek` is read-only and skips it (093 IC-018 b, see `runsUpkeep`).
|
|
78
|
-
const NO_UPKEEP = new Set(['storage', 'config', 'plan-check', 'review', 'protocol', 'rules', 'primary', 'package', 'models', 'fleet', 'transcript']);
|
|
82
|
+
const NO_UPKEEP = new Set(['storage', 'config', 'plan-check', 'review', 'protocol', 'rules', 'primary', 'package', 'models', 'fleet', 'transcript', 'worktree', 'attendance']);
|
|
79
83
|
const runsUpkeep = (o ) => (!NO_UPKEEP.has(o.cmd) && !(o.cmd === 'slot' && o.actionGiven && o.action === 'peek'))
|
|
80
84
|
|| (o.cmd === 'package' && ((typeof o.planId === 'string' && (o.action === 'add' || o.action === 'update')) || o.action === 'remove'));
|
|
81
85
|
// Required on first use, so a CLI whose failure modules cannot be loaded still runs every other verb.
|
|
@@ -378,10 +382,12 @@ function dispatchRuntimeCommandInner(o , moment ) {
|
|
|
378
382
|
// The feature is resolved from the working directory (or --slug) by the same resolver
|
|
379
383
|
// `scaffold` uses; the flags are the caller's own statements, validated by the register module.
|
|
380
384
|
case 'decision': return handleDecisionCommand({ action: o.action, projectRoot: evidenceRoot(o), slug: o.slug, json: o.json, flags: { topic: o.topic, statement: o.statement, channel: o.channel, stage: o.stage, rationale: o.rationale, supersedes: o.supersedes, refs: o.refs, source: o.source, batch: o.batchPath, all: o.all } });
|
|
385
|
+
// 136: the feature is resolved the way `decision` resolves it; the engine moves files only inside its additional-context folder.
|
|
386
|
+
case 'intake': return handleIntakeCommand({ action: o.action, projectRoot: evidenceRoot(o), slug: o.slug, json: o.json, flags: { runId: o.runId, stdin: o.stdin, file: o.file, stage: o.stage } });
|
|
381
387
|
// Feature 046: the project's follow-up store lives at the repository root (never `-g`, which
|
|
382
388
|
// the handlers refuse), so the working directory is the starting point, not a scope switch.
|
|
383
|
-
case 'followup': return handleFollowupCommand({ action: o.actionGiven ? o.action : undefined, cwd: evidenceRoot(o), global: o.global, slug: o.slug, json: o.json, flags: { statement: o.statement, stage: o.stage, source: o.source, taskClass: o.taskClass, taskId: o.taskId, release: o.release, batch: o.batchPath, channel: o.channel, state: o.state, ids: o.ids, as: o.as, reason: o.reason, evidence: o.evidence, title: o.title, file: o.file, stdin: o.stdin, text: o.text, feature: o.feature, tag: o.tag, confirm: o.confirm, exclude: o.exclude } });
|
|
384
|
-
case 'lifecycle': return handleLifecycleCommand({ action: o.actionGiven ? o.action : undefined, cwd: evidenceRoot(o), global: o.global, slug: o.slug, json: o.json, flags: { take: o.take, intent: o.intent, goal: o.goal, maintain: o.maintain, since: o.since, tag: o.tag, confirm: o.confirm, feature: o.feature, exclude: o.exclude, reason: o.reason, channel: o.channel } });
|
|
389
|
+
case 'followup': return handleFollowupCommand({ action: o.actionGiven ? o.action : undefined, cwd: evidenceRoot(o), global: o.global, slug: o.slug, json: o.json, flags: { statement: o.statement, stage: o.stage, source: o.source, taskClass: o.taskClass, taskId: o.taskId, release: o.release, batch: o.batchPath, channel: o.channel, state: o.state, ids: o.ids, view: o.view, cursor: o.cursor, limit: o.limit, as: o.as, reason: o.reason, evidence: o.evidence, title: o.title, file: o.file, stdin: o.stdin, text: o.text, feature: o.feature, tag: o.tag, confirm: o.confirm, exclude: o.exclude } });
|
|
390
|
+
case 'lifecycle': return handleLifecycleCommand({ action: o.actionGiven ? o.action : undefined, cwd: evidenceRoot(o), global: o.global, slug: o.slug, json: o.json, flags: { view: o.view, take: o.take, intent: o.intent, goal: o.goal, maintain: o.maintain, since: o.since, tag: o.tag, confirm: o.confirm, feature: o.feature, exclude: o.exclude, reason: o.reason, channel: o.channel } });
|
|
385
391
|
case 'goal': return handleGoalCommand({ action: o.actionGiven ? o.action : undefined, cwd: evidenceRoot(o), global: o.global, slug: o.slug, json: o.json, flags: { goal: o.goal, statement: o.statement, item: o.item, text: o.text, evidence: o.evidence, unmet: o.unmet, replace: o.replace, reason: o.reason, channel: o.channel } });
|
|
386
392
|
// The failure store is per machine, so `-g` changes nothing and the working directory only matters
|
|
387
393
|
// to `settle --as imported`, which looks for the DoFlow repository from there.
|
|
@@ -389,7 +395,7 @@ function dispatchRuntimeCommandInner(o , moment ) {
|
|
|
389
395
|
// The rest of design §4.2's Node arm. REPO_ROOT locates the registries that ship with the
|
|
390
396
|
// package (workflows, capabilities, verification); evidenceRoot(o) locates the caller's own
|
|
391
397
|
// state and source tree, following the same scope rules as every other command.
|
|
392
|
-
case 'classify': return handleClassifyCommand({ taskClass: o.taskClass, rationale: o.rationale, proposedBy: o.proposedBy, callingSkill: o.callingSkill, request: o.request, requestFile: o.requestFile, taskId: o.taskId, projectRoot: evidenceRoot(o), json: o.json });
|
|
398
|
+
case 'classify': return handleClassifyCommand({ taskClass: o.taskClass, rationale: o.rationale, proposedBy: o.proposedBy, callingSkill: o.callingSkill, view: o.view, request: o.request, requestFile: o.requestFile, taskId: o.taskId, projectRoot: evidenceRoot(o), json: o.json });
|
|
393
399
|
case 'workflow': return handleWorkflowCommand({ taskClass: o.taskClass, json: o.json });
|
|
394
400
|
case 'orchestrate': return handleOrchestrateCommand({ action: o.action, taskId: o.taskId, taskClass: o.taskClass, stage: o.stage, gate: o.gate, node: o.node, decision: o.decision, policy: o.policy, note: o.note, reason: o.reason, forced: o.forced, verificationPlan: o.verificationPlan, scope: o.scope, invariants: o.invariants, result: o.result, mcpOutcome: o.mcpOutcome, callingSkill: o.callingSkill, slug: o.slug, recordSlug: storeSlug, dryRun: o.dryRun, json: o.json, repoRoot: REPO_ROOT, stateRoot: storeRoot, sourceRoot: evidenceRoot(o), buildAdapters: buildAdapterRegistry });
|
|
395
401
|
case 'research-request': return handleResearchRequestCommand({ slug: storeSlug, action: o.action === 'status' ? 'list' : o.action, taskId: requireTaskId(o), stageId: o.stageId, question: o.question, reason: o.reason, blocking: o.blocking === undefined ? undefined : o.blocking === 'true' ? true : o.blocking === 'false' ? false : o.blocking, requestId: o.requestId, outcome: o.researchOutcome, claimId: o.claimId, evidenceIds: o.evidenceIds, gap: o.gap, json: o.json, projectRoot: storeRoot, sourceRoot: evidenceRoot(o) });
|
|
@@ -409,7 +415,7 @@ function dispatchRuntimeCommandInner(o , moment ) {
|
|
|
409
415
|
case 'brief-tools': return handleBriefToolsCommand({ owner: o.owner, tier: o.tier, harness: o.harness, slug: o.slug, headingLevel: o.headingLevel, tasks: o.tasks, json: o.json, repoRoot: REPO_ROOT, projectRoot: storeRoot, buildAdapters: buildAdapterRegistry });
|
|
410
416
|
case 'advise': return handleAdviseCommand({ action: o.action, actionGiven: o.actionGiven, kind: o.kind, server: o.server, state: o.state, stateFile: o.stateFile, staticAnswer: o.staticAnswer, questionsFile: o.questionsFile, taskId: o.taskId, pickId: o.pickId, actual: o.actual, model: o.model, endpoint: o.endpoint, off: o.off, project: o.project, force: o.force, global: o.global, json: o.json, projectRoot: evidenceRoot(o) });
|
|
411
417
|
case 'model-role': return handleModelRoleCommand({ role: o.role, exclude: o.exclude, excludeFamilies: o.excludeFamily, json: o.json, repoRoot: REPO_ROOT });
|
|
412
|
-
case 'route': return handleRouteCommand({ intent: o.intent, query: o.query, check: o.check, json: o.json, projectRoot: evidenceRoot(o) });
|
|
418
|
+
case 'route': return handleRouteCommand({ intent: o.intent, query: o.query, harness: o.harness, check: o.check, json: o.json, projectRoot: evidenceRoot(o) });
|
|
413
419
|
case 'claim': return handleClaimCommand({ slug: storeSlug, taskId: requireTaskId(o), action: o.action, statement: o.statement, claimId: o.claimId, evidenceId: o.evidenceId, replacedBy: o.replacedBy, relation: o.relation, role: o.role, json: o.json, stateRoot: storeRoot });
|
|
414
420
|
case 'context-pack': return handleContextPackCommand({ taskId: requireTaskId(o), taskClass: o.taskClass, objective: o.objective, json: o.json, stateRoot: storeRoot, sourceRoot: evidenceRoot(o), slug: storeSlug, explicitSlug: o.slug || null, mandatoryFile: o.mandatoryFile, harness: o.harness, maxBytes: o.maxBytes, bare: o.bare, noRecord: o.noRecord, reason: o.reason, since: o.since, expand: o.expand });
|
|
415
421
|
// 078 IC-005 and IC-013: identity is the process environment's alone, never a flag.
|
|
@@ -432,6 +438,8 @@ function dispatchRuntimeCommandInner(o , moment ) {
|
|
|
432
438
|
case 'fleet': return handleFleetCommand({ action: o.actionGiven ? o.action : undefined, json: o.json, flags: o.flagsGiven, env: process.env, cwd: process.cwd() });
|
|
433
439
|
case 'transcript': return handleTranscriptCommand({ action: o.actionGiven ? o.action : undefined, agent: o.agent, cursor: o.cursor, limit: o.limit, window: o.window, json: o.json, flags: o.flagsGiven, env: process.env, cwd: process.cwd() });
|
|
434
440
|
case 'models': return handleModelsCommand({ action: o.actionGiven ? o.action : (o.positional?.[0] || 'list'), positional: o.positional, harness: o.harness, model: o.model, json: o.json, projectRoot: evidenceRoot(o) });
|
|
441
|
+
case 'worktree': return handleWorktreeCommand({ action: o.actionGiven ? o.action : undefined, slug: o.slug, json: o.json, flags: o.flagsGiven, env: process.env, cwd: process.cwd() });
|
|
442
|
+
case 'attendance': return handleAttendanceCommand({ taskId: o.taskId, run: o.run, json: o.json, flags: o.flagsGiven, env: process.env, cwd: process.cwd() });
|
|
435
443
|
default: console.error(`doflow: unknown command '${o.cmd}'`); process.exit(1);
|
|
436
444
|
}
|
|
437
445
|
}
|
package/src/cli/shared.js
CHANGED
|
@@ -10,7 +10,7 @@ const fs = require('node:fs');
|
|
|
10
10
|
const os = require('node:os');
|
|
11
11
|
const path = require('node:path');
|
|
12
12
|
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
13
|
-
const { doflowPaths } = require('../install/paths.js');
|
|
13
|
+
const { doflowPaths, canonicalPath, canonicalLedger } = require('../install/paths.js');
|
|
14
14
|
const { readInstallManifest } = require('../install/manifest.js');
|
|
15
15
|
const { DEFAULT_BACKUP_RETENTION, formatBytes } = require('../install/backup.js');
|
|
16
16
|
const { stateRoot, readLedger } = require('../state/index.js');
|
|
@@ -64,7 +64,9 @@ function buildAdapterRegistry() {
|
|
|
64
64
|
|
|
65
65
|
/** Resolve {global, projectRoot} scope options for src/install/targets.js#toolDirs from parsed args. */
|
|
66
66
|
function scopeOf(o ) {
|
|
67
|
-
|
|
67
|
+
// The single place a project path enters the verbs: canonical, so an aliased spelling (macOS
|
|
68
|
+
// /var -> /private/var) and the real one name the same scope, ledger rows and lock.
|
|
69
|
+
return { global: o.global, projectRoot: o.global ? (o.positional[0] || '.') : canonicalPath(o.positional[0] || '.') };
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
function installPaths(scope ) {
|
|
@@ -141,7 +143,7 @@ function scopeSelectionState(scope ) {
|
|
|
141
143
|
const scopeRoot = scope.global ? os.homedir() : path.resolve(scope.projectRoot);
|
|
142
144
|
return {
|
|
143
145
|
lock: readLock(scope.global ? { scope: 'global', homeDir: scopeRoot } : { scope: 'project', projectRoot: scopeRoot }),
|
|
144
|
-
ledger: readLedger(stateRoot({ scope: scope.global ? 'global' : 'project', projectRoot: scopeRoot, homeDir: scopeRoot })),
|
|
146
|
+
ledger: canonicalLedger(readLedger(stateRoot({ scope: scope.global ? 'global' : 'project', projectRoot: scopeRoot, homeDir: scopeRoot })), scopeRoot),
|
|
145
147
|
manifestServers: readInstallManifest({ scopeRoot })?.mcpServers ?? null,
|
|
146
148
|
};
|
|
147
149
|
}
|
|
@@ -70,8 +70,7 @@ function mergeMarkedSection(srcAbs , dstAbs , { dryRun = false, co
|
|
|
70
70
|
// original file's matching trailing "\n" (if present) must be consumed here —
|
|
71
71
|
// otherwise a replace with no real change grows the file by one newline
|
|
72
72
|
// every time it runs (breaks idempotency).
|
|
73
|
-
|
|
74
|
-
if (existing[afterEnd] === '\n') afterEnd += 1;
|
|
73
|
+
const afterEnd = afterEndMarker(existing, endIdx + MARKER_END.length);
|
|
75
74
|
newContent = existing.slice(0, startIdx) + section + existing.slice(afterEnd);
|
|
76
75
|
}
|
|
77
76
|
}
|
|
@@ -84,10 +83,16 @@ function mergeMarkedSection(srcAbs , dstAbs , { dryRun = false, co
|
|
|
84
83
|
return { changed: true };
|
|
85
84
|
}
|
|
86
85
|
|
|
86
|
+
/** `at` moved past the one line break (LF or CRLF) that follows an end marker, if there is one. */
|
|
87
|
+
function afterEndMarker(text , at ) {
|
|
88
|
+
if (text[at] === '\n') return at + 1;
|
|
89
|
+
return text[at] === '\r' && text[at + 1] === '\n' ? at + 2 : at;
|
|
90
|
+
}
|
|
91
|
+
|
|
87
92
|
/**
|
|
88
93
|
* Where `text`'s doflow span is: the first of `startMarkers`, the first MARKER_END after it, and no
|
|
89
94
|
* start marker after that end (a second span is `malformed`). `after` is past the end marker and the
|
|
90
|
-
* one
|
|
95
|
+
* one line break (LF or CRLF) that follows it.
|
|
91
96
|
*/
|
|
92
97
|
function markedSpan(text , startMarkers = [MARKER_START]) {
|
|
93
98
|
const found = startMarkers.map((marker) => ({ at: text.indexOf(marker), length: marker.length })).filter((item) => item.at !== -1);
|
|
@@ -95,10 +100,9 @@ function markedSpan(text , startMarkers = [MARKER_START])
|
|
|
95
100
|
const first = found.reduce((a, b) => (b.at < a.at ? b : a));
|
|
96
101
|
const end = text.indexOf(MARKER_END, first.at + first.length);
|
|
97
102
|
if (end === -1) return { state: 'malformed' };
|
|
98
|
-
|
|
99
|
-
if (startMarkers.some((marker) => text.indexOf(marker,
|
|
100
|
-
|
|
101
|
-
return { state: 'ok', start: first.at, after };
|
|
103
|
+
const closed = end + MARKER_END.length;
|
|
104
|
+
if (startMarkers.some((marker) => text.indexOf(marker, closed) !== -1)) return { state: 'malformed' };
|
|
105
|
+
return { state: 'ok', start: first.at, after: afterEndMarker(text, closed) };
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
/**
|
|
@@ -89,8 +89,9 @@ function addCandidate(candidates , file , harness
|
|
|
89
89
|
* lock); an entry at or under one of them is reported in `excluded`, not backed up.
|
|
90
90
|
* `records` lists DoFlow record files backed up beside those files although an `exclude` root holds
|
|
91
91
|
* them (the ownership ledger), with no harness; a rollback restores only the ledger rows of the files
|
|
92
|
-
* it writes back (backup.ts mergeRestoredLedger). They join
|
|
93
|
-
*
|
|
92
|
+
* it writes back (backup.ts mergeRestoredLedger). They join a set that holds some file already, or one whose
|
|
93
|
+
* plan gives up a retired harness's ledger row (only the ledger is then snapshotted); a run that changes
|
|
94
|
+
* neither backs up nothing.
|
|
94
95
|
*/
|
|
95
96
|
function backupSetFromPlan({ plan, scope, scopeRoot, exclude = [], records = [], fsImpl = fs } ) {
|
|
96
97
|
const candidates = new Map();
|
|
@@ -131,7 +132,10 @@ function backupSetFromPlan({ plan, scope, scopeRoot, exclude = [], records = [],
|
|
|
131
132
|
const excluded = [...mcpOnly]
|
|
132
133
|
.filter(([target, onlyMcp]) => onlyMcp && !candidates.has(target))
|
|
133
134
|
.map(([target]) => ({ path: target, reason: 'mcp-entries-only' }));
|
|
134
|
-
|
|
135
|
+
// A retired harness's row given up (every sweep action but `defer`) is a ledger change even when no file
|
|
136
|
+
// changes: the ledger alone is snapshotted then, so a rollback restores the row. Deferred rows stay.
|
|
137
|
+
const givesUpRow = plan.changes.some((change ) => change.retired === true && change.action !== 'defer');
|
|
138
|
+
if (candidates.size || givesUpRow) for (const file of records) addCandidate(candidates, file, null, 'record');
|
|
135
139
|
return toBackupSet(candidates, { scope, scopeRoot, exclude, excluded, records, fsImpl });
|
|
136
140
|
}
|
|
137
141
|
|
package/src/install/backup.js
CHANGED
|
@@ -591,11 +591,12 @@ function planRestore({ bid, backupRoot, scope, scopeRoot, targets, explicitTarge
|
|
|
591
591
|
* staler than the current one; a current row of it the backup lacks stays. A restored row whose file
|
|
592
592
|
* a live harness now claims is released by the next sweep (claim rule 2), file untouched.
|
|
593
593
|
*/
|
|
594
|
-
function mergeRestoredLedger(backup , current , restored , retired = new Set()) {
|
|
594
|
+
function mergeRestoredLedger(backup , current , restored , retired = new Set(), lost = new Set()) {
|
|
595
595
|
if (!current) return backup;
|
|
596
596
|
const at = (target ) => typeof target === 'string' && restored.has(path.resolve(target));
|
|
597
597
|
const whole = (row ) => retired.has(row?.harness);
|
|
598
|
-
|
|
598
|
+
// A retired row whose file this restore tried and could not write back stays out: it would name a file the next sweep drops as absent.
|
|
599
|
+
const back = backup.resources.filter((r ) => at(r.target) || (whole(r) && !(typeof r.target === 'string' && lost.has(path.resolve(r.target)))));
|
|
599
600
|
const backKeys = new Set(back.filter(whole).map(ownershipKey));
|
|
600
601
|
const targets = Object.entries(backup.targets ?? {}).filter(([id]) => retired.has(id));
|
|
601
602
|
return {
|
|
@@ -610,20 +611,23 @@ function mergeRestoredLedger(backup , current , restored , r
|
|
|
610
611
|
}
|
|
611
612
|
|
|
612
613
|
/** Ids the registry retires (row 127 IC-001): `registry` when a caller passes one, else the shipped
|
|
613
|
-
* registry. One that cannot be read
|
|
614
|
+
* registry. One that cannot be read throws, so a restore stops before it writes anything rather than
|
|
615
|
+
* silently skipping the retired rows and targets. */
|
|
614
616
|
function retiredIds(registry ) {
|
|
615
617
|
if (registry === undefined) {
|
|
616
618
|
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
617
|
-
try { registry = require('../registry/index.js').loadRegistry({ repoRoot: REPO_ROOT }); } catch {
|
|
619
|
+
try { registry = require('../registry/index.js').loadRegistry({ repoRoot: REPO_ROOT }); } catch (err ) {
|
|
620
|
+
throw new BackupError(`cannot read the registry to find retired harnesses: ${err?.message ?? err}`);
|
|
621
|
+
}
|
|
618
622
|
}
|
|
619
623
|
return new Set((Array.isArray(registry?.retired) ? registry.retired : []).map((entry ) => entry?.id).filter((id ) => typeof id === 'string'));
|
|
620
624
|
}
|
|
621
625
|
|
|
622
626
|
/** The bytes a ledger restore item writes: the backup's ledger merged onto the current one. */
|
|
623
|
-
function restoredLedgerBytes(item , restored , retired , fsImpl ) {
|
|
627
|
+
function restoredLedgerBytes(item , restored , retired , lost , fsImpl ) {
|
|
624
628
|
const backup = validateLedger(JSON.parse(fsImpl.readFileSync(item.stored, 'utf8')));
|
|
625
629
|
const current = fsImpl.existsSync(item.path) ? validateLedger(JSON.parse(fsImpl.readFileSync(item.path, 'utf8'))) : null;
|
|
626
|
-
return Buffer.from(`${JSON.stringify(validateLedger(mergeRestoredLedger(backup, current, restored, retired)), null, 2)}\n`);
|
|
630
|
+
return Buffer.from(`${JSON.stringify(validateLedger(mergeRestoredLedger(backup, current, restored, retired, lost)), null, 2)}\n`);
|
|
627
631
|
}
|
|
628
632
|
|
|
629
633
|
/**
|
|
@@ -641,6 +645,8 @@ function executeRestore(plan , { backupRoot, dryRun = false, registry, fsImp
|
|
|
641
645
|
return { restored: plan.v1.tools.map((t ) => t.tool), failed: [], absent: [], refused: [], untargeted: 0, legacy: true };
|
|
642
646
|
}
|
|
643
647
|
|
|
648
|
+
// Read before the first write: an unreadable registry stops the restore with nothing changed.
|
|
649
|
+
const retired = dryRun || plan.narrowed || !plan.restore.some((item ) => item.ledger) ? new Set () : retiredIds(registry);
|
|
644
650
|
const restored = [];
|
|
645
651
|
const failed = [];
|
|
646
652
|
const files = new Set ();
|
|
@@ -654,7 +660,7 @@ function executeRestore(plan , { backupRoot, dryRun = false, registry, fsImp
|
|
|
654
660
|
try {
|
|
655
661
|
const dir = path.dirname(item.path);
|
|
656
662
|
fsImpl.mkdirSync(dir, { recursive: true });
|
|
657
|
-
const bytes = item.ledger ? restoredLedgerBytes(item, files, plan.
|
|
663
|
+
const bytes = item.ledger ? restoredLedgerBytes(item, files, retired, new Set([...failed.map((f ) => f.path), ...plan.refused.map((f ) => f.path)].filter((p ) => typeof p === 'string').map((p ) => path.resolve(p))), fsImpl) : fsImpl.readFileSync(item.stored);
|
|
658
664
|
let isLink = false;
|
|
659
665
|
try {
|
|
660
666
|
isLink = fsImpl.lstatSync(item.path).isSymbolicLink();
|
package/src/install/context.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
8
8
|
// caller's `require('../install/context.js')` keeps working while the tree is still `.js`.
|
|
9
9
|
const path = require('node:path');
|
|
10
|
+
const { canonicalPath } = require('./paths.js');
|
|
10
11
|
const { sourceCommit: gitSourceCommit } = require('../helper/git.js');
|
|
11
12
|
|
|
12
13
|
|
|
@@ -27,7 +28,7 @@ const { sourceCommit: gitSourceCommit } = require('../helper/git.js');
|
|
|
27
28
|
function resolveContext({ repoRoot, global, projectRoot, targets, dirs, sourceCommit, managedResources } ) {
|
|
28
29
|
const context = {
|
|
29
30
|
scope: global ? 'global' : 'project',
|
|
30
|
-
scopeRoot: global ? path.dirname(dirs.claude) :
|
|
31
|
+
scopeRoot: global ? path.dirname(dirs.claude) : canonicalPath(projectRoot),
|
|
31
32
|
targets,
|
|
32
33
|
dirs: Object.fromEntries(targets.map((t) => [t, dirs[t]])),
|
|
33
34
|
repoRoot,
|
package/src/install/manifest.js
CHANGED
|
@@ -25,6 +25,20 @@ function canonicalManifestPath(scopeRoot ) {
|
|
|
25
25
|
return path.join(path.resolve(scopeRoot), '.doflow', MANIFEST_FILE_NAME);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Atomic same-directory write of the manifest document. */
|
|
29
|
+
function atomicWrite(file , manifest , date ) {
|
|
30
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
31
|
+
// Security: the temp file must live next to `file` itself, not a shared os.tmpdir() — a
|
|
32
|
+
// world-writable /tmp plus a PID+timestamp-derived (i.e. guessable) name is a symlink-race
|
|
33
|
+
// waiting to happen (plant a symlink at the predicted path, the write clobbers its target).
|
|
34
|
+
// 'wx' (O_CREAT|O_EXCL) refuses to open through an existing symlink or file at all, and being
|
|
35
|
+
// on the same filesystem as `file` makes the final rename a guaranteed atomic same-fs op
|
|
36
|
+
// (os.tmpdir() offered no such guarantee — e.g. containers where /tmp is a separate tmpfs mount).
|
|
37
|
+
const tmp = path.join(path.dirname(file), `.install-manifest-${process.pid}-${date.getTime()}.json.tmp`);
|
|
38
|
+
fs.writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' });
|
|
39
|
+
fs.renameSync(tmp, file); // atomic on the same filesystem, matches sync.sh's mktemp + mv
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
|
|
29
43
|
|
|
30
44
|
|
|
@@ -88,19 +102,29 @@ function writeManifest({ scopeRoot, claudeDir, scriptVersion, operation, repoRoo
|
|
|
88
102
|
const resources = managedResources ?? existingManagedResources;
|
|
89
103
|
if (resources !== undefined) manifest.managed_resources = resources;
|
|
90
104
|
|
|
91
|
-
|
|
92
|
-
// Security: the temp file must live next to `file` itself, not a shared os.tmpdir() — a
|
|
93
|
-
// world-writable /tmp plus a PID+timestamp-derived (i.e. guessable) name is a symlink-race
|
|
94
|
-
// waiting to happen (plant a symlink at the predicted path, the write clobbers its target).
|
|
95
|
-
// 'wx' (O_CREAT|O_EXCL) refuses to open through an existing symlink or file at all, and being
|
|
96
|
-
// on the same filesystem as `file` makes the final rename a guaranteed atomic same-fs op
|
|
97
|
-
// (os.tmpdir() offered no such guarantee — e.g. containers where /tmp is a separate tmpfs mount).
|
|
98
|
-
const tmp = path.join(path.dirname(file), `.install-manifest-${process.pid}-${date.getTime()}.json.tmp`);
|
|
99
|
-
fs.writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' });
|
|
100
|
-
fs.renameSync(tmp, file); // atomic on the same filesystem, matches sync.sh's mktemp + mv
|
|
105
|
+
atomicWrite(file, manifest, date);
|
|
101
106
|
return file;
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
/**
|
|
110
|
+
* After a removal: stop listing `tools` as installed. Nothing else in the record changes except the
|
|
111
|
+
* operation and time, and a missing or unreadable manifest is left alone (nothing lists them).
|
|
112
|
+
* Returns whether the file was rewritten.
|
|
113
|
+
*/
|
|
114
|
+
function forgetManifestTools({ scopeRoot, tools, date = new Date() } ) {
|
|
115
|
+
const file = canonicalManifestPath(scopeRoot);
|
|
116
|
+
if (!fs.existsSync(file)) return false;
|
|
117
|
+
let manifest ;
|
|
118
|
+
try { manifest = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return false; }
|
|
119
|
+
const listed = tools.filter((tool) => manifest?.tools && Object.hasOwn(manifest.tools, tool));
|
|
120
|
+
if (!listed.length) return false;
|
|
121
|
+
for (const tool of listed) delete manifest.tools[tool];
|
|
122
|
+
manifest.last_operation = 'remove';
|
|
123
|
+
manifest.last_run = date.toISOString().replace(/\.\d+Z$/, 'Z');
|
|
124
|
+
atomicWrite(file, manifest, date);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
104
128
|
/** null if no manifest yet. */
|
|
105
129
|
function readManifestFile(file ) {
|
|
106
130
|
if (!fs.existsSync(file)) return null;
|
|
@@ -141,5 +165,5 @@ function readManifest(claudeDir ) {
|
|
|
141
165
|
|
|
142
166
|
module.exports = {
|
|
143
167
|
LEGACY_MANIFEST_FILE_NAME, MANIFEST_FILE_NAME, manifestPath, canonicalManifestPath,
|
|
144
|
-
writeManifest, readManifest, readInstallManifest,
|
|
168
|
+
writeManifest, forgetManifestTools, readManifest, readInstallManifest,
|
|
145
169
|
};
|
package/src/install/paths.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
4
4
|
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
5
5
|
// caller's `require('../install/paths.js')` keeps working while the rest of the tree is still `.js`.
|
|
6
|
+
const fs = require('node:fs');
|
|
6
7
|
const path = require('node:path');
|
|
7
8
|
|
|
8
9
|
const DOFLOW_DIR_NAME = '.doflow';
|
|
@@ -72,7 +73,44 @@ function doflowPaths({ scopeRoot } ) {
|
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/** The one spelling of a path that ownership comparisons use: `fs.realpathSync` of the resolved path,
|
|
77
|
+
* or the resolved path itself when it does not exist (nothing to follow yet). macOS reaches the
|
|
78
|
+
* temp folder as both /var/... and /private/var/..., and a ledger row written under one spelling
|
|
79
|
+
* must match the other. */
|
|
80
|
+
function canonicalPath(target ) {
|
|
81
|
+
const resolved = path.resolve(target);
|
|
82
|
+
try { return fs.realpathSync(resolved); } catch { return resolved; }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rebaseString(value , from , to ) {
|
|
86
|
+
if (value === from) return to;
|
|
87
|
+
return value.startsWith(from + path.sep) ? to + value.slice(from.length) : value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function rebaseDeep(value , from , to ) {
|
|
91
|
+
if (typeof value === 'string') return rebaseString(value, from, to);
|
|
92
|
+
if (Array.isArray(value)) return value.map((item) => rebaseDeep(item, from, to));
|
|
93
|
+
if (value && typeof value === 'object') {
|
|
94
|
+
const out = {};
|
|
95
|
+
for (const key of Object.keys(value)) out[key] = rebaseDeep(value[key], from, to);
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A ledger read from disk, with every path that names the scope root under a different spelling of
|
|
102
|
+
* it (the aliased path an earlier install recorded) rewritten to `scopeRoot`, the canonical one.
|
|
103
|
+
* The stored format is unchanged; a ledger already in canonical form, with no scope root of its
|
|
104
|
+
* own, or recording an unrelated directory is returned as it came. */
|
|
105
|
+
function canonicalLedger(ledger , scopeRoot ) {
|
|
106
|
+
const recorded = ledger && typeof ledger.scopeRoot === 'string' && ledger.scopeRoot ? path.resolve(ledger.scopeRoot) : null;
|
|
107
|
+
if (!recorded || recorded === scopeRoot || canonicalPath(recorded) !== scopeRoot) return ledger;
|
|
108
|
+
return rebaseDeep(ledger, recorded, scopeRoot);
|
|
109
|
+
}
|
|
110
|
+
|
|
75
111
|
module.exports = {
|
|
112
|
+
canonicalPath,
|
|
113
|
+
canonicalLedger,
|
|
76
114
|
DOFLOW_DIR_NAME,
|
|
77
115
|
BACKUP_DIR_NAME,
|
|
78
116
|
INSTALL_MANIFEST_FILE_NAME,
|
package/src/install/targets.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// caller's `require('../install/targets.js')` keeps working while the rest of the tree is still `.js`.
|
|
15
15
|
const os = require('node:os');
|
|
16
16
|
const path = require('node:path');
|
|
17
|
+
const { canonicalPath } = require('./paths.js');
|
|
17
18
|
|
|
18
19
|
const VALID = ['claude', 'codex', 'copilot', 'kiro', 'opencode', 'pi', 'antigravity'];
|
|
19
20
|
|
|
@@ -58,6 +59,14 @@ function resolveTargets(requested , { registry }
|
|
|
58
59
|
return [...new Set(targets)];
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/** The harnesses an install manifest lists as installed, in VALID order; [] when there is no
|
|
63
|
+
* manifest or it lists none DoFlow still knows (a retired id is not a target). `update` without
|
|
64
|
+
* `-t` acts on these instead of DEFAULT_TARGETS, so it never installs a harness the scope lacks. */
|
|
65
|
+
function installedTargets(manifest ) {
|
|
66
|
+
const tools = manifest?.tools ?? {};
|
|
67
|
+
return VALID.filter((id) => Object.hasOwn(tools, id) && tools[id]?.installed !== false);
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
|
|
62
71
|
|
|
63
72
|
|
|
@@ -91,7 +100,7 @@ function resolveTargets(requested , { registry }
|
|
|
91
100
|
*/
|
|
92
101
|
function toolDirs(opts = {}) {
|
|
93
102
|
const { global = false, projectRoot = '.' } = opts;
|
|
94
|
-
const root = global ? os.homedir() :
|
|
103
|
+
const root = global ? os.homedir() : canonicalPath(projectRoot);
|
|
95
104
|
return {
|
|
96
105
|
claude: path.join(root, '.claude'),
|
|
97
106
|
codex: path.join(root, '.codex'),
|
|
@@ -102,4 +111,4 @@ function toolDirs(opts = {}) {
|
|
|
102
111
|
};
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
module.exports = { VALID, DEFAULT_TARGETS, resolveTargets, toolDirs };
|
|
114
|
+
module.exports = { VALID, DEFAULT_TARGETS, resolveTargets, installedTargets, toolDirs };
|