cclaw-cli 6.6.0 → 6.8.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 (34) hide show
  1. package/dist/artifact-linter/findings-dedup.d.ts +56 -0
  2. package/dist/artifact-linter/findings-dedup.js +232 -0
  3. package/dist/artifact-linter/plan.js +3 -2
  4. package/dist/artifact-linter/shared.d.ts +49 -0
  5. package/dist/artifact-linter/shared.js +35 -0
  6. package/dist/artifact-linter.d.ts +1 -1
  7. package/dist/artifact-linter.js +45 -3
  8. package/dist/content/hooks.js +241 -7
  9. package/dist/content/node-hooks.js +43 -0
  10. package/dist/content/skills-elicitation.js +3 -6
  11. package/dist/content/skills.js +3 -1
  12. package/dist/content/stages/brainstorm.js +4 -4
  13. package/dist/content/stages/scope.js +2 -2
  14. package/dist/content/templates.js +3 -2
  15. package/dist/delegation.d.ts +107 -0
  16. package/dist/delegation.js +223 -6
  17. package/dist/internal/advance-stage/advance.js +23 -1
  18. package/dist/internal/advance-stage/parsers.d.ts +8 -0
  19. package/dist/internal/advance-stage/parsers.js +7 -0
  20. package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +3 -0
  21. package/dist/internal/advance-stage/proactive-delegation-trace.js +8 -1
  22. package/dist/internal/advance-stage/rewind.js +2 -2
  23. package/dist/internal/advance-stage/start-flow.js +4 -1
  24. package/dist/internal/advance-stage.js +41 -2
  25. package/dist/internal/flow-state-repair.d.ts +13 -0
  26. package/dist/internal/flow-state-repair.js +65 -0
  27. package/dist/internal/waiver-grant.d.ts +62 -0
  28. package/dist/internal/waiver-grant.js +294 -0
  29. package/dist/run-persistence.d.ts +70 -0
  30. package/dist/run-persistence.js +215 -3
  31. package/dist/runs.d.ts +1 -1
  32. package/dist/runs.js +1 -1
  33. package/dist/runtime/run-hook.mjs +43 -0
  34. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { RUNTIME_ROOT } from "./constants.js";
@@ -15,9 +16,49 @@ export class InvalidStageTransitionError extends Error {
15
16
  }
16
17
  }
17
18
  const FLOW_STATE_REL_PATH = `${RUNTIME_ROOT}/state/flow-state.json`;
19
+ const FLOW_STATE_GUARD_REL_PATH = `${RUNTIME_ROOT}/.flow-state.guard.json`;
20
+ const FLOW_STATE_REPAIR_LOG_REL_PATH = `${RUNTIME_ROOT}/.flow-state-repair.log`;
18
21
  const ARCHIVE_DIR_REL_PATH = `${RUNTIME_ROOT}/archive`;
19
22
  const ACTIVE_ARTIFACTS_REL_PATH = `${RUNTIME_ROOT}/artifacts`;
20
23
  const FLOW_STAGE_SET = new Set(FLOW_STAGES);
24
+ const DEFAULT_WRITER_SUBSYSTEM = "cclaw-cli";
25
+ const DEFAULT_REPAIR_REASON_PATTERN = /^[a-z][a-z0-9_-]{2,}$/u;
26
+ export class FlowStateGuardMismatchError extends Error {
27
+ expectedSha;
28
+ actualSha;
29
+ lastWriter;
30
+ writtenAt;
31
+ runId;
32
+ statePath;
33
+ guardPath;
34
+ repairCommand;
35
+ constructor(details) {
36
+ super(`flow-state guard mismatch: ${details.runId}\n` +
37
+ `expected sha: ${details.expectedSha}\n` +
38
+ `actual sha: ${details.actualSha}\n` +
39
+ `last writer: ${details.lastWriter}@${details.writtenAt}\n` +
40
+ `do not edit flow-state.json by hand. To recover, run:\n` +
41
+ ` ${details.repairCommand}`);
42
+ this.name = "FlowStateGuardMismatchError";
43
+ this.expectedSha = details.expectedSha;
44
+ this.actualSha = details.actualSha;
45
+ this.lastWriter = details.lastWriter;
46
+ this.writtenAt = details.writtenAt;
47
+ this.runId = details.runId;
48
+ this.statePath = details.statePath;
49
+ this.guardPath = details.guardPath;
50
+ this.repairCommand = details.repairCommand;
51
+ }
52
+ }
53
+ function canonicalFlowStateShaFromRaw(raw) {
54
+ return createHash("sha256").update(raw, "utf8").digest("hex");
55
+ }
56
+ function guardSidecarPath(projectRoot) {
57
+ return path.join(projectRoot, FLOW_STATE_GUARD_REL_PATH);
58
+ }
59
+ function repairLogPath(projectRoot) {
60
+ return path.join(projectRoot, FLOW_STATE_REPAIR_LOG_REL_PATH);
61
+ }
21
62
  function validateFlowTransition(prev, next) {
22
63
  if (prev.activeRunId !== next.activeRunId) {
23
64
  // New run — only reset paths may change the runId, but those set allowReset.
@@ -488,6 +529,72 @@ async function quarantineCorruptState(statePath, cause) {
488
529
  }
489
530
  throw new CorruptFlowStateError(statePath, quarantinedPath, cause);
490
531
  }
532
+ function buildRepairCommand(reason = "<manual_edit_recovery>") {
533
+ return `cclaw-cli internal flow-state-repair --reason "${reason}"`;
534
+ }
535
+ async function readGuardSidecar(projectRoot) {
536
+ const guardPath = guardSidecarPath(projectRoot);
537
+ try {
538
+ const raw = await fs.readFile(guardPath, "utf8");
539
+ const parsed = JSON.parse(raw);
540
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
541
+ return null;
542
+ }
543
+ const sha256 = typeof parsed.sha256 === "string" ? parsed.sha256 : "";
544
+ const writtenAt = typeof parsed.writtenAt === "string" ? parsed.writtenAt : "";
545
+ const writerSubsystem = typeof parsed.writerSubsystem === "string" ? parsed.writerSubsystem : "";
546
+ const runId = typeof parsed.runId === "string" ? parsed.runId : "";
547
+ if (!sha256 || !writtenAt || !writerSubsystem || !runId) {
548
+ return null;
549
+ }
550
+ return { sha256, writtenAt, writerSubsystem, runId };
551
+ }
552
+ catch {
553
+ return null;
554
+ }
555
+ }
556
+ async function verifyFlowStateGuardFromRaw(projectRoot, statePath, rawContents) {
557
+ const sidecar = await readGuardSidecar(projectRoot);
558
+ if (!sidecar) {
559
+ // Legacy: flow-state.json was written by a pre-guard runtime, or sidecar
560
+ // was intentionally reset. Permit the read so existing projects keep
561
+ // working; the next legitimate stage-complete writes a fresh sidecar.
562
+ return;
563
+ }
564
+ const actualSha = canonicalFlowStateShaFromRaw(rawContents);
565
+ if (actualSha === sidecar.sha256) {
566
+ return;
567
+ }
568
+ throw new FlowStateGuardMismatchError({
569
+ expectedSha: sidecar.sha256,
570
+ actualSha,
571
+ lastWriter: sidecar.writerSubsystem,
572
+ writtenAt: sidecar.writtenAt,
573
+ runId: sidecar.runId,
574
+ statePath,
575
+ guardPath: guardSidecarPath(projectRoot),
576
+ repairCommand: buildRepairCommand("manual_edit_recovery")
577
+ });
578
+ }
579
+ /**
580
+ * Verify the on-disk flow-state against the sha256 sidecar. Throws
581
+ * `FlowStateGuardMismatchError` when manual editing is detected. Safe to
582
+ * call on projects that have never written a sidecar: a missing sidecar is
583
+ * treated as "legacy runtime" and the check silently succeeds.
584
+ */
585
+ export async function verifyFlowStateGuard(projectRoot) {
586
+ const statePath = flowStatePath(projectRoot);
587
+ if (!(await exists(statePath)))
588
+ return;
589
+ let raw;
590
+ try {
591
+ raw = await fs.readFile(statePath, "utf8");
592
+ }
593
+ catch {
594
+ return;
595
+ }
596
+ await verifyFlowStateGuardFromRaw(projectRoot, statePath, raw);
597
+ }
491
598
  export async function readFlowState(projectRoot, options = {}) {
492
599
  void options;
493
600
  const statePath = flowStatePath(projectRoot);
@@ -513,13 +620,46 @@ export async function readFlowState(projectRoot, options = {}) {
513
620
  }
514
621
  return coerceFlowState(parsed).state;
515
622
  }
623
+ /**
624
+ * Guarded read wrapper used by runtime hook scripts and the repair CLI.
625
+ * Unlike `readFlowState`, it enforces the sha256 sidecar before returning:
626
+ * a manual edit to flow-state.json fails fast with
627
+ * `FlowStateGuardMismatchError`.
628
+ */
629
+ export async function readFlowStateGuarded(projectRoot, options = {}) {
630
+ void options;
631
+ const statePath = flowStatePath(projectRoot);
632
+ if (!(await exists(statePath))) {
633
+ return createInitialFlowState();
634
+ }
635
+ let raw;
636
+ try {
637
+ raw = await fs.readFile(statePath, "utf8");
638
+ }
639
+ catch (readErr) {
640
+ throw new CorruptFlowStateError(statePath, statePath, readErr);
641
+ }
642
+ await verifyFlowStateGuardFromRaw(projectRoot, statePath, raw);
643
+ let parsed;
644
+ try {
645
+ parsed = JSON.parse(raw);
646
+ }
647
+ catch (parseErr) {
648
+ await quarantineCorruptState(statePath, parseErr);
649
+ }
650
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
651
+ await quarantineCorruptState(statePath, new Error("flow-state.json did not deserialize to a JSON object"));
652
+ }
653
+ return coerceFlowState(parsed).state;
654
+ }
516
655
  export async function writeFlowState(projectRoot, state, options = {}) {
656
+ const writerSubsystem = options.writerSubsystem?.trim() || DEFAULT_WRITER_SUBSYSTEM;
517
657
  const doWrite = async () => {
518
658
  const statePath = flowStatePath(projectRoot);
519
659
  if (!options.allowReset && (await exists(statePath))) {
520
660
  try {
521
- const raw = await fs.readFile(statePath, "utf8");
522
- const parsed = JSON.parse(raw);
661
+ const rawExisting = await fs.readFile(statePath, "utf8");
662
+ const parsed = JSON.parse(rawExisting);
523
663
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
524
664
  const prev = coerceFlowState(parsed).state;
525
665
  validateFlowTransition(prev, state);
@@ -533,7 +673,16 @@ export async function writeFlowState(projectRoot, state, options = {}) {
533
673
  }
534
674
  }
535
675
  const safe = coerceFlowState({ ...state }).state;
536
- await writeFileSafe(statePath, `${JSON.stringify(safe, null, 2)}\n`, { mode: 0o600 });
676
+ const canonicalPayload = `${JSON.stringify(safe, null, 2)}\n`;
677
+ const sha256 = canonicalFlowStateShaFromRaw(canonicalPayload);
678
+ await writeFileSafe(statePath, canonicalPayload, { mode: 0o600 });
679
+ const sidecar = {
680
+ sha256,
681
+ writtenAt: new Date().toISOString(),
682
+ writerSubsystem,
683
+ runId: safe.activeRunId
684
+ };
685
+ await writeFileSafe(guardSidecarPath(projectRoot), `${JSON.stringify(sidecar, null, 2)}\n`, { mode: 0o600 });
537
686
  };
538
687
  if (options.skipLock) {
539
688
  await doWrite();
@@ -542,6 +691,69 @@ export async function writeFlowState(projectRoot, state, options = {}) {
542
691
  await withDirectoryLock(flowStateLockPath(projectRoot), doWrite);
543
692
  }
544
693
  }
694
+ /**
695
+ * Named entry point for the write-guard workstream. Equivalent to
696
+ * `writeFlowState`: the write always produces the sha256 sidecar via
697
+ * the internal implementation so every existing writer inherits the
698
+ * guard without rewriting callsites.
699
+ */
700
+ export async function writeFlowStateGuarded(projectRoot, state, options = {}) {
701
+ await writeFlowState(projectRoot, state, options);
702
+ }
703
+ /**
704
+ * Recompute the write-guard sidecar from the current on-disk flow-state
705
+ * contents and append an audit entry to `.cclaw/.flow-state-repair.log`.
706
+ * The reason is required so no repair happens without an operator-visible
707
+ * rationale. Intended to be called only from the explicit
708
+ * `cclaw-cli internal flow-state-repair` subcommand.
709
+ */
710
+ export async function repairFlowStateGuard(projectRoot, reason) {
711
+ const trimmed = reason.trim();
712
+ if (trimmed.length === 0) {
713
+ throw new Error("flow-state-repair requires --reason=<slug> (e.g. --reason=\"manual_edit_recovery\").");
714
+ }
715
+ if (!DEFAULT_REPAIR_REASON_PATTERN.test(trimmed)) {
716
+ throw new Error("flow-state-repair --reason must match /^[a-z][a-z0-9_-]{2,}$/ (short lowercase slug).");
717
+ }
718
+ const statePath = flowStatePath(projectRoot);
719
+ if (!(await exists(statePath))) {
720
+ throw new Error(`flow-state-repair: ${FLOW_STATE_REL_PATH} does not exist; nothing to repair.`);
721
+ }
722
+ return withDirectoryLock(flowStateLockPath(projectRoot), async () => {
723
+ const raw = await fs.readFile(statePath, "utf8");
724
+ let runId = "unknown-run";
725
+ try {
726
+ const parsed = JSON.parse(raw);
727
+ const coerced = coerceFlowState(parsed).state;
728
+ runId = coerced.activeRunId;
729
+ }
730
+ catch {
731
+ // parsing failure falls back to "unknown-run"; repair intentionally
732
+ // accepts the contents as-is so operators can recover even from
733
+ // borderline JSON after manual edits.
734
+ }
735
+ const sha256 = canonicalFlowStateShaFromRaw(raw);
736
+ const sidecar = {
737
+ sha256,
738
+ writtenAt: new Date().toISOString(),
739
+ writerSubsystem: "flow-state-repair",
740
+ runId
741
+ };
742
+ const guardPath = guardSidecarPath(projectRoot);
743
+ await writeFileSafe(guardPath, `${JSON.stringify(sidecar, null, 2)}\n`, { mode: 0o600 });
744
+ const logPath = repairLogPath(projectRoot);
745
+ await ensureDir(path.dirname(logPath));
746
+ const logLine = `${sidecar.writtenAt} reason=${trimmed} runId=${sidecar.runId} sha256=${sidecar.sha256}\n`;
747
+ await fs.appendFile(logPath, logLine, "utf8");
748
+ return { sidecar, repairLogPath: logPath, guardPath };
749
+ });
750
+ }
751
+ export function flowStateGuardSidecarPathFor(projectRoot) {
752
+ return guardSidecarPath(projectRoot);
753
+ }
754
+ export function flowStateRepairLogPathFor(projectRoot) {
755
+ return repairLogPath(projectRoot);
756
+ }
545
757
  /**
546
758
  * Exposed path helper so callers that need to serialize a multi-step
547
759
  * state operation with flow-state writes (e.g. run archival) can
package/dist/runs.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { CorruptFlowStateError, InvalidStageTransitionError, type WriteFlowStateOptions, ensureRunSystem, readFlowState, writeFlowState } from "./run-persistence.js";
1
+ export { CorruptFlowStateError, FlowStateGuardMismatchError, InvalidStageTransitionError, type FlowStateGuardSidecar, type FlowStateRepairResult, type WriteFlowStateOptions, ensureRunSystem, flowStateGuardSidecarPathFor, flowStateRepairLogPathFor, readFlowState, readFlowStateGuarded, repairFlowStateGuard, verifyFlowStateGuard, writeFlowState, writeFlowStateGuarded } from "./run-persistence.js";
2
2
  export { ARCHIVE_DISPOSITIONS, archiveRun, countActiveKnowledgeEntries, listRuns, type ArchiveDisposition, type ArchiveManifest, type ArchiveRunOptions, type ArchiveRunResult, type CclawRunMeta } from "./run-archive.js";
package/dist/runs.js CHANGED
@@ -1,2 +1,2 @@
1
- export { CorruptFlowStateError, InvalidStageTransitionError, ensureRunSystem, readFlowState, writeFlowState } from "./run-persistence.js";
1
+ export { CorruptFlowStateError, FlowStateGuardMismatchError, InvalidStageTransitionError, ensureRunSystem, flowStateGuardSidecarPathFor, flowStateRepairLogPathFor, readFlowState, readFlowStateGuarded, repairFlowStateGuard, verifyFlowStateGuard, writeFlowState, writeFlowStateGuarded } from "./run-persistence.js";
2
2
  export { ARCHIVE_DISPOSITIONS, archiveRun, countActiveKnowledgeEntries, listRuns } from "./run-archive.js";
@@ -7573,12 +7573,14 @@ function nodeHookRuntimeScript(options = {}) {
7573
7573
  const defaultDisabledHooks = [];
7574
7574
  const cliRuntime = resolveCliRuntimeForGeneratedHook();
7575
7575
  return `#!/usr/bin/env node
7576
+ import { createHash } from "node:crypto";
7576
7577
  import fs from "node:fs/promises";
7577
7578
  import path from "node:path";
7578
7579
  import process from "node:process";
7579
7580
  import { spawn } from "node:child_process";
7580
7581
 
7581
7582
  const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
7583
+ const FLOW_STATE_GUARD_REL_PATH = RUNTIME_ROOT + "/.flow-state.guard.json";
7582
7584
  // Single strictness default, derived from config.strictness at install time.
7583
7585
  // \`CCLAW_STRICTNESS\` env var overrides for the current process. All guards
7584
7586
  // (prompt, workflow, TDD, iron-laws) route through \`resolveStrictness()\`.
@@ -8541,6 +8543,40 @@ function extractCodePathsFromText(value) {
8541
8543
  return out;
8542
8544
  }
8543
8545
 
8546
+ async function verifyFlowStateGuardInline(root, hookName) {
8547
+ const statePath = path.join(root, RUNTIME_ROOT, "state", "flow-state.json");
8548
+ const guardPath = path.join(root, FLOW_STATE_GUARD_REL_PATH);
8549
+ let raw;
8550
+ try {
8551
+ raw = await fs.readFile(statePath, "utf8");
8552
+ } catch {
8553
+ return true;
8554
+ }
8555
+ let guard;
8556
+ try {
8557
+ const guardRaw = await fs.readFile(guardPath, "utf8");
8558
+ guard = JSON.parse(guardRaw);
8559
+ } catch {
8560
+ return true;
8561
+ }
8562
+ if (!guard || typeof guard !== "object" || typeof guard.sha256 !== "string") {
8563
+ return true;
8564
+ }
8565
+ const actual = createHash("sha256").update(raw, "utf8").digest("hex");
8566
+ if (actual === guard.sha256) return true;
8567
+ const hookLabel = typeof hookName === "string" && hookName.length > 0 ? hookName : "hook";
8568
+ process.stderr.write(
8569
+ "[cclaw] " + hookLabel + ": flow-state guard mismatch: " + (guard.runId || "unknown-run") + "\\n" +
8570
+ "expected sha: " + guard.sha256 + "\\n" +
8571
+ "actual sha: " + actual + "\\n" +
8572
+ "last writer: " + (guard.writerSubsystem || "unknown") + "@" + (guard.writtenAt || "unknown") + "\\n" +
8573
+ "do not edit flow-state.json by hand. To recover, run:\\n" +
8574
+ " cclaw-cli internal flow-state-repair --reason \\"manual_edit_recovery\\"\\n"
8575
+ );
8576
+ await recordHookError(root, hookLabel, "flow-state guard mismatch actual=" + actual + " expected=" + guard.sha256).catch(() => undefined);
8577
+ return false;
8578
+ }
8579
+
8544
8580
  async function readFlowState(root) {
8545
8581
  const statePath = path.join(root, RUNTIME_ROOT, "state", "flow-state.json");
8546
8582
  // Loud-on-corrupt: if flow-state.json exists but fails JSON.parse, log
@@ -9634,6 +9670,13 @@ async function main() {
9634
9670
  };
9635
9671
 
9636
9672
  try {
9673
+ if (hookName === "session-start" || hookName === "stop-handoff") {
9674
+ const guardOk = await verifyFlowStateGuardInline(runtime.root, hookName);
9675
+ if (!guardOk) {
9676
+ process.exitCode = 2;
9677
+ return;
9678
+ }
9679
+ }
9637
9680
  if (hookName === "session-start") {
9638
9681
  process.exitCode = await handleSessionStart(runtime);
9639
9682
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "6.6.0",
3
+ "version": "6.8.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {