cclaw-cli 0.55.2 → 1.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.
- package/README.md +3 -3
- package/dist/artifact-linter/brainstorm.js +45 -1
- package/dist/artifact-linter/design.js +32 -1
- package/dist/artifact-linter/plan.js +22 -1
- package/dist/artifact-linter/review.js +35 -1
- package/dist/artifact-linter/scope.js +19 -9
- package/dist/artifact-linter/shared.d.ts +11 -10
- package/dist/artifact-linter/shared.js +70 -41
- package/dist/artifact-linter/ship.js +36 -0
- package/dist/artifact-linter/spec.js +23 -1
- package/dist/artifact-linter/tdd.js +74 -0
- package/dist/artifact-linter.d.ts +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -0
- package/dist/content/closeout-guidance.d.ts +1 -1
- package/dist/content/closeout-guidance.js +10 -11
- package/dist/content/core-agents.d.ts +35 -36
- package/dist/content/core-agents.js +189 -99
- package/dist/content/diff-command.js +1 -1
- package/dist/content/examples.d.ts +0 -3
- package/dist/content/examples.js +197 -752
- package/dist/content/idea.d.ts +60 -0
- package/dist/content/idea.js +404 -0
- package/dist/content/learnings.d.ts +2 -4
- package/dist/content/learnings.js +10 -26
- package/dist/content/node-hooks.js +131 -97
- package/dist/content/opencode-plugin.js +12 -26
- package/dist/content/reference-patterns.js +2 -2
- package/dist/content/runtime-shared-snippets.d.ts +8 -0
- package/dist/content/runtime-shared-snippets.js +80 -0
- package/dist/content/session-hooks.js +1 -1
- package/dist/content/skills.d.ts +1 -0
- package/dist/content/skills.js +50 -0
- package/dist/content/stage-schema.js +107 -63
- package/dist/content/stages/review.js +8 -8
- package/dist/content/stages/schema-types.d.ts +2 -2
- package/dist/content/stages/scope.js +1 -1
- package/dist/content/stages/ship.js +1 -1
- package/dist/content/status-command.js +3 -3
- package/dist/content/subagent-context-skills.js +156 -1
- package/dist/content/subagents.d.ts +0 -5
- package/dist/content/subagents.js +12 -82
- package/dist/content/templates.js +87 -6
- package/dist/content/utility-skills.js +26 -97
- package/dist/flow-state.d.ts +5 -6
- package/dist/flow-state.js +4 -6
- package/dist/gate-evidence.d.ts +0 -31
- package/dist/gate-evidence.js +3 -181
- package/dist/harness-adapters.js +1 -1
- package/dist/install.js +38 -4
- package/dist/internal/advance-stage/advance.js +0 -1
- package/dist/internal/advance-stage/review-loop.js +1 -10
- package/dist/knowledge-store.d.ts +2 -20
- package/dist/knowledge-store.js +43 -57
- package/dist/policy.js +3 -3
- package/dist/retro-gate.js +8 -90
- package/dist/run-archive.js +1 -4
- package/dist/run-persistence.js +14 -109
- package/dist/runtime/run-hook.entry.d.ts +3 -0
- package/dist/runtime/run-hook.entry.js +5 -0
- package/dist/runtime/run-hook.mjs +9477 -0
- package/package.json +4 -2
- package/dist/content/hook-inline-snippets.d.ts +0 -96
- package/dist/content/hook-inline-snippets.js +0 -515
- package/dist/content/idea-command.d.ts +0 -8
- package/dist/content/idea-command.js +0 -322
- package/dist/content/idea-frames.d.ts +0 -31
- package/dist/content/idea-frames.js +0 -140
- package/dist/content/idea-ranking.d.ts +0 -25
- package/dist/content/idea-ranking.js +0 -65
- package/dist/trace-matrix.d.ts +0 -27
- package/dist/trace-matrix.js +0 -226
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cclaw-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Installer-first flow toolkit for coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"clean:dist": "node -e \"import('node:fs/promises').then((fs) => fs.rm('dist', { recursive: true, force: true }))\"",
|
|
20
|
-
"build": "npm run clean:dist && tsc -p tsconfig.json && node scripts/chmod-bin.mjs",
|
|
20
|
+
"build": "npm run clean:dist && tsc -p tsconfig.json && npm run build:hook-bundle && node scripts/chmod-bin.mjs",
|
|
21
|
+
"build:hook-bundle": "esbuild src/runtime/run-hook.entry.ts --bundle --platform=node --format=esm --outfile=dist/runtime/run-hook.mjs",
|
|
21
22
|
"test": "vitest run",
|
|
22
23
|
"test:watch": "vitest",
|
|
23
24
|
"test:coverage": "vitest run --coverage",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
48
49
|
"@types/node": "^24.7.2",
|
|
49
50
|
"@vitest/coverage-v8": "^3.2.4",
|
|
51
|
+
"esbuild": "^0.28.0",
|
|
50
52
|
"typescript": "^5.9.3",
|
|
51
53
|
"vitest": "^3.2.4"
|
|
52
54
|
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hook-inline-snippets.ts
|
|
3
|
-
*
|
|
4
|
-
* Runtime `.cclaw/hooks/run-hook.mjs` is a **standalone Node script** that
|
|
5
|
-
* cannot import from `cclaw-cli` — it must work inside the end-user's
|
|
6
|
-
* project even when the CLI is not installed. Two derived computations,
|
|
7
|
-
* though, must remain 1:1 with the canonical TS implementations:
|
|
8
|
-
*
|
|
9
|
-
* 1. `computeCompoundReadinessInline` mirrors
|
|
10
|
-
* `src/knowledge-store.ts::computeCompoundReadiness`.
|
|
11
|
-
* 2. `computeRalphLoopStatusInline` mirrors
|
|
12
|
-
* `src/tdd-cycle.ts::computeRalphLoopStatus`.
|
|
13
|
-
* 3. `computeEarlyLoopStatusInline` mirrors
|
|
14
|
-
* `src/early-loop.ts::computeEarlyLoopStatus`.
|
|
15
|
-
*
|
|
16
|
-
* Previously those bodies lived inline in `src/content/node-hooks.ts` — a
|
|
17
|
-
* ~2000-line file — next to unrelated hook-handler code. Any silent drift
|
|
18
|
-
* only surfaced when someone remembered to update both sides.
|
|
19
|
-
*
|
|
20
|
-
* This module centralizes the inline JavaScript snippets so:
|
|
21
|
-
*
|
|
22
|
-
* - There is exactly **one place** (this file) that holds each inline
|
|
23
|
-
* JS body.
|
|
24
|
-
* - Each snippet carries an explicit "mirrors X, parity enforced by Y"
|
|
25
|
-
* header comment and is emitted into `run-hook.mjs` verbatim.
|
|
26
|
-
* - `src/content/node-hooks.ts` only interpolates the snippets, it no
|
|
27
|
-
* longer owns their source code.
|
|
28
|
-
*
|
|
29
|
-
* Parity with the TypeScript canonical implementations is enforced by
|
|
30
|
-
* `tests/unit/ralph-loop-parity.test.ts` and
|
|
31
|
-
* `tests/unit/early-loop-parity.test.ts`. Any structural change to the
|
|
32
|
-
* canonical TS code MUST:
|
|
33
|
-
*
|
|
34
|
-
* 1. Update the matching snippet below.
|
|
35
|
-
* 2. Re-run parity tests for the touched snippet.
|
|
36
|
-
*
|
|
37
|
-
* DO NOT inline tests here — keep the parity check in its dedicated test
|
|
38
|
-
* file.
|
|
39
|
-
*/
|
|
40
|
-
/**
|
|
41
|
-
* Inline JS helpers used by both compound-readiness and ralph-loop
|
|
42
|
-
* snippets. Kept small and locked: they are shared across the two inline
|
|
43
|
-
* routines and must not grow into a hidden utility namespace.
|
|
44
|
-
*
|
|
45
|
-
* - `normalizeCompoundLastUpdatedAt` produces a stable ISO-8601 UTC
|
|
46
|
-
* timestamp so the hook-written `compound-readiness.json` is byte-equal
|
|
47
|
-
* to the CLI-written version for the same input.
|
|
48
|
-
* - `countArchivedRunsInline` counts immediate subdirectories of
|
|
49
|
-
* `<root>/.cclaw/archive/` so both the hook and the CLI see the same
|
|
50
|
-
* `archivedRunsCount` for the small-project relaxation.
|
|
51
|
-
* - `formatCompoundReadinessLineInline` mirrors the one-line summary shape
|
|
52
|
-
* used by `src/internal/compound-readiness.ts::formatCompoundReadinessLine`
|
|
53
|
-
* so session-start and internal CLI command stay wording-compatible.
|
|
54
|
-
*/
|
|
55
|
-
export declare const HOOK_INLINE_SHARED_HELPERS = "\nfunction normalizeCompoundLastUpdatedAt(date) {\n return date.toISOString().replace(/\\.\\d{3}Z$/u, \"Z\");\n}\n\n// Count archived runs as sub-directories under `.cclaw/archive/`. Missing\n// dir returns 0; unexpected errors return undefined so the caller can\n// skip the small-project relaxation rather than guess.\nasync function countArchivedRunsInline(root) {\n const dir = path.join(root, RUNTIME_ROOT, \"archive\");\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n return entries.filter((entry) => entry.isDirectory()).length;\n } catch (error) {\n const code = error && typeof error === \"object\" && \"code\" in error ? error.code : null;\n if (code === \"ENOENT\") return 0;\n return undefined;\n }\n}\n\nfunction formatCompoundReadinessLineInline(readiness) {\n if (!readiness || typeof readiness !== \"object\") {\n return \"\";\n }\n const ready = Array.isArray(readiness.ready) ? readiness.ready : [];\n const readyCount =\n typeof readiness.readyCount === \"number\" && Number.isFinite(readiness.readyCount)\n ? Math.trunc(readiness.readyCount)\n : ready.length;\n const clusterCount =\n typeof readiness.clusterCount === \"number\" && Number.isFinite(readiness.clusterCount)\n ? Math.trunc(readiness.clusterCount)\n : 0;\n const threshold =\n typeof readiness.threshold === \"number\" && Number.isFinite(readiness.threshold)\n ? Math.trunc(readiness.threshold)\n : COMPOUND_RECURRENCE_THRESHOLD;\n if (readyCount === 0) {\n return \"Compound readiness: no candidates (clusters=\" +\n String(clusterCount) + \", threshold=\" + String(threshold) + \")\";\n }\n const critical = ready.filter(\n (entry) => entry && typeof entry === \"object\" && entry.severity === \"critical\"\n ).length;\n const criticalSuffix = critical > 0 ? \" (critical=\" + String(critical) + \")\" : \"\";\n return \"Compound readiness: clusters=\" + String(clusterCount) +\n \", ready=\" + String(readyCount) + criticalSuffix;\n}\n";
|
|
56
|
-
/**
|
|
57
|
-
* Inline mirror of `src/knowledge-store.ts::computeCompoundReadiness`.
|
|
58
|
-
*
|
|
59
|
-
* Parity enforced by
|
|
60
|
-
* `tests/unit/ralph-loop-parity.test.ts::compound-readiness parity`.
|
|
61
|
-
*
|
|
62
|
-
* Signature contract:
|
|
63
|
-
* async function computeCompoundReadinessInline(root, options) -> CompoundReadiness
|
|
64
|
-
*
|
|
65
|
-
* Accepted options (all optional):
|
|
66
|
-
* - prereadRaw: string | undefined — pre-read `knowledge.jsonl` contents.
|
|
67
|
-
* - threshold: integer >= 1 — default recurrence threshold.
|
|
68
|
-
* - archivedRunsCount: integer >= 0 — enables small-project relaxation.
|
|
69
|
-
* - maxReady: integer >= 1 — cap on returned `ready` cluster count
|
|
70
|
-
* (default 10).
|
|
71
|
-
*
|
|
72
|
-
* Depends on: `SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD`,
|
|
73
|
-
* `SMALL_PROJECT_RECURRENCE_THRESHOLD`, `COMPOUND_RECURRENCE_THRESHOLD`,
|
|
74
|
-
* and `HOOK_INLINE_SHARED_HELPERS` being in the same runtime scope.
|
|
75
|
-
*/
|
|
76
|
-
export declare const COMPOUND_READINESS_INLINE_SOURCE = "\nasync function computeCompoundReadinessInline(root, options) {\n const filePath = path.join(root, RUNTIME_ROOT, \"knowledge.jsonl\");\n // Caller may supply pre-read raw to avoid double-reading knowledge.jsonl.\n const raw = typeof (options && options.prereadRaw) === \"string\"\n ? options.prereadRaw\n : await readTextFile(filePath, \"\");\n const baseThresholdRaw = options && options.threshold;\n const baseThreshold = Number.isInteger(baseThresholdRaw) && baseThresholdRaw >= 1\n ? baseThresholdRaw\n : COMPOUND_RECURRENCE_THRESHOLD;\n const archivedRunsCount =\n typeof (options && options.archivedRunsCount) === \"number\" &&\n Number.isFinite(options.archivedRunsCount) &&\n options.archivedRunsCount >= 0\n ? Math.floor(options.archivedRunsCount)\n : undefined;\n const smallProjectRelaxationApplied =\n archivedRunsCount !== undefined &&\n archivedRunsCount < SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD &&\n baseThreshold > SMALL_PROJECT_RECURRENCE_THRESHOLD;\n const threshold = smallProjectRelaxationApplied\n ? SMALL_PROJECT_RECURRENCE_THRESHOLD\n : baseThreshold;\n const maxReady = Number.isInteger(options && options.maxReady) && options.maxReady >= 1\n ? options.maxReady\n : 10;\n const normalize = (value) => String(value == null ? \"\" : value).trim().replace(/\\s+/gu, \" \").toLowerCase();\n const severityWeight = (sev) => {\n if (sev === \"critical\") return 3;\n if (sev === \"important\") return 2;\n if (sev === \"suggestion\") return 1;\n return 0;\n };\n const buckets = new Map();\n for (const rawLine of raw.split(/\\r?\\n/gu)) {\n const line = rawLine.trim();\n if (line.length === 0) continue;\n let row;\n try { row = JSON.parse(line); } catch { continue; }\n if (!row || typeof row !== \"object\" || Array.isArray(row)) continue;\n if (row.maturity === \"lifted-to-enforcement\" || typeof row.superseded_by === \"string\") continue;\n const type = typeof row.type === \"string\" ? row.type : \"\";\n const trigger = typeof row.trigger === \"string\" ? row.trigger : \"\";\n const action = typeof row.action === \"string\" ? row.action : \"\";\n if (type.length === 0 || trigger.length === 0 || action.length === 0) continue;\n const key = type + \"||\" + normalize(trigger) + \"||\" + normalize(action);\n const frequency = Number.isInteger(row.frequency) && row.frequency > 0 ? Math.floor(row.frequency) : 1;\n const lastSeen = typeof row.last_seen_ts === \"string\" ? row.last_seen_ts : \"\";\n let bucket = buckets.get(key);\n if (!bucket) {\n bucket = {\n trigger,\n action,\n recurrence: frequency,\n entryCount: 1,\n severity: typeof row.severity === \"string\" ? row.severity : undefined,\n lastSeenTs: lastSeen,\n types: new Set([type]),\n maturity: new Set([typeof row.maturity === \"string\" ? row.maturity : \"raw\"])\n };\n buckets.set(key, bucket);\n continue;\n }\n bucket.recurrence += frequency;\n bucket.entryCount += 1;\n bucket.types.add(type);\n bucket.maturity.add(typeof row.maturity === \"string\" ? row.maturity : \"raw\");\n if (row.severity === \"critical\") {\n bucket.severity = \"critical\";\n } else if (row.severity === \"important\" && bucket.severity !== \"critical\") {\n bucket.severity = \"important\";\n }\n if (lastSeen && Date.parse(lastSeen) > Date.parse(bucket.lastSeenTs || \"0\")) {\n bucket.lastSeenTs = lastSeen;\n }\n }\n const ready = [];\n for (const bucket of buckets.values()) {\n const criticalOverride = bucket.severity === \"critical\";\n const meetsRecurrence = bucket.recurrence >= threshold;\n if (!criticalOverride && !meetsRecurrence) continue;\n ready.push({\n trigger: bucket.trigger,\n action: bucket.action,\n recurrence: bucket.recurrence,\n entryCount: bucket.entryCount,\n qualification: criticalOverride && !meetsRecurrence ? \"critical_override\" : \"recurrence\",\n ...(bucket.severity ? { severity: bucket.severity } : {}),\n lastSeenTs: bucket.lastSeenTs,\n types: Array.from(bucket.types).sort(),\n maturity: Array.from(bucket.maturity).sort()\n });\n }\n ready.sort((a, b) => {\n const sevDiff = severityWeight(b.severity) - severityWeight(a.severity);\n if (sevDiff !== 0) return sevDiff;\n if (b.recurrence !== a.recurrence) return b.recurrence - a.recurrence;\n const recencyDiff = Date.parse(b.lastSeenTs || \"0\") - Date.parse(a.lastSeenTs || \"0\");\n if (!Number.isNaN(recencyDiff) && recencyDiff !== 0) return recencyDiff;\n return String(a.trigger).localeCompare(String(b.trigger));\n });\n return {\n schemaVersion: 2,\n threshold,\n baseThreshold,\n ...(archivedRunsCount !== undefined ? { archivedRunsCount } : {}),\n smallProjectRelaxationApplied,\n clusterCount: buckets.size,\n readyCount: ready.length,\n ready: ready.slice(0, maxReady),\n lastUpdatedAt: normalizeCompoundLastUpdatedAt(new Date())\n };\n}\n";
|
|
77
|
-
/**
|
|
78
|
-
* Inline mirror of `src/tdd-cycle.ts::computeRalphLoopStatus`.
|
|
79
|
-
*
|
|
80
|
-
* Parity enforced by
|
|
81
|
-
* `tests/unit/ralph-loop-parity.test.ts::ralph-loop parity`.
|
|
82
|
-
*
|
|
83
|
-
* Signature contract:
|
|
84
|
-
* async function computeRalphLoopStatusInline(stateDir, runId) -> RalphLoopStatus
|
|
85
|
-
*/
|
|
86
|
-
export declare const RALPH_LOOP_INLINE_SOURCE = "\nasync function computeRalphLoopStatusInline(stateDir, runId) {\n const filePath = path.join(stateDir, \"tdd-cycle-log.jsonl\");\n const raw = await readTextFile(filePath, \"\");\n const sliceMap = new Map();\n const acClosed = new Set();\n const redOpenSlices = [];\n let loopIteration = 0;\n for (const rawLine of raw.split(/\\r?\\n/gu)) {\n const line = rawLine.trim();\n if (line.length === 0) continue;\n let row;\n try { row = JSON.parse(line); } catch { continue; }\n if (!row || typeof row !== \"object\" || Array.isArray(row)) continue;\n const rowRun = typeof row.runId === \"string\" && row.runId.length > 0 ? row.runId : runId;\n if (rowRun !== runId) continue;\n const slice = typeof row.slice === \"string\" && row.slice.length > 0 ? row.slice : \"S-unknown\";\n let state = sliceMap.get(slice);\n if (!state) {\n state = { slice, redCount: 0, greenCount: 0, refactorCount: 0, redOpen: false, acIds: [] };\n sliceMap.set(slice, state);\n }\n const exitCode = typeof row.exitCode === \"number\" ? row.exitCode : undefined;\n if (row.phase === \"red\") {\n state.redCount += 1;\n if (exitCode !== undefined && exitCode !== 0) state.redOpen = true;\n } else if (row.phase === \"green\") {\n state.greenCount += 1;\n state.redOpen = false;\n loopIteration += 1;\n if (Array.isArray(row.acIds)) {\n for (const acId of row.acIds) {\n if (typeof acId !== \"string\" || acId.length === 0) continue;\n acClosed.add(acId);\n if (!state.acIds.includes(acId)) state.acIds.push(acId);\n }\n }\n } else if (row.phase === \"refactor\") {\n state.refactorCount += 1;\n }\n }\n for (const state of sliceMap.values()) {\n if (state.redOpen) redOpenSlices.push(state.slice);\n }\n const slices = Array.from(sliceMap.values()).sort((a, b) => a.slice.localeCompare(b.slice, \"en\"));\n return {\n schemaVersion: 1,\n runId,\n loopIteration,\n redOpen: redOpenSlices.length > 0,\n redOpenSlices,\n acClosed: Array.from(acClosed).sort(),\n sliceCount: slices.length,\n slices,\n lastUpdatedAt: new Date().toISOString()\n };\n}\n";
|
|
87
|
-
/**
|
|
88
|
-
* Inline mirror of `src/early-loop.ts::computeEarlyLoopStatus`.
|
|
89
|
-
*
|
|
90
|
-
* Parity enforced by
|
|
91
|
-
* `tests/unit/early-loop-parity.test.ts::early-loop parity`.
|
|
92
|
-
*
|
|
93
|
-
* Signature contract:
|
|
94
|
-
* async function computeEarlyLoopStatusInline(stateDir, stageId, runId, maxIterations) -> EarlyLoopStatus
|
|
95
|
-
*/
|
|
96
|
-
export declare const EARLY_LOOP_INLINE_SOURCE = "\nfunction normalizeEarlyLoopSeverityInline(value) {\n if (value === \"critical\" || value === \"important\" || value === \"suggestion\") {\n return value;\n }\n return \"important\";\n}\n\nfunction normalizeEarlyLoopTextInline(value, fallback) {\n if (typeof value !== \"string\") return fallback;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : fallback;\n}\n\nfunction stableConcernFallbackIdInline(locator, summary) {\n const seed = (String(locator) + \"::\" + String(summary)).trim().toLowerCase();\n let hash = 0;\n for (let index = 0; index < seed.length; index += 1) {\n hash = (Math.imul(31, hash) + seed.charCodeAt(index)) >>> 0;\n }\n return \"C-\" + hash.toString(16).padStart(8, \"0\");\n}\n\nfunction normalizeEarlyLoopConcernInline(row) {\n if (!row || typeof row !== \"object\" || Array.isArray(row)) return null;\n const locator = normalizeEarlyLoopTextInline(row.locator, \"unknown-location\");\n const summary = normalizeEarlyLoopTextInline(row.summary, \"missing-summary\");\n const id = typeof row.id === \"string\" && row.id.trim().length > 0\n ? row.id.trim()\n : stableConcernFallbackIdInline(locator, summary);\n return {\n id,\n severity: normalizeEarlyLoopSeverityInline(row.severity),\n locator,\n summary\n };\n}\n\nfunction normalizeEarlyLoopMaxIterationsInline(value) {\n return Number.isInteger(value) && value >= 1 ? value : 3;\n}\n\nfunction earlyLoopSeverityWeightInline(value) {\n if (value === \"critical\") return 3;\n if (value === \"important\") return 2;\n return 1;\n}\n\nfunction sortEarlyLoopConcernsInline(a, b) {\n const severityDiff = earlyLoopSeverityWeightInline(b.severity) - earlyLoopSeverityWeightInline(a.severity);\n if (severityDiff !== 0) return severityDiff;\n if (a.firstSeenIteration !== b.firstSeenIteration) {\n return a.firstSeenIteration - b.firstSeenIteration;\n }\n if (a.lastSeenIteration !== b.lastSeenIteration) {\n return a.lastSeenIteration - b.lastSeenIteration;\n }\n return String(a.id).localeCompare(String(b.id), \"en\");\n}\n\nfunction formatEarlyLoopStatusLineInline(status) {\n if (!status || typeof status !== \"object\") return \"\";\n const convergence = status.convergenceTripped ? \"tripped\" : \"clear\";\n return \"Early Loop: stage=\" + String(status.stage) +\n \", iter=\" + String(status.iteration) + \"/\" + String(status.maxIterations) +\n \", open=\" + String(Array.isArray(status.openConcerns) ? status.openConcerns.length : 0) +\n \", convergence=\" + convergence;\n}\n\nasync function computeEarlyLoopStatusInline(stateDir, stageId, runId, maxIterations) {\n const filePath = path.join(stateDir, \"early-loop-log.jsonl\");\n const raw = await readTextFile(filePath, \"\");\n const maxIters = normalizeEarlyLoopMaxIterationsInline(maxIterations);\n const concernsMap = new Map();\n let previousSnapshotKey = \"\";\n let sameConcernStreak = 0;\n let convergenceTripped = false;\n let escalationReason = undefined;\n let currentIteration = 0;\n let lastSeenConcernIds = [];\n\n for (const rawLine of raw.split(/\\r?\\n/gu)) {\n const line = rawLine.trim();\n if (line.length === 0) continue;\n let parsed;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n const rowRunId = typeof parsed.runId === \"string\" && parsed.runId.trim().length > 0\n ? parsed.runId.trim()\n : \"active\";\n const rowStage = typeof parsed.stage === \"string\" && parsed.stage.trim().length > 0\n ? parsed.stage.trim()\n : \"brainstorm\";\n if (rowRunId !== runId || rowStage !== stageId) continue;\n\n currentIteration += 1;\n const iteration = Number.isInteger(parsed.iteration) && parsed.iteration >= 1\n ? parsed.iteration\n : currentIteration;\n const seenThisIteration = new Set();\n const concerns = Array.isArray(parsed.concerns) ? parsed.concerns : [];\n for (const rawConcern of concerns) {\n const concern = normalizeEarlyLoopConcernInline(rawConcern);\n if (!concern) continue;\n seenThisIteration.add(concern.id);\n const existing = concernsMap.get(concern.id);\n if (!existing) {\n concernsMap.set(concern.id, {\n id: concern.id,\n severity: concern.severity,\n locator: concern.locator,\n summary: concern.summary,\n firstSeenIteration: iteration,\n lastSeenIteration: iteration\n });\n continue;\n }\n existing.lastSeenIteration = iteration;\n existing.locator = concern.locator;\n existing.summary = concern.summary;\n if (earlyLoopSeverityWeightInline(concern.severity) >= earlyLoopSeverityWeightInline(existing.severity)) {\n existing.severity = concern.severity;\n }\n delete existing.resolvedAtIteration;\n }\n\n const resolvedConcernIds = Array.isArray(parsed.resolvedConcernIds)\n ? parsed.resolvedConcernIds\n .filter((entry) => typeof entry === \"string\" && entry.trim().length > 0)\n .map((entry) => entry.trim())\n : [];\n for (const concernId of resolvedConcernIds) {\n const existing = concernsMap.get(concernId);\n if (!existing) continue;\n if (seenThisIteration.has(concernId)) continue;\n if (existing.resolvedAtIteration === undefined) {\n existing.resolvedAtIteration = iteration;\n }\n }\n\n for (const concern of concernsMap.values()) {\n if (concern.resolvedAtIteration !== undefined) continue;\n if (seenThisIteration.has(concern.id)) continue;\n concern.resolvedAtIteration = iteration;\n }\n\n const openConcernIds = Array.from(concernsMap.values())\n .filter((concern) => concern.resolvedAtIteration === undefined)\n .map((concern) => concern.id)\n .sort((a, b) => String(a).localeCompare(String(b), \"en\"));\n lastSeenConcernIds = openConcernIds;\n const snapshotKey = openConcernIds.join(\"|\");\n if (snapshotKey.length > 0 && snapshotKey === previousSnapshotKey) {\n sameConcernStreak += 1;\n if (!convergenceTripped && sameConcernStreak >= 2) {\n convergenceTripped = true;\n escalationReason = \"same concerns \" + String(sameConcernStreak) + \" iterations in a row\";\n }\n } else {\n sameConcernStreak = snapshotKey.length > 0 ? 1 : 0;\n }\n previousSnapshotKey = snapshotKey;\n }\n\n const openConcerns = Array.from(concernsMap.values())\n .filter((concern) => concern.resolvedAtIteration === undefined)\n .sort(sortEarlyLoopConcernsInline);\n const resolvedConcerns = Array.from(concernsMap.values())\n .filter((concern) => concern.resolvedAtIteration !== undefined)\n .sort((a, b) => {\n if (a.resolvedAtIteration !== b.resolvedAtIteration) {\n return a.resolvedAtIteration - b.resolvedAtIteration;\n }\n return sortEarlyLoopConcernsInline(a, b);\n });\n\n if (!convergenceTripped && openConcerns.length > 0 && currentIteration >= maxIters) {\n convergenceTripped = true;\n escalationReason = \"max iterations \" + String(maxIters) +\n \" reached with \" + String(openConcerns.length) + \" open concern(s)\";\n }\n\n return {\n schemaVersion: 1,\n stage: stageId,\n runId,\n iteration: currentIteration,\n maxIterations: maxIters,\n openConcerns,\n resolvedConcerns,\n lastSeenConcernIds,\n convergenceTripped,\n ...(escalationReason ? { escalationReason } : {}),\n lastUpdatedAt: new Date().toISOString()\n };\n}\n";
|
|
@@ -1,515 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hook-inline-snippets.ts
|
|
3
|
-
*
|
|
4
|
-
* Runtime `.cclaw/hooks/run-hook.mjs` is a **standalone Node script** that
|
|
5
|
-
* cannot import from `cclaw-cli` — it must work inside the end-user's
|
|
6
|
-
* project even when the CLI is not installed. Two derived computations,
|
|
7
|
-
* though, must remain 1:1 with the canonical TS implementations:
|
|
8
|
-
*
|
|
9
|
-
* 1. `computeCompoundReadinessInline` mirrors
|
|
10
|
-
* `src/knowledge-store.ts::computeCompoundReadiness`.
|
|
11
|
-
* 2. `computeRalphLoopStatusInline` mirrors
|
|
12
|
-
* `src/tdd-cycle.ts::computeRalphLoopStatus`.
|
|
13
|
-
* 3. `computeEarlyLoopStatusInline` mirrors
|
|
14
|
-
* `src/early-loop.ts::computeEarlyLoopStatus`.
|
|
15
|
-
*
|
|
16
|
-
* Previously those bodies lived inline in `src/content/node-hooks.ts` — a
|
|
17
|
-
* ~2000-line file — next to unrelated hook-handler code. Any silent drift
|
|
18
|
-
* only surfaced when someone remembered to update both sides.
|
|
19
|
-
*
|
|
20
|
-
* This module centralizes the inline JavaScript snippets so:
|
|
21
|
-
*
|
|
22
|
-
* - There is exactly **one place** (this file) that holds each inline
|
|
23
|
-
* JS body.
|
|
24
|
-
* - Each snippet carries an explicit "mirrors X, parity enforced by Y"
|
|
25
|
-
* header comment and is emitted into `run-hook.mjs` verbatim.
|
|
26
|
-
* - `src/content/node-hooks.ts` only interpolates the snippets, it no
|
|
27
|
-
* longer owns their source code.
|
|
28
|
-
*
|
|
29
|
-
* Parity with the TypeScript canonical implementations is enforced by
|
|
30
|
-
* `tests/unit/ralph-loop-parity.test.ts` and
|
|
31
|
-
* `tests/unit/early-loop-parity.test.ts`. Any structural change to the
|
|
32
|
-
* canonical TS code MUST:
|
|
33
|
-
*
|
|
34
|
-
* 1. Update the matching snippet below.
|
|
35
|
-
* 2. Re-run parity tests for the touched snippet.
|
|
36
|
-
*
|
|
37
|
-
* DO NOT inline tests here — keep the parity check in its dedicated test
|
|
38
|
-
* file.
|
|
39
|
-
*/
|
|
40
|
-
/**
|
|
41
|
-
* Inline JS helpers used by both compound-readiness and ralph-loop
|
|
42
|
-
* snippets. Kept small and locked: they are shared across the two inline
|
|
43
|
-
* routines and must not grow into a hidden utility namespace.
|
|
44
|
-
*
|
|
45
|
-
* - `normalizeCompoundLastUpdatedAt` produces a stable ISO-8601 UTC
|
|
46
|
-
* timestamp so the hook-written `compound-readiness.json` is byte-equal
|
|
47
|
-
* to the CLI-written version for the same input.
|
|
48
|
-
* - `countArchivedRunsInline` counts immediate subdirectories of
|
|
49
|
-
* `<root>/.cclaw/archive/` so both the hook and the CLI see the same
|
|
50
|
-
* `archivedRunsCount` for the small-project relaxation.
|
|
51
|
-
* - `formatCompoundReadinessLineInline` mirrors the one-line summary shape
|
|
52
|
-
* used by `src/internal/compound-readiness.ts::formatCompoundReadinessLine`
|
|
53
|
-
* so session-start and internal CLI command stay wording-compatible.
|
|
54
|
-
*/
|
|
55
|
-
export const HOOK_INLINE_SHARED_HELPERS = `
|
|
56
|
-
function normalizeCompoundLastUpdatedAt(date) {
|
|
57
|
-
return date.toISOString().replace(/\\.\\d{3}Z$/u, "Z");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Count archived runs as sub-directories under \`.cclaw/archive/\`. Missing
|
|
61
|
-
// dir returns 0; unexpected errors return undefined so the caller can
|
|
62
|
-
// skip the small-project relaxation rather than guess.
|
|
63
|
-
async function countArchivedRunsInline(root) {
|
|
64
|
-
const dir = path.join(root, RUNTIME_ROOT, "archive");
|
|
65
|
-
try {
|
|
66
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
67
|
-
return entries.filter((entry) => entry.isDirectory()).length;
|
|
68
|
-
} catch (error) {
|
|
69
|
-
const code = error && typeof error === "object" && "code" in error ? error.code : null;
|
|
70
|
-
if (code === "ENOENT") return 0;
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function formatCompoundReadinessLineInline(readiness) {
|
|
76
|
-
if (!readiness || typeof readiness !== "object") {
|
|
77
|
-
return "";
|
|
78
|
-
}
|
|
79
|
-
const ready = Array.isArray(readiness.ready) ? readiness.ready : [];
|
|
80
|
-
const readyCount =
|
|
81
|
-
typeof readiness.readyCount === "number" && Number.isFinite(readiness.readyCount)
|
|
82
|
-
? Math.trunc(readiness.readyCount)
|
|
83
|
-
: ready.length;
|
|
84
|
-
const clusterCount =
|
|
85
|
-
typeof readiness.clusterCount === "number" && Number.isFinite(readiness.clusterCount)
|
|
86
|
-
? Math.trunc(readiness.clusterCount)
|
|
87
|
-
: 0;
|
|
88
|
-
const threshold =
|
|
89
|
-
typeof readiness.threshold === "number" && Number.isFinite(readiness.threshold)
|
|
90
|
-
? Math.trunc(readiness.threshold)
|
|
91
|
-
: COMPOUND_RECURRENCE_THRESHOLD;
|
|
92
|
-
if (readyCount === 0) {
|
|
93
|
-
return "Compound readiness: no candidates (clusters=" +
|
|
94
|
-
String(clusterCount) + ", threshold=" + String(threshold) + ")";
|
|
95
|
-
}
|
|
96
|
-
const critical = ready.filter(
|
|
97
|
-
(entry) => entry && typeof entry === "object" && entry.severity === "critical"
|
|
98
|
-
).length;
|
|
99
|
-
const criticalSuffix = critical > 0 ? " (critical=" + String(critical) + ")" : "";
|
|
100
|
-
return "Compound readiness: clusters=" + String(clusterCount) +
|
|
101
|
-
", ready=" + String(readyCount) + criticalSuffix;
|
|
102
|
-
}
|
|
103
|
-
`;
|
|
104
|
-
/**
|
|
105
|
-
* Inline mirror of `src/knowledge-store.ts::computeCompoundReadiness`.
|
|
106
|
-
*
|
|
107
|
-
* Parity enforced by
|
|
108
|
-
* `tests/unit/ralph-loop-parity.test.ts::compound-readiness parity`.
|
|
109
|
-
*
|
|
110
|
-
* Signature contract:
|
|
111
|
-
* async function computeCompoundReadinessInline(root, options) -> CompoundReadiness
|
|
112
|
-
*
|
|
113
|
-
* Accepted options (all optional):
|
|
114
|
-
* - prereadRaw: string | undefined — pre-read `knowledge.jsonl` contents.
|
|
115
|
-
* - threshold: integer >= 1 — default recurrence threshold.
|
|
116
|
-
* - archivedRunsCount: integer >= 0 — enables small-project relaxation.
|
|
117
|
-
* - maxReady: integer >= 1 — cap on returned `ready` cluster count
|
|
118
|
-
* (default 10).
|
|
119
|
-
*
|
|
120
|
-
* Depends on: `SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD`,
|
|
121
|
-
* `SMALL_PROJECT_RECURRENCE_THRESHOLD`, `COMPOUND_RECURRENCE_THRESHOLD`,
|
|
122
|
-
* and `HOOK_INLINE_SHARED_HELPERS` being in the same runtime scope.
|
|
123
|
-
*/
|
|
124
|
-
export const COMPOUND_READINESS_INLINE_SOURCE = `
|
|
125
|
-
async function computeCompoundReadinessInline(root, options) {
|
|
126
|
-
const filePath = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
127
|
-
// Caller may supply pre-read raw to avoid double-reading knowledge.jsonl.
|
|
128
|
-
const raw = typeof (options && options.prereadRaw) === "string"
|
|
129
|
-
? options.prereadRaw
|
|
130
|
-
: await readTextFile(filePath, "");
|
|
131
|
-
const baseThresholdRaw = options && options.threshold;
|
|
132
|
-
const baseThreshold = Number.isInteger(baseThresholdRaw) && baseThresholdRaw >= 1
|
|
133
|
-
? baseThresholdRaw
|
|
134
|
-
: COMPOUND_RECURRENCE_THRESHOLD;
|
|
135
|
-
const archivedRunsCount =
|
|
136
|
-
typeof (options && options.archivedRunsCount) === "number" &&
|
|
137
|
-
Number.isFinite(options.archivedRunsCount) &&
|
|
138
|
-
options.archivedRunsCount >= 0
|
|
139
|
-
? Math.floor(options.archivedRunsCount)
|
|
140
|
-
: undefined;
|
|
141
|
-
const smallProjectRelaxationApplied =
|
|
142
|
-
archivedRunsCount !== undefined &&
|
|
143
|
-
archivedRunsCount < SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD &&
|
|
144
|
-
baseThreshold > SMALL_PROJECT_RECURRENCE_THRESHOLD;
|
|
145
|
-
const threshold = smallProjectRelaxationApplied
|
|
146
|
-
? SMALL_PROJECT_RECURRENCE_THRESHOLD
|
|
147
|
-
: baseThreshold;
|
|
148
|
-
const maxReady = Number.isInteger(options && options.maxReady) && options.maxReady >= 1
|
|
149
|
-
? options.maxReady
|
|
150
|
-
: 10;
|
|
151
|
-
const normalize = (value) => String(value == null ? "" : value).trim().replace(/\\s+/gu, " ").toLowerCase();
|
|
152
|
-
const severityWeight = (sev) => {
|
|
153
|
-
if (sev === "critical") return 3;
|
|
154
|
-
if (sev === "important") return 2;
|
|
155
|
-
if (sev === "suggestion") return 1;
|
|
156
|
-
return 0;
|
|
157
|
-
};
|
|
158
|
-
const buckets = new Map();
|
|
159
|
-
for (const rawLine of raw.split(/\\r?\\n/gu)) {
|
|
160
|
-
const line = rawLine.trim();
|
|
161
|
-
if (line.length === 0) continue;
|
|
162
|
-
let row;
|
|
163
|
-
try { row = JSON.parse(line); } catch { continue; }
|
|
164
|
-
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
165
|
-
if (row.maturity === "lifted-to-enforcement" || typeof row.superseded_by === "string") continue;
|
|
166
|
-
const type = typeof row.type === "string" ? row.type : "";
|
|
167
|
-
const trigger = typeof row.trigger === "string" ? row.trigger : "";
|
|
168
|
-
const action = typeof row.action === "string" ? row.action : "";
|
|
169
|
-
if (type.length === 0 || trigger.length === 0 || action.length === 0) continue;
|
|
170
|
-
const key = type + "||" + normalize(trigger) + "||" + normalize(action);
|
|
171
|
-
const frequency = Number.isInteger(row.frequency) && row.frequency > 0 ? Math.floor(row.frequency) : 1;
|
|
172
|
-
const lastSeen = typeof row.last_seen_ts === "string" ? row.last_seen_ts : "";
|
|
173
|
-
let bucket = buckets.get(key);
|
|
174
|
-
if (!bucket) {
|
|
175
|
-
bucket = {
|
|
176
|
-
trigger,
|
|
177
|
-
action,
|
|
178
|
-
recurrence: frequency,
|
|
179
|
-
entryCount: 1,
|
|
180
|
-
severity: typeof row.severity === "string" ? row.severity : undefined,
|
|
181
|
-
lastSeenTs: lastSeen,
|
|
182
|
-
types: new Set([type]),
|
|
183
|
-
maturity: new Set([typeof row.maturity === "string" ? row.maturity : "raw"])
|
|
184
|
-
};
|
|
185
|
-
buckets.set(key, bucket);
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
bucket.recurrence += frequency;
|
|
189
|
-
bucket.entryCount += 1;
|
|
190
|
-
bucket.types.add(type);
|
|
191
|
-
bucket.maturity.add(typeof row.maturity === "string" ? row.maturity : "raw");
|
|
192
|
-
if (row.severity === "critical") {
|
|
193
|
-
bucket.severity = "critical";
|
|
194
|
-
} else if (row.severity === "important" && bucket.severity !== "critical") {
|
|
195
|
-
bucket.severity = "important";
|
|
196
|
-
}
|
|
197
|
-
if (lastSeen && Date.parse(lastSeen) > Date.parse(bucket.lastSeenTs || "0")) {
|
|
198
|
-
bucket.lastSeenTs = lastSeen;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
const ready = [];
|
|
202
|
-
for (const bucket of buckets.values()) {
|
|
203
|
-
const criticalOverride = bucket.severity === "critical";
|
|
204
|
-
const meetsRecurrence = bucket.recurrence >= threshold;
|
|
205
|
-
if (!criticalOverride && !meetsRecurrence) continue;
|
|
206
|
-
ready.push({
|
|
207
|
-
trigger: bucket.trigger,
|
|
208
|
-
action: bucket.action,
|
|
209
|
-
recurrence: bucket.recurrence,
|
|
210
|
-
entryCount: bucket.entryCount,
|
|
211
|
-
qualification: criticalOverride && !meetsRecurrence ? "critical_override" : "recurrence",
|
|
212
|
-
...(bucket.severity ? { severity: bucket.severity } : {}),
|
|
213
|
-
lastSeenTs: bucket.lastSeenTs,
|
|
214
|
-
types: Array.from(bucket.types).sort(),
|
|
215
|
-
maturity: Array.from(bucket.maturity).sort()
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
ready.sort((a, b) => {
|
|
219
|
-
const sevDiff = severityWeight(b.severity) - severityWeight(a.severity);
|
|
220
|
-
if (sevDiff !== 0) return sevDiff;
|
|
221
|
-
if (b.recurrence !== a.recurrence) return b.recurrence - a.recurrence;
|
|
222
|
-
const recencyDiff = Date.parse(b.lastSeenTs || "0") - Date.parse(a.lastSeenTs || "0");
|
|
223
|
-
if (!Number.isNaN(recencyDiff) && recencyDiff !== 0) return recencyDiff;
|
|
224
|
-
return String(a.trigger).localeCompare(String(b.trigger));
|
|
225
|
-
});
|
|
226
|
-
return {
|
|
227
|
-
schemaVersion: 2,
|
|
228
|
-
threshold,
|
|
229
|
-
baseThreshold,
|
|
230
|
-
...(archivedRunsCount !== undefined ? { archivedRunsCount } : {}),
|
|
231
|
-
smallProjectRelaxationApplied,
|
|
232
|
-
clusterCount: buckets.size,
|
|
233
|
-
readyCount: ready.length,
|
|
234
|
-
ready: ready.slice(0, maxReady),
|
|
235
|
-
lastUpdatedAt: normalizeCompoundLastUpdatedAt(new Date())
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
`;
|
|
239
|
-
/**
|
|
240
|
-
* Inline mirror of `src/tdd-cycle.ts::computeRalphLoopStatus`.
|
|
241
|
-
*
|
|
242
|
-
* Parity enforced by
|
|
243
|
-
* `tests/unit/ralph-loop-parity.test.ts::ralph-loop parity`.
|
|
244
|
-
*
|
|
245
|
-
* Signature contract:
|
|
246
|
-
* async function computeRalphLoopStatusInline(stateDir, runId) -> RalphLoopStatus
|
|
247
|
-
*/
|
|
248
|
-
export const RALPH_LOOP_INLINE_SOURCE = `
|
|
249
|
-
async function computeRalphLoopStatusInline(stateDir, runId) {
|
|
250
|
-
const filePath = path.join(stateDir, "tdd-cycle-log.jsonl");
|
|
251
|
-
const raw = await readTextFile(filePath, "");
|
|
252
|
-
const sliceMap = new Map();
|
|
253
|
-
const acClosed = new Set();
|
|
254
|
-
const redOpenSlices = [];
|
|
255
|
-
let loopIteration = 0;
|
|
256
|
-
for (const rawLine of raw.split(/\\r?\\n/gu)) {
|
|
257
|
-
const line = rawLine.trim();
|
|
258
|
-
if (line.length === 0) continue;
|
|
259
|
-
let row;
|
|
260
|
-
try { row = JSON.parse(line); } catch { continue; }
|
|
261
|
-
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
262
|
-
const rowRun = typeof row.runId === "string" && row.runId.length > 0 ? row.runId : runId;
|
|
263
|
-
if (rowRun !== runId) continue;
|
|
264
|
-
const slice = typeof row.slice === "string" && row.slice.length > 0 ? row.slice : "S-unknown";
|
|
265
|
-
let state = sliceMap.get(slice);
|
|
266
|
-
if (!state) {
|
|
267
|
-
state = { slice, redCount: 0, greenCount: 0, refactorCount: 0, redOpen: false, acIds: [] };
|
|
268
|
-
sliceMap.set(slice, state);
|
|
269
|
-
}
|
|
270
|
-
const exitCode = typeof row.exitCode === "number" ? row.exitCode : undefined;
|
|
271
|
-
if (row.phase === "red") {
|
|
272
|
-
state.redCount += 1;
|
|
273
|
-
if (exitCode !== undefined && exitCode !== 0) state.redOpen = true;
|
|
274
|
-
} else if (row.phase === "green") {
|
|
275
|
-
state.greenCount += 1;
|
|
276
|
-
state.redOpen = false;
|
|
277
|
-
loopIteration += 1;
|
|
278
|
-
if (Array.isArray(row.acIds)) {
|
|
279
|
-
for (const acId of row.acIds) {
|
|
280
|
-
if (typeof acId !== "string" || acId.length === 0) continue;
|
|
281
|
-
acClosed.add(acId);
|
|
282
|
-
if (!state.acIds.includes(acId)) state.acIds.push(acId);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
} else if (row.phase === "refactor") {
|
|
286
|
-
state.refactorCount += 1;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
for (const state of sliceMap.values()) {
|
|
290
|
-
if (state.redOpen) redOpenSlices.push(state.slice);
|
|
291
|
-
}
|
|
292
|
-
const slices = Array.from(sliceMap.values()).sort((a, b) => a.slice.localeCompare(b.slice, "en"));
|
|
293
|
-
return {
|
|
294
|
-
schemaVersion: 1,
|
|
295
|
-
runId,
|
|
296
|
-
loopIteration,
|
|
297
|
-
redOpen: redOpenSlices.length > 0,
|
|
298
|
-
redOpenSlices,
|
|
299
|
-
acClosed: Array.from(acClosed).sort(),
|
|
300
|
-
sliceCount: slices.length,
|
|
301
|
-
slices,
|
|
302
|
-
lastUpdatedAt: new Date().toISOString()
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
`;
|
|
306
|
-
/**
|
|
307
|
-
* Inline mirror of `src/early-loop.ts::computeEarlyLoopStatus`.
|
|
308
|
-
*
|
|
309
|
-
* Parity enforced by
|
|
310
|
-
* `tests/unit/early-loop-parity.test.ts::early-loop parity`.
|
|
311
|
-
*
|
|
312
|
-
* Signature contract:
|
|
313
|
-
* async function computeEarlyLoopStatusInline(stateDir, stageId, runId, maxIterations) -> EarlyLoopStatus
|
|
314
|
-
*/
|
|
315
|
-
export const EARLY_LOOP_INLINE_SOURCE = `
|
|
316
|
-
function normalizeEarlyLoopSeverityInline(value) {
|
|
317
|
-
if (value === "critical" || value === "important" || value === "suggestion") {
|
|
318
|
-
return value;
|
|
319
|
-
}
|
|
320
|
-
return "important";
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function normalizeEarlyLoopTextInline(value, fallback) {
|
|
324
|
-
if (typeof value !== "string") return fallback;
|
|
325
|
-
const trimmed = value.trim();
|
|
326
|
-
return trimmed.length > 0 ? trimmed : fallback;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function stableConcernFallbackIdInline(locator, summary) {
|
|
330
|
-
const seed = (String(locator) + "::" + String(summary)).trim().toLowerCase();
|
|
331
|
-
let hash = 0;
|
|
332
|
-
for (let index = 0; index < seed.length; index += 1) {
|
|
333
|
-
hash = (Math.imul(31, hash) + seed.charCodeAt(index)) >>> 0;
|
|
334
|
-
}
|
|
335
|
-
return "C-" + hash.toString(16).padStart(8, "0");
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function normalizeEarlyLoopConcernInline(row) {
|
|
339
|
-
if (!row || typeof row !== "object" || Array.isArray(row)) return null;
|
|
340
|
-
const locator = normalizeEarlyLoopTextInline(row.locator, "unknown-location");
|
|
341
|
-
const summary = normalizeEarlyLoopTextInline(row.summary, "missing-summary");
|
|
342
|
-
const id = typeof row.id === "string" && row.id.trim().length > 0
|
|
343
|
-
? row.id.trim()
|
|
344
|
-
: stableConcernFallbackIdInline(locator, summary);
|
|
345
|
-
return {
|
|
346
|
-
id,
|
|
347
|
-
severity: normalizeEarlyLoopSeverityInline(row.severity),
|
|
348
|
-
locator,
|
|
349
|
-
summary
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function normalizeEarlyLoopMaxIterationsInline(value) {
|
|
354
|
-
return Number.isInteger(value) && value >= 1 ? value : 3;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function earlyLoopSeverityWeightInline(value) {
|
|
358
|
-
if (value === "critical") return 3;
|
|
359
|
-
if (value === "important") return 2;
|
|
360
|
-
return 1;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function sortEarlyLoopConcernsInline(a, b) {
|
|
364
|
-
const severityDiff = earlyLoopSeverityWeightInline(b.severity) - earlyLoopSeverityWeightInline(a.severity);
|
|
365
|
-
if (severityDiff !== 0) return severityDiff;
|
|
366
|
-
if (a.firstSeenIteration !== b.firstSeenIteration) {
|
|
367
|
-
return a.firstSeenIteration - b.firstSeenIteration;
|
|
368
|
-
}
|
|
369
|
-
if (a.lastSeenIteration !== b.lastSeenIteration) {
|
|
370
|
-
return a.lastSeenIteration - b.lastSeenIteration;
|
|
371
|
-
}
|
|
372
|
-
return String(a.id).localeCompare(String(b.id), "en");
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function formatEarlyLoopStatusLineInline(status) {
|
|
376
|
-
if (!status || typeof status !== "object") return "";
|
|
377
|
-
const convergence = status.convergenceTripped ? "tripped" : "clear";
|
|
378
|
-
return "Early Loop: stage=" + String(status.stage) +
|
|
379
|
-
", iter=" + String(status.iteration) + "/" + String(status.maxIterations) +
|
|
380
|
-
", open=" + String(Array.isArray(status.openConcerns) ? status.openConcerns.length : 0) +
|
|
381
|
-
", convergence=" + convergence;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
async function computeEarlyLoopStatusInline(stateDir, stageId, runId, maxIterations) {
|
|
385
|
-
const filePath = path.join(stateDir, "early-loop-log.jsonl");
|
|
386
|
-
const raw = await readTextFile(filePath, "");
|
|
387
|
-
const maxIters = normalizeEarlyLoopMaxIterationsInline(maxIterations);
|
|
388
|
-
const concernsMap = new Map();
|
|
389
|
-
let previousSnapshotKey = "";
|
|
390
|
-
let sameConcernStreak = 0;
|
|
391
|
-
let convergenceTripped = false;
|
|
392
|
-
let escalationReason = undefined;
|
|
393
|
-
let currentIteration = 0;
|
|
394
|
-
let lastSeenConcernIds = [];
|
|
395
|
-
|
|
396
|
-
for (const rawLine of raw.split(/\\r?\\n/gu)) {
|
|
397
|
-
const line = rawLine.trim();
|
|
398
|
-
if (line.length === 0) continue;
|
|
399
|
-
let parsed;
|
|
400
|
-
try {
|
|
401
|
-
parsed = JSON.parse(line);
|
|
402
|
-
} catch {
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
406
|
-
const rowRunId = typeof parsed.runId === "string" && parsed.runId.trim().length > 0
|
|
407
|
-
? parsed.runId.trim()
|
|
408
|
-
: "active";
|
|
409
|
-
const rowStage = typeof parsed.stage === "string" && parsed.stage.trim().length > 0
|
|
410
|
-
? parsed.stage.trim()
|
|
411
|
-
: "brainstorm";
|
|
412
|
-
if (rowRunId !== runId || rowStage !== stageId) continue;
|
|
413
|
-
|
|
414
|
-
currentIteration += 1;
|
|
415
|
-
const iteration = Number.isInteger(parsed.iteration) && parsed.iteration >= 1
|
|
416
|
-
? parsed.iteration
|
|
417
|
-
: currentIteration;
|
|
418
|
-
const seenThisIteration = new Set();
|
|
419
|
-
const concerns = Array.isArray(parsed.concerns) ? parsed.concerns : [];
|
|
420
|
-
for (const rawConcern of concerns) {
|
|
421
|
-
const concern = normalizeEarlyLoopConcernInline(rawConcern);
|
|
422
|
-
if (!concern) continue;
|
|
423
|
-
seenThisIteration.add(concern.id);
|
|
424
|
-
const existing = concernsMap.get(concern.id);
|
|
425
|
-
if (!existing) {
|
|
426
|
-
concernsMap.set(concern.id, {
|
|
427
|
-
id: concern.id,
|
|
428
|
-
severity: concern.severity,
|
|
429
|
-
locator: concern.locator,
|
|
430
|
-
summary: concern.summary,
|
|
431
|
-
firstSeenIteration: iteration,
|
|
432
|
-
lastSeenIteration: iteration
|
|
433
|
-
});
|
|
434
|
-
continue;
|
|
435
|
-
}
|
|
436
|
-
existing.lastSeenIteration = iteration;
|
|
437
|
-
existing.locator = concern.locator;
|
|
438
|
-
existing.summary = concern.summary;
|
|
439
|
-
if (earlyLoopSeverityWeightInline(concern.severity) >= earlyLoopSeverityWeightInline(existing.severity)) {
|
|
440
|
-
existing.severity = concern.severity;
|
|
441
|
-
}
|
|
442
|
-
delete existing.resolvedAtIteration;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const resolvedConcernIds = Array.isArray(parsed.resolvedConcernIds)
|
|
446
|
-
? parsed.resolvedConcernIds
|
|
447
|
-
.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
448
|
-
.map((entry) => entry.trim())
|
|
449
|
-
: [];
|
|
450
|
-
for (const concernId of resolvedConcernIds) {
|
|
451
|
-
const existing = concernsMap.get(concernId);
|
|
452
|
-
if (!existing) continue;
|
|
453
|
-
if (seenThisIteration.has(concernId)) continue;
|
|
454
|
-
if (existing.resolvedAtIteration === undefined) {
|
|
455
|
-
existing.resolvedAtIteration = iteration;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
for (const concern of concernsMap.values()) {
|
|
460
|
-
if (concern.resolvedAtIteration !== undefined) continue;
|
|
461
|
-
if (seenThisIteration.has(concern.id)) continue;
|
|
462
|
-
concern.resolvedAtIteration = iteration;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const openConcernIds = Array.from(concernsMap.values())
|
|
466
|
-
.filter((concern) => concern.resolvedAtIteration === undefined)
|
|
467
|
-
.map((concern) => concern.id)
|
|
468
|
-
.sort((a, b) => String(a).localeCompare(String(b), "en"));
|
|
469
|
-
lastSeenConcernIds = openConcernIds;
|
|
470
|
-
const snapshotKey = openConcernIds.join("|");
|
|
471
|
-
if (snapshotKey.length > 0 && snapshotKey === previousSnapshotKey) {
|
|
472
|
-
sameConcernStreak += 1;
|
|
473
|
-
if (!convergenceTripped && sameConcernStreak >= 2) {
|
|
474
|
-
convergenceTripped = true;
|
|
475
|
-
escalationReason = "same concerns " + String(sameConcernStreak) + " iterations in a row";
|
|
476
|
-
}
|
|
477
|
-
} else {
|
|
478
|
-
sameConcernStreak = snapshotKey.length > 0 ? 1 : 0;
|
|
479
|
-
}
|
|
480
|
-
previousSnapshotKey = snapshotKey;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const openConcerns = Array.from(concernsMap.values())
|
|
484
|
-
.filter((concern) => concern.resolvedAtIteration === undefined)
|
|
485
|
-
.sort(sortEarlyLoopConcernsInline);
|
|
486
|
-
const resolvedConcerns = Array.from(concernsMap.values())
|
|
487
|
-
.filter((concern) => concern.resolvedAtIteration !== undefined)
|
|
488
|
-
.sort((a, b) => {
|
|
489
|
-
if (a.resolvedAtIteration !== b.resolvedAtIteration) {
|
|
490
|
-
return a.resolvedAtIteration - b.resolvedAtIteration;
|
|
491
|
-
}
|
|
492
|
-
return sortEarlyLoopConcernsInline(a, b);
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
if (!convergenceTripped && openConcerns.length > 0 && currentIteration >= maxIters) {
|
|
496
|
-
convergenceTripped = true;
|
|
497
|
-
escalationReason = "max iterations " + String(maxIters) +
|
|
498
|
-
" reached with " + String(openConcerns.length) + " open concern(s)";
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return {
|
|
502
|
-
schemaVersion: 1,
|
|
503
|
-
stage: stageId,
|
|
504
|
-
runId,
|
|
505
|
-
iteration: currentIteration,
|
|
506
|
-
maxIterations: maxIters,
|
|
507
|
-
openConcerns,
|
|
508
|
-
resolvedConcerns,
|
|
509
|
-
lastSeenConcernIds,
|
|
510
|
-
convergenceTripped,
|
|
511
|
-
...(escalationReason ? { escalationReason } : {}),
|
|
512
|
-
lastUpdatedAt: new Date().toISOString()
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
`;
|