@mjasnikovs/pi-task 0.40.50 → 0.42.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 +7 -4
- package/dist/config/config.d.ts +61 -18
- package/dist/config/config.js +50 -6
- package/dist/config/register.js +18 -5
- package/dist/shared/content-tokens.d.ts +16 -0
- package/dist/shared/content-tokens.js +137 -0
- package/dist/task/accept-debt.d.ts +31 -1
- package/dist/task/accept-debt.js +49 -3
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +66 -7
- package/dist/task/auto-io.js +152 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +184 -112
- package/dist/task/child-runner.d.ts +18 -0
- package/dist/task/child-runner.js +7 -8
- package/dist/task/command-run.d.ts +4 -0
- package/dist/task/command-run.js +4 -2
- package/dist/task/constraint-policy.d.ts +52 -0
- package/dist/task/constraint-policy.js +66 -0
- package/dist/task/context-silence.d.ts +5 -4
- package/dist/task/context-silence.js +13 -5
- package/dist/task/coverage-loop.d.ts +7 -4
- package/dist/task/coverage-loop.js +16 -138
- package/dist/task/debug-log.d.ts +25 -2
- package/dist/task/debug-log.js +64 -10
- package/dist/task/decompose-fidelity.d.ts +17 -4
- package/dist/task/decompose-fidelity.js +95 -55
- package/dist/task/decompose-granularity.d.ts +7 -7
- package/dist/task/decompose-granularity.js +8 -8
- package/dist/task/env-notes.d.ts +65 -19
- package/dist/task/env-notes.js +152 -50
- package/dist/task/external-context.js +25 -5
- package/dist/task/fix-context.d.ts +39 -0
- package/dist/task/fix-context.js +58 -0
- package/dist/task/gate-deps.d.ts +49 -0
- package/dist/task/gate-deps.js +201 -39
- package/dist/task/gate-evidence.d.ts +51 -0
- package/dist/task/gate-evidence.js +90 -0
- package/dist/task/gate-resolution.d.ts +119 -0
- package/dist/task/gate-resolution.js +120 -0
- package/dist/task/git-state-guard.js +3 -26
- package/dist/task/handoff.d.ts +35 -0
- package/dist/task/handoff.js +100 -0
- package/dist/task/health-baseline.d.ts +87 -0
- package/dist/task/health-baseline.js +158 -0
- package/dist/task/health-repair.d.ts +68 -0
- package/dist/task/health-repair.js +124 -0
- package/dist/task/ledger.d.ts +10 -0
- package/dist/task/ledger.js +22 -11
- package/dist/task/lint-fix.d.ts +11 -0
- package/dist/task/lint-fix.js +14 -8
- package/dist/task/loop-detector.d.ts +52 -3
- package/dist/task/loop-detector.js +81 -5
- package/dist/task/mentions.d.ts +17 -0
- package/dist/task/mentions.js +86 -0
- package/dist/task/orchestrator.d.ts +53 -9
- package/dist/task/orchestrator.js +104 -28
- package/dist/task/orientation.d.ts +135 -29
- package/dist/task/orientation.js +259 -67
- package/dist/task/owned-freeze-reassign.d.ts +7 -3
- package/dist/task/owned-freeze-reassign.js +13 -5
- package/dist/task/parsers.d.ts +6 -1
- package/dist/task/parsers.js +21 -4
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +125 -153
- package/dist/task/plan-orchestrator.js +5 -6
- package/dist/task/plan-rounds.d.ts +1 -0
- package/dist/task/plan-rounds.js +8 -1
- package/dist/task/prohibition-probe.d.ts +6 -1
- package/dist/task/prohibition-probe.js +26 -6
- package/dist/task/prompts.js +13 -2
- package/dist/task/qa-transcript.d.ts +17 -0
- package/dist/task/qa-transcript.js +28 -0
- package/dist/task/question-source.d.ts +16 -2
- package/dist/task/question-source.js +21 -8
- package/dist/task/repo-health-check.d.ts +27 -0
- package/dist/task/repo-health-check.js +39 -5
- package/dist/task/requirements.d.ts +67 -13
- package/dist/task/requirements.js +129 -67
- package/dist/task/research-worker.d.ts +43 -2
- package/dist/task/research-worker.js +103 -8
- package/dist/task/run-bracket.d.ts +9 -4
- package/dist/task/run-bracket.js +9 -0
- package/dist/task/run-context.d.ts +152 -0
- package/dist/task/run-context.js +277 -0
- package/dist/task/spec-doc.d.ts +78 -0
- package/dist/task/spec-doc.js +205 -0
- package/dist/task/spec-model.d.ts +70 -0
- package/dist/task/spec-model.js +139 -0
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +9 -5
- package/dist/task/state-dir.d.ts +29 -0
- package/dist/task/state-dir.js +80 -0
- package/dist/task/suppression-probe.d.ts +67 -0
- package/dist/task/suppression-probe.js +78 -0
- package/dist/task/task-gates.d.ts +52 -58
- package/dist/task/task-gates.js +161 -199
- package/dist/task/task-io.d.ts +10 -0
- package/dist/task/task-io.js +69 -18
- package/dist/task/task-parsers.js +6 -6
- package/dist/task/task-types.d.ts +7 -0
- package/dist/task/timings.d.ts +11 -0
- package/dist/task/timings.js +21 -0
- package/dist/task/tree-hash.d.ts +8 -0
- package/dist/task/tree-hash.js +55 -0
- package/dist/task/verify-reconcile.d.ts +4 -3
- package/dist/task/verify-reconcile.js +7 -8
- package/dist/task/verify-resolution.d.ts +32 -6
- package/dist/task/verify-resolution.js +78 -15
- package/dist/task/verify-work.d.ts +105 -35
- package/dist/task/verify-work.js +232 -60
- package/dist/task/yolo.d.ts +0 -13
- package/dist/task/yolo.js +9 -21
- package/dist/workers/pi-worker-core.d.ts +20 -0
- package/dist/workers/pi-worker-core.js +16 -11
- package/dist/workers/pi-worker-docs.d.ts +6 -5
- package/dist/workers/pi-worker-docs.js +8 -7
- package/dist/workers/pi-worker-fetch.d.ts +3 -2
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +5 -4
- package/dist/workers/research-cache.d.ts +23 -1
- package/dist/workers/research-cache.js +63 -3
- package/dist/workers/shared.d.ts +18 -0
- package/dist/workers/shared.js +38 -22
- package/package.json +1 -1
|
@@ -16,11 +16,17 @@ import { parseAutoAnswer, autoAnswerHasTag, deriveTitle } from './parsers.js';
|
|
|
16
16
|
import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
|
|
17
17
|
import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT, DECOMPOSE_COVERAGE_PROMPT } from './auto-prompts.js';
|
|
18
18
|
import { GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT } from './prompts.js';
|
|
19
|
-
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseCoverageVerdict, parseTaskList, checkOffTask, stampTaskInProgress, insertTaskAfter, findResumableAutoDetailed } from './auto-io.js';
|
|
19
|
+
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseCoverageVerdict, UNNAMED_COVERAGE_GAP, parseTaskList, planKeyAt, checkOffTask, stampTaskInProgress, beginTaskAttempt, recordTaskEnd, insertTaskAfter, insertTaskBefore, findResumableAutoDetailed } from './auto-io.js';
|
|
20
20
|
import { decideResume, UNATTENDED_STATES } from './resume-gap.js';
|
|
21
|
+
import { ENTRY_ATTEMPT_BUDGET } from './gate-resolution.js';
|
|
22
|
+
import { recordDebt } from './accept-debt.js';
|
|
21
23
|
import { drainRepairQueue, mergeRepairCandidates, planHasRepairFor, parseRepairTitleFile, buildRepairTitle, buildRepairScopeFence, extractFailingCommand } from './root-cause-repair.js';
|
|
22
|
-
import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath
|
|
23
|
-
|
|
24
|
+
import { writeTaskFile, readTaskFile, readSection, updateTaskFrontMatter, taskFilePath } from './task-io.js';
|
|
25
|
+
// Re-exported as well as used: the @-mention helpers moved to their own module so
|
|
26
|
+
// the research phase can select a cited spec doc without importing this one, and
|
|
27
|
+
// the planning call sites still name them here.
|
|
28
|
+
import { expandFeatureMentions, readableMentions } from './mentions.js';
|
|
29
|
+
export { expandFeatureMentions, readableMentions };
|
|
24
30
|
import { findPhantomImports, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
|
|
25
31
|
import { isFatalChildCause, prependHint, USER_CANCELLED } from './child-runner.js';
|
|
26
32
|
import { requestCancel, resetCancel, isCancelRequested, cancelCheckpoint } from './cancel-points.js';
|
|
@@ -31,21 +37,25 @@ import { getParentContextWindow } from './context-usage.js';
|
|
|
31
37
|
import { ChildStatus, runPlanningChild, statusCallbacks } from './child-status.js';
|
|
32
38
|
import { buildGateDeps, collectTreeChanges } from './gate-deps.js';
|
|
33
39
|
import { runGatesForTask } from './task-gates.js';
|
|
40
|
+
import { buildHealthRepairFence, buildHealthRepairTitle, healthRedSubject, parseHealthRepairTitle, planCoversHealthRed } from './health-repair.js';
|
|
41
|
+
import { HEALTH_BASELINE_SECTION, parseHealthBaseline } from './health-baseline.js';
|
|
34
42
|
import { runFinalGateStage } from './run-final-gate.js';
|
|
35
43
|
import { gitUnmergedPaths, gitStashRef } from './auto-commit.js';
|
|
36
44
|
import { runFinalIntegrationGate, deriveOpenDebts } from './final-gate.js';
|
|
37
45
|
import { spawnCommand } from './command-run.js';
|
|
38
46
|
import { getConfig } from '../config/config.js';
|
|
39
|
-
import { debugLogLevel, shouldLogDebug } from './debug-log.js';
|
|
47
|
+
import { debugLogLevel, sanitizeDebugLine, shouldLogDebug } from './debug-log.js';
|
|
48
|
+
import { pruneRunLogs, runLogPath } from './state-dir.js';
|
|
40
49
|
import { isYoloMode, yoloPickAnswer } from './yolo.js';
|
|
41
50
|
import { QaTranscript, CLARIFY_QA_POLICY } from './qa-transcript.js';
|
|
42
51
|
import { makeQuestionSource } from './question-source.js';
|
|
43
52
|
import { CoverageLedger } from './plan-rounds.js';
|
|
44
53
|
import { CLARIFY_QUALITY_RULES, PLAN_FORMAT_HINT } from './plan-session.js';
|
|
45
54
|
import { configureResearchRun, resumeResearchRun } from '../workers/research-cache.js';
|
|
55
|
+
import { currentRunContext } from './run-context.js';
|
|
46
56
|
import { CONTRACT_EXTRACT_PROMPT, parseContractLines, keepGroundedContracts, appendContracts } from './contracts.js';
|
|
47
|
-
import { reconcileTitleSources } from './decompose-fidelity.js';
|
|
48
|
-
import { granularityFloor, granularitySplitHint, isPlanShapeQuestion, isTooCoarse, planShapeIsHostsToAnswer, PLAN_SHAPE_ANSWER } from './decompose-granularity.js';
|
|
57
|
+
import { reconcileTitleSources, renderFragments } from './decompose-fidelity.js';
|
|
58
|
+
import { granularityFloor, granularitySplitHint, isPlanShapeQuestion, isTooCoarse, planShapeIsHostsToAnswer, PLAN_SHAPE_ANSWER, PLAN_SHAPE_TOPIC } from './decompose-granularity.js';
|
|
49
59
|
import { mandatesTestsInSameChange, rewriteBatchTestPlan } from './batch-test-task.js';
|
|
50
60
|
import { REQUIREMENT_EXTRACT_PROMPT, COVERAGE_MAP_PROMPT, parseRequirementLines, keepGroundedRequirements, capRequirements, writeOwnedRequirements, enumerateObligationPassages, uncoveredPassages, extractionRetryHint, parseCoverageMap, accountCoverage, isCrossCuttingRequirement, appendCarriedRequirements, buildRequirementsLedger } from './requirements.js';
|
|
51
61
|
import { groundedCoverage } from './coverage-loop.js';
|
|
@@ -126,23 +136,6 @@ function suspectPlanHint(count) {
|
|
|
126
136
|
+ 'list for the ENTIRE feature, covering every part of the spec end to end. '
|
|
127
137
|
+ 'Output every task, one "- [ ] " line each, nothing else.]');
|
|
128
138
|
}
|
|
129
|
-
// Matches pi's @-file completion token (a path after @, until whitespace).
|
|
130
|
-
const MENTION_RE = /(?:^|\s)@([^\s]+)/g;
|
|
131
|
-
// Trailing punctuation a user naturally types AFTER an @-mention when it sits in
|
|
132
|
-
// prose — "Implement @design.md, reuse…" or "see @spec.md." — which the greedy
|
|
133
|
-
// [^\s]+ above would otherwise swallow into the path. Left unstripped, the
|
|
134
|
-
// resulting "design.md," resolves to no file, expansion is silently skipped, and
|
|
135
|
-
// the planner reasons over a one-line "Implement @design.md" with NO spec inline
|
|
136
|
-
// → it fabricates generic questions and tasks the spec never called for.
|
|
137
|
-
//
|
|
138
|
-
// Measured against a real file: the greedy token from "Implement @design.md,
|
|
139
|
-
// reuse the parser" is `design.md,`, which does not exist; stripped, `design.md`
|
|
140
|
-
// does. None of these chars are legitimate trailing characters of a doc path.
|
|
141
|
-
const MENTION_TRAILING_PUNCT = /[.,;:!?)\]}>"']+$/;
|
|
142
|
-
/** The cleaned path token of an @-mention: greedy match minus trailing prose punctuation. */
|
|
143
|
-
function mentionPath(token) {
|
|
144
|
-
return token.replace(MENTION_TRAILING_PUNCT, '');
|
|
145
|
-
}
|
|
146
139
|
/**
|
|
147
140
|
* Every plan-debug write not yet on disk, chained.
|
|
148
141
|
*
|
|
@@ -161,9 +154,9 @@ export function flushPlanDebug() {
|
|
|
161
154
|
* Fire-and-forget debug line for the PLAN phase (clarify/decompose). It is the
|
|
162
155
|
* only trail that phase has: planning runs before any task file exists, so there
|
|
163
156
|
* is no per-task `TASK_NNNN-debug.log` to write into yet. This goes to
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
157
|
+
* `plan-debug.log` in the run's state dir, beside every other log of the run, so
|
|
158
|
+
* one grep still finds them all. Never throws — the mkdir and the append are both
|
|
159
|
+
* best-effort.
|
|
167
160
|
*
|
|
168
161
|
* Every call site records a plan DECISION (how many titles a round produced,
|
|
169
162
|
* whether a retry was adopted, which clarify answer was auto-resolved), so all of
|
|
@@ -173,11 +166,11 @@ export function flushPlanDebug() {
|
|
|
173
166
|
function logPlanDebug(cwd, msg) {
|
|
174
167
|
if (!shouldLogDebug('event', debugLogLevel()))
|
|
175
168
|
return;
|
|
176
|
-
const line = `${new Date().toISOString()} ${msg}\n`;
|
|
177
|
-
const
|
|
169
|
+
const line = `${new Date().toISOString()} ${sanitizeDebugLine(msg)}\n`;
|
|
170
|
+
const file = runLogPath(cwd, 'plan-debug.log');
|
|
178
171
|
planDebugChain = planDebugChain
|
|
179
|
-
.then(() => fsp.mkdir(
|
|
180
|
-
.then(() => fsp.appendFile(
|
|
172
|
+
.then(() => fsp.mkdir(path.dirname(file), { recursive: true }))
|
|
173
|
+
.then(() => fsp.appendFile(file, line))
|
|
181
174
|
.catch(() => { });
|
|
182
175
|
}
|
|
183
176
|
/**
|
|
@@ -240,61 +233,6 @@ async function triageClarifyQuestion(deps, cwd, featureForModel, existingFilesBl
|
|
|
240
233
|
return null;
|
|
241
234
|
}
|
|
242
235
|
}
|
|
243
|
-
/**
|
|
244
|
-
* Expand any @file references in the feature text by appending each referenced
|
|
245
|
-
* file's contents, so the planning children (clarify, decompose) always see the
|
|
246
|
-
* real spec inline instead of relying on the model to open the file itself.
|
|
247
|
-
* Without this, clarify on a one-line "Implement @spec.md" tends to bail with
|
|
248
|
-
* NONE because, to the model, the request looks small and unambiguous.
|
|
249
|
-
* Unreadable mentions (typos, non-file @tokens) are left untouched; the feature
|
|
250
|
-
* is returned verbatim when nothing readable is referenced.
|
|
251
|
-
*/
|
|
252
|
-
export async function expandFeatureMentions(cwd, feature) {
|
|
253
|
-
const seen = new Set();
|
|
254
|
-
const blocks = [];
|
|
255
|
-
for (const m of feature.matchAll(MENTION_RE)) {
|
|
256
|
-
const rel = mentionPath(m[1]);
|
|
257
|
-
if (rel === '' || seen.has(rel))
|
|
258
|
-
continue;
|
|
259
|
-
seen.add(rel);
|
|
260
|
-
try {
|
|
261
|
-
// Normalize CRLF/CR so an @-mentioned design doc saved on Windows
|
|
262
|
-
// inlines with LF endings the downstream phase parsers expect.
|
|
263
|
-
const body = await readTextFile(path.resolve(cwd, rel));
|
|
264
|
-
if (body.trim().length > 0) {
|
|
265
|
-
blocks.push(`--- contents of ${rel} ---\n${body.trim()}`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
catch {
|
|
269
|
-
// not a readable file — leave the @token in place, skip expansion
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return blocks.length === 0 ? feature : `${feature.trim()}\n\n${blocks.join('\n\n')}`;
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* The @file references in the feature that point at a readable file on disk —
|
|
276
|
-
* the bare path tokens, deduped, in first-seen order. Unreadable mentions
|
|
277
|
-
* (typos, non-file @tokens) are dropped so we never advertise a missing file as
|
|
278
|
-
* an authoritative spec.
|
|
279
|
-
*/
|
|
280
|
-
export async function readableMentions(cwd, feature) {
|
|
281
|
-
const out = [];
|
|
282
|
-
const seen = new Set();
|
|
283
|
-
for (const m of feature.matchAll(MENTION_RE)) {
|
|
284
|
-
const rel = mentionPath(m[1]);
|
|
285
|
-
if (rel === '' || seen.has(rel))
|
|
286
|
-
continue;
|
|
287
|
-
seen.add(rel);
|
|
288
|
-
try {
|
|
289
|
-
await fsp.access(path.resolve(cwd, rel));
|
|
290
|
-
out.push(rel);
|
|
291
|
-
}
|
|
292
|
-
catch {
|
|
293
|
-
// not a readable file — don't thread it into task titles
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return out;
|
|
297
|
-
}
|
|
298
236
|
/** A trailing "[decisions: …]" clause decompose may attach to a task line. */
|
|
299
237
|
const DECISIONS_RE = /\s*\[decisions:\s*(.+?)\]\s*$/i;
|
|
300
238
|
/**
|
|
@@ -342,29 +280,37 @@ export function attachSpecRefs(titles, refs) {
|
|
|
342
280
|
*
|
|
343
281
|
* The plan listing strips the threaded "| decisions … | spec …" tail from each
|
|
344
282
|
* title and keeps the human-readable head, so the model reads clean step names,
|
|
345
|
-
* and it marks the current one
|
|
283
|
+
* and it marks the current one `[this]` — both confirmed by building a fence
|
|
346
284
|
* over threaded titles. The authoritative spec ref still rides on THIS step's own
|
|
347
285
|
* title via attachSpecRefs.
|
|
286
|
+
*
|
|
287
|
+
* Every sibling used to read as unbuilt, which is a lie about the first half of
|
|
288
|
+
* any plan past step one: a step told its predecessors are "done in later runs"
|
|
289
|
+
* re-scaffolds what is already in the tree. The entries carry their own done
|
|
290
|
+
* state, so the listing states it.
|
|
348
291
|
*/
|
|
349
|
-
export function buildScopeFence(
|
|
350
|
-
const n =
|
|
351
|
-
const listing =
|
|
352
|
-
.map((
|
|
353
|
-
const head =
|
|
354
|
-
const
|
|
355
|
-
|
|
292
|
+
export function buildScopeFence(entries, currentIndex) {
|
|
293
|
+
const n = entries.length;
|
|
294
|
+
const listing = entries
|
|
295
|
+
.map((e, i) => {
|
|
296
|
+
const head = e.title.split(' | ')[0].trim();
|
|
297
|
+
const marker = i === currentIndex ? 'this'
|
|
298
|
+
: e.done ? 'done'
|
|
299
|
+
: 'later';
|
|
300
|
+
return `[${i + 1}] [${marker}] ${head}`;
|
|
356
301
|
})
|
|
357
302
|
.join('\n');
|
|
358
303
|
return (`PLAN CONTEXT — this task is STEP ${currentIndex + 1} of ${n} in an already-decomposed plan. `
|
|
359
|
-
+ `Each step below is implemented by its OWN separate run
|
|
360
|
-
+ `
|
|
304
|
+
+ `Each step below is implemented by its OWN separate run: a [done] step is ALREADY BUILT and `
|
|
305
|
+
+ `in the tree (build on it — never rebuild, duplicate or re-scaffold it), a [later] step is `
|
|
306
|
+
+ `not built yet and is NOT your job. Implement ONLY the slice named in "Task" below.\n\n`
|
|
361
307
|
+ `The design/spec document the task references describes the WHOLE system across all ${n} `
|
|
362
308
|
+ `steps. Read it to get exact names, types, and signatures for YOUR step and to understand `
|
|
363
309
|
+ `how your step fits — but DO NOT design, scaffold, schema, route, page, query, component, or `
|
|
364
310
|
+ `test anything that belongs to another step listed below. Your GOAL / CONSTRAINTS / `
|
|
365
311
|
+ `KNOWN-UNKNOWNS must cover only THIS step's slice. Do not pull in tables, endpoints, pages, `
|
|
366
312
|
+ `components, or flows owned by a later step.\n\n`
|
|
367
|
-
+ `The full plan (these run separately — do NOT implement
|
|
313
|
+
+ `The full plan (these run separately — do NOT implement the [later] ones here):\n${listing}`);
|
|
368
314
|
}
|
|
369
315
|
/**
|
|
370
316
|
* The scope fence for step `currentIndex`, plus the REPAIR fence when that step is
|
|
@@ -377,12 +323,16 @@ export function buildScopeFence(titles, currentIndex) {
|
|
|
377
323
|
* `bun run test` yields it while a plain-prose defect yields undefined and only the
|
|
378
324
|
* file pin applies.
|
|
379
325
|
*/
|
|
380
|
-
function buildStepFence(
|
|
381
|
-
const base = buildScopeFence(
|
|
382
|
-
const
|
|
326
|
+
export function buildStepFence(entries, currentIndex) {
|
|
327
|
+
const base = buildScopeFence(entries, currentIndex);
|
|
328
|
+
const title = entries[currentIndex]?.title ?? '';
|
|
329
|
+
const healthRepair = parseHealthRepairTitle(title);
|
|
330
|
+
if (healthRepair)
|
|
331
|
+
return `${base}\n\n${buildHealthRepairFence(healthRepair)}`;
|
|
332
|
+
const repairFile = parseRepairTitleFile(title);
|
|
383
333
|
if (!repairFile)
|
|
384
334
|
return base;
|
|
385
|
-
return `${base}\n\n${buildRepairScopeFence(repairFile, extractFailingCommand(
|
|
335
|
+
return `${base}\n\n${buildRepairScopeFence(repairFile, extractFailingCommand(title))}`;
|
|
386
336
|
}
|
|
387
337
|
/**
|
|
388
338
|
* Drain the gate's root-cause repair queue into the running plan: one scoped
|
|
@@ -428,6 +378,53 @@ async function schedulePendingRepairs(cwd, id, afterIndex, ctx, deps) {
|
|
|
428
378
|
// the plan is best-effort here; the underlying debt is already recorded
|
|
429
379
|
}
|
|
430
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* A red health check at the pre-task checkpoint becomes a repair entry spliced
|
|
383
|
+
* BEFORE the task about to run (health-repair.ts). Returns whether the plan
|
|
384
|
+
* changed — the caller then re-reads it and runs the repair first. No splice when
|
|
385
|
+
* the plan already covers this red (a repair that ran and failed included): the
|
|
386
|
+
* task then proceeds and inherits it, as the baseline differential intends.
|
|
387
|
+
*/
|
|
388
|
+
async function spliceHealthRepair(cwd, id, next, entries, health, ctx, deps) {
|
|
389
|
+
try {
|
|
390
|
+
const red = healthRedSubject(health, cwd, (await deps.repoFiles?.(cwd)) ?? null);
|
|
391
|
+
if (!red)
|
|
392
|
+
return false;
|
|
393
|
+
if (planCoversHealthRed(entries.map(e => e.title), red))
|
|
394
|
+
return false;
|
|
395
|
+
const owners = [];
|
|
396
|
+
for (const f of red.files) {
|
|
397
|
+
const owner = await deps.introducedBy?.(cwd, f);
|
|
398
|
+
if (owner && !owners.includes(owner))
|
|
399
|
+
owners.push(owner);
|
|
400
|
+
}
|
|
401
|
+
const title = buildHealthRepairTitle({ ...red, owners });
|
|
402
|
+
if (!(await insertTaskBefore(cwd, id, next.index, title)))
|
|
403
|
+
return false;
|
|
404
|
+
await deps.record?.(cwd, id, `plan: \`${red.command}\` is red at the checkpoint before step ${next.index + 1} — inserted repair step first: ${title}`);
|
|
405
|
+
notifyRun(ctx, `${id}: \`${red.command}\` is red before "${next.title}" — queued a repair for `
|
|
406
|
+
+ `${red.files.length > 0 ? red.files.join(', ') : 'it'} to run first.`, 'warning');
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* The health baseline a task starts from: the one its (resumed) task file already
|
|
415
|
+
* holds, else a fresh capture at the checkpoint. Null when neither is available.
|
|
416
|
+
*/
|
|
417
|
+
async function baselineAtCheckpoint(ctx, cwd, resumeId, label, deps) {
|
|
418
|
+
if (resumeId) {
|
|
419
|
+
const stored = await readSection(cwd, resumeId, HEALTH_BASELINE_SECTION).catch(() => null);
|
|
420
|
+
const parsed = parseHealthBaseline(stored);
|
|
421
|
+
if (parsed)
|
|
422
|
+
return parsed;
|
|
423
|
+
}
|
|
424
|
+
if (!deps.captureHealthBaseline)
|
|
425
|
+
return null;
|
|
426
|
+
return deps.captureHealthBaseline(ctx, cwd, label);
|
|
427
|
+
}
|
|
431
428
|
/**
|
|
432
429
|
* ORIENT — read the feature, strike what must never reach a planning child, and
|
|
433
430
|
* derive the requirement ledger. Depends on nothing but the feature and the tree,
|
|
@@ -566,6 +563,7 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
|
|
|
566
563
|
generate: hint => deps.runChild('auto-clarify', 'read', prependHint(hint, AUTO_CLARIFY_PROMPT(featureForModel, transcript.forGenerator()))),
|
|
567
564
|
formatHint: PLAN_FORMAT_HINT,
|
|
568
565
|
rules: CLARIFY_QUALITY_RULES,
|
|
566
|
+
topics: [{ id: PLAN_SHAPE_TOPIC, match: isPlanShapeQuestion }],
|
|
569
567
|
cap: MAX_CLARIFY_QUESTIONS,
|
|
570
568
|
log: msg => logPlanDebug(cwd, `clarify: ${msg}`)
|
|
571
569
|
});
|
|
@@ -589,6 +587,10 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
|
|
|
589
587
|
if (planShapeIsHostsToAnswer(ownableRequirements) && isPlanShapeQuestion(plainQ)) {
|
|
590
588
|
logPlanDebug(cwd, `plan-shape question answered host-side (not the triage): ${plainQ.replace(/\s+/g, ' ').slice(0, 120)}`);
|
|
591
589
|
transcript.add('host-set', plainQ, PLAN_SHAPE_ANSWER);
|
|
590
|
+
// The transcript entry alone never stopped the re-draw: the generator
|
|
591
|
+
// is stateless and reworded the same fork (often into the plural),
|
|
592
|
+
// which then reached the user or the triage as if it were open.
|
|
593
|
+
source.settle(PLAN_SHAPE_TOPIC);
|
|
592
594
|
continue;
|
|
593
595
|
}
|
|
594
596
|
// Answer-side triage (grill parity): if the inlined spec already settles
|
|
@@ -658,7 +660,7 @@ export async function decomposePlan(cwd, deps, oriented, clarifications) {
|
|
|
658
660
|
logPlanDebug(cwd, `decompose fidelity: ${plan.sourced}/${plan.titles.length} titles cited a grounded source; `
|
|
659
661
|
+ `${plan.restored.length} restoration(s)`
|
|
660
662
|
+ plan.restored
|
|
661
|
-
.map(r => ` [task ${r.index + 1}: ${r.fragments
|
|
663
|
+
.map(r => ` [task ${r.index + 1}: ${renderFragments(r.fragments)}]`)
|
|
662
664
|
.join(''));
|
|
663
665
|
}
|
|
664
666
|
// Batch-test ban (item 6): drop or scope a whole-project "write all the
|
|
@@ -777,7 +779,12 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
|
|
|
777
779
|
rethrowIfCancelled(err);
|
|
778
780
|
verdict = null;
|
|
779
781
|
}
|
|
780
|
-
|
|
782
|
+
// An `unparseable` verdict IS a verdict of INCOMPLETE — it just names no
|
|
783
|
+
// area. Reading it as "nothing missing" shipped a plan the judge had ruled
|
|
784
|
+
// incomplete; the host names the gap instead so the round still happens.
|
|
785
|
+
const verdictMissing = verdict?.kind === 'incomplete' ? verdict.missing
|
|
786
|
+
: verdict?.kind === 'unparseable' ? [UNNAMED_COVERAGE_GAP]
|
|
787
|
+
: [];
|
|
781
788
|
let acc = null;
|
|
782
789
|
// The monotonic guard's owned-set is grounded DETERMINISTICALLY in
|
|
783
790
|
// requirement↔title token overlap — NOT the coverage-map model's TASK
|
|
@@ -880,6 +887,10 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
|
|
|
880
887
|
.slice(0, 200)}]`
|
|
881
888
|
: ''));
|
|
882
889
|
}
|
|
890
|
+
if (outcome.decision.terminal === true) {
|
|
891
|
+
logPlanDebug(cwd, 'decompose-coverage: TERMINAL — another round asks the same question');
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
883
894
|
}
|
|
884
895
|
const best = rounds.best();
|
|
885
896
|
const round = rounds.round();
|
|
@@ -1026,16 +1037,23 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
1026
1037
|
ground: emitted => keepGroundedScripts(emitted, featureForModel),
|
|
1027
1038
|
append: appendDeclaredScripts
|
|
1028
1039
|
});
|
|
1029
|
-
// Persist the TASK-MAPPED requirements
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
1040
|
+
// Persist the TASK-MAPPED requirements under the plan key of the entry each
|
|
1041
|
+
// was mapped to — the same key `buildAutoBody` is about to write onto the
|
|
1042
|
+
// checkbox line, so the two sides of the join are minted from one index. With
|
|
1043
|
+
// only cross-cutting entries travelling, the mapped ones shape the title list
|
|
1044
|
+
// and then vanish, and a task can narrow a requirement out of its own spec
|
|
1045
|
+
// with nothing to stop it.
|
|
1033
1046
|
// Inert until the owned-requirements injection is wired into the phase
|
|
1034
1047
|
// prompts; recorded regardless so the plan's mapping is auditable per run.
|
|
1035
1048
|
if (best.accounting && best.accounting.mapped.length > 0) {
|
|
1036
1049
|
await writeOwnedRequirements(cwd, best.accounting.mapped
|
|
1037
1050
|
.filter(m => m.task >= 1 && m.task <= titles.length)
|
|
1038
|
-
.map(m => ({
|
|
1051
|
+
.map(m => ({
|
|
1052
|
+
quote: m.req.quote,
|
|
1053
|
+
anchor: m.req.anchor,
|
|
1054
|
+
key: planKeyAt(m.task - 1),
|
|
1055
|
+
title: titles[m.task - 1]
|
|
1056
|
+
})));
|
|
1039
1057
|
}
|
|
1040
1058
|
// persist
|
|
1041
1059
|
const id = await allocateAutoId(cwd);
|
|
@@ -1276,9 +1294,28 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1276
1294
|
return;
|
|
1277
1295
|
}
|
|
1278
1296
|
await updateTaskFrontMatter(cwd, id, { state: 'completed' });
|
|
1297
|
+
// Retention runs HERE and nowhere else: a completed run is the one
|
|
1298
|
+
// moment the state dir is quiet and this process still knows which
|
|
1299
|
+
// repo it was serving. A failed or cancelled run keeps its logs —
|
|
1300
|
+
// those are the ones somebody is about to read.
|
|
1301
|
+
await pruneRunLogs(cwd);
|
|
1279
1302
|
announceDone(active, stage.message, stage.level);
|
|
1280
1303
|
return;
|
|
1281
1304
|
}
|
|
1305
|
+
// ABANDON an entry that has spent its attempt budget. A task that
|
|
1306
|
+
// crashes before it can be checked off is re-entered by every resume,
|
|
1307
|
+
// for as long as anyone keeps resuming — the plan never advances and
|
|
1308
|
+
// the entry never converges. Give up on THIS entry, write down what was
|
|
1309
|
+
// left unfinished, and let the rest of the plan run.
|
|
1310
|
+
if ((next.attempts ?? 0) >= ENTRY_ATTEMPT_BUDGET) {
|
|
1311
|
+
await recordDebt(cwd, next.producedId ?? id, `"${next.title}" was abandoned after ${next.attempts} attempts`
|
|
1312
|
+
+ `${next.lastEnd ? ` (last ended: ${next.lastEnd})` : ''}`
|
|
1313
|
+
+ ' — no verified artifact; the plan moved on without it', 'abandoned');
|
|
1314
|
+
await checkOffTask(cwd, id, next.index, next.producedId ?? '', next.title);
|
|
1315
|
+
notifyBoth(active, `${id}: abandoned "${next.title}" after ${next.attempts} attempts — `
|
|
1316
|
+
+ 'recorded as debt; continuing with the rest of the plan.', 'warning');
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1282
1319
|
// REFUSE to start on a conflicted tree: an unmerged index dooms every
|
|
1283
1320
|
// commit ahead and a `git add -A` would silently mis-resolve it.
|
|
1284
1321
|
const unmerged = deps.unmergedPaths ? await deps.unmergedPaths(cwd) : [];
|
|
@@ -1323,6 +1360,24 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1323
1360
|
if (checkpoint.committed) {
|
|
1324
1361
|
notifyRun(active, `${id}: checkpointed uncommitted work before "${next.title}".`, 'info');
|
|
1325
1362
|
}
|
|
1363
|
+
// REPO-HEALTH BASELINE, taken here because the checkpoint above just made
|
|
1364
|
+
// the tree clean: what the project's own statics say now is what this task
|
|
1365
|
+
// INHERITED, and the verify gate attributes a red check against it instead
|
|
1366
|
+
// of failing the task for a sibling's defect (health-baseline.ts). The
|
|
1367
|
+
// inner task file does not exist yet, so the result is handed to the
|
|
1368
|
+
// runner, which writes the section once its id is allocated.
|
|
1369
|
+
//
|
|
1370
|
+
// A RED baseline is also the one moment every way red enters the tree
|
|
1371
|
+
// is visible before anything builds on it — an accepted regression, a
|
|
1372
|
+
// leftover the checkpoint just committed, a repo red at run start — so
|
|
1373
|
+
// it is where the repair is scheduled (health-repair.ts).
|
|
1374
|
+
const baseline = await baselineAtCheckpoint(active, cwd, resumeId, next.title, deps);
|
|
1375
|
+
if (baseline
|
|
1376
|
+
&& !baseline.outcome.ok
|
|
1377
|
+
&& (await spliceHealthRepair(cwd, id, next, entries, baseline.outcome, active, deps))) {
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
const healthBaseline = baseline ? { healthBaseline: () => Promise.resolve(baseline) } : {};
|
|
1326
1381
|
// Stash ref before the task: compared after the gates so a stash pushed
|
|
1327
1382
|
// during the task (impl model or any child) and left behind is called
|
|
1328
1383
|
// out instead of silently waiting to detonate in a later task.
|
|
@@ -1336,14 +1391,21 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1336
1391
|
announceDone(active, `${id} cancelled before "${next.title}" — resume with /task-auto-resume.`, 'warning');
|
|
1337
1392
|
return;
|
|
1338
1393
|
}
|
|
1394
|
+
// Counted BEFORE the run, not after it: a run that takes pi down
|
|
1395
|
+
// with it would never reach an after-the-fact increment, and an
|
|
1396
|
+
// attempt nobody counted is exactly the one the budget exists to
|
|
1397
|
+
// bound.
|
|
1398
|
+
await beginTaskAttempt(cwd, id, next.index);
|
|
1339
1399
|
const res = await deps.runTask(active, cwd, next.title, {
|
|
1340
1400
|
resumeId,
|
|
1401
|
+
...healthBaseline,
|
|
1341
1402
|
// Fence this step against re-expanding the whole referenced spec:
|
|
1342
1403
|
// name the sibling steps so refine bounds this step's slice. Only
|
|
1343
1404
|
// matters when refine runs fresh (a resumed task past refine ignores
|
|
1344
1405
|
// it), but always supplied so a resume that restarts at refine is
|
|
1345
1406
|
// fenced too.
|
|
1346
|
-
planContext: buildStepFence(entries
|
|
1407
|
+
planContext: buildStepFence(entries, next.index),
|
|
1408
|
+
...(next.key !== undefined && { planKey: next.key }),
|
|
1347
1409
|
onStart: resumeId ? undefined : (innerId => stampTaskInProgress(cwd, id, next.index, innerId, next.title))
|
|
1348
1410
|
});
|
|
1349
1411
|
active = res.ctx ?? active;
|
|
@@ -1356,6 +1418,10 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1356
1418
|
// only the wording is this command's.
|
|
1357
1419
|
if (!runSucceeded(res.end)) {
|
|
1358
1420
|
const policy = RUN_END_POLICY[res.end.kind];
|
|
1421
|
+
// The ending goes on the ENTRY, where the next resume reads it:
|
|
1422
|
+
// the inner task file's own state cannot distinguish the entry
|
|
1423
|
+
// that keeps faulting from the one a user stopped once.
|
|
1424
|
+
await recordTaskEnd(cwd, id, next.index, res.end.kind);
|
|
1359
1425
|
// Demote the INNER task file: it reads `completed` from
|
|
1360
1426
|
// spec-handoff, and leaving it that way is how a failed run's task
|
|
1361
1427
|
// file claims success after the run failed.
|
|
@@ -1396,7 +1462,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1396
1462
|
title: next.title,
|
|
1397
1463
|
tag: id,
|
|
1398
1464
|
// Fence an AUTOFIX re-run against re-expanding the whole spec.
|
|
1399
|
-
planContext: buildStepFence(entries
|
|
1465
|
+
planContext: buildStepFence(entries, next.index),
|
|
1400
1466
|
// res.ok === true means runner.run() completed, so res.taskId is the
|
|
1401
1467
|
// allocated TASK_NNNN id (never empty here). The parent task-list
|
|
1402
1468
|
// check-off runs after verify passes/accepts and before the commit,
|
|
@@ -1481,10 +1547,12 @@ async function handleTaskAuto(args, ctx) {
|
|
|
1481
1547
|
// and the ordinary command path cannot reach us.
|
|
1482
1548
|
try {
|
|
1483
1549
|
await withRun(ctx, { onCancel: terminalCancel }, async () => {
|
|
1484
|
-
// Stamp
|
|
1550
|
+
// Stamp the per-run research-cache id BEFORE planning so enrichment and
|
|
1485
1551
|
// every task's research phase share one run's cache; disabled ⇒ clears any token a
|
|
1486
|
-
// prior run left, so nothing is cached.
|
|
1487
|
-
|
|
1552
|
+
// prior run left, so nothing is cached. It is the RUN's own id (the bracket
|
|
1553
|
+
// opened the run context one statement ago), so the cache file and the
|
|
1554
|
+
// env-notes ledger name this run the same way.
|
|
1555
|
+
configureResearchRun(getConfig().researchCache, currentRunContext(cwd).runId);
|
|
1488
1556
|
const abort = new AbortController();
|
|
1489
1557
|
const deps = defaultDeps(ctx, cwd, abort.signal, deriveTitle(raw));
|
|
1490
1558
|
let id;
|
|
@@ -1557,6 +1625,10 @@ async function handleTaskAutoResume(args, ctx) {
|
|
|
1557
1625
|
// working cache, and a whole-file freshness gate can never hold on a
|
|
1558
1626
|
// greenfield run that installs packages as it goes — so invalidation is
|
|
1559
1627
|
// per entry. See resumeResearchRun.
|
|
1628
|
+
//
|
|
1629
|
+
// The RUN context keeps its own fresh id, deliberately: an external doc
|
|
1630
|
+
// digest does not go stale over a pause, and an environment fact measured
|
|
1631
|
+
// against the tree before it does.
|
|
1560
1632
|
const research = await resumeResearchRun(cwd, getConfig().researchCache);
|
|
1561
1633
|
if (research.reused) {
|
|
1562
1634
|
logPlanDebug(cwd, `research cache: resume reused ${research.entries} entr(ies), `
|
|
@@ -16,6 +16,7 @@ import type { docsRaw, docsFocused } from '../workers/docs-core.js';
|
|
|
16
16
|
import type { fetchRaw, fetchFocused } from '../workers/fetch-core.js';
|
|
17
17
|
import type { npmVersionLookup } from '../workers/npm-version.js';
|
|
18
18
|
import type { SearchCoreInput, SearchCoreResult } from '../workers/search-core.js';
|
|
19
|
+
import type { RunContext } from './run-context.js';
|
|
19
20
|
export declare const USER_CANCELLED = "__user_cancelled__";
|
|
20
21
|
/**
|
|
21
22
|
* Why a phase child did not answer.
|
|
@@ -65,6 +66,13 @@ export interface PhaseDeps {
|
|
|
65
66
|
cwd: string;
|
|
66
67
|
taskId: string;
|
|
67
68
|
signal: AbortSignal;
|
|
69
|
+
/**
|
|
70
|
+
* What is true of the RUN rather than of this task: the inventory, the
|
|
71
|
+
* orientation core, the manifest deps, the ecosystems, the verified tooling.
|
|
72
|
+
* Absent → the phase builds a throwaway context for itself, which is the old
|
|
73
|
+
* per-task behaviour (see run-context.ts).
|
|
74
|
+
*/
|
|
75
|
+
runContext?: RunContext;
|
|
68
76
|
onChildOutput?: (line: string) => void;
|
|
69
77
|
onContextUsage?: (snapshot: ContextSnapshot) => void;
|
|
70
78
|
/**
|
|
@@ -215,6 +223,16 @@ export declare function runPhaseChild(deps: PhaseDeps, name: string, tools: stri
|
|
|
215
223
|
*/
|
|
216
224
|
export declare function formatDegradeHint(hit: LoopHit): string;
|
|
217
225
|
export declare function prependHint(hint: string | null, prompt: string): string;
|
|
226
|
+
/**
|
|
227
|
+
* Append one line per loop kill to the task file's `loop events` section.
|
|
228
|
+
*
|
|
229
|
+
* Best-effort by contract: it runs for every phase child AND every research
|
|
230
|
+
* worker, and not every caller
|
|
231
|
+
* owns a task file on disk (a scripted harness, a bare unit deps bag). A trail
|
|
232
|
+
* that cannot be written must cost the phase nothing — the loop kill itself is
|
|
233
|
+
* already reported through the debug log and the thrown error.
|
|
234
|
+
*/
|
|
235
|
+
export declare function appendLoopEvents(cwd: string, taskId: string, phase: string, r: RunWorkerResult): Promise<void>;
|
|
218
236
|
/**
|
|
219
237
|
* Run a child up to twice; the second attempt gets `emphasized=true` to escalate
|
|
220
238
|
* the prompt. On success, return the validator's value; on two failures, throw
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
12
12
|
import { classifyWorkerFailure } from '../workers/worker-failure.js';
|
|
13
13
|
import { isFatalKill } from '../workers/worker-kill.js';
|
|
14
|
-
import { MAX_LOOP_RESTARTS } from './loop-detector.js';
|
|
14
|
+
import { describeLoopHit, MAX_LOOP_RESTARTS } from './loop-detector.js';
|
|
15
15
|
import { MAX_LEAK_RETRIES } from '../shared/leaked-tool-call.js';
|
|
16
|
-
import {
|
|
16
|
+
import { mergeTaskSection } from './task-io.js';
|
|
17
17
|
import { streamStallCause } from '../shared/stream-watchdog.js';
|
|
18
18
|
import { getConfig } from '../config/config.js';
|
|
19
19
|
import { groupChildArgs, groupWindow } from '../config/group-args.js';
|
|
@@ -238,15 +238,15 @@ export function prependHint(hint, prompt) {
|
|
|
238
238
|
/**
|
|
239
239
|
* Append one line per loop kill to the task file's `loop events` section.
|
|
240
240
|
*
|
|
241
|
-
* Best-effort by contract: it runs for
|
|
241
|
+
* Best-effort by contract: it runs for every phase child AND every research
|
|
242
|
+
* worker, and not every caller
|
|
242
243
|
* owns a task file on disk (a scripted harness, a bare unit deps bag). A trail
|
|
243
244
|
* that cannot be written must cost the phase nothing — the loop kill itself is
|
|
244
245
|
* already reported through the debug log and the thrown error.
|
|
245
246
|
*/
|
|
246
|
-
async function appendLoopEvents(cwd, taskId, phase, r) {
|
|
247
|
+
export async function appendLoopEvents(cwd, taskId, phase, r) {
|
|
247
248
|
const line = (hit, strike, outcome) => `- ${new Date().toISOString()} ${phase} strike ${strike}/${MAX_LOOP_RESTARTS + 1} `
|
|
248
|
-
+ `${
|
|
249
|
-
+ `${hit.windowSize} calls → ${outcome}`;
|
|
249
|
+
+ `${describeLoopHit(hit)} → ${outcome}`;
|
|
250
250
|
const lines = r.restarts.flatMap(x => x.loopHit ?
|
|
251
251
|
[
|
|
252
252
|
line(x.loopHit, x.attempt, x.rescue ? 'degraded — no-tools final attempt' : 'restarted with hint')
|
|
@@ -257,8 +257,7 @@ async function appendLoopEvents(cwd, taskId, phase, r) {
|
|
|
257
257
|
if (lines.length === 0)
|
|
258
258
|
return;
|
|
259
259
|
try {
|
|
260
|
-
|
|
261
|
-
await setTaskSection(cwd, taskId, 'loop events', [existing, ...lines].filter(Boolean).join('\n'));
|
|
260
|
+
await mergeTaskSection(cwd, taskId, 'loop events', existing => [existing ?? '', ...lines].filter(Boolean).join('\n'));
|
|
262
261
|
}
|
|
263
262
|
catch {
|
|
264
263
|
/* best-effort: a trail is never worth failing a phase for */
|
|
@@ -172,6 +172,10 @@ export type VerifyRerunOutcome = {
|
|
|
172
172
|
outcome: 'gap';
|
|
173
173
|
detail: string;
|
|
174
174
|
};
|
|
175
|
+
/** The command word of a shell line, past any leading `VAR=value` assignments.
|
|
176
|
+
* Exported for gate-evidence, which spawns the same shape of line and must
|
|
177
|
+
* resolve the same runner against the same stripped PATH. */
|
|
178
|
+
export declare function leadingBin(line: string): string | null;
|
|
175
179
|
/**
|
|
176
180
|
* Re-run one VERIFY-block command line under the gate's existing
|
|
177
181
|
* env-gap contract, so a debt whose reason NAMES that command can be closed by the
|
package/dist/task/command-run.js
CHANGED
|
@@ -308,8 +308,10 @@ export function classifyCommandRun(run, gapPatterns = [], opts = {}) {
|
|
|
308
308
|
tail: outputTail(run.stdout, run.stderr)
|
|
309
309
|
};
|
|
310
310
|
}
|
|
311
|
-
/** The command word of a shell line, past any leading `VAR=value` assignments.
|
|
312
|
-
|
|
311
|
+
/** The command word of a shell line, past any leading `VAR=value` assignments.
|
|
312
|
+
* Exported for gate-evidence, which spawns the same shape of line and must
|
|
313
|
+
* resolve the same runner against the same stripped PATH. */
|
|
314
|
+
export function leadingBin(line) {
|
|
313
315
|
for (const tok of line.trim().split(/\s+/)) {
|
|
314
316
|
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tok))
|
|
315
317
|
continue;
|