cclaw-cli 6.14.1 → 6.14.3
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/shared.d.ts +15 -0
- package/dist/artifact-linter/tdd.js +237 -8
- package/dist/artifact-linter.js +9 -1
- package/dist/content/core-agents.js +2 -2
- package/dist/content/hooks.js +190 -0
- package/dist/content/stages/tdd.js +3 -2
- package/dist/flow-state.d.ts +46 -0
- package/dist/flow-state.js +18 -0
- package/dist/install.js +175 -14
- package/dist/internal/advance-stage.js +21 -3
- package/dist/internal/cohesion-contract-stub.d.ts +29 -0
- package/dist/internal/cohesion-contract-stub.js +166 -0
- package/dist/internal/set-checkpoint-mode.d.ts +16 -0
- package/dist/internal/set-checkpoint-mode.js +72 -0
- package/dist/internal/set-integration-overseer-mode.d.ts +14 -0
- package/dist/internal/set-integration-overseer-mode.js +69 -0
- package/dist/internal/wave-status.d.ts +51 -0
- package/dist/internal/wave-status.js +285 -0
- package/dist/run-persistence.js +20 -0
- package/package.json +1 -1
package/dist/flow-state.d.ts
CHANGED
|
@@ -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;
|
package/dist/flow-state.js
CHANGED
|
@@ -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
|
@@ -939,8 +939,20 @@ async function applyTddCutoverIfNeeded(projectRoot) {
|
|
|
939
939
|
if (typeof obj.tddCutoverSliceId === "string" && obj.tddCutoverSliceId.length > 0) {
|
|
940
940
|
return;
|
|
941
941
|
}
|
|
942
|
-
|
|
943
|
-
|
|
942
|
+
// v6.14.3 — refresh the SHA256 sidecar by writing through
|
|
943
|
+
// `writeFlowState`. The previous direct `writeFileSafe` invocation
|
|
944
|
+
// left the sidecar stale, so the very next guarded hook on a synced
|
|
945
|
+
// legacy project rejected its own `tddCutoverSliceId` stamp.
|
|
946
|
+
try {
|
|
947
|
+
const state = await readFlowState(projectRoot);
|
|
948
|
+
await writeFlowState(projectRoot, { ...state, tddCutoverSliceId: cutoverSliceId }, {
|
|
949
|
+
allowReset: true,
|
|
950
|
+
writerSubsystem: "sync-v6.12-tdd-cutover-stamp"
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
// Best-effort: corrupt/missing state is handled elsewhere on sync.
|
|
955
|
+
}
|
|
944
956
|
}
|
|
945
957
|
const V613_LEGACY_PLAN_BANNER = "<!-- legacy-continuation: predates v6.13 parallel metadata. New units MAY add dependsOn/claimedPaths/parallelizable; existing units treated as best-effort serial. -->";
|
|
946
958
|
/**
|
|
@@ -1005,12 +1017,25 @@ async function applyPlanLegacyContinuationIfNeeded(projectRoot) {
|
|
|
1005
1017
|
}
|
|
1006
1018
|
/**
|
|
1007
1019
|
* v6.14.0 — set stream-style defaults on `cclaw-cli sync` and print a
|
|
1008
|
-
* one-line hint when defaults change.
|
|
1020
|
+
* one-line hint when defaults change.
|
|
1021
|
+
*
|
|
1022
|
+
* v6.14.2 update — flip the legacyContinuation defaults from
|
|
1023
|
+
* `global-red`/`always` to `per-slice`/`conditional`. Rationale: hox-shape
|
|
1024
|
+
* projects ran into S-17 misroutes precisely because the default
|
|
1025
|
+
* preserved the v6.12 wave barrier even after the project itself had
|
|
1026
|
+
* moved to stream-mode. Existing flow-state values are NEVER overwritten
|
|
1027
|
+
* — operators who want to pin `global-red`/`always` may set them
|
|
1028
|
+
* explicitly via `cclaw-cli internal set-checkpoint-mode global-red` and
|
|
1029
|
+
* `set-integration-overseer-mode always`.
|
|
1030
|
+
*
|
|
1031
|
+
* Strategy:
|
|
1009
1032
|
*
|
|
1010
|
-
* - When `legacyContinuation: true` and `tddCheckpointMode` is unset,
|
|
1011
|
-
* `tddCheckpointMode: "
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1033
|
+
* - When `legacyContinuation: true` and `tddCheckpointMode` is unset,
|
|
1034
|
+
* default to `tddCheckpointMode: "per-slice"` (v6.14.2 — was
|
|
1035
|
+
* `global-red` in v6.14.0/v6.14.1).
|
|
1036
|
+
* - When `legacyContinuation: true` and `integrationOverseerMode` is
|
|
1037
|
+
* unset, default to `integrationOverseerMode: "conditional"` (v6.14.2
|
|
1038
|
+
* — was `always` in v6.14.0/v6.14.1).
|
|
1014
1039
|
* - When `legacyContinuation` is NOT true (new / standard projects) and
|
|
1015
1040
|
* neither field is set, default to `tddCheckpointMode: "per-slice"`,
|
|
1016
1041
|
* `integrationOverseerMode: "conditional"`. Also default
|
|
@@ -1053,12 +1078,12 @@ async function applyV614DefaultsIfNeeded(projectRoot) {
|
|
|
1053
1078
|
const legacyContinuation = obj.legacyContinuation === true;
|
|
1054
1079
|
if (legacyContinuation) {
|
|
1055
1080
|
if (!tddCheckpointModeSet) {
|
|
1056
|
-
updates.tddCheckpointMode = "
|
|
1057
|
-
summary.push("tddCheckpointMode=
|
|
1081
|
+
updates.tddCheckpointMode = "per-slice";
|
|
1082
|
+
summary.push("tddCheckpointMode=per-slice (legacyContinuation, v6.14.2 default flip)");
|
|
1058
1083
|
}
|
|
1059
1084
|
if (!integrationOverseerModeSet) {
|
|
1060
|
-
updates.integrationOverseerMode = "
|
|
1061
|
-
summary.push("integrationOverseerMode=
|
|
1085
|
+
updates.integrationOverseerMode = "conditional";
|
|
1086
|
+
summary.push("integrationOverseerMode=conditional (legacyContinuation, v6.14.2 default flip)");
|
|
1062
1087
|
}
|
|
1063
1088
|
}
|
|
1064
1089
|
else {
|
|
@@ -1078,14 +1103,146 @@ async function applyV614DefaultsIfNeeded(projectRoot) {
|
|
|
1078
1103
|
if (summary.length === 0) {
|
|
1079
1104
|
return null;
|
|
1080
1105
|
}
|
|
1081
|
-
|
|
1106
|
+
// v6.14.3 — refresh the SHA256 sidecar in lockstep so guarded reads
|
|
1107
|
+
// (verify-current-state, advance-stage, etc.) don't trip a guard
|
|
1108
|
+
// mismatch immediately after `cclaw-cli sync`/`upgrade` writes the
|
|
1109
|
+
// v6.14.2 stream-style defaults.
|
|
1110
|
+
try {
|
|
1111
|
+
const state = await readFlowState(projectRoot);
|
|
1112
|
+
await writeFlowState(projectRoot, { ...state, ...updates }, {
|
|
1113
|
+
allowReset: true,
|
|
1114
|
+
writerSubsystem: "sync-v6.14.2-stream-defaults"
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
catch {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
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="...".`;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* v6.14.2 — auto-stamp `tddWorktreeCutoverSliceId` for legacyContinuation
|
|
1124
|
+
* projects in worktree-first mode that haven't yet recorded a boundary.
|
|
1125
|
+
*
|
|
1126
|
+
* Detection ("any-metadata" rule): scan the active run's
|
|
1127
|
+
* `slice-implementer` rows. The boundary is the highest `S-N` whose
|
|
1128
|
+
* rows for the active run lack ALL of `claimToken`, `ownerLaneId`, and
|
|
1129
|
+
* `leasedUntil`. When no such slice exists (every slice carries at
|
|
1130
|
+
* least one worktree field), fall back to `tddCutoverSliceId` so the
|
|
1131
|
+
* v6.12 cutover marker still confers exemption.
|
|
1132
|
+
*
|
|
1133
|
+
* Idempotent: when the field is already set, returns null without
|
|
1134
|
+
* writing. Best-effort: read failures, missing ledger, or unparseable
|
|
1135
|
+
* rows all short-circuit silently — the existing flow-state.json is
|
|
1136
|
+
* never corrupted.
|
|
1137
|
+
*
|
|
1138
|
+
* Returns a one-line hint string (or `null` if nothing changed).
|
|
1139
|
+
*/
|
|
1140
|
+
async function applyV6142WorktreeCutoverIfNeeded(projectRoot) {
|
|
1141
|
+
const flowStatePath = runtimePath(projectRoot, "state", "flow-state.json");
|
|
1142
|
+
let flowStateRaw;
|
|
1143
|
+
try {
|
|
1144
|
+
flowStateRaw = await fs.readFile(flowStatePath, "utf8");
|
|
1145
|
+
}
|
|
1146
|
+
catch {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
let parsed;
|
|
1150
|
+
try {
|
|
1151
|
+
parsed = JSON.parse(flowStateRaw);
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1157
|
+
return null;
|
|
1158
|
+
}
|
|
1159
|
+
const obj = parsed;
|
|
1160
|
+
if (obj.legacyContinuation !== true)
|
|
1161
|
+
return null;
|
|
1162
|
+
if (obj.worktreeExecutionMode !== "worktree-first")
|
|
1163
|
+
return null;
|
|
1164
|
+
if (typeof obj.tddWorktreeCutoverSliceId === "string" &&
|
|
1165
|
+
obj.tddWorktreeCutoverSliceId.trim().length > 0) {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
const ledgerPath = runtimePath(projectRoot, "state", "delegation-log.json");
|
|
1169
|
+
const activeRunId = typeof obj.activeRunId === "string" ? obj.activeRunId : "";
|
|
1170
|
+
let ledgerRaw;
|
|
1082
1171
|
try {
|
|
1083
|
-
await
|
|
1172
|
+
ledgerRaw = await fs.readFile(ledgerPath, "utf8");
|
|
1084
1173
|
}
|
|
1085
1174
|
catch {
|
|
1175
|
+
ledgerRaw = "";
|
|
1176
|
+
}
|
|
1177
|
+
let ledgerParsed = null;
|
|
1178
|
+
if (ledgerRaw.length > 0) {
|
|
1179
|
+
try {
|
|
1180
|
+
ledgerParsed = JSON.parse(ledgerRaw);
|
|
1181
|
+
}
|
|
1182
|
+
catch {
|
|
1183
|
+
ledgerParsed = null;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
const entries = ledgerParsed &&
|
|
1187
|
+
typeof ledgerParsed === "object" &&
|
|
1188
|
+
!Array.isArray(ledgerParsed) &&
|
|
1189
|
+
Array.isArray(ledgerParsed.entries)
|
|
1190
|
+
? ledgerParsed.entries
|
|
1191
|
+
: [];
|
|
1192
|
+
let boundary = -1;
|
|
1193
|
+
for (const entry of entries) {
|
|
1194
|
+
if (entry.agent !== "slice-implementer")
|
|
1195
|
+
continue;
|
|
1196
|
+
if (entry.status !== "completed")
|
|
1197
|
+
continue;
|
|
1198
|
+
if (typeof entry.sliceId !== "string")
|
|
1199
|
+
continue;
|
|
1200
|
+
if (activeRunId && entry.runId && entry.runId !== activeRunId)
|
|
1201
|
+
continue;
|
|
1202
|
+
const tok = typeof entry.claimToken === "string" ? entry.claimToken.trim() : "";
|
|
1203
|
+
const lane = typeof entry.ownerLaneId === "string" ? entry.ownerLaneId.trim() : "";
|
|
1204
|
+
const lease = typeof entry.leasedUntil === "string" ? entry.leasedUntil.trim() : "";
|
|
1205
|
+
if (tok.length > 0 || lane.length > 0 || lease.length > 0)
|
|
1206
|
+
continue;
|
|
1207
|
+
const m = /^S-(\d+)$/u.exec(entry.sliceId);
|
|
1208
|
+
if (!m)
|
|
1209
|
+
continue;
|
|
1210
|
+
const n = Number.parseInt(m[1], 10);
|
|
1211
|
+
if (!Number.isFinite(n))
|
|
1212
|
+
continue;
|
|
1213
|
+
if (n > boundary)
|
|
1214
|
+
boundary = n;
|
|
1215
|
+
}
|
|
1216
|
+
let stamped = null;
|
|
1217
|
+
if (boundary >= 0) {
|
|
1218
|
+
stamped = `S-${boundary}`;
|
|
1219
|
+
}
|
|
1220
|
+
else if (typeof obj.tddCutoverSliceId === "string" &&
|
|
1221
|
+
/^S-\d+$/u.test(obj.tddCutoverSliceId)) {
|
|
1222
|
+
stamped = obj.tddCutoverSliceId;
|
|
1223
|
+
}
|
|
1224
|
+
if (!stamped)
|
|
1086
1225
|
return null;
|
|
1226
|
+
// v6.14.3 — go through `writeFlowState` so the SHA256 sidecar
|
|
1227
|
+
// (`.cclaw/.flow-state.guard.json`) is refreshed in lockstep with
|
|
1228
|
+
// the on-disk flow-state.json. The previous v6.14.2 implementation
|
|
1229
|
+
// wrote the field via `writeFileSafe` directly, which left the
|
|
1230
|
+
// sidecar pointing at the pre-stamp digest; the next guarded hook
|
|
1231
|
+
// (e.g. `cclaw internal verify-current-state`) then failed with
|
|
1232
|
+
// `flow-state guard mismatch` and demanded a manual repair.
|
|
1233
|
+
try {
|
|
1234
|
+
const state = await readFlowState(projectRoot);
|
|
1235
|
+
await writeFlowState(projectRoot, { ...state, tddWorktreeCutoverSliceId: stamped }, {
|
|
1236
|
+
allowReset: true,
|
|
1237
|
+
writerSubsystem: "sync-v6.14.2-worktree-cutover-stamp"
|
|
1238
|
+
});
|
|
1087
1239
|
}
|
|
1088
|
-
|
|
1240
|
+
catch {
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
return (`v6.14.2 stamped tddWorktreeCutoverSliceId=${stamped}; closed slices ≤ ${stamped} ` +
|
|
1244
|
+
"are exempt from worktree-first findings under legacyContinuation. " +
|
|
1245
|
+
"Edit .cclaw/state/flow-state.json to override (advisory).");
|
|
1089
1246
|
}
|
|
1090
1247
|
async function cleanLegacyArtifacts(projectRoot) {
|
|
1091
1248
|
for (const legacyFolder of DEPRECATED_UTILITY_SKILL_FOLDERS) {
|
|
@@ -1266,6 +1423,10 @@ async function materializeRuntime(projectRoot, config, forceStateReset, operatio
|
|
|
1266
1423
|
if (v614Hint) {
|
|
1267
1424
|
process.stdout.write(`cclaw: ${v614Hint}\n`);
|
|
1268
1425
|
}
|
|
1426
|
+
const v6142Hint = await applyV6142WorktreeCutoverIfNeeded(projectRoot);
|
|
1427
|
+
if (v6142Hint) {
|
|
1428
|
+
process.stdout.write(`cclaw: ${v6142Hint}\n`);
|
|
1429
|
+
}
|
|
1269
1430
|
}
|
|
1270
1431
|
try {
|
|
1271
1432
|
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
|
-
|
|
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>;
|