@mjasnikovs/pi-task 0.38.11 → 0.38.13
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 +8 -5
- package/dist/config/config.d.ts +0 -1
- package/dist/config/config.js +0 -1
- package/dist/config/register.js +0 -2
- package/dist/index.js +0 -2
- package/dist/shared/child-process.d.ts +8 -0
- package/dist/shared/command-watchdog.d.ts +1 -1
- package/dist/shared/command-watchdog.js +1 -1
- package/dist/task/accept-debt.d.ts +47 -0
- package/dist/task/accept-debt.js +127 -28
- package/dist/task/auto-orchestrator.js +91 -114
- package/dist/task/child-runner.d.ts +39 -25
- package/dist/task/child-runner.js +59 -31
- package/dist/task/child-status.d.ts +95 -0
- package/dist/task/child-status.js +99 -0
- package/dist/task/command-run.d.ts +36 -0
- package/dist/task/command-run.js +48 -1
- package/dist/task/command-watchdog.js +1 -1
- package/dist/task/context-usage.d.ts +4 -3
- package/dist/task/context-usage.js +4 -3
- package/dist/task/contracts.js +18 -35
- package/dist/task/deep-render-check.d.ts +47 -0
- package/dist/task/deep-render-check.js +110 -65
- package/dist/task/env-notes.d.ts +3 -3
- package/dist/task/env-notes.js +24 -35
- package/dist/task/final-gate-fix.d.ts +1 -1
- package/dist/task/final-gate-fix.js +1 -1
- package/dist/task/final-gate.d.ts +5 -151
- package/dist/task/final-gate.js +81 -379
- package/dist/task/gate-child.d.ts +8 -10
- package/dist/task/gate-child.js +15 -19
- package/dist/task/gate-deps.d.ts +29 -0
- package/dist/task/gate-deps.js +192 -206
- package/dist/task/gate-tally.d.ts +189 -0
- package/dist/task/gate-tally.js +249 -0
- package/dist/task/implementation-turn.d.ts +201 -0
- package/dist/task/implementation-turn.js +263 -0
- package/dist/task/launch-contract.js +27 -43
- package/dist/task/ledger.d.ts +38 -0
- package/dist/task/ledger.js +83 -0
- package/dist/task/loop-detector.d.ts +14 -8
- package/dist/task/loop-detector.js +36 -12
- package/dist/task/orchestrator.d.ts +61 -126
- package/dist/task/orchestrator.js +67 -294
- package/dist/task/plan-orchestrator.js +34 -33
- package/dist/task/requirements.d.ts +1 -1
- package/dist/task/requirements.js +50 -66
- package/dist/task/root-cause-repair.js +20 -32
- package/dist/task/run-bracket.d.ts +75 -0
- package/dist/task/run-bracket.js +41 -0
- package/dist/task/stall-detector.d.ts +110 -0
- package/dist/task/stall-detector.js +159 -0
- package/dist/task/verify-work.d.ts +53 -67
- package/dist/task/verify-work.js +15 -11
- package/dist/workers/single-read-extension.d.ts +1 -1
- package/dist/workers/single-read-extension.js +5 -4
- package/dist/workers/single-read-guard.d.ts +32 -10
- package/dist/workers/single-read-guard.js +67 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/@mjasnikovs/pi-task)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
[](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)
|
|
12
|
-
[](#development)
|
|
13
13
|
[](./tsconfig.json)
|
|
14
14
|
|
|
15
15
|
</div>
|
|
@@ -72,7 +72,7 @@ A whole plan — `/task-auto` splits it into an ordered task list and runs each
|
|
|
72
72
|
| `/task-auto <feature>` | Plan a feature into a task list and run each title through `/task` in order (resumable). |
|
|
73
73
|
| `/task-auto-resume [--unattended]` | Resume the active `/task-auto` run at the next unfinished task. `--unattended` is the boot-hook form: in-flight runs only. |
|
|
74
74
|
| `/task-auto-cancel` | Stop the `/task-auto` loop after the current task (still resumable). |
|
|
75
|
-
| `/task-config` | Toggle pi-task settings in an editor dialog: remote control,
|
|
75
|
+
| `/task-config` | Toggle pi-task settings in an editor dialog: remote control, auto-commit, verify work, enforce guidelines, project tour, parallel research, research cache, search engine, command timeout, stuck reply retry, yolo mode, debug logs, one `watch:` toggle per live tool, and one `ext:` toggle per installed host extension. |
|
|
76
76
|
| `/remote` | Show the QR code & URLs for the web view (`/remote stop` to stop). Answer grill questions, start tasks, and watch progress from your phone. |
|
|
77
77
|
|
|
78
78
|
## The pipeline
|
|
@@ -204,7 +204,6 @@ Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings pe
|
|
|
204
204
|
| Setting | Default | What it does |
|
|
205
205
|
| --- | --- | --- |
|
|
206
206
|
| **remote control** | on | The remote UI server (QR code, phone access). Turn off to never start it. |
|
|
207
|
-
| **compress thinking** | on | After each message, compresses the model's `<think>` blocks down to the decisions/constraints/facts that matter later — keeping long local-model runs from drowning their own context in self-talk. |
|
|
208
207
|
| **auto-commit** | on | Snapshots the working tree into one git commit per `/task-auto` sub-task (see above). |
|
|
209
208
|
| **verify work** | on | After each `/task` (and `/task-auto` task) implements — but **before** it's checked off or committed — actually **runs** the spec's own `VERIFY` block in the real workspace. pi-task otherwise only _authors_ a VERIFY block and never executes it, so a task that doesn't build is indistinguishable from one that works. A fresh `read` + `bash` child of the same local model runs the declared check, observes the real output, and reports **PASS/FAIL** (a legitimately no-op VERIFY is a PASS). On FAIL the run doesn't dead-stop: you get a boxed picker — **Autofix** (re-run the implementation turn against the failure, then re-verify; no attempt cap) or **Accept** (override a misjudged artifact) — and dismissing it pauses the run, resumable. A genuine clean pass is also the behavioral signal that lets **enforce guidelines** fix in place (see below). |
|
|
210
209
|
| **enforce guidelines** | on | After each `/task` (and `/task-auto` task) is committed, re-checks that commit's work against the project's `AGENTS.md` / `CLAUDE.md` (in the working directory). A bare fix-in-place pass trashes working code (A/B-proven), so enforcement is gated on the **verify work** signal. **With** a genuine verify pass: a fresh `read` + `edit` child of the same local model reads the **last commit's** diff and fixes violations in place; its fixes are committed **separately** as an `ENFORCE GUIDELINES` commit, then the verify signal is re-run against the enforced tree — a regression **reverts** the enforce commit and keeps the verified work. **Without** that signal (verify off, no spec, or an accept-override): the pass runs read-only and only **reports** violations, never rewrites logic. Either way a violation it can't clear (or a pass that can't run) only **warns** — the task commit already landed, so the run continues. Skipped when nothing was committed for the task. |
|
|
@@ -233,13 +232,17 @@ Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings pe
|
|
|
233
232
|
| `CHROME_BIN` | verify-work render check | Explicit headless Chrome-family binary. Tried before the Playwright cache and a browser on `PATH`. No browser found ⇒ the render check SKIPs; it never installs one. |
|
|
234
233
|
| `PLAYWRIGHT_BROWSERS_PATH` | verify-work render check | Where to look for a cached Playwright Chromium (defaults to `~/.cache/ms-playwright`, or `~/Library/Caches/ms-playwright` on macOS). |
|
|
235
234
|
|
|
236
|
-
Tasks are persisted to `<cwd>/.pi-tasks/TASK_NNNN.md`.
|
|
235
|
+
Tasks are persisted to `<cwd>/.pi-tasks/TASK_NNNN.md`. A run also keeps small
|
|
236
|
+
line-oriented ledgers beside them — contracts, launch contract, environment
|
|
237
|
+
notes, accepted debt, repair queue, requirements — plus `*-debug.log` when
|
|
238
|
+
**debug logs** is on. Add `.pi-tasks/` to your `.gitignore` if you don't want
|
|
239
|
+
them checked in.
|
|
237
240
|
|
|
238
241
|
## Development
|
|
239
242
|
|
|
240
243
|
```sh
|
|
241
244
|
bun install
|
|
242
|
-
bun run test #
|
|
245
|
+
bun run test # 3772 tests across 212 files
|
|
243
246
|
bun run lint # prettier + eslint + tsc --noEmit
|
|
244
247
|
bun run build # tsc → dist/
|
|
245
248
|
```
|
package/dist/config/config.d.ts
CHANGED
package/dist/config/config.js
CHANGED
package/dist/config/register.js
CHANGED
|
@@ -94,8 +94,6 @@ function booleanItem(id, label, description) {
|
|
|
94
94
|
export const ITEMS = [
|
|
95
95
|
booleanItem('remote', 'remote control', 'Serve the task UI on your local network so you can follow and steer a run from '
|
|
96
96
|
+ 'your phone. Prints a QR code to scan when it starts'),
|
|
97
|
-
booleanItem('compressReasoning', 'compress thinking', "Shrink the model's thinking blocks once it has moved on, so a long run keeps more "
|
|
98
|
-
+ 'room for the work itself'),
|
|
99
97
|
booleanItem('autoCommit', 'auto-commit', 'Make a git commit before and after every sub-task, so each step is a checkpoint '
|
|
100
98
|
+ 'you can read back or roll back to'),
|
|
101
99
|
booleanItem('verifyWork', 'verify work', 'When a task says it is done, actually run the checks its spec asks for and report '
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,6 @@ import { registerTaskAuto } from './task/auto-orchestrator.js';
|
|
|
4
4
|
import { registerTaskPlan } from './task/plan-orchestrator.js';
|
|
5
5
|
import { registerWorkers } from './workers/index.js';
|
|
6
6
|
import { registerRemote } from './remote/register.js';
|
|
7
|
-
import { registerThinkingCompression } from './thinking/compress.js';
|
|
8
7
|
import { registerCommandWatchdog } from './task/command-watchdog.js';
|
|
9
8
|
import { registerStreamWatchdog } from './task/stream-watchdog.js';
|
|
10
9
|
export default function (pi) {
|
|
@@ -14,7 +13,6 @@ export default function (pi) {
|
|
|
14
13
|
registerTaskPlan(pi);
|
|
15
14
|
registerWorkers(pi);
|
|
16
15
|
registerRemote(pi);
|
|
17
|
-
registerThinkingCompression(pi);
|
|
18
16
|
registerCommandWatchdog(pi);
|
|
19
17
|
registerStreamWatchdog(pi);
|
|
20
18
|
}
|
|
@@ -81,6 +81,14 @@ export interface LoopHit {
|
|
|
81
81
|
call: ToolCall;
|
|
82
82
|
count: number;
|
|
83
83
|
windowSize: number;
|
|
84
|
+
/**
|
|
85
|
+
* Set when the kill came from the whole-run StallDetector rather than the
|
|
86
|
+
* short-window LoopDetector, naming which of its two rules tripped
|
|
87
|
+
* (task/stall-detector.ts). Absent for an ordinary loop hit. Carried here so
|
|
88
|
+
* a stall rides the kill/restart plumbing the loop hit already has instead of
|
|
89
|
+
* needing a second channel.
|
|
90
|
+
*/
|
|
91
|
+
stall?: 'no-new-ground' | 'context-churn';
|
|
84
92
|
}
|
|
85
93
|
export interface ContextSnapshot {
|
|
86
94
|
tokens: number;
|
|
@@ -54,7 +54,7 @@ export interface WatchdogDeps {
|
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* Stable substring of {@link reminderMessage}, used by the steer loop
|
|
57
|
-
* (
|
|
57
|
+
* (implementation-turn steerUntilDone) to recognise the watchdog's follow-up turn in
|
|
58
58
|
* the session entries — the artifact that distinguishes a watchdog abort from a
|
|
59
59
|
* human ESC. Interpolated into the message so the detector and the text cannot
|
|
60
60
|
* drift apart.
|
|
@@ -53,7 +53,7 @@ function correction() {
|
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
55
|
* Stable substring of {@link reminderMessage}, used by the steer loop
|
|
56
|
-
* (
|
|
56
|
+
* (implementation-turn steerUntilDone) to recognise the watchdog's follow-up turn in
|
|
57
57
|
* the session entries — the artifact that distinguishes a watchdog abort from a
|
|
58
58
|
* human ESC. Interpolated into the message so the detector and the text cannot
|
|
59
59
|
* drift apart.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CommandRunner } from './command-run.js';
|
|
1
2
|
/**
|
|
2
3
|
* Provenance of a recorded defect:
|
|
3
4
|
* - 'accepted' — the user chose ACCEPT despite a verify-FAIL (the original class).
|
|
@@ -226,3 +227,49 @@ export declare function buildAcceptDebtNote(open: AcceptDebt[]): string;
|
|
|
226
227
|
* origin falls back to the 'accepted' class exactly as the branch chain did.
|
|
227
228
|
*/
|
|
228
229
|
export declare function describeDebt(d: AcceptDebt): string;
|
|
230
|
+
/**
|
|
231
|
+
* ACCEPT-debt re-check (mx5 run 4 B3 / run 8 TASK_0012): read the ledger of tasks
|
|
232
|
+
* the user accepted despite a verify-FAIL and re-check each against the CURRENT
|
|
233
|
+
* tree. A static-class debt whose statics now pass is provably RESOLVED (a later
|
|
234
|
+
* task fixed it) and pruned from the ledger; every other debt cannot be proven
|
|
235
|
+
* resolved deterministically, so it stays OPEN and is surfaced — a run may not
|
|
236
|
+
* complete silently carrying an accepted defect. FP-safe by construction (see
|
|
237
|
+
* accept-debt.ts). Best-effort: a ledger read/write failure must never break the
|
|
238
|
+
* caller.
|
|
239
|
+
*
|
|
240
|
+
* FACTORED OUT of runFinalIntegrationGate (nexttask 6): the derivation has to be
|
|
241
|
+
* runnable at a SECOND moment — after a converged final-gate autofix, where the
|
|
242
|
+
* orchestrator used to rebuild its gate outcome as a bare `{ok, reason}` and drop
|
|
243
|
+
* `openDebts` entirely. The report a run ends on has to be derived from the tree
|
|
244
|
+
* the run ends with, not from the tree as it was before the fix pass.
|
|
245
|
+
*
|
|
246
|
+
* `staticOk` is the caller's claim about the CURRENT statics, and it is the only
|
|
247
|
+
* thing that can auto-close a static-class debt — so a caller that does not know
|
|
248
|
+
* must pass `false` (unprovable ⇒ stays open), never a guess.
|
|
249
|
+
*/
|
|
250
|
+
export declare function deriveOpenDebts(cwd: string, staticOk: boolean): Promise<{
|
|
251
|
+
openDebts: AcceptDebt[];
|
|
252
|
+
debtNote?: string;
|
|
253
|
+
trail?: string[];
|
|
254
|
+
}>;
|
|
255
|
+
/**
|
|
256
|
+
* Re-run ONE debt's stored VERIFY command for the re-check, with the no-write guard
|
|
257
|
+
* (`inv-no-write`) wrapped around it.
|
|
258
|
+
*
|
|
259
|
+
* A VERIFY command is the project's own command and may legitimately write (a build
|
|
260
|
+
* emits `dist/`, a suite writes a snapshot). What it may NOT do is turn the tree into
|
|
261
|
+
* a passing tree and have that count as the debt being fixed — the run would then be
|
|
262
|
+
* certifying its own side effect. So tracked state is captured before and after, and
|
|
263
|
+
* a pass that came with a tracked change is downgraded to INCONCLUSIVE with the
|
|
264
|
+
* change named. Untracked output is left alone: it is what a build legitimately
|
|
265
|
+
* produces, and `git status --porcelain` in a repo with the usual ignores does not
|
|
266
|
+
* see it.
|
|
267
|
+
*
|
|
268
|
+
* A repository the guard cannot read (no git, git absent) is not a licence to skip
|
|
269
|
+
* the guard: the re-run is INCONCLUSIVE there, because "nothing changed" would be an
|
|
270
|
+
* assumption rather than an observation.
|
|
271
|
+
*/
|
|
272
|
+
export declare function rerunDebtVerifyCommand(cwd: string, command: string,
|
|
273
|
+
/** The spawner, for BOTH the command and the tracked-state reads. Injected so
|
|
274
|
+
* the guard's four outcomes are testable without a repo or a real command. */
|
|
275
|
+
run?: CommandRunner): VerifyRerunResult;
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -25,10 +25,14 @@
|
|
|
25
25
|
* model, so it is SURFACED, never auto-closed — biasing hard toward informing the
|
|
26
26
|
* user rather than re-hiding a live defect. "Nothing still open = clean."
|
|
27
27
|
*/
|
|
28
|
-
import
|
|
28
|
+
import { existsSync } from 'node:fs';
|
|
29
29
|
import * as path from 'node:path';
|
|
30
|
+
import * as fsp from 'node:fs/promises';
|
|
31
|
+
import { runVerifyCommandLine, spawnCommand } from './command-run.js';
|
|
32
|
+
import { taskThatIntroduced } from './task-provenance.js';
|
|
33
|
+
import { makeLedger } from './ledger.js';
|
|
30
34
|
import { parseVerifyBlockStrict } from './spec-validation.js';
|
|
31
|
-
import { taskFilePath
|
|
35
|
+
import { taskFilePath } from './task-io.js';
|
|
32
36
|
import { isUnfailableCommand } from './unfailable-command.js';
|
|
33
37
|
const ACCEPT_DEBT_FILE = 'accept-debt.md';
|
|
34
38
|
/** Cap kept records so a run that accepts many FAILs cannot grow the report unboundedly. */
|
|
@@ -69,16 +73,11 @@ function isKnownOrigin(origin) {
|
|
|
69
73
|
return origin !== undefined && Object.hasOwn(DEBT_LABELS, origin);
|
|
70
74
|
}
|
|
71
75
|
export function acceptDebtFile(cwd) {
|
|
72
|
-
return path
|
|
76
|
+
return ledger.path(cwd);
|
|
73
77
|
}
|
|
74
78
|
/** The raw stored ledger ('' when none recorded yet). Parse with parseAcceptDebts. */
|
|
75
79
|
export async function readAcceptDebtsRaw(cwd) {
|
|
76
|
-
|
|
77
|
-
return (await fsp.readFile(acceptDebtFile(cwd), 'utf8')).trim();
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return '';
|
|
81
|
-
}
|
|
80
|
+
return ledger.readRaw(cwd);
|
|
82
81
|
}
|
|
83
82
|
/**
|
|
84
83
|
* Parse the stored ledger into records. Fields are tab-separated: `id`, `reason`, and
|
|
@@ -117,7 +116,7 @@ export function parseAcceptDebts(raw) {
|
|
|
117
116
|
}
|
|
118
117
|
/** Read + parse in one step. */
|
|
119
118
|
export async function readAcceptDebts(cwd) {
|
|
120
|
-
return
|
|
119
|
+
return ledger.read(cwd);
|
|
121
120
|
}
|
|
122
121
|
function normaliseReason(reason) {
|
|
123
122
|
return reason
|
|
@@ -142,6 +141,18 @@ function serialize(d) {
|
|
|
142
141
|
function debtKey(d) {
|
|
143
142
|
return `${d.origin ?? 'accepted'} ${d.taskId.toLowerCase()} ${d.reason.toLowerCase()}`;
|
|
144
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* The debt ledger. `onNoop: 'skip'` — a duplicate is a return, not a rewrite: the
|
|
146
|
+
* file is touched only when a NEW debt enters it.
|
|
147
|
+
*/
|
|
148
|
+
const ledger = makeLedger({
|
|
149
|
+
file: ACCEPT_DEBT_FILE,
|
|
150
|
+
max: MAX_DEBTS,
|
|
151
|
+
key: debtKey,
|
|
152
|
+
serialize,
|
|
153
|
+
parse: parseAcceptDebts,
|
|
154
|
+
onNoop: 'skip'
|
|
155
|
+
});
|
|
145
156
|
/**
|
|
146
157
|
* Append one accepted-despite-FAIL record, deduplicated against what is already
|
|
147
158
|
* stored (case-insensitive on task id + reason), keeping the newest MAX_DEBTS.
|
|
@@ -158,13 +169,7 @@ async function appendDebt(cwd, entry) {
|
|
|
158
169
|
const verifyCommand = entry.verifyCommand ?? (await classifyVerifyCommand(cwd, entry.taskId, entry.reason));
|
|
159
170
|
if (verifyCommand !== null && verifyCommand !== undefined)
|
|
160
171
|
entry = { ...entry, verifyCommand };
|
|
161
|
-
|
|
162
|
-
const seen = new Set(existing.map(debtKey));
|
|
163
|
-
if (seen.has(debtKey(entry)))
|
|
164
|
-
return;
|
|
165
|
-
const kept = [...existing, entry].slice(-MAX_DEBTS);
|
|
166
|
-
await fsp.mkdir(tasksDir(cwd), { recursive: true });
|
|
167
|
-
await fsp.writeFile(acceptDebtFile(cwd), kept.map(serialize).join('\n') + '\n', 'utf8');
|
|
172
|
+
await ledger.append(cwd, [entry]);
|
|
168
173
|
}
|
|
169
174
|
catch {
|
|
170
175
|
// best-effort ledger
|
|
@@ -217,17 +222,7 @@ export function extractDeletedDebtPath(reason) {
|
|
|
217
222
|
}
|
|
218
223
|
/** Overwrite the ledger with exactly these records (used to prune resolved debts). */
|
|
219
224
|
export async function writeAcceptDebts(cwd, debts) {
|
|
220
|
-
|
|
221
|
-
await fsp.mkdir(tasksDir(cwd), { recursive: true });
|
|
222
|
-
if (debts.length === 0) {
|
|
223
|
-
await fsp.writeFile(acceptDebtFile(cwd), '', 'utf8');
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
await fsp.writeFile(acceptDebtFile(cwd), debts.map(serialize).join('\n') + '\n', 'utf8');
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
// best-effort ledger
|
|
230
|
-
}
|
|
225
|
+
await ledger.write(cwd, debts);
|
|
231
226
|
}
|
|
232
227
|
/**
|
|
233
228
|
* STATIC-CLASS debt: one whose accepted FAIL was the deterministic whole-repo static
|
|
@@ -484,3 +479,107 @@ export function buildAcceptDebtNote(open) {
|
|
|
484
479
|
export function describeDebt(d) {
|
|
485
480
|
return isKnownOrigin(d.origin) ? DEBT_LABELS[d.origin] : DEBT_LABELS.accepted;
|
|
486
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* ACCEPT-debt re-check (mx5 run 4 B3 / run 8 TASK_0012): read the ledger of tasks
|
|
484
|
+
* the user accepted despite a verify-FAIL and re-check each against the CURRENT
|
|
485
|
+
* tree. A static-class debt whose statics now pass is provably RESOLVED (a later
|
|
486
|
+
* task fixed it) and pruned from the ledger; every other debt cannot be proven
|
|
487
|
+
* resolved deterministically, so it stays OPEN and is surfaced — a run may not
|
|
488
|
+
* complete silently carrying an accepted defect. FP-safe by construction (see
|
|
489
|
+
* accept-debt.ts). Best-effort: a ledger read/write failure must never break the
|
|
490
|
+
* caller.
|
|
491
|
+
*
|
|
492
|
+
* FACTORED OUT of runFinalIntegrationGate (nexttask 6): the derivation has to be
|
|
493
|
+
* runnable at a SECOND moment — after a converged final-gate autofix, where the
|
|
494
|
+
* orchestrator used to rebuild its gate outcome as a bare `{ok, reason}` and drop
|
|
495
|
+
* `openDebts` entirely. The report a run ends on has to be derived from the tree
|
|
496
|
+
* the run ends with, not from the tree as it was before the fix pass.
|
|
497
|
+
*
|
|
498
|
+
* `staticOk` is the caller's claim about the CURRENT statics, and it is the only
|
|
499
|
+
* thing that can auto-close a static-class debt — so a caller that does not know
|
|
500
|
+
* must pass `false` (unprovable ⇒ stays open), never a guess.
|
|
501
|
+
*/
|
|
502
|
+
export async function deriveOpenDebts(cwd, staticOk) {
|
|
503
|
+
const { open: openRaw, resolved, trail } = recheckAcceptDebts(await readAcceptDebts(cwd), {
|
|
504
|
+
staticOk,
|
|
505
|
+
// Cross-task-deletion debts auto-close iff the deleted file is back in the
|
|
506
|
+
// tree — a deterministic existence check, corroborating the per-file
|
|
507
|
+
// provenance the record already carries.
|
|
508
|
+
fileExists: rel => existsSync(path.join(cwd, rel)),
|
|
509
|
+
// VERIFY-COMMAND class (nexttask 5): a debt that NAMES a command is settled
|
|
510
|
+
// by running that command, under the gate's own env-gap contract and behind
|
|
511
|
+
// the no-write guard below.
|
|
512
|
+
rerunVerify: cmd => rerunDebtVerifyCommand(cwd, cmd)
|
|
513
|
+
});
|
|
514
|
+
if (resolved.length > 0)
|
|
515
|
+
await writeAcceptDebts(cwd, openRaw);
|
|
516
|
+
// Conflicting-claim annotation (mx5 run 11): an existence-as-failure debt whose
|
|
517
|
+
// named file is another task's committed deliverable is a plan defect — surface
|
|
518
|
+
// the contradiction with the debt so nobody (human or child) treats the claim as
|
|
519
|
+
// a deletion instruction. Pure git-history lookup; degrades to no annotation.
|
|
520
|
+
const openDebts = annotateDebtConflicts(openRaw, p => taskThatIntroduced(cwd, p));
|
|
521
|
+
const debtNote = buildAcceptDebtNote(openDebts);
|
|
522
|
+
return { openDebts, ...(debtNote ? { debtNote } : {}), ...(trail.length > 0 ? { trail } : {}) };
|
|
523
|
+
}
|
|
524
|
+
/** Per-command ceiling for a debt re-run (`inv-bounded`). */
|
|
525
|
+
const DEBT_RERUN_TIMEOUT_MS = 300_000;
|
|
526
|
+
/**
|
|
527
|
+
* Extra infrastructure-gap shapes recognised ONLY when re-running a debt's command,
|
|
528
|
+
* never in the gate's own verdicts. A driver that reports its connection simply
|
|
529
|
+
* closed (`ERR_POSTGRES_CONNECTION_CLOSED` — what bun's SQL client says when the
|
|
530
|
+
* database is not there at all, as on this box with the mx5 container stopped) is an
|
|
531
|
+
* absent dependency, and calling that "the defect is still present" would be a
|
|
532
|
+
* finding the environment invented. Kept out of INFRA_GAP_OUTPUT_RE on purpose: in a
|
|
533
|
+
* gate verdict the same wording can be a real fault the suite must own, and only the
|
|
534
|
+
* debt re-check needs the conservative reading — where it costs nothing, because gap
|
|
535
|
+
* and fail both leave the debt open.
|
|
536
|
+
*/
|
|
537
|
+
const DEBT_INFRA_GAP_RE = /ERR_POSTGRES_CONNECTION_CLOSED|ERR_MYSQL_CONNECTION|ECONNRESET/i;
|
|
538
|
+
/**
|
|
539
|
+
* Re-run ONE debt's stored VERIFY command for the re-check, with the no-write guard
|
|
540
|
+
* (`inv-no-write`) wrapped around it.
|
|
541
|
+
*
|
|
542
|
+
* A VERIFY command is the project's own command and may legitimately write (a build
|
|
543
|
+
* emits `dist/`, a suite writes a snapshot). What it may NOT do is turn the tree into
|
|
544
|
+
* a passing tree and have that count as the debt being fixed — the run would then be
|
|
545
|
+
* certifying its own side effect. So tracked state is captured before and after, and
|
|
546
|
+
* a pass that came with a tracked change is downgraded to INCONCLUSIVE with the
|
|
547
|
+
* change named. Untracked output is left alone: it is what a build legitimately
|
|
548
|
+
* produces, and `git status --porcelain` in a repo with the usual ignores does not
|
|
549
|
+
* see it.
|
|
550
|
+
*
|
|
551
|
+
* A repository the guard cannot read (no git, git absent) is not a licence to skip
|
|
552
|
+
* the guard: the re-run is INCONCLUSIVE there, because "nothing changed" would be an
|
|
553
|
+
* assumption rather than an observation.
|
|
554
|
+
*/
|
|
555
|
+
export function rerunDebtVerifyCommand(cwd, command,
|
|
556
|
+
/** The spawner, for BOTH the command and the tracked-state reads. Injected so
|
|
557
|
+
* the guard's four outcomes are testable without a repo or a real command. */
|
|
558
|
+
run = spawnCommand) {
|
|
559
|
+
const tracked = () => {
|
|
560
|
+
const r = run({
|
|
561
|
+
cwd,
|
|
562
|
+
bin: 'git',
|
|
563
|
+
args: ['status', '--porcelain', '--untracked-files=no'],
|
|
564
|
+
timeoutMs: 60_000
|
|
565
|
+
});
|
|
566
|
+
return r.failedToStart || r.status !== 0 ? null : r.stdout;
|
|
567
|
+
};
|
|
568
|
+
const before = tracked();
|
|
569
|
+
const r = runVerifyCommandLine(cwd, command, DEBT_RERUN_TIMEOUT_MS, DEBT_INFRA_GAP_RE, run);
|
|
570
|
+
if (r.outcome === 'fail')
|
|
571
|
+
return { outcome: 'fail', detail: `exit ${r.status} — ${r.tail}` };
|
|
572
|
+
if (r.outcome === 'gap')
|
|
573
|
+
return { outcome: 'gap', detail: r.detail };
|
|
574
|
+
const after = tracked();
|
|
575
|
+
if (before === null || after === null) {
|
|
576
|
+
return { outcome: 'gap', detail: 'tracked-state guard could not read git status' };
|
|
577
|
+
}
|
|
578
|
+
if (before !== after) {
|
|
579
|
+
return {
|
|
580
|
+
outcome: 'gap',
|
|
581
|
+
detail: 'the re-run itself CHANGED tracked files — a command that edits the tree into a pass proves nothing'
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
return { outcome: 'pass' };
|
|
585
|
+
}
|