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.
- package/dist/content/next-command.js +16 -7
- package/dist/content/research-playbooks.js +36 -0
- package/dist/content/stage-schema.js +8 -1
- package/dist/content/stages/design.js +14 -5
- package/dist/content/templates.js +49 -0
- package/dist/doctor.js +18 -1
- package/dist/gate-evidence.d.ts +23 -1
- package/dist/gate-evidence.js +190 -3
- package/dist/install.js +4 -0
- package/dist/run-archive.js +2 -0
- package/package.json +1 -1
|
@@ -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.
|
|
44
|
-
5. Let \`
|
|
45
|
-
6.
|
|
46
|
-
7.
|
|
47
|
-
8.
|
|
48
|
-
9. If
|
|
49
|
-
10. If
|
|
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: [
|
|
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: [
|
|
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 : [];
|
package/dist/gate-evidence.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FlowState, StageGateState } from "./flow-state.js";
|
|
2
|
-
import type
|
|
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 {
|
package/dist/gate-evidence.js
CHANGED
|
@@ -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,
|
|
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({
|
package/dist/run-archive.js
CHANGED
|
@@ -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();
|