@slowcook-ai/cli 0.19.0-alpha.41 → 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 +2 -2
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/brand/index.d.ts +3 -35
- package/dist/commands/brand/index.d.ts.map +1 -1
- 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 +81 -29
- package/dist/commands/brew/index.js.map +1 -1
- package/dist/commands/brew/pair-navigator.d.ts +7 -0
- package/dist/commands/brew/pair-navigator.d.ts.map +1 -1
- package/dist/commands/brew/pair-navigator.js +4 -0
- package/dist/commands/brew/pair-navigator.js.map +1 -1
- package/dist/commands/chef/drift-fix.d.ts +32 -3
- package/dist/commands/chef/drift-fix.d.ts.map +1 -1
- package/dist/commands/chef/drift-fix.js +381 -14
- package/dist/commands/chef/drift-fix.js.map +1 -1
- package/dist/commands/dev-env/config.d.ts +18 -97
- package/dist/commands/dev-env/config.d.ts.map +1 -1
- package/dist/commands/eval/index.d.ts.map +1 -1
- package/dist/commands/eval/index.js +13 -2
- package/dist/commands/eval/index.js.map +1 -1
- 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.map +1 -1
- package/dist/commands/init/mock-vite.js +2 -0
- package/dist/commands/init/mock-vite.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/recon/index.d.ts +14 -1
- package/dist/commands/recon/index.d.ts.map +1 -1
- package/dist/commands/recon/index.js +43 -2
- package/dist/commands/recon/index.js.map +1 -1
- package/dist/commands/refine/agent.d.ts +11 -0
- package/dist/commands/refine/agent.d.ts.map +1 -1
- package/dist/commands/refine/agent.js +72 -2
- package/dist/commands/refine/agent.js.map +1 -1
- 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 +425 -11
- 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 +7 -0
- package/dist/commands/refine/history-index.d.ts.map +1 -1
- 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 +76 -20
- 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/spec-yaml.d.ts +211 -1231
- package/dist/commands/refine/spec-yaml.d.ts.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/testgen/agent.d.ts +13 -0
- package/dist/commands/testgen/agent.d.ts.map +1 -1
- package/dist/commands/testgen/agent.js +103 -2
- 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/lib/budget.d.ts +2 -52
- package/dist/lib/budget.d.ts.map +1 -1
- package/dist/lib/mock-shape.d.ts +5 -20
- package/dist/lib/mock-shape.d.ts.map +1 -1
- package/package.json +9 -6
|
@@ -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.
|
|
@@ -26,15 +34,71 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
|
|
|
26
34
|
import { dirname, join } from "node:path";
|
|
27
35
|
import { AnthropicClient, CHEF_SYSTEM, buildChefPrompt, } from "@slowcook-ai/llm-anthropic";
|
|
28
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
|
+
*/
|
|
29
85
|
const FROZEN_PATH_PATTERNS = [
|
|
30
|
-
/^tests\//,
|
|
31
|
-
/^
|
|
86
|
+
/^tests\/integration\//,
|
|
87
|
+
/^tests\/schema\//,
|
|
88
|
+
/^tests\/acceptance\//,
|
|
32
89
|
/^\.brewing\/code-map\.(json|md|target\.md)$/,
|
|
33
90
|
/^\.brewing\/recon-result\.json$/,
|
|
34
91
|
/^\.brewing\/history-index\.json$/,
|
|
35
92
|
/^\.brewing\/auto-gen\//,
|
|
36
93
|
];
|
|
37
|
-
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;
|
|
38
102
|
return FROZEN_PATH_PATTERNS.some((re) => re.test(path));
|
|
39
103
|
}
|
|
40
104
|
/**
|
|
@@ -99,12 +163,27 @@ export function collectImportedSourceFiles(testContents) {
|
|
|
99
163
|
// (we exclude scoped packages by requiring the next char after "@"
|
|
100
164
|
// to be "/", which rules out "@scope/pkg" forms).
|
|
101
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;
|
|
102
172
|
for (const [testFile, content] of Object.entries(testContents)) {
|
|
103
173
|
const sources = new Set();
|
|
104
174
|
let m;
|
|
105
175
|
while ((m = importRe.exec(content)) !== null) {
|
|
106
176
|
sources.add(m[1]);
|
|
107
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
|
+
}
|
|
108
187
|
out[testFile] = [...sources];
|
|
109
188
|
}
|
|
110
189
|
return out;
|
|
@@ -133,6 +212,13 @@ export function resolveImportToFile(importPath, testFile, repoRoot, exists) {
|
|
|
133
212
|
baseDir = dirname(join(repoRoot, testFile));
|
|
134
213
|
rest = importPath;
|
|
135
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
|
+
}
|
|
136
222
|
else {
|
|
137
223
|
return null;
|
|
138
224
|
}
|
|
@@ -239,7 +325,7 @@ Options:
|
|
|
239
325
|
audit comment on the PR (not the source issue).
|
|
240
326
|
|
|
241
327
|
Requires: ANTHROPIC_API_KEY in env. Run from consumer repo root.
|
|
242
|
-
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).
|
|
243
329
|
`);
|
|
244
330
|
}
|
|
245
331
|
function loadHistoryIndex(repoRoot) {
|
|
@@ -486,6 +572,66 @@ function pushChefEditsToPrBranch(repoRoot, prBranch, localRef) {
|
|
|
486
572
|
return false;
|
|
487
573
|
}
|
|
488
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
|
+
}
|
|
489
635
|
function commitChefEdits(repoRoot, summary, edits) {
|
|
490
636
|
// Stage ONLY the files chef edited (never `git add -A` — that
|
|
491
637
|
// accidentally captures workflow-side clones like `_slowcook/` etc.
|
|
@@ -512,8 +658,13 @@ function commitChefEdits(repoRoot, summary, edits) {
|
|
|
512
658
|
}
|
|
513
659
|
catch { /* ledger dir may not exist if first move ran into early error */ }
|
|
514
660
|
// Empty commit guard: if nothing staged, skip.
|
|
515
|
-
|
|
516
|
-
|
|
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)
|
|
517
668
|
return { sha: null, pushed: false };
|
|
518
669
|
// Write commit message to file (handle quotes cleanly).
|
|
519
670
|
const msgFile = "/tmp/chef-drift-commit-msg.txt";
|
|
@@ -704,8 +855,40 @@ export async function chefDrift(argv, cliVersion) {
|
|
|
704
855
|
// material it needs to write surgical search_replace pairs.
|
|
705
856
|
if (args.triggerKind === "brew_halt_class") {
|
|
706
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 = [];
|
|
707
868
|
if (typeof runnerOutput === "string" && runnerOutput.length > 0) {
|
|
708
|
-
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) {
|
|
709
892
|
const failingTestContents = {};
|
|
710
893
|
for (const f of failingFiles) {
|
|
711
894
|
const abs = join(args.repoRoot, f);
|
|
@@ -732,6 +915,25 @@ export async function chefDrift(argv, cliVersion) {
|
|
|
732
915
|
sourceContents[projRel] = c.length > 6000 ? c.slice(0, 6000) + "\n// ... truncated" : c;
|
|
733
916
|
}
|
|
734
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
|
+
}
|
|
735
937
|
triggerRaw = {
|
|
736
938
|
...triggerRaw,
|
|
737
939
|
failing_test_files: failingFiles,
|
|
@@ -739,7 +941,8 @@ export async function chefDrift(argv, cliVersion) {
|
|
|
739
941
|
failing_test_contents: failingTestContents,
|
|
740
942
|
imported_source_files_per_test: importedSourcesMap,
|
|
741
943
|
source_file_contents: sourceContents,
|
|
742
|
-
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.",
|
|
743
946
|
};
|
|
744
947
|
console.log(` brew-halt enriched: ${failingFiles.length} failing test file(s), ${Object.keys(sourceContents).length} source file(s) under test`);
|
|
745
948
|
}
|
|
@@ -778,6 +981,13 @@ export async function chefDrift(argv, cliVersion) {
|
|
|
778
981
|
maxTokens: 8192,
|
|
779
982
|
});
|
|
780
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.
|
|
781
991
|
let verdict;
|
|
782
992
|
try {
|
|
783
993
|
const text = resp.text.trim();
|
|
@@ -785,9 +995,10 @@ export async function chefDrift(argv, cliVersion) {
|
|
|
785
995
|
verdict = JSON.parse(fence ? fence[1] : text);
|
|
786
996
|
}
|
|
787
997
|
catch (e) {
|
|
788
|
-
|
|
789
|
-
console.
|
|
790
|
-
|
|
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);
|
|
791
1002
|
}
|
|
792
1003
|
console.log(` chef verdict: ${verdict.kind.toUpperCase()}`);
|
|
793
1004
|
console.log(` rationale: ${verdict.rationale}`);
|
|
@@ -916,6 +1127,47 @@ export async function chefDrift(argv, cliVersion) {
|
|
|
916
1127
|
if (pushed)
|
|
917
1128
|
console.log(` finisher: pushed ${commitSha.slice(0, 7)} → ${prCheckout.branchName}`);
|
|
918
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
|
+
}
|
|
919
1171
|
}
|
|
920
1172
|
}
|
|
921
1173
|
ledger.moves.push(moveEntry);
|
|
@@ -947,4 +1199,119 @@ export async function chefDrift(argv, cliVersion) {
|
|
|
947
1199
|
if (verdict.kind === "halt" || moveEntry.post_state === "still-broken")
|
|
948
1200
|
process.exit(1);
|
|
949
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
|
+
}
|
|
950
1317
|
//# sourceMappingURL=drift-fix.js.map
|