cclaw-cli 0.46.12 → 0.46.14

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):**
@@ -102,6 +102,42 @@ Summarize citable domain practices for a narrow design decision.
102
102
 
103
103
  - Cite authoritative sources (official docs/standards).
104
104
  - State uncertainty explicitly when consensus is weak.
105
+ `,
106
+ "research-fleet.md": `# Parallel Research Fleet Playbook
107
+
108
+ ## Purpose
109
+
110
+ Run a four-lens investigation before design lock so architecture choices are grounded
111
+ in current ecosystem data, not intuition.
112
+
113
+ ## Dispatch Lenses (fan-out)
114
+
115
+ Launch four independent investigation threads in parallel when the harness supports it
116
+ (or sequentially with explicit role-switch logs when it does not):
117
+
118
+ 1. **stack-researcher** — dependency compatibility, alternatives, deprecations.
119
+ 2. **features-researcher** — domain conventions and product/UX patterns.
120
+ 3. **architecture-researcher** — architecture options and trade-off matrix.
121
+ 4. **pitfalls-researcher** — known failure modes, CVEs, and operational traps.
122
+
123
+ ## Output Contract
124
+
125
+ Write findings to \`.cclaw/artifacts/02a-research.md\` with these sections:
126
+
127
+ - \`## Stack Analysis\`
128
+ - \`## Features & Patterns\`
129
+ - \`## Architecture Options\`
130
+ - \`## Pitfalls & Risks\`
131
+ - \`## Synthesis\`
132
+
133
+ Each section must contain concrete notes and at least one evidence reference
134
+ (source URL, file path, or command output anchor).
135
+
136
+ ## Guardrails
137
+
138
+ - Investigate first; no production code edits in this playbook.
139
+ - Keep lenses independent during fan-out; merge only in synthesis.
140
+ - If any lens is incomplete, record it explicitly in \`## Synthesis\` as a blocker.
105
141
  `,
106
142
  "git-history.md": `# Git History Playbook
107
143
 
@@ -13,6 +13,7 @@ const REQUIRED_GATE_IDS = {
13
13
  "scope_user_approved"
14
14
  ],
15
15
  design: [
16
+ "design_research_complete",
16
17
  "design_architecture_locked",
17
18
  "design_data_flow_mapped",
18
19
  "design_failure_modes_mapped",
@@ -53,7 +54,13 @@ const REQUIRED_GATE_IDS = {
53
54
  const REQUIRED_ARTIFACT_SECTIONS = {
54
55
  brainstorm: ["Context", "Problem", "Approaches", "Selected Direction"],
55
56
  scope: ["Scope Mode", "In Scope / Out of Scope", "Completion Dashboard", "Scope Summary"],
56
- design: ["Architecture Boundaries", "Architecture Diagram", "Failure Mode Table", "Completion Dashboard"],
57
+ design: [
58
+ "Research Fleet Synthesis",
59
+ "Architecture Boundaries",
60
+ "Architecture Diagram",
61
+ "Failure Mode Table",
62
+ "Completion Dashboard"
63
+ ],
57
64
  spec: ["Acceptance Criteria", "Edge Cases", "Testability Map", "Approval"],
58
65
  plan: ["Task List", "Dependency Batches", "Acceptance Mapping", "WAIT_FOR_CONFIRM"],
59
66
  tdd: ["RED Evidence", "GREEN Evidence", "REFACTOR Notes", "Traceability", "Verification Ladder"],
@@ -21,6 +21,7 @@ export const DESIGN = {
21
21
  ],
22
22
  checklist: [
23
23
  "Trivial-Change Escape Hatch — If scope artifact shows ≤3 files, zero new interfaces, and no cross-module data flow, skip full review sections. Produce a mini-design: one paragraph of rationale, list of changed files, one risk to watch. Proceed to spec.",
24
+ "Parallel Research Fleet — run `research/research-fleet.md` before architecture lock. Record 4-lens findings in `.cclaw/artifacts/02a-research.md` and summarize resulting decisions in `## Research Fleet Synthesis`.",
24
25
  "Design Doc Check — read existing design docs, scope artifact, brainstorm artifact. If a design doc exists that covers this area, check for 'Supersedes:' and use the latest. Use upstream artifacts as source of truth.",
25
26
  "Codebase Investigation — Before any design decision, read the actual code in the blast radius. List every file that will be touched, its current responsibilities, and existing patterns (error handling, naming, test style). Design must conform to discovered patterns, not impose new ones without justification.",
26
27
  "Step 0: Scope Challenge — what existing code solves sub-problems? Minimum change set? Complexity check: 8+ files or 2+ new services = complexity smell → flag for possible scope reduction.",
@@ -50,6 +51,7 @@ export const DESIGN = {
50
51
  ],
51
52
  process: [
52
53
  "Read upstream artifacts (brainstorm, scope).",
54
+ "Run the research fleet playbook and write `.cclaw/artifacts/02a-research.md` before locking architecture choices.",
53
55
  "Investigate codebase: read files in blast radius, catalogue current patterns and responsibilities.",
54
56
  "Run Step 0 scope challenge: existing code leverage, minimum change set, complexity check.",
55
57
  "Walk through each review section interactively.",
@@ -62,12 +64,14 @@ export const DESIGN = {
62
64
  "Write design lock artifact for downstream spec/plan."
63
65
  ],
64
66
  requiredGates: [
67
+ { id: "design_research_complete", description: "Parallel research artifact is complete and synthesized into design decisions." },
65
68
  { id: "design_architecture_locked", description: "Architecture boundaries are explicit and approved." },
66
69
  { id: "design_data_flow_mapped", description: "Data/state flow includes edge-case paths." },
67
70
  { id: "design_failure_modes_mapped", description: "Failure modes and mitigations are documented." },
68
71
  { id: "design_test_and_perf_defined", description: "Test strategy and performance budget are defined." }
69
72
  ],
70
73
  requiredEvidence: [
74
+ "Research artifact written to `.cclaw/artifacts/02a-research.md` with stack/features/architecture/pitfalls sections plus synthesis.",
71
75
  "Artifact written to `.cclaw/artifacts/03-design.md`.",
72
76
  "Failure-mode table exists with mitigations.",
73
77
  "Test strategy includes unit/integration/e2e expectations.",
@@ -77,15 +81,18 @@ export const DESIGN = {
77
81
  ],
78
82
  inputs: ["scope contract", "system constraints", "non-functional requirements"],
79
83
  requiredContext: [
84
+ "parallel research synthesis from `.cclaw/artifacts/02a-research.md`",
80
85
  "existing architecture and boundaries",
81
86
  "operational constraints",
82
87
  "security and reliability expectations"
83
88
  ],
84
89
  researchPlaybooks: [
90
+ "research/research-fleet.md",
85
91
  "research/framework-docs-lookup.md",
86
92
  "research/best-practices-lookup.md"
87
93
  ],
88
94
  outputs: [
95
+ "parallel research synthesis artifact",
89
96
  "architecture lock",
90
97
  "risk and failure map",
91
98
  "test and performance baseline",
@@ -110,17 +117,14 @@ export const DESIGN = {
110
117
  "Missing data-flow edge cases",
111
118
  "No performance budget for critical path",
112
119
  "Batching multiple design issues into one question",
113
- "Skipping review sections because plan seems simple",
114
120
  "Agreeing with user's architecture choice without evaluating alternatives",
115
121
  "Hedging every recommendation with 'it depends' instead of taking a position",
116
- "No explicit architecture boundary section",
117
- "No failure recovery strategy",
118
- "No defined test/perf baseline",
119
122
  "No NOT-in-scope output section",
120
123
  "No What-already-exists output section",
121
124
  "Design decisions made without reading the actual code first"
122
125
  ],
123
126
  policyNeedles: [
127
+ "Parallel Research Fleet",
124
128
  "Architecture",
125
129
  "Data Flow",
126
130
  "Failure Modes and Mitigation",
@@ -186,11 +190,16 @@ export const DESIGN = {
186
190
  ],
187
191
  completionStatus: ["DONE", "DONE_WITH_CONCERNS", "BLOCKED"],
188
192
  crossStageTrace: {
189
- readsFrom: [".cclaw/artifacts/01-brainstorm.md", ".cclaw/artifacts/02-scope.md"],
193
+ readsFrom: [
194
+ ".cclaw/artifacts/01-brainstorm.md",
195
+ ".cclaw/artifacts/02-scope.md",
196
+ ".cclaw/artifacts/02a-research.md"
197
+ ],
190
198
  writesTo: [".cclaw/artifacts/03-design.md"],
191
199
  traceabilityRule: "Every architecture decision must trace to a scope boundary. Every downstream spec requirement must trace to a design decision."
192
200
  },
193
201
  artifactValidation: [
202
+ { section: "Research Fleet Synthesis", required: true, validationRule: "Must summarize all four lenses (stack/features/architecture/pitfalls) and map findings to concrete design decisions." },
194
203
  { section: "Codebase Investigation", required: false, validationRule: "Must list blast-radius files with current responsibilities and discovered patterns." },
195
204
  { section: "Search Before Building", required: false, validationRule: "For each technical choice: Layer 1 (exact match), Layer 2 (partial match), Layer 3 (inspiration), EUREKA labels with reuse-first default." },
196
205
  { section: "Architecture Boundaries", required: true, validationRule: "Must list component boundaries with ownership." },
@@ -152,6 +152,47 @@ inputs_hash: sha256:pending
152
152
  - Deferred:
153
153
  - Explicitly excluded:
154
154
 
155
+ ## Learnings
156
+ - None this stage.
157
+ `,
158
+ "02a-research.md": `---
159
+ stage: design
160
+ schema_version: 1
161
+ version: 0.18.0
162
+ feature: <feature-id>
163
+ locked_decisions: []
164
+ inputs_hash: sha256:pending
165
+ ---
166
+
167
+ # Research Report
168
+
169
+ ## Stack Analysis
170
+ | Topic | Finding | Evidence |
171
+ |---|---|---|
172
+ | Dependency compatibility | | |
173
+ | Alternatives/deprecations | | |
174
+
175
+ ## Features & Patterns
176
+ | Topic | Finding | Evidence |
177
+ |---|---|---|
178
+ | Domain conventions | | |
179
+ | UX/product patterns | | |
180
+
181
+ ## Architecture Options
182
+ | Option | Trade-offs | Recommendation | Evidence |
183
+ |---|---|---|---|
184
+ | A | | | |
185
+ | B | | | |
186
+
187
+ ## Pitfalls & Risks
188
+ | Risk | Impact | Mitigation | Evidence |
189
+ |---|---|---|---|
190
+ | | | | |
191
+
192
+ ## Synthesis
193
+ - Key decisions informed by research:
194
+ - Open questions:
195
+
155
196
  ## Learnings
156
197
  - None this stage.
157
198
  `,
@@ -178,6 +219,14 @@ inputs_hash: sha256:pending
178
219
  | Layer 2 | | |
179
220
  | Layer 3 | | |
180
221
 
222
+ ## Research Fleet Synthesis
223
+ | Lens | Key findings | Design impact | Evidence |
224
+ |---|---|---|---|
225
+ | stack-researcher | | | |
226
+ | features-researcher | | | |
227
+ | architecture-researcher | | | |
228
+ | pitfalls-researcher | | | |
229
+
181
230
  ## Architecture Boundaries
182
231
  | Component | Responsibility | Owner |
183
232
  |---|---|---|
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 {
@@ -1,11 +1,12 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { checkReviewVerdictConsistency, lintArtifact, validateReviewArmy } from "./artifact-linter.js";
3
+ import { checkReviewVerdictConsistency, extractMarkdownSectionBody, 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 = [
@@ -25,6 +26,23 @@ async function currentStageArtifactExists(projectRoot, stage, track) {
25
26
  return false;
26
27
  }
27
28
  }
29
+ async function readArtifactMarkdown(projectRoot, artifactFile) {
30
+ const candidates = [
31
+ path.join(projectRoot, RUNTIME_ROOT, "artifacts", artifactFile),
32
+ path.join(projectRoot, artifactFile)
33
+ ];
34
+ for (const candidate of candidates) {
35
+ if (!(await exists(candidate)))
36
+ continue;
37
+ try {
38
+ return await fs.readFile(candidate, "utf8");
39
+ }
40
+ catch {
41
+ // Try next location.
42
+ }
43
+ }
44
+ return null;
45
+ }
28
46
  function unique(values) {
29
47
  return [...new Set(values)];
30
48
  }
@@ -33,6 +51,102 @@ function sameStringArray(a, b) {
33
51
  return false;
34
52
  return a.every((value, index) => value === b[index]);
35
53
  }
54
+ const RECONCILIATION_NOTICES_FILE = "reconciliation-notices.json";
55
+ const RECONCILIATION_NOTICES_SCHEMA_VERSION = 1;
56
+ const DESIGN_RESEARCH_REQUIRED_SECTIONS = [
57
+ "Stack Analysis",
58
+ "Features & Patterns",
59
+ "Architecture Options",
60
+ "Pitfalls & Risks",
61
+ "Synthesis"
62
+ ];
63
+ export const RECONCILIATION_NOTICES_REL_PATH = `${RUNTIME_ROOT}/state/${RECONCILIATION_NOTICES_FILE}`;
64
+ function isFlowStageValue(value) {
65
+ return typeof value === "string" && FLOW_STAGES.includes(value);
66
+ }
67
+ function reconciliationNoticesPath(projectRoot) {
68
+ return path.join(projectRoot, RUNTIME_ROOT, "state", RECONCILIATION_NOTICES_FILE);
69
+ }
70
+ function defaultReconciliationNoticesPayload() {
71
+ return {
72
+ schemaVersion: RECONCILIATION_NOTICES_SCHEMA_VERSION,
73
+ notices: []
74
+ };
75
+ }
76
+ function sanitizeReconciliationNotice(raw) {
77
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
78
+ return null;
79
+ }
80
+ const typed = raw;
81
+ if (typeof typed.id !== "string" ||
82
+ typeof typed.runId !== "string" ||
83
+ !isFlowStageValue(typed.stage) ||
84
+ typeof typed.gateId !== "string" ||
85
+ typeof typed.reason !== "string" ||
86
+ typeof typed.demotedAt !== "string") {
87
+ return null;
88
+ }
89
+ return {
90
+ id: typed.id,
91
+ runId: typed.runId,
92
+ stage: typed.stage,
93
+ gateId: typed.gateId,
94
+ reason: typed.reason,
95
+ demotedAt: typed.demotedAt
96
+ };
97
+ }
98
+ export async function readReconciliationNotices(projectRoot) {
99
+ const filePath = reconciliationNoticesPath(projectRoot);
100
+ if (!(await exists(filePath))) {
101
+ return defaultReconciliationNoticesPayload();
102
+ }
103
+ try {
104
+ const raw = JSON.parse(await fs.readFile(filePath, "utf8"));
105
+ const notices = Array.isArray(raw.notices)
106
+ ? raw.notices
107
+ .map((value) => sanitizeReconciliationNotice(value))
108
+ .filter((value) => value !== null)
109
+ : [];
110
+ return {
111
+ schemaVersion: RECONCILIATION_NOTICES_SCHEMA_VERSION,
112
+ notices
113
+ };
114
+ }
115
+ catch {
116
+ return defaultReconciliationNoticesPayload();
117
+ }
118
+ }
119
+ async function writeReconciliationNotices(projectRoot, payload) {
120
+ const filePath = reconciliationNoticesPath(projectRoot);
121
+ await ensureDir(path.dirname(filePath));
122
+ await writeFileSafe(filePath, `${JSON.stringify({
123
+ schemaVersion: RECONCILIATION_NOTICES_SCHEMA_VERSION,
124
+ notices: payload.notices
125
+ }, null, 2)}\n`);
126
+ }
127
+ export function classifyReconciliationNotices(flowState, notices) {
128
+ const activeBlocked = [];
129
+ const currentStageBlocked = [];
130
+ const unsynced = [];
131
+ const staleRun = [];
132
+ for (const notice of notices) {
133
+ if (notice.runId !== flowState.activeRunId) {
134
+ staleRun.push(notice);
135
+ continue;
136
+ }
137
+ const stageCatalog = flowState.stageGateCatalog[notice.stage];
138
+ const blocked = stageCatalog.blocked.includes(notice.gateId);
139
+ if (!blocked) {
140
+ unsynced.push(notice);
141
+ continue;
142
+ }
143
+ activeBlocked.push(notice);
144
+ if (notice.stage === flowState.currentStage) {
145
+ currentStageBlocked.push(notice);
146
+ }
147
+ }
148
+ return { activeBlocked, currentStageBlocked, unsynced, staleRun };
149
+ }
36
150
  export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
37
151
  const stage = flowState.currentStage;
38
152
  const schema = stageSchema(stage, flowState.track);
@@ -132,6 +246,37 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
132
246
  }
133
247
  }
134
248
  }
249
+ if (stage === "design") {
250
+ const researchGateRequired = schema.requiredGates.some((gate) => gate.id === "design_research_complete" && gate.tier === "required");
251
+ if (researchGateRequired) {
252
+ const researchMarkdown = await readArtifactMarkdown(projectRoot, "02a-research.md");
253
+ if (!researchMarkdown) {
254
+ issues.push("design research gate blocked (design_research_complete): missing `.cclaw/artifacts/02a-research.md`.");
255
+ }
256
+ else {
257
+ const missingSections = [];
258
+ for (const section of DESIGN_RESEARCH_REQUIRED_SECTIONS) {
259
+ const body = extractMarkdownSectionBody(researchMarkdown, section);
260
+ if (body === null) {
261
+ missingSections.push(section);
262
+ continue;
263
+ }
264
+ const meaningfulLines = body
265
+ .split(/\r?\n/gu)
266
+ .map((line) => line.trim())
267
+ .filter((line) => line.length > 0)
268
+ .filter((line) => !/^\|?(?:[-:\s|])+$/u.test(line));
269
+ const nonPlaceholder = meaningfulLines.filter((line) => !/\b(?:TODO|TBD|FIXME|pending|<fill-in>)\b/iu.test(line));
270
+ if (nonPlaceholder.length === 0) {
271
+ missingSections.push(`${section} (empty or placeholder)`);
272
+ }
273
+ }
274
+ if (missingSections.length > 0) {
275
+ issues.push(`design research gate blocked (design_research_complete): ${missingSections.join(", ")}.`);
276
+ }
277
+ }
278
+ }
279
+ }
135
280
  }
136
281
  const passedSet = new Set(catalog.passed);
137
282
  const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
@@ -208,6 +353,7 @@ export function reconcileCurrentStageGateCatalog(flowState) {
208
353
  const allowedSet = new Set([...required, ...recommended]);
209
354
  const catalog = flowState.stageGateCatalog[stage];
210
355
  const notes = [];
356
+ const demotedGateIds = new Set();
211
357
  const before = {
212
358
  required: [...catalog.required],
213
359
  recommended: [...catalog.recommended],
@@ -248,6 +394,7 @@ export function reconcileCurrentStageGateCatalog(flowState) {
248
394
  continue;
249
395
  }
250
396
  passedSet.delete(gateId);
397
+ demotedGateIds.add(gateId);
251
398
  notes.push(`resolved overlap for "${gateId}" in favor of blocked (missing evidence)`);
252
399
  }
253
400
  for (const gateId of [...passedSet]) {
@@ -256,6 +403,7 @@ export function reconcileCurrentStageGateCatalog(flowState) {
256
403
  continue;
257
404
  passedSet.delete(gateId);
258
405
  blockedSet.add(gateId);
406
+ demotedGateIds.add(gateId);
259
407
  notes.push(`moved "${gateId}" from passed to blocked (missing evidence)`);
260
408
  }
261
409
  const after = {
@@ -288,6 +436,7 @@ export function reconcileCurrentStageGateCatalog(flowState) {
288
436
  changed,
289
437
  before,
290
438
  after,
439
+ demotedGateIds: [...required, ...recommended].filter((gateId) => demotedGateIds.has(gateId)),
291
440
  notes
292
441
  }
293
442
  };
@@ -295,8 +444,46 @@ export function reconcileCurrentStageGateCatalog(flowState) {
295
444
  export async function reconcileAndWriteCurrentStageGateCatalog(projectRoot) {
296
445
  const state = await readFlowState(projectRoot);
297
446
  const { nextState, reconciliation } = reconcileCurrentStageGateCatalog(state);
447
+ const effectiveState = reconciliation.changed ? nextState : state;
298
448
  if (reconciliation.changed) {
299
- await writeFlowState(projectRoot, nextState);
449
+ await writeFlowState(projectRoot, effectiveState);
450
+ }
451
+ const noticesPayload = await readReconciliationNotices(projectRoot);
452
+ let noticesChanged = false;
453
+ const noticeBuckets = classifyReconciliationNotices(effectiveState, noticesPayload.notices);
454
+ if (noticeBuckets.unsynced.length > 0 || noticeBuckets.staleRun.length > 0) {
455
+ const dropIds = new Set([...noticeBuckets.unsynced, ...noticeBuckets.staleRun].map((notice) => notice.id));
456
+ noticesPayload.notices = noticesPayload.notices.filter((notice) => !dropIds.has(notice.id));
457
+ noticesChanged = true;
458
+ }
459
+ if (reconciliation.demotedGateIds.length > 0) {
460
+ const existing = new Set(noticesPayload.notices.map((notice) => `${notice.runId}:${notice.stage}:${notice.gateId}`));
461
+ for (const gateId of reconciliation.demotedGateIds) {
462
+ const dedupeKey = `${effectiveState.activeRunId}:${reconciliation.stage}:${gateId}`;
463
+ if (existing.has(dedupeKey)) {
464
+ continue;
465
+ }
466
+ const ts = new Date().toISOString();
467
+ noticesPayload.notices.push({
468
+ id: `${dedupeKey}:${ts}`,
469
+ runId: effectiveState.activeRunId,
470
+ stage: reconciliation.stage,
471
+ gateId,
472
+ reason: "demoted from passed to blocked during gate reconciliation (missing evidence)",
473
+ demotedAt: ts
474
+ });
475
+ existing.add(dedupeKey);
476
+ noticesChanged = true;
477
+ }
478
+ }
479
+ if (noticesChanged) {
480
+ noticesPayload.notices.sort((a, b) => {
481
+ if (a.demotedAt === b.demotedAt) {
482
+ return a.id.localeCompare(b.id);
483
+ }
484
+ return a.demotedAt.localeCompare(b.demotedAt);
485
+ });
486
+ await writeReconciliationNotices(projectRoot, noticesPayload);
300
487
  }
301
488
  return {
302
489
  ...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.14",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {