cclaw-cli 6.8.0 → 6.10.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.
Files changed (40) hide show
  1. package/dist/artifact-linter/design.js +1 -1
  2. package/dist/artifact-linter/plan.js +37 -0
  3. package/dist/artifact-linter/shared.d.ts +48 -2
  4. package/dist/artifact-linter/shared.js +54 -5
  5. package/dist/artifact-linter/tdd.d.ts +31 -0
  6. package/dist/artifact-linter/tdd.js +357 -17
  7. package/dist/artifact-linter.js +87 -2
  8. package/dist/content/examples.js +9 -9
  9. package/dist/content/harness-doc.js +1 -1
  10. package/dist/content/hooks.js +140 -3
  11. package/dist/content/iron-laws.js +6 -2
  12. package/dist/content/node-hooks.js +15 -1308
  13. package/dist/content/reference-patterns.js +2 -2
  14. package/dist/content/skills-elicitation.js +2 -2
  15. package/dist/content/skills.js +1 -1
  16. package/dist/content/stages/brainstorm.js +2 -2
  17. package/dist/content/stages/design.js +2 -2
  18. package/dist/content/stages/scope.js +2 -2
  19. package/dist/content/stages/tdd.js +7 -8
  20. package/dist/content/subagents.js +20 -2
  21. package/dist/content/templates.js +5 -15
  22. package/dist/delegation.d.ts +102 -3
  23. package/dist/delegation.js +172 -14
  24. package/dist/early-loop.js +15 -1
  25. package/dist/gate-evidence.js +15 -23
  26. package/dist/harness-adapters.js +4 -2
  27. package/dist/install.js +37 -221
  28. package/dist/internal/advance-stage.js +19 -3
  29. package/dist/internal/detect-supply-chain-changes.d.ts +6 -0
  30. package/dist/internal/detect-supply-chain-changes.js +138 -0
  31. package/dist/internal/flow-state-repair.d.ts +7 -0
  32. package/dist/internal/flow-state-repair.js +57 -18
  33. package/dist/internal/plan-split-waves.d.ts +66 -0
  34. package/dist/internal/plan-split-waves.js +249 -0
  35. package/dist/run-persistence.d.ts +2 -0
  36. package/dist/run-persistence.js +62 -3
  37. package/dist/runtime/run-hook.mjs +44 -8729
  38. package/dist/tdd-slices.d.ts +90 -0
  39. package/dist/tdd-slices.js +375 -0
  40. package/package.json +1 -1
@@ -137,9 +137,23 @@ export function parseEarlyLoopLog(text, options = {}) {
137
137
  continue;
138
138
  }
139
139
  }
140
+ // v6.9.0 schema repair: legacy logs may carry rows with no runId
141
+ // (the prior parser silently coerced them to "active", which then
142
+ // collided across runs). Surface a structured warning on read but
143
+ // skip the row so derived status doesn't fold cross-run state.
144
+ // Writers must always provide a runId (enforced upstream in the
145
+ // CLI/hook surface).
146
+ if (runId.length === 0) {
147
+ issues?.push({
148
+ lineNumber,
149
+ reason: "missing-runId: legacy entry skipped to avoid cross-run pollution",
150
+ rawLine: raw
151
+ });
152
+ continue;
153
+ }
140
154
  entries.push({
141
155
  ts: normalizeText(parsed.ts, ""),
142
- runId: runId.length > 0 ? runId : "active",
156
+ runId,
143
157
  stage: stage.length > 0 ? stage : "brainstorm",
144
158
  iteration,
145
159
  concerns,
@@ -9,8 +9,8 @@ import { readDelegationLedger } from "./delegation.js";
9
9
  import { exists } from "./fs-utils.js";
10
10
  import { computeEarlyLoopStatus, isEarlyLoopStage, normalizeEarlyLoopMaxIterations } from "./early-loop.js";
11
11
  import { detectPublicApiChanges } from "./internal/detect-public-api-changes.js";
12
+ import { detectSupplyChainChanges } from "./internal/detect-supply-chain-changes.js";
12
13
  import { readFlowState, writeFlowState } from "./runs.js";
13
- import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
14
14
  import { validateTddVerificationEvidence } from "./tdd-verification-evidence.js";
15
15
  async function currentStageArtifactExists(projectRoot, stage, track) {
16
16
  const resolved = await resolveArtifactPath(stage, {
@@ -376,35 +376,20 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState, opt
376
376
  }
377
377
  if (stage === "tdd") {
378
378
  const docsDriftDetection = await detectPublicApiChanges(projectRoot);
379
- if (docsDriftDetection.triggered) {
379
+ const supplyChainDetection = await detectSupplyChainChanges(projectRoot);
380
+ if (docsDriftDetection.triggered || supplyChainDetection.triggered) {
380
381
  const ledger = await readDelegationLedger(projectRoot);
381
382
  const hasDocUpdaterCompletion = ledger.entries.some((entry) => entry.runId === flowState.activeRunId &&
382
383
  entry.stage === "tdd" &&
383
384
  entry.agent === "doc-updater" &&
384
385
  entry.status === "completed");
385
386
  if (!hasDocUpdaterCompletion) {
386
- issues.push(`tdd docs drift gate blocked (tdd_docs_drift_check): public surface changes detected (${docsDriftDetection.changedFiles.join(", ")}) but no completed doc-updater delegation exists for the active run.`);
387
- }
388
- }
389
- const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
390
- if (await exists(tddLogPath)) {
391
- try {
392
- const tddLogRaw = await fs.readFile(tddLogPath, "utf8");
393
- const parsedCycles = parseTddCycleLog(tddLogRaw);
394
- const tddOrderValidation = validateTddCycleOrder(parsedCycles, {
395
- runId: flowState.activeRunId
396
- });
397
- if (!tddOrderValidation.ok) {
398
- const details = [...tddOrderValidation.issues];
399
- if (tddOrderValidation.openRedSlices.length > 0) {
400
- details.push(`open red slices: ${tddOrderValidation.openRedSlices.join(", ")}`);
401
- }
402
- issues.push(`tdd cycle order gate blocked: ${details.join("; ")}`);
387
+ if (docsDriftDetection.triggered) {
388
+ issues.push(`tdd docs drift gate blocked (tdd_docs_drift_check): public surface changes detected (${docsDriftDetection.changedFiles.join(", ")}) but no completed doc-updater delegation exists for the active run.`);
389
+ }
390
+ if (supplyChainDetection.triggered) {
391
+ issues.push(`tdd docs drift gate blocked (tdd_docs_drift_check): supply-chain changes detected (${supplyChainDetection.changedFiles.join(", ")}) but no completed doc-updater delegation exists for the active run.`);
403
392
  }
404
- }
405
- catch (err) {
406
- const reason = err instanceof Error ? err.message : String(err);
407
- issues.push(`tdd cycle order gate blocked: unable to read tdd-cycle-log.jsonl (${reason}).`);
408
393
  }
409
394
  }
410
395
  }
@@ -477,6 +462,13 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState, opt
477
462
  forcingPending: floor.forcingPending,
478
463
  noNewDecisions: floor.noNewDecisions
479
464
  };
465
+ // v6.9.0 — when the QA log floor is blocking, mirror that decision into
466
+ // `gates.issues` so the harness has a single structured source of truth
467
+ // for "this stage is blocked". The `qa_log_unconverged` linter rule
468
+ // remains the verbose detail/fallback channel.
469
+ if (qaLogFloor.blocking) {
470
+ issues.push(`qa log floor blocked (qa_log_unconverged): ${qaLogFloor.count}/${qaLogFloor.min} entries on stage "${stage}" (track=${flowState.track}, discoveryMode=${flowState.discoveryMode ?? "default"}). Continue elicitation or pass --skip-questions to record the stop.`);
471
+ }
480
472
  }
481
473
  return {
482
474
  ok: issues.length === 0,
@@ -293,9 +293,11 @@ export function harnessesByTier() {
293
293
  });
294
294
  }
295
295
  function ironLawsAgentsMdBlock() {
296
+ // v6.9.0: keep this set in sync with `ironLawsSkillMarkdown()` —
297
+ // post-Phase A, only `stop-clean-or-handoff` is still hook-enforced
298
+ // (Stop hook). All other iron laws live in stage HARD-GATE blocks.
296
299
  const enforcedLawIds = new Set([
297
- "stop-clean-or-handoff",
298
- "review-coverage-complete-before-ship"
300
+ "stop-clean-or-handoff"
299
301
  ]);
300
302
  const enforcedRows = IRON_LAWS
301
303
  .filter((law) => enforcedLawIds.has(law.id))
package/dist/install.js CHANGED
@@ -38,8 +38,6 @@ import { FLOW_STAGES } from "./types.js";
38
38
  const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
39
39
  const CURSOR_RULE_REL_PATH = ".cursor/rules/cclaw-workflow.mdc";
40
40
  const CURSOR_GUIDELINES_REL_PATH = ".cursor/rules/cclaw-guidelines.mdc";
41
- const GIT_HOOK_MANAGED_MARKER = "cclaw-managed-git-hook";
42
- const GIT_HOOK_RUNTIME_REL_DIR = `${RUNTIME_ROOT}/hooks/git`;
43
41
  const INIT_SENTINEL_FILE = ".init-in-progress";
44
42
  const execFileAsync = promisify(execFile);
45
43
  function runtimePath(projectRoot, ...segments) {
@@ -145,13 +143,17 @@ const DEPRECATED_COMMAND_FILES = [
145
143
  const DEPRECATED_SKILL_FILES = [
146
144
  ["flow-finish", "SKILL.md"],
147
145
  ["flow-ops", "SKILL.md"],
148
- ["tdd-cycle-log", "SKILL.md"],
149
146
  ["flow-retro", "SKILL.md"],
150
147
  ["flow-compound", "SKILL.md"],
151
148
  ["flow-archive", "SKILL.md"],
152
149
  ["flow-rewind", "SKILL.md"],
153
150
  ["using-git-worktrees", "SKILL.md"]
154
151
  ];
152
+ // Skill folders whose entire directory should be removed on sync so the
153
+ // abandoned tree doesn't linger in user projects.
154
+ const DEPRECATED_SKILL_FOLDERS_FULL = [
155
+ "tdd-cycle-log"
156
+ ];
155
157
  const DEPRECATED_STATE_FILES = [
156
158
  "checkpoint.json",
157
159
  "flow-state.snapshot.json",
@@ -161,7 +163,10 @@ const DEPRECATED_STATE_FILES = [
161
163
  "harness-gaps.json",
162
164
  "context-mode.json",
163
165
  "session-digest.md",
164
- "context-warnings.jsonl"
166
+ "context-warnings.jsonl",
167
+ // Runtime Honesty 6.9.0 removed the per-run TDD cycle JSONL: gate evidence
168
+ // now reads cycle phase progression directly from the artifact table.
169
+ "tdd-cycle-log.jsonl"
165
170
  ];
166
171
  const DEPRECATED_HOOK_FILES = [
167
172
  "observe.sh",
@@ -193,225 +198,33 @@ async function resolveGitHooksDir(projectRoot) {
193
198
  return null;
194
199
  }
195
200
  }
196
- function managedGitRuntimeScript(hookName) {
197
- return `#!/usr/bin/env node
198
- // ${GIT_HOOK_MANAGED_MARKER}: runtime ${hookName}
199
- import fs from "node:fs";
200
- import path from "node:path";
201
- import process from "node:process";
202
- import { spawnSync } from "node:child_process";
203
-
204
- const HOOK_NAME = ${JSON.stringify(hookName)};
205
- const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
206
-
207
- function runGit(args, cwd) {
208
- const result = spawnSync("git", args, {
209
- cwd,
210
- encoding: "utf8",
211
- stdio: ["ignore", "pipe", "ignore"]
212
- });
213
- return {
214
- status: typeof result.status === "number" ? result.status : 1,
215
- stdout: typeof result.stdout === "string" ? result.stdout : ""
216
- };
217
- }
218
-
219
- function resolveRepoRoot() {
220
- const result = runGit(["rev-parse", "--show-toplevel"], process.cwd());
221
- if (result.status === 0) {
222
- const root = result.stdout.trim();
223
- if (root.length > 0) return root;
224
- }
225
- return process.cwd();
226
- }
227
-
228
- function isZeroSha(value) {
229
- return /^0{40,64}$/u.test(value);
230
- }
231
-
232
- function readStdin() {
233
- try {
234
- return fs.readFileSync(0, "utf8");
235
- } catch {
236
- return "";
237
- }
238
- }
239
-
240
- function uniqueLines(chunks) {
241
- return [...new Set(chunks
242
- .join("\n")
243
- .split(/\r?\n/gu)
244
- .map((line) => line.trim())
245
- .filter((line) => line.length > 0))].join("\n");
246
- }
247
-
248
- function diffNames(root, range) {
249
- const result = runGit(["diff", "--name-only", range], root);
250
- return result.status === 0 ? result.stdout : "";
251
- }
252
-
253
- function changedFilesFromUnpushedCommits(root, localSha = "HEAD") {
254
- const revList = runGit(["rev-list", "--reverse", localSha, "--not", "--remotes"], root);
255
- if (revList.status !== 0 || revList.stdout.trim().length === 0) {
256
- return "";
257
- }
258
- const chunks = [];
259
- for (const commit of revList.stdout.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean)) {
260
- const diffTree = runGit(["diff-tree", "--no-commit-id", "--name-only", "-r", "--root", commit], root);
261
- if (diffTree.status === 0) chunks.push(diffTree.stdout);
262
- }
263
- return uniqueLines(chunks);
264
- }
265
-
266
- function changedFilesFromPrePushStdin(root, stdin) {
267
- const chunks = [];
268
- for (const rawLine of stdin.split(/\r?\n/gu)) {
269
- const parts = rawLine.trim().split(/\s+/u);
270
- if (parts.length < 4) continue;
271
- const [localRef, localSha, remoteRef, remoteSha] = parts;
272
- void localRef;
273
- void remoteRef;
274
- if (!localSha || isZeroSha(localSha)) continue;
275
- if (remoteSha && !isZeroSha(remoteSha)) {
276
- chunks.push(diffNames(root, remoteSha + ".." + localSha));
277
- continue;
278
- }
279
- const upstream = runGit(["rev-parse", "--verify", "--quiet", "@{upstream}"], root);
280
- if (upstream.status === 0 && upstream.stdout.trim().length > 0) {
281
- chunks.push(diffNames(root, upstream.stdout.trim() + ".." + localSha));
282
- continue;
283
- }
284
- chunks.push(changedFilesFromUnpushedCommits(root, localSha));
285
- }
286
- return uniqueLines(chunks);
287
- }
288
-
289
- function resolveChangedFiles(root) {
290
- if (HOOK_NAME === "pre-commit") {
291
- const result = runGit(["diff", "--cached", "--name-only"], root);
292
- return result.status === 0 ? result.stdout : "";
293
- }
294
- const stdinChanged = changedFilesFromPrePushStdin(root, readStdin());
295
- if (stdinChanged.length > 0) {
296
- return stdinChanged;
297
- }
298
- const upstreamResult = runGit(["diff", "--name-only", "@{upstream}..HEAD"], root);
299
- if (upstreamResult.status === 0) {
300
- return upstreamResult.stdout;
301
- }
302
- const unpushed = changedFilesFromUnpushedCommits(root);
303
- if (unpushed.length > 0) {
304
- return unpushed;
305
- }
306
- const fallback = runGit(["diff", "--name-only", "HEAD~1...HEAD"], root);
307
- return fallback.status === 0 ? fallback.stdout : "";
308
- }
309
-
310
- const root = resolveRepoRoot();
311
- const runtimeHook = path.join(root, RUNTIME_ROOT, "hooks", "run-hook.mjs");
312
- if (!fs.existsSync(runtimeHook)) {
313
- // cclaw git relay is installed but the runtime entrypoint is missing —
314
- // warn visibly (without blocking the commit) so the drift is noticed.
315
- process.stderr.write(
316
- "[cclaw] " + HOOK_NAME + ": " + runtimeHook + " not found; run \`cclaw sync\` to reinstall\\n"
317
- );
318
- process.exit(0);
319
- }
320
-
321
- const changedFiles = resolveChangedFiles(root)
322
- .split(/\\r?\\n/gu)
323
- .map((line) => line.trim())
324
- .filter((line) => line.length > 0);
325
- if (changedFiles.length === 0) {
326
- process.exit(0);
327
- }
328
-
329
- const payload = JSON.stringify({
330
- tool_name: "Write",
331
- tool_input: {
332
- path: changedFiles.join("\\n"),
333
- paths: changedFiles
334
- }
335
- });
336
-
337
- const result = spawnSync(process.execPath, [runtimeHook, "prompt-guard"], {
338
- cwd: root,
339
- env: process.env,
340
- input: payload,
341
- encoding: "utf8",
342
- stdio: ["pipe", "ignore", "inherit"]
343
- });
344
- process.exit(typeof result.status === "number" ? result.status : 1);
345
- `;
346
- }
347
- function managedGitRelayHook(hookName) {
348
- return `#!/usr/bin/env node
349
- // ${GIT_HOOK_MANAGED_MARKER}: relay ${hookName}
350
- import fs from "node:fs";
351
- import path from "node:path";
352
- import process from "node:process";
353
- import { spawn, spawnSync } from "node:child_process";
354
-
355
- const RUNTIME_REL_DIR = ${JSON.stringify(GIT_HOOK_RUNTIME_REL_DIR)};
356
- const HOOK_NAME = ${JSON.stringify(hookName)};
357
-
358
- function resolveRepoRoot() {
359
- const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
360
- cwd: process.cwd(),
361
- encoding: "utf8",
362
- stdio: ["ignore", "pipe", "ignore"]
363
- });
364
- if (typeof result.status === "number" && result.status === 0) {
365
- const root = (result.stdout || "").trim();
366
- if (root.length > 0) return root;
367
- }
368
- return process.cwd();
369
- }
370
-
371
- const root = resolveRepoRoot();
372
- const runtimeHook = path.join(root, RUNTIME_REL_DIR, HOOK_NAME + ".mjs");
373
- if (!fs.existsSync(runtimeHook)) {
374
- process.exit(0);
375
- }
376
-
377
- const child = spawn(process.execPath, [runtimeHook, ...process.argv.slice(2)], {
378
- cwd: root,
379
- env: process.env,
380
- stdio: "inherit"
381
- });
382
- child.on("error", () => process.exit(1));
383
- child.on("close", (code, signal) => {
384
- process.exit(signal ? 1 : typeof code === "number" ? code : 1);
385
- });
386
- `;
387
- }
388
- async function removeManagedGitHookRelays(projectRoot) {
201
+ // Legacy cleanup: prior versions installed Node-based git pre-commit/pre-push relays
202
+ // under .git/hooks/* and a runtime tree at .cclaw/hooks/git/. Runtime Honesty 6.9.0
203
+ // removed managed git hooks entirely; the cleanup below stays so existing installs
204
+ // shed the leftover files on next sync/uninstall.
205
+ const LEGACY_GIT_HOOK_MANAGED_MARKER = "cclaw-managed-git-hook";
206
+ const LEGACY_GIT_HOOK_RUNTIME_REL_DIR = `${RUNTIME_ROOT}/hooks/git`;
207
+ async function cleanupLegacyManagedGitHookRelays(projectRoot) {
389
208
  const hooksDir = await resolveGitHooksDir(projectRoot);
390
- if (!hooksDir) {
391
- return;
392
- }
393
- for (const hookName of ["pre-commit", "pre-push"]) {
394
- const hookPath = path.join(hooksDir, hookName);
395
- if (!(await exists(hookPath)))
396
- continue;
397
- let content = "";
398
- try {
399
- content = await fs.readFile(hookPath, "utf8");
400
- }
401
- catch {
402
- content = "";
403
- }
404
- if (!content.includes(GIT_HOOK_MANAGED_MARKER)) {
405
- continue;
209
+ if (hooksDir) {
210
+ for (const hookName of ["pre-commit", "pre-push"]) {
211
+ const hookPath = path.join(hooksDir, hookName);
212
+ if (!(await exists(hookPath)))
213
+ continue;
214
+ let content = "";
215
+ try {
216
+ content = await fs.readFile(hookPath, "utf8");
217
+ }
218
+ catch {
219
+ content = "";
220
+ }
221
+ if (!content.includes(LEGACY_GIT_HOOK_MANAGED_MARKER))
222
+ continue;
223
+ await fs.rm(hookPath, { force: true });
406
224
  }
407
- await fs.rm(hookPath, { force: true });
408
225
  }
409
- }
410
- async function syncManagedGitHooks(projectRoot, config) {
411
- void config;
412
- await removeManagedGitHookRelays(projectRoot);
413
226
  try {
414
- await fs.rm(path.join(projectRoot, GIT_HOOK_RUNTIME_REL_DIR), { recursive: true, force: true });
227
+ await fs.rm(path.join(projectRoot, LEGACY_GIT_HOOK_RUNTIME_REL_DIR), { recursive: true, force: true });
415
228
  }
416
229
  catch {
417
230
  // best-effort cleanup
@@ -1021,6 +834,9 @@ async function cleanLegacyArtifacts(projectRoot) {
1021
834
  for (const legacyFolder of DEPRECATED_STAGE_SKILL_FOLDERS) {
1022
835
  await removeBestEffort(runtimePath(projectRoot, "skills", legacyFolder), true);
1023
836
  }
837
+ for (const legacyFolder of DEPRECATED_SKILL_FOLDERS_FULL) {
838
+ await removeBestEffort(runtimePath(projectRoot, "skills", legacyFolder), true);
839
+ }
1024
840
  for (const legacyAgentFile of DEPRECATED_AGENT_FILES) {
1025
841
  await removeBestEffort(runtimePath(projectRoot, "agents", legacyAgentFile));
1026
842
  }
@@ -1173,7 +989,7 @@ async function materializeRuntime(projectRoot, config, forceStateReset, operatio
1173
989
  await ensureKnowledgeStore(projectRoot);
1174
990
  await writeHooks(projectRoot, config);
1175
991
  await syncDisabledHarnessArtifacts(projectRoot, harnesses);
1176
- await syncManagedGitHooks(projectRoot, config);
992
+ await cleanupLegacyManagedGitHookRelays(projectRoot);
1177
993
  await syncHarnessShims(projectRoot, harnesses);
1178
994
  await assertExpectedHarnessShims(projectRoot, harnesses);
1179
995
  await writeCursorWorkflowRule(projectRoot, harnesses);
@@ -1401,7 +1217,7 @@ export async function uninstallCclaw(projectRoot) {
1401
1217
  }
1402
1218
  await removeCclawFromAgentsMd(projectRoot);
1403
1219
  await removeGitignorePatterns(projectRoot);
1404
- await removeManagedGitHookRelays(projectRoot);
1220
+ await cleanupLegacyManagedGitHookRelays(projectRoot);
1405
1221
  const hookFiles = [
1406
1222
  ".claude/hooks/hooks.json",
1407
1223
  ".cursor/hooks.json",
@@ -14,7 +14,9 @@ import { parseAdvanceStageArgs, parseCancelRunArgs, parseHookArgs, parseRewindAr
14
14
  import { parseFlowStateRepairArgs, runFlowStateRepair } from "./flow-state-repair.js";
15
15
  import { parseWaiverGrantArgs, runWaiverGrant } from "./waiver-grant.js";
16
16
  import { FlowStateGuardMismatchError, verifyFlowStateGuard } from "../run-persistence.js";
17
- import { DelegationTimestampError, DispatchDuplicateError } from "../delegation.js";
17
+ import { DelegationTimestampError, DispatchCapError, DispatchDuplicateError, DispatchOverlapError } from "../delegation.js";
18
+ import { parseTddSliceRecordArgs, runTddSliceRecord } from "../tdd-slices.js";
19
+ import { parsePlanSplitWavesArgs, runPlanSplitWaves } from "./plan-split-waves.js";
18
20
  /**
19
21
  * Subcommands that mutate or consult flow-state.json via the CLI runtime.
20
22
  * They all require the sha256 sidecar to match before continuing so a
@@ -32,7 +34,7 @@ const GUARD_ENFORCED_SUBCOMMANDS = new Set([
32
34
  export async function runInternalCommand(projectRoot, argv, io) {
33
35
  const [subcommand, ...tokens] = argv;
34
36
  if (!subcommand) {
35
- io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant\n");
37
+ io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | tdd-slice-record | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant | plan-split-waves\n");
36
38
  return 1;
37
39
  }
38
40
  try {
@@ -84,7 +86,13 @@ export async function runInternalCommand(projectRoot, argv, io) {
84
86
  if (subcommand === "waiver-grant") {
85
87
  return await runWaiverGrant(projectRoot, parseWaiverGrantArgs(tokens), io);
86
88
  }
87
- io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant\n`);
89
+ if (subcommand === "tdd-slice-record") {
90
+ return await runTddSliceRecord(projectRoot, parseTddSliceRecordArgs(tokens), io);
91
+ }
92
+ if (subcommand === "plan-split-waves") {
93
+ return await runPlanSplitWaves(projectRoot, parsePlanSplitWavesArgs(tokens), io);
94
+ }
95
+ io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | tdd-slice-record | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant | plan-split-waves\n`);
88
96
  return 1;
89
97
  }
90
98
  catch (err) {
@@ -100,6 +108,14 @@ export async function runInternalCommand(projectRoot, argv, io) {
100
108
  io.stderr.write(`error: dispatch_duplicate — ${err.message}\n`);
101
109
  return 2;
102
110
  }
111
+ if (err instanceof DispatchOverlapError) {
112
+ io.stderr.write(`error: dispatch_overlap — ${err.message}\n`);
113
+ return 2;
114
+ }
115
+ if (err instanceof DispatchCapError) {
116
+ io.stderr.write(`error: dispatch_cap — ${err.message}\n`);
117
+ return 2;
118
+ }
103
119
  io.stderr.write(`cclaw internal ${subcommand} failed: ${err instanceof Error ? err.message : String(err)}\n`);
104
120
  return 1;
105
121
  }
@@ -0,0 +1,6 @@
1
+ export interface SupplyChainChangeDetection {
2
+ triggered: boolean;
3
+ changedFiles: string[];
4
+ reasons: string[];
5
+ }
6
+ export declare function detectSupplyChainChanges(projectRoot: string): Promise<SupplyChainChangeDetection>;
@@ -0,0 +1,138 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ const WORKFLOW_PATH = /(^|\/)\.github\/workflows\//u;
5
+ const CURSOR_CONFIG_PATH = /(^|\/)\.cursor\//u;
6
+ const PACKAGE_JSON_PATH = /(^|\/)package\.json$/u;
7
+ const SUPPLY_CHAIN_DEP_KEYS = [
8
+ "dependencies",
9
+ "devDependencies",
10
+ "peerDependencies",
11
+ "optionalDependencies"
12
+ ];
13
+ async function resolveDiffBase(projectRoot) {
14
+ try {
15
+ const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD~1"], {
16
+ cwd: projectRoot
17
+ });
18
+ const base = stdout.trim();
19
+ return base.length > 0 ? base : null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ async function readFileAtRev(projectRoot, rev, filePath) {
26
+ try {
27
+ const { stdout } = await execFileAsync("git", ["show", `${rev}:${filePath}`], {
28
+ cwd: projectRoot,
29
+ maxBuffer: 32 * 1024 * 1024
30
+ });
31
+ return stdout;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ function dependencyMapsDiffer(before, after) {
38
+ const beforeKeys = before ? Object.keys(before).sort() : [];
39
+ const afterKeys = after ? Object.keys(after).sort() : [];
40
+ if (beforeKeys.length !== afterKeys.length)
41
+ return true;
42
+ for (let i = 0; i < beforeKeys.length; i += 1) {
43
+ if (beforeKeys[i] !== afterKeys[i])
44
+ return true;
45
+ const k = beforeKeys[i];
46
+ if (before[k] !== after[k]) {
47
+ return true;
48
+ }
49
+ }
50
+ return false;
51
+ }
52
+ async function packageJsonHasDependencyDiff(projectRoot, base, filePath) {
53
+ const beforeRaw = await readFileAtRev(projectRoot, base, filePath);
54
+ const afterRaw = await readFileAtRev(projectRoot, "HEAD", filePath);
55
+ // If either side is missing or unparseable, treat as changed (be conservative).
56
+ if (beforeRaw === null || afterRaw === null)
57
+ return true;
58
+ let beforeJson;
59
+ let afterJson;
60
+ try {
61
+ beforeJson = JSON.parse(beforeRaw);
62
+ }
63
+ catch {
64
+ return true;
65
+ }
66
+ try {
67
+ afterJson = JSON.parse(afterRaw);
68
+ }
69
+ catch {
70
+ return true;
71
+ }
72
+ const beforeObj = beforeJson !== null && typeof beforeJson === "object"
73
+ ? beforeJson
74
+ : {};
75
+ const afterObj = afterJson !== null && typeof afterJson === "object"
76
+ ? afterJson
77
+ : {};
78
+ for (const key of SUPPLY_CHAIN_DEP_KEYS) {
79
+ const beforeMap = (beforeObj[key] !== null && typeof beforeObj[key] === "object")
80
+ ? beforeObj[key]
81
+ : undefined;
82
+ const afterMap = (afterObj[key] !== null && typeof afterObj[key] === "object")
83
+ ? afterObj[key]
84
+ : undefined;
85
+ if (dependencyMapsDiffer(beforeMap, afterMap)) {
86
+ return true;
87
+ }
88
+ }
89
+ return false;
90
+ }
91
+ export async function detectSupplyChainChanges(projectRoot) {
92
+ const base = await resolveDiffBase(projectRoot);
93
+ if (!base) {
94
+ return { triggered: false, changedFiles: [], reasons: [] };
95
+ }
96
+ let changed = [];
97
+ try {
98
+ const range = `${base}..HEAD`;
99
+ const { stdout } = await execFileAsync("git", ["diff", "--name-only", range], {
100
+ cwd: projectRoot
101
+ });
102
+ changed = stdout
103
+ .split(/\r?\n/gu)
104
+ .map((line) => line.trim())
105
+ .filter((line) => line.length > 0);
106
+ }
107
+ catch {
108
+ return { triggered: false, changedFiles: [], reasons: [] };
109
+ }
110
+ const matchedFiles = [];
111
+ const reasons = [];
112
+ for (const filePath of changed) {
113
+ if (WORKFLOW_PATH.test(filePath)) {
114
+ matchedFiles.push(filePath);
115
+ reasons.push(`.github/workflows changed: ${filePath}`);
116
+ continue;
117
+ }
118
+ if (CURSOR_CONFIG_PATH.test(filePath)) {
119
+ matchedFiles.push(filePath);
120
+ reasons.push(`.cursor config changed: ${filePath}`);
121
+ continue;
122
+ }
123
+ if (PACKAGE_JSON_PATH.test(filePath)) {
124
+ // Only flag when supply-chain dependency keys differ.
125
+ const depDiffers = await packageJsonHasDependencyDiff(projectRoot, base, filePath);
126
+ if (depDiffers) {
127
+ matchedFiles.push(filePath);
128
+ reasons.push(`${filePath} dependencies/devDependencies/peerDependencies/optionalDependencies changed`);
129
+ }
130
+ continue;
131
+ }
132
+ }
133
+ return {
134
+ triggered: matchedFiles.length > 0,
135
+ changedFiles: matchedFiles,
136
+ reasons
137
+ };
138
+ }
@@ -7,6 +7,13 @@ export interface FlowStateRepairArgs {
7
7
  reason: string;
8
8
  json: boolean;
9
9
  quiet: boolean;
10
+ /**
11
+ * v6.9.0 — when true, normalize `state/early-loop.json` to the canonical
12
+ * shape derived from `early-loop-log.jsonl`. Lets operators recover from
13
+ * legacy hand-written `early-loop.json` files that drifted from the
14
+ * source-of-truth log.
15
+ */
16
+ earlyLoop: boolean;
10
17
  }
11
18
  export declare function parseFlowStateRepairArgs(tokens: string[]): FlowStateRepairArgs;
12
19
  export declare function runFlowStateRepair(projectRoot: string, args: FlowStateRepairArgs, io: InternalIo): Promise<number>;