cclaw-cli 6.1.1 → 6.3.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.
- package/README.md +2 -2
- package/dist/artifact-linter/brainstorm.js +13 -13
- package/dist/artifact-linter/design.js +5 -5
- package/dist/artifact-linter/scope.js +3 -3
- package/dist/artifact-linter/shared.d.ts +18 -19
- package/dist/artifact-linter/shared.js +34 -31
- package/dist/artifact-linter.js +4 -0
- package/dist/content/hooks.js +154 -2
- package/dist/content/skills-elicitation.js +8 -19
- package/dist/content/skills.js +1 -0
- package/dist/content/stage-schema.d.ts +3 -3
- package/dist/content/stage-schema.js +31 -6
- package/dist/content/stages/brainstorm.js +5 -5
- package/dist/content/stages/design.js +1 -1
- package/dist/content/stages/schema-types.d.ts +6 -0
- package/dist/content/stages/scope.js +2 -2
- package/dist/content/start-command.d.ts +2 -2
- package/dist/content/start-command.js +23 -18
- package/dist/content/subagents.js +1 -1
- package/dist/content/templates.d.ts +1 -1
- package/dist/content/templates.js +1 -0
- package/dist/delegation.js +2 -2
- package/dist/flow-state.d.ts +14 -1
- package/dist/flow-state.js +6 -1
- package/dist/gate-evidence.js +4 -3
- package/dist/internal/advance-stage/advance.js +20 -4
- package/dist/internal/advance-stage/parsers.d.ts +2 -1
- package/dist/internal/advance-stage/parsers.js +12 -1
- package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +21 -0
- package/dist/internal/advance-stage/proactive-delegation-trace.js +60 -0
- package/dist/internal/advance-stage/start-flow.d.ts +3 -1
- package/dist/internal/advance-stage/start-flow.js +81 -2
- package/dist/internal/advance-stage/verify.d.ts +0 -8
- package/dist/internal/advance-stage/verify.js +2 -30
- package/dist/run-persistence.js +37 -2
- package/dist/track-heuristics.d.ts +2 -2
- package/dist/track-heuristics.js +11 -6
- package/dist/types.d.ts +2 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ Legacy `.cclaw/runs/` directories are only auto-removed when empty. If the direc
|
|
|
68
68
|
|
|
69
69
|
That gives you:
|
|
70
70
|
|
|
71
|
-
- **One path** from idea to ship, with `
|
|
71
|
+
- **One path** from idea to ship, with one user-chosen discovery mode (`lean`, `guided`, `deep`) and internal `quick` / `medium` / `standard` tracks.
|
|
72
72
|
- **Real gates** for evidence, tests, review, delegation, stale-stage recovery, and closeout.
|
|
73
73
|
- **Subagents with accountability**: controller owns state, workers do bounded tasks, overseers validate, evidence lands in `delegation-log.json`.
|
|
74
74
|
- **Recovery instead of confusion**: `npx cclaw-cli sync` tells you blockers and next fixes.
|
|
@@ -101,7 +101,7 @@ medium brainstorm -> spec -> plan -> tdd -> review -> ship
|
|
|
101
101
|
standard brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
Track selection
|
|
104
|
+
At `/cc <idea>`, the user picks **one discovery mode** (`lean`, `guided`, `deep`) for upstream shaping. Track selection remains **model-guided and advisory** during start-up; runtime enforcement begins after state is written: subsequent `/cc` turns follow the selected internal track, persisted `discoveryMode`, required gates, delegation rules, stale-stage markers, and `closeout.shipSubstate`.
|
|
105
105
|
|
|
106
106
|
## When Blocked
|
|
107
107
|
|
|
@@ -20,7 +20,7 @@ export async function lintBrainstormStage(ctx) {
|
|
|
20
20
|
});
|
|
21
21
|
if (!brainstormShortCircuitActivated) {
|
|
22
22
|
const skipQuestions = ctx.activeStageFlags.includes("--skip-questions");
|
|
23
|
-
const floor = evaluateQaLogFloor(qaLogBody, track, "brainstorm", { skipQuestions });
|
|
23
|
+
const floor = evaluateQaLogFloor(qaLogBody, track, "brainstorm", { discoveryMode: ctx.discoveryMode, skipQuestions });
|
|
24
24
|
findings.push({
|
|
25
25
|
section: "qa_log_unconverged",
|
|
26
26
|
required: !floor.skipQuestionsAdvisory,
|
|
@@ -53,7 +53,7 @@ export async function lintBrainstormStage(ctx) {
|
|
|
53
53
|
const ok = hasDecisionLine;
|
|
54
54
|
findings.push({
|
|
55
55
|
section: "Approach Tier Classification",
|
|
56
|
-
required:
|
|
56
|
+
required: false,
|
|
57
57
|
rule: "Approach Tier must explicitly classify depth as one of `lite` (a.k.a. `Lightweight`), `Standard`, or `Deep`.",
|
|
58
58
|
found: ok,
|
|
59
59
|
details: ok
|
|
@@ -77,14 +77,14 @@ export async function lintBrainstormStage(ctx) {
|
|
|
77
77
|
});
|
|
78
78
|
findings.push({
|
|
79
79
|
section: "Approaches Role/Upside Taxonomy",
|
|
80
|
-
required:
|
|
80
|
+
required: false,
|
|
81
81
|
rule: "Approaches table must use canonical Role and Upside enum values.",
|
|
82
82
|
found: approachesTaxonomy.roleUpsideOk,
|
|
83
83
|
details: approachesTaxonomy.details
|
|
84
84
|
});
|
|
85
85
|
findings.push({
|
|
86
86
|
section: "Challenger Alternative Enforcement",
|
|
87
|
-
required:
|
|
87
|
+
required: false,
|
|
88
88
|
rule: "Approaches must include one challenger option with explicit high/higher upside.",
|
|
89
89
|
found: approachesTaxonomy.challengerOk,
|
|
90
90
|
details: approachesTaxonomy.details
|
|
@@ -96,7 +96,7 @@ export async function lintBrainstormStage(ctx) {
|
|
|
96
96
|
const orderOk = reactionIndex >= 0 && reactionIndex < directionIndex;
|
|
97
97
|
findings.push({
|
|
98
98
|
section: "Approach Reaction Ordering",
|
|
99
|
-
required:
|
|
99
|
+
required: false,
|
|
100
100
|
rule: "Approach Reaction must appear before Selected Direction (propose -> react -> recommend).",
|
|
101
101
|
found: orderOk,
|
|
102
102
|
details: orderOk
|
|
@@ -151,7 +151,7 @@ export async function lintBrainstormStage(ctx) {
|
|
|
151
151
|
const hasStatus = statusValue.length > 0;
|
|
152
152
|
findings.push({
|
|
153
153
|
section: "Short-Circuit Status",
|
|
154
|
-
required:
|
|
154
|
+
required: false,
|
|
155
155
|
rule: "Short-Circuit Decision must include a `Status:` line (`activated` or `bypassed`).",
|
|
156
156
|
found: hasStatus,
|
|
157
157
|
details: hasStatus
|
|
@@ -187,7 +187,7 @@ export async function lintBrainstormStage(ctx) {
|
|
|
187
187
|
const selfReview = validateCalibratedSelfReview(selfReviewBody);
|
|
188
188
|
findings.push({
|
|
189
189
|
section: "Calibrated Self-Review Format",
|
|
190
|
-
required:
|
|
190
|
+
required: false,
|
|
191
191
|
rule: "When Self-Review Notes are present, they must use the calibrated review prompt output shape.",
|
|
192
192
|
found: selfReview.ok,
|
|
193
193
|
details: selfReview.details
|
|
@@ -197,7 +197,7 @@ export async function lintBrainstormStage(ctx) {
|
|
|
197
197
|
if (criticPredictions !== null) {
|
|
198
198
|
findings.push({
|
|
199
199
|
section: "critic.predictions_missing",
|
|
200
|
-
required:
|
|
200
|
+
required: false,
|
|
201
201
|
rule: "[P2] critic.predictions_missing — pre-commitment predictions block missing or empty",
|
|
202
202
|
found: criticPredictions.found,
|
|
203
203
|
details: criticPredictions.details
|
|
@@ -227,7 +227,7 @@ export async function lintBrainstormStage(ctx) {
|
|
|
227
227
|
const ok = tokenMatches.size === 1 && !isPlaceholder;
|
|
228
228
|
findings.push({
|
|
229
229
|
section: "Mode Block Token",
|
|
230
|
-
required:
|
|
230
|
+
required: false,
|
|
231
231
|
rule: "Mode Block must declare exactly one mode token: STARTUP, BUILDER, ENGINEERING, OPS, or RESEARCH.",
|
|
232
232
|
found: ok,
|
|
233
233
|
details: ok
|
|
@@ -246,7 +246,7 @@ export async function lintBrainstormStage(ctx) {
|
|
|
246
246
|
/^RECOMMENDATION:/imu.test(raw)) {
|
|
247
247
|
findings.push({
|
|
248
248
|
section: "Approach Detail Cards",
|
|
249
|
-
required:
|
|
249
|
+
required: false,
|
|
250
250
|
rule: "Approach Detail Cards must include ≥2 `#### APPROACH <letter>` blocks each with Summary/Effort/Risk/Pros/Cons/Reuses.",
|
|
251
251
|
found: cardCount >= 2,
|
|
252
252
|
details: cardCount >= 2
|
|
@@ -257,7 +257,7 @@ export async function lintBrainstormStage(ctx) {
|
|
|
257
257
|
const hasRecommendation = recommendationLine !== null && recommendationLine[1] !== undefined && recommendationLine[1].trim().length > 0;
|
|
258
258
|
findings.push({
|
|
259
259
|
section: "Approach Recommendation Marker",
|
|
260
|
-
required:
|
|
260
|
+
required: false,
|
|
261
261
|
rule: "Approach Detail Cards must conclude with a single `RECOMMENDATION:` line citing the chosen letter and rationale.",
|
|
262
262
|
found: hasRecommendation,
|
|
263
263
|
details: hasRecommendation
|
|
@@ -272,7 +272,7 @@ export async function lintBrainstormStage(ctx) {
|
|
|
272
272
|
const optedOut = /\bnot used\b|\bn\/a\b|\bnone\b/iu.test(outsideVoiceBody);
|
|
273
273
|
findings.push({
|
|
274
274
|
section: "Outside Voice Slot Shape",
|
|
275
|
-
required:
|
|
275
|
+
required: false,
|
|
276
276
|
rule: "Outside Voice section must either declare opt-out (`not used`/`none`) or include `source:`, `prompt:`, `tension:`, `resolution:`.",
|
|
277
277
|
found: optedOut || missing.length === 0,
|
|
278
278
|
details: optedOut || missing.length === 0
|
|
@@ -337,7 +337,7 @@ export async function lintBrainstormStage(ctx) {
|
|
|
337
337
|
const waveDriftAddressed = hasCarryForwardSection && hasCarryForwardContent && hasDriftAuditMarkers;
|
|
338
338
|
findings.push({
|
|
339
339
|
section: "wave.drift_unaddressed",
|
|
340
|
-
required:
|
|
340
|
+
required: false,
|
|
341
341
|
rule: "[P1] wave.drift_unaddressed — when `.cclaw/wave-plans/` has >=2 entries, brainstorm must include `## Wave Carry-forward` with carry-forward and drift audit markers.",
|
|
342
342
|
found: waveDriftAddressed,
|
|
343
343
|
details: waveDriftAddressed
|
|
@@ -284,7 +284,7 @@ export async function lintDesignStage(ctx) {
|
|
|
284
284
|
});
|
|
285
285
|
{
|
|
286
286
|
const skipQuestions = activeStageFlags.includes("--skip-questions");
|
|
287
|
-
const floor = evaluateQaLogFloor(qaLogBody, track, "design", { skipQuestions });
|
|
287
|
+
const floor = evaluateQaLogFloor(qaLogBody, track, "design", { discoveryMode: ctx.discoveryMode, skipQuestions });
|
|
288
288
|
findings.push({
|
|
289
289
|
section: "qa_log_unconverged",
|
|
290
290
|
required: !floor.skipQuestionsAdvisory,
|
|
@@ -297,7 +297,7 @@ export async function lintDesignStage(ctx) {
|
|
|
297
297
|
if (criticPredictions !== null) {
|
|
298
298
|
findings.push({
|
|
299
299
|
section: "critic.predictions_missing",
|
|
300
|
-
required:
|
|
300
|
+
required: false,
|
|
301
301
|
rule: "[P2] critic.predictions_missing — pre-commitment predictions block missing or empty",
|
|
302
302
|
found: criticPredictions.found,
|
|
303
303
|
details: criticPredictions.details
|
|
@@ -390,7 +390,7 @@ export async function lintDesignStage(ctx) {
|
|
|
390
390
|
const ack = markdownFieldRegex("Iron rule acknowledged", "yes|true|y").test(regressionBody);
|
|
391
391
|
findings.push({
|
|
392
392
|
section: "Regression Iron Rule Acknowledgement",
|
|
393
|
-
required:
|
|
393
|
+
required: false,
|
|
394
394
|
rule: "Regression Iron Rule section must affirm `Iron rule acknowledged: yes`.",
|
|
395
395
|
found: ack,
|
|
396
396
|
details: ack
|
|
@@ -409,7 +409,7 @@ export async function lintDesignStage(ctx) {
|
|
|
409
409
|
const ok = isEmpty || validRows.length >= 1;
|
|
410
410
|
findings.push({
|
|
411
411
|
section: "Calibrated Finding Format",
|
|
412
|
-
required:
|
|
412
|
+
required: false,
|
|
413
413
|
rule: "Calibrated Findings must either declare `None this stage` or contain at least one finding in the form `[P1|P2|P3] (confidence: <n>/10) <path>[:<line>] — <description>`.",
|
|
414
414
|
found: ok,
|
|
415
415
|
details: isEmpty
|
|
@@ -423,7 +423,7 @@ export async function lintDesignStage(ctx) {
|
|
|
423
423
|
if (layeredDocumentReview !== null) {
|
|
424
424
|
findings.push({
|
|
425
425
|
section: "Document Reviewer Structured Findings",
|
|
426
|
-
required:
|
|
426
|
+
required: false,
|
|
427
427
|
rule: "When Layered review references coherence-reviewer/scope-guardian-reviewer/feasibility-reviewer, include explicit reviewer status plus calibrated finding lines.",
|
|
428
428
|
found: layeredDocumentReview.missingStructured.length === 0,
|
|
429
429
|
details: layeredDocumentReview.missingStructured.length === 0
|
|
@@ -23,7 +23,7 @@ export async function lintScopeStage(ctx) {
|
|
|
23
23
|
});
|
|
24
24
|
{
|
|
25
25
|
const skipQuestions = activeStageFlags.includes("--skip-questions");
|
|
26
|
-
const floor = evaluateQaLogFloor(qaLogBody, track, "scope", { skipQuestions });
|
|
26
|
+
const floor = evaluateQaLogFloor(qaLogBody, track, "scope", { discoveryMode: ctx.discoveryMode, skipQuestions });
|
|
27
27
|
findings.push({
|
|
28
28
|
section: "qa_log_unconverged",
|
|
29
29
|
required: !floor.skipQuestionsAdvisory,
|
|
@@ -94,7 +94,7 @@ export async function lintScopeStage(ctx) {
|
|
|
94
94
|
if (criticPredictions !== null) {
|
|
95
95
|
findings.push({
|
|
96
96
|
section: "critic.predictions_missing",
|
|
97
|
-
required:
|
|
97
|
+
required: false,
|
|
98
98
|
rule: "[P2] critic.predictions_missing — pre-commitment predictions block missing or empty",
|
|
99
99
|
found: criticPredictions.found,
|
|
100
100
|
details: criticPredictions.details
|
|
@@ -139,7 +139,7 @@ export async function lintScopeStage(ctx) {
|
|
|
139
139
|
}
|
|
140
140
|
findings.push({
|
|
141
141
|
section: "Locked Decisions ID Integrity",
|
|
142
|
-
required:
|
|
142
|
+
required: false,
|
|
143
143
|
rule: "Locked Decisions section must list each decision with a unique stable D-XX ID. (D-XX IDs replaced the legacy LD#<sha8> hash anchors in Wave 22.)",
|
|
144
144
|
found: issues.length === 0,
|
|
145
145
|
details: issues.length === 0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FlowStage, type FlowTrack } from "../types.js";
|
|
1
|
+
import { type DiscoveryMode, type FlowStage, type FlowTrack } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Stages that run adaptive elicitation. The `qa_log_unconverged` rule
|
|
4
4
|
* only fires for these. Other stages may still record a Q&A Log but no
|
|
@@ -20,6 +20,7 @@ export interface ForcingQuestionTopic {
|
|
|
20
20
|
topic: string;
|
|
21
21
|
}
|
|
22
22
|
export interface QaLogFloorOptions {
|
|
23
|
+
discoveryMode?: DiscoveryMode;
|
|
23
24
|
/**
|
|
24
25
|
* When true, downgrades the finding to advisory (`required: false`).
|
|
25
26
|
* Set when `--skip-questions` was persisted to the active stage flags.
|
|
@@ -97,25 +98,22 @@ export declare function extractForcingQuestions(stage: FlowStage): ForcingQuesti
|
|
|
97
98
|
* design artifact. Returns ok=true when convergence is reached or any
|
|
98
99
|
* escape hatch fires.
|
|
99
100
|
*
|
|
100
|
-
* Convergence sources (any one
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* - `--skip-questions`
|
|
109
|
-
*
|
|
110
|
-
* -
|
|
111
|
-
* refactor) AND the artifact has at least one substantive row — treat
|
|
112
|
-
* as converged because there is nothing left to force.
|
|
101
|
+
* Convergence sources (any one can set ok=true — see also
|
|
102
|
+
* `adaptiveElicitationSkillMarkdown`):
|
|
103
|
+
* - Every forcing-question topic id from the stage checklist is tagged
|
|
104
|
+
* `[topic:<id>]` on at least one `## Q&A Log` row.
|
|
105
|
+
* - Ralph-Loop path: last 2 substantive rows read as no-new-decisions,
|
|
106
|
+
* substantive count ≥ max(2, questionBudgetHint(discoveryMode, stage).min),
|
|
107
|
+
* and not (guided/deep discovery with pending forcing-topic ids).
|
|
108
|
+
* - Stop-signal row (`QA_LOG_STOP_SIGNAL_PATTERNS`).
|
|
109
|
+
* - `--skip-questions` (`options.skipQuestions`): ok remains false but
|
|
110
|
+
* `skipQuestionsAdvisory` is true (linter treats as non-blocking).
|
|
111
|
+
* - No forcing-questions row in the checklist and ≥1 substantive row.
|
|
113
112
|
*
|
|
114
|
-
* Wave 23
|
|
115
|
-
* `
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* harness UI compatibility but are always 0/false.
|
|
113
|
+
* Wave 23 retired the fixed English-only count floor; Wave 24 made
|
|
114
|
+
* `[topic:<id>]` mandatory for topic coverage. The `min` and
|
|
115
|
+
* `liteShortCircuit` fields stay for harness compatibility (min is always 0;
|
|
116
|
+
* liteShortCircuit false).
|
|
119
117
|
*/
|
|
120
118
|
export declare function evaluateQaLogFloor(qaLogBody: string | null, track: FlowTrack, stage: FlowStage, options?: QaLogFloorOptions): QaLogFloorResult;
|
|
121
119
|
export interface LintFinding {
|
|
@@ -448,6 +446,7 @@ export interface StageLintContext {
|
|
|
448
446
|
projectRoot: string;
|
|
449
447
|
stage: FlowStage;
|
|
450
448
|
track: FlowTrack;
|
|
449
|
+
discoveryMode: DiscoveryMode;
|
|
451
450
|
raw: string;
|
|
452
451
|
absFile: string;
|
|
453
452
|
sections: H2SectionMap;
|
|
@@ -207,25 +207,22 @@ function lastTwoRowsAllNoDecision(substantiveRows) {
|
|
|
207
207
|
* design artifact. Returns ok=true when convergence is reached or any
|
|
208
208
|
* escape hatch fires.
|
|
209
209
|
*
|
|
210
|
-
* Convergence sources (any one
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* - `--skip-questions`
|
|
219
|
-
*
|
|
220
|
-
* -
|
|
221
|
-
* refactor) AND the artifact has at least one substantive row — treat
|
|
222
|
-
* as converged because there is nothing left to force.
|
|
210
|
+
* Convergence sources (any one can set ok=true — see also
|
|
211
|
+
* `adaptiveElicitationSkillMarkdown`):
|
|
212
|
+
* - Every forcing-question topic id from the stage checklist is tagged
|
|
213
|
+
* `[topic:<id>]` on at least one `## Q&A Log` row.
|
|
214
|
+
* - Ralph-Loop path: last 2 substantive rows read as no-new-decisions,
|
|
215
|
+
* substantive count ≥ max(2, questionBudgetHint(discoveryMode, stage).min),
|
|
216
|
+
* and not (guided/deep discovery with pending forcing-topic ids).
|
|
217
|
+
* - Stop-signal row (`QA_LOG_STOP_SIGNAL_PATTERNS`).
|
|
218
|
+
* - `--skip-questions` (`options.skipQuestions`): ok remains false but
|
|
219
|
+
* `skipQuestionsAdvisory` is true (linter treats as non-blocking).
|
|
220
|
+
* - No forcing-questions row in the checklist and ≥1 substantive row.
|
|
223
221
|
*
|
|
224
|
-
* Wave 23
|
|
225
|
-
* `
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* harness UI compatibility but are always 0/false.
|
|
222
|
+
* Wave 23 retired the fixed English-only count floor; Wave 24 made
|
|
223
|
+
* `[topic:<id>]` mandatory for topic coverage. The `min` and
|
|
224
|
+
* `liteShortCircuit` fields stay for harness compatibility (min is always 0;
|
|
225
|
+
* liteShortCircuit false).
|
|
229
226
|
*/
|
|
230
227
|
export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
|
|
231
228
|
const rows = qaLogBody !== null ? getMarkdownTableRows(qaLogBody) : [];
|
|
@@ -233,6 +230,7 @@ export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
|
|
|
233
230
|
const count = substantiveRows.length;
|
|
234
231
|
const hasStopSignal = detectStopSignal(rows);
|
|
235
232
|
const skipQuestionsAdvisory = options.skipQuestions === true;
|
|
233
|
+
const discoveryMode = options.discoveryMode ?? (track === "quick" ? "lean" : "guided");
|
|
236
234
|
const forcingTopics = (options.forcingQuestions ?? extractForcingQuestions(stage)).map((entry) => (typeof entry === "string" ? { id: entry, topic: entry } : entry));
|
|
237
235
|
const forcingCovered = [];
|
|
238
236
|
const forcingPending = [];
|
|
@@ -242,9 +240,13 @@ export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
|
|
|
242
240
|
else
|
|
243
241
|
forcingPending.push(topic.id);
|
|
244
242
|
}
|
|
243
|
+
const budget = questionBudgetHint(discoveryMode, stage);
|
|
245
244
|
const noNewDecisions = lastTwoRowsAllNoDecision(substantiveRows);
|
|
246
245
|
const allForcingCovered = forcingTopics.length > 0 ? forcingPending.length === 0 : count >= 1;
|
|
247
|
-
const
|
|
246
|
+
const minimumRowsReached = count >= Math.max(2, budget.min);
|
|
247
|
+
const riskEscalationNeeded = forcingPending.length > 0 && /^(guided|deep)$/u.test(discoveryMode);
|
|
248
|
+
const noNewDecisionConverged = noNewDecisions && minimumRowsReached && !riskEscalationNeeded;
|
|
249
|
+
const ok = allForcingCovered || noNewDecisionConverged || hasStopSignal;
|
|
248
250
|
const pendingIdsBracket = forcingPending.length > 0
|
|
249
251
|
? `[${forcingPending.join(", ")}]`
|
|
250
252
|
: "[none]";
|
|
@@ -256,10 +258,10 @@ export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
|
|
|
256
258
|
else if (allForcingCovered) {
|
|
257
259
|
details = `Q&A Log converged: stage exposes no forcing-questions row and ${count} substantive entry recorded.`;
|
|
258
260
|
}
|
|
259
|
-
else if (
|
|
261
|
+
else if (noNewDecisionConverged) {
|
|
260
262
|
const remaining = forcingPending.length > 0
|
|
261
|
-
? ` ${forcingPending.length} forcing topic IDs still pending: ${pendingIdsBracket}
|
|
262
|
-
:
|
|
263
|
+
? ` ${forcingPending.length} forcing topic IDs still pending: ${pendingIdsBracket} after the minimum ${budget.min}-row discovery pass.`
|
|
264
|
+
: ` Ralph-Loop convergence detector says no new decision-changing rows in the last 2 turns after the minimum ${budget.min}-row discovery pass.`;
|
|
263
265
|
details = `Q&A Log converged via no-new-decisions detector at ${count} row(s).${remaining}`;
|
|
264
266
|
}
|
|
265
267
|
else {
|
|
@@ -269,27 +271,28 @@ export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
|
|
|
269
271
|
else if (skipQuestionsAdvisory) {
|
|
270
272
|
details = `Q&A Log unconverged at ${count} row(s); --skip-questions flag downgraded the finding to advisory. Forcing topic IDs pending: ${pendingIdsBracket}.`;
|
|
271
273
|
}
|
|
274
|
+
else if (noNewDecisions && !minimumRowsReached) {
|
|
275
|
+
details = `Q&A Log still below the minimum ${budget.min}-row ${discoveryMode} discovery pass (${count} substantive row(s)). Forcing topic IDs pending: ${pendingIdsBracket}. Continue asking decision-changing questions before drafting.`;
|
|
276
|
+
}
|
|
277
|
+
else if (riskEscalationNeeded && noNewDecisions) {
|
|
278
|
+
details = `Q&A Log cannot converge via Ralph-Loop yet because ${discoveryMode} mode keeps pending forcing topic IDs blocking: ${pendingIdsBracket}. Cover the remaining topics or record an explicit stop-signal row.`;
|
|
279
|
+
}
|
|
272
280
|
else {
|
|
273
|
-
details = `Q&A Log unconverged at ${count} row(s). Forcing topic IDs pending: ${pendingIdsBracket}. Tag each Q&A row with \`[topic:<id>]\` to mark coverage,
|
|
281
|
+
details = `Q&A Log unconverged at ${count} row(s). Forcing topic IDs pending: ${pendingIdsBracket}. Tag each Q&A row with \`[topic:<id>]\` to mark coverage, complete the minimum ${budget.min}-row ${discoveryMode} discovery pass, or record an explicit user stop-signal row.`;
|
|
274
282
|
}
|
|
275
|
-
|
|
276
|
-
// blocking count. `recommended` is the soft budget per track/stage.
|
|
277
|
-
const advisoryBudget = questionBudgetHint(track, stage).recommended;
|
|
283
|
+
const advisoryBudget = budget.recommended;
|
|
278
284
|
return {
|
|
279
285
|
ok,
|
|
280
286
|
count,
|
|
281
|
-
// Wave 23: floor no longer enforces a count. Surfacing 0 keeps the
|
|
282
|
-
// QaLogFloorSignal shape stable for harness consumers; harness UIs
|
|
283
|
-
// may show `recommended` from `questionBudgetHint` separately.
|
|
284
287
|
min: 0,
|
|
285
288
|
hasStopSignal,
|
|
286
289
|
liteShortCircuit: false,
|
|
287
290
|
skipQuestionsAdvisory,
|
|
288
291
|
forcingCovered,
|
|
289
292
|
forcingPending,
|
|
290
|
-
noNewDecisions,
|
|
293
|
+
noNewDecisions: noNewDecisionConverged,
|
|
291
294
|
details: advisoryBudget > 0
|
|
292
|
-
? `${details} (advisory budget for ${
|
|
295
|
+
? `${details} (advisory budget for ${discoveryMode}/${stage}: ~${advisoryBudget} Q&A turns)`
|
|
293
296
|
: details
|
|
294
297
|
};
|
|
295
298
|
}
|
package/dist/artifact-linter.js
CHANGED
|
@@ -114,6 +114,7 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
114
114
|
// Same flow-state read powers the post-loop demotion + audit log
|
|
115
115
|
// below; we cache the result here to avoid two disk reads.
|
|
116
116
|
let activeStageFlags = [];
|
|
117
|
+
let discoveryMode = "guided";
|
|
117
118
|
let taskClass = null;
|
|
118
119
|
let activeRunId = null;
|
|
119
120
|
try {
|
|
@@ -121,11 +122,13 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
121
122
|
const hint = flowState.interactionHints?.[stage];
|
|
122
123
|
if (hint?.skipQuestions === true)
|
|
123
124
|
activeStageFlags.push("--skip-questions");
|
|
125
|
+
discoveryMode = flowState.discoveryMode ?? "guided";
|
|
124
126
|
taskClass = flowState.taskClass ?? null;
|
|
125
127
|
activeRunId = flowState.activeRunId ?? null;
|
|
126
128
|
}
|
|
127
129
|
catch {
|
|
128
130
|
activeStageFlags = [];
|
|
131
|
+
discoveryMode = "guided";
|
|
129
132
|
taskClass = null;
|
|
130
133
|
activeRunId = null;
|
|
131
134
|
}
|
|
@@ -195,6 +198,7 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
195
198
|
projectRoot,
|
|
196
199
|
stage,
|
|
197
200
|
track,
|
|
201
|
+
discoveryMode,
|
|
198
202
|
raw,
|
|
199
203
|
absFile,
|
|
200
204
|
sections,
|
package/dist/content/hooks.js
CHANGED
|
@@ -185,13 +185,13 @@ void main();
|
|
|
185
185
|
`;
|
|
186
186
|
}
|
|
187
187
|
export function startFlowScript() {
|
|
188
|
-
return internalHelperScript("start-flow", "start-flow", "Usage: node " + RUNTIME_ROOT + "/hooks/start-flow.mjs --track=<standard|medium|quick> [--class=...] [--prompt=...] [--stack=...] [--reason=...] [--reclassify] [--force-reset]", { defaultQuietEnvVar: "CCLAW_START_FLOW_QUIET" });
|
|
188
|
+
return internalHelperScript("start-flow", "start-flow", "Usage: node " + RUNTIME_ROOT + "/hooks/start-flow.mjs --track=<standard|medium|quick> [--discovery-mode=<lean|guided|deep>] [--class=...] [--prompt=...] [--stack=...] [--reason=...] [--reclassify] [--force-reset]", { defaultQuietEnvVar: "CCLAW_START_FLOW_QUIET" });
|
|
189
189
|
}
|
|
190
190
|
export function cancelRunScript() {
|
|
191
191
|
return internalHelperScript("cancel-run", "cancel-run", "Usage: node " + RUNTIME_ROOT + "/hooks/cancel-run.mjs --reason=<text> [--disposition=<cancelled|abandoned>] [--name=<slug>]");
|
|
192
192
|
}
|
|
193
193
|
export function stageCompleteScript() {
|
|
194
|
-
return internalHelperScript("stage-complete", "advance-stage", "Usage: node " + RUNTIME_ROOT + "/hooks/stage-complete.mjs <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...] [--accept-proactive-waiver] [--accept-proactive-waiver-reason
|
|
194
|
+
return internalHelperScript("stage-complete", "advance-stage", "Usage: node " + RUNTIME_ROOT + "/hooks/stage-complete.mjs <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...] [--accept-proactive-waiver] [--accept-proactive-waiver-reason=\"<why safe>\"] [--skip-questions] [--json]", {
|
|
195
195
|
positionalArgName: "stage",
|
|
196
196
|
positionalArgRequired: true,
|
|
197
197
|
defaultQuietEnvVar: "CCLAW_STAGE_COMPLETE_QUIET"
|
|
@@ -296,6 +296,7 @@ function usage() {
|
|
|
296
296
|
"Usage:",
|
|
297
297
|
" node .cclaw/hooks/delegation-record.mjs --stage=<stage> --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<id> [--dispatch-id=<id>] [--worker-run-id=<id>] [--dispatch-surface=<surface>] [--agent-definition-path=<path>] [--ack-ts=<iso>] [--launched-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--waiver-reason=<text>] [--json]",
|
|
298
298
|
" node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path> [--ack-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--json]",
|
|
299
|
+
" node .cclaw/hooks/delegation-record.mjs --repair --span-id=<id> --repair-reason=\"<why>\" [--json]",
|
|
299
300
|
"",
|
|
300
301
|
"Allowed --dispatch-surface values:",
|
|
301
302
|
" " + VALID_DISPATCH_SURFACES.join(", "),
|
|
@@ -496,10 +497,161 @@ async function runRerecord(args, json) {
|
|
|
496
497
|
process.stdout.write(JSON.stringify({ ok: true, event, rerecord: true }, null, 2) + "\\n");
|
|
497
498
|
}
|
|
498
499
|
|
|
500
|
+
const LIFECYCLE_PHASES = ["scheduled", "launched", "acknowledged", "completed"];
|
|
501
|
+
|
|
502
|
+
function mergeSpanTemplate(spanEvents) {
|
|
503
|
+
const base = {};
|
|
504
|
+
const keys = [
|
|
505
|
+
"stage",
|
|
506
|
+
"agent",
|
|
507
|
+
"mode",
|
|
508
|
+
"runId",
|
|
509
|
+
"dispatchId",
|
|
510
|
+
"dispatchSurface",
|
|
511
|
+
"agentDefinitionPath",
|
|
512
|
+
"workerRunId",
|
|
513
|
+
"fulfillmentMode",
|
|
514
|
+
"schemaVersion",
|
|
515
|
+
"parentSpanId",
|
|
516
|
+
"evidenceRefs",
|
|
517
|
+
"waiverReason"
|
|
518
|
+
];
|
|
519
|
+
for (const e of spanEvents) {
|
|
520
|
+
if (!e || typeof e !== "object") continue;
|
|
521
|
+
for (const k of keys) {
|
|
522
|
+
if (base[k] === undefined && e[k] !== undefined) {
|
|
523
|
+
base[k] = e[k];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return base;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function repairFulfillmentMode(base) {
|
|
531
|
+
if (base.fulfillmentMode) return base.fulfillmentMode;
|
|
532
|
+
if (base.dispatchSurface === "role-switch") return "role-switch";
|
|
533
|
+
if (base.dispatchSurface === "cursor-task" || base.dispatchSurface === "generic-task") {
|
|
534
|
+
return "generic-dispatch";
|
|
535
|
+
}
|
|
536
|
+
return "isolated";
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function runRepair(args, json) {
|
|
540
|
+
const problems = [];
|
|
541
|
+
if (!args["span-id"]) problems.push("repair mode requires --span-id");
|
|
542
|
+
if (!args["repair-reason"] || String(args["repair-reason"]).trim().length === 0) {
|
|
543
|
+
problems.push("repair mode requires --repair-reason=<text>");
|
|
544
|
+
}
|
|
545
|
+
if (problems.length > 0) {
|
|
546
|
+
emitProblems(problems, json, 2);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const spanId = args["span-id"];
|
|
550
|
+
const repairedReason = String(args["repair-reason"]).trim();
|
|
551
|
+
const root = await detectRoot();
|
|
552
|
+
const events = await readDelegationEvents(root);
|
|
553
|
+
const spanEvents = events.filter(
|
|
554
|
+
(e) => e && e.spanId === spanId && typeof e.event === "string" && LIFECYCLE_PHASES.includes(e.event)
|
|
555
|
+
);
|
|
556
|
+
if (spanEvents.length === 0) {
|
|
557
|
+
emitProblems(
|
|
558
|
+
["repair refused: no lifecycle delegation-events.jsonl rows found for --span-id=" + spanId],
|
|
559
|
+
json,
|
|
560
|
+
2
|
|
561
|
+
);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const present = new Set(spanEvents.map((e) => e.event));
|
|
565
|
+
const base = mergeSpanTemplate(spanEvents);
|
|
566
|
+
if (!base.stage || !base.agent || !base.mode) {
|
|
567
|
+
emitProblems(["repair refused: span events missing stage/agent/mode to clone"], json, 2);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const runId =
|
|
571
|
+
typeof base.runId === "string" && base.runId.length > 0 ? base.runId : await readRunId(root);
|
|
572
|
+
const fulfillmentMode = repairFulfillmentMode(base);
|
|
573
|
+
const schemaVersion =
|
|
574
|
+
typeof base.schemaVersion === "number" && base.schemaVersion > 0
|
|
575
|
+
? base.schemaVersion
|
|
576
|
+
: LEDGER_SCHEMA_VERSION;
|
|
577
|
+
const evidenceRefs = Array.isArray(base.evidenceRefs)
|
|
578
|
+
? base.evidenceRefs.filter((r) => typeof r === "string" && r.trim().length > 0)
|
|
579
|
+
: [];
|
|
580
|
+
const now = new Date().toISOString();
|
|
581
|
+
const appended = [];
|
|
582
|
+
|
|
583
|
+
for (const status of LIFECYCLE_PHASES) {
|
|
584
|
+
if (present.has(status)) continue;
|
|
585
|
+
if (status === "completed" && base.dispatchSurface !== "role-switch") {
|
|
586
|
+
if (!base.dispatchId || !base.dispatchSurface || !base.agentDefinitionPath) {
|
|
587
|
+
emitProblems(
|
|
588
|
+
[
|
|
589
|
+
"repair refused: cannot synthesize completed row without dispatchId, dispatchSurface, and agentDefinitionPath on span " +
|
|
590
|
+
spanId
|
|
591
|
+
],
|
|
592
|
+
json,
|
|
593
|
+
2
|
|
594
|
+
);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (status === "completed" && base.dispatchSurface === "role-switch" && evidenceRefs.length === 0) {
|
|
599
|
+
emitProblems(
|
|
600
|
+
["repair refused: role-switch completed synthesis requires evidenceRefs on span " + spanId],
|
|
601
|
+
json,
|
|
602
|
+
2
|
|
603
|
+
);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const launchedTs =
|
|
607
|
+
status === "launched" || status === "acknowledged" || status === "completed" ? now : undefined;
|
|
608
|
+
const ackTs = status === "acknowledged" || status === "completed" ? now : undefined;
|
|
609
|
+
const completedTs = status === "completed" ? now : undefined;
|
|
610
|
+
const endTs = status === "completed" ? now : undefined;
|
|
611
|
+
const row = {
|
|
612
|
+
stage: base.stage,
|
|
613
|
+
agent: base.agent,
|
|
614
|
+
mode: base.mode,
|
|
615
|
+
status,
|
|
616
|
+
spanId,
|
|
617
|
+
dispatchId: base.dispatchId,
|
|
618
|
+
workerRunId: base.workerRunId,
|
|
619
|
+
dispatchSurface: base.dispatchSurface,
|
|
620
|
+
agentDefinitionPath: base.agentDefinitionPath,
|
|
621
|
+
fulfillmentMode,
|
|
622
|
+
evidenceRefs,
|
|
623
|
+
runId,
|
|
624
|
+
startTs: now,
|
|
625
|
+
ts: now,
|
|
626
|
+
launchedTs,
|
|
627
|
+
ackTs,
|
|
628
|
+
completedTs,
|
|
629
|
+
endTs,
|
|
630
|
+
schemaVersion
|
|
631
|
+
};
|
|
632
|
+
const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
|
|
633
|
+
const event = { ...clean, event: status, eventTs: now, repairedAt: now, repairedReason };
|
|
634
|
+
await persistEntry(root, runId, clean, event);
|
|
635
|
+
present.add(status);
|
|
636
|
+
appended.push(status);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (json) {
|
|
640
|
+
process.stdout.write(
|
|
641
|
+
JSON.stringify({ ok: true, repair: true, spanId, appended, repairedAt: now, repairedReason }, null, 2) + "\\n"
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
499
646
|
async function main() {
|
|
500
647
|
const args = parseArgs(process.argv.slice(2));
|
|
501
648
|
const json = args.json !== undefined;
|
|
502
649
|
|
|
650
|
+
if (args.repair) {
|
|
651
|
+
await runRepair(args, json);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
503
655
|
if (args.rerecord) {
|
|
504
656
|
await runRerecord(args, json);
|
|
505
657
|
return;
|