cclaw-cli 4.0.0 → 5.0.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.
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { checkCriticPredictionsContract, evaluateQaLogFloor, sectionBodyByName, validateApproachesTaxonomy, headingLineIndex, meaningfulLineCount, getMarkdownTableRows, parseShortCircuitStatus, validateCalibratedSelfReview, markdownFieldRegex } from "./shared.js";
4
+ import { readFlowState } from "../run-persistence.js";
4
5
  export async function lintBrainstormStage(ctx) {
5
6
  const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
6
7
  const qaLogBody = sectionBodyByName(sections, "Q&A Log");
@@ -21,9 +22,9 @@ export async function lintBrainstormStage(ctx) {
21
22
  const skipQuestions = ctx.activeStageFlags.includes("--skip-questions");
22
23
  const floor = evaluateQaLogFloor(qaLogBody, track, "brainstorm", { skipQuestions });
23
24
  findings.push({
24
- section: "qa_log_below_min",
25
+ section: "qa_log_unconverged",
25
26
  required: !floor.skipQuestionsAdvisory,
26
- rule: "[P1] qa_log_below_min — Q&A Log below the adaptive elicitation floor for this track. Continue the loop or record an explicit user stop-signal row.",
27
+ rule: "[P1] qa_log_unconverged — Q&A Log has not converged for this stage. Continue elicitation until forcing-question topics are addressed, the last 2 rows produce no decision-changing impact (Ralph-Loop), or an explicit user stop-signal row is appended.",
27
28
  found: floor.ok,
28
29
  details: floor.details
29
30
  });
@@ -279,6 +280,43 @@ export async function lintBrainstormStage(ctx) {
279
280
  : `Outside Voice section is missing field(s): ${missing.join(", ")}.`
280
281
  });
281
282
  }
283
+ let ideaHint = {};
284
+ try {
285
+ const flowState = await readFlowState(projectRoot);
286
+ const hint = flowState.interactionHints?.brainstorm;
287
+ if (hint) {
288
+ ideaHint = {
289
+ fromIdeaArtifact: hint.fromIdeaArtifact,
290
+ fromIdeaCandidateId: hint.fromIdeaCandidateId
291
+ };
292
+ }
293
+ }
294
+ catch {
295
+ ideaHint = {};
296
+ }
297
+ if (ideaHint.fromIdeaArtifact) {
298
+ const carryBody = sectionBodyByName(sections, "Idea Evidence Carry-forward");
299
+ const hasSection = carryBody !== null && meaningfulLineCount(carryBody) > 0;
300
+ const sourceCited = hasSection &&
301
+ carryBody.includes(ideaHint.fromIdeaArtifact);
302
+ const candidateCited = ideaHint.fromIdeaCandidateId
303
+ ? hasSection && carryBody.toUpperCase().includes(ideaHint.fromIdeaCandidateId.toUpperCase())
304
+ : true;
305
+ const ok = hasSection && sourceCited && candidateCited;
306
+ findings.push({
307
+ section: "brainstorm.idea_evidence_carry_forward",
308
+ required: true,
309
+ rule: "[P1] brainstorm.idea_evidence_carry_forward — when `flow-state.interactionHints.brainstorm.fromIdeaArtifact` is set (Wave 23 / v5.0.0), the brainstorm artifact MUST include `## Idea Evidence Carry-forward` citing the idea artifact path and chosen `I-#`. Reuse divergent + critique + rank work from `/cc-ideate` as the `baseline` Approach; only newly generate the higher-upside challenger row(s).",
310
+ found: ok,
311
+ details: ok
312
+ ? `Idea Evidence Carry-forward cites ${ideaHint.fromIdeaArtifact}${ideaHint.fromIdeaCandidateId ? ` (${ideaHint.fromIdeaCandidateId})` : ""}.`
313
+ : !hasSection
314
+ ? `Brainstorm started from /cc-ideate (artifact ${ideaHint.fromIdeaArtifact}${ideaHint.fromIdeaCandidateId ? `, candidate ${ideaHint.fromIdeaCandidateId}` : ""}) but \`## Idea Evidence Carry-forward\` is missing or empty.`
315
+ : !sourceCited
316
+ ? `\`## Idea Evidence Carry-forward\` does not cite the source idea artifact path \`${ideaHint.fromIdeaArtifact}\`.`
317
+ : `\`## Idea Evidence Carry-forward\` does not cite the chosen candidate id \`${ideaHint.fromIdeaCandidateId ?? ""}\`.`
318
+ });
319
+ }
282
320
  const wavePlansDir = path.join(projectRoot, ".cclaw", "wave-plans");
283
321
  let wavePlanEntries = [];
284
322
  try {
@@ -222,9 +222,9 @@ export async function lintDesignStage(ctx) {
222
222
  const skipQuestions = activeStageFlags.includes("--skip-questions");
223
223
  const floor = evaluateQaLogFloor(qaLogBody, track, "design", { skipQuestions });
224
224
  findings.push({
225
- section: "qa_log_below_min",
225
+ section: "qa_log_unconverged",
226
226
  required: !floor.skipQuestionsAdvisory,
227
- rule: "[P1] qa_log_below_min — Q&A Log below the adaptive elicitation floor for this track. Continue the loop or record an explicit user stop-signal row.",
227
+ rule: "[P1] qa_log_unconverged — Q&A Log has not converged for this stage. Continue elicitation until forcing-question topics are addressed, the last 2 rows produce no decision-changing impact (Ralph-Loop), or an explicit user stop-signal row is appended.",
228
228
  found: floor.ok,
229
229
  details: floor.details
230
230
  });
@@ -21,4 +21,29 @@ export interface ReviewSecurityNoChangeAttestationResult {
21
21
  * APPROVED while open Critical findings or shipBlockers remain.
22
22
  */
23
23
  export declare function checkReviewVerdictConsistency(projectRoot: string): Promise<ReviewVerdictConsistencyResult>;
24
+ export interface ReviewTddDuplicationConflict {
25
+ findingId: string;
26
+ tddSeverity: string | null;
27
+ reviewSeverity: string | null;
28
+ tddDisposition: string | null;
29
+ reviewDisposition: string | null;
30
+ }
31
+ export interface ReviewTddDuplicationResult {
32
+ ok: boolean;
33
+ errors: string[];
34
+ conflicts: ReviewTddDuplicationConflict[];
35
+ tddArtifactExists: boolean;
36
+ reviewArtifactExists: boolean;
37
+ }
38
+ /**
39
+ * Cross-artifact duplication guard (Wave 23 / v5.0.0).
40
+ *
41
+ * When the same finding ID (`F-NN`) appears in both
42
+ * `06-tdd.md > Per-Slice Review` and `07-review-army.json`, the
43
+ * severity and disposition MUST match. Per-slice tdd reviews own
44
+ * single-slice findings; review cites them, never re-classifies.
45
+ *
46
+ * If neither artifact uses `F-NN` IDs, the check is a no-op.
47
+ */
48
+ export declare function checkReviewTddNoCrossArtifactDuplication(projectRoot: string): Promise<ReviewTddDuplicationResult>;
24
49
  export declare function checkReviewSecurityNoChangeAttestation(projectRoot: string): Promise<ReviewSecurityNoChangeAttestationResult>;
@@ -319,6 +319,161 @@ export async function checkReviewVerdictConsistency(projectRoot) {
319
319
  shipBlockerCount
320
320
  };
321
321
  }
322
+ const FINDING_ID_PATTERN = /\bF-\d+\b/giu;
323
+ const SEVERITY_TOKENS = ["Critical", "Important", "Suggestion"];
324
+ const DISPOSITION_TOKENS = ["open", "accepted", "resolved", "deferred", "won't-fix", "wont-fix"];
325
+ function findFirstToken(text, tokens) {
326
+ for (const token of tokens) {
327
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
328
+ const regex = new RegExp(`\\b${escaped}\\b`, "iu");
329
+ if (regex.test(text))
330
+ return token;
331
+ }
332
+ return null;
333
+ }
334
+ function normalizeDisposition(value) {
335
+ if (value === null)
336
+ return null;
337
+ const lower = value.toLowerCase();
338
+ if (lower === "wont-fix" || lower === "won't-fix")
339
+ return "won't-fix";
340
+ return lower;
341
+ }
342
+ function extractTddPerSliceFindings(perSliceBody) {
343
+ const rows = new Map();
344
+ const lines = perSliceBody.split(/\r?\n/u);
345
+ for (const line of lines) {
346
+ const ids = line.match(FINDING_ID_PATTERN);
347
+ if (!ids || ids.length === 0)
348
+ continue;
349
+ const severity = findFirstToken(line, SEVERITY_TOKENS);
350
+ const disposition = normalizeDisposition(findFirstToken(line, DISPOSITION_TOKENS));
351
+ for (const rawId of ids) {
352
+ const id = rawId.toUpperCase();
353
+ if (rows.has(id))
354
+ continue;
355
+ rows.set(id, { id, severity, disposition });
356
+ }
357
+ }
358
+ return rows;
359
+ }
360
+ /**
361
+ * Cross-artifact duplication guard (Wave 23 / v5.0.0).
362
+ *
363
+ * When the same finding ID (`F-NN`) appears in both
364
+ * `06-tdd.md > Per-Slice Review` and `07-review-army.json`, the
365
+ * severity and disposition MUST match. Per-slice tdd reviews own
366
+ * single-slice findings; review cites them, never re-classifies.
367
+ *
368
+ * If neither artifact uses `F-NN` IDs, the check is a no-op.
369
+ */
370
+ export async function checkReviewTddNoCrossArtifactDuplication(projectRoot) {
371
+ const tddPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "06-tdd.md");
372
+ const armyPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "07-review-army.json");
373
+ const tddArtifactExists = await exists(tddPath);
374
+ const reviewArtifactExists = await exists(armyPath);
375
+ if (!tddArtifactExists || !reviewArtifactExists) {
376
+ return {
377
+ ok: true,
378
+ errors: [],
379
+ conflicts: [],
380
+ tddArtifactExists,
381
+ reviewArtifactExists
382
+ };
383
+ }
384
+ const tddRaw = await fs.readFile(tddPath, "utf8");
385
+ const tddSections = extractH2Sections(tddRaw);
386
+ const perSliceBody = sectionBodyByName(tddSections, "Per-Slice Review");
387
+ if (!perSliceBody) {
388
+ return {
389
+ ok: true,
390
+ errors: [],
391
+ conflicts: [],
392
+ tddArtifactExists,
393
+ reviewArtifactExists
394
+ };
395
+ }
396
+ const tddFindings = extractTddPerSliceFindings(perSliceBody);
397
+ if (tddFindings.size === 0) {
398
+ return {
399
+ ok: true,
400
+ errors: [],
401
+ conflicts: [],
402
+ tddArtifactExists,
403
+ reviewArtifactExists
404
+ };
405
+ }
406
+ let parsed;
407
+ try {
408
+ parsed = JSON.parse(await fs.readFile(armyPath, "utf8"));
409
+ }
410
+ catch {
411
+ return {
412
+ ok: true,
413
+ errors: [],
414
+ conflicts: [],
415
+ tddArtifactExists,
416
+ reviewArtifactExists
417
+ };
418
+ }
419
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
420
+ return {
421
+ ok: true,
422
+ errors: [],
423
+ conflicts: [],
424
+ tddArtifactExists,
425
+ reviewArtifactExists
426
+ };
427
+ }
428
+ const root = parsed;
429
+ const findings = Array.isArray(root.findings) ? root.findings : [];
430
+ const conflicts = [];
431
+ for (const f of findings) {
432
+ if (!f || typeof f !== "object" || Array.isArray(f))
433
+ continue;
434
+ const o = f;
435
+ if (typeof o.id !== "string")
436
+ continue;
437
+ const id = o.id.toUpperCase();
438
+ const tddRow = tddFindings.get(id);
439
+ if (!tddRow)
440
+ continue;
441
+ const reviewSeverity = typeof o.severity === "string" ? o.severity : null;
442
+ const reviewDisposition = normalizeDisposition(typeof o.status === "string" ? o.status : null);
443
+ const severityMismatch = tddRow.severity !== null &&
444
+ reviewSeverity !== null &&
445
+ tddRow.severity.toLowerCase() !== reviewSeverity.toLowerCase();
446
+ const dispositionMismatch = tddRow.disposition !== null &&
447
+ reviewDisposition !== null &&
448
+ tddRow.disposition !== reviewDisposition;
449
+ if (severityMismatch || dispositionMismatch) {
450
+ conflicts.push({
451
+ findingId: id,
452
+ tddSeverity: tddRow.severity,
453
+ reviewSeverity,
454
+ tddDisposition: tddRow.disposition,
455
+ reviewDisposition
456
+ });
457
+ }
458
+ }
459
+ const errors = conflicts.map((c) => {
460
+ const parts = [];
461
+ if (c.tddSeverity !== null && c.reviewSeverity !== null && c.tddSeverity.toLowerCase() !== c.reviewSeverity.toLowerCase()) {
462
+ parts.push(`severity tdd=${c.tddSeverity} vs review-army=${c.reviewSeverity}`);
463
+ }
464
+ if (c.tddDisposition !== null && c.reviewDisposition !== null && c.tddDisposition !== c.reviewDisposition) {
465
+ parts.push(`disposition tdd=${c.tddDisposition} vs review-army=${c.reviewDisposition}`);
466
+ }
467
+ return `Finding ${c.findingId} appears in both 06-tdd.md > Per-Slice Review and 07-review-army.json with mismatched ${parts.join(" and ")}. Review must cite, not re-classify.`;
468
+ });
469
+ return {
470
+ ok: errors.length === 0,
471
+ errors,
472
+ conflicts,
473
+ tddArtifactExists,
474
+ reviewArtifactExists
475
+ };
476
+ }
322
477
  export async function checkReviewSecurityNoChangeAttestation(projectRoot) {
323
478
  const reviewMdPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "07-review.md");
324
479
  if (!(await exists(reviewMdPath))) {
@@ -1,4 +1,5 @@
1
1
  import { markdownFieldRegex, sectionBodyByName } from "./shared.js";
2
+ import { checkReviewTddNoCrossArtifactDuplication } from "./review-army.js";
2
3
  export async function lintReviewStage(ctx) {
3
4
  const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
4
5
  // Universal Layer 2.7 structural checks (superpowers requesting + receiving).
@@ -62,6 +63,18 @@ export async function lintReviewStage(ctx) {
62
63
  : "Receiving Posture is missing the anti-sycophancy acknowledgement line."
63
64
  });
64
65
  }
66
+ const dupResult = await checkReviewTddNoCrossArtifactDuplication(projectRoot);
67
+ findings.push({
68
+ section: "review.no_cross_artifact_duplication",
69
+ required: true,
70
+ rule: "[P1] review.no_cross_artifact_duplication — when a finding ID appears in both `06-tdd.md > Per-Slice Review` and `07-review-army.json`, severity and disposition must match (review cites tdd; never re-classifies).",
71
+ found: dupResult.ok,
72
+ details: dupResult.ok
73
+ ? dupResult.tddArtifactExists && dupResult.reviewArtifactExists
74
+ ? "No cross-artifact severity/disposition conflicts between tdd Per-Slice Review and review-army findings."
75
+ : "Skipped: tdd Per-Slice Review or review-army artifact not present."
76
+ : dupResult.errors.join(" ")
77
+ });
65
78
  const lensCoverageBody = sectionBodyByName(sections, "Lens Coverage");
66
79
  if (lensCoverageBody === null) {
67
80
  findings.push({
@@ -23,9 +23,9 @@ export async function lintScopeStage(ctx) {
23
23
  const skipQuestions = activeStageFlags.includes("--skip-questions");
24
24
  const floor = evaluateQaLogFloor(qaLogBody, track, "scope", { skipQuestions });
25
25
  findings.push({
26
- section: "qa_log_below_min",
26
+ section: "qa_log_unconverged",
27
27
  required: !floor.skipQuestionsAdvisory,
28
- rule: "[P1] qa_log_below_min — Q&A Log below the adaptive elicitation floor for this track. Continue the loop or record an explicit user stop-signal row.",
28
+ rule: "[P1] qa_log_unconverged — Q&A Log has not converged for this stage. Continue elicitation until forcing-question topics are addressed, the last 2 rows produce no decision-changing impact (Ralph-Loop), or an explicit user stop-signal row is appended.",
29
29
  found: floor.ok,
30
30
  details: floor.details
31
31
  });
@@ -108,19 +108,11 @@ export async function lintScopeStage(ctx) {
108
108
  : issues.join("; ")
109
109
  });
110
110
  }
111
- // Universal Layer 2.2 structural checks (gstack plan-ceo-review). All
112
- // present-only they validate shape when the section exists.
113
- const altsBody = sectionBodyByName(sections, "Implementation Alternatives");
114
- if (altsBody !== null) {
115
- const recommendation = /^RECOMMENDATION:\s*(.+)$/imu.test(altsBody);
116
- findings.push({
117
- section: "Implementation Alternatives Recommendation",
118
- required: true,
119
- rule: "Implementation Alternatives must conclude with a `RECOMMENDATION:` line citing the chosen option and rationale.",
120
- found: recommendation,
121
- details: recommendation
122
- ? "Recommendation marker present."
123
- : "Missing or empty `RECOMMENDATION:` line under Implementation Alternatives."
124
- });
125
- }
111
+ // Wave 23 (v5.0.0): scope no longer owns architecture-tier alternatives
112
+ // (`## Implementation Alternatives` was removed from the scope template
113
+ // and stage schema). Design OWNS the architecture-tier decision via
114
+ // `## Architecture Decision Record (ADR)` and `## Engineering Lock`.
115
+ // The legacy linter rule `Implementation Alternatives Recommendation`
116
+ // was removed in Wave 23 — if a legacy artifact still has the section,
117
+ // it is now treated as informational only.
126
118
  }
@@ -1,43 +1,93 @@
1
1
  import { type FlowStage, type FlowTrack } from "../types.js";
2
2
  /**
3
- * Stages that run adaptive elicitation. The `qa_log_below_min` rule only
4
- * fires for these. Other stages may still record a Q&A Log but no floor is
5
- * enforced.
3
+ * Stages that run adaptive elicitation. The `qa_log_unconverged` rule
4
+ * only fires for these. Other stages may still record a Q&A Log but no
5
+ * convergence floor is enforced.
6
6
  */
7
7
  export declare const ELICITATION_STAGES: ReadonlySet<FlowStage>;
8
8
  export interface QaLogFloorOptions {
9
9
  /**
10
- * When true, downgrades a below-floor finding to advisory (`required: false`).
10
+ * When true, downgrades the finding to advisory (`required: false`).
11
11
  * Set when `--skip-questions` was persisted to the active stage flags.
12
12
  */
13
13
  skipQuestions?: boolean;
14
+ /**
15
+ * Optional pre-extracted forcing-question topics. When omitted, the
16
+ * evaluator calls `extractForcingQuestions(stage)` which scans the
17
+ * stage's checklist row.
18
+ */
19
+ forcingQuestions?: string[];
14
20
  }
15
21
  export interface QaLogFloorResult {
16
- /** Whether the floor is satisfied (passes the gate). */
22
+ /** Whether convergence is satisfied (passes the gate). */
17
23
  ok: boolean;
18
24
  /** Substantive Q&A Log row count (excludes `skipped`/`waived` only rows). */
19
25
  count: number;
20
- /** Required minimum count from `questionBudgetHint(track, stage).min`. */
26
+ /**
27
+ * Legacy field, retained for harness UI compatibility. Always 0 in
28
+ * Wave 23 — the convergence floor no longer enforces a fixed count.
29
+ * Harness can still surface `questionBudgetHint(track, stage).recommended`
30
+ * as a soft hint, but it is NOT tied to gate blocking.
31
+ */
21
32
  min: number;
22
33
  /** Whether a stop-signal row was detected. */
23
34
  hasStopSignal: boolean;
24
- /** Whether the lite-tier short-circuit applies (lite track + count >= 1). */
35
+ /**
36
+ * Legacy field, retained for harness UI compatibility. Always false in
37
+ * Wave 23 — convergence semantics replaced the lite-tier short-circuit.
38
+ */
25
39
  liteShortCircuit: boolean;
26
40
  /** Whether `--skip-questions` flag downgraded the finding to advisory. */
27
41
  skipQuestionsAdvisory: boolean;
42
+ /** Forcing-question topics deemed addressed (substring match in Q&A). */
43
+ forcingCovered: string[];
44
+ /** Forcing-question topics still pending (no matching Q&A row). */
45
+ forcingPending: string[];
46
+ /**
47
+ * True when the last 2 substantive rows have decision_impact marking
48
+ * `skip`/`continue`/`no-change`/`done`/etc. — i.e. Q&A is no longer
49
+ * surfacing decision-changing answers (Ralph-Loop convergence detector).
50
+ */
51
+ noNewDecisions: boolean;
28
52
  /** Human-readable details for the linter finding. */
29
53
  details: string;
30
54
  }
31
55
  /**
32
- * Evaluate the Q&A Log floor for a brainstorm / scope / design artifact.
33
- * Returns ok=true when the floor is satisfied or any escape hatch fires.
56
+ * Extract forcing-question topics from a stage's checklist. Looks for
57
+ * the canonical `**<Stage> forcing questions (must be covered or
58
+ * explicitly waived)** — <topic1>, <topic2>, ...` row and tokenizes the
59
+ * comma-separated topic list. Returns trimmed topic strings stripped of
60
+ * leading question words (`what`/`who`/`where`/`which`/`how`/`is`/`do`/`does`).
34
61
  *
35
- * Escape hatches (any one is sufficient):
36
- * - Q&A Log contains a stop-signal row.
62
+ * Returns empty array when no forcing-questions row is present (caller
63
+ * should treat absence as "no forcing requirement" — convergence falls
64
+ * back to the no-new-decisions / stop-signal detectors).
65
+ */
66
+ export declare function extractForcingQuestions(stage: FlowStage): string[];
67
+ /**
68
+ * Evaluate the Q&A Log convergence floor for a brainstorm / scope /
69
+ * design artifact. Returns ok=true when convergence is reached or any
70
+ * escape hatch fires.
71
+ *
72
+ * Convergence sources (any one is sufficient):
73
+ * - All forcing-question topics from the stage checklist appear addressed
74
+ * in `## Q&A Log` (substring keyword match in question/answer columns).
75
+ * - The Ralph-Loop convergence detector reports the last 2 substantive
76
+ * rows have decision_impact marking `skip`/`continue`/`no-change`/`done`
77
+ * (i.e. the dialogue is no longer producing decision-changing rows).
78
+ * - Q&A Log contains a stop-signal row (existing
79
+ * `QA_LOG_STOP_SIGNAL_PATTERNS` keep working).
37
80
  * - `--skip-questions` flag was persisted to the active stage flags
38
- * (passed via `options.skipQuestions=true`); finding downgrades to advisory.
39
- * - Track is `quick` (lite tier ~ lightweight complexity) AND substantive
40
- * count >= 1.
81
+ * (`options.skipQuestions=true`); finding downgrades to advisory.
82
+ * - The stage checklist exposes no forcing-questions row (e.g. simple
83
+ * refactor) AND the artifact has at least one substantive row — treat
84
+ * as converged because there is nothing left to force.
85
+ *
86
+ * Wave 23 (v5.0.0) replaces the count-based `qa_log_below_min` rule with
87
+ * `qa_log_unconverged`. The fixed count constant (10 for standard) and
88
+ * the `CCLAW_ELICITATION_FLOOR=advisory` env override were removed. The
89
+ * `min` and `liteShortCircuit` fields on the result are retained for
90
+ * harness UI compatibility but are always 0/false.
41
91
  */
42
92
  export declare function evaluateQaLogFloor(qaLogBody: string | null, track: FlowTrack, stage: FlowStage, options?: QaLogFloorOptions): QaLogFloorResult;
43
93
  export interface LintFinding {
@@ -130,10 +180,6 @@ export declare function canonicalModesInText(text: string): CanonicalScopeMode[]
130
180
  export declare function shortModeToCanonical(text: string): CanonicalScopeMode | null;
131
181
  export declare function canonicalModeFromCandidate(candidate: string): CanonicalScopeMode | null;
132
182
  export declare function extractCanonicalScopeMode(body: string): CanonicalScopeMode | null;
133
- export declare function validatePremiseChallenge(sectionBody: string): {
134
- ok: boolean;
135
- details: string;
136
- };
137
183
  export declare function validateScopeSummary(sectionBody: string): {
138
184
  ok: boolean;
139
185
  details: string;
@@ -282,7 +328,7 @@ export interface StageLintContext {
282
328
  /**
283
329
  * Stage-level flags persisted to flow-state.json `activeRun.currentStage.flags`
284
330
  * (or equivalent). Used as escape-hatch signal for the Q&A floor rule
285
- * (e.g. `--skip-questions` downgrades `qa_log_below_min` to advisory).
331
+ * (e.g. `--skip-questions` downgrades `qa_log_unconverged` to advisory).
286
332
  * When orchestrator cannot read flow-state, defaults to an empty array.
287
333
  */
288
334
  activeStageFlags: string[];