cclaw-cli 0.48.31 → 0.48.33

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.
Files changed (36) hide show
  1. package/dist/artifact-linter.js +609 -10
  2. package/dist/config.d.ts +1 -1
  3. package/dist/config.js +82 -4
  4. package/dist/content/examples.js +23 -6
  5. package/dist/content/hook-inline-snippets.d.ts +80 -0
  6. package/dist/content/hook-inline-snippets.js +270 -0
  7. package/dist/content/ideate-command.d.ts +6 -2
  8. package/dist/content/ideate-command.js +43 -16
  9. package/dist/content/ideate-frames.d.ts +31 -0
  10. package/dist/content/ideate-frames.js +140 -0
  11. package/dist/content/ideate-ranking.d.ts +25 -0
  12. package/dist/content/ideate-ranking.js +65 -0
  13. package/dist/content/node-hooks.js +9 -197
  14. package/dist/content/review-loop.d.ts +192 -0
  15. package/dist/content/review-loop.js +689 -0
  16. package/dist/content/seed-shelf.d.ts +36 -0
  17. package/dist/content/seed-shelf.js +236 -0
  18. package/dist/content/skills.js +77 -4
  19. package/dist/content/stage-schema.d.ts +1 -1
  20. package/dist/content/stage-schema.js +18 -2
  21. package/dist/content/stages/brainstorm.js +20 -4
  22. package/dist/content/stages/design.js +36 -8
  23. package/dist/content/stages/plan.js +5 -0
  24. package/dist/content/stages/review.js +5 -0
  25. package/dist/content/stages/schema-types.d.ts +29 -0
  26. package/dist/content/stages/scope.js +22 -6
  27. package/dist/content/stages/ship.js +6 -0
  28. package/dist/content/stages/spec.js +6 -0
  29. package/dist/content/stages/tdd.js +6 -0
  30. package/dist/content/start-command.js +24 -18
  31. package/dist/content/templates.js +108 -4
  32. package/dist/internal/advance-stage.js +143 -1
  33. package/dist/trace-matrix.d.ts +14 -0
  34. package/dist/trace-matrix.js +55 -1
  35. package/dist/types.d.ts +27 -0
  36. package/package.json +1 -1
@@ -0,0 +1,31 @@
1
+ export type IdeateFrameId = "pain-friction" | "inversion" | "assumption-break" | "leverage" | "cross-domain-analogy" | "constraint-flip";
2
+ export interface IdeateFrame {
3
+ id: IdeateFrameId;
4
+ label: string;
5
+ prompt: string;
6
+ examplePatterns: string[];
7
+ }
8
+ export interface IdeateFrameDispatchInput {
9
+ focus: string;
10
+ mode: "repo-grounded" | "elsewhere-software" | "elsewhere-non-software";
11
+ signalSummary: string[];
12
+ }
13
+ export interface IdeateFrameDispatchPlanEntry {
14
+ frameId: IdeateFrameId;
15
+ label: string;
16
+ prompt: string;
17
+ }
18
+ export interface IdeateCandidateDraft {
19
+ title: string;
20
+ evidencePath: string;
21
+ summary: string;
22
+ frameId: IdeateFrameId;
23
+ }
24
+ export interface IdeateCandidateMerged extends Omit<IdeateCandidateDraft, "frameId"> {
25
+ frameIds: IdeateFrameId[];
26
+ }
27
+ export declare const DEFAULT_IDEATE_FRAME_IDS: readonly IdeateFrameId[];
28
+ export declare const IDEATE_FRAMES: readonly IdeateFrame[];
29
+ export declare function resolveIdeateFrames(frameIds?: readonly IdeateFrameId[]): IdeateFrame[];
30
+ export declare function buildIdeateFrameDispatchPlan(input: IdeateFrameDispatchInput, frameIds?: readonly IdeateFrameId[]): IdeateFrameDispatchPlanEntry[];
31
+ export declare function dedupeIdeateCandidates(drafts: readonly IdeateCandidateDraft[]): IdeateCandidateMerged[];
@@ -0,0 +1,140 @@
1
+ const FRAME_REGISTRY = {
2
+ "pain-friction": {
3
+ id: "pain-friction",
4
+ label: "pain/friction",
5
+ prompt: "Find repeated friction in the repo workflow. Prioritize changes that eliminate recurring toil, fragile handoffs, or repeated manual recovery.",
6
+ examplePatterns: [
7
+ "Repeated TODO/FIXME hotspots in one subsystem",
8
+ "Flows that require manual retries or ad-hoc scripts",
9
+ "Developer loops slowed by avoidable boilerplate"
10
+ ]
11
+ },
12
+ inversion: {
13
+ id: "inversion",
14
+ label: "inversion",
15
+ prompt: "Invert a dominant assumption in the current implementation (e.g., push vs pull, synchronous vs queued, implicit vs explicit) and evaluate upside.",
16
+ examplePatterns: [
17
+ "Replace optimistic assumptions with fail-closed defaults",
18
+ "Switch from post-facto checks to pre-flight validation",
19
+ "Move from global policy to module-local contracts"
20
+ ]
21
+ },
22
+ "assumption-break": {
23
+ id: "assumption-break",
24
+ label: "assumption-break",
25
+ prompt: "List assumptions that might be false in production. Generate ideas that remain correct even when those assumptions fail.",
26
+ examplePatterns: [
27
+ "Edge paths currently treated as impossible",
28
+ "Implicit coupling between modules with no explicit contract",
29
+ "Latency, scale, or environment assumptions baked into logic"
30
+ ]
31
+ },
32
+ leverage: {
33
+ id: "leverage",
34
+ label: "leverage",
35
+ prompt: "Target interventions with asymmetric payoff: one change that improves multiple stages, teams, or failure classes at once.",
36
+ examplePatterns: [
37
+ "A shared helper replacing duplicated logic in many files",
38
+ "One lint/gate that blocks an entire class of regressions",
39
+ "A protocol update that improves multiple stage outputs"
40
+ ]
41
+ },
42
+ "cross-domain-analogy": {
43
+ id: "cross-domain-analogy",
44
+ label: "cross-domain analogy",
45
+ prompt: "Borrow a proven pattern from another domain and adapt it to this repo. Keep it concrete and grounded in local constraints.",
46
+ examplePatterns: [
47
+ "Apply SRE-style error budgets to planning artifacts",
48
+ "Use security threat-model thinking for reliability design",
49
+ "Import CI release-train discipline into stage completion gates"
50
+ ]
51
+ },
52
+ "constraint-flip": {
53
+ id: "constraint-flip",
54
+ label: "constraint-flip",
55
+ prompt: "Flip one assumed constraint (time, team size, risk tolerance, compatibility) and derive better options under the new boundary.",
56
+ examplePatterns: [
57
+ "Assume near-zero migration window and redesign rollout",
58
+ "Assume one maintainer and optimize for low operational burden",
59
+ "Assume strict auditability and remove ambiguous behaviors"
60
+ ]
61
+ }
62
+ };
63
+ export const DEFAULT_IDEATE_FRAME_IDS = Object.freeze([
64
+ "pain-friction",
65
+ "inversion",
66
+ "assumption-break",
67
+ "leverage",
68
+ "cross-domain-analogy",
69
+ "constraint-flip"
70
+ ]);
71
+ export const IDEATE_FRAMES = Object.freeze(DEFAULT_IDEATE_FRAME_IDS.map((id) => FRAME_REGISTRY[id]));
72
+ export function resolveIdeateFrames(frameIds) {
73
+ if (!frameIds || frameIds.length === 0) {
74
+ return [...IDEATE_FRAMES];
75
+ }
76
+ const seen = new Set();
77
+ const resolved = [];
78
+ for (const rawId of frameIds) {
79
+ if (!DEFAULT_IDEATE_FRAME_IDS.includes(rawId)) {
80
+ throw new Error(`Unknown ideate frame id: ${rawId}`);
81
+ }
82
+ if (seen.has(rawId))
83
+ continue;
84
+ seen.add(rawId);
85
+ resolved.push(FRAME_REGISTRY[rawId]);
86
+ }
87
+ return resolved;
88
+ }
89
+ export function buildIdeateFrameDispatchPlan(input, frameIds) {
90
+ const signalBlock = input.signalSummary.length > 0
91
+ ? input.signalSummary.map((line) => `- ${line}`).join("\n")
92
+ : "- no pre-scan signals captured yet";
93
+ return resolveIdeateFrames(frameIds).map((frame) => ({
94
+ frameId: frame.id,
95
+ label: frame.label,
96
+ prompt: [
97
+ `Frame: ${frame.label} (${frame.id})`,
98
+ `Mode: ${input.mode}`,
99
+ `Focus: ${input.focus || "open-ended scan"}`,
100
+ "",
101
+ "Signal summary:",
102
+ signalBlock,
103
+ "",
104
+ `Frame prompt: ${frame.prompt}`,
105
+ "",
106
+ "Generate 3-5 concrete candidates with repo-grounded evidence."
107
+ ].join("\n")
108
+ }));
109
+ }
110
+ function normalizeCandidateKey(title, evidencePath) {
111
+ const normalizedTitle = title.trim().toLowerCase().replace(/[^a-z0-9]+/gu, " ").trim();
112
+ const normalizedEvidence = evidencePath
113
+ .trim()
114
+ .toLowerCase()
115
+ .replace(/\\/gu, "/");
116
+ return `${normalizedTitle}::${normalizedEvidence}`;
117
+ }
118
+ export function dedupeIdeateCandidates(drafts) {
119
+ const merged = new Map();
120
+ for (const draft of drafts) {
121
+ const key = normalizeCandidateKey(draft.title, draft.evidencePath);
122
+ const existing = merged.get(key);
123
+ if (!existing) {
124
+ merged.set(key, {
125
+ title: draft.title,
126
+ evidencePath: draft.evidencePath,
127
+ summary: draft.summary,
128
+ frameIds: [draft.frameId]
129
+ });
130
+ continue;
131
+ }
132
+ if (!existing.frameIds.includes(draft.frameId)) {
133
+ existing.frameIds.push(draft.frameId);
134
+ }
135
+ if (draft.summary.length > existing.summary.length) {
136
+ existing.summary = draft.summary;
137
+ }
138
+ }
139
+ return [...merged.values()];
140
+ }
@@ -0,0 +1,25 @@
1
+ export type IdeateImpact = "high" | "medium" | "low";
2
+ export type IdeateEffort = "s" | "m" | "l";
3
+ export type IdeateConfidence = "high" | "medium" | "low";
4
+ export interface IdeateCandidateEvaluationInput {
5
+ id: string;
6
+ title: string;
7
+ impact: IdeateImpact;
8
+ effort: IdeateEffort;
9
+ confidence: IdeateConfidence;
10
+ rationaleStrength: number;
11
+ counterArgumentStrength: number;
12
+ }
13
+ export interface IdeateCandidateEvaluation extends IdeateCandidateEvaluationInput {
14
+ disposition: "survivor" | "critiqued-out";
15
+ rankingScore: number;
16
+ }
17
+ export interface IdeateRankingResult {
18
+ survivors: IdeateCandidateEvaluation[];
19
+ critiquedOut: IdeateCandidateEvaluation[];
20
+ recommendationId: string | null;
21
+ }
22
+ export declare function isCritiquedOut(rationaleStrength: number, counterArgumentStrength: number): boolean;
23
+ export declare function scoreIdeateCandidate(impact: IdeateImpact, effort: IdeateEffort, confidence: IdeateConfidence): number;
24
+ export declare function evaluateIdeateCandidate(input: IdeateCandidateEvaluationInput): IdeateCandidateEvaluation;
25
+ export declare function rankIdeateCandidates(inputs: readonly IdeateCandidateEvaluationInput[], maxSurvivors?: number): IdeateRankingResult;
@@ -0,0 +1,65 @@
1
+ const IMPACT_POINTS = {
2
+ high: 9,
3
+ medium: 6,
4
+ low: 3
5
+ };
6
+ const EFFORT_COST = {
7
+ s: 1,
8
+ m: 2,
9
+ l: 3
10
+ };
11
+ const CONFIDENCE_MULTIPLIER = {
12
+ high: 1,
13
+ medium: 0.75,
14
+ low: 0.5
15
+ };
16
+ function clampStrength(value) {
17
+ if (!Number.isFinite(value))
18
+ return 0;
19
+ if (value < 0)
20
+ return 0;
21
+ if (value > 1)
22
+ return 1;
23
+ return value;
24
+ }
25
+ export function isCritiquedOut(rationaleStrength, counterArgumentStrength) {
26
+ return clampStrength(counterArgumentStrength) > clampStrength(rationaleStrength);
27
+ }
28
+ export function scoreIdeateCandidate(impact, effort, confidence) {
29
+ const raw = (IMPACT_POINTS[impact] / EFFORT_COST[effort]) * CONFIDENCE_MULTIPLIER[confidence];
30
+ return Number(raw.toFixed(3));
31
+ }
32
+ export function evaluateIdeateCandidate(input) {
33
+ const disposition = isCritiquedOut(input.rationaleStrength, input.counterArgumentStrength)
34
+ ? "critiqued-out"
35
+ : "survivor";
36
+ return {
37
+ ...input,
38
+ disposition,
39
+ rankingScore: scoreIdeateCandidate(input.impact, input.effort, input.confidence)
40
+ };
41
+ }
42
+ export function rankIdeateCandidates(inputs, maxSurvivors = 10) {
43
+ const evaluated = inputs.map(evaluateIdeateCandidate);
44
+ const survivors = evaluated
45
+ .filter((candidate) => candidate.disposition === "survivor")
46
+ .sort((left, right) => {
47
+ if (right.rankingScore !== left.rankingScore) {
48
+ return right.rankingScore - left.rankingScore;
49
+ }
50
+ if (right.rationaleStrength !== left.rationaleStrength) {
51
+ return right.rationaleStrength - left.rationaleStrength;
52
+ }
53
+ return left.id.localeCompare(right.id);
54
+ })
55
+ .slice(0, Math.max(0, maxSurvivors));
56
+ const survivorIds = new Set(survivors.map((candidate) => candidate.id));
57
+ const critiquedOut = evaluated
58
+ .filter((candidate) => candidate.disposition === "critiqued-out" || !survivorIds.has(candidate.id))
59
+ .sort((left, right) => left.id.localeCompare(right.id));
60
+ return {
61
+ survivors,
62
+ critiquedOut,
63
+ recommendationId: survivors[0]?.id ?? null
64
+ };
65
+ }
@@ -1,6 +1,7 @@
1
1
  import { DEFAULT_COMPOUND_RECURRENCE_THRESHOLD } from "../config.js";
2
2
  import { RUNTIME_ROOT } from "../constants.js";
3
3
  import { SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD, SMALL_PROJECT_RECURRENCE_THRESHOLD } from "../knowledge-store.js";
4
+ import { HOOK_INLINE_SHARED_HELPERS, COMPOUND_READINESS_INLINE_SOURCE, RALPH_LOOP_INLINE_SOURCE } from "./hook-inline-snippets.js";
4
5
  function normalizePatterns(patterns, fallback) {
5
6
  if (!patterns || patterns.length === 0)
6
7
  return [...fallback];
@@ -1451,203 +1452,14 @@ async function handlePromptGuard(runtime) {
1451
1452
  return 0;
1452
1453
  }
1453
1454
 
1454
- function normalizeCompoundLastUpdatedAt(date) {
1455
- return date.toISOString().replace(/\\.\\d{3}Z$/u, "Z");
1456
- }
1457
-
1458
- // Count archived runs as sub-directories under \`.cclaw/runs/\`. Missing
1459
- // dir returns 0; unexpected errors return undefined so the caller can
1460
- // skip the small-project relaxation rather than guess.
1461
- async function countArchivedRunsInline(root) {
1462
- const dir = path.join(root, RUNTIME_ROOT, "runs");
1463
- try {
1464
- const entries = await fs.readdir(dir, { withFileTypes: true });
1465
- return entries.filter((entry) => entry.isDirectory()).length;
1466
- } catch (error) {
1467
- const code = error && typeof error === "object" && "code" in error ? error.code : null;
1468
- if (code === "ENOENT") return 0;
1469
- return undefined;
1470
- }
1471
- }
1472
-
1473
- // Mirrors src/knowledge-store.ts::computeCompoundReadiness — kept inline so
1474
- // SessionStart can refresh compound-readiness.json without the CLI binary.
1475
- // Any schema change must update src/knowledge-store.ts::computeCompoundReadiness
1476
- // and src/internal/compound-readiness.ts in lockstep. Parity is enforced by
1477
- // tests/unit/ralph-loop-parity.test.ts.
1478
- async function computeCompoundReadinessInline(root, options) {
1479
- const filePath = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
1480
- // Caller may supply pre-read raw to avoid double-reading knowledge.jsonl.
1481
- const raw = typeof (options && options.prereadRaw) === "string"
1482
- ? options.prereadRaw
1483
- : await readTextFile(filePath, "");
1484
- const baseThresholdRaw = options && options.threshold;
1485
- const baseThreshold = Number.isInteger(baseThresholdRaw) && baseThresholdRaw >= 1
1486
- ? baseThresholdRaw
1487
- : COMPOUND_RECURRENCE_THRESHOLD;
1488
- const archivedRunsCount =
1489
- typeof (options && options.archivedRunsCount) === "number" &&
1490
- Number.isFinite(options.archivedRunsCount) &&
1491
- options.archivedRunsCount >= 0
1492
- ? Math.floor(options.archivedRunsCount)
1493
- : undefined;
1494
- const smallProjectRelaxationApplied =
1495
- archivedRunsCount !== undefined &&
1496
- archivedRunsCount < SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD &&
1497
- baseThreshold > SMALL_PROJECT_RECURRENCE_THRESHOLD;
1498
- const threshold = smallProjectRelaxationApplied
1499
- ? SMALL_PROJECT_RECURRENCE_THRESHOLD
1500
- : baseThreshold;
1501
- const maxReady = Number.isInteger(options && options.maxReady) && options.maxReady >= 1
1502
- ? options.maxReady
1503
- : 10;
1504
- const normalize = (value) => String(value == null ? "" : value).trim().replace(/\\s+/gu, " ").toLowerCase();
1505
- const severityWeight = (sev) => {
1506
- if (sev === "critical") return 3;
1507
- if (sev === "important") return 2;
1508
- if (sev === "suggestion") return 1;
1509
- return 0;
1510
- };
1511
- const buckets = new Map();
1512
- for (const rawLine of raw.split(/\\r?\\n/gu)) {
1513
- const line = rawLine.trim();
1514
- if (line.length === 0) continue;
1515
- let row;
1516
- try { row = JSON.parse(line); } catch { continue; }
1517
- if (!row || typeof row !== "object" || Array.isArray(row)) continue;
1518
- if (row.maturity === "lifted-to-enforcement") continue;
1519
- const type = typeof row.type === "string" ? row.type : "";
1520
- const trigger = typeof row.trigger === "string" ? row.trigger : "";
1521
- const action = typeof row.action === "string" ? row.action : "";
1522
- if (type.length === 0 || trigger.length === 0 || action.length === 0) continue;
1523
- const key = type + "||" + normalize(trigger) + "||" + normalize(action);
1524
- const frequency = Number.isInteger(row.frequency) && row.frequency > 0 ? Math.floor(row.frequency) : 1;
1525
- const lastSeen = typeof row.last_seen_ts === "string" ? row.last_seen_ts : "";
1526
- let bucket = buckets.get(key);
1527
- if (!bucket) {
1528
- bucket = {
1529
- trigger,
1530
- action,
1531
- recurrence: frequency,
1532
- entryCount: 1,
1533
- severity: typeof row.severity === "string" ? row.severity : undefined,
1534
- lastSeenTs: lastSeen,
1535
- types: new Set([type]),
1536
- maturity: new Set([typeof row.maturity === "string" ? row.maturity : "raw"])
1537
- };
1538
- buckets.set(key, bucket);
1539
- continue;
1540
- }
1541
- bucket.recurrence += frequency;
1542
- bucket.entryCount += 1;
1543
- bucket.types.add(type);
1544
- bucket.maturity.add(typeof row.maturity === "string" ? row.maturity : "raw");
1545
- if (row.severity === "critical") {
1546
- bucket.severity = "critical";
1547
- } else if (row.severity === "important" && bucket.severity !== "critical") {
1548
- bucket.severity = "important";
1549
- }
1550
- if (lastSeen && Date.parse(lastSeen) > Date.parse(bucket.lastSeenTs || "0")) {
1551
- bucket.lastSeenTs = lastSeen;
1552
- }
1553
- }
1554
- const ready = [];
1555
- for (const bucket of buckets.values()) {
1556
- const criticalOverride = bucket.severity === "critical";
1557
- const meetsRecurrence = bucket.recurrence >= threshold;
1558
- if (!criticalOverride && !meetsRecurrence) continue;
1559
- ready.push({
1560
- trigger: bucket.trigger,
1561
- action: bucket.action,
1562
- recurrence: bucket.recurrence,
1563
- entryCount: bucket.entryCount,
1564
- qualification: criticalOverride && !meetsRecurrence ? "critical_override" : "recurrence",
1565
- ...(bucket.severity ? { severity: bucket.severity } : {}),
1566
- lastSeenTs: bucket.lastSeenTs,
1567
- types: Array.from(bucket.types).sort(),
1568
- maturity: Array.from(bucket.maturity).sort()
1569
- });
1570
- }
1571
- ready.sort((a, b) => {
1572
- const sevDiff = severityWeight(b.severity) - severityWeight(a.severity);
1573
- if (sevDiff !== 0) return sevDiff;
1574
- if (b.recurrence !== a.recurrence) return b.recurrence - a.recurrence;
1575
- const recencyDiff = Date.parse(b.lastSeenTs || "0") - Date.parse(a.lastSeenTs || "0");
1576
- if (!Number.isNaN(recencyDiff) && recencyDiff !== 0) return recencyDiff;
1577
- return String(a.trigger).localeCompare(String(b.trigger));
1578
- });
1579
- return {
1580
- schemaVersion: 2,
1581
- threshold,
1582
- baseThreshold,
1583
- ...(archivedRunsCount !== undefined ? { archivedRunsCount } : {}),
1584
- smallProjectRelaxationApplied,
1585
- clusterCount: buckets.size,
1586
- readyCount: ready.length,
1587
- ready: ready.slice(0, maxReady),
1588
- lastUpdatedAt: normalizeCompoundLastUpdatedAt(new Date())
1589
- };
1590
- }
1591
-
1592
- // Mirrors src/tdd-cycle.ts::computeRalphLoopStatus — kept inline so the
1593
- // SessionStart hook can write ralph-loop.json without depending on the CLI
1594
- // binary being installed globally. Any schema change must update both copies.
1595
- async function computeRalphLoopStatusInline(stateDir, runId) {
1596
- const filePath = path.join(stateDir, "tdd-cycle-log.jsonl");
1597
- const raw = await readTextFile(filePath, "");
1598
- const sliceMap = new Map();
1599
- const acClosed = new Set();
1600
- const redOpenSlices = [];
1601
- let loopIteration = 0;
1602
- for (const rawLine of raw.split(/\\r?\\n/gu)) {
1603
- const line = rawLine.trim();
1604
- if (line.length === 0) continue;
1605
- let row;
1606
- try { row = JSON.parse(line); } catch { continue; }
1607
- if (!row || typeof row !== "object" || Array.isArray(row)) continue;
1608
- const rowRun = typeof row.runId === "string" && row.runId.length > 0 ? row.runId : runId;
1609
- if (rowRun !== runId) continue;
1610
- const slice = typeof row.slice === "string" && row.slice.length > 0 ? row.slice : "S-unknown";
1611
- let state = sliceMap.get(slice);
1612
- if (!state) {
1613
- state = { slice, redCount: 0, greenCount: 0, refactorCount: 0, redOpen: false, acIds: [] };
1614
- sliceMap.set(slice, state);
1615
- }
1616
- const exitCode = typeof row.exitCode === "number" ? row.exitCode : undefined;
1617
- if (row.phase === "red") {
1618
- state.redCount += 1;
1619
- if (exitCode !== undefined && exitCode !== 0) state.redOpen = true;
1620
- } else if (row.phase === "green") {
1621
- state.greenCount += 1;
1622
- state.redOpen = false;
1623
- loopIteration += 1;
1624
- if (Array.isArray(row.acIds)) {
1625
- for (const acId of row.acIds) {
1626
- if (typeof acId !== "string" || acId.length === 0) continue;
1627
- acClosed.add(acId);
1628
- if (!state.acIds.includes(acId)) state.acIds.push(acId);
1629
- }
1630
- }
1631
- } else if (row.phase === "refactor") {
1632
- state.refactorCount += 1;
1633
- }
1634
- }
1635
- for (const state of sliceMap.values()) {
1636
- if (state.redOpen) redOpenSlices.push(state.slice);
1637
- }
1638
- const slices = Array.from(sliceMap.values()).sort((a, b) => a.slice.localeCompare(b.slice, "en"));
1639
- return {
1640
- schemaVersion: 1,
1641
- runId,
1642
- loopIteration,
1643
- redOpen: redOpenSlices.length > 0,
1644
- redOpenSlices,
1645
- acClosed: Array.from(acClosed).sort(),
1646
- sliceCount: slices.length,
1647
- slices,
1648
- lastUpdatedAt: new Date().toISOString()
1649
- };
1650
- }
1455
+ // Inline mirrors of canonical CLI computations (compound-readiness,
1456
+ // ralph-loop) are factored into src/content/hook-inline-snippets.ts so
1457
+ // this 2000+-line file no longer owns their bodies. Each snippet carries
1458
+ // an explicit "mirrors X, parity enforced by Y" comment in the snippets
1459
+ // module. Parity is enforced by tests/unit/ralph-loop-parity.test.ts.
1460
+ ${HOOK_INLINE_SHARED_HELPERS}
1461
+ ${COMPOUND_READINESS_INLINE_SOURCE}
1462
+ ${RALPH_LOOP_INLINE_SOURCE}
1651
1463
 
1652
1464
  async function hasFailingRedEvidenceForPath(stateDir, runId, rawPath) {
1653
1465
  const cycleRaw = await readTextFile(path.join(stateDir, "tdd-cycle-log.jsonl"), "");
@@ -0,0 +1,192 @@
1
+ import type { FlowStage } from "../types.js";
2
+ import type { SkillEnvelope } from "./stage-schema.js";
3
+ export declare const REVIEW_LOOP_STAGES: readonly ["scope", "design"];
4
+ export type ReviewLoopStage = (typeof REVIEW_LOOP_STAGES)[number];
5
+ export declare const REVIEW_LOOP_DEFAULT_MAX_ITERATIONS = 3;
6
+ export declare const REVIEW_LOOP_DEFAULT_TARGET_SCORE = 0.8;
7
+ export type ReviewFindingSeverity = "critical" | "important" | "suggestion";
8
+ export type ReviewLoopStopReason = "quality_threshold_met" | "max_iterations_reached" | "user_opt_out";
9
+ export interface ReviewLoopDimension {
10
+ id: string;
11
+ label: string;
12
+ weight: number;
13
+ guidance: string;
14
+ }
15
+ export interface ReviewLoopDimensionScore {
16
+ dimensionId: string;
17
+ score: number;
18
+ weight?: number;
19
+ rationale?: string;
20
+ }
21
+ export interface ReviewFinding {
22
+ id: string;
23
+ dimensionId: string;
24
+ severity: ReviewFindingSeverity;
25
+ summary: string;
26
+ evidence?: string;
27
+ recommendation?: string;
28
+ }
29
+ export interface ReviewLoopBudget {
30
+ maxIterations?: number;
31
+ targetScore?: number;
32
+ }
33
+ export interface ReviewLoopIterationSummary {
34
+ iteration: number;
35
+ qualityScore: number;
36
+ findingsCount: number;
37
+ }
38
+ export interface ReviewLoopInput {
39
+ artifactPath: string;
40
+ stage: ReviewLoopStage;
41
+ checklist?: readonly ReviewLoopDimension[];
42
+ priorIterations?: ReadonlyArray<ReviewLoopIterationSummary>;
43
+ budget?: ReviewLoopBudget;
44
+ }
45
+ export interface ReviewLoopDispatchRequest {
46
+ stage: ReviewLoopStage;
47
+ artifactPath: string;
48
+ checklist: readonly ReviewLoopDimension[];
49
+ priorIterations: ReadonlyArray<ReviewLoopIterationSummary>;
50
+ iteration: number;
51
+ budget: Required<ReviewLoopBudget>;
52
+ }
53
+ export interface ReviewLoopIterationResult {
54
+ qualityScore: number;
55
+ findings: ReviewFinding[];
56
+ iteration: number;
57
+ shouldContinue: boolean;
58
+ dimensionScores: ReviewLoopDimensionScore[];
59
+ }
60
+ export interface ReviewLoopEnvelope {
61
+ type: "review-loop";
62
+ version: "1";
63
+ stage: ReviewLoopStage;
64
+ artifactPath: string;
65
+ targetScore: number;
66
+ maxIterations: number;
67
+ stopReason: ReviewLoopStopReason;
68
+ iterations: ReviewLoopIterationSummary[];
69
+ }
70
+ export interface ReviewLoopRunResult {
71
+ iterations: ReviewLoopIterationResult[];
72
+ qualityScore: number;
73
+ stopReason: ReviewLoopStopReason;
74
+ envelope: ReviewLoopEnvelope;
75
+ }
76
+ export type ReviewLoopDispatcher = (request: ReviewLoopDispatchRequest) => Promise<unknown>;
77
+ export interface ReviewLoopDispatchAdapterRequest {
78
+ request: ReviewLoopDispatchRequest;
79
+ prompt: string;
80
+ responseSchema: string;
81
+ }
82
+ export type ReviewLoopDispatchAdapter = (payload: ReviewLoopDispatchAdapterRequest) => Promise<unknown>;
83
+ export interface ReviewLoopSecondOpinionPolicy {
84
+ enabled?: boolean;
85
+ scoreDeltaThreshold?: number;
86
+ modelLabel?: string;
87
+ }
88
+ export interface ReviewLoopSecondOpinionMeta {
89
+ enabled: boolean;
90
+ modelLabel?: string;
91
+ primaryScore: number;
92
+ secondOpinionScore: number;
93
+ scoreDelta: number;
94
+ threshold: number;
95
+ }
96
+ export type ReviewLoopApplyFindings = (iteration: ReviewLoopIterationResult) => Promise<void> | void;
97
+ export interface RunReviewLoopOptions {
98
+ dispatcher: ReviewLoopDispatcher;
99
+ applyFindings: ReviewLoopApplyFindings;
100
+ shouldOptOut?: () => boolean;
101
+ emitEnvelope?: (envelope: ReviewLoopEnvelope) => void;
102
+ }
103
+ export declare const REVIEW_LOOP_CHECKLISTS: {
104
+ readonly scope: readonly [{
105
+ readonly id: "premise_fit";
106
+ readonly label: "Premise fit";
107
+ readonly weight: 1;
108
+ readonly guidance: "Does the scope contract solve the actual user/problem framing without drifting into adjacent asks?";
109
+ }, {
110
+ readonly id: "alternatives_coverage";
111
+ readonly label: "Alternatives coverage";
112
+ readonly weight: 1;
113
+ readonly guidance: "Are meaningful alternatives compared with explicit trade-offs and one clear recommendation?";
114
+ }, {
115
+ readonly id: "error_rescue_registry";
116
+ readonly label: "Error and rescue coverage";
117
+ readonly weight: 1;
118
+ readonly guidance: "Does each scoped capability define failure mode, detection signal, and fallback/rescue behavior?";
119
+ }, {
120
+ readonly id: "scope_creep_risk";
121
+ readonly label: "Scope-creep risk";
122
+ readonly weight: 1;
123
+ readonly guidance: "Are in/out boundaries explicit and protected against silent expansion/reduction language?";
124
+ }, {
125
+ readonly id: "completion_status_fidelity";
126
+ readonly label: "Completion status fidelity";
127
+ readonly weight: 1;
128
+ readonly guidance: "Does the completion dashboard honestly report unresolved risks, decision count, and stop reason?";
129
+ }];
130
+ readonly design: readonly [{
131
+ readonly id: "architecture_fit";
132
+ readonly label: "Architecture fit";
133
+ readonly weight: 1;
134
+ readonly guidance: "Do architecture boundaries and diagrams align with scope and real blast-radius code?";
135
+ }, {
136
+ readonly id: "failure_mode_coverage";
137
+ readonly label: "Failure-mode coverage";
138
+ readonly weight: 1;
139
+ readonly guidance: "Does the failure-mode table capture method/exception/rescue/user-visible impact for critical paths?";
140
+ }, {
141
+ readonly id: "test_coverage_realism";
142
+ readonly label: "Test coverage realism";
143
+ readonly weight: 1;
144
+ readonly guidance: "Is the proposed test split realistic (unit/integration/e2e) with explicit gap handling?";
145
+ }, {
146
+ readonly id: "performance_budget";
147
+ readonly label: "Performance budget";
148
+ readonly weight: 1;
149
+ readonly guidance: "Are critical metrics, thresholds, and measurement methods concrete and enforceable?";
150
+ }, {
151
+ readonly id: "observability_adequacy";
152
+ readonly label: "Observability adequacy";
153
+ readonly weight: 1;
154
+ readonly guidance: "Can on-call trace a failure from user symptom to root cause via logs/metrics/traces/alerts?";
155
+ }];
156
+ };
157
+ export declare function buildOutsideVoiceReviewPrompt(request: ReviewLoopDispatchRequest): string;
158
+ export declare function createOutsideVoiceDispatcher(adapter: ReviewLoopDispatchAdapter): ReviewLoopDispatcher;
159
+ export declare function parseReviewLoopDispatcherResult(raw: unknown, checklist: readonly ReviewLoopDimension[]): {
160
+ findings: ReviewFinding[];
161
+ dimensionScores: ReviewLoopDimensionScore[];
162
+ };
163
+ export declare function mergeSecondOpinionResults(primaryRaw: unknown, secondOpinionRaw: unknown, checklist: readonly ReviewLoopDimension[], policy?: ReviewLoopSecondOpinionPolicy): {
164
+ findings: ReviewFinding[];
165
+ dimensionScores: ReviewLoopDimensionScore[];
166
+ secondOpinion: ReviewLoopSecondOpinionMeta;
167
+ };
168
+ export declare function createSecondOpinionDispatcher(args: {
169
+ primary: ReviewLoopDispatcher;
170
+ secondOpinion?: ReviewLoopDispatcher;
171
+ policy?: ReviewLoopSecondOpinionPolicy;
172
+ }): ReviewLoopDispatcher;
173
+ export declare function aggregateQualityScore(scores: readonly ReviewLoopDimensionScore[], checklist: readonly ReviewLoopDimension[]): number;
174
+ export declare function runReviewLoopIteration(input: ReviewLoopInput & {
175
+ iteration: number;
176
+ }, dispatcher: ReviewLoopDispatcher): Promise<ReviewLoopIterationResult>;
177
+ export declare function buildReviewLoopEnvelope(args: {
178
+ stage: ReviewLoopStage;
179
+ artifactPath: string;
180
+ targetScore: number;
181
+ maxIterations: number;
182
+ stopReason: ReviewLoopStopReason;
183
+ iterations: ReadonlyArray<ReviewLoopIterationSummary>;
184
+ }): ReviewLoopEnvelope;
185
+ export declare function renderReviewLoopHeader(envelope: ReviewLoopEnvelope): string;
186
+ export declare function upsertReviewLoopHeader(markdown: string, envelope: ReviewLoopEnvelope): string;
187
+ export declare function renderReviewLoopSummarySection(envelope: ReviewLoopEnvelope): string;
188
+ export declare function upsertReviewLoopSummary(markdown: string, envelope: ReviewLoopEnvelope): string;
189
+ export declare function extractReviewLoopEnvelopeFromArtifact(markdown: string, stage: ReviewLoopStage, artifactPath: string): ReviewLoopEnvelope | null;
190
+ export declare function toSkillEnvelope(envelope: ReviewLoopEnvelope, emittedAt?: string, agent?: string): SkillEnvelope;
191
+ export declare function runReviewLoop(input: ReviewLoopInput, options: RunReviewLoopOptions): Promise<ReviewLoopRunResult>;
192
+ export declare function isReviewLoopStage(stage: FlowStage): stage is ReviewLoopStage;