cclaw-cli 6.5.0 → 6.7.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/artifact-linter/brainstorm.js +2 -1
- package/dist/artifact-linter/design.js +2 -1
- package/dist/artifact-linter/findings-dedup.d.ts +56 -0
- package/dist/artifact-linter/findings-dedup.js +232 -0
- package/dist/artifact-linter/plan.js +4 -2
- package/dist/artifact-linter/review.js +2 -1
- package/dist/artifact-linter/scope.js +2 -1
- package/dist/artifact-linter/shared.d.ts +103 -0
- package/dist/artifact-linter/shared.js +177 -0
- package/dist/artifact-linter/tdd.js +2 -1
- package/dist/artifact-linter.d.ts +1 -1
- package/dist/artifact-linter.js +45 -3
- package/dist/content/examples.d.ts +32 -0
- package/dist/content/examples.js +74 -0
- package/dist/content/hooks.js +36 -1
- package/dist/content/node-hooks.js +43 -0
- package/dist/content/skills-elicitation.js +3 -6
- package/dist/content/skills.d.ts +10 -0
- package/dist/content/skills.js +44 -2
- package/dist/content/stages/brainstorm.js +7 -5
- package/dist/content/stages/design.js +3 -1
- package/dist/content/stages/plan.js +3 -1
- package/dist/content/stages/review.js +3 -1
- package/dist/content/stages/scope.js +5 -3
- package/dist/content/stages/ship.js +2 -1
- package/dist/content/stages/spec.js +3 -1
- package/dist/content/stages/tdd.js +3 -1
- package/dist/content/templates.d.ts +9 -0
- package/dist/content/templates.js +45 -2
- package/dist/delegation.d.ts +9 -0
- package/dist/delegation.js +3 -0
- package/dist/internal/advance-stage/advance.js +23 -1
- package/dist/internal/advance-stage/parsers.d.ts +8 -0
- package/dist/internal/advance-stage/parsers.js +7 -0
- package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +3 -0
- package/dist/internal/advance-stage/proactive-delegation-trace.js +8 -1
- package/dist/internal/advance-stage/rewind.js +2 -2
- package/dist/internal/advance-stage/start-flow.js +4 -1
- package/dist/internal/advance-stage.js +32 -2
- package/dist/internal/flow-state-repair.d.ts +13 -0
- package/dist/internal/flow-state-repair.js +65 -0
- package/dist/internal/waiver-grant.d.ts +62 -0
- package/dist/internal/waiver-grant.js +294 -0
- package/dist/run-persistence.d.ts +70 -0
- package/dist/run-persistence.js +215 -3
- package/dist/runs.d.ts +1 -1
- package/dist/runs.js +1 -1
- package/dist/runtime/run-hook.mjs +43 -0
- package/package.json +1 -1
package/dist/run-persistence.js
CHANGED
|
@@ -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
|
|
522
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
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;
|