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.
Files changed (49) hide show
  1. package/dist/artifact-linter/brainstorm.js +2 -1
  2. package/dist/artifact-linter/design.js +2 -1
  3. package/dist/artifact-linter/findings-dedup.d.ts +56 -0
  4. package/dist/artifact-linter/findings-dedup.js +232 -0
  5. package/dist/artifact-linter/plan.js +4 -2
  6. package/dist/artifact-linter/review.js +2 -1
  7. package/dist/artifact-linter/scope.js +2 -1
  8. package/dist/artifact-linter/shared.d.ts +103 -0
  9. package/dist/artifact-linter/shared.js +177 -0
  10. package/dist/artifact-linter/tdd.js +2 -1
  11. package/dist/artifact-linter.d.ts +1 -1
  12. package/dist/artifact-linter.js +45 -3
  13. package/dist/content/examples.d.ts +32 -0
  14. package/dist/content/examples.js +74 -0
  15. package/dist/content/hooks.js +36 -1
  16. package/dist/content/node-hooks.js +43 -0
  17. package/dist/content/skills-elicitation.js +3 -6
  18. package/dist/content/skills.d.ts +10 -0
  19. package/dist/content/skills.js +44 -2
  20. package/dist/content/stages/brainstorm.js +7 -5
  21. package/dist/content/stages/design.js +3 -1
  22. package/dist/content/stages/plan.js +3 -1
  23. package/dist/content/stages/review.js +3 -1
  24. package/dist/content/stages/scope.js +5 -3
  25. package/dist/content/stages/ship.js +2 -1
  26. package/dist/content/stages/spec.js +3 -1
  27. package/dist/content/stages/tdd.js +3 -1
  28. package/dist/content/templates.d.ts +9 -0
  29. package/dist/content/templates.js +45 -2
  30. package/dist/delegation.d.ts +9 -0
  31. package/dist/delegation.js +3 -0
  32. package/dist/internal/advance-stage/advance.js +23 -1
  33. package/dist/internal/advance-stage/parsers.d.ts +8 -0
  34. package/dist/internal/advance-stage/parsers.js +7 -0
  35. package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +3 -0
  36. package/dist/internal/advance-stage/proactive-delegation-trace.js +8 -1
  37. package/dist/internal/advance-stage/rewind.js +2 -2
  38. package/dist/internal/advance-stage/start-flow.js +4 -1
  39. package/dist/internal/advance-stage.js +32 -2
  40. package/dist/internal/flow-state-repair.d.ts +13 -0
  41. package/dist/internal/flow-state-repair.js +65 -0
  42. package/dist/internal/waiver-grant.d.ts +62 -0
  43. package/dist/internal/waiver-grant.js +294 -0
  44. package/dist/run-persistence.d.ts +70 -0
  45. package/dist/run-persistence.js +215 -3
  46. package/dist/runs.d.ts +1 -1
  47. package/dist/runs.js +1 -1
  48. package/dist/runtime/run-hook.mjs +43 -0
  49. package/package.json +1 -1
@@ -11,13 +11,33 @@ import { runRewind } from "./advance-stage/rewind.js";
11
11
  import { runVerifyFlowStateDiff, runVerifyCurrentState } from "./advance-stage/verify.js";
12
12
  import { runHookCommand } from "./advance-stage/hook.js";
13
13
  import { parseAdvanceStageArgs, parseCancelRunArgs, parseHookArgs, parseRewindArgs, parseStartFlowArgs, parseVerifyCurrentStateArgs, parseVerifyFlowStateDiffArgs } from "./advance-stage/parsers.js";
14
+ import { parseFlowStateRepairArgs, runFlowStateRepair } from "./flow-state-repair.js";
15
+ import { parseWaiverGrantArgs, runWaiverGrant } from "./waiver-grant.js";
16
+ import { FlowStateGuardMismatchError, verifyFlowStateGuard } from "../run-persistence.js";
17
+ /**
18
+ * Subcommands that mutate or consult flow-state.json via the CLI runtime.
19
+ * They all require the sha256 sidecar to match before continuing so a
20
+ * manual edit hard-blocks with exit code 2 (same contract as the inline
21
+ * hook checks).
22
+ */
23
+ const GUARD_ENFORCED_SUBCOMMANDS = new Set([
24
+ "advance-stage",
25
+ "start-flow",
26
+ "cancel-run",
27
+ "rewind",
28
+ "verify-flow-state-diff",
29
+ "verify-current-state"
30
+ ]);
14
31
  export async function runInternalCommand(projectRoot, argv, io) {
15
32
  const [subcommand, ...tokens] = argv;
16
33
  if (!subcommand) {
17
- 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\n");
34
+ 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");
18
35
  return 1;
19
36
  }
20
37
  try {
38
+ if (GUARD_ENFORCED_SUBCOMMANDS.has(subcommand)) {
39
+ await verifyFlowStateGuard(projectRoot);
40
+ }
21
41
  if (subcommand === "advance-stage") {
22
42
  return await runAdvanceStage(projectRoot, parseAdvanceStageArgs(tokens), io);
23
43
  }
@@ -57,10 +77,20 @@ export async function runInternalCommand(projectRoot, argv, io) {
57
77
  if (subcommand === "hook") {
58
78
  return await runHookCommand(projectRoot, parseHookArgs(tokens), io);
59
79
  }
60
- 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\n`);
80
+ if (subcommand === "flow-state-repair") {
81
+ return await runFlowStateRepair(projectRoot, parseFlowStateRepairArgs(tokens), io);
82
+ }
83
+ if (subcommand === "waiver-grant") {
84
+ return await runWaiverGrant(projectRoot, parseWaiverGrantArgs(tokens), io);
85
+ }
86
+ 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`);
61
87
  return 1;
62
88
  }
63
89
  catch (err) {
90
+ if (err instanceof FlowStateGuardMismatchError) {
91
+ io.stderr.write(`cclaw internal ${subcommand}: ${err.message}\n`);
92
+ return 2;
93
+ }
64
94
  io.stderr.write(`cclaw internal ${subcommand} failed: ${err instanceof Error ? err.message : String(err)}\n`);
65
95
  return 1;
66
96
  }
@@ -0,0 +1,13 @@
1
+ import type { Writable } from "node:stream";
2
+ interface InternalIo {
3
+ stdout: Writable;
4
+ stderr: Writable;
5
+ }
6
+ export interface FlowStateRepairArgs {
7
+ reason: string;
8
+ json: boolean;
9
+ quiet: boolean;
10
+ }
11
+ export declare function parseFlowStateRepairArgs(tokens: string[]): FlowStateRepairArgs;
12
+ export declare function runFlowStateRepair(projectRoot: string, args: FlowStateRepairArgs, io: InternalIo): Promise<number>;
13
+ export {};
@@ -0,0 +1,65 @@
1
+ import path from "node:path";
2
+ import { RUNTIME_ROOT } from "../constants.js";
3
+ import { repairFlowStateGuard } from "../run-persistence.js";
4
+ export function parseFlowStateRepairArgs(tokens) {
5
+ let reason;
6
+ let json = false;
7
+ let quiet = false;
8
+ for (let i = 0; i < tokens.length; i += 1) {
9
+ const token = tokens[i];
10
+ const nextToken = tokens[i + 1];
11
+ if (token === "--json") {
12
+ json = true;
13
+ continue;
14
+ }
15
+ if (token === "--quiet") {
16
+ quiet = true;
17
+ continue;
18
+ }
19
+ if (token === "--reason") {
20
+ if (!nextToken || nextToken.startsWith("--")) {
21
+ throw new Error("--reason requires a short slug value.");
22
+ }
23
+ reason = nextToken.trim();
24
+ i += 1;
25
+ continue;
26
+ }
27
+ if (token.startsWith("--reason=")) {
28
+ reason = token.slice("--reason=".length).trim();
29
+ continue;
30
+ }
31
+ throw new Error(`Unknown flag for internal flow-state-repair: ${token}`);
32
+ }
33
+ if (!reason || reason.length === 0) {
34
+ throw new Error("internal flow-state-repair requires --reason=<slug> (e.g. --reason=manual_edit_recovery).");
35
+ }
36
+ return { reason, json, quiet };
37
+ }
38
+ export async function runFlowStateRepair(projectRoot, args, io) {
39
+ const result = await repairFlowStateGuard(projectRoot, args.reason);
40
+ const logRel = path.relative(projectRoot, result.repairLogPath).replace(/\\/gu, "/");
41
+ const guardRel = path.relative(projectRoot, result.guardPath).replace(/\\/gu, "/");
42
+ if (args.json) {
43
+ io.stdout.write(`${JSON.stringify({
44
+ ok: true,
45
+ command: "flow-state-repair",
46
+ reason: args.reason,
47
+ sidecar: result.sidecar,
48
+ guardPath: guardRel,
49
+ repairLogPath: logRel,
50
+ runtimeRoot: RUNTIME_ROOT
51
+ })}\n`);
52
+ return 0;
53
+ }
54
+ if (!args.quiet) {
55
+ io.stdout.write(`${JSON.stringify({
56
+ ok: true,
57
+ command: "flow-state-repair",
58
+ reason: args.reason,
59
+ sidecar: result.sidecar,
60
+ guardPath: guardRel,
61
+ repairLogPath: logRel
62
+ }, null, 2)}\n`);
63
+ }
64
+ return 0;
65
+ }
@@ -0,0 +1,62 @@
1
+ import { type FlowStage } from "../types.js";
2
+ import type { Writable } from "node:stream";
3
+ interface InternalIo {
4
+ stdout: Writable;
5
+ stderr: Writable;
6
+ }
7
+ export declare const WAIVER_TOKEN_DEFAULT_TTL_MINUTES = 30;
8
+ export declare const WAIVER_TOKEN_MAX_TTL_MINUTES = 120;
9
+ export declare const WAIVER_REASON_PATTERN: RegExp;
10
+ export interface WaiverRecord {
11
+ token: string;
12
+ stage: FlowStage;
13
+ reason: string;
14
+ issuedAt: string;
15
+ expiresAt: string;
16
+ consumedAt: string | null;
17
+ issuerSubsystem: string;
18
+ consumedBy?: string;
19
+ }
20
+ export interface WaiverLedger {
21
+ schemaVersion: number;
22
+ pending: WaiverRecord[];
23
+ consumed: WaiverRecord[];
24
+ }
25
+ export interface IssueWaiverTokenOptions {
26
+ stage: FlowStage;
27
+ reason: string;
28
+ expiresInMinutes?: number;
29
+ issuerSubsystem?: string;
30
+ now?: Date;
31
+ }
32
+ export interface ConsumeWaiverOptions {
33
+ stage: FlowStage;
34
+ token: string;
35
+ consumedBy?: string;
36
+ now?: Date;
37
+ }
38
+ export declare function formatWaiverToken(stage: FlowStage, fingerprint: string, expiresAt: Date): string;
39
+ export declare function issueWaiverToken(projectRoot: string, options: IssueWaiverTokenOptions): Promise<WaiverRecord>;
40
+ export type ConsumeWaiverFailureReason = "not-found" | "wrong-stage" | "expired" | "already-consumed";
41
+ export interface ConsumeWaiverSuccess {
42
+ ok: true;
43
+ record: WaiverRecord;
44
+ }
45
+ export interface ConsumeWaiverFailure {
46
+ ok: false;
47
+ reason: ConsumeWaiverFailureReason;
48
+ record?: WaiverRecord;
49
+ detail: string;
50
+ }
51
+ export type ConsumeWaiverResult = ConsumeWaiverSuccess | ConsumeWaiverFailure;
52
+ export declare function consumeWaiverToken(projectRoot: string, options: ConsumeWaiverOptions): Promise<ConsumeWaiverResult>;
53
+ export interface WaiverGrantArgs {
54
+ stage: FlowStage;
55
+ reason: string;
56
+ ttlMinutes: number;
57
+ json: boolean;
58
+ quiet: boolean;
59
+ }
60
+ export declare function parseWaiverGrantArgs(tokens: string[]): WaiverGrantArgs;
61
+ export declare function runWaiverGrant(projectRoot: string, args: WaiverGrantArgs, io: InternalIo): Promise<number>;
62
+ export {};
@@ -0,0 +1,294 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { RUNTIME_ROOT } from "../constants.js";
5
+ import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "../fs-utils.js";
6
+ import { FLOW_STAGES } from "../types.js";
7
+ /**
8
+ * Tokens issued by `cclaw internal waiver-grant` live under the runtime
9
+ * root. The ledger also tracks `consumed[]` entries so consumption is
10
+ * traceable and one-shot.
11
+ */
12
+ const WAIVER_LEDGER_REL_PATH = `${RUNTIME_ROOT}/.waivers.json`;
13
+ const WAIVER_LEDGER_LOCK_REL_PATH = `${RUNTIME_ROOT}/.waivers.json.lock`;
14
+ export const WAIVER_TOKEN_DEFAULT_TTL_MINUTES = 30;
15
+ export const WAIVER_TOKEN_MAX_TTL_MINUTES = 120;
16
+ export const WAIVER_REASON_PATTERN = /^[a-z][a-z0-9_-]{2,}$/u;
17
+ const WAIVER_TOKEN_PREFIX = "WV";
18
+ const WAIVER_LEDGER_SCHEMA_VERSION = 1;
19
+ function waiverLedgerPath(projectRoot) {
20
+ return path.join(projectRoot, WAIVER_LEDGER_REL_PATH);
21
+ }
22
+ function waiverLedgerLockPath(projectRoot) {
23
+ return path.join(projectRoot, WAIVER_LEDGER_LOCK_REL_PATH);
24
+ }
25
+ function sanitizeWaiverRecord(value) {
26
+ if (!value || typeof value !== "object" || Array.isArray(value))
27
+ return null;
28
+ const row = value;
29
+ const token = typeof row.token === "string" ? row.token : "";
30
+ const stage = row.stage;
31
+ const reason = typeof row.reason === "string" ? row.reason : "";
32
+ const issuedAt = typeof row.issuedAt === "string" ? row.issuedAt : "";
33
+ const expiresAt = typeof row.expiresAt === "string" ? row.expiresAt : "";
34
+ const consumedAt = typeof row.consumedAt === "string" ? row.consumedAt : null;
35
+ const issuerSubsystem = typeof row.issuerSubsystem === "string" ? row.issuerSubsystem : "";
36
+ const consumedBy = typeof row.consumedBy === "string" ? row.consumedBy : undefined;
37
+ if (token.length === 0 ||
38
+ typeof stage !== "string" ||
39
+ !FLOW_STAGES.includes(stage) ||
40
+ reason.length === 0 ||
41
+ issuedAt.length === 0 ||
42
+ expiresAt.length === 0 ||
43
+ issuerSubsystem.length === 0) {
44
+ return null;
45
+ }
46
+ return {
47
+ token,
48
+ stage: stage,
49
+ reason,
50
+ issuedAt,
51
+ expiresAt,
52
+ consumedAt,
53
+ issuerSubsystem,
54
+ ...(consumedBy ? { consumedBy } : {})
55
+ };
56
+ }
57
+ async function readWaiverLedger(projectRoot) {
58
+ const statePath = waiverLedgerPath(projectRoot);
59
+ if (!(await exists(statePath))) {
60
+ return { schemaVersion: WAIVER_LEDGER_SCHEMA_VERSION, pending: [], consumed: [] };
61
+ }
62
+ let raw;
63
+ try {
64
+ raw = await fs.readFile(statePath, "utf8");
65
+ }
66
+ catch {
67
+ return { schemaVersion: WAIVER_LEDGER_SCHEMA_VERSION, pending: [], consumed: [] };
68
+ }
69
+ let parsed;
70
+ try {
71
+ parsed = JSON.parse(raw);
72
+ }
73
+ catch {
74
+ return { schemaVersion: WAIVER_LEDGER_SCHEMA_VERSION, pending: [], consumed: [] };
75
+ }
76
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
77
+ return { schemaVersion: WAIVER_LEDGER_SCHEMA_VERSION, pending: [], consumed: [] };
78
+ }
79
+ const typed = parsed;
80
+ const pending = Array.isArray(typed.pending)
81
+ ? typed.pending
82
+ .map((item) => sanitizeWaiverRecord(item))
83
+ .filter((item) => item !== null)
84
+ : [];
85
+ const consumed = Array.isArray(typed.consumed)
86
+ ? typed.consumed
87
+ .map((item) => sanitizeWaiverRecord(item))
88
+ .filter((item) => item !== null)
89
+ : [];
90
+ return { schemaVersion: WAIVER_LEDGER_SCHEMA_VERSION, pending, consumed };
91
+ }
92
+ async function writeWaiverLedger(projectRoot, ledger) {
93
+ const next = {
94
+ schemaVersion: WAIVER_LEDGER_SCHEMA_VERSION,
95
+ pending: ledger.pending,
96
+ consumed: ledger.consumed
97
+ };
98
+ await writeFileSafe(waiverLedgerPath(projectRoot), `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
99
+ }
100
+ function formatExpiresSlug(expiresAt) {
101
+ // Minute-precision slug for the token: e.g. `20260502T220500Z`
102
+ return expiresAt.toISOString().replace(/[-:]/gu, "").replace(/\..+$/u, "").concat("Z");
103
+ }
104
+ function minuteFingerprint(stage, reason, issuedAt) {
105
+ const payload = `${stage}|${reason}|${issuedAt.toISOString()}|${Math.random().toString(16).slice(2, 12)}`;
106
+ return createHash("sha256").update(payload, "utf8").digest("hex").slice(0, 8);
107
+ }
108
+ export function formatWaiverToken(stage, fingerprint, expiresAt) {
109
+ return `${WAIVER_TOKEN_PREFIX}-${stage}-${fingerprint}-${formatExpiresSlug(expiresAt)}`;
110
+ }
111
+ export async function issueWaiverToken(projectRoot, options) {
112
+ if (!FLOW_STAGES.includes(options.stage)) {
113
+ throw new Error(`waiver-grant: --stage must be one of ${FLOW_STAGES.join(", ")}.`);
114
+ }
115
+ const reason = options.reason.trim();
116
+ if (reason.length === 0) {
117
+ throw new Error("waiver-grant: --reason is required.");
118
+ }
119
+ if (!WAIVER_REASON_PATTERN.test(reason)) {
120
+ throw new Error("waiver-grant: --reason must match /^[a-z][a-z0-9_-]{2,}$/ (short lowercase slug, e.g. architect_unavailable).");
121
+ }
122
+ const ttlRaw = typeof options.expiresInMinutes === "number" && Number.isFinite(options.expiresInMinutes)
123
+ ? Math.floor(options.expiresInMinutes)
124
+ : WAIVER_TOKEN_DEFAULT_TTL_MINUTES;
125
+ if (ttlRaw < 1) {
126
+ throw new Error("waiver-grant: --ttl must be >= 1 minute.");
127
+ }
128
+ if (ttlRaw > WAIVER_TOKEN_MAX_TTL_MINUTES) {
129
+ throw new Error(`waiver-grant: --ttl must be <= ${WAIVER_TOKEN_MAX_TTL_MINUTES} minutes.`);
130
+ }
131
+ const issuedAtDate = options.now ?? new Date();
132
+ const expiresAtDate = new Date(issuedAtDate.getTime() + ttlRaw * 60 * 1000);
133
+ const fingerprint = minuteFingerprint(options.stage, reason, issuedAtDate);
134
+ const token = formatWaiverToken(options.stage, fingerprint, expiresAtDate);
135
+ const record = {
136
+ token,
137
+ stage: options.stage,
138
+ reason,
139
+ issuedAt: issuedAtDate.toISOString(),
140
+ expiresAt: expiresAtDate.toISOString(),
141
+ consumedAt: null,
142
+ issuerSubsystem: options.issuerSubsystem?.trim() || "cli"
143
+ };
144
+ await ensureDir(path.dirname(waiverLedgerPath(projectRoot)));
145
+ await withDirectoryLock(waiverLedgerLockPath(projectRoot), async () => {
146
+ const ledger = await readWaiverLedger(projectRoot);
147
+ ledger.pending.push(record);
148
+ await writeWaiverLedger(projectRoot, ledger);
149
+ });
150
+ return record;
151
+ }
152
+ export async function consumeWaiverToken(projectRoot, options) {
153
+ const token = options.token.trim();
154
+ if (token.length === 0) {
155
+ return {
156
+ ok: false,
157
+ reason: "not-found",
158
+ detail: "waiver token is required"
159
+ };
160
+ }
161
+ const now = (options.now ?? new Date()).getTime();
162
+ return withDirectoryLock(waiverLedgerLockPath(projectRoot), async () => {
163
+ const ledger = await readWaiverLedger(projectRoot);
164
+ const pendingIdx = ledger.pending.findIndex((entry) => entry.token === token);
165
+ const consumedMatch = ledger.consumed.find((entry) => entry.token === token);
166
+ if (pendingIdx < 0) {
167
+ if (consumedMatch) {
168
+ return {
169
+ ok: false,
170
+ reason: "already-consumed",
171
+ record: consumedMatch,
172
+ detail: `waiver token ${token} was already consumed at ${consumedMatch.consumedAt ?? "unknown time"}`
173
+ };
174
+ }
175
+ return {
176
+ ok: false,
177
+ reason: "not-found",
178
+ detail: `no pending waiver token "${token}" found in ${WAIVER_LEDGER_REL_PATH}`
179
+ };
180
+ }
181
+ const record = ledger.pending[pendingIdx];
182
+ if (record.stage !== options.stage) {
183
+ return {
184
+ ok: false,
185
+ reason: "wrong-stage",
186
+ record,
187
+ detail: `waiver token ${token} was issued for stage "${record.stage}", not "${options.stage}"`
188
+ };
189
+ }
190
+ const expiresAt = Date.parse(record.expiresAt);
191
+ if (Number.isFinite(expiresAt) && expiresAt < now) {
192
+ return {
193
+ ok: false,
194
+ reason: "expired",
195
+ record,
196
+ detail: `waiver token ${token} expired at ${record.expiresAt}`
197
+ };
198
+ }
199
+ const consumedAtIso = (options.now ?? new Date()).toISOString();
200
+ const consumedRecord = {
201
+ ...record,
202
+ consumedAt: consumedAtIso,
203
+ ...(options.consumedBy ? { consumedBy: options.consumedBy } : {})
204
+ };
205
+ ledger.pending.splice(pendingIdx, 1);
206
+ ledger.consumed.push(consumedRecord);
207
+ await writeWaiverLedger(projectRoot, ledger);
208
+ return { ok: true, record: consumedRecord };
209
+ });
210
+ }
211
+ export function parseWaiverGrantArgs(tokens) {
212
+ let stage;
213
+ let reason;
214
+ let ttlMinutes = WAIVER_TOKEN_DEFAULT_TTL_MINUTES;
215
+ let json = false;
216
+ let quiet = false;
217
+ for (let i = 0; i < tokens.length; i += 1) {
218
+ const token = tokens[i];
219
+ const nextToken = tokens[i + 1];
220
+ const readValue = (flag) => {
221
+ if (token.startsWith(`${flag}=`))
222
+ return token.slice(flag.length + 1);
223
+ if (token === flag && nextToken && !nextToken.startsWith("--")) {
224
+ i += 1;
225
+ return nextToken;
226
+ }
227
+ throw new Error(`${flag} requires a value.`);
228
+ };
229
+ if (token === "--json") {
230
+ json = true;
231
+ continue;
232
+ }
233
+ if (token === "--quiet") {
234
+ quiet = true;
235
+ continue;
236
+ }
237
+ if (token === "--stage" || token.startsWith("--stage=")) {
238
+ const raw = readValue("--stage").trim();
239
+ if (!FLOW_STAGES.includes(raw)) {
240
+ throw new Error(`waiver-grant: --stage must be one of ${FLOW_STAGES.join(", ")}.`);
241
+ }
242
+ stage = raw;
243
+ continue;
244
+ }
245
+ if (token === "--reason" || token.startsWith("--reason=")) {
246
+ reason = readValue("--reason").trim();
247
+ continue;
248
+ }
249
+ if (token === "--ttl" || token.startsWith("--ttl=")) {
250
+ const raw = readValue("--ttl").trim();
251
+ if (!/^[0-9]+$/u.test(raw)) {
252
+ throw new Error("waiver-grant: --ttl must be an integer number of minutes.");
253
+ }
254
+ ttlMinutes = Number(raw);
255
+ continue;
256
+ }
257
+ throw new Error(`Unknown flag for internal waiver-grant: ${token}`);
258
+ }
259
+ if (!stage) {
260
+ throw new Error(`internal waiver-grant requires --stage=<${FLOW_STAGES.join("|")}>.`);
261
+ }
262
+ if (!reason) {
263
+ throw new Error(`internal waiver-grant requires --reason=<short-slug> (e.g. architect_unavailable).`);
264
+ }
265
+ return { stage, reason, ttlMinutes, json, quiet };
266
+ }
267
+ export async function runWaiverGrant(projectRoot, args, io) {
268
+ const record = await issueWaiverToken(projectRoot, {
269
+ stage: args.stage,
270
+ reason: args.reason,
271
+ expiresInMinutes: args.ttlMinutes,
272
+ issuerSubsystem: "cli"
273
+ });
274
+ if (args.json) {
275
+ io.stdout.write(`${JSON.stringify({
276
+ ok: true,
277
+ command: "waiver-grant",
278
+ token: record.token,
279
+ stage: record.stage,
280
+ reason: record.reason,
281
+ issuedAt: record.issuedAt,
282
+ expiresAt: record.expiresAt,
283
+ ttlMinutes: args.ttlMinutes,
284
+ consumption: `cclaw-cli internal advance-stage ${record.stage} --accept-proactive-waiver=${record.token} --accept-proactive-waiver-reason="${record.reason}"`
285
+ })}\n`);
286
+ return 0;
287
+ }
288
+ io.stdout.write(`${record.token}\n`);
289
+ if (!args.quiet) {
290
+ io.stdout.write(`Waiver token issued for stage="${record.stage}" reason="${record.reason}" expires=${record.expiresAt}.\n`);
291
+ io.stdout.write(`Consume with: node ${RUNTIME_ROOT}/hooks/stage-complete.mjs ${record.stage} --accept-proactive-waiver=${record.token} --accept-proactive-waiver-reason="${record.reason}"\n`);
292
+ }
293
+ return 0;
294
+ }
@@ -18,6 +18,13 @@ export interface WriteFlowStateOptions {
18
18
  * flow-state reset inside one atomic lock window.
19
19
  */
20
20
  skipLock?: boolean;
21
+ /**
22
+ * Free-form writer identifier persisted in the `.flow-state.guard.json`
23
+ * sidecar. Helps operators trace which subsystem wrote a given state
24
+ * (e.g. `advance-stage`, `start-flow`, `run-archive`). Defaults to
25
+ * `cclaw-cli` when omitted.
26
+ */
27
+ writerSubsystem?: string;
21
28
  }
22
29
  export interface ReadFlowStateOptions {
23
30
  /**
@@ -26,13 +33,76 @@ export interface ReadFlowStateOptions {
26
33
  */
27
34
  repairFeatureSystem?: boolean;
28
35
  }
36
+ export interface FlowStateGuardSidecar {
37
+ sha256: string;
38
+ writtenAt: string;
39
+ writerSubsystem: string;
40
+ runId: string;
41
+ }
42
+ export interface FlowStateGuardMismatchDetails {
43
+ expectedSha: string;
44
+ actualSha: string;
45
+ lastWriter: string;
46
+ writtenAt: string;
47
+ runId: string;
48
+ statePath: string;
49
+ guardPath: string;
50
+ repairCommand: string;
51
+ }
52
+ export declare class FlowStateGuardMismatchError extends Error {
53
+ readonly expectedSha: string;
54
+ readonly actualSha: string;
55
+ readonly lastWriter: string;
56
+ readonly writtenAt: string;
57
+ readonly runId: string;
58
+ readonly statePath: string;
59
+ readonly guardPath: string;
60
+ readonly repairCommand: string;
61
+ constructor(details: FlowStateGuardMismatchDetails);
62
+ }
29
63
  export declare class CorruptFlowStateError extends Error {
30
64
  readonly statePath: string;
31
65
  readonly quarantinedPath: string;
32
66
  constructor(statePath: string, quarantinedPath: string, cause: unknown);
33
67
  }
68
+ /**
69
+ * Verify the on-disk flow-state against the sha256 sidecar. Throws
70
+ * `FlowStateGuardMismatchError` when manual editing is detected. Safe to
71
+ * call on projects that have never written a sidecar: a missing sidecar is
72
+ * treated as "legacy runtime" and the check silently succeeds.
73
+ */
74
+ export declare function verifyFlowStateGuard(projectRoot: string): Promise<void>;
34
75
  export declare function readFlowState(projectRoot: string, options?: ReadFlowStateOptions): Promise<FlowState>;
76
+ /**
77
+ * Guarded read wrapper used by runtime hook scripts and the repair CLI.
78
+ * Unlike `readFlowState`, it enforces the sha256 sidecar before returning:
79
+ * a manual edit to flow-state.json fails fast with
80
+ * `FlowStateGuardMismatchError`.
81
+ */
82
+ export declare function readFlowStateGuarded(projectRoot: string, options?: ReadFlowStateOptions): Promise<FlowState>;
35
83
  export declare function writeFlowState(projectRoot: string, state: FlowState, options?: WriteFlowStateOptions): Promise<void>;
84
+ /**
85
+ * Named entry point for the write-guard workstream. Equivalent to
86
+ * `writeFlowState`: the write always produces the sha256 sidecar via
87
+ * the internal implementation so every existing writer inherits the
88
+ * guard without rewriting callsites.
89
+ */
90
+ export declare function writeFlowStateGuarded(projectRoot: string, state: FlowState, options?: WriteFlowStateOptions): Promise<void>;
91
+ export interface FlowStateRepairResult {
92
+ sidecar: FlowStateGuardSidecar;
93
+ repairLogPath: string;
94
+ guardPath: string;
95
+ }
96
+ /**
97
+ * Recompute the write-guard sidecar from the current on-disk flow-state
98
+ * contents and append an audit entry to `.cclaw/.flow-state-repair.log`.
99
+ * The reason is required so no repair happens without an operator-visible
100
+ * rationale. Intended to be called only from the explicit
101
+ * `cclaw-cli internal flow-state-repair` subcommand.
102
+ */
103
+ export declare function repairFlowStateGuard(projectRoot: string, reason: string): Promise<FlowStateRepairResult>;
104
+ export declare function flowStateGuardSidecarPathFor(projectRoot: string): string;
105
+ export declare function flowStateRepairLogPathFor(projectRoot: string): string;
36
106
  /**
37
107
  * Exposed path helper so callers that need to serialize a multi-step
38
108
  * state operation with flow-state writes (e.g. run archival) can