cclaw-cli 7.1.1 → 7.2.0

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/dist/cli.d.ts CHANGED
@@ -9,6 +9,7 @@ interface ParsedArgs {
9
9
  track?: FlowTrack;
10
10
  dryRun?: boolean;
11
11
  interactive?: boolean;
12
+ syncCheck?: boolean;
12
13
  archiveName?: string;
13
14
  archiveSkipRetro?: boolean;
14
15
  archiveSkipRetroReason?: string;
package/dist/cli.js CHANGED
@@ -40,6 +40,7 @@ Commands:
40
40
  sync Reconcile generated runtime files with the current config.
41
41
  Flags: --harnesses=<list> Update configured harnesses before syncing.
42
42
  --interactive Pick harnesses from a numbered TTY menu.
43
+ --check Verify managed hook files are byte-identical to canonical generators.
43
44
  upgrade Refresh generated files in .cclaw. Preserves your config.yaml.
44
45
  archive Archive the active run and reset flow state for the next run.
45
46
  Flags: --name=<slug> Override archive folder suffix.
@@ -57,6 +58,7 @@ Examples:
57
58
  npx cclaw-cli
58
59
  npx cclaw-cli init --harnesses=claude,cursor --no-interactive
59
60
  npx cclaw-cli sync --interactive
61
+ npx cclaw-cli sync --check
60
62
  npx cclaw-cli archive --name=my-run
61
63
  npx cclaw-cli archive --disposition=cancelled --reason="deprioritized"
62
64
  npx cclaw-cli upgrade
@@ -334,6 +336,7 @@ function parseArgs(argv) {
334
336
  return flag.startsWith("--harnesses=") ||
335
337
  (parsed.command === "init" && flag.startsWith("--track=")) ||
336
338
  (parsed.command === "init" && flag.startsWith("--profile=")) ||
339
+ (parsed.command === "sync" && flag === "--check") ||
337
340
  flag === "--interactive" ||
338
341
  flag === "--no-interactive" ||
339
342
  (parsed.command === "init" && flag === "--dry-run");
@@ -374,6 +377,10 @@ function parseArgs(argv) {
374
377
  parsed.dryRun = true;
375
378
  continue;
376
379
  }
380
+ if (flag === "--check") {
381
+ parsed.syncCheck = true;
382
+ continue;
383
+ }
377
384
  if (flag.startsWith("--name=")) {
378
385
  parsed.archiveName = flag.replace("--name=", "").trim();
379
386
  continue;
@@ -446,6 +453,11 @@ async function runCommand(parsed, ctx) {
446
453
  return 0;
447
454
  }
448
455
  if (command === "sync") {
456
+ if (parsed.syncCheck === true) {
457
+ await syncCclaw(ctx.cwd, { check: true });
458
+ info(ctx, "Managed hook drift check passed (no sync required).");
459
+ return 0;
460
+ }
449
461
  const resolved = await resolveSyncInputs(parsed, ctx);
450
462
  await syncCclaw(ctx.cwd, { harnesses: resolved.harnesses });
451
463
  const harnessNote = resolved.harnesses ? ` (${resolved.harnesses.join(", ")})` : "";
@@ -97,7 +97,7 @@ node .cclaw/hooks/delegation-record.mjs \\
97
97
  --json
98
98
  \`\`\`
99
99
 
100
- Reuse the same \`<spanId>\` and \`<dispatchId>\` across both rows. **GREEN evidence freshness** (slice-builder): the FIRST \`--evidence-ref\` MUST (1) reference the same test the matching \`phase=red\` row cited (basename/stem substring; reject \`green_evidence_red_test_mismatch\`), (2) include a recognized passing-runner line such as \`=> N passed; 0 failed\`, \`N passed in 0.42s\`, or \`ok pkg 0.12s\` (reject \`green_evidence_passing_assertion_missing\`), AND (3) be captured AFTER \`ackTs\` of this span — \`completedTs - ackTs\` must be ≥ \`flow-state.json::tddGreenMinElapsedMs\` (default 4000ms; reject \`green_evidence_too_fresh\`). Escape clause for legitimate observational GREEN: pass BOTH \`--allow-fast-green --green-mode=observational\`. \`--ack-ts\` and \`--completed-ts\` must be monotonic on the span (\`startTs ≤ launchedTs ≤ ackTs ≤ completedTs\`); the helper rejects out-of-order writes with \`delegation_timestamp_non_monotonic\`. If the helper rejects with \`dispatch_active_span_collision\` against a stale span, surface the conflicting \`spanId\` to the parent — do NOT silently retry with \`--allow-parallel\`.`;
100
+ Reuse the same \`<spanId>\` and \`<dispatchId>\` across both rows. **GREEN evidence freshness** (slice-builder): the FIRST \`--evidence-ref\` MUST (1) reference the same test the matching \`phase=red\` row cited (basename/stem substring; reject \`green_evidence_red_test_mismatch\`), (2) include a recognized passing-runner line such as \`=> N passed; 0 failed\`, \`N passed in 0.42s\`, or \`ok pkg 0.12s\` (reject \`green_evidence_passing_assertion_missing\`), AND (3) be captured AFTER \`ackTs\` of this span — \`completedTs - ackTs\` must be ≥ \`flow-state.json::tddGreenMinElapsedMs\` (default 4000ms; reject \`green_evidence_too_fresh\`). Escape clause for legitimate observational GREEN: pass \`--green-mode=observational\`. \`--ack-ts\` and \`--completed-ts\` must be monotonic on the span (\`startTs ≤ launchedTs ≤ ackTs ≤ completedTs\`); the helper rejects out-of-order writes with \`delegation_timestamp_non_monotonic\`. If the helper rejects with \`dispatch_active_span_collision\` against a stale span, surface the conflicting \`spanId\` to the parent — do NOT silently retry with \`--allow-parallel\`.`;
101
101
  }
102
102
  function formatReturnSchema(schema) {
103
103
  const lines = [
@@ -334,19 +334,20 @@ function extractRedTestNameInline(redEvidenceRef) {
334
334
  return trimmed;
335
335
  }
336
336
 
337
- // Match canonical runner pass lines:
338
- // cargo: "test result: ok. N passed; 0 failed"
339
- // pytest: "===== N passed in 0.42s ====="
340
- // go test: "ok pkg 0.123s"
341
- // npm/jest/vitest: "Tests: N passed"
342
- // We accept a generic shape: "=> N passed; 0 failed" plus four
343
- // runner-specific patterns.
337
+ // Match canonical runner pass lines using language-agnostic examples:
338
+ // Node/TS (vitest/jest): "=> N passed; 0 failed" or "Tests: N passed"
339
+ // Python (pytest): "===== N passed in 0.42s ====="
340
+ // Go (go test): "ok pkg 0.123s"
341
+ // Rust (cargo test): "test result: ok. N passed; 0 failed"
342
+ // Java/JVM (maven/surefire): "Tests run: N, Failures: 0, Errors: 0"
343
+ // We accept a generic "passed/failed" shape plus runner-specific patterns.
344
344
  const GREEN_PASS_PATTERNS = [
345
345
  /=>\\s*\\d+\\s+passed/iu,
346
346
  /\\b\\d+\\s+passed[;,]\\s*0\\s+failed\\b/iu,
347
347
  /\\btest\\s+result:\\s*ok\\b/iu,
348
348
  /\\b\\d+\\s+passed\\s+in\\s+\\d+(?:\\.\\d+)?\\s*s\\b/iu,
349
- /^ok\\s+\\S+\\s+\\d+(?:\\.\\d+)?s\\b/imu
349
+ /^ok\\s+\\S+\\s+\\d+(?:\\.\\d+)?s\\b/imu,
350
+ /tests\\s+run\\s*:\\s*\\d+\\s*,\\s*failures\\s*:\\s*0\\s*,\\s*errors\\s*:\\s*0/iu
350
351
  ];
351
352
 
352
353
  function matchesPassingAssertionInline(value) {
@@ -373,6 +374,16 @@ async function readDelegationEvents(root) {
373
374
  }
374
375
  }
375
376
 
377
+ async function appendAuditEventInline(root, payload) {
378
+ const stateDir = path.join(root, RUNTIME_ROOT, "state");
379
+ await fs.mkdir(stateDir, { recursive: true });
380
+ await fs.appendFile(
381
+ path.join(stateDir, "delegation-events.jsonl"),
382
+ JSON.stringify(payload) + "\\n",
383
+ { encoding: "utf8", mode: 0o600 }
384
+ );
385
+ }
386
+
376
387
  function hasPriorAck(events, args, runId) {
377
388
  return events.some((event) =>
378
389
  event.runId === runId &&
@@ -388,7 +399,7 @@ function hasPriorAck(events, args, runId) {
388
399
  function usage() {
389
400
  process.stderr.write([
390
401
  "Usage:",
391
- " node .cclaw/hooks/delegation-record.mjs --stage=<stage> --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<id> [--dispatch-id=<id>] [--worker-run-id=<id>] [--dispatch-surface=<surface>] [--agent-definition-path=<path>] [--ack-ts=<iso>] [--launched-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--waiver-reason=<text>] [--supersede=<prevSpanId>] [--allow-parallel] [--paths=<comma-separated>] [--override-cap=<int>] [--json]",
402
+ " node .cclaw/hooks/delegation-record.mjs --stage=<stage> --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<id> [--dispatch-id=<id>] [--worker-run-id=<id>] [--dispatch-surface=<surface>] [--agent-definition-path=<path>] [--ack-ts=<iso>] [--launched-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--waiver-reason=<text>] [--supersede=<prevSpanId>] [--allow-parallel] [--paths=<comma-separated>] [--override-cap=<int>] [--reason=<slug>] [--json]",
392
403
  " node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path> [--ack-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--json]",
393
404
  " node .cclaw/hooks/delegation-record.mjs --repair --span-id=<id> --repair-reason=\\\"<why>\\\" [--json]",
394
405
  " node .cclaw/hooks/delegation-record.mjs --audit-kind=cclaw_integration_overseer_skipped [--audit-reason=\\\"<comma-separated reasons>\\\"] [--slice-ids=\\\"S-1,S-2\\\"] [--json] # non-delegation audit row",
@@ -406,11 +417,12 @@ function usage() {
406
417
  "TDD parallel scheduler:",
407
418
  " --paths=<a,b,c> repo-relative paths the slice-builder will edit; disjoint sets auto-promote to allowParallel, overlap throws DispatchOverlapError",
408
419
  " --override-cap=<int> raise the slice worker fan-out cap once for this dispatch (default cap " + String(5) + ", env CCLAW_MAX_PARALLEL_SLICE_BUILDERS overrides globally)",
420
+ " --reason=<slug> required with --override-cap so cap bypasses are auditable (e.g. red-checkpoint-retry)",
409
421
  "",
410
422
  "TDD slice phase tagging:",
411
423
  " --slice=<id> TDD slice identifier (e.g. S-1) used by the linter to auto-derive the Watched-RED + Vertical Slice Cycle tables.",
412
424
  " --phase=<phase> one of " + VALID_DELEGATION_PHASES.join(", ") + ". Pair with --slice to record a TDD slice phase event.",
413
- " --refactor-rationale=<t> required when --phase=refactor-deferred unless --evidence-ref carries the rationale text; also paired with --refactor-outcome on phase=green.",
425
+ " --refactor-rationale=<t> required for deferred refactor paths; must be >=80 chars and mention slice + task context (e.g. S-12 / T-103).",
414
426
  " --refactor-outcome=<m> one of inline|deferred. Folds REFACTOR into the phase=green event so a single row can close RED→GREEN→REFACTOR. Pair --refactor-outcome=deferred with --refactor-rationale.",
415
427
  " --risk-tier=<t> one of low|medium|high. high triggers integration-overseer in conditional mode.",
416
428
  ""
@@ -505,6 +517,28 @@ function normalizeEvidenceRefs(args) {
505
517
  return [];
506
518
  }
507
519
 
520
+ function validateDeferredRationaleInline(rationaleRaw, args) {
521
+ const rationale = typeof rationaleRaw === "string" ? rationaleRaw.trim() : "";
522
+ if (rationale.length === 0) {
523
+ return "missing";
524
+ }
525
+ if (rationale.length < 80) {
526
+ return "too-short";
527
+ }
528
+ const lower = rationale.toLowerCase();
529
+ const sliceRaw = typeof args.slice === "string" ? args.slice.trim().toLowerCase() : "";
530
+ const hasSliceMention =
531
+ (sliceRaw.length > 0 && lower.includes(sliceRaw)) ||
532
+ /\\bs-\\d+\\b/iu.test(rationale);
533
+ const hasTaskMention =
534
+ /\\bt-\\d{3}[a-z]?(?:\\.\\d{1,3})?\\b/iu.test(rationale) ||
535
+ /\\btask\\b/iu.test(rationale);
536
+ if (!hasSliceMention || !hasTaskMention) {
537
+ return "missing-context";
538
+ }
539
+ return "ok";
540
+ }
541
+
508
542
  function buildRow(args, status, runId, now, options) {
509
543
  const fulfillmentMode = args["dispatch-surface"] === "role-switch"
510
544
  ? "role-switch"
@@ -911,7 +945,8 @@ async function findLegacyEntry(root, spanId) {
911
945
  // the helper. Keep in sync with NON_DELEGATION_AUDIT_EVENTS in
912
946
  // src/delegation.ts.
913
947
  const VALID_AUDIT_KINDS = new Set([
914
- "cclaw_integration_overseer_skipped"
948
+ "cclaw_integration_overseer_skipped",
949
+ "cclaw_allow_parallel_auto_flip"
915
950
  ]);
916
951
 
917
952
  async function runAuditEmit(args, json) {
@@ -1347,15 +1382,15 @@ async function main() {
1347
1382
  return;
1348
1383
  }
1349
1384
  if (args.phase === "refactor-deferred") {
1350
- const rationaleProvided =
1351
- typeof args["refactor-rationale"] === "string" && args["refactor-rationale"].trim().length > 0;
1352
- const evidenceProvided =
1353
- (typeof args["evidence-ref"] === "string" && args["evidence-ref"].trim().length > 0) ||
1354
- (Array.isArray(args["evidence-refs"]) && args["evidence-refs"].some(
1355
- (ref) => typeof ref === "string" && ref.trim().length > 0
1356
- ));
1357
- if (!rationaleProvided && !evidenceProvided) {
1358
- problems.push("--phase=refactor-deferred requires --refactor-rationale=<text> or --evidence-ref=<text>");
1385
+ const rationaleQuality = validateDeferredRationaleInline(args["refactor-rationale"], args);
1386
+ if (rationaleQuality !== "ok") {
1387
+ if (rationaleQuality === "missing") {
1388
+ problems.push("--phase=refactor-deferred requires --refactor-rationale=<text>");
1389
+ } else if (rationaleQuality === "too-short") {
1390
+ problems.push("--refactor-rationale for deferred refactor must be at least 80 characters");
1391
+ } else {
1392
+ problems.push("--refactor-rationale for deferred refactor must mention slice/task context (e.g. S-12 and T-103)");
1393
+ }
1359
1394
  emitProblems(problems, json, 2);
1360
1395
  return;
1361
1396
  }
@@ -1375,15 +1410,15 @@ async function main() {
1375
1410
  return;
1376
1411
  }
1377
1412
  if (args["refactor-outcome"] === "deferred") {
1378
- const rationaleProvided =
1379
- typeof args["refactor-rationale"] === "string" && args["refactor-rationale"].trim().length > 0;
1380
- const evidenceProvided =
1381
- (typeof args["evidence-ref"] === "string" && args["evidence-ref"].trim().length > 0) ||
1382
- (Array.isArray(args["evidence-refs"]) && args["evidence-refs"].some(
1383
- (ref) => typeof ref === "string" && ref.trim().length > 0
1384
- ));
1385
- if (!rationaleProvided && !evidenceProvided) {
1386
- problems.push("--refactor-outcome=deferred requires --refactor-rationale=<text> or --evidence-ref=<text>");
1413
+ const rationaleQuality = validateDeferredRationaleInline(args["refactor-rationale"], args);
1414
+ if (rationaleQuality !== "ok") {
1415
+ if (rationaleQuality === "missing") {
1416
+ problems.push("--refactor-outcome=deferred requires --refactor-rationale=<text>");
1417
+ } else if (rationaleQuality === "too-short") {
1418
+ problems.push("--refactor-rationale for deferred refactor must be at least 80 characters");
1419
+ } else {
1420
+ problems.push("--refactor-rationale for deferred refactor must mention slice/task context (e.g. S-12 and T-103)");
1421
+ }
1387
1422
  emitProblems(problems, json, 2);
1388
1423
  return;
1389
1424
  }
@@ -1398,6 +1433,21 @@ async function main() {
1398
1433
  emitProblems(problems, json, 2);
1399
1434
  return;
1400
1435
  }
1436
+ if (args["override-cap"] !== undefined) {
1437
+ const overrideRaw = String(args["override-cap"]).trim();
1438
+ const overrideNum = Number(overrideRaw);
1439
+ if (!Number.isInteger(overrideNum) || overrideNum < 1) {
1440
+ problems.push("--override-cap must be an integer >= 1");
1441
+ emitProblems(problems, json, 2);
1442
+ return;
1443
+ }
1444
+ const reasonRaw = typeof args.reason === "string" ? args.reason.trim() : "";
1445
+ if (reasonRaw.length === 0) {
1446
+ problems.push("--override-cap requires --reason=<slug>");
1447
+ emitProblems(problems, json, 2);
1448
+ return;
1449
+ }
1450
+ }
1401
1451
 
1402
1452
  if (args.status === "completed" && args["dispatch-surface"] !== "role-switch") {
1403
1453
  for (const key of ["dispatch-id", "dispatch-surface", "agent-definition-path"]) {
@@ -1479,6 +1529,7 @@ async function main() {
1479
1529
  const row = buildRow(args, status, runId, now, { spanStartTs });
1480
1530
  const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
1481
1531
  const event = { ...clean, event: status, eventTs: now };
1532
+ let autoParallelAuditEvent = null;
1482
1533
 
1483
1534
  const violation = validateMonotonicTimestampsInline(clean, priorLedger);
1484
1535
  if (violation) {
@@ -1487,8 +1538,8 @@ async function main() {
1487
1538
  }
1488
1539
 
1489
1540
  // File-overlap scheduler + fan-out cap. Run before the dispatch
1490
- // dedup so disjoint claimedPaths can auto-promote to allowParallel and
1491
- // bypass the duplicate guard.
1541
+ // dedup so disjoint claimedPaths can auto-promote to allowParallel,
1542
+ // emit an audit event for the flip, and bypass the duplicate guard.
1492
1543
  if (status === "scheduled") {
1493
1544
  const sameRunPrior = priorLedger.filter((entry) => entry.runId === runId);
1494
1545
  const activeForRun = computeActiveSubagentsInline(sameRunPrior);
@@ -1501,6 +1552,18 @@ async function main() {
1501
1552
  clean.allowParallel = true;
1502
1553
  args["allow-parallel"] = true;
1503
1554
  event.allowParallel = true;
1555
+ autoParallelAuditEvent = {
1556
+ event: "cclaw_allow_parallel_auto_flip",
1557
+ runId,
1558
+ ts: now,
1559
+ eventTs: now,
1560
+ stage: clean.stage,
1561
+ agent: clean.agent,
1562
+ spanId: clean.spanId,
1563
+ sliceId: clean.sliceId,
1564
+ reason: "disjoint-claimed-paths-auto-flip",
1565
+ claimedPaths: Array.isArray(clean.claimedPaths) ? clean.claimedPaths : []
1566
+ };
1504
1567
  }
1505
1568
  const overrideRaw = typeof args["override-cap"] === "string" ? args["override-cap"] : null;
1506
1569
  const override = overrideRaw !== null ? Number(overrideRaw) : null;
@@ -1548,8 +1611,7 @@ async function main() {
1548
1611
  // 3. green_evidence_too_fresh — completedTs minus ackTs must be
1549
1612
  // >= flow-state.json::tddGreenMinElapsedMs (default 4000ms).
1550
1613
  // Escape hatch for legitimate observational GREENs (cross-slice
1551
- // handoff, no-op verification): pair --allow-fast-green with
1552
- // --green-mode=observational. Both flags are required.
1614
+ // handoff, no-op verification): --green-mode=observational.
1553
1615
  if (
1554
1616
  clean.stage === "tdd" &&
1555
1617
  clean.agent === "slice-builder" &&
@@ -1559,7 +1621,6 @@ async function main() {
1559
1621
  const isObservational =
1560
1622
  typeof args["green-mode"] === "string" &&
1561
1623
  args["green-mode"].trim().toLowerCase() === "observational";
1562
- const allowFastGreen = args["allow-fast-green"] === true;
1563
1624
  const greenEvidenceFirst =
1564
1625
  Array.isArray(clean.evidenceRefs) && clean.evidenceRefs.length > 0
1565
1626
  ? String(clean.evidenceRefs[0])
@@ -1586,10 +1647,9 @@ async function main() {
1586
1647
  // nothing to verify GREEN against (legacy ledger imports, RED
1587
1648
  // happened outside cclaw harness, or test fixtures that bypass
1588
1649
  // RED). Once a RED row is present, the contract becomes
1589
- // mandatory unless explicitly waived via --allow-fast-green
1590
- // --green-mode=observational.
1650
+ // mandatory unless explicitly waived via --green-mode=observational.
1591
1651
  const hasRedContext = redEvidenceRef !== null;
1592
- const escapeFastGreen = allowFastGreen && isObservational;
1652
+ const escapeFastGreen = isObservational;
1593
1653
 
1594
1654
  if (hasRedContext && !escapeFastGreen) {
1595
1655
  // Check 1: RED test name match.
@@ -1618,7 +1678,7 @@ async function main() {
1618
1678
  sliceId: clean.sliceId,
1619
1679
  greenEvidenceFirst,
1620
1680
  remediation:
1621
- "evidenceRefs[0] on the GREEN row must contain a passing-assertion line such as \\"=> N passed; 0 failed\\" (cargo/jest/vitest), \\"N passed in 0.42s\\" (pytest), \\"ok pkg 0.12s\\" (go test), or equivalent runner output. Re-run the test and paste a fresh runner line."
1681
+ "evidenceRefs[0] on the GREEN row must contain a passing-assertion line (language-agnostic examples: Node/Vitest \\"=> N passed; 0 failed\\", Python/Pytest \\"N passed in 0.42s\\", Go \\"ok pkg 0.12s\\", Rust \\"test result: ok\\", Java/Maven \\"Tests run: N, Failures: 0, Errors: 0\\"). Re-run the test and paste a fresh runner line."
1622
1682
  },
1623
1683
  json
1624
1684
  );
@@ -1657,7 +1717,7 @@ async function main() {
1657
1717
  elapsedMs: elapsed,
1658
1718
  minMs,
1659
1719
  remediation:
1660
- "GREEN completedTs - ackTs is below the freshness floor. Either run the verification test for real and re-record, or pass --allow-fast-green --green-mode=observational for legitimate no-op verification spans."
1720
+ "GREEN completedTs - ackTs is below the freshness floor. Either run the verification test for real and re-record, or pass --green-mode=observational for legitimate no-op verification spans."
1661
1721
  },
1662
1722
  json
1663
1723
  );
@@ -1687,6 +1747,9 @@ async function main() {
1687
1747
  }
1688
1748
 
1689
1749
  await persistEntry(root, runId, clean, event);
1750
+ if (autoParallelAuditEvent) {
1751
+ await appendAuditEventInline(root, autoParallelAuditEvent);
1752
+ }
1690
1753
 
1691
1754
  process.stdout.write(JSON.stringify({ ok: true, event }, null, 2) + "\\n");
1692
1755
  }
@@ -4,6 +4,7 @@ import { BRAINSTORM, SCOPE, DESIGN, SPEC, PLAN, TDD, REVIEW, SHIP } from "./stag
4
4
  import { stagePolicyNeedlesFromMetadata } from "./stages/_lint-metadata/index.js";
5
5
  import { tddStageForTrack } from "./stages/tdd.js";
6
6
  import { trackRenderContext } from "./track-render-context.js";
7
+ import { STACK_REVIEW_ROUTE_PROFILES } from "../stack-detection.js";
7
8
  // ---------------------------------------------------------------------------
8
9
  // NOTE: The former QUESTION_FORMAT_SPEC / ERROR_BUDGET_SPEC exports were
9
10
  // hoisted into `src/content/meta-skill.ts` (Shared Decision + Tool-Use
@@ -31,38 +32,12 @@ const COMPLEXITY_TIER_ORDER = {
31
32
  standard: 1,
32
33
  deep: 2
33
34
  };
34
- const REVIEW_STACK_AWARE_ROUTES = [
35
- {
36
- stack: "TypeScript/JavaScript",
37
- agent: "reviewer",
38
- signals: ["package.json", "tsconfig.json"],
39
- focus: "type safety, package scripts, build/test config, dependency boundaries"
40
- },
41
- {
42
- stack: "Python",
43
- agent: "reviewer",
44
- signals: ["pyproject.toml", "requirements.txt"],
45
- focus: "packaging, virtualenv assumptions, typing, pytest or unittest evidence"
46
- },
47
- {
48
- stack: "Ruby/Rails",
49
- agent: "reviewer",
50
- signals: ["Gemfile", "config/"],
51
- focus: "Rails conventions, migrations, routes/controllers, RSpec or Minitest evidence"
52
- },
53
- {
54
- stack: "Go",
55
- agent: "reviewer",
56
- signals: ["go.mod"],
57
- focus: "interfaces, concurrency, error handling, go test coverage"
58
- },
59
- {
60
- stack: "Rust",
61
- agent: "reviewer",
62
- signals: ["Cargo.toml"],
63
- focus: "ownership, error/result handling, feature flags, cargo test coverage"
64
- }
65
- ];
35
+ const REVIEW_STACK_AWARE_ROUTES = STACK_REVIEW_ROUTE_PROFILES.map((profile) => ({
36
+ stack: profile.stack,
37
+ agent: "reviewer",
38
+ signals: [...profile.reviewSignals],
39
+ focus: profile.focus
40
+ }));
66
41
  function stackAwareRoutesForStage(stage) {
67
42
  return stage === "review" ? reviewStackAwareRoutes() : [];
68
43
  }
@@ -375,6 +375,7 @@ const NON_DELEGATION_AUDIT_EVENTS = new Set([
375
375
  "cclaw_fanin_resolved",
376
376
  "cclaw_fanin_abandoned",
377
377
  "cclaw_integration_overseer_skipped",
378
+ "cclaw_allow_parallel_auto_flip",
378
379
  "slice-completed"
379
380
  ]);
380
381
  function isAuditEventLine(parsed) {
@@ -124,7 +124,7 @@ export interface FlowState {
124
124
  * Minimum elapsed milliseconds between `acknowledged` and `completed`
125
125
  * for a `slice-builder --phase green` row. The hook helper rejects
126
126
  * fast-greens (`completedTs - ackTs < this`) with `green_evidence_too_fresh`
127
- * unless the dispatch carries `--allow-fast-green --green-mode=observational`.
127
+ * unless the dispatch carries `--green-mode=observational`.
128
128
  *
129
129
  * Default 4000ms when omitted (see `effectiveTddGreenMinElapsedMs`).
130
130
  * Operators tuning the floor for very fast suites may set it lower
package/dist/install.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface InitOptions {
6
6
  }
7
7
  export interface SyncOptions {
8
8
  harnesses?: HarnessId[];
9
+ check?: boolean;
9
10
  }
10
11
  export declare function initCclaw(options: InitOptions): Promise<void>;
11
12
  export declare function syncCclaw(projectRoot: string, options?: SyncOptions): Promise<void>;
package/dist/install.js CHANGED
@@ -750,6 +750,49 @@ async function writeHooks(projectRoot, config) {
750
750
  // OpenCode registration is auto-managed via opencode.json/opencode.jsonc.
751
751
  }
752
752
  }
753
+ async function canonicalHookScripts() {
754
+ const hookRuntimeOptions = {};
755
+ const bundledHookRuntime = await readBundledRunHookRuntimeScript(hookRuntimeOptions);
756
+ return {
757
+ "stage-complete.mjs": stageCompleteScript(),
758
+ "start-flow.mjs": startFlowScript(),
759
+ "cancel-run.mjs": cancelRunScript(),
760
+ "run-hook.mjs": bundledHookRuntime ?? nodeHookRuntimeScript(hookRuntimeOptions),
761
+ "run-hook.cmd": runHookCmdScript(),
762
+ "delegation-record.mjs": delegationRecordScript(),
763
+ "slice-commit.mjs": sliceCommitScript(),
764
+ "opencode-plugin.mjs": opencodePluginJs()
765
+ };
766
+ }
767
+ async function checkManagedHookDrift(projectRoot) {
768
+ const hooksDir = runtimePath(projectRoot, "hooks");
769
+ const canonical = await canonicalHookScripts();
770
+ const findings = [];
771
+ for (const [fileName, expectedSource] of Object.entries(canonical)) {
772
+ const targetPath = path.join(hooksDir, fileName);
773
+ let actual;
774
+ try {
775
+ actual = await fs.readFile(targetPath);
776
+ }
777
+ catch {
778
+ findings.push({ file: fileName, reason: "missing" });
779
+ continue;
780
+ }
781
+ const expected = Buffer.from(expectedSource, "utf8");
782
+ if (!actual.equals(expected)) {
783
+ findings.push({ file: fileName, reason: "content_mismatch" });
784
+ }
785
+ }
786
+ return findings;
787
+ }
788
+ function formatManagedHookDriftError(findings) {
789
+ const details = findings
790
+ .map((finding) => `- .cclaw/hooks/${finding.file}: ${finding.reason === "missing" ? "missing" : "content differs from canonical renderer"}`)
791
+ .join("\n");
792
+ return ("[sync --check] Managed hook drift detected.\n" +
793
+ `${details}\n` +
794
+ "Re-run `npx cclaw-cli sync` to rewrite managed hooks.");
795
+ }
753
796
  async function ensureKnowledgeStore(projectRoot) {
754
797
  const storePath = runtimePath(projectRoot, "knowledge.jsonl");
755
798
  if (!(await exists(storePath))) {
@@ -1064,6 +1107,13 @@ export async function syncCclaw(projectRoot, options = {}) {
1064
1107
  if (options.harnesses !== undefined && options.harnesses.length === 0) {
1065
1108
  throw new Error("Select at least one harness.");
1066
1109
  }
1110
+ if (options.check === true) {
1111
+ const drift = await checkManagedHookDrift(projectRoot);
1112
+ if (drift.length > 0) {
1113
+ throw new Error(formatManagedHookDriftError(drift));
1114
+ }
1115
+ return;
1116
+ }
1067
1117
  const configExists = await exists(configPath(projectRoot));
1068
1118
  let config = await readConfig(projectRoot);
1069
1119
  if (!configExists) {
@@ -4,6 +4,7 @@ import { RUNTIME_ROOT } from "../../constants.js";
4
4
  import { createInitialFlowState } from "../../flow-state.js";
5
5
  import { readFlowState, writeFlowState } from "../../runs.js";
6
6
  import { listExistingFiles, listFilesUnder, pathExists } from "./helpers.js";
7
+ import { STACK_DISCOVERY_DIR_MARKERS, STACK_DISCOVERY_MARKERS } from "../../stack-detection.js";
7
8
  import { TRACK_STAGES } from "../../types.js";
8
9
  import { buildValidationReport } from "./advance.js";
9
10
  import { carriedCompletedStageCatalog, completedStageClosureEvidenceIssues, firstIncompleteStageForTrack } from "./verify.js";
@@ -91,24 +92,11 @@ export async function discoverStartFlowContext(projectRoot) {
91
92
  lines.push(originFiles.length > 0
92
93
  ? `- Origin docs scanned: found ${originFiles.join(", ")}.`
93
94
  : "- Origin docs scanned: no PRD/RFC/ADR/design/spec files found in configured locations.");
94
- const stackMarkers = await listExistingFiles(projectRoot, [
95
- "package.json",
96
- "pyproject.toml",
97
- "requirements.txt",
98
- "requirements-dev.txt",
99
- ".python-version",
100
- "go.mod",
101
- "Cargo.toml",
102
- "pom.xml",
103
- "build.gradle",
104
- "build.gradle.kts",
105
- "Dockerfile",
106
- "docker-compose.yml",
107
- "docker-compose.yaml",
108
- ".gitlab-ci.yml"
109
- ]);
110
- if (await pathExists(projectRoot, ".github/workflows")) {
111
- stackMarkers.push(".github/workflows/");
95
+ const stackMarkers = await listExistingFiles(projectRoot, [...STACK_DISCOVERY_MARKERS]);
96
+ for (const markerDir of STACK_DISCOVERY_DIR_MARKERS) {
97
+ if (await pathExists(projectRoot, markerDir)) {
98
+ stackMarkers.push(`${markerDir}/`);
99
+ }
112
100
  }
113
101
  lines.push(stackMarkers.length > 0
114
102
  ? `- Stack markers scanned: found ${stackMarkers.join(", ")}.`
@@ -3,6 +3,7 @@ import { RUNTIME_ROOT } from "../../constants.js";
3
3
  import { stageSchema } from "../../content/stage-schema.js";
4
4
  import { readFlowState } from "../../runs.js";
5
5
  import { TRACK_STAGES } from "../../types.js";
6
+ import { STACK_DISCOVERY_DIR_MARKERS, STACK_DISCOVERY_MARKERS } from "../../stack-detection.js";
6
7
  import { coerceCandidateFlowState } from "./flow-state-coercion.js";
7
8
  import { buildValidationReport } from "./advance.js";
8
9
  import fs from "node:fs/promises";
@@ -171,24 +172,11 @@ export async function discoverStartFlowContext(projectRoot) {
171
172
  lines.push(originFiles.length > 0
172
173
  ? `- Origin docs scanned: found ${originFiles.join(", ")}.`
173
174
  : "- Origin docs scanned: no PRD/RFC/ADR/design/spec files found in configured locations.");
174
- const stackMarkers = await listExistingFiles(projectRoot, [
175
- "package.json",
176
- "pyproject.toml",
177
- "requirements.txt",
178
- "requirements-dev.txt",
179
- ".python-version",
180
- "go.mod",
181
- "Cargo.toml",
182
- "pom.xml",
183
- "build.gradle",
184
- "build.gradle.kts",
185
- "Dockerfile",
186
- "docker-compose.yml",
187
- "docker-compose.yaml",
188
- ".gitlab-ci.yml"
189
- ]);
190
- if (await pathExists(projectRoot, ".github/workflows")) {
191
- stackMarkers.push(".github/workflows/");
175
+ const stackMarkers = await listExistingFiles(projectRoot, [...STACK_DISCOVERY_MARKERS]);
176
+ for (const markerDir of STACK_DISCOVERY_DIR_MARKERS) {
177
+ if (await pathExists(projectRoot, markerDir)) {
178
+ stackMarkers.push(`${markerDir}/`);
179
+ }
192
180
  }
193
181
  lines.push(stackMarkers.length > 0
194
182
  ? `- Stack markers scanned: found ${stackMarkers.join(", ")}.`
@@ -0,0 +1,22 @@
1
+ export interface StackReviewRouteProfile {
2
+ stack: string;
3
+ /**
4
+ * Signals shown in review routing documentation/skills.
5
+ * These are human-facing pointers, not strict parsers.
6
+ */
7
+ reviewSignals: string[];
8
+ /** Root-level markers used by start-flow context discovery. */
9
+ discoveryMarkers: string[];
10
+ focus: string;
11
+ }
12
+ export declare const STACK_REVIEW_ROUTE_PROFILES: readonly StackReviewRouteProfile[];
13
+ /**
14
+ * Unified root-marker list used by start-flow context discovery.
15
+ * Keep this in one place so stage skill routing and start-flow scanning
16
+ * evolve together.
17
+ */
18
+ export declare const STACK_DISCOVERY_MARKERS: readonly string[];
19
+ /**
20
+ * Directory markers (checked with pathExists) for stack discovery.
21
+ */
22
+ export declare const STACK_DISCOVERY_DIR_MARKERS: readonly string[];
@@ -0,0 +1,58 @@
1
+ export const STACK_REVIEW_ROUTE_PROFILES = [
2
+ {
3
+ stack: "TypeScript/JavaScript",
4
+ reviewSignals: ["package.json", "tsconfig.json"],
5
+ discoveryMarkers: ["package.json", "tsconfig.json", "jsconfig.json"],
6
+ focus: "type safety, package scripts, build/test config, dependency boundaries"
7
+ },
8
+ {
9
+ stack: "Python",
10
+ reviewSignals: ["pyproject.toml", "requirements.txt"],
11
+ discoveryMarkers: ["pyproject.toml", "requirements.txt", "requirements-dev.txt", ".python-version"],
12
+ focus: "packaging, virtualenv assumptions, typing, pytest or unittest evidence"
13
+ },
14
+ {
15
+ stack: "Ruby/Rails",
16
+ reviewSignals: ["Gemfile", "config/"],
17
+ discoveryMarkers: ["Gemfile"],
18
+ focus: "Rails conventions, migrations, routes/controllers, RSpec or Minitest evidence"
19
+ },
20
+ {
21
+ stack: "Go",
22
+ reviewSignals: ["go.mod"],
23
+ discoveryMarkers: ["go.mod"],
24
+ focus: "interfaces, concurrency, error handling, go test coverage"
25
+ },
26
+ {
27
+ stack: "Rust",
28
+ reviewSignals: ["Cargo.toml"],
29
+ discoveryMarkers: ["Cargo.toml"],
30
+ focus: "ownership, error/result handling, feature flags, cargo test coverage"
31
+ }
32
+ ];
33
+ const EXTRA_DISCOVERY_MARKERS = [
34
+ "pom.xml",
35
+ "build.gradle",
36
+ "build.gradle.kts",
37
+ "Dockerfile",
38
+ "docker-compose.yml",
39
+ "docker-compose.yaml",
40
+ ".gitlab-ci.yml"
41
+ ];
42
+ /**
43
+ * Unified root-marker list used by start-flow context discovery.
44
+ * Keep this in one place so stage skill routing and start-flow scanning
45
+ * evolve together.
46
+ */
47
+ export const STACK_DISCOVERY_MARKERS = [
48
+ ...new Set([
49
+ ...STACK_REVIEW_ROUTE_PROFILES.flatMap((profile) => profile.discoveryMarkers),
50
+ ...EXTRA_DISCOVERY_MARKERS
51
+ ])
52
+ ];
53
+ /**
54
+ * Directory markers (checked with pathExists) for stack discovery.
55
+ */
56
+ export const STACK_DISCOVERY_DIR_MARKERS = [
57
+ ".github/workflows"
58
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "7.1.1",
3
+ "version": "7.2.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {