cclaw-cli 0.42.0 → 0.44.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.
@@ -0,0 +1,425 @@
1
+ import fs from "node:fs/promises";
2
+ import { stageSchema } from "../content/stage-schema.js";
3
+ import { appendDelegation, checkMandatoryDelegations } from "../delegation.js";
4
+ import { verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "../gate-evidence.js";
5
+ import { isFlowTrack, nextStage } from "../flow-state.js";
6
+ import { readFlowState, writeFlowState } from "../runs.js";
7
+ import { FLOW_STAGES } from "../types.js";
8
+ function unique(values) {
9
+ return [...new Set(values)];
10
+ }
11
+ function parseStringList(raw) {
12
+ if (!Array.isArray(raw))
13
+ return [];
14
+ return raw
15
+ .filter((item) => typeof item === "string")
16
+ .map((item) => item.trim())
17
+ .filter((item) => item.length > 0);
18
+ }
19
+ function isFlowStageValue(value) {
20
+ return typeof value === "string" && FLOW_STAGES.includes(value);
21
+ }
22
+ function parseGuardEvidence(value) {
23
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
24
+ return {};
25
+ }
26
+ const next = {};
27
+ for (const [key, raw] of Object.entries(value)) {
28
+ if (typeof raw !== "string")
29
+ continue;
30
+ const trimmed = raw.trim();
31
+ if (trimmed.length === 0)
32
+ continue;
33
+ next[key] = trimmed;
34
+ }
35
+ return next;
36
+ }
37
+ function parseCandidateGateCatalog(value, fallback) {
38
+ const next = {};
39
+ for (const stage of FLOW_STAGES) {
40
+ const base = fallback[stage];
41
+ next[stage] = {
42
+ required: [...base.required],
43
+ recommended: [...base.recommended],
44
+ conditional: [...base.conditional],
45
+ triggered: [...base.triggered],
46
+ passed: [...base.passed],
47
+ blocked: [...base.blocked]
48
+ };
49
+ }
50
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
51
+ return next;
52
+ }
53
+ const rawCatalog = value;
54
+ for (const stage of FLOW_STAGES) {
55
+ const rawStage = rawCatalog[stage];
56
+ if (!rawStage || typeof rawStage !== "object" || Array.isArray(rawStage)) {
57
+ continue;
58
+ }
59
+ const typed = rawStage;
60
+ const base = fallback[stage];
61
+ const allowed = new Set([...base.required, ...base.recommended, ...base.conditional]);
62
+ const conditional = new Set(base.conditional);
63
+ const passed = unique(parseStringList(typed.passed)).filter((gateId) => allowed.has(gateId));
64
+ const blocked = unique(parseStringList(typed.blocked)).filter((gateId) => allowed.has(gateId));
65
+ const triggered = unique([
66
+ ...parseStringList(typed.triggered).filter((gateId) => conditional.has(gateId)),
67
+ ...passed.filter((gateId) => conditional.has(gateId)),
68
+ ...blocked.filter((gateId) => conditional.has(gateId))
69
+ ]);
70
+ next[stage] = {
71
+ required: [...base.required],
72
+ recommended: [...base.recommended],
73
+ conditional: [...base.conditional],
74
+ triggered,
75
+ passed,
76
+ blocked
77
+ };
78
+ }
79
+ return next;
80
+ }
81
+ function coerceCandidateFlowState(raw, fallback) {
82
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
83
+ return fallback;
84
+ }
85
+ const typed = raw;
86
+ const track = isFlowTrack(typed.track) ? typed.track : fallback.track;
87
+ const currentStage = isFlowStageValue(typed.currentStage)
88
+ ? typed.currentStage
89
+ : fallback.currentStage;
90
+ const completedStages = unique(parseStringList(typed.completedStages).filter((stage) => isFlowStageValue(stage)));
91
+ const skippedStagesRaw = parseStringList(typed.skippedStages).filter((stage) => isFlowStageValue(stage));
92
+ const skippedStages = skippedStagesRaw.length > 0 ? skippedStagesRaw : fallback.skippedStages;
93
+ return {
94
+ ...fallback,
95
+ currentStage,
96
+ completedStages,
97
+ track,
98
+ skippedStages,
99
+ guardEvidence: parseGuardEvidence(typed.guardEvidence),
100
+ stageGateCatalog: parseCandidateGateCatalog(typed.stageGateCatalog, fallback.stageGateCatalog)
101
+ };
102
+ }
103
+ function parseEvidenceByGate(raw) {
104
+ if (!raw || raw.trim().length === 0) {
105
+ return {};
106
+ }
107
+ let parsed;
108
+ try {
109
+ parsed = JSON.parse(raw);
110
+ }
111
+ catch (err) {
112
+ throw new Error(`--evidence-json must be valid JSON object: ${err instanceof Error ? err.message : String(err)}`);
113
+ }
114
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
115
+ throw new Error("--evidence-json must deserialize to an object.");
116
+ }
117
+ const next = {};
118
+ for (const [key, value] of Object.entries(parsed)) {
119
+ if (typeof value !== "string")
120
+ continue;
121
+ const trimmed = value.trim();
122
+ if (trimmed.length === 0)
123
+ continue;
124
+ next[key] = trimmed;
125
+ }
126
+ return next;
127
+ }
128
+ function parseCsv(raw) {
129
+ if (!raw)
130
+ return [];
131
+ return raw
132
+ .split(",")
133
+ .map((item) => item.trim())
134
+ .filter((item) => item.length > 0);
135
+ }
136
+ function parseAdvanceStageArgs(tokens) {
137
+ const [stageRaw, ...flagTokens] = tokens;
138
+ if (!isFlowStageValue(stageRaw)) {
139
+ throw new Error(`internal advance-stage requires a stage positional argument (${FLOW_STAGES.join(", ")}).`);
140
+ }
141
+ let evidenceJson;
142
+ let passed = [];
143
+ let waiveDelegations = [];
144
+ let waiverReason;
145
+ let quiet = false;
146
+ for (const token of flagTokens) {
147
+ if (token === "--quiet") {
148
+ quiet = true;
149
+ continue;
150
+ }
151
+ if (token.startsWith("--evidence-json=")) {
152
+ evidenceJson = token.replace("--evidence-json=", "");
153
+ continue;
154
+ }
155
+ if (token.startsWith("--passed=")) {
156
+ passed = [...passed, ...parseCsv(token.replace("--passed=", ""))];
157
+ continue;
158
+ }
159
+ if (token.startsWith("--waive-delegation=")) {
160
+ waiveDelegations = [
161
+ ...waiveDelegations,
162
+ ...parseCsv(token.replace("--waive-delegation=", ""))
163
+ ];
164
+ continue;
165
+ }
166
+ if (token.startsWith("--waiver-reason=")) {
167
+ waiverReason = token.replace("--waiver-reason=", "").trim();
168
+ continue;
169
+ }
170
+ throw new Error(`Unknown flag for internal advance-stage: ${token}`);
171
+ }
172
+ return {
173
+ stage: stageRaw,
174
+ passedGateIds: unique(passed),
175
+ evidenceByGate: parseEvidenceByGate(evidenceJson),
176
+ waiveDelegations: unique(waiveDelegations),
177
+ waiverReason,
178
+ quiet
179
+ };
180
+ }
181
+ function parseVerifyFlowStateDiffArgs(tokens) {
182
+ let afterJson;
183
+ let afterFile;
184
+ let quiet = false;
185
+ for (const token of tokens) {
186
+ if (token === "--quiet") {
187
+ quiet = true;
188
+ continue;
189
+ }
190
+ if (token.startsWith("--after-json=")) {
191
+ afterJson = token.replace("--after-json=", "");
192
+ continue;
193
+ }
194
+ if (token.startsWith("--after-file=")) {
195
+ afterFile = token.replace("--after-file=", "");
196
+ continue;
197
+ }
198
+ throw new Error(`Unknown flag for internal verify-flow-state-diff: ${token}`);
199
+ }
200
+ if (!afterJson && !afterFile) {
201
+ throw new Error("internal verify-flow-state-diff requires --after-json=<json> or --after-file=<path>.");
202
+ }
203
+ return { afterJson, afterFile, quiet };
204
+ }
205
+ function parseVerifyCurrentStateArgs(tokens) {
206
+ let quiet = false;
207
+ for (const token of tokens) {
208
+ if (token === "--quiet") {
209
+ quiet = true;
210
+ continue;
211
+ }
212
+ throw new Error(`Unknown flag for internal verify-current-state: ${token}`);
213
+ }
214
+ return { quiet };
215
+ }
216
+ async function buildValidationReport(projectRoot, flowState) {
217
+ const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage);
218
+ const gates = await verifyCurrentStageGateEvidence(projectRoot, flowState);
219
+ const completedStages = verifyCompletedStagesGateClosure(flowState);
220
+ const ok = delegation.satisfied && gates.ok && gates.complete && completedStages.ok;
221
+ return {
222
+ ok,
223
+ stage: flowState.currentStage,
224
+ delegation: {
225
+ satisfied: delegation.satisfied,
226
+ missing: delegation.missing,
227
+ waived: delegation.waived,
228
+ missingEvidence: delegation.missingEvidence,
229
+ expectedMode: delegation.expectedMode
230
+ },
231
+ gates: {
232
+ ok: gates.ok,
233
+ complete: gates.complete,
234
+ issues: gates.issues,
235
+ missingRequired: gates.missingRequired,
236
+ missingTriggeredConditional: gates.missingTriggeredConditional
237
+ },
238
+ completedStages: {
239
+ ok: completedStages.ok,
240
+ issues: completedStages.issues
241
+ }
242
+ };
243
+ }
244
+ async function runAdvanceStage(projectRoot, args, io) {
245
+ const flowState = await readFlowState(projectRoot);
246
+ if (flowState.currentStage !== args.stage) {
247
+ io.stderr.write(`cclaw internal advance-stage: current stage is "${flowState.currentStage}", not "${args.stage}".\n`);
248
+ return 1;
249
+ }
250
+ const schema = stageSchema(args.stage);
251
+ const requiredGateIds = schema.requiredGates
252
+ .filter((gate) => gate.tier === "required")
253
+ .map((gate) => gate.id);
254
+ const allowedGateIds = new Set(schema.requiredGates.map((gate) => gate.id));
255
+ const selectedGateIds = args.passedGateIds.length > 0
256
+ ? args.passedGateIds.filter((gateId) => allowedGateIds.has(gateId))
257
+ : requiredGateIds;
258
+ const missingRequired = requiredGateIds.filter((gateId) => !selectedGateIds.includes(gateId));
259
+ if (missingRequired.length > 0) {
260
+ io.stderr.write(`cclaw internal advance-stage: required gates not selected as passed: ${missingRequired.join(", ")}.\n`);
261
+ return 1;
262
+ }
263
+ const mandatory = new Set(schema.mandatoryDelegations);
264
+ for (const agent of args.waiveDelegations) {
265
+ if (!mandatory.has(agent)) {
266
+ io.stderr.write(`cclaw internal advance-stage: cannot waive "${agent}" for stage "${args.stage}" (not mandatory).\n`);
267
+ return 1;
268
+ }
269
+ }
270
+ if (args.waiveDelegations.length > 0) {
271
+ const waiverReason = args.waiverReason && args.waiverReason.length > 0
272
+ ? args.waiverReason
273
+ : "manual_waiver";
274
+ for (const agent of args.waiveDelegations) {
275
+ await appendDelegation(projectRoot, {
276
+ stage: args.stage,
277
+ agent,
278
+ mode: "mandatory",
279
+ status: "waived",
280
+ waiverReason,
281
+ fulfillmentMode: "role-switch",
282
+ ts: new Date().toISOString()
283
+ });
284
+ }
285
+ }
286
+ const catalog = flowState.stageGateCatalog[args.stage];
287
+ const nextPassed = unique([...catalog.passed, ...selectedGateIds]).filter((gateId) => allowedGateIds.has(gateId));
288
+ const nextBlocked = unique(catalog.blocked.filter((gateId) => !nextPassed.includes(gateId))).filter((gateId) => allowedGateIds.has(gateId));
289
+ const conditional = new Set(catalog.conditional);
290
+ const nextTriggered = unique([
291
+ ...catalog.triggered.filter((gateId) => conditional.has(gateId)),
292
+ ...nextPassed.filter((gateId) => conditional.has(gateId)),
293
+ ...nextBlocked.filter((gateId) => conditional.has(gateId))
294
+ ]);
295
+ const nextGuardEvidence = { ...flowState.guardEvidence };
296
+ for (const gateId of nextPassed) {
297
+ const existing = nextGuardEvidence[gateId];
298
+ if (typeof existing === "string" && existing.trim().length > 0)
299
+ continue;
300
+ const provided = args.evidenceByGate[gateId];
301
+ nextGuardEvidence[gateId] = provided && provided.trim().length > 0
302
+ ? provided.trim()
303
+ : `stage-complete helper auto-evidence for ${gateId} @ ${new Date().toISOString()} (${schema.artifactFile})`;
304
+ }
305
+ const nextStageCatalog = {
306
+ required: [...catalog.required],
307
+ recommended: [...catalog.recommended],
308
+ conditional: [...catalog.conditional],
309
+ triggered: nextTriggered,
310
+ passed: nextPassed,
311
+ blocked: nextBlocked
312
+ };
313
+ const candidateState = {
314
+ ...flowState,
315
+ guardEvidence: nextGuardEvidence,
316
+ stageGateCatalog: {
317
+ ...flowState.stageGateCatalog,
318
+ [args.stage]: nextStageCatalog
319
+ }
320
+ };
321
+ const validation = await buildValidationReport(projectRoot, candidateState);
322
+ if (!validation.ok) {
323
+ io.stderr.write(`cclaw internal advance-stage: validation failed for stage "${args.stage}".\n`);
324
+ if (validation.delegation.missing.length > 0) {
325
+ io.stderr.write(`- missing delegations: ${validation.delegation.missing.join(", ")}\n`);
326
+ }
327
+ if (validation.delegation.missingEvidence.length > 0) {
328
+ io.stderr.write(`- role-switch evidence missing: ${validation.delegation.missingEvidence.join(", ")}\n`);
329
+ }
330
+ if (validation.gates.issues.length > 0) {
331
+ io.stderr.write(`- gate issues: ${validation.gates.issues.join(" | ")}\n`);
332
+ }
333
+ if (validation.completedStages.issues.length > 0) {
334
+ io.stderr.write(`- completed-stage closure issues: ${validation.completedStages.issues.join(" | ")}\n`);
335
+ }
336
+ return 1;
337
+ }
338
+ const successor = nextStage(args.stage, flowState.track);
339
+ const completedStages = flowState.completedStages.includes(args.stage)
340
+ ? [...flowState.completedStages]
341
+ : [...flowState.completedStages, args.stage];
342
+ const finalState = {
343
+ ...candidateState,
344
+ completedStages,
345
+ currentStage: successor ?? args.stage
346
+ };
347
+ await writeFlowState(projectRoot, finalState);
348
+ if (!args.quiet) {
349
+ io.stdout.write(`${JSON.stringify({
350
+ ok: true,
351
+ command: "advance-stage",
352
+ stage: args.stage,
353
+ nextStage: successor,
354
+ currentStage: finalState.currentStage,
355
+ completedStages: finalState.completedStages
356
+ }, null, 2)}\n`);
357
+ }
358
+ return 0;
359
+ }
360
+ async function runVerifyFlowStateDiff(projectRoot, args, io) {
361
+ let raw = args.afterJson;
362
+ if (!raw && args.afterFile) {
363
+ raw = await fs.readFile(args.afterFile, "utf8");
364
+ }
365
+ if (!raw) {
366
+ io.stderr.write("cclaw internal verify-flow-state-diff: no candidate state payload.\n");
367
+ return 1;
368
+ }
369
+ let parsed;
370
+ try {
371
+ parsed = JSON.parse(raw);
372
+ }
373
+ catch (err) {
374
+ io.stderr.write(`cclaw internal verify-flow-state-diff: invalid JSON payload (${err instanceof Error ? err.message : String(err)}).\n`);
375
+ return 1;
376
+ }
377
+ const current = await readFlowState(projectRoot);
378
+ const candidate = coerceCandidateFlowState(parsed, current);
379
+ const validation = await buildValidationReport(projectRoot, candidate);
380
+ if (!args.quiet) {
381
+ io.stdout.write(`${JSON.stringify(validation, null, 2)}\n`);
382
+ }
383
+ if (!validation.ok) {
384
+ io.stderr.write(`cclaw internal verify-flow-state-diff: candidate state is invalid for stage "${validation.stage}".\n`);
385
+ }
386
+ return validation.ok ? 0 : 1;
387
+ }
388
+ async function runVerifyCurrentState(projectRoot, args, io) {
389
+ const current = await readFlowState(projectRoot);
390
+ const validation = await buildValidationReport(projectRoot, current);
391
+ if (!args.quiet) {
392
+ io.stdout.write(`${JSON.stringify(validation, null, 2)}\n`);
393
+ }
394
+ if (!validation.ok) {
395
+ const unmetDelegations = validation.delegation.missing.length + validation.delegation.missingEvidence.length;
396
+ const gatesWithoutEvidence = validation.gates.issues.filter((issue) => issue.includes("missing guardEvidence entry")).length;
397
+ io.stderr.write(`cclaw: current stage has ${unmetDelegations} unmet mandatory delegations and ${gatesWithoutEvidence} gates without evidence.\n`);
398
+ io.stderr.write(`cclaw internal verify-current-state: unresolved stage constraints for "${validation.stage}".\n`);
399
+ }
400
+ return validation.ok ? 0 : 1;
401
+ }
402
+ export async function runInternalCommand(projectRoot, argv, io) {
403
+ const [subcommand, ...tokens] = argv;
404
+ if (!subcommand) {
405
+ io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state\n");
406
+ return 1;
407
+ }
408
+ try {
409
+ if (subcommand === "advance-stage") {
410
+ return await runAdvanceStage(projectRoot, parseAdvanceStageArgs(tokens), io);
411
+ }
412
+ if (subcommand === "verify-flow-state-diff") {
413
+ return await runVerifyFlowStateDiff(projectRoot, parseVerifyFlowStateDiffArgs(tokens), io);
414
+ }
415
+ if (subcommand === "verify-current-state") {
416
+ return await runVerifyCurrentState(projectRoot, parseVerifyCurrentStateArgs(tokens), io);
417
+ }
418
+ io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | verify-flow-state-diff | verify-current-state\n`);
419
+ return 1;
420
+ }
421
+ catch (err) {
422
+ io.stderr.write(`cclaw internal ${subcommand} failed: ${err instanceof Error ? err.message : String(err)}\n`);
423
+ return 1;
424
+ }
425
+ }
package/dist/types.d.ts CHANGED
@@ -92,9 +92,30 @@ export interface VibyConfig {
92
92
  version: string;
93
93
  flowVersion: string;
94
94
  harnesses: HarnessId[];
95
- /** Prompt guard behavior for runtime write-risk detection hooks. */
95
+ /**
96
+ * Single-knob strictness for both guard families. When set, cclaw derives
97
+ * `promptGuardMode` and `tddEnforcement` from this value unless the legacy
98
+ * fields are explicitly provided. Default: "advisory".
99
+ *
100
+ * Added in v0.43.0 to collapse two fields that always moved together for
101
+ * ~99% of users. Power users who want asymmetric strictness (e.g. strict
102
+ * prompt guard, advisory TDD) can still set the legacy fields directly —
103
+ * explicit per-axis values override the derived strictness.
104
+ */
105
+ strictness?: "advisory" | "strict";
106
+ /**
107
+ * Prompt guard behavior for runtime write-risk detection hooks.
108
+ *
109
+ * Since v0.43.0 this is an advanced override. Prefer `strictness` in new
110
+ * configs; set this explicitly only when you need strict prompt guarding
111
+ * while keeping TDD advisory, or vice versa.
112
+ */
96
113
  promptGuardMode?: "advisory" | "strict";
97
- /** TDD red->green->refactor enforcement mode used by workflow guard hooks. */
114
+ /**
115
+ * TDD red->green->refactor enforcement mode used by workflow guard hooks.
116
+ *
117
+ * Since v0.43.0 this is an advanced override — see `strictness`.
118
+ */
98
119
  tddEnforcement?: "advisory" | "strict";
99
120
  /** Optional test file globs used by guard guidance and /cc-ops tdd-log docs. */
100
121
  tddTestGlobs?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {