@slowcook-ai/cli 0.19.0-alpha.8 → 0.19.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/AGENTS.md +240 -0
- package/REPORTING.md +193 -0
- package/dist/cli.js +78 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/brand/index.d.ts +26 -0
- package/dist/commands/brand/index.d.ts.map +1 -0
- package/dist/commands/brand/index.js +257 -0
- package/dist/commands/brand/index.js.map +1 -0
- package/dist/commands/brew/agent.d.ts +7 -2
- package/dist/commands/brew/agent.d.ts.map +1 -1
- package/dist/commands/brew/agent.js +9 -0
- package/dist/commands/brew/agent.js.map +1 -1
- package/dist/commands/brew/halt.d.ts +26 -0
- package/dist/commands/brew/halt.d.ts.map +1 -1
- package/dist/commands/brew/halt.js.map +1 -1
- package/dist/commands/brew/index.d.ts.map +1 -1
- package/dist/commands/brew/index.js +142 -18
- package/dist/commands/brew/index.js.map +1 -1
- package/dist/commands/brew/pair-navigator.d.ts +119 -0
- package/dist/commands/brew/pair-navigator.d.ts.map +1 -0
- package/dist/commands/brew/pair-navigator.js +187 -0
- package/dist/commands/brew/pair-navigator.js.map +1 -0
- package/dist/commands/budget/index.d.ts +2 -0
- package/dist/commands/budget/index.d.ts.map +1 -0
- package/dist/commands/budget/index.js +252 -0
- package/dist/commands/budget/index.js.map +1 -0
- package/dist/commands/chef/drift-fix.d.ts +33 -4
- package/dist/commands/chef/drift-fix.d.ts.map +1 -1
- package/dist/commands/chef/drift-fix.js +417 -29
- package/dist/commands/chef/drift-fix.js.map +1 -1
- package/dist/commands/chef/index.js +13 -3
- package/dist/commands/chef/index.js.map +1 -1
- package/dist/commands/chef/orchestrate.d.ts +1 -1
- package/dist/commands/chef/orchestrate.d.ts.map +1 -1
- package/dist/commands/chef/orchestrate.js +32 -9
- package/dist/commands/chef/orchestrate.js.map +1 -1
- package/dist/commands/dev-env/config.d.ts +57 -0
- package/dist/commands/dev-env/config.d.ts.map +1 -0
- package/dist/commands/dev-env/config.js +96 -0
- package/dist/commands/dev-env/config.js.map +1 -0
- package/dist/commands/dev-env/index.d.ts +27 -0
- package/dist/commands/dev-env/index.d.ts.map +1 -0
- package/dist/commands/dev-env/index.js +226 -0
- package/dist/commands/dev-env/index.js.map +1 -0
- package/dist/commands/dev-env/init.d.ts +28 -0
- package/dist/commands/dev-env/init.d.ts.map +1 -0
- package/dist/commands/dev-env/init.js +135 -0
- package/dist/commands/dev-env/init.js.map +1 -0
- package/dist/commands/docs/index.d.ts +16 -0
- package/dist/commands/docs/index.d.ts.map +1 -0
- package/dist/commands/docs/index.js +127 -0
- package/dist/commands/docs/index.js.map +1 -0
- package/dist/commands/eval/index.d.ts +54 -0
- package/dist/commands/eval/index.d.ts.map +1 -0
- package/dist/commands/eval/index.js +294 -0
- package/dist/commands/eval/index.js.map +1 -0
- package/dist/commands/extract/index.d.ts.map +1 -1
- package/dist/commands/extract/index.js +23 -1
- package/dist/commands/extract/index.js.map +1 -1
- package/dist/commands/garnish/index.d.ts +56 -0
- package/dist/commands/garnish/index.d.ts.map +1 -0
- package/dist/commands/garnish/index.js +281 -0
- package/dist/commands/garnish/index.js.map +1 -0
- package/dist/commands/garnish/trailer.d.ts +79 -0
- package/dist/commands/garnish/trailer.d.ts.map +1 -0
- package/dist/commands/garnish/trailer.js +118 -0
- package/dist/commands/garnish/trailer.js.map +1 -0
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +33 -0
- package/dist/commands/init/index.js.map +1 -1
- package/dist/commands/init/mock-vite.d.ts +54 -0
- package/dist/commands/init/mock-vite.d.ts.map +1 -0
- package/dist/commands/init/mock-vite.js +613 -0
- package/dist/commands/init/mock-vite.js.map +1 -0
- package/dist/commands/init/mock.d.ts +6 -0
- package/dist/commands/init/mock.d.ts.map +1 -1
- package/dist/commands/init/mock.js +20 -4
- package/dist/commands/init/mock.js.map +1 -1
- package/dist/commands/init/plan.d.ts +26 -1
- package/dist/commands/init/plan.d.ts.map +1 -1
- package/dist/commands/init/plan.js +41 -3
- package/dist/commands/init/plan.js.map +1 -1
- package/dist/commands/init/templates.d.ts.map +1 -1
- package/dist/commands/init/templates.js +12 -4
- package/dist/commands/init/templates.js.map +1 -1
- package/dist/commands/knowledge-add.d.ts +52 -0
- package/dist/commands/knowledge-add.d.ts.map +1 -0
- package/dist/commands/knowledge-add.js +232 -0
- package/dist/commands/knowledge-add.js.map +1 -0
- package/dist/commands/map/emit-typeorm.d.ts +117 -0
- package/dist/commands/map/emit-typeorm.d.ts.map +1 -0
- package/dist/commands/map/emit-typeorm.js +341 -0
- package/dist/commands/map/emit-typeorm.js.map +1 -0
- package/dist/commands/map/index.d.ts +18 -0
- package/dist/commands/map/index.d.ts.map +1 -1
- package/dist/commands/map/index.js +28 -0
- package/dist/commands/map/index.js.map +1 -1
- package/dist/commands/plate/agent.d.ts +7 -0
- package/dist/commands/plate/agent.d.ts.map +1 -1
- package/dist/commands/plate/agent.js +1 -1
- package/dist/commands/plate/agent.js.map +1 -1
- package/dist/commands/plate/index.d.ts.map +1 -1
- package/dist/commands/plate/index.js +6 -0
- package/dist/commands/plate/index.js.map +1 -1
- package/dist/commands/recon/index.d.ts +16 -3
- package/dist/commands/recon/index.d.ts.map +1 -1
- package/dist/commands/recon/index.js +267 -16
- package/dist/commands/recon/index.js.map +1 -1
- package/dist/commands/recon/migration-gate.d.ts +59 -0
- package/dist/commands/recon/migration-gate.d.ts.map +1 -0
- package/dist/commands/recon/migration-gate.js +131 -0
- package/dist/commands/recon/migration-gate.js.map +1 -0
- package/dist/commands/recon/reuse.d.ts +32 -0
- package/dist/commands/recon/reuse.d.ts.map +1 -1
- package/dist/commands/recon/reuse.js +66 -0
- package/dist/commands/recon/reuse.js.map +1 -1
- package/dist/commands/recon/stale-stubs.d.ts +65 -0
- package/dist/commands/recon/stale-stubs.d.ts.map +1 -0
- package/dist/commands/recon/stale-stubs.js +84 -0
- package/dist/commands/recon/stale-stubs.js.map +1 -0
- package/dist/commands/refine/agent.d.ts +23 -0
- package/dist/commands/refine/agent.d.ts.map +1 -1
- package/dist/commands/refine/agent.js +245 -15
- package/dist/commands/refine/agent.js.map +1 -1
- package/dist/commands/refine/brownfield-answer.d.ts +81 -0
- package/dist/commands/refine/brownfield-answer.d.ts.map +1 -0
- package/dist/commands/refine/brownfield-answer.js +231 -0
- package/dist/commands/refine/brownfield-answer.js.map +1 -0
- package/dist/commands/refine/context.d.ts +27 -1
- package/dist/commands/refine/context.d.ts.map +1 -1
- package/dist/commands/refine/context.js +438 -6
- package/dist/commands/refine/context.js.map +1 -1
- package/dist/commands/refine/git-attention.d.ts +123 -0
- package/dist/commands/refine/git-attention.d.ts.map +1 -0
- package/dist/commands/refine/git-attention.js +378 -0
- package/dist/commands/refine/git-attention.js.map +1 -0
- package/dist/commands/refine/history-index.d.ts +66 -0
- package/dist/commands/refine/history-index.d.ts.map +1 -1
- package/dist/commands/refine/history-index.js +195 -8
- package/dist/commands/refine/history-index.js.map +1 -1
- package/dist/commands/refine/index.d.ts.map +1 -1
- package/dist/commands/refine/index.js +105 -18
- package/dist/commands/refine/index.js.map +1 -1
- package/dist/commands/refine/multifurcate.d.ts +129 -0
- package/dist/commands/refine/multifurcate.d.ts.map +1 -0
- package/dist/commands/refine/multifurcate.js +247 -0
- package/dist/commands/refine/multifurcate.js.map +1 -0
- package/dist/commands/refine/proposals-synth.d.ts +50 -1
- package/dist/commands/refine/proposals-synth.d.ts.map +1 -1
- package/dist/commands/refine/proposals-synth.js +199 -35
- package/dist/commands/refine/proposals-synth.js.map +1 -1
- package/dist/commands/refine/spec-yaml.d.ts +214 -1210
- package/dist/commands/refine/spec-yaml.d.ts.map +1 -1
- package/dist/commands/refine/spec-yaml.js +10 -0
- package/dist/commands/refine/spec-yaml.js.map +1 -1
- package/dist/commands/refresh-knowledge.d.ts +126 -0
- package/dist/commands/refresh-knowledge.d.ts.map +1 -0
- package/dist/commands/refresh-knowledge.js +1010 -0
- package/dist/commands/refresh-knowledge.js.map +1 -0
- package/dist/commands/run-mock/index.d.ts.map +1 -1
- package/dist/commands/run-mock/index.js +135 -22
- package/dist/commands/run-mock/index.js.map +1 -1
- package/dist/commands/testgen/agent.d.ts +13 -0
- package/dist/commands/testgen/agent.d.ts.map +1 -1
- package/dist/commands/testgen/agent.js +137 -11
- package/dist/commands/testgen/agent.js.map +1 -1
- package/dist/commands/upsert-agent-docs.d.ts +48 -0
- package/dist/commands/upsert-agent-docs.d.ts.map +1 -0
- package/dist/commands/upsert-agent-docs.js +298 -0
- package/dist/commands/upsert-agent-docs.js.map +1 -0
- package/dist/commands/vibe/agent.d.ts +7 -0
- package/dist/commands/vibe/agent.d.ts.map +1 -1
- package/dist/commands/vibe/agent.js +2 -2
- package/dist/commands/vibe/agent.js.map +1 -1
- package/dist/commands/vibe/index.d.ts.map +1 -1
- package/dist/commands/vibe/index.js +7 -1
- package/dist/commands/vibe/index.js.map +1 -1
- package/dist/cost-store.d.ts +52 -0
- package/dist/cost-store.d.ts.map +1 -0
- package/dist/cost-store.js +108 -0
- package/dist/cost-store.js.map +1 -0
- package/dist/lib/budget.d.ts +73 -0
- package/dist/lib/budget.d.ts.map +1 -0
- package/dist/lib/budget.js +225 -0
- package/dist/lib/budget.js.map +1 -0
- package/dist/lib/mock-shape.d.ts +29 -0
- package/dist/lib/mock-shape.d.ts.map +1 -0
- package/dist/lib/mock-shape.js +77 -0
- package/dist/lib/mock-shape.js.map +1 -0
- package/dist/lib/read-only.d.ts +22 -0
- package/dist/lib/read-only.d.ts.map +1 -0
- package/dist/lib/read-only.js +34 -0
- package/dist/lib/read-only.js.map +1 -0
- package/package.json +17 -12
|
@@ -8,9 +8,17 @@
|
|
|
8
8
|
* ChefVerdict, applies edits surgically, validates, commits, posts an
|
|
9
9
|
* audit comment.
|
|
10
10
|
*
|
|
11
|
-
* Frozen surface (HARD):
|
|
12
|
-
* .brewing/
|
|
13
|
-
*
|
|
11
|
+
* Frozen surface (HARD): tests/integration/, tests/schema/,
|
|
12
|
+
* tests/acceptance/ (spec-contract assertions) + .brewing/auto-gen/,
|
|
13
|
+
* .brewing/code-map.*, .brewing/recon-result.json, .brewing/history-
|
|
14
|
+
* index.json (slowcook-managed artifacts). If a fix requires an
|
|
15
|
+
* ASSERTION change → escalates to PM via pm_comment (option B =
|
|
16
|
+
* testgen --regenerate).
|
|
17
|
+
*
|
|
18
|
+
* NOT frozen (α.54 — chef owns test infrastructure):
|
|
19
|
+
* tests/helpers/, vitest.config.*, playwright.config.*, package.json
|
|
20
|
+
* (devDeps), tsconfig.json, setup files. Chef can fix the runner
|
|
21
|
+
* machinery freehand.
|
|
14
22
|
*
|
|
15
23
|
* Ledger at .brewing/chef/<story-id>.json tracks moves + cost. Cycle
|
|
16
24
|
* detection + budget cap enforce convergence.
|
|
@@ -25,15 +33,72 @@ import { execSync } from "node:child_process";
|
|
|
25
33
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
26
34
|
import { dirname, join } from "node:path";
|
|
27
35
|
import { AnthropicClient, CHEF_SYSTEM, buildChefPrompt, } from "@slowcook-ai/llm-anthropic";
|
|
36
|
+
import { isReadOnlyMode, logReadOnlyBanner } from "../../lib/read-only.js";
|
|
37
|
+
import { knowledgeAddCore } from "../knowledge-add.js";
|
|
38
|
+
/**
|
|
39
|
+
* α.67 — distil a chef rationale into a one-line curated insight.
|
|
40
|
+
* The rationale is typically a paragraph explaining what+why+how;
|
|
41
|
+
* curated/ entries should be a single class-of-problem claim
|
|
42
|
+
* (≤250 chars). Strategy: take the first sentence, cap, strip
|
|
43
|
+
* trailing whitespace. The full rationale lives in the ledger for
|
|
44
|
+
* deep audit.
|
|
45
|
+
*/
|
|
46
|
+
function distilRationaleToClaim(rationale) {
|
|
47
|
+
const firstSentence = rationale.split(/(?<=[.!?])\s/)[0] ?? rationale;
|
|
48
|
+
const trimmed = firstSentence.trim().replace(/\s+/g, " ");
|
|
49
|
+
if (trimmed.length <= 250)
|
|
50
|
+
return trimmed;
|
|
51
|
+
return trimmed.slice(0, 247) + "…";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Paths chef MAY edit even though they "live under tests/" or look
|
|
55
|
+
* config-shaped. α.54: chef gains ownership of test INFRASTRUCTURE —
|
|
56
|
+
* runner config, helpers, setup files — because those are
|
|
57
|
+
* agent-fixable failures that previously had no owner.
|
|
58
|
+
*
|
|
59
|
+
* Principle: tests encode the spec CONTRACT (the assertions that
|
|
60
|
+
* define what "story-N done" means). Their enforcement MACHINERY
|
|
61
|
+
* (vitest config, helpers, setup) is not the contract. Distinguishing
|
|
62
|
+
* by file content/location, not by folder.
|
|
63
|
+
*
|
|
64
|
+
* Caught dogfooding delgoosh#656 — chef diagnosed a missing JSX
|
|
65
|
+
* transform but couldn't fix vitest.config.ts. PM, brew, testgen,
|
|
66
|
+
* and chef were ALL blocked from owning it. α.54 closes that gap by
|
|
67
|
+
* giving chef the editing rights.
|
|
68
|
+
*/
|
|
69
|
+
const TEST_INFRA_ESCAPE_PATTERNS = [
|
|
70
|
+
/^tests\/helpers\//, // render, a11y, mock helpers
|
|
71
|
+
/^tests\/setup\.(ts|tsx|js)$/, // vitest globalSetup / setupFiles
|
|
72
|
+
/^vitest\.setup\.(ts|tsx|js)$/,
|
|
73
|
+
];
|
|
74
|
+
/**
|
|
75
|
+
* Paths chef NEVER edits:
|
|
76
|
+
* - tests/integration/**, tests/schema/**, tests/acceptance/** —
|
|
77
|
+
* spec-contract assertions. If a test is wrong, the right fix is
|
|
78
|
+
* testgen --regenerate, not chef rewriting the assertion.
|
|
79
|
+
* - .brewing/auto-gen/**, code-map, history-index, recon-result —
|
|
80
|
+
* slowcook-managed artifacts other agents derive from.
|
|
81
|
+
*
|
|
82
|
+
* vitest.config.*, playwright.config.*, package.json, tsconfig.json
|
|
83
|
+
* are NOT frozen — they're infra chef can fix.
|
|
84
|
+
*/
|
|
28
85
|
const FROZEN_PATH_PATTERNS = [
|
|
29
|
-
/^tests\//,
|
|
30
|
-
/^
|
|
86
|
+
/^tests\/integration\//,
|
|
87
|
+
/^tests\/schema\//,
|
|
88
|
+
/^tests\/acceptance\//,
|
|
31
89
|
/^\.brewing\/code-map\.(json|md|target\.md)$/,
|
|
32
90
|
/^\.brewing\/recon-result\.json$/,
|
|
33
91
|
/^\.brewing\/history-index\.json$/,
|
|
34
92
|
/^\.brewing\/auto-gen\//,
|
|
35
93
|
];
|
|
36
|
-
function isFrozenPath(path) {
|
|
94
|
+
export function isFrozenPath(path) {
|
|
95
|
+
// α.54 — explicit escapes win first. A file under tests/helpers/
|
|
96
|
+
// shouldn't be caught by a generic tests/ rule (and the new
|
|
97
|
+
// FROZEN_PATH_PATTERNS list above is already specific to assertion
|
|
98
|
+
// subfolders, but keeping the escape-first check makes the policy
|
|
99
|
+
// intent visible at the call site for future readers).
|
|
100
|
+
if (TEST_INFRA_ESCAPE_PATTERNS.some((re) => re.test(path)))
|
|
101
|
+
return false;
|
|
37
102
|
return FROZEN_PATH_PATTERNS.some((re) => re.test(path));
|
|
38
103
|
}
|
|
39
104
|
/**
|
|
@@ -98,12 +163,27 @@ export function collectImportedSourceFiles(testContents) {
|
|
|
98
163
|
// (we exclude scoped packages by requiring the next char after "@"
|
|
99
164
|
// to be "/", which rules out "@scope/pkg" forms).
|
|
100
165
|
const importRe = /from\s+["'](\.{1,2}\/[^"']+|@\/[^"']+|~\/[^"']+)["']/g;
|
|
166
|
+
// α.59 — some tests do not `import` their target; the styling/shape
|
|
167
|
+
// presence pattern (slowcook 0.7.21+) uses `readFileSync` on a string
|
|
168
|
+
// literal source path. Also pick up these direct references so chef
|
|
169
|
+
// sees the file contents instead of guessing.
|
|
170
|
+
// Matches string literals like "src/foo/bar.tsx", 'src/foo', "mock/x.ts".
|
|
171
|
+
const literalRe = /["'](?:src|mock)\/[\w./-]+\.(?:ts|tsx|js|jsx)["']/g;
|
|
101
172
|
for (const [testFile, content] of Object.entries(testContents)) {
|
|
102
173
|
const sources = new Set();
|
|
103
174
|
let m;
|
|
104
175
|
while ((m = importRe.exec(content)) !== null) {
|
|
105
176
|
sources.add(m[1]);
|
|
106
177
|
}
|
|
178
|
+
while ((m = literalRe.exec(content)) !== null) {
|
|
179
|
+
// Strip the surrounding quotes; downstream resolver handles
|
|
180
|
+
// bare-repo-relative paths via the './' branch by anchoring on
|
|
181
|
+
// testFile's directory, which won't work for cross-directory
|
|
182
|
+
// paths like "src/components/...". So we mark these as a
|
|
183
|
+
// separate prefix that the resolver knows is repo-root-relative.
|
|
184
|
+
const literal = m[0].slice(1, -1);
|
|
185
|
+
sources.add("//" + literal); // sentinel for repo-root-relative
|
|
186
|
+
}
|
|
107
187
|
out[testFile] = [...sources];
|
|
108
188
|
}
|
|
109
189
|
return out;
|
|
@@ -132,6 +212,13 @@ export function resolveImportToFile(importPath, testFile, repoRoot, exists) {
|
|
|
132
212
|
baseDir = dirname(join(repoRoot, testFile));
|
|
133
213
|
rest = importPath;
|
|
134
214
|
}
|
|
215
|
+
else if (importPath.startsWith("//")) {
|
|
216
|
+
// α.59 — sentinel for repo-root-relative paths captured by the
|
|
217
|
+
// literal-path regex (collectImportedSourceFiles). Strip "//" and
|
|
218
|
+
// anchor at repoRoot.
|
|
219
|
+
baseDir = repoRoot;
|
|
220
|
+
rest = importPath.slice(2);
|
|
221
|
+
}
|
|
135
222
|
else {
|
|
136
223
|
return null;
|
|
137
224
|
}
|
|
@@ -238,7 +325,7 @@ Options:
|
|
|
238
325
|
audit comment on the PR (not the source issue).
|
|
239
326
|
|
|
240
327
|
Requires: ANTHROPIC_API_KEY in env. Run from consumer repo root.
|
|
241
|
-
Frozen surface: tests
|
|
328
|
+
Frozen surface: tests/{integration,schema,acceptance}/ + .brewing/{auto-gen,code-map,history-index,recon-result} — never edited. Test INFRA (tests/helpers/, vitest.config.*, package.json devDeps, setup files) IS chef's to edit (α.54).
|
|
242
329
|
`);
|
|
243
330
|
}
|
|
244
331
|
function loadHistoryIndex(repoRoot) {
|
|
@@ -485,6 +572,66 @@ function pushChefEditsToPrBranch(repoRoot, prBranch, localRef) {
|
|
|
485
572
|
return false;
|
|
486
573
|
}
|
|
487
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* α.58 — brew-halt finisher path. Called when chef-drift was invoked
|
|
577
|
+
* by chef-on-brew-halt (workflow_run trigger), which checked out the
|
|
578
|
+
* brew checkpoint branch + ran chef. Chef has committed locally;
|
|
579
|
+
* persist by pushing the current branch back to origin + opening a PR
|
|
580
|
+
* (brew_branch → main) so the maintainer sees the fix as a reviewable
|
|
581
|
+
* change instead of losing it with the ephemeral runner.
|
|
582
|
+
*
|
|
583
|
+
* Best-effort: any failure is logged but doesn't throw — the chef
|
|
584
|
+
* verdict is already audited via ledger + the source-issue comment.
|
|
585
|
+
*/
|
|
586
|
+
function publishChefBrewHaltFix(repoRoot, storyId, rationale) {
|
|
587
|
+
// Current branch is whatever the workflow checked out — chef-on-brew-halt
|
|
588
|
+
// switches to brew_branch from the halt artifact. Capturing it via
|
|
589
|
+
// `rev-parse --abbrev-ref HEAD` is more robust than carrying it through
|
|
590
|
+
// CLI args (which would require every consumer workflow to pass it).
|
|
591
|
+
const branch = execSync(`git -C "${repoRoot}" rev-parse --abbrev-ref HEAD`, { encoding: "utf8" }).trim();
|
|
592
|
+
if (!branch || branch === "HEAD") {
|
|
593
|
+
console.warn(` warn: brew-halt publish skipped — detached or empty HEAD`);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
// Push the chef commit (now at HEAD of brew_branch) back to origin.
|
|
597
|
+
// Set-upstream so subsequent fetches see the chef SHA too.
|
|
598
|
+
execSync(`git -C "${repoRoot}" push --set-upstream origin "${branch}"`, { stdio: "inherit" });
|
|
599
|
+
console.log(` brew-halt publish: pushed ${branch}`);
|
|
600
|
+
// Open a PR brew_branch → main if one doesn't already exist. gh is
|
|
601
|
+
// the only consumer-side dep we already assume (used by L2 finisher
|
|
602
|
+
// PR enrichment + audit comments). Repo slug derived from origin URL.
|
|
603
|
+
const repoSlug = execSync(`git -C "${repoRoot}" remote get-url origin | sed -E 's|^.*github\\.com[:/]||; s|\\.git$||'`, { encoding: "utf8" }).trim();
|
|
604
|
+
let existingPr = "";
|
|
605
|
+
try {
|
|
606
|
+
existingPr = execSync(`gh pr list --repo "${repoSlug}" --head "${branch}" --json number --jq '.[0].number // ""'`, { encoding: "utf8" }).trim();
|
|
607
|
+
}
|
|
608
|
+
catch { /* gh might be missing or token might be scoped narrow; let create-attempt log instead */ }
|
|
609
|
+
if (existingPr) {
|
|
610
|
+
console.log(` brew-halt publish: PR already exists (#${existingPr}) for ${branch}`);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const title = `[chef] story-${storyId}: autonomous fix after brew halt`;
|
|
614
|
+
// Trim rationale to keep the body tight; full rationale lives in the
|
|
615
|
+
// audit comment chef posts on the source issue.
|
|
616
|
+
const trimmedRationale = rationale.length > 500 ? rationale.slice(0, 500) + "…" : rationale;
|
|
617
|
+
const bodyFile = "/tmp/chef-brew-halt-pr-body.md";
|
|
618
|
+
writeFileSync(bodyFile, [
|
|
619
|
+
`Autonomous fix from \`slowcook chef-drift\` after brew halted on story-${storyId}.`,
|
|
620
|
+
``,
|
|
621
|
+
`**Rationale:** ${trimmedRationale}`,
|
|
622
|
+
``,
|
|
623
|
+
`Brew halted with checkpoints committed on this branch; chef applied a surgical edit + the failing test now passes locally on the runner. This PR persists chef's edit + the brew checkpoints together for review.`,
|
|
624
|
+
``,
|
|
625
|
+
`_Generated by \`slowcook chef-drift\` brew-halt finisher (α.58)._`,
|
|
626
|
+
].join("\n"), "utf8");
|
|
627
|
+
try {
|
|
628
|
+
execSync(`gh pr create --repo "${repoSlug}" --base main --head "${branch}" --title "${title.replace(/"/g, '\\"')}" --body-file "${bodyFile}"`, { stdio: "inherit" });
|
|
629
|
+
console.log(` brew-halt publish: opened PR for ${branch}`);
|
|
630
|
+
}
|
|
631
|
+
catch (e) {
|
|
632
|
+
console.warn(` warn: brew-halt PR create failed: ${e.message.slice(0, 200)}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
488
635
|
function commitChefEdits(repoRoot, summary, edits) {
|
|
489
636
|
// Stage ONLY the files chef edited (never `git add -A` — that
|
|
490
637
|
// accidentally captures workflow-side clones like `_slowcook/` etc.
|
|
@@ -511,8 +658,13 @@ function commitChefEdits(repoRoot, summary, edits) {
|
|
|
511
658
|
}
|
|
512
659
|
catch { /* ledger dir may not exist if first move ran into early error */ }
|
|
513
660
|
// Empty commit guard: if nothing staged, skip.
|
|
514
|
-
|
|
515
|
-
|
|
661
|
+
// α.60 — was `git status --porcelain --cached` which is not a valid
|
|
662
|
+
// git status invocation (--cached isn't a status flag); execSync
|
|
663
|
+
// threw, the outer try/catch silently absorbed it, and chef NEVER
|
|
664
|
+
// actually committed in any drift-fix path. `diff --cached
|
|
665
|
+
// --name-only` lists staged files; empty output = nothing to commit.
|
|
666
|
+
const staged = execSync(`git -C "${repoRoot}" diff --cached --name-only`, { encoding: "utf8" }).trim();
|
|
667
|
+
if (!staged)
|
|
516
668
|
return { sha: null, pushed: false };
|
|
517
669
|
// Write commit message to file (handle quotes cleanly).
|
|
518
670
|
const msgFile = "/tmp/chef-drift-commit-msg.txt";
|
|
@@ -553,9 +705,14 @@ function buildAuditCommentBody(args) {
|
|
|
553
705
|
lines.push("");
|
|
554
706
|
}
|
|
555
707
|
lines.push(`**Cost:** $${move.cost_usd.toFixed(2)} (cumulative: $${args.ledger.cumulative_cost_usd.toFixed(2)})`);
|
|
708
|
+
// 0.19.0-α.12 — slowcook:cost HTML marker for downstream aggregation
|
|
709
|
+
// (`gh issue view N | grep slowcook:cost`). Format matches vibe / plate /
|
|
710
|
+
// brew / refine / testgen.
|
|
711
|
+
lines.push("");
|
|
712
|
+
lines.push(`<!-- slowcook:cost agent=chef-drift usd=${move.cost_usd.toFixed(4)} cumulative_usd=${args.ledger.cumulative_cost_usd.toFixed(4)} decision=${move.decision} trigger=${move.trigger_kind} story=${args.ledger.story_id} move=${move.n} cli=${args.cliVersion} -->`);
|
|
556
713
|
return lines.join("\n");
|
|
557
714
|
}
|
|
558
|
-
export async function chefDrift(argv,
|
|
715
|
+
export async function chefDrift(argv, cliVersion) {
|
|
559
716
|
const args = parseArgs(argv);
|
|
560
717
|
const apiKey = process.env["ANTHROPIC_API_KEY"];
|
|
561
718
|
if (!apiKey) {
|
|
@@ -563,6 +720,7 @@ export async function chefDrift(argv, _cliVersion) {
|
|
|
563
720
|
process.exit(2);
|
|
564
721
|
}
|
|
565
722
|
console.log(`slowcook chef-drift · story-${args.storyId} · trigger=${args.triggerKind}${args.prNumber ? ` · finisher mode (PR #${args.prNumber})` : ""}`);
|
|
723
|
+
logReadOnlyBanner("chef-drift");
|
|
566
724
|
// L2 finisher mode: check out the PR branch BEFORE reading any
|
|
567
725
|
// ledger or repo state. The brew PR's branch has its own .brewing/
|
|
568
726
|
// and a different working tree shape than main.
|
|
@@ -697,8 +855,40 @@ export async function chefDrift(argv, _cliVersion) {
|
|
|
697
855
|
// material it needs to write surgical search_replace pairs.
|
|
698
856
|
if (args.triggerKind === "brew_halt_class") {
|
|
699
857
|
const runnerOutput = triggerRaw["runner_output"] ?? args.triggerDetail;
|
|
858
|
+
// α.56 — when runner_output is absent (typical for AGENT_STALLED /
|
|
859
|
+
// ITERATION_CAP halts where there's no vitest failure to parse),
|
|
860
|
+
// derive failing-test files from iteration_diffs[].target_test_id.
|
|
861
|
+
// Format: "tests/path/file.test.ts > suite > name" — split on " > "
|
|
862
|
+
// and take part[0] as the test file path. The same enrichment then
|
|
863
|
+
// loads file contents + imported source files. Without this, chef
|
|
864
|
+
// hallucinates target file contents (seen 2026-05-26 dogfood:
|
|
865
|
+
// delgoosh#003 chef invented two different "stub" source bodies).
|
|
866
|
+
let failingFiles = [];
|
|
867
|
+
let failingTestNames = [];
|
|
700
868
|
if (typeof runnerOutput === "string" && runnerOutput.length > 0) {
|
|
701
|
-
const
|
|
869
|
+
const parsed = parseBrewHaltOutput(runnerOutput);
|
|
870
|
+
failingFiles = parsed.failingFiles;
|
|
871
|
+
failingTestNames = parsed.failingTestNames;
|
|
872
|
+
}
|
|
873
|
+
else if (Array.isArray(triggerRaw["iteration_diffs"])) {
|
|
874
|
+
const diffs = triggerRaw["iteration_diffs"];
|
|
875
|
+
const filesSet = new Set();
|
|
876
|
+
const namesSet = new Set();
|
|
877
|
+
for (const d of diffs) {
|
|
878
|
+
const tid = d?.target_test_id;
|
|
879
|
+
if (typeof tid !== "string" || tid.length === 0)
|
|
880
|
+
continue;
|
|
881
|
+
const parts = tid.split(" > ");
|
|
882
|
+
const file = parts[0].trim();
|
|
883
|
+
if (file)
|
|
884
|
+
filesSet.add(file);
|
|
885
|
+
if (parts.length > 1)
|
|
886
|
+
namesSet.add(parts.slice(1).join(" > "));
|
|
887
|
+
}
|
|
888
|
+
failingFiles = Array.from(filesSet);
|
|
889
|
+
failingTestNames = Array.from(namesSet);
|
|
890
|
+
}
|
|
891
|
+
if (failingFiles.length > 0) {
|
|
702
892
|
const failingTestContents = {};
|
|
703
893
|
for (const f of failingFiles) {
|
|
704
894
|
const abs = join(args.repoRoot, f);
|
|
@@ -725,6 +915,25 @@ export async function chefDrift(argv, _cliVersion) {
|
|
|
725
915
|
sourceContents[projRel] = c.length > 6000 ? c.slice(0, 6000) + "\n// ... truncated" : c;
|
|
726
916
|
}
|
|
727
917
|
}
|
|
918
|
+
// α.59 — also load files brew itself touched across all iters.
|
|
919
|
+
// Even if the failing test doesn't import them, they're the live
|
|
920
|
+
// story state chef should reason about (e.g., the page.tsx brew
|
|
921
|
+
// wrote in iter 1 imports the component the styling test asserts
|
|
922
|
+
// on; chef needs both to make a coherent edit).
|
|
923
|
+
if (Array.isArray(triggerRaw["iteration_diffs"])) {
|
|
924
|
+
const diffs = triggerRaw["iteration_diffs"];
|
|
925
|
+
for (const d of diffs) {
|
|
926
|
+
for (const f of d?.files_touched ?? []) {
|
|
927
|
+
if (typeof f !== "string" || sourceContents[f])
|
|
928
|
+
continue;
|
|
929
|
+
const abs = join(args.repoRoot, f);
|
|
930
|
+
if (!existsSync(abs))
|
|
931
|
+
continue;
|
|
932
|
+
const c = readFileSync(abs, "utf8");
|
|
933
|
+
sourceContents[f] = c.length > 6000 ? c.slice(0, 6000) + "\n// ... truncated" : c;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
728
937
|
triggerRaw = {
|
|
729
938
|
...triggerRaw,
|
|
730
939
|
failing_test_files: failingFiles,
|
|
@@ -732,7 +941,8 @@ export async function chefDrift(argv, _cliVersion) {
|
|
|
732
941
|
failing_test_contents: failingTestContents,
|
|
733
942
|
imported_source_files_per_test: importedSourcesMap,
|
|
734
943
|
source_file_contents: sourceContents,
|
|
735
|
-
enrichment_note: "cli-precomputed (chef has no read tools): failing_test_contents shows you each red test verbatim. source_file_contents gives you the full text of every source file imported by those tests. Plan search_replace pairs against source_file_contents — never edit failing_test_contents (tests/ is frozen). If the failure can only be fixed by changing a test, return pm_question instead
|
|
944
|
+
enrichment_note: "cli-precomputed (chef has no read tools): failing_test_contents shows you each red test verbatim. source_file_contents gives you the full text of every source file imported by those tests. Plan search_replace pairs against source_file_contents — never edit failing_test_contents (tests/ is frozen). If the failure can only be fixed by changing a test, return pm_question instead.\n\n" +
|
|
945
|
+
"ALSO available straight from the halt JSON (already on trigger.raw): `brew_mode` (freehand | plate | auto) and `allowed_paths[]` (the runtime restriction brew enforced). If iteration_diffs show outcome=rejected-overflow, the fix is to widen brew's CLI --mode arg, NOT to edit the spec. `allowed_paths` is not a spec yaml field — see chef prompt §1.6.",
|
|
736
946
|
};
|
|
737
947
|
console.log(` brew-halt enriched: ${failingFiles.length} failing test file(s), ${Object.keys(sourceContents).length} source file(s) under test`);
|
|
738
948
|
}
|
|
@@ -771,6 +981,13 @@ export async function chefDrift(argv, _cliVersion) {
|
|
|
771
981
|
maxTokens: 8192,
|
|
772
982
|
});
|
|
773
983
|
console.log(` chef LLM: ${resp.usage.inputTokens}→${resp.usage.outputTokens} tok · $${resp.costUsd.toFixed(4)}`);
|
|
984
|
+
// α.53 — parse-tolerant: a malformed JSON envelope (e.g. an
|
|
985
|
+
// unterminated string mid-rationale, observed on delgoosh#656
|
|
986
|
+
// story-003 chef run 26397941242) used to crash the workflow
|
|
987
|
+
// with exit 1, losing the entire run. Recover whatever signal we
|
|
988
|
+
// can (typically the rationale prose up to the truncation point)
|
|
989
|
+
// and escalate as a pm_question instead. The PM still gets
|
|
990
|
+
// actionable text; the workflow exits clean.
|
|
774
991
|
let verdict;
|
|
775
992
|
try {
|
|
776
993
|
const text = resp.text.trim();
|
|
@@ -778,9 +995,10 @@ export async function chefDrift(argv, _cliVersion) {
|
|
|
778
995
|
verdict = JSON.parse(fence ? fence[1] : text);
|
|
779
996
|
}
|
|
780
997
|
catch (e) {
|
|
781
|
-
|
|
782
|
-
console.
|
|
783
|
-
|
|
998
|
+
const parseErr = e.message;
|
|
999
|
+
console.warn(` ! chef JSON parse failed: ${parseErr}`);
|
|
1000
|
+
console.warn(` recovering rationale from raw text + escalating as pm_question`);
|
|
1001
|
+
verdict = recoverChefVerdictFromMalformedJson(resp.text, parseErr, args.storyId, issueNumber);
|
|
784
1002
|
}
|
|
785
1003
|
console.log(` chef verdict: ${verdict.kind.toUpperCase()}`);
|
|
786
1004
|
console.log(` rationale: ${verdict.rationale}`);
|
|
@@ -890,18 +1108,66 @@ export async function chefDrift(argv, _cliVersion) {
|
|
|
890
1108
|
}
|
|
891
1109
|
// Commit chef's edits locally (workflow handles the push) when validation passed.
|
|
892
1110
|
// L2 finisher mode: also push to the PR's branch directly so the PR re-runs CI.
|
|
1111
|
+
// SLOWCOOK_READ_ONLY=1 gates BOTH the commit AND the push so a maintainer
|
|
1112
|
+
// running chef-drift on a consumer's repo doesn't pollute their git history.
|
|
893
1113
|
let commitSha = null;
|
|
894
1114
|
if (verdict.kind === "autonomous_fix" && moveEntry.post_state === "clean" && !args.dryRun) {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if (result.sha) {
|
|
898
|
-
commitSha = result.sha;
|
|
899
|
-
console.log(` committed: ${result.sha.slice(0, 7)}`);
|
|
1115
|
+
if (isReadOnlyMode()) {
|
|
1116
|
+
console.log(` [SLOWCOOK_READ_ONLY=1] would commit ${verdict.edits.length} edit(s) + push to PR; skipping.`);
|
|
900
1117
|
}
|
|
901
|
-
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
1118
|
+
else {
|
|
1119
|
+
const summary = `move ${moveN} on story-${args.storyId} — ${args.triggerKind}: ${verdict.rationale.slice(0, 120)}`;
|
|
1120
|
+
const result = commitChefEdits(args.repoRoot, summary, verdict.edits);
|
|
1121
|
+
if (result.sha) {
|
|
1122
|
+
commitSha = result.sha;
|
|
1123
|
+
console.log(` committed: ${result.sha.slice(0, 7)}`);
|
|
1124
|
+
}
|
|
1125
|
+
if (commitSha && prCheckout) {
|
|
1126
|
+
const pushed = pushChefEditsToPrBranch(args.repoRoot, prCheckout.branchName, prCheckout.localRef);
|
|
1127
|
+
if (pushed)
|
|
1128
|
+
console.log(` finisher: pushed ${commitSha.slice(0, 7)} → ${prCheckout.branchName}`);
|
|
1129
|
+
}
|
|
1130
|
+
// α.58 — brew-halt mode: chef-drift was invoked by chef-on-brew-halt
|
|
1131
|
+
// (workflow_run-triggered). The workflow checked out brew_branch
|
|
1132
|
+
// (see slowcook halt artifact field `brew_branch`); chef committed
|
|
1133
|
+
// on top. Persist that work: push HEAD back to origin/<branch>,
|
|
1134
|
+
// open a PR brew_branch → main if none exists. Without this the
|
|
1135
|
+
// commit dies with the ephemeral runner.
|
|
1136
|
+
if (commitSha && !prCheckout && args.triggerKind === "brew_halt_class") {
|
|
1137
|
+
try {
|
|
1138
|
+
publishChefBrewHaltFix(args.repoRoot, args.storyId, verdict.rationale);
|
|
1139
|
+
}
|
|
1140
|
+
catch (e) {
|
|
1141
|
+
console.warn(` warn: chef brew-halt publish failed: ${e.message.slice(0, 200)}`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
// α.67 — close the knowledge-layer loop: chef writes back to
|
|
1145
|
+
// curated/chef-known-fixes.md so the next chef invocation (or
|
|
1146
|
+
// refine reading the curated block) sees this fix as durable
|
|
1147
|
+
// organizational memory. Soft signal — each entry carries
|
|
1148
|
+
// evidence trail (PR + file + last-verified) so staleness is
|
|
1149
|
+
// reviewable, not auto-invalidating. Best-effort.
|
|
1150
|
+
if (commitSha) {
|
|
1151
|
+
try {
|
|
1152
|
+
// Distil the chef rationale into a one-line claim. The
|
|
1153
|
+
// rationale is often a paragraph; we want the WHAT and the
|
|
1154
|
+
// HOW, compactly. Take the first sentence + cap at ~250
|
|
1155
|
+
// chars. The full rationale + the move ledger entry are
|
|
1156
|
+
// already in .brewing/chef/<story>.json for deep audit.
|
|
1157
|
+
const claim = distilRationaleToClaim(verdict.rationale);
|
|
1158
|
+
const evidenceFile = verdict.edits[0]?.file;
|
|
1159
|
+
knowledgeAddCore({
|
|
1160
|
+
repoRoot: args.repoRoot,
|
|
1161
|
+
agent: "chef",
|
|
1162
|
+
topic: "chef-known-fixes",
|
|
1163
|
+
claim,
|
|
1164
|
+
evidenceFile,
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
catch (e) {
|
|
1168
|
+
console.warn(` warn: knowledge-write skipped: ${e.message.slice(0, 200)}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
905
1171
|
}
|
|
906
1172
|
}
|
|
907
1173
|
ledger.moves.push(moveEntry);
|
|
@@ -910,15 +1176,22 @@ export async function chefDrift(argv, _cliVersion) {
|
|
|
910
1176
|
saveLedger(args.repoRoot, ledger);
|
|
911
1177
|
// Audit comment routing: in finisher mode (--pr) write to the PR;
|
|
912
1178
|
// otherwise write to the source issue (L1 behavior).
|
|
1179
|
+
// SLOWCOOK_READ_ONLY=1 gates the post too — maintainer-side replay
|
|
1180
|
+
// doesn't comment on the consumer's repo.
|
|
913
1181
|
if (verdict.kind === "autonomous_fix" && moveEntry.post_state === "clean" && !args.dryRun) {
|
|
914
|
-
const body = buildAuditCommentBody({ ledger, move: moveEntry, verdict });
|
|
1182
|
+
const body = buildAuditCommentBody({ ledger, move: moveEntry, verdict, cliVersion });
|
|
915
1183
|
const target = args.prNumber ?? (issueNumber > 0 ? issueNumber : null);
|
|
916
1184
|
if (target !== null) {
|
|
917
|
-
|
|
918
|
-
|
|
1185
|
+
if (isReadOnlyMode()) {
|
|
1186
|
+
console.log(` [SLOWCOOK_READ_ONLY=1] would post audit comment on issue/PR #${target}; skipping.`);
|
|
919
1187
|
}
|
|
920
|
-
|
|
921
|
-
|
|
1188
|
+
else {
|
|
1189
|
+
try {
|
|
1190
|
+
postIssueComment(args.repoRoot, target, body);
|
|
1191
|
+
}
|
|
1192
|
+
catch (e) {
|
|
1193
|
+
console.warn(` warn: audit comment failed (non-fatal): ${e.message.slice(0, 200)}`);
|
|
1194
|
+
}
|
|
922
1195
|
}
|
|
923
1196
|
}
|
|
924
1197
|
}
|
|
@@ -926,4 +1199,119 @@ export async function chefDrift(argv, _cliVersion) {
|
|
|
926
1199
|
if (verdict.kind === "halt" || moveEntry.post_state === "still-broken")
|
|
927
1200
|
process.exit(1);
|
|
928
1201
|
}
|
|
1202
|
+
/**
|
|
1203
|
+
* α.53 — extract whatever signal we can from a malformed chef JSON.
|
|
1204
|
+
*
|
|
1205
|
+
* The model often returns valid prose (`"rationale": "long sentence about
|
|
1206
|
+
* what's wrong"`) but the JSON envelope breaks if the rationale runs past
|
|
1207
|
+
* Anthropic's max-tokens midstring or contains an unescaped quote.
|
|
1208
|
+
*
|
|
1209
|
+
* Strategy:
|
|
1210
|
+
* 1. Look for ```json fence — strip it.
|
|
1211
|
+
* 2. Try a quick "auto-close unterminated string" repair: count
|
|
1212
|
+
* unescaped quotes; if odd, append a closing quote + `}` and retry
|
|
1213
|
+
* JSON.parse.
|
|
1214
|
+
* 3. If still broken, regex-extract the `"rationale"` field value up
|
|
1215
|
+
* to wherever it terminates (literal quote or end of buffer).
|
|
1216
|
+
* 4. Build a pm_question verdict with the recovered rationale plus a
|
|
1217
|
+
* bookkeeping note about the parse failure. Empty edits, no
|
|
1218
|
+
* next-dispatch — the PM is the next step.
|
|
1219
|
+
*/
|
|
1220
|
+
export function recoverChefVerdictFromMalformedJson(rawText, parseError, storyId, issueNumber) {
|
|
1221
|
+
const text = rawText.trim();
|
|
1222
|
+
const fence = text.match(/```json\s*([\s\S]*?)(?:```|$)/);
|
|
1223
|
+
const body = fence ? fence[1] : text;
|
|
1224
|
+
// Attempt 1: auto-close unterminated string + closing brace.
|
|
1225
|
+
const repaired = autoCloseStringAndBrace(body);
|
|
1226
|
+
if (repaired) {
|
|
1227
|
+
try {
|
|
1228
|
+
const v = JSON.parse(repaired);
|
|
1229
|
+
if (typeof v.rationale === "string") {
|
|
1230
|
+
return {
|
|
1231
|
+
rationale: v.rationale +
|
|
1232
|
+
"\n\n_(chef α.53: chef-drift recovered this verdict from a truncated JSON envelope. " +
|
|
1233
|
+
`Original parse error: ${parseError.slice(0, 120)})_`,
|
|
1234
|
+
kind: "pm_question",
|
|
1235
|
+
edits: [],
|
|
1236
|
+
validation: null,
|
|
1237
|
+
next_dispatch: null,
|
|
1238
|
+
pm_comment: {
|
|
1239
|
+
issue_number: issueNumber,
|
|
1240
|
+
body: "**[chef] Reasoning recovered from a truncated JSON envelope.**\n\n" +
|
|
1241
|
+
v.rationale +
|
|
1242
|
+
"\n\n---\n\n_chef α.53: the LLM produced valid reasoning but the JSON envelope was malformed " +
|
|
1243
|
+
"(likely a long rationale exceeded max-tokens midstring). Chef recovered the prose via auto-close repair. " +
|
|
1244
|
+
`Edits/validation/next-dispatch were not parseable. Original parse error: \`${parseError.slice(0, 200)}\`._`,
|
|
1245
|
+
},
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
catch {
|
|
1250
|
+
// fall through to attempt 2
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
// Attempt 2: regex-extract just the rationale field's value.
|
|
1254
|
+
const rationaleMatch = body.match(/"rationale"\s*:\s*"((?:[^"\\]|\\.)*)/);
|
|
1255
|
+
const rationale = rationaleMatch
|
|
1256
|
+
? rationaleMatch[1].replace(/\\n/g, "\n").replace(/\\"/g, '"')
|
|
1257
|
+
: "(chef returned a malformed JSON envelope and the rationale could not be recovered. See workflow log for raw text.)";
|
|
1258
|
+
return {
|
|
1259
|
+
rationale: rationale +
|
|
1260
|
+
`\n\n_(chef α.53: parse error ${parseError.slice(0, 120)} on story-${storyId} — escalating as pm_question with recovered text only)_`,
|
|
1261
|
+
kind: "pm_question",
|
|
1262
|
+
edits: [],
|
|
1263
|
+
validation: null,
|
|
1264
|
+
next_dispatch: null,
|
|
1265
|
+
pm_comment: {
|
|
1266
|
+
issue_number: issueNumber,
|
|
1267
|
+
body: "**[chef] Reasoning recovered from a malformed JSON envelope (partial).**\n\n" +
|
|
1268
|
+
rationale +
|
|
1269
|
+
"\n\n---\n\n_chef α.53: the LLM produced reasoning but the JSON envelope was malformed beyond auto-repair. " +
|
|
1270
|
+
"What you see above is regex-extracted rationale prose; edits, validation, and next-dispatch fields were not " +
|
|
1271
|
+
`recoverable. Original parse error: \`${parseError.slice(0, 200)}\`. Inspect the workflow log for the full raw text._`,
|
|
1272
|
+
},
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Conservative auto-close: if the body's quotes are imbalanced (one
|
|
1277
|
+
* unterminated string), append `"` + a guess at how many braces are
|
|
1278
|
+
* still open. Returns the repaired string or null if nothing salvageable.
|
|
1279
|
+
*/
|
|
1280
|
+
function autoCloseStringAndBrace(body) {
|
|
1281
|
+
// Count unescaped quotes outside strings is structurally hard; the
|
|
1282
|
+
// heuristic here: if quotes count is odd, append a closing quote.
|
|
1283
|
+
let inString = false;
|
|
1284
|
+
let escape = false;
|
|
1285
|
+
let openBraces = 0;
|
|
1286
|
+
for (const ch of body) {
|
|
1287
|
+
if (escape) {
|
|
1288
|
+
escape = false;
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
if (ch === "\\") {
|
|
1292
|
+
escape = true;
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
if (ch === '"') {
|
|
1296
|
+
inString = !inString;
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
if (!inString) {
|
|
1300
|
+
if (ch === "{")
|
|
1301
|
+
openBraces++;
|
|
1302
|
+
else if (ch === "}")
|
|
1303
|
+
openBraces--;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (!inString && openBraces === 0)
|
|
1307
|
+
return null; // nothing to repair
|
|
1308
|
+
let repaired = body;
|
|
1309
|
+
if (inString)
|
|
1310
|
+
repaired += '"';
|
|
1311
|
+
while (openBraces > 0) {
|
|
1312
|
+
repaired += "}";
|
|
1313
|
+
openBraces--;
|
|
1314
|
+
}
|
|
1315
|
+
return repaired;
|
|
1316
|
+
}
|
|
929
1317
|
//# sourceMappingURL=drift-fix.js.map
|