cclaw-cli 6.14.1 → 6.14.2

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.
@@ -37,7 +37,8 @@ export const TDD = {
37
37
  },
38
38
  executionModel: {
39
39
  checklist: [
40
- "**Stream-style wave dispatch (v6.14.0):** Before routing, read the Parallel Execution Plan (managed block in the track planning artifact) and `<artifacts-dir>/wave-plans/`. Per-lane stream: each lane runs RED→GREEN→REFACTOR independently as soon as its `dependsOn` closes no global RED checkpoint between Phase A and Phase B. The linter enforces RED-before-GREEN per slice via `tdd_slice_red_completed_before_green`; cross-lane interleaving is allowed. **Legacy `global-red` mode** is preserved for projects with `legacyContinuation: true` and any project that explicitly sets `flow-state.json::tddCheckpointMode: \"global-red\"` (rule `tdd_red_checkpoint_violation` still fires there). Multi-ready waves still get one AskQuestion (launch wave vs single-slice); then per-lane GREEN+DOC dispatch with worktree-first flags. Integration-overseer fires only on cross-slice trigger (see `integrationCheckRequired()` heuristic). Resume partial waves by parallelizing remaining members only (see top-of-skill `## Wave Batch Mode`).",
40
+ "**Wave dispatch — discovery hardened (v6.14.2):** Before routing, your FIRST tool call after entering TDD MUST be `node .cclaw/cli.mjs internal wave-status --json` (or the harness equivalent `npx cclaw-cli internal wave-status --json`). Do NOT page through `05-plan.md` to find the managed block the helper reads the managed `<!-- parallel-exec-managed-start -->` block deterministically and prints `{ waves, nextDispatch.readyToDispatch, warnings }`. Open `05-plan.md` only AFTER `wave-status` names a slice that needs context. Multi-ready waves: one AskQuestion (launch wave vs single-slice); then RED checkpoint (when `tddCheckpointMode: \"global-red\"`) or per-lane stream (when `tddCheckpointMode: \"per-slice\"`, the v6.14+ default), parallel GREEN+DOC with worktree-first flags, per-lane REFACTOR. Resume partial waves by parallelizing remaining members only (see top-of-skill `## Wave Batch Mode`).",
41
+ "**Stream-style wave dispatch (v6.14.0):** After `wave-status` resolves the next dispatch, route accordingly. Per-lane stream: each lane runs RED→GREEN→REFACTOR independently as soon as its `dependsOn` closes — no global RED checkpoint between Phase A and Phase B. The linter enforces RED-before-GREEN per slice via `tdd_slice_red_completed_before_green`; cross-lane interleaving is allowed. **Legacy `global-red` mode** is preserved for projects with `legacyContinuation: true` and any project that explicitly sets `flow-state.json::tddCheckpointMode: \"global-red\"` (rule `tdd_red_checkpoint_violation` still fires there). Multi-ready waves still get one AskQuestion (launch wave vs single-slice); then per-lane GREEN+DOC dispatch with worktree-first flags. Integration-overseer fires only on cross-slice trigger (see `integrationCheckRequired()` heuristic).",
41
42
  "**Controller dispatch ordering (v6.14.1 — record BEFORE dispatch).** For every `Task` subagent the controller spawns, record `scheduled` then `launched` ledger events via `node .cclaw/hooks/delegation-record.mjs --status=scheduled ...` and `--status=launched ...` **BEFORE** the `Task(...)` call (one message: ledger writes first, then the matching `Task` calls). Workers self-record `acknowledged` and `completed`; controller back-fill is reserved for `--repair` recovery only. Pass `--span-id`, `--lane-id`, `--claim-token`, `--lease-until` through to the worker so its own helper invocations reuse them.",
42
43
  "**Wave closure — integration-overseer decision (v6.14.1).** When every dispatched lane has a `phase=green status=completed` event AND per-lane REFACTOR coverage is satisfied (separate phase event OR `refactorOutcome` folded into GREEN), call `integrationCheckRequired(events, fanInAudits)` from `src/delegation.ts`. (1) `required: true` → dispatch `integration-overseer` as before. (2) `required: false` → emit the audit row via `node .cclaw/hooks/delegation-record.mjs --audit-kind=cclaw_integration_overseer_skipped --audit-reason=\"<reasons>\" --slice-ids=\"S-1,S-2\" --json` and SKIP the dispatch. Linter advisory `tdd_integration_overseer_skipped_audit_missing` flags a wave that closes without either an overseer dispatch or this audit row.",
43
44
  "**Inline DOC opt-in (v6.14.1 — single-slice non-deep waves).** Default remains parallel `slice-documenter --phase doc` dispatched alongside `slice-implementer --phase green`. For single-slice waves where `flow-state.json::discoveryMode != \"deep\"`, the controller MAY skip the parallel documenter and instead invoke `slice-implementer --finalize-doc --slice S-<id> --paths <artifacts-dir>/tdd-slices/S-<id>.md` synchronously after GREEN. Multi-slice waves and any `discoveryMode=deep` run keep parallel slice-documenter mandatory.",
@@ -114,7 +115,7 @@ export const TDD = {
114
115
  "Relevant existing test files, helpers, fixtures, and exact commands identified before RED.",
115
116
  "Callbacks, state transitions, interfaces, schemas, and contracts checked for impact before implementation.",
116
117
  "Execution posture and vertical-slice RED/GREEN/REFACTOR checkpoint plan recorded, including commit boundaries when the repo workflow supports them.",
117
- "RED observability: a `phase=red` event in `delegation-events.jsonl` for each slice with non-empty evidenceRefs (test path, span ref, or pasted-output pointer). Slices created **before the v6.12.0 cutover marker** in `flow-state.json::tddCutoverSliceId` may retain legacy `## Watched-RED Proof` / `## RED Evidence` markdown tables; slices created **after the cutover marker** MUST use phase events + slice-documenter doc, and legacy table writes are surfaced by the advisory `tdd_legacy_section_writes_after_cutover` rule.",
118
+ "RED observability: a `phase=red` event in `delegation-events.jsonl` for each slice with non-empty evidenceRefs (test path, span ref, or pasted-output pointer). **`flow-state.json::tddCutoverSliceId` is a HISTORICAL boundary set by `cclaw-cli sync` at upgrade time; it is NOT a pointer to the active slice and the controller MUST NOT dispatch new work for that slice id on its basis.** Slices created at or before the cutover marker may retain legacy `## Watched-RED Proof` / `## RED Evidence` markdown tables; slices created after the cutover marker MUST use phase events + slice-documenter doc, and legacy table writes are surfaced by the advisory `tdd_legacy_section_writes_after_cutover` rule. To find the ACTIVE slice, run `cclaw-cli internal wave-status --json` (Fix 1, v6.14.2) — never derive it from `tddCutoverSliceId`.",
118
119
  "GREEN observability: a `phase=green` event in `delegation-events.jsonl` per slice whose `completedTs` >= the matching `phase=red` `completedTs`, authored by `slice-implementer` (linter rule `tdd_slice_implementer_missing` blocks the gate otherwise), and whose evidenceRefs name the failing-now-passing test. Pre-cutover slices may keep legacy `## GREEN Evidence` markdown.",
119
120
  "REFACTOR observability: per slice, a `phase=refactor` event OR a `phase=refactor-deferred` event whose evidenceRefs / refactor rationale captures why refactor was deferred.",
120
121
  "Per slice, a `phase=doc` event from `slice-documenter` whose evidenceRefs name `<artifacts-dir>/tdd-slices/S-<id>.md`. Mandatory regardless of `discoveryMode` (v6.12.0 Phase R). Linter rule `tdd_slice_documenter_missing` blocks the gate when missing.",
@@ -136,6 +136,29 @@ export interface FlowState {
136
136
  * sync hasn't visited yet.
137
137
  */
138
138
  tddCutoverSliceId?: string;
139
+ /**
140
+ * v6.14.2 — boundary slice id at which worktree-first protocol began
141
+ * applying. `cclaw-cli sync` auto-stamps this when
142
+ * `legacyContinuation: true` AND `worktreeExecutionMode: "worktree-first"`
143
+ * AND the value is not already set.
144
+ *
145
+ * Detection rule (v6.14.2): the highest `S-N` among slices with at
146
+ * least one completed `slice-implementer` row in the active run that
147
+ * carries NONE of the worktree-first metadata fields (`claimToken`,
148
+ * `ownerLaneId`, `leasedUntil`). When no such slice exists, sync
149
+ * falls back to `tddCutoverSliceId` so legacy v6.12 cutover marks
150
+ * still confer the exemption.
151
+ *
152
+ * Effect: closed slices whose numeric id is `<= tddWorktreeCutoverSliceId`
153
+ * AND whose `slice-implementer` rows in the active run lack ALL
154
+ * three worktree fields are exempt from `tdd_slice_lane_metadata_missing`,
155
+ * `tdd_slice_claim_token_missing`, and `tdd_lease_expired_unreclaimed`.
156
+ *
157
+ * One-shot: subsequent sync runs leave the value untouched. Operators
158
+ * may pin it earlier/later by direct edit + `cclaw-cli internal
159
+ * flow-state-repair --reason=<slug>`.
160
+ */
161
+ tddWorktreeCutoverSliceId?: string;
139
162
  /**
140
163
  * v6.13.0 — when `worktree-first` (default for newly initialized runs),
141
164
  * slice-implementer work happens in isolated git worktrees with explicit
@@ -185,6 +208,20 @@ export interface FlowState {
185
208
  * Omitted on legacy state files (treated as `"always"`).
186
209
  */
187
210
  integrationOverseerMode?: "conditional" | "always";
211
+ /**
212
+ * v6.14.2 — minimum elapsed milliseconds between `acknowledged` and
213
+ * `completed` for a `slice-implementer --phase green` row. The hook
214
+ * helper rejects fast-greens (`completedTs - ackTs < this`) with
215
+ * `green_evidence_too_fresh` unless the dispatch carries
216
+ * `--allow-fast-green --green-mode=observational`.
217
+ *
218
+ * Default 4000ms when omitted (see `effectiveTddGreenMinElapsedMs`).
219
+ * Operators tuning the floor for very fast suites may set it lower
220
+ * (e.g. `1500`) or set it to `0` to disable the check entirely while
221
+ * keeping the other Fix 4 contracts (RED test name match, passing
222
+ * assertion line) active.
223
+ */
224
+ tddGreenMinElapsedMs?: number;
188
225
  }
189
226
  /**
190
227
  * Effective worktree mode: legacy state files without the field keep
@@ -202,6 +239,15 @@ export declare function effectiveTddCheckpointMode(state: FlowState): "per-slice
202
239
  * the field default to `always` (matches v6.13 behavior).
203
240
  */
204
241
  export declare function effectiveIntegrationOverseerMode(state: FlowState): "conditional" | "always";
242
+ export declare const DEFAULT_TDD_GREEN_MIN_ELAPSED_MS = 4000;
243
+ /**
244
+ * v6.14.2 — effective minimum GREEN elapsed window in milliseconds.
245
+ * Returns the per-project override when present and finite; otherwise
246
+ * the documented 4000ms default. Negative values or NaN fall through
247
+ * to the default so a hand-edited `flow-state.json` cannot accidentally
248
+ * disable the check via `-1` or `"oops"`.
249
+ */
250
+ export declare function effectiveTddGreenMinElapsedMs(state: FlowState): number;
205
251
  export interface StageInteractionHint {
206
252
  skipQuestions?: boolean;
207
253
  sourceStage?: FlowStage;
@@ -69,6 +69,24 @@ export function effectiveTddCheckpointMode(state) {
69
69
  export function effectiveIntegrationOverseerMode(state) {
70
70
  return state.integrationOverseerMode === "conditional" ? "conditional" : "always";
71
71
  }
72
+ export const DEFAULT_TDD_GREEN_MIN_ELAPSED_MS = 4000;
73
+ /**
74
+ * v6.14.2 — effective minimum GREEN elapsed window in milliseconds.
75
+ * Returns the per-project override when present and finite; otherwise
76
+ * the documented 4000ms default. Negative values or NaN fall through
77
+ * to the default so a hand-edited `flow-state.json` cannot accidentally
78
+ * disable the check via `-1` or `"oops"`.
79
+ */
80
+ export function effectiveTddGreenMinElapsedMs(state) {
81
+ const raw = state.tddGreenMinElapsedMs;
82
+ if (typeof raw !== "number")
83
+ return DEFAULT_TDD_GREEN_MIN_ELAPSED_MS;
84
+ if (!Number.isFinite(raw))
85
+ return DEFAULT_TDD_GREEN_MIN_ELAPSED_MS;
86
+ if (raw < 0)
87
+ return DEFAULT_TDD_GREEN_MIN_ELAPSED_MS;
88
+ return Math.floor(raw);
89
+ }
72
90
  export function isFlowTrack(value) {
73
91
  return typeof value === "string" && FLOW_TRACKS.includes(value);
74
92
  }
package/dist/install.js CHANGED
@@ -1005,12 +1005,25 @@ async function applyPlanLegacyContinuationIfNeeded(projectRoot) {
1005
1005
  }
1006
1006
  /**
1007
1007
  * v6.14.0 — set stream-style defaults on `cclaw-cli sync` and print a
1008
- * one-line hint when defaults change. Strategy:
1008
+ * one-line hint when defaults change.
1009
1009
  *
1010
- * - When `legacyContinuation: true` and `tddCheckpointMode` is unset, force
1011
- * `tddCheckpointMode: "global-red"` (preserves hox wave protocol).
1012
- * - When `legacyContinuation: true` and `integrationOverseerMode` is unset,
1013
- * force `integrationOverseerMode: "always"` (preserves v6.13 behavior).
1010
+ * v6.14.2 update flip the legacyContinuation defaults from
1011
+ * `global-red`/`always` to `per-slice`/`conditional`. Rationale: hox-shape
1012
+ * projects ran into S-17 misroutes precisely because the default
1013
+ * preserved the v6.12 wave barrier even after the project itself had
1014
+ * moved to stream-mode. Existing flow-state values are NEVER overwritten
1015
+ * — operators who want to pin `global-red`/`always` may set them
1016
+ * explicitly via `cclaw-cli internal set-checkpoint-mode global-red` and
1017
+ * `set-integration-overseer-mode always`.
1018
+ *
1019
+ * Strategy:
1020
+ *
1021
+ * - When `legacyContinuation: true` and `tddCheckpointMode` is unset,
1022
+ * default to `tddCheckpointMode: "per-slice"` (v6.14.2 — was
1023
+ * `global-red` in v6.14.0/v6.14.1).
1024
+ * - When `legacyContinuation: true` and `integrationOverseerMode` is
1025
+ * unset, default to `integrationOverseerMode: "conditional"` (v6.14.2
1026
+ * — was `always` in v6.14.0/v6.14.1).
1014
1027
  * - When `legacyContinuation` is NOT true (new / standard projects) and
1015
1028
  * neither field is set, default to `tddCheckpointMode: "per-slice"`,
1016
1029
  * `integrationOverseerMode: "conditional"`. Also default
@@ -1053,12 +1066,12 @@ async function applyV614DefaultsIfNeeded(projectRoot) {
1053
1066
  const legacyContinuation = obj.legacyContinuation === true;
1054
1067
  if (legacyContinuation) {
1055
1068
  if (!tddCheckpointModeSet) {
1056
- updates.tddCheckpointMode = "global-red";
1057
- summary.push("tddCheckpointMode=global-red (legacyContinuation)");
1069
+ updates.tddCheckpointMode = "per-slice";
1070
+ summary.push("tddCheckpointMode=per-slice (legacyContinuation, v6.14.2 default flip)");
1058
1071
  }
1059
1072
  if (!integrationOverseerModeSet) {
1060
- updates.integrationOverseerMode = "always";
1061
- summary.push("integrationOverseerMode=always (legacyContinuation)");
1073
+ updates.integrationOverseerMode = "conditional";
1074
+ summary.push("integrationOverseerMode=conditional (legacyContinuation, v6.14.2 default flip)");
1062
1075
  }
1063
1076
  }
1064
1077
  else {
@@ -1085,7 +1098,124 @@ async function applyV614DefaultsIfNeeded(projectRoot) {
1085
1098
  catch {
1086
1099
  return null;
1087
1100
  }
1088
- return `v6.14.0 stream-style defaults applied: ${summary.join(", ")}. To opt out, edit .cclaw/state/flow-state.json directly or pin the legacy mode (tddCheckpointMode="global-red", integrationOverseerMode="always").`;
1101
+ return `v6.14.2 stream-style defaults applied: ${summary.join(", ")}. To opt out, run cclaw-cli internal set-checkpoint-mode global-red --reason="..." and/or cclaw-cli internal set-integration-overseer-mode always --reason="...".`;
1102
+ }
1103
+ /**
1104
+ * v6.14.2 — auto-stamp `tddWorktreeCutoverSliceId` for legacyContinuation
1105
+ * projects in worktree-first mode that haven't yet recorded a boundary.
1106
+ *
1107
+ * Detection ("any-metadata" rule): scan the active run's
1108
+ * `slice-implementer` rows. The boundary is the highest `S-N` whose
1109
+ * rows for the active run lack ALL of `claimToken`, `ownerLaneId`, and
1110
+ * `leasedUntil`. When no such slice exists (every slice carries at
1111
+ * least one worktree field), fall back to `tddCutoverSliceId` so the
1112
+ * v6.12 cutover marker still confers exemption.
1113
+ *
1114
+ * Idempotent: when the field is already set, returns null without
1115
+ * writing. Best-effort: read failures, missing ledger, or unparseable
1116
+ * rows all short-circuit silently — the existing flow-state.json is
1117
+ * never corrupted.
1118
+ *
1119
+ * Returns a one-line hint string (or `null` if nothing changed).
1120
+ */
1121
+ async function applyV6142WorktreeCutoverIfNeeded(projectRoot) {
1122
+ const flowStatePath = runtimePath(projectRoot, "state", "flow-state.json");
1123
+ let flowStateRaw;
1124
+ try {
1125
+ flowStateRaw = await fs.readFile(flowStatePath, "utf8");
1126
+ }
1127
+ catch {
1128
+ return null;
1129
+ }
1130
+ let parsed;
1131
+ try {
1132
+ parsed = JSON.parse(flowStateRaw);
1133
+ }
1134
+ catch {
1135
+ return null;
1136
+ }
1137
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1138
+ return null;
1139
+ }
1140
+ const obj = parsed;
1141
+ if (obj.legacyContinuation !== true)
1142
+ return null;
1143
+ if (obj.worktreeExecutionMode !== "worktree-first")
1144
+ return null;
1145
+ if (typeof obj.tddWorktreeCutoverSliceId === "string" &&
1146
+ obj.tddWorktreeCutoverSliceId.trim().length > 0) {
1147
+ return null;
1148
+ }
1149
+ const ledgerPath = runtimePath(projectRoot, "state", "delegation-log.json");
1150
+ const activeRunId = typeof obj.activeRunId === "string" ? obj.activeRunId : "";
1151
+ let ledgerRaw;
1152
+ try {
1153
+ ledgerRaw = await fs.readFile(ledgerPath, "utf8");
1154
+ }
1155
+ catch {
1156
+ ledgerRaw = "";
1157
+ }
1158
+ let ledgerParsed = null;
1159
+ if (ledgerRaw.length > 0) {
1160
+ try {
1161
+ ledgerParsed = JSON.parse(ledgerRaw);
1162
+ }
1163
+ catch {
1164
+ ledgerParsed = null;
1165
+ }
1166
+ }
1167
+ const entries = ledgerParsed &&
1168
+ typeof ledgerParsed === "object" &&
1169
+ !Array.isArray(ledgerParsed) &&
1170
+ Array.isArray(ledgerParsed.entries)
1171
+ ? ledgerParsed.entries
1172
+ : [];
1173
+ let boundary = -1;
1174
+ for (const entry of entries) {
1175
+ if (entry.agent !== "slice-implementer")
1176
+ continue;
1177
+ if (entry.status !== "completed")
1178
+ continue;
1179
+ if (typeof entry.sliceId !== "string")
1180
+ continue;
1181
+ if (activeRunId && entry.runId && entry.runId !== activeRunId)
1182
+ continue;
1183
+ const tok = typeof entry.claimToken === "string" ? entry.claimToken.trim() : "";
1184
+ const lane = typeof entry.ownerLaneId === "string" ? entry.ownerLaneId.trim() : "";
1185
+ const lease = typeof entry.leasedUntil === "string" ? entry.leasedUntil.trim() : "";
1186
+ if (tok.length > 0 || lane.length > 0 || lease.length > 0)
1187
+ continue;
1188
+ const m = /^S-(\d+)$/u.exec(entry.sliceId);
1189
+ if (!m)
1190
+ continue;
1191
+ const n = Number.parseInt(m[1], 10);
1192
+ if (!Number.isFinite(n))
1193
+ continue;
1194
+ if (n > boundary)
1195
+ boundary = n;
1196
+ }
1197
+ let stamped = null;
1198
+ if (boundary >= 0) {
1199
+ stamped = `S-${boundary}`;
1200
+ }
1201
+ else if (typeof obj.tddCutoverSliceId === "string" &&
1202
+ /^S-\d+$/u.test(obj.tddCutoverSliceId)) {
1203
+ stamped = obj.tddCutoverSliceId;
1204
+ }
1205
+ if (!stamped)
1206
+ return null;
1207
+ const merged = { ...obj, tddWorktreeCutoverSliceId: stamped };
1208
+ try {
1209
+ await writeFileSafe(flowStatePath, `${JSON.stringify(merged, null, 2)}\n`, {
1210
+ mode: 0o600
1211
+ });
1212
+ }
1213
+ catch {
1214
+ return null;
1215
+ }
1216
+ return (`v6.14.2 stamped tddWorktreeCutoverSliceId=${stamped}; closed slices ≤ ${stamped} ` +
1217
+ "are exempt from worktree-first findings under legacyContinuation. " +
1218
+ "Edit .cclaw/state/flow-state.json to override (advisory).");
1089
1219
  }
1090
1220
  async function cleanLegacyArtifacts(projectRoot) {
1091
1221
  for (const legacyFolder of DEPRECATED_UTILITY_SKILL_FOLDERS) {
@@ -1266,6 +1396,10 @@ async function materializeRuntime(projectRoot, config, forceStateReset, operatio
1266
1396
  if (v614Hint) {
1267
1397
  process.stdout.write(`cclaw: ${v614Hint}\n`);
1268
1398
  }
1399
+ const v6142Hint = await applyV6142WorktreeCutoverIfNeeded(projectRoot);
1400
+ if (v6142Hint) {
1401
+ process.stdout.write(`cclaw: ${v6142Hint}\n`);
1402
+ }
1269
1403
  }
1270
1404
  try {
1271
1405
  await ensureRunSystem(projectRoot, { createIfMissing: false });
@@ -17,6 +17,10 @@ import { FlowStateGuardMismatchError, verifyFlowStateGuard } from "../run-persis
17
17
  import { DelegationTimestampError, DispatchCapError, DispatchClaimInvalidError, DispatchDuplicateError, DispatchOverlapError } from "../delegation.js";
18
18
  import { parsePlanSplitWavesArgs, runPlanSplitWaves } from "./plan-split-waves.js";
19
19
  import { runSetWorktreeMode } from "./set-worktree-mode.js";
20
+ import { runSetCheckpointMode } from "./set-checkpoint-mode.js";
21
+ import { runSetIntegrationOverseerMode } from "./set-integration-overseer-mode.js";
22
+ import { runWaveStatusCommand } from "./wave-status.js";
23
+ import { runCohesionContractCommand } from "./cohesion-contract-stub.js";
20
24
  /**
21
25
  * Subcommands that mutate or consult flow-state.json via the CLI runtime.
22
26
  * They all require the sha256 sidecar to match before continuing so a
@@ -30,12 +34,14 @@ const GUARD_ENFORCED_SUBCOMMANDS = new Set([
30
34
  "rewind",
31
35
  "verify-flow-state-diff",
32
36
  "verify-current-state",
33
- "set-worktree-mode"
37
+ "set-worktree-mode",
38
+ "set-checkpoint-mode",
39
+ "set-integration-overseer-mode"
34
40
  ]);
35
41
  export async function runInternalCommand(projectRoot, argv, io) {
36
42
  const [subcommand, ...tokens] = argv;
37
43
  if (!subcommand) {
38
- 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 | plan-split-waves | set-worktree-mode\n");
44
+ 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 | plan-split-waves | set-worktree-mode | set-checkpoint-mode | set-integration-overseer-mode | wave-status | cohesion-contract\n");
39
45
  return 1;
40
46
  }
41
47
  try {
@@ -93,7 +99,19 @@ export async function runInternalCommand(projectRoot, argv, io) {
93
99
  if (subcommand === "set-worktree-mode") {
94
100
  return await runSetWorktreeMode(projectRoot, tokens, io);
95
101
  }
96
- 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 | plan-split-waves | set-worktree-mode\n`);
102
+ if (subcommand === "set-checkpoint-mode") {
103
+ return await runSetCheckpointMode(projectRoot, tokens, io);
104
+ }
105
+ if (subcommand === "set-integration-overseer-mode") {
106
+ return await runSetIntegrationOverseerMode(projectRoot, tokens, io);
107
+ }
108
+ if (subcommand === "wave-status") {
109
+ return await runWaveStatusCommand(projectRoot, tokens, io);
110
+ }
111
+ if (subcommand === "cohesion-contract") {
112
+ return await runCohesionContractCommand(projectRoot, tokens, io);
113
+ }
114
+ 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 | plan-split-waves | set-worktree-mode | set-checkpoint-mode | set-integration-overseer-mode | wave-status | cohesion-contract\n`);
97
115
  return 1;
98
116
  }
99
117
  catch (err) {
@@ -0,0 +1,29 @@
1
+ import type { Writable } from "node:stream";
2
+ interface InternalIo {
3
+ stdout: Writable;
4
+ stderr: Writable;
5
+ }
6
+ export interface CohesionContractArgs {
7
+ stub: boolean;
8
+ force: boolean;
9
+ reason: string | null;
10
+ }
11
+ export declare function parseCohesionContractArgs(tokens: string[]): CohesionContractArgs | null;
12
+ /**
13
+ * v6.14.2 — emit a minimal advisory cohesion contract that satisfies
14
+ * the linter shape check (`cohesion-contract.{md,json}`) so projects
15
+ * with `legacyContinuation: true` and ≥ 2 completed slice-implementer
16
+ * rows can clear the soft `tdd.cohesion_contract_missing` finding
17
+ * without hand-authoring the document.
18
+ *
19
+ * The stub is intentionally bare — `sharedTypes`, `touchpoints`, and
20
+ * `slices` are populated from the active run delegation ledger so
21
+ * downstream tooling can see which slices the contract acknowledges,
22
+ * but the contract carries `status.verdict: "advisory_legacy"` so
23
+ * reviewers know not to treat it as authoritative.
24
+ *
25
+ * Refuses to overwrite an existing contract unless `--force` is
26
+ * passed; the existing file is treated as authored work.
27
+ */
28
+ export declare function runCohesionContractCommand(projectRoot: string, tokens: string[], io: InternalIo): Promise<number>;
29
+ export {};
@@ -0,0 +1,166 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { RUNTIME_ROOT } from "../constants.js";
4
+ import { writeFileSafe } from "../fs-utils.js";
5
+ import { readDelegationLedger } from "../delegation.js";
6
+ export function parseCohesionContractArgs(tokens) {
7
+ const args = { stub: false, force: false, reason: null };
8
+ for (const token of tokens) {
9
+ if (token === "--stub") {
10
+ args.stub = true;
11
+ continue;
12
+ }
13
+ if (token === "--force") {
14
+ args.force = true;
15
+ continue;
16
+ }
17
+ if (token.startsWith("--reason=")) {
18
+ const raw = token.slice("--reason=".length).trim();
19
+ if (raw.length > 0)
20
+ args.reason = raw;
21
+ continue;
22
+ }
23
+ return null;
24
+ }
25
+ if (!args.stub)
26
+ return null;
27
+ return args;
28
+ }
29
+ /**
30
+ * v6.14.2 — emit a minimal advisory cohesion contract that satisfies
31
+ * the linter shape check (`cohesion-contract.{md,json}`) so projects
32
+ * with `legacyContinuation: true` and ≥ 2 completed slice-implementer
33
+ * rows can clear the soft `tdd.cohesion_contract_missing` finding
34
+ * without hand-authoring the document.
35
+ *
36
+ * The stub is intentionally bare — `sharedTypes`, `touchpoints`, and
37
+ * `slices` are populated from the active run delegation ledger so
38
+ * downstream tooling can see which slices the contract acknowledges,
39
+ * but the contract carries `status.verdict: "advisory_legacy"` so
40
+ * reviewers know not to treat it as authoritative.
41
+ *
42
+ * Refuses to overwrite an existing contract unless `--force` is
43
+ * passed; the existing file is treated as authored work.
44
+ */
45
+ export async function runCohesionContractCommand(projectRoot, tokens, io) {
46
+ const parsed = parseCohesionContractArgs(tokens);
47
+ if (!parsed) {
48
+ io.stderr.write("cclaw internal cohesion-contract: usage: --stub [--force] [--reason=\"<short>\"]\n");
49
+ return 1;
50
+ }
51
+ const artifactsDir = path.join(projectRoot, RUNTIME_ROOT, "artifacts");
52
+ const mdPath = path.join(artifactsDir, "cohesion-contract.md");
53
+ const jsonPath = path.join(artifactsDir, "cohesion-contract.json");
54
+ if (!parsed.force) {
55
+ const mdExists = await fileExists(mdPath);
56
+ const jsonExists = await fileExists(jsonPath);
57
+ if (mdExists || jsonExists) {
58
+ io.stderr.write("cclaw internal cohesion-contract: existing cohesion-contract.{md,json} present; pass --force to overwrite.\n");
59
+ return 1;
60
+ }
61
+ }
62
+ const ledger = await readDelegationLedger(projectRoot).catch(() => null);
63
+ const sliceIds = collectSliceIds(ledger?.entries ?? []);
64
+ const reasonNote = parsed.reason
65
+ ? `Reason: ${parsed.reason}`
66
+ : "Reason: legacyContinuation auto-stub.";
67
+ const md = [
68
+ "# Cohesion Contract",
69
+ "",
70
+ "_Advisory stub generated by `cclaw-cli internal cohesion-contract --stub`._",
71
+ "_Status: `advisory_legacy` — populated for legacyContinuation projects so the_",
72
+ "_TDD linter does not block stage-complete on `tdd.cohesion_contract_missing`._",
73
+ "",
74
+ `${reasonNote}`,
75
+ "",
76
+ "## Shared Types & Interfaces",
77
+ "| Symbol | Path | Signature | Owner slice |",
78
+ "|---|---|---|---|",
79
+ "| (none recorded) | (none) | (none) | (n/a) |",
80
+ "",
81
+ "## Naming Conventions",
82
+ "- Stub: per-slice modules continue to follow the existing repo conventions.",
83
+ "",
84
+ "## Invariants",
85
+ "- Stub: no cross-slice invariants asserted; treat each slice as independent until upgraded.",
86
+ "",
87
+ "## Integration Touchpoints",
88
+ "| From slice | To slice | Surface | Integration test name |",
89
+ "|---|---|---|---|",
90
+ "| (none recorded) | (n/a) | (n/a) | (n/a) |",
91
+ "",
92
+ "## Behavior Specifications per Slice",
93
+ sliceIds.length === 0
94
+ ? "- (none recorded)"
95
+ : sliceIds
96
+ .map((sid) => `### Slice ${sid}\n- Behavior: see \`tdd-slices/${sid}.md\` (if present).`)
97
+ .join("\n\n"),
98
+ "",
99
+ "## Status",
100
+ "| Slice | Implemented | Tests pass | Cohesion verified |",
101
+ "|---|---|---|---|",
102
+ sliceIds.length === 0
103
+ ? "| (none) | n/a | n/a | n/a |"
104
+ : sliceIds.map((sid) => `| ${sid} | yes | yes | advisory |`).join("\n"),
105
+ ""
106
+ ].join("\n");
107
+ const jsonStub = {
108
+ version: 1,
109
+ sharedTypes: [],
110
+ touchpoints: [],
111
+ slices: sliceIds.map((sid) => ({
112
+ sliceId: sid,
113
+ description: `Stub entry for ${sid}; advisory under legacyContinuation.`,
114
+ implemented: true,
115
+ testsPass: true,
116
+ cohesionVerified: false
117
+ })),
118
+ status: {
119
+ verdict: "advisory_legacy",
120
+ generatedBy: "cclaw-cli internal cohesion-contract --stub",
121
+ reason: parsed.reason ?? "legacyContinuation auto-stub"
122
+ }
123
+ };
124
+ await writeFileSafe(mdPath, md);
125
+ await writeFileSafe(jsonPath, `${JSON.stringify(jsonStub, null, 2)}\n`);
126
+ io.stdout.write(`cclaw: cohesion-contract stub written (${sliceIds.length} slice(s) referenced). ` +
127
+ "Status: advisory_legacy — review and replace once cross-slice cohesion data is real.\n");
128
+ return 0;
129
+ }
130
+ async function fileExists(filePath) {
131
+ try {
132
+ await fs.access(filePath);
133
+ return true;
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
139
+ function collectSliceIds(entries) {
140
+ const set = new Set();
141
+ for (const entry of entries) {
142
+ if (entry.agent !== "slice-implementer")
143
+ continue;
144
+ if (entry.status !== "completed")
145
+ continue;
146
+ if (typeof entry.sliceId !== "string")
147
+ continue;
148
+ if (entry.sliceId.length === 0)
149
+ continue;
150
+ set.add(entry.sliceId);
151
+ }
152
+ return [...set].sort((a, b) => {
153
+ const an = parseSliceNum(a);
154
+ const bn = parseSliceNum(b);
155
+ if (an !== null && bn !== null)
156
+ return an - bn;
157
+ return a.localeCompare(b);
158
+ });
159
+ }
160
+ function parseSliceNum(sliceId) {
161
+ const m = /^S-(\d+)$/u.exec(sliceId);
162
+ if (!m)
163
+ return null;
164
+ const n = Number.parseInt(m[1], 10);
165
+ return Number.isFinite(n) ? n : null;
166
+ }
@@ -0,0 +1,16 @@
1
+ import type { Writable } from "node:stream";
2
+ export interface SetCheckpointModeArgs {
3
+ mode: "per-slice" | "global-red";
4
+ reason: string | null;
5
+ }
6
+ export declare function parseSetCheckpointModeArgs(tokens: string[]): SetCheckpointModeArgs | null;
7
+ /**
8
+ * v6.14.2 — set `flow-state.json::tddCheckpointMode` without advancing
9
+ * the stage DAG. Mirrors `set-worktree-mode`. The `--reason` flag is
10
+ * optional but recommended for the audit trail; it is currently passed
11
+ * through to the writer subsystem string so operators can grep the
12
+ * `.flow-state.guard.json` sidecar.
13
+ */
14
+ export declare function runSetCheckpointMode(projectRoot: string, tokens: string[], io: {
15
+ stderr: Writable;
16
+ }): Promise<number>;
@@ -0,0 +1,72 @@
1
+ import { readFlowState, writeFlowState } from "../runs.js";
2
+ export function parseSetCheckpointModeArgs(tokens) {
3
+ let mode = null;
4
+ let reason = null;
5
+ let positional = null;
6
+ for (const token of tokens) {
7
+ if (token.startsWith("--mode=")) {
8
+ const raw = token.slice("--mode=".length).trim();
9
+ if (raw === "per-slice" || raw === "global-red") {
10
+ mode = raw;
11
+ }
12
+ else {
13
+ return null;
14
+ }
15
+ continue;
16
+ }
17
+ if (token.startsWith("--reason=")) {
18
+ const raw = token.slice("--reason=".length).trim();
19
+ if (raw.length > 0)
20
+ reason = raw;
21
+ continue;
22
+ }
23
+ if (token.startsWith("--")) {
24
+ // unknown flag — let the caller surface usage.
25
+ return null;
26
+ }
27
+ if (positional === null) {
28
+ positional = token.trim();
29
+ continue;
30
+ }
31
+ return null;
32
+ }
33
+ if (mode === null && positional !== null) {
34
+ if (positional === "per-slice" || positional === "global-red") {
35
+ mode = positional;
36
+ }
37
+ else {
38
+ return null;
39
+ }
40
+ }
41
+ if (mode === null)
42
+ return null;
43
+ return { mode, reason };
44
+ }
45
+ /**
46
+ * v6.14.2 — set `flow-state.json::tddCheckpointMode` without advancing
47
+ * the stage DAG. Mirrors `set-worktree-mode`. The `--reason` flag is
48
+ * optional but recommended for the audit trail; it is currently passed
49
+ * through to the writer subsystem string so operators can grep the
50
+ * `.flow-state.guard.json` sidecar.
51
+ */
52
+ export async function runSetCheckpointMode(projectRoot, tokens, io) {
53
+ const parsed = parseSetCheckpointModeArgs(tokens);
54
+ if (!parsed) {
55
+ io.stderr.write("cclaw internal set-checkpoint-mode: usage: <per-slice|global-red> [--reason=\"<short>\"] " +
56
+ "(or --mode=<per-slice|global-red>)\n");
57
+ return 1;
58
+ }
59
+ const state = await readFlowState(projectRoot);
60
+ const writerSubsystem = parsed.reason
61
+ ? `set-checkpoint-mode:${slugifyReason(parsed.reason)}`
62
+ : "set-checkpoint-mode";
63
+ await writeFlowState(projectRoot, { ...state, tddCheckpointMode: parsed.mode }, { writerSubsystem });
64
+ return 0;
65
+ }
66
+ function slugifyReason(reason) {
67
+ return (reason
68
+ .toLowerCase()
69
+ .replace(/[^a-z0-9_-]+/gu, "-")
70
+ .replace(/^-+|-+$/gu, "")
71
+ .slice(0, 60) || "unspecified");
72
+ }
@@ -0,0 +1,14 @@
1
+ import type { Writable } from "node:stream";
2
+ export interface SetIntegrationOverseerModeArgs {
3
+ mode: "conditional" | "always";
4
+ reason: string | null;
5
+ }
6
+ export declare function parseSetIntegrationOverseerModeArgs(tokens: string[]): SetIntegrationOverseerModeArgs | null;
7
+ /**
8
+ * v6.14.2 — set `flow-state.json::integrationOverseerMode` without
9
+ * advancing the stage DAG. Mirrors `set-worktree-mode` and
10
+ * `set-checkpoint-mode`.
11
+ */
12
+ export declare function runSetIntegrationOverseerMode(projectRoot: string, tokens: string[], io: {
13
+ stderr: Writable;
14
+ }): Promise<number>;