cclaw-cli 0.5.14 → 0.5.16
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 +6 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +76 -12
- package/dist/content/agents.js +3 -3
- package/dist/content/contracts.js +8 -2
- package/dist/content/examples.js +23 -17
- package/dist/content/stage-schema.js +20 -10
- package/dist/content/subagents.js +1 -1
- package/dist/content/templates.js +6 -2
- package/dist/runs.d.ts +17 -0
- package/dist/runs.js +114 -5
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -120,6 +120,12 @@ Required repository secret:
|
|
|
120
120
|
└── runs/ # archived feature snapshots (YYYY-MM-DD-feature-name)
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
## Harness Integration
|
|
124
|
+
|
|
125
|
+
Supported harnesses: `claude`, `cursor`, `opencode`, `codex`. The full
|
|
126
|
+
per-harness install surface, feature matrix, and lifecycle details live in
|
|
127
|
+
[docs/harnesses.md](./docs/harnesses.md).
|
|
128
|
+
|
|
123
129
|
## License
|
|
124
130
|
|
|
125
131
|
[MIT](./LICENSE)
|
package/dist/cli.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ interface ParsedArgs {
|
|
|
6
6
|
harnesses?: HarnessId[];
|
|
7
7
|
reconcileGates?: boolean;
|
|
8
8
|
archiveName?: string;
|
|
9
|
+
showHelp?: boolean;
|
|
10
|
+
showVersion?: boolean;
|
|
9
11
|
}
|
|
12
|
+
export declare function usage(): string;
|
|
10
13
|
declare function parseHarnesses(raw: string): HarnessId[];
|
|
11
14
|
declare function parseArgs(argv: string[]): ParsedArgs;
|
|
12
15
|
export { parseArgs, parseHarnesses };
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -9,18 +9,63 @@ import { initCclaw, syncCclaw, uninstallCclaw, upgradeCclaw } from "./install.js
|
|
|
9
9
|
import { error, info } from "./logger.js";
|
|
10
10
|
import { archiveRun } from "./runs.js";
|
|
11
11
|
const INSTALLER_COMMANDS = ["init", "sync", "doctor", "upgrade", "uninstall", "archive"];
|
|
12
|
-
function usage() {
|
|
12
|
+
export function usage() {
|
|
13
13
|
return `cclaw - installer-first flow toolkit
|
|
14
14
|
|
|
15
15
|
Usage:
|
|
16
|
-
cclaw
|
|
17
|
-
cclaw
|
|
18
|
-
cclaw
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
cclaw
|
|
16
|
+
cclaw <command> [flags]
|
|
17
|
+
cclaw --help | -h
|
|
18
|
+
cclaw --version | -v
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
init Bootstrap .cclaw runtime, state, and harness shims in this project.
|
|
22
|
+
Flags: --harnesses=<list> Comma list of harnesses (claude,cursor,opencode,codex).
|
|
23
|
+
sync Regenerate harness shim files from the current .cclaw config (non-destructive).
|
|
24
|
+
doctor Run health checks against the local .cclaw runtime. Exit code 2 on failure.
|
|
25
|
+
Flags: --reconcile-gates Recompute current-stage gate evidence before checks.
|
|
26
|
+
archive Move .cclaw/artifacts into .cclaw/runs/<date>-<slug> and reset flow state.
|
|
27
|
+
Flags: --name=<feature> Feature slug (default: inferred from 00-idea.md).
|
|
28
|
+
upgrade Refresh generated files in .cclaw without modifying user artifacts.
|
|
29
|
+
uninstall Remove .cclaw runtime and the generated harness shim files.
|
|
30
|
+
|
|
31
|
+
Global flags:
|
|
32
|
+
-h, --help Show this help message and exit 0.
|
|
33
|
+
-v, --version Print the cclaw CLI version and exit 0.
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
cclaw init --harnesses=claude,cursor
|
|
37
|
+
cclaw doctor --reconcile-gates
|
|
38
|
+
cclaw archive --name=payments-revamp
|
|
39
|
+
|
|
40
|
+
Docs: https://github.com/zuevrs/cclaw
|
|
41
|
+
Issues: https://github.com/zuevrs/cclaw/issues
|
|
22
42
|
`;
|
|
23
43
|
}
|
|
44
|
+
function cliPackageVersion() {
|
|
45
|
+
try {
|
|
46
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
47
|
+
const candidates = [
|
|
48
|
+
path.resolve(here, "../package.json"),
|
|
49
|
+
path.resolve(here, "../../package.json")
|
|
50
|
+
];
|
|
51
|
+
for (const candidate of candidates) {
|
|
52
|
+
try {
|
|
53
|
+
const raw = readFileSync(candidate, "utf8");
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
if (parsed.name === "cclaw-cli" && typeof parsed.version === "string") {
|
|
56
|
+
return parsed.version;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// fall through
|
|
66
|
+
}
|
|
67
|
+
return "unknown";
|
|
68
|
+
}
|
|
24
69
|
function parseHarnesses(raw) {
|
|
25
70
|
const requested = raw
|
|
26
71
|
.split(",")
|
|
@@ -33,11 +78,19 @@ function parseHarnesses(raw) {
|
|
|
33
78
|
return requested;
|
|
34
79
|
}
|
|
35
80
|
function parseArgs(argv) {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
81
|
+
const parsed = {};
|
|
82
|
+
const helpFlag = argv.find((arg) => arg === "--help" || arg === "-h");
|
|
83
|
+
if (helpFlag) {
|
|
84
|
+
parsed.showHelp = true;
|
|
85
|
+
}
|
|
86
|
+
const versionFlag = argv.find((arg) => arg === "--version" || arg === "-v");
|
|
87
|
+
if (versionFlag) {
|
|
88
|
+
parsed.showVersion = true;
|
|
89
|
+
}
|
|
90
|
+
const [commandRaw, ...flags] = argv.filter((arg) => arg !== "--help" && arg !== "-h" && arg !== "--version" && arg !== "-v");
|
|
91
|
+
parsed.command = INSTALLER_COMMANDS.includes(commandRaw)
|
|
38
92
|
? commandRaw
|
|
39
93
|
: undefined;
|
|
40
|
-
const parsed = { command };
|
|
41
94
|
for (const flag of flags) {
|
|
42
95
|
if (flag.startsWith("--harnesses=")) {
|
|
43
96
|
parsed.harnesses = parseHarnesses(flag.replace("--harnesses=", ""));
|
|
@@ -54,6 +107,14 @@ function parseArgs(argv) {
|
|
|
54
107
|
return parsed;
|
|
55
108
|
}
|
|
56
109
|
async function runCommand(parsed, ctx) {
|
|
110
|
+
if (parsed.showHelp) {
|
|
111
|
+
ctx.stdout.write(usage());
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
if (parsed.showVersion) {
|
|
115
|
+
ctx.stdout.write(`cclaw ${cliPackageVersion()}\n`);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
57
118
|
const command = parsed.command;
|
|
58
119
|
if (!command) {
|
|
59
120
|
ctx.stderr.write(usage());
|
|
@@ -88,7 +149,10 @@ async function runCommand(parsed, ctx) {
|
|
|
88
149
|
}
|
|
89
150
|
if (command === "archive") {
|
|
90
151
|
const archived = await archiveRun(ctx.cwd, parsed.archiveName);
|
|
91
|
-
|
|
152
|
+
const snapshotSummary = archived.snapshottedStateFiles.length > 0
|
|
153
|
+
? ` Snapshotted ${archived.snapshottedStateFiles.length} state file(s) under ${archived.archivePath}/state and wrote archive-manifest.json.`
|
|
154
|
+
: "";
|
|
155
|
+
info(ctx, `Archived active artifacts to ${archived.archivePath}. Flow state reset to brainstorm.${snapshotSummary}`);
|
|
92
156
|
return 0;
|
|
93
157
|
}
|
|
94
158
|
await uninstallCclaw(ctx.cwd);
|
package/dist/content/agents.js
CHANGED
|
@@ -216,7 +216,7 @@ export function agentRoutingTable() {
|
|
|
216
216
|
| Brainstorm (start with \`/cc <idea>\`) | planner | — |
|
|
217
217
|
| Scope / Design / Spec / Plan (advance via \`/cc-next\`) | planner | security-reviewer on design, spec-reviewer on spec |
|
|
218
218
|
| TDD (via \`/cc-next\`) | test-author | doc-updater |
|
|
219
|
-
| Review (via \`/cc-next\`) | spec-reviewer, code-reviewer
|
|
219
|
+
| Review (via \`/cc-next\`) | spec-reviewer, code-reviewer, security-reviewer | — |
|
|
220
220
|
| Ship (via \`/cc-next\`) | — | doc-updater |
|
|
221
221
|
`;
|
|
222
222
|
}
|
|
@@ -231,8 +231,8 @@ Cclaw provides specialist agents under \`.cclaw/agents/\` for targeted delegatio
|
|
|
231
231
|
${agentRoutingTable()}
|
|
232
232
|
|
|
233
233
|
**Activation modes:**
|
|
234
|
-
- **Mandatory:** MUST be used when the related stage runs (spec-reviewer, code-reviewer during review)
|
|
235
|
-
- **Proactive:** Should be used automatically when context matches (planner for complex features, security-reviewer
|
|
234
|
+
- **Mandatory:** MUST be used when the related stage runs (spec-reviewer, code-reviewer, and security-reviewer during review; planner during scope and design; test-author during tdd; doc-updater during ship). Even if a change has no trust-boundary impact, security-reviewer produces an explicit no-change attestation.
|
|
235
|
+
- **Proactive:** Should be used automatically when context matches (planner for complex features, security-reviewer escalations outside review, doc-updater on behavior changes)
|
|
236
236
|
- **On-demand:** Invoked only when explicitly requested
|
|
237
237
|
|
|
238
238
|
**Agent files:** \`.cclaw/agents/{name}.md\` — each contains YAML frontmatter with tools and model tier.
|
|
@@ -11,6 +11,12 @@ export function commandContract(stage) {
|
|
|
11
11
|
const gateIds = schema.requiredGates
|
|
12
12
|
.map((g) => `\`${g.id}\``)
|
|
13
13
|
.join(", ");
|
|
14
|
+
const writes = schema.crossStageTrace.writesTo;
|
|
15
|
+
const writesLine = writes.map((w) => `\`${w}\``).join(", ");
|
|
16
|
+
const primaryArtifact = `.cclaw/artifacts/${schema.artifactFile}`;
|
|
17
|
+
const writeStepPaths = writes.length > 1
|
|
18
|
+
? writes.map((w) => `\`${w}\``).join(" and ")
|
|
19
|
+
: `\`${primaryArtifact}\``;
|
|
14
20
|
return `# /cc-${stage}
|
|
15
21
|
|
|
16
22
|
Load and follow **${skillPath}** — it contains the full checklist, examples, interaction protocol, and verification discipline.
|
|
@@ -20,7 +26,7 @@ ${schema.hardGate}
|
|
|
20
26
|
|
|
21
27
|
## In / Out
|
|
22
28
|
- **Reads:** ${readsLine}
|
|
23
|
-
- **Writes:**
|
|
29
|
+
- **Writes:** ${writesLine}
|
|
24
30
|
- **Next:** \`/cc-next\` (updates flow-state and loads the next stage)
|
|
25
31
|
|
|
26
32
|
## Context Hydration (mandatory before stage work)
|
|
@@ -29,7 +35,7 @@ ${schema.hardGate}
|
|
|
29
35
|
3. Load required upstream artifacts for this stage:
|
|
30
36
|
${hydrationLines}
|
|
31
37
|
4. Load \`.cclaw/knowledge.md\` and apply relevant entries.
|
|
32
|
-
5. Write stage output to
|
|
38
|
+
5. Write stage output to ${writeStepPaths}.
|
|
33
39
|
6. Do NOT copy artifacts into \`.cclaw/runs/\`; archival is handled only by \`cclaw archive\`.
|
|
34
40
|
|
|
35
41
|
## Gates
|
package/dist/content/examples.js
CHANGED
|
@@ -398,33 +398,39 @@ Execution rule: complete and verify each wave before starting the next wave.
|
|
|
398
398
|
### Final Verdict
|
|
399
399
|
|
|
400
400
|
- BLOCKED`,
|
|
401
|
-
ship: `### Preflight
|
|
401
|
+
ship: `### Preflight Results
|
|
402
402
|
|
|
403
|
-
-
|
|
404
|
-
-
|
|
405
|
-
-
|
|
406
|
-
-
|
|
403
|
+
- Review verdict: APPROVED_WITH_CONCERNS (R-1 resolved, R-2 accepted as known debt)
|
|
404
|
+
- Build: pass (\`pnpm build\` succeeds)
|
|
405
|
+
- Tests: pass (\`pnpm vitest run && pnpm playwright test\` — 47 passed, 0 failed)
|
|
406
|
+
- Lint: pass (\`pnpm lint\` clean)
|
|
407
|
+
- Type-check: pass (\`pnpm typecheck\` clean)
|
|
408
|
+
- Working tree clean: yes (\`git status\` shows no uncommitted changes)
|
|
407
409
|
|
|
408
|
-
### Release
|
|
410
|
+
### Release Notes
|
|
409
411
|
|
|
410
|
-
- **Added:** In-app notification feed with SSE updates and REST fallback snapshotting.
|
|
411
|
-
- **Changed:** Notification payloads now include a stable dedupe key for idempotent rendering.
|
|
412
|
+
- **Added:** In-app notification feed with SSE updates and REST fallback snapshotting (AC-1, AC-3).
|
|
413
|
+
- **Changed:** Notification payloads now include a stable dedupe key for idempotent rendering (AC-2).
|
|
412
414
|
- **Fixed:** Panel no longer drops the newest item when reconnecting after sleep/resume.
|
|
415
|
+
- **Breaking changes:** None.
|
|
413
416
|
|
|
414
|
-
### Rollback
|
|
417
|
+
### Rollback Plan
|
|
415
418
|
|
|
416
|
-
|
|
417
|
-
|
|
419
|
+
- Trigger conditions: error rate on \`/notifications/stream\` exceeds 5% for >5 minutes, or p95 publish-to-visible lag exceeds 10s.
|
|
420
|
+
- Rollback steps: \`git revert <merge-sha> && git push origin main\` then redeploy; if DB migrations shipped, run \`2026_04_12_notifications_cursor_down.sql\` before traffic.
|
|
421
|
+
- Verification steps: confirm error rate returns to pre-release baseline within 10 minutes; smoke-test feed panel manually.
|
|
418
422
|
|
|
419
|
-
###
|
|
423
|
+
### Monitoring
|
|
420
424
|
|
|
421
|
-
-
|
|
422
|
-
-
|
|
425
|
+
- Metrics/logs to watch: error rate on \`/notifications/stream\` and snapshot endpoint for 24h; p95 publish-to-visible lag via metrics dashboard.
|
|
426
|
+
- Risk note (if no monitoring): N/A — monitoring is in place.
|
|
423
427
|
|
|
424
|
-
###
|
|
428
|
+
### Finalization
|
|
425
429
|
|
|
426
|
-
-
|
|
427
|
-
-
|
|
430
|
+
- Selected enum: FINALIZE_OPEN_PR
|
|
431
|
+
- Selected label: B
|
|
432
|
+
- Execution result: PR #42 created via \`gh pr create\`; CI passed; squash-merged to main.
|
|
433
|
+
- PR URL: https://github.com/example/repo/pull/42`,
|
|
428
434
|
};
|
|
429
435
|
export function stageExamples(stage) {
|
|
430
436
|
const examples = STAGE_EXAMPLES[stage];
|
|
@@ -1350,7 +1350,8 @@ const SHIP = {
|
|
|
1350
1350
|
{ id: "ship_release_notes_written", description: "Release notes are complete and accurate." },
|
|
1351
1351
|
{ id: "ship_rollback_plan_ready", description: "Rollback trigger, steps, and verification are documented." },
|
|
1352
1352
|
{ id: "ship_finalization_mode_selected", description: "Exactly one finalization action is selected." },
|
|
1353
|
-
{ id: "ship_finalization_executed", description: "Selected finalization action was executed and verified." }
|
|
1353
|
+
{ id: "ship_finalization_executed", description: "Selected finalization action was executed and verified." },
|
|
1354
|
+
{ id: "ship_post_merge_tests", description: "Full test suite re-run on the merged result (not just the branch). Post-merge failures caught before release." }
|
|
1354
1355
|
],
|
|
1355
1356
|
requiredEvidence: [
|
|
1356
1357
|
"Artifact written to `.cclaw/artifacts/08-ship.md`.",
|
|
@@ -1383,7 +1384,10 @@ const SHIP = {
|
|
|
1383
1384
|
rationalizations: [
|
|
1384
1385
|
{ claim: "Rollback details can be written after release.", reality: "Rollback is part of release readiness, not post-release cleanup." },
|
|
1385
1386
|
{ claim: "Finalization choice is obvious from context.", reality: "Explicit branch action prevents accidental release state." },
|
|
1386
|
-
{ claim: "Urgent fixes can skip preflight.", reality: "Urgency increases risk; preflight discipline matters more, not less." }
|
|
1387
|
+
{ claim: "Urgent fixes can skip preflight.", reality: "Urgency increases risk; preflight discipline matters more, not less." },
|
|
1388
|
+
{ claim: "Monitoring can be set up after deploy.", reality: "If you cannot observe the release, you cannot detect failure. Monitoring is a ship prerequisite, not a follow-up task." },
|
|
1389
|
+
{ claim: "A small merge does not need post-merge testing.", reality: "Small merges on diverged bases cause silent conflicts. Post-merge suite runs catch what branch-only CI misses." },
|
|
1390
|
+
{ claim: "Release notes are internal documentation, not a ship gate.", reality: "Release notes are the rollback decision input. Without them, the team cannot assess what changed or why a rollback is needed." }
|
|
1387
1391
|
],
|
|
1388
1392
|
redFlags: [
|
|
1389
1393
|
"No rollback trigger/steps",
|
|
@@ -1408,7 +1412,8 @@ const SHIP = {
|
|
|
1408
1412
|
{ name: "Rollback-First Thinking", description: "Before shipping, answer: what tells me this is broken? How do I undo it? How do I verify the undo worked? If you cannot answer all three, you are not ready." },
|
|
1409
1413
|
{ name: "Explicit Over Implicit Finalization", description: "Merge, PR, keep, discard — each has different consequences. Pick one. Say it out loud. Write it down. Never let finalization be 'whatever the default is.'" },
|
|
1410
1414
|
{ name: "Post-Merge Paranoia", description: "The merge itself can introduce failures even when both branches pass independently. Always run the full suite AFTER merge, not just before." },
|
|
1411
|
-
{ name: "Observability Before Ship", description: "If you cannot monitor the change in production, you cannot know if it is broken. Monitoring/logging is a ship prerequisite, not a follow-up." }
|
|
1415
|
+
{ name: "Observability Before Ship", description: "If you cannot monitor the change in production, you cannot know if it is broken. Monitoring/logging is a ship prerequisite, not a follow-up." },
|
|
1416
|
+
{ name: "Release Blast Radius", description: "Before shipping, map the blast radius: how many users are affected if this breaks? Is it one endpoint or the entire app? Scale rollback urgency and monitoring to the blast radius, not the diff size. A 3-line auth change can have infinite blast radius." }
|
|
1412
1417
|
],
|
|
1413
1418
|
reviewSections: [
|
|
1414
1419
|
{
|
|
@@ -1435,17 +1440,22 @@ const SHIP = {
|
|
|
1435
1440
|
],
|
|
1436
1441
|
completionStatus: ["SHIPPED", "SHIPPED_WITH_EXCEPTIONS", "BLOCKED"],
|
|
1437
1442
|
crossStageTrace: {
|
|
1438
|
-
readsFrom: [".cclaw/artifacts/07-review.md", ".cclaw/artifacts/06-tdd.md"],
|
|
1443
|
+
readsFrom: [".cclaw/artifacts/07-review.md", ".cclaw/artifacts/06-tdd.md", ".cclaw/artifacts/05-plan.md", ".cclaw/artifacts/04-spec.md"],
|
|
1439
1444
|
writesTo: [".cclaw/artifacts/08-ship.md"],
|
|
1440
|
-
traceabilityRule: "Ship artifact must reference review verdict and resolution status. Rollback plan must reference specific changes that could fail."
|
|
1445
|
+
traceabilityRule: "Ship artifact must reference review verdict and resolution status. Release notes must reference spec criteria. Rollback plan must reference specific changes that could fail."
|
|
1441
1446
|
},
|
|
1442
1447
|
artifactValidation: [
|
|
1443
1448
|
{ section: "Preflight Results", required: true, validationRule: "Build, test, lint, type-check results captured with fresh output. Exceptions documented if any." },
|
|
1444
1449
|
{ section: "Release Notes", required: true, validationRule: "What changed, why, impact. References spec criteria. Breaking changes flagged." },
|
|
1445
1450
|
{ section: "Rollback Plan", required: true, validationRule: "Trigger conditions, rollback steps (exact commands), verification steps." },
|
|
1446
1451
|
{ section: "Monitoring", required: false, validationRule: "If applicable: what metrics/logs to watch post-deploy. Risk note if no monitoring." },
|
|
1447
|
-
{ section: "Finalization", required: true, validationRule: "Exactly one finalization enum token selected. Execution result documented. Worktree cleaned if applicable." }
|
|
1448
|
-
|
|
1452
|
+
{ section: "Finalization", required: true, validationRule: "Exactly one finalization enum token selected. Execution result documented. Worktree cleaned if applicable." },
|
|
1453
|
+
{ section: "Completion Status", required: false, validationRule: "If present: exactly one of SHIPPED, SHIPPED_WITH_EXCEPTIONS, BLOCKED. Exceptions documented when applicable." }
|
|
1454
|
+
],
|
|
1455
|
+
namedAntiPattern: {
|
|
1456
|
+
title: "Green CI Means Safe to Merge",
|
|
1457
|
+
description: "CI passing on a feature branch does not prove the merged result is safe. Post-merge test failures are common when the base branch has diverged. Re-run the full suite on the merge result, not just the branch. A green branch badge is a necessary condition, not a sufficient one."
|
|
1458
|
+
}
|
|
1449
1459
|
};
|
|
1450
1460
|
// ---------------------------------------------------------------------------
|
|
1451
1461
|
// Stage map and accessors
|
|
@@ -1546,9 +1556,9 @@ const STAGE_AUTO_SUBAGENT_DISPATCH = {
|
|
|
1546
1556
|
},
|
|
1547
1557
|
{
|
|
1548
1558
|
agent: "security-reviewer",
|
|
1549
|
-
mode: "
|
|
1550
|
-
when: "
|
|
1551
|
-
purpose: "
|
|
1559
|
+
mode: "mandatory",
|
|
1560
|
+
when: "Always in review stage. Even when no trust boundaries changed, produce an explicit 'no-change' security attestation.",
|
|
1561
|
+
purpose: "Guarantee a dedicated security pass on every diff: auth, input validation, secrets, injection, privilege, and blast-radius review are never opt-in.",
|
|
1552
1562
|
requiresUserGate: false
|
|
1553
1563
|
}
|
|
1554
1564
|
],
|
|
@@ -35,7 +35,7 @@ For cclaw flow stages, machine-only specialist work should auto-dispatch without
|
|
|
35
35
|
|
|
36
36
|
- **design/plan:** planner
|
|
37
37
|
- **tdd:** test-author
|
|
38
|
-
- **review:** spec-reviewer + code-reviewer (security-reviewer when trust boundaries moved)
|
|
38
|
+
- **review:** spec-reviewer + code-reviewer + security-reviewer (security-reviewer is always mandatory; produce an explicit no-change attestation when no trust boundaries moved)
|
|
39
39
|
- **ship:** doc-updater
|
|
40
40
|
|
|
41
41
|
Human input remains mandatory only at explicit approval gates (plan approval, user challenge resolution, release finalization mode).
|
|
@@ -137,9 +137,9 @@ export const ARTIFACT_TEMPLATES = {
|
|
|
137
137
|
|
|
138
138
|
## Architecture Diagram
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
\`\`\`
|
|
141
141
|
(ASCII, Mermaid, or tool-generated diagram showing component boundaries and data flow direction)
|
|
142
|
-
|
|
142
|
+
\`\`\`
|
|
143
143
|
|
|
144
144
|
## What Already Exists
|
|
145
145
|
| Sub-problem | Existing code/library | Layer | Reuse decision |
|
|
@@ -422,6 +422,10 @@ Execution rule: complete and verify each wave before starting the next wave.
|
|
|
422
422
|
- Selected label (A/B/C/D):
|
|
423
423
|
- Execution result:
|
|
424
424
|
- PR URL / merge commit / kept branch / discard confirmation:
|
|
425
|
+
|
|
426
|
+
## Completion Status
|
|
427
|
+
- SHIPPED | SHIPPED_WITH_EXCEPTIONS | BLOCKED
|
|
428
|
+
- Exceptions (if any):
|
|
425
429
|
`
|
|
426
430
|
};
|
|
427
431
|
export const RULEBOOK_MARKDOWN = `# Cclaw Rulebook
|
package/dist/runs.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type FlowState } from "./flow-state.js";
|
|
2
|
+
import type { FlowStage } from "./types.js";
|
|
2
3
|
export interface CclawRunMeta {
|
|
3
4
|
id: string;
|
|
4
5
|
title: string;
|
|
@@ -10,10 +11,26 @@ export interface ArchiveRunResult {
|
|
|
10
11
|
archivedAt: string;
|
|
11
12
|
featureName: string;
|
|
12
13
|
resetState: FlowState;
|
|
14
|
+
snapshottedStateFiles: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface ArchiveManifest {
|
|
17
|
+
version: 1;
|
|
18
|
+
archiveId: string;
|
|
19
|
+
archivedAt: string;
|
|
20
|
+
featureName: string;
|
|
21
|
+
sourceRunId: string;
|
|
22
|
+
sourceCurrentStage: FlowStage;
|
|
23
|
+
sourceCompletedStages: FlowStage[];
|
|
24
|
+
snapshottedStateFiles: string[];
|
|
13
25
|
}
|
|
14
26
|
interface EnsureRunSystemOptions {
|
|
15
27
|
createIfMissing?: boolean;
|
|
16
28
|
}
|
|
29
|
+
export declare class CorruptFlowStateError extends Error {
|
|
30
|
+
readonly statePath: string;
|
|
31
|
+
readonly quarantinedPath: string;
|
|
32
|
+
constructor(statePath: string, quarantinedPath: string, cause: unknown);
|
|
33
|
+
}
|
|
17
34
|
export declare function readFlowState(projectRoot: string): Promise<FlowState>;
|
|
18
35
|
export declare function writeFlowState(projectRoot: string, state: FlowState): Promise<void>;
|
|
19
36
|
export declare function ensureRunSystem(projectRoot: string, _options?: EnsureRunSystemOptions): Promise<FlowState>;
|
package/dist/runs.js
CHANGED
|
@@ -6,7 +6,13 @@ import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.
|
|
|
6
6
|
const FLOW_STATE_REL_PATH = `${RUNTIME_ROOT}/state/flow-state.json`;
|
|
7
7
|
const RUNS_DIR_REL_PATH = `${RUNTIME_ROOT}/runs`;
|
|
8
8
|
const ACTIVE_ARTIFACTS_REL_PATH = `${RUNTIME_ROOT}/artifacts`;
|
|
9
|
+
const STATE_DIR_REL_PATH = `${RUNTIME_ROOT}/state`;
|
|
9
10
|
const FLOW_STAGE_SET = new Set(COMMAND_FILE_ORDER);
|
|
11
|
+
/** State filenames explicitly excluded from the archive snapshot. */
|
|
12
|
+
const STATE_SNAPSHOT_EXCLUDE = new Set([
|
|
13
|
+
".flow-state.lock",
|
|
14
|
+
".delegation.lock"
|
|
15
|
+
]);
|
|
10
16
|
function flowStatePath(projectRoot) {
|
|
11
17
|
return path.join(projectRoot, FLOW_STATE_REL_PATH);
|
|
12
18
|
}
|
|
@@ -19,6 +25,46 @@ function runsRoot(projectRoot) {
|
|
|
19
25
|
function activeArtifactsPath(projectRoot) {
|
|
20
26
|
return path.join(projectRoot, ACTIVE_ARTIFACTS_REL_PATH);
|
|
21
27
|
}
|
|
28
|
+
function stateDirPath(projectRoot) {
|
|
29
|
+
return path.join(projectRoot, STATE_DIR_REL_PATH);
|
|
30
|
+
}
|
|
31
|
+
async function snapshotStateDirectory(projectRoot, destinationRoot) {
|
|
32
|
+
const sourceDir = stateDirPath(projectRoot);
|
|
33
|
+
if (!(await exists(sourceDir))) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
await ensureDir(destinationRoot);
|
|
37
|
+
const copied = [];
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (STATE_SNAPSHOT_EXCLUDE.has(entry.name))
|
|
47
|
+
continue;
|
|
48
|
+
if (entry.name.startsWith(".") && !entry.name.endsWith(".json"))
|
|
49
|
+
continue;
|
|
50
|
+
const from = path.join(sourceDir, entry.name);
|
|
51
|
+
const to = path.join(destinationRoot, entry.name);
|
|
52
|
+
try {
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
await fs.cp(from, to, { recursive: true });
|
|
55
|
+
copied.push(`${entry.name}/`);
|
|
56
|
+
}
|
|
57
|
+
else if (entry.isFile()) {
|
|
58
|
+
await fs.copyFile(from, to);
|
|
59
|
+
copied.push(entry.name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// best-effort snapshot; continue on individual failures
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return copied.sort((a, b) => a.localeCompare(b));
|
|
67
|
+
}
|
|
22
68
|
function isFlowStage(value) {
|
|
23
69
|
return typeof value === "string" && FLOW_STAGE_SET.has(value);
|
|
24
70
|
}
|
|
@@ -144,18 +190,66 @@ async function uniqueArchiveId(projectRoot, baseId) {
|
|
|
144
190
|
}
|
|
145
191
|
return candidate;
|
|
146
192
|
}
|
|
193
|
+
export class CorruptFlowStateError extends Error {
|
|
194
|
+
statePath;
|
|
195
|
+
quarantinedPath;
|
|
196
|
+
constructor(statePath, quarantinedPath, cause) {
|
|
197
|
+
super(`Corrupt flow-state.json detected at ${statePath}. ` +
|
|
198
|
+
`Quarantined to ${quarantinedPath}. ` +
|
|
199
|
+
`Inspect the quarantined file, reconcile by hand, then re-run your command ` +
|
|
200
|
+
`or delete ${statePath} to start over. ` +
|
|
201
|
+
`Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
202
|
+
this.name = "CorruptFlowStateError";
|
|
203
|
+
this.statePath = statePath;
|
|
204
|
+
this.quarantinedPath = quarantinedPath;
|
|
205
|
+
if (cause instanceof Error) {
|
|
206
|
+
this.cause = cause;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function quarantineTimestamp(date = new Date()) {
|
|
211
|
+
return date.toISOString().replace(/[:.]/gu, "-");
|
|
212
|
+
}
|
|
213
|
+
async function quarantineCorruptState(statePath, cause) {
|
|
214
|
+
const quarantinedPath = `${statePath}.corrupt-${quarantineTimestamp()}.json`;
|
|
215
|
+
try {
|
|
216
|
+
await fs.rename(statePath, quarantinedPath);
|
|
217
|
+
}
|
|
218
|
+
catch (renameErr) {
|
|
219
|
+
try {
|
|
220
|
+
const raw = await fs.readFile(statePath, "utf8");
|
|
221
|
+
await fs.writeFile(quarantinedPath, raw, "utf8");
|
|
222
|
+
await fs.unlink(statePath).catch(() => undefined);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
throw new CorruptFlowStateError(statePath, quarantinedPath, renameErr);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
throw new CorruptFlowStateError(statePath, quarantinedPath, cause);
|
|
229
|
+
}
|
|
147
230
|
export async function readFlowState(projectRoot) {
|
|
148
231
|
const statePath = flowStatePath(projectRoot);
|
|
149
232
|
if (!(await exists(statePath))) {
|
|
150
233
|
return createInitialFlowState();
|
|
151
234
|
}
|
|
235
|
+
let raw;
|
|
152
236
|
try {
|
|
153
|
-
|
|
154
|
-
return coerceFlowState(parsed);
|
|
237
|
+
raw = await fs.readFile(statePath, "utf8");
|
|
155
238
|
}
|
|
156
|
-
catch {
|
|
157
|
-
|
|
239
|
+
catch (readErr) {
|
|
240
|
+
throw new CorruptFlowStateError(statePath, statePath, readErr);
|
|
241
|
+
}
|
|
242
|
+
let parsed;
|
|
243
|
+
try {
|
|
244
|
+
parsed = JSON.parse(raw);
|
|
245
|
+
}
|
|
246
|
+
catch (parseErr) {
|
|
247
|
+
await quarantineCorruptState(statePath, parseErr);
|
|
248
|
+
}
|
|
249
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
250
|
+
await quarantineCorruptState(statePath, new Error("flow-state.json did not deserialize to a JSON object"));
|
|
158
251
|
}
|
|
252
|
+
return coerceFlowState(parsed);
|
|
159
253
|
}
|
|
160
254
|
export async function writeFlowState(projectRoot, state) {
|
|
161
255
|
await withDirectoryLock(flowStateLockPath(projectRoot), async () => {
|
|
@@ -214,17 +308,32 @@ export async function archiveRun(projectRoot, featureName) {
|
|
|
214
308
|
const archiveId = await uniqueArchiveId(projectRoot, archiveBaseId);
|
|
215
309
|
const archivePath = path.join(runsDir, archiveId);
|
|
216
310
|
const archiveArtifactsPath = path.join(archivePath, "artifacts");
|
|
311
|
+
const sourceState = await readFlowState(projectRoot);
|
|
217
312
|
await ensureDir(archivePath);
|
|
218
313
|
await fs.rename(artifactsDir, archiveArtifactsPath);
|
|
219
314
|
await ensureDir(artifactsDir);
|
|
315
|
+
const archiveStatePath = path.join(archivePath, "state");
|
|
316
|
+
const snapshottedStateFiles = await snapshotStateDirectory(projectRoot, archiveStatePath);
|
|
220
317
|
const resetState = createInitialFlowState();
|
|
221
318
|
await writeFlowState(projectRoot, resetState);
|
|
222
319
|
const archivedAt = new Date().toISOString();
|
|
320
|
+
const manifest = {
|
|
321
|
+
version: 1,
|
|
322
|
+
archiveId,
|
|
323
|
+
archivedAt,
|
|
324
|
+
featureName: feature,
|
|
325
|
+
sourceRunId: sourceState.activeRunId,
|
|
326
|
+
sourceCurrentStage: sourceState.currentStage,
|
|
327
|
+
sourceCompletedStages: sourceState.completedStages,
|
|
328
|
+
snapshottedStateFiles
|
|
329
|
+
};
|
|
330
|
+
await writeFileSafe(path.join(archivePath, "archive-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
223
331
|
return {
|
|
224
332
|
archiveId,
|
|
225
333
|
archivePath,
|
|
226
334
|
archivedAt,
|
|
227
335
|
featureName: feature,
|
|
228
|
-
resetState
|
|
336
|
+
resetState,
|
|
337
|
+
snapshottedStateFiles
|
|
229
338
|
};
|
|
230
339
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cclaw-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.16",
|
|
4
4
|
"description": "Installer-first flow toolkit for coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"build": "npm run clean:dist && tsc -p tsconfig.json",
|
|
21
21
|
"test": "vitest run",
|
|
22
22
|
"test:watch": "vitest",
|
|
23
|
+
"test:coverage": "vitest run --coverage",
|
|
23
24
|
"smoke:runtime": "npm run build && node scripts/smoke-init.mjs",
|
|
24
25
|
"lint:hooks": "npm run build && node scripts/lint-generated-hooks.mjs",
|
|
25
26
|
"build:plugin-manifests": "npm run build && node scripts/build-plugin-manifests.mjs",
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/node": "^24.7.2",
|
|
46
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
45
47
|
"typescript": "^5.9.3",
|
|
46
48
|
"vitest": "^3.2.4"
|
|
47
49
|
}
|