cclaw-cli 0.46.12 → 0.46.13

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.
@@ -9,6 +9,9 @@ function flowStatePath() {
9
9
  function delegationLogPathLine() {
10
10
  return `${RUNTIME_ROOT}/state/delegation-log.json`;
11
11
  }
12
+ function reconciliationNoticesPathLine() {
13
+ return `${RUNTIME_ROOT}/state/reconciliation-notices.json`;
14
+ }
12
15
  /**
13
16
  * Command contract for /cc-next — the primary progression command.
14
17
  * Reads flow-state, starts the current stage if unfinished, or advances if all gates pass.
@@ -17,6 +20,7 @@ export function nextCommandContract() {
17
20
  const flowPath = flowStatePath();
18
21
  const skillRel = `${RUNTIME_ROOT}/skills/${NEXT_SKILL_FOLDER}/SKILL.md`;
19
22
  const delegationPath = delegationLogPathLine();
23
+ const reconciliationNoticesPath = reconciliationNoticesPathLine();
20
24
  return `# /cc-next
21
25
 
22
26
  ## Purpose
@@ -40,13 +44,14 @@ This is the only progression command the user needs to drive the entire flow. St
40
44
  1. Read **\`${flowPath}\`**. If missing → **BLOCKED** (state missing).
41
45
  2. Parse JSON. Capture \`currentStage\` and \`stageGateCatalog[currentStage]\`.
42
46
  3. If \`staleStages[currentStage]\` exists, do not advance automatically. Re-run the stage artifact work, then clear the marker with \`/cc-ops rewind --ack <currentStage>\`.
43
- 4. Let \`G\` = \`requiredGates\` for **\`currentStage\`** from the stage schema.
44
- 5. Let \`catalog\` = \`stageGateCatalog[currentStage]\` from flow state.
45
- 6. **Satisfied** for gate id \`g\`: \`g\` in \`catalog.passed\` and \`g\` not in \`catalog.blocked\`.
46
- 7. Let \`M\` = \`mandatoryDelegations\` for \`currentStage\`.
47
- 8. If \`M\` is non-empty, inspect **\`${delegationPath}\`**. Treat as satisfied only if each mandatory agent is **completed** or **waived**.
48
- 9. If any mandatory delegation is missing and no waiver exists: **STOP** and ask the user whether to dispatch now or waive with rationale. Do not mark gates passed while delegation is unresolved.
49
- 10. If \`currentStage === "review"\` and \`catalog.blocked\` includes \`review_criticals_resolved\`, treat this as a hard remediation branch: recommend \`/cc-ops rewind tdd "review_blocked_by_critical"\` with the blocking finding IDs, and do not attempt to advance toward ship.
47
+ 4. Read **\`${reconciliationNoticesPath}\`** when present. If it contains entries for \`activeRunId + currentStage\` and the listed gate is still blocked in \`stageGateCatalog[currentStage].blocked\`, emit a structured warning before any stage-advance decision.
48
+ 5. Let \`G\` = \`requiredGates\` for **\`currentStage\`** from the stage schema.
49
+ 6. Let \`catalog\` = \`stageGateCatalog[currentStage]\` from flow state.
50
+ 7. **Satisfied** for gate id \`g\`: \`g\` in \`catalog.passed\` and \`g\` not in \`catalog.blocked\`.
51
+ 8. Let \`M\` = \`mandatoryDelegations\` for \`currentStage\`.
52
+ 9. If \`M\` is non-empty, inspect **\`${delegationPath}\`**. Treat as satisfied only if each mandatory agent is **completed** or **waived**.
53
+ 10. If any mandatory delegation is missing and no waiver exists: **STOP** and ask the user whether to dispatch now or waive with rationale. Do not mark gates passed while delegation is unresolved.
54
+ 11. If \`currentStage === "review"\` and \`catalog.blocked\` includes \`review_criticals_resolved\`, treat this as a hard remediation branch: recommend \`/cc-ops rewind tdd "review_blocked_by_critical"\` with the blocking finding IDs, and do not attempt to advance toward ship.
50
55
 
51
56
  ### Path A: Current stage is NOT complete (any gate unmet or delegation missing)
52
57
 
@@ -107,6 +112,7 @@ This is the only progression command the user needs to drive the entire flow. St
107
112
  export function nextCommandSkillMarkdown() {
108
113
  const flowPath = flowStatePath();
109
114
  const delegationPath = delegationLogPathLine();
115
+ const reconciliationNoticesPath = reconciliationNoticesPathLine();
110
116
  const stageRows = ["brainstorm", "scope", "design", "spec", "plan", "tdd", "review", "ship"]
111
117
  .map((stage) => {
112
118
  const schema = stageSchema(stage);
@@ -146,6 +152,7 @@ Do **not** mark gates satisfied from memory alone. Cite **artifact evidence** (p
146
152
  2. Record \`currentStage\` and \`stageGateCatalog[currentStage]\`.
147
153
  3. If \`staleStages[currentStage]\` exists, re-run the stage and clear marker via \`/cc-ops rewind --ack <currentStage>\` before advancing.
148
154
  4. If the file is missing or invalid JSON → **BLOCKED** (report and stop).
155
+ 5. Read \`${reconciliationNoticesPath}\` when present. For entries matching \`activeRunId + currentStage\` whose gate is still in \`stageGateCatalog[currentStage].blocked\`, show a warning with gate id + reason before proceeding.
149
156
 
150
157
  ### Step 2: Evaluate gates
151
158
 
@@ -157,6 +164,8 @@ Check \`mandatoryDelegations\` via **\`${delegationPath}\`** — satisfied only
157
164
  If a mandatory delegation is missing and no waiver exists, **STOP** and ask:
158
165
  (A) dispatch now, (B) waive with rationale, (C) cancel stage advance.
159
166
 
167
+ If reconciliation warnings were emitted in Step 1, treat them as a pre-advance stop point: require explicit acknowledgement before continuing Path A or Path B.
168
+
160
169
  ### Step 3: Act
161
170
 
162
171
  **Path A — stage NOT complete (any gate unmet):**
package/dist/doctor.js CHANGED
@@ -16,7 +16,7 @@ import { TRACK_STAGES } from "./types.js";
16
16
  import { checkMandatoryDelegations } from "./delegation.js";
17
17
  import { ensureFeatureSystem, listFeatures, readActiveFeature, readFeatureWorktreeRegistry, resolveFeatureWorkspacePath, worktreeRegistryPath } from "./feature-system.js";
18
18
  import { buildTraceMatrix } from "./trace-matrix.js";
19
- import { reconcileAndWriteCurrentStageGateCatalog, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
19
+ import { classifyReconciliationNotices, reconcileAndWriteCurrentStageGateCatalog, readReconciliationNotices, RECONCILIATION_NOTICES_REL_PATH, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
20
20
  import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
21
21
  import { stageSkillFolder } from "./content/skills.js";
22
22
  import { doctorCheckMetadata } from "./doctor-registry.js";
@@ -1249,6 +1249,23 @@ export async function doctorChecks(projectRoot, options = {}) {
1249
1249
  ok: activeRunId.length > 0,
1250
1250
  details: `${RUNTIME_ROOT}/state/flow-state.json must include activeRunId`
1251
1251
  });
1252
+ const reconciliationNotices = await readReconciliationNotices(projectRoot);
1253
+ const noticeBuckets = classifyReconciliationNotices(flowState, reconciliationNotices.notices);
1254
+ const formatNoticeList = (items) => items
1255
+ .slice(0, 8)
1256
+ .map((notice) => `${notice.stage}.${notice.gateId}`)
1257
+ .join(", ");
1258
+ checks.push({
1259
+ name: "state:reconciliation_notices",
1260
+ ok: noticeBuckets.unsynced.length === 0,
1261
+ details: noticeBuckets.unsynced.length > 0
1262
+ ? `reconciliation notices out of sync in ${RECONCILIATION_NOTICES_REL_PATH}: ${formatNoticeList(noticeBuckets.unsynced)}. Run \`cclaw doctor --reconcile-gates\` to resync and clear stale entries.`
1263
+ : noticeBuckets.currentStageBlocked.length > 0
1264
+ ? `active reconciliation notices for current stage "${flowState.currentStage}": ${formatNoticeList(noticeBuckets.currentStageBlocked)}`
1265
+ : noticeBuckets.activeBlocked.length > 0
1266
+ ? `active reconciliation notices for run "${flowState.activeRunId}": ${formatNoticeList(noticeBuckets.activeBlocked)}`
1267
+ : `no active reconciliation notices in ${RECONCILIATION_NOTICES_REL_PATH}`
1268
+ });
1252
1269
  const activeTrack = flowState.track ?? "standard";
1253
1270
  const trackStageList = TRACK_STAGES[activeTrack];
1254
1271
  const skippedFromState = Array.isArray(flowState.skippedStages) ? flowState.skippedStages : [];
@@ -1,5 +1,5 @@
1
1
  import type { FlowState, StageGateState } from "./flow-state.js";
2
- import type { FlowStage } from "./types.js";
2
+ import { type FlowStage } from "./types.js";
3
3
  export interface GateEvidenceCheckResult {
4
4
  ok: boolean;
5
5
  stage: FlowStage;
@@ -29,6 +29,27 @@ export interface CompletedStagesClosureResult {
29
29
  blocked: string[];
30
30
  }>;
31
31
  }
32
+ export declare const RECONCILIATION_NOTICES_REL_PATH = ".cclaw/state/reconciliation-notices.json";
33
+ export interface ReconciliationNotice {
34
+ id: string;
35
+ runId: string;
36
+ stage: FlowStage;
37
+ gateId: string;
38
+ reason: string;
39
+ demotedAt: string;
40
+ }
41
+ export interface ReconciliationNoticesPayload {
42
+ schemaVersion: number;
43
+ notices: ReconciliationNotice[];
44
+ }
45
+ export interface ReconciliationNoticeBuckets {
46
+ activeBlocked: ReconciliationNotice[];
47
+ currentStageBlocked: ReconciliationNotice[];
48
+ unsynced: ReconciliationNotice[];
49
+ staleRun: ReconciliationNotice[];
50
+ }
51
+ export declare function readReconciliationNotices(projectRoot: string): Promise<ReconciliationNoticesPayload>;
52
+ export declare function classifyReconciliationNotices(flowState: FlowState, notices: ReconciliationNotice[]): ReconciliationNoticeBuckets;
32
53
  export declare function verifyCurrentStageGateEvidence(projectRoot: string, flowState: FlowState): Promise<GateEvidenceCheckResult>;
33
54
  export declare function verifyCompletedStagesGateClosure(flowState: FlowState): CompletedStagesClosureResult;
34
55
  export interface GateReconciliationResult {
@@ -36,6 +57,7 @@ export interface GateReconciliationResult {
36
57
  changed: boolean;
37
58
  before: StageGateState;
38
59
  after: StageGateState;
60
+ demotedGateIds: string[];
39
61
  notes: string[];
40
62
  }
41
63
  export interface GateReconciliationWritebackResult extends GateReconciliationResult {
@@ -3,9 +3,10 @@ import path from "node:path";
3
3
  import { checkReviewVerdictConsistency, lintArtifact, validateReviewArmy } from "./artifact-linter.js";
4
4
  import { RUNTIME_ROOT } from "./constants.js";
5
5
  import { stageSchema } from "./content/stage-schema.js";
6
- import { exists } from "./fs-utils.js";
6
+ import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
7
7
  import { readFlowState, writeFlowState } from "./runs.js";
8
8
  import { buildTraceMatrix } from "./trace-matrix.js";
9
+ import { FLOW_STAGES } from "./types.js";
9
10
  async function currentStageArtifactExists(projectRoot, stage, track) {
10
11
  const artifactFile = stageSchema(stage, track).artifactFile;
11
12
  const candidates = [
@@ -33,6 +34,95 @@ function sameStringArray(a, b) {
33
34
  return false;
34
35
  return a.every((value, index) => value === b[index]);
35
36
  }
37
+ const RECONCILIATION_NOTICES_FILE = "reconciliation-notices.json";
38
+ const RECONCILIATION_NOTICES_SCHEMA_VERSION = 1;
39
+ export const RECONCILIATION_NOTICES_REL_PATH = `${RUNTIME_ROOT}/state/${RECONCILIATION_NOTICES_FILE}`;
40
+ function isFlowStageValue(value) {
41
+ return typeof value === "string" && FLOW_STAGES.includes(value);
42
+ }
43
+ function reconciliationNoticesPath(projectRoot) {
44
+ return path.join(projectRoot, RUNTIME_ROOT, "state", RECONCILIATION_NOTICES_FILE);
45
+ }
46
+ function defaultReconciliationNoticesPayload() {
47
+ return {
48
+ schemaVersion: RECONCILIATION_NOTICES_SCHEMA_VERSION,
49
+ notices: []
50
+ };
51
+ }
52
+ function sanitizeReconciliationNotice(raw) {
53
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
54
+ return null;
55
+ }
56
+ const typed = raw;
57
+ if (typeof typed.id !== "string" ||
58
+ typeof typed.runId !== "string" ||
59
+ !isFlowStageValue(typed.stage) ||
60
+ typeof typed.gateId !== "string" ||
61
+ typeof typed.reason !== "string" ||
62
+ typeof typed.demotedAt !== "string") {
63
+ return null;
64
+ }
65
+ return {
66
+ id: typed.id,
67
+ runId: typed.runId,
68
+ stage: typed.stage,
69
+ gateId: typed.gateId,
70
+ reason: typed.reason,
71
+ demotedAt: typed.demotedAt
72
+ };
73
+ }
74
+ export async function readReconciliationNotices(projectRoot) {
75
+ const filePath = reconciliationNoticesPath(projectRoot);
76
+ if (!(await exists(filePath))) {
77
+ return defaultReconciliationNoticesPayload();
78
+ }
79
+ try {
80
+ const raw = JSON.parse(await fs.readFile(filePath, "utf8"));
81
+ const notices = Array.isArray(raw.notices)
82
+ ? raw.notices
83
+ .map((value) => sanitizeReconciliationNotice(value))
84
+ .filter((value) => value !== null)
85
+ : [];
86
+ return {
87
+ schemaVersion: RECONCILIATION_NOTICES_SCHEMA_VERSION,
88
+ notices
89
+ };
90
+ }
91
+ catch {
92
+ return defaultReconciliationNoticesPayload();
93
+ }
94
+ }
95
+ async function writeReconciliationNotices(projectRoot, payload) {
96
+ const filePath = reconciliationNoticesPath(projectRoot);
97
+ await ensureDir(path.dirname(filePath));
98
+ await writeFileSafe(filePath, `${JSON.stringify({
99
+ schemaVersion: RECONCILIATION_NOTICES_SCHEMA_VERSION,
100
+ notices: payload.notices
101
+ }, null, 2)}\n`);
102
+ }
103
+ export function classifyReconciliationNotices(flowState, notices) {
104
+ const activeBlocked = [];
105
+ const currentStageBlocked = [];
106
+ const unsynced = [];
107
+ const staleRun = [];
108
+ for (const notice of notices) {
109
+ if (notice.runId !== flowState.activeRunId) {
110
+ staleRun.push(notice);
111
+ continue;
112
+ }
113
+ const stageCatalog = flowState.stageGateCatalog[notice.stage];
114
+ const blocked = stageCatalog.blocked.includes(notice.gateId);
115
+ if (!blocked) {
116
+ unsynced.push(notice);
117
+ continue;
118
+ }
119
+ activeBlocked.push(notice);
120
+ if (notice.stage === flowState.currentStage) {
121
+ currentStageBlocked.push(notice);
122
+ }
123
+ }
124
+ return { activeBlocked, currentStageBlocked, unsynced, staleRun };
125
+ }
36
126
  export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
37
127
  const stage = flowState.currentStage;
38
128
  const schema = stageSchema(stage, flowState.track);
@@ -208,6 +298,7 @@ export function reconcileCurrentStageGateCatalog(flowState) {
208
298
  const allowedSet = new Set([...required, ...recommended]);
209
299
  const catalog = flowState.stageGateCatalog[stage];
210
300
  const notes = [];
301
+ const demotedGateIds = new Set();
211
302
  const before = {
212
303
  required: [...catalog.required],
213
304
  recommended: [...catalog.recommended],
@@ -248,6 +339,7 @@ export function reconcileCurrentStageGateCatalog(flowState) {
248
339
  continue;
249
340
  }
250
341
  passedSet.delete(gateId);
342
+ demotedGateIds.add(gateId);
251
343
  notes.push(`resolved overlap for "${gateId}" in favor of blocked (missing evidence)`);
252
344
  }
253
345
  for (const gateId of [...passedSet]) {
@@ -256,6 +348,7 @@ export function reconcileCurrentStageGateCatalog(flowState) {
256
348
  continue;
257
349
  passedSet.delete(gateId);
258
350
  blockedSet.add(gateId);
351
+ demotedGateIds.add(gateId);
259
352
  notes.push(`moved "${gateId}" from passed to blocked (missing evidence)`);
260
353
  }
261
354
  const after = {
@@ -288,6 +381,7 @@ export function reconcileCurrentStageGateCatalog(flowState) {
288
381
  changed,
289
382
  before,
290
383
  after,
384
+ demotedGateIds: [...required, ...recommended].filter((gateId) => demotedGateIds.has(gateId)),
291
385
  notes
292
386
  }
293
387
  };
@@ -295,8 +389,46 @@ export function reconcileCurrentStageGateCatalog(flowState) {
295
389
  export async function reconcileAndWriteCurrentStageGateCatalog(projectRoot) {
296
390
  const state = await readFlowState(projectRoot);
297
391
  const { nextState, reconciliation } = reconcileCurrentStageGateCatalog(state);
392
+ const effectiveState = reconciliation.changed ? nextState : state;
298
393
  if (reconciliation.changed) {
299
- await writeFlowState(projectRoot, nextState);
394
+ await writeFlowState(projectRoot, effectiveState);
395
+ }
396
+ const noticesPayload = await readReconciliationNotices(projectRoot);
397
+ let noticesChanged = false;
398
+ const noticeBuckets = classifyReconciliationNotices(effectiveState, noticesPayload.notices);
399
+ if (noticeBuckets.unsynced.length > 0 || noticeBuckets.staleRun.length > 0) {
400
+ const dropIds = new Set([...noticeBuckets.unsynced, ...noticeBuckets.staleRun].map((notice) => notice.id));
401
+ noticesPayload.notices = noticesPayload.notices.filter((notice) => !dropIds.has(notice.id));
402
+ noticesChanged = true;
403
+ }
404
+ if (reconciliation.demotedGateIds.length > 0) {
405
+ const existing = new Set(noticesPayload.notices.map((notice) => `${notice.runId}:${notice.stage}:${notice.gateId}`));
406
+ for (const gateId of reconciliation.demotedGateIds) {
407
+ const dedupeKey = `${effectiveState.activeRunId}:${reconciliation.stage}:${gateId}`;
408
+ if (existing.has(dedupeKey)) {
409
+ continue;
410
+ }
411
+ const ts = new Date().toISOString();
412
+ noticesPayload.notices.push({
413
+ id: `${dedupeKey}:${ts}`,
414
+ runId: effectiveState.activeRunId,
415
+ stage: reconciliation.stage,
416
+ gateId,
417
+ reason: "demoted from passed to blocked during gate reconciliation (missing evidence)",
418
+ demotedAt: ts
419
+ });
420
+ existing.add(dedupeKey);
421
+ noticesChanged = true;
422
+ }
423
+ }
424
+ if (noticesChanged) {
425
+ noticesPayload.notices.sort((a, b) => {
426
+ if (a.demotedAt === b.demotedAt) {
427
+ return a.id.localeCompare(b.id);
428
+ }
429
+ return a.demotedAt.localeCompare(b.demotedAt);
430
+ });
431
+ await writeReconciliationNotices(projectRoot, noticesPayload);
300
432
  }
301
433
  return {
302
434
  ...reconciliation,
package/dist/install.js CHANGED
@@ -887,6 +887,10 @@ async function ensureSessionStateFiles(projectRoot) {
887
887
  if (!(await exists(tddCycleLogPath))) {
888
888
  await writeFileSafe(tddCycleLogPath, "");
889
889
  }
890
+ const reconciliationNoticesPath = path.join(stateDir, "reconciliation-notices.json");
891
+ if (!(await exists(reconciliationNoticesPath))) {
892
+ await writeFileSafe(reconciliationNoticesPath, `${JSON.stringify({ schemaVersion: 1, notices: [] }, null, 2)}\n`);
893
+ }
890
894
  const flowSnapshotPath = path.join(stateDir, "flow-state.snapshot.json");
891
895
  if (!(await exists(flowSnapshotPath))) {
892
896
  await writeFileSafe(flowSnapshotPath, `${JSON.stringify({
@@ -16,6 +16,7 @@ const STATE_SNAPSHOT_EXCLUDE = new Set([
16
16
  ]);
17
17
  const DELEGATION_LOG_FILE = "delegation-log.json";
18
18
  const TDD_CYCLE_LOG_FILE = "tdd-cycle-log.jsonl";
19
+ const RECONCILIATION_NOTICES_FILE = "reconciliation-notices.json";
19
20
  function runsRoot(projectRoot) {
20
21
  return path.join(projectRoot, RUNS_DIR_REL_PATH);
21
22
  }
@@ -67,6 +68,7 @@ async function resetCarryoverStateFiles(projectRoot, activeRunId) {
67
68
  await ensureDir(stateDir);
68
69
  await writeFileSafe(path.join(stateDir, DELEGATION_LOG_FILE), `${JSON.stringify({ runId: activeRunId, entries: [] }, null, 2)}\n`);
69
70
  await writeFileSafe(path.join(stateDir, TDD_CYCLE_LOG_FILE), "");
71
+ await writeFileSafe(path.join(stateDir, RECONCILIATION_NOTICES_FILE), `${JSON.stringify({ schemaVersion: 1, notices: [] }, null, 2)}\n`);
70
72
  }
71
73
  function toArchiveDate(date = new Date()) {
72
74
  const yyyy = date.getFullYear().toString();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.46.12",
3
+ "version": "0.46.13",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {