@mjasnikovs/pi-task 0.37.7 → 0.38.1
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/dist/shared/child-output.d.ts +19 -3
- package/dist/shared/child-output.js +21 -5
- package/dist/shared/git-runner.d.ts +39 -0
- package/dist/shared/git-runner.js +38 -0
- package/dist/task/accept-debt.d.ts +37 -61
- package/dist/task/accept-debt.js +67 -130
- package/dist/task/auto-orchestrator.d.ts +7 -57
- package/dist/task/auto-orchestrator.js +25 -499
- package/dist/task/child-runner.d.ts +2 -0
- package/dist/task/child-runner.js +74 -70
- package/dist/task/enforce-guidelines.d.ts +1 -1
- package/dist/task/enforce-guidelines.js +2 -2
- package/dist/task/external-context.d.ts +85 -7
- package/dist/task/external-context.js +100 -63
- package/dist/task/file-inventory.js +22 -41
- package/dist/task/final-gate.d.ts +80 -0
- package/dist/task/final-gate.js +102 -49
- package/dist/task/gate-deps.js +6 -23
- package/dist/task/git-state-guard.d.ts +1 -1
- package/dist/task/git-state-guard.js +1 -7
- package/dist/task/phases.js +40 -83
- package/dist/task/run-final-gate.d.ts +127 -0
- package/dist/task/run-final-gate.js +492 -0
- package/dist/task/task-gates.d.ts +20 -57
- package/dist/task/task-gates.js +11 -11
- package/dist/task/verify-work.d.ts +40 -32
- package/dist/task/verify-work.js +301 -241
- package/dist/workers/docs-core.d.ts +14 -0
- package/dist/workers/docs-core.js +28 -16
- package/dist/workers/fetch-core.d.ts +6 -1
- package/dist/workers/fetch-core.js +26 -33
- package/dist/workers/focused-extractor.d.ts +73 -0
- package/dist/workers/focused-extractor.js +72 -0
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +48 -42
- package/dist/workers/pi-worker-fetch.js +6 -8
- package/dist/workers/typeonly-log.d.ts +13 -0
- package/package.json +1 -1
|
@@ -3,8 +3,22 @@ export declare function parseChildOutput(stdout: string): {
|
|
|
3
3
|
excerpt?: string;
|
|
4
4
|
};
|
|
5
5
|
export declare function normaliseWhitespace(s: string): string;
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
6
|
+
/**
|
|
7
|
+
* THE excerpt predicate: does this excerpt appear verbatim in the source content
|
|
8
|
+
* (whitespace-normalised)? {@link verifyExcerpt} delegates to it, so there is exactly one
|
|
9
|
+
* definition of "verified" in the codebase.
|
|
10
|
+
*
|
|
11
|
+
* False for an excerpt that is empty OR whitespace-only. The emptiness test is applied to
|
|
12
|
+
* the NORMALISED excerpt, not the raw one: `" "` is a non-empty string that normalises to
|
|
13
|
+
* `""`, and `content.includes('')` is true for every content — so the raw-string guard let a
|
|
14
|
+
* whitespace-only excerpt "verify" against anything, while `verifyExcerpt` (which tested the
|
|
15
|
+
* normalised length) called the same input unverified. A citation with no characters in it is
|
|
16
|
+
* not evidence, so both now answer false.
|
|
17
|
+
*
|
|
18
|
+
* Unreachable from the shipped extractors — `parseChildOutput` already maps a whitespace-only
|
|
19
|
+
* `<excerpt>` to `undefined` via `.trim() || undefined`, and every call site guards on that —
|
|
20
|
+
* so this is a latent-disagreement fix, not a behaviour change to any recorded verdict.
|
|
21
|
+
*/
|
|
8
22
|
export declare function isExcerptInContent(excerpt: string, content: string): boolean;
|
|
9
23
|
/**
|
|
10
24
|
* The same verdict as {@link isExcerptInContent}, PLUS a retained record of what was
|
|
@@ -13,7 +27,9 @@ export declare function isExcerptInContent(excerpt: string, content: string): bo
|
|
|
13
27
|
* DIAGNOSABLE after the fact, so it can be attributed to fabrication (the excerpt is nowhere
|
|
14
28
|
* near the content) versus a normaliser gap (it is a markdown-escape or entity variant of
|
|
15
29
|
* text that IS present) WITHOUT re-fetching. It deliberately does NOT loosen the verifier:
|
|
16
|
-
* `.verified`
|
|
30
|
+
* `.verified` IS `isExcerptInContent` — delegated, not re-implemented, so the two cannot
|
|
31
|
+
* drift apart again (they had: a whitespace-only excerpt verified there and failed here).
|
|
32
|
+
* F-3(f) — whether the normaliser needs
|
|
17
33
|
* markdown-escape handling — is left unproven on purpose; you decide that from the retained
|
|
18
34
|
* evidence, not by weakening the one working hallucination detector first.
|
|
19
35
|
*/
|
|
@@ -20,18 +20,34 @@ export function parseChildOutput(stdout) {
|
|
|
20
20
|
export function normaliseWhitespace(s) {
|
|
21
21
|
return s.replace(/\s+/g, ' ').trim();
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
23
|
+
/**
|
|
24
|
+
* THE excerpt predicate: does this excerpt appear verbatim in the source content
|
|
25
|
+
* (whitespace-normalised)? {@link verifyExcerpt} delegates to it, so there is exactly one
|
|
26
|
+
* definition of "verified" in the codebase.
|
|
27
|
+
*
|
|
28
|
+
* False for an excerpt that is empty OR whitespace-only. The emptiness test is applied to
|
|
29
|
+
* the NORMALISED excerpt, not the raw one: `" "` is a non-empty string that normalises to
|
|
30
|
+
* `""`, and `content.includes('')` is true for every content — so the raw-string guard let a
|
|
31
|
+
* whitespace-only excerpt "verify" against anything, while `verifyExcerpt` (which tested the
|
|
32
|
+
* normalised length) called the same input unverified. A citation with no characters in it is
|
|
33
|
+
* not evidence, so both now answer false.
|
|
34
|
+
*
|
|
35
|
+
* Unreachable from the shipped extractors — `parseChildOutput` already maps a whitespace-only
|
|
36
|
+
* `<excerpt>` to `undefined` via `.trim() || undefined`, and every call site guards on that —
|
|
37
|
+
* so this is a latent-disagreement fix, not a behaviour change to any recorded verdict.
|
|
38
|
+
*/
|
|
25
39
|
export function isExcerptInContent(excerpt, content) {
|
|
26
|
-
|
|
40
|
+
const ne = normaliseWhitespace(excerpt);
|
|
41
|
+
if (ne.length === 0)
|
|
27
42
|
return false;
|
|
28
|
-
return normaliseWhitespace(content).includes(
|
|
43
|
+
return normaliseWhitespace(content).includes(ne);
|
|
29
44
|
}
|
|
30
45
|
export function verifyExcerpt(excerpt, content) {
|
|
31
46
|
const nc = normaliseWhitespace(content);
|
|
32
47
|
const ne = normaliseWhitespace(excerpt);
|
|
33
48
|
return {
|
|
34
|
-
|
|
49
|
+
// Delegated on purpose: this struct adds EVIDENCE, never a second opinion.
|
|
50
|
+
verified: isExcerptInContent(excerpt, content),
|
|
35
51
|
contentSha256: createHash('sha256').update(nc).digest('hex'),
|
|
36
52
|
contentLength: nc.length,
|
|
37
53
|
normalisedExcerpt: ne
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git-runner — the ONE way this codebase runs `git`.
|
|
3
|
+
*
|
|
4
|
+
* WHY IT EXISTS. The right shape already existed, privately, inside
|
|
5
|
+
* `task/git-state-guard.ts`: an abort-aware async runner carrying an injectable
|
|
6
|
+
* `spawnFn` seam, returning `{stdout, exitCode}` and never throwing. Nothing
|
|
7
|
+
* outside that file could reach it, so ~27 other sites hand-rolled their own —
|
|
8
|
+
* with FOUR incompatible failure contracts (throws / returns null / returns '' /
|
|
9
|
+
* returns a result object) and THREE different maxBuffer limits. A caller reading
|
|
10
|
+
* one of them learns nothing about the next, and a git failure means something
|
|
11
|
+
* different at every site. Lifting the seam here does not add a capability; it
|
|
12
|
+
* removes the ambiguity about which contract you are holding.
|
|
13
|
+
*
|
|
14
|
+
* THE CONTRACT, deliberately narrow:
|
|
15
|
+
* - NEVER THROWS. A missing git, a non-repo cwd, a fatal subcommand — all of
|
|
16
|
+
* them arrive as a non-zero `exitCode`. Callers branch on the number, they do
|
|
17
|
+
* not wrap in try/catch. (`git rev-parse --verify HEAD` on an unborn HEAD is
|
|
18
|
+
* an ordinary answer, not an exception, and every guard built on this one
|
|
19
|
+
* treats "git could not tell me" as "no claim" rather than a crash.)
|
|
20
|
+
* - Only `stdout` and `exitCode` are exposed. stderr is deliberately absent:
|
|
21
|
+
* the callers that legitimately need it (auto-commit's identity-failure
|
|
22
|
+
* sniffing) also need `aborted`, and widening this type for them would push
|
|
23
|
+
* two fields nobody else reads onto every call site.
|
|
24
|
+
* - `signal` is honoured by the underlying runChild, including its listener
|
|
25
|
+
* detach discipline (GitHub issue #9) — a run-long orchestrator signal does
|
|
26
|
+
* not accumulate one listener per git invocation.
|
|
27
|
+
* - `env` entries are MERGED over `process.env` (the GIT_INDEX_FILE
|
|
28
|
+
* throwaway-index pattern), not substituted for it.
|
|
29
|
+
* - `spawnFn` is the test seam: pass a fake from `test-utils/fake-spawn.ts` and
|
|
30
|
+
* the runner never touches a real repo.
|
|
31
|
+
*/
|
|
32
|
+
import { type SpawnFn } from './child-process.js';
|
|
33
|
+
export interface GitRunner {
|
|
34
|
+
(args: string[], env?: Record<string, string>): Promise<{
|
|
35
|
+
stdout: string;
|
|
36
|
+
exitCode: number;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
export declare function makeGit(cwd: string, signal?: AbortSignal, spawnFn?: SpawnFn): GitRunner;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git-runner — the ONE way this codebase runs `git`.
|
|
3
|
+
*
|
|
4
|
+
* WHY IT EXISTS. The right shape already existed, privately, inside
|
|
5
|
+
* `task/git-state-guard.ts`: an abort-aware async runner carrying an injectable
|
|
6
|
+
* `spawnFn` seam, returning `{stdout, exitCode}` and never throwing. Nothing
|
|
7
|
+
* outside that file could reach it, so ~27 other sites hand-rolled their own —
|
|
8
|
+
* with FOUR incompatible failure contracts (throws / returns null / returns '' /
|
|
9
|
+
* returns a result object) and THREE different maxBuffer limits. A caller reading
|
|
10
|
+
* one of them learns nothing about the next, and a git failure means something
|
|
11
|
+
* different at every site. Lifting the seam here does not add a capability; it
|
|
12
|
+
* removes the ambiguity about which contract you are holding.
|
|
13
|
+
*
|
|
14
|
+
* THE CONTRACT, deliberately narrow:
|
|
15
|
+
* - NEVER THROWS. A missing git, a non-repo cwd, a fatal subcommand — all of
|
|
16
|
+
* them arrive as a non-zero `exitCode`. Callers branch on the number, they do
|
|
17
|
+
* not wrap in try/catch. (`git rev-parse --verify HEAD` on an unborn HEAD is
|
|
18
|
+
* an ordinary answer, not an exception, and every guard built on this one
|
|
19
|
+
* treats "git could not tell me" as "no claim" rather than a crash.)
|
|
20
|
+
* - Only `stdout` and `exitCode` are exposed. stderr is deliberately absent:
|
|
21
|
+
* the callers that legitimately need it (auto-commit's identity-failure
|
|
22
|
+
* sniffing) also need `aborted`, and widening this type for them would push
|
|
23
|
+
* two fields nobody else reads onto every call site.
|
|
24
|
+
* - `signal` is honoured by the underlying runChild, including its listener
|
|
25
|
+
* detach discipline (GitHub issue #9) — a run-long orchestrator signal does
|
|
26
|
+
* not accumulate one listener per git invocation.
|
|
27
|
+
* - `env` entries are MERGED over `process.env` (the GIT_INDEX_FILE
|
|
28
|
+
* throwaway-index pattern), not substituted for it.
|
|
29
|
+
* - `spawnFn` is the test seam: pass a fake from `test-utils/fake-spawn.ts` and
|
|
30
|
+
* the runner never touches a real repo.
|
|
31
|
+
*/
|
|
32
|
+
import { runChildDefault } from './child-process.js';
|
|
33
|
+
export function makeGit(cwd, signal, spawnFn) {
|
|
34
|
+
return async (args, env) => {
|
|
35
|
+
const r = await runChildDefault({ command: 'git', args, ...(env ? { env: { ...process.env, ...env } } : {}) }, cwd, signal, { mode: 'text' }, spawnFn);
|
|
36
|
+
return { stdout: r.stdout, exitCode: r.exitCode };
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -92,71 +92,43 @@ export declare function readAcceptDebtsRaw(cwd: string): Promise<string>;
|
|
|
92
92
|
export declare function parseAcceptDebts(raw: string): AcceptDebt[];
|
|
93
93
|
/** Read + parse in one step. */
|
|
94
94
|
export declare function readAcceptDebts(cwd: string): Promise<AcceptDebt[]>;
|
|
95
|
-
/** Record a user-ACCEPTED-despite-verify-FAIL debt. */
|
|
96
|
-
export declare function recordAcceptDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
97
95
|
/**
|
|
98
|
-
* Record
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
export declare function recordCrossTaskDeletionDebt(cwd: string, taskId: string, deletion: {
|
|
96
|
+
* Record one durable defect against the run ledger, stamped with the DebtOrigin that
|
|
97
|
+
* says how it was reached (see DebtOrigin's doc for what each class asserts).
|
|
98
|
+
*
|
|
99
|
+
* This was eight exported wrappers that differed only in that one string literal, so
|
|
100
|
+
* a new class cost six edit sites. The origin is not cosmetic: the final gate
|
|
101
|
+
* re-checks and reports BY class, and an unattended auto-pick may never be recorded
|
|
102
|
+
* as the 'accepted' class — that one asserts a human weighed the failing artifact.
|
|
103
|
+
*
|
|
104
|
+
* Best-effort by construction (appendDebt swallows its own faults): the ledger is an
|
|
105
|
+
* auditing aid and must never break the gate sequence that calls it.
|
|
106
|
+
*
|
|
107
|
+
* `origin` is REQUIRED and deliberately has no default. It used to default to
|
|
108
|
+
* 'accepted' (the legacy 2-field on-disk shape), and that default silently absorbed
|
|
109
|
+
* a dropped argument: the eight-recorder collapse migrated one call in
|
|
110
|
+
* scripts/ignored-writes-ab.ts without its origin, so a run-level 'final-gate'
|
|
111
|
+
* demotion was stamped as a human 'accepted'. The wrappers each carried their class
|
|
112
|
+
* in the NAME, so no migration of them could lose it; a defaulted parameter can.
|
|
113
|
+
* Making it explicit turns that whole class of slip into a compile error.
|
|
114
|
+
*/
|
|
115
|
+
export declare function recordDebt(cwd: string, taskId: string, reason: string, origin: DebtOrigin): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* The reason text a CROSS-TASK DELETION debt stores (mx5 run 12 PROMPT 2): the task's
|
|
118
|
+
* work deleted a file a DIFFERENT task's commit introduced, verify FAILed, and the
|
|
119
|
+
* user ACCEPTed — so the deletion survives into the next commit. The shape is fixed
|
|
120
|
+
* and machine-parseable so the final gate's re-check can extract the path and prove
|
|
121
|
+
* the debt resolved iff the file is back in the tree; it lives here, next to
|
|
122
|
+
* extractDeletedDebtPath, because the writer and the reader of that shape have to
|
|
123
|
+
* move together.
|
|
124
|
+
*/
|
|
125
|
+
export declare function crossTaskDeletionReason(deletion: {
|
|
129
126
|
path: string;
|
|
130
127
|
owner: string;
|
|
131
|
-
}):
|
|
132
|
-
/**
|
|
133
|
-
* Record a YOLO-ACCEPTED debt: unattended auto-pick took the verify-FAIL picker's
|
|
134
|
-
* ACCEPT branch because there was nobody to ask (yolo.ts). Its own origin — and
|
|
135
|
-
* therefore its own line in the final gate's surfaced report — so a later audit
|
|
136
|
-
* reading only the artifacts can never read it as "a human decided this".
|
|
137
|
-
*/
|
|
138
|
-
export declare function recordYoloAcceptDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
139
|
-
/**
|
|
140
|
-
* Record a FINAL-GATE UNOBSERVED debt (mx5 run 14): the final gate demoted one of its
|
|
141
|
-
* OWN checks after two tree-changing fix attempts returned an identical ranked-first
|
|
142
|
-
* failure — unfalsifiable in this environment, so the gate stopped paying for it and
|
|
143
|
-
* converged on the remaining checks. Durable so the NEXT run's gate re-checks it: it
|
|
144
|
-
* is model-/environment-judged, so the re-check surfaces it (never auto-closes it).
|
|
145
|
-
* The taskId is the run's parent id — the demotion is a run-level decision.
|
|
146
|
-
*/
|
|
147
|
-
export declare function recordFinalGateUnobservedDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
148
|
-
/**
|
|
149
|
-
* Record a ROOT-CAUSE debt (mx5 run 14 / PROMPT item 5): this task's verify FAILed
|
|
150
|
-
* on a pre-existing defect in a file ANOTHER task created and this task never
|
|
151
|
-
* touched. The current task is not at fault — its work (and, at the enforce site,
|
|
152
|
-
* the enforce pass's edits) is KEPT — but the defect is real and still in the tree,
|
|
153
|
-
* so it is recorded here and a scoped repair task is queued (root-cause-repair.ts).
|
|
154
|
-
* Behavioral/model-judged, so the final gate surfaces it rather than auto-closing it.
|
|
155
|
-
*/
|
|
156
|
-
export declare function recordRootCauseDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
128
|
+
}): string;
|
|
157
129
|
/**
|
|
158
130
|
* The deleted path a cross-task-deletion debt names (the fixed shape
|
|
159
|
-
*
|
|
131
|
+
* crossTaskDeletionReason builds). Null on any other reason text — an
|
|
160
132
|
* unextractable path means the re-check cannot prove anything, so the debt
|
|
161
133
|
* stays open (surface, never re-hide).
|
|
162
134
|
*/
|
|
@@ -248,5 +220,9 @@ export declare function annotateDebtConflicts(debts: AcceptDebt[], introducedBy:
|
|
|
248
220
|
* instructions — and a conflicting claim carries its contradiction inline.
|
|
249
221
|
*/
|
|
250
222
|
export declare function buildAcceptDebtNote(open: AcceptDebt[]): string;
|
|
251
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* One-line provenance label for a debt, for the surfaced report. Straight off
|
|
225
|
+
* DEBT_LABELS, so a new origin cannot ship without one; an absent or unregistered
|
|
226
|
+
* origin falls back to the 'accepted' class exactly as the branch chain did.
|
|
227
|
+
*/
|
|
252
228
|
export declare function describeDebt(d: AcceptDebt): string;
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -40,6 +40,33 @@ const MAX_REASON_LENGTH = 300;
|
|
|
40
40
|
* cleanly and any stray tab in a reason is flattened to a space before storage.
|
|
41
41
|
*/
|
|
42
42
|
const FIELD_SEP = '\t';
|
|
43
|
+
/**
|
|
44
|
+
* Origin → the one-line provenance label the surfaced report prints for it
|
|
45
|
+
* (describeDebt). This table IS the origin registry: `Record<DebtOrigin, string>`
|
|
46
|
+
* makes a new union member a compile error until it has a label, and both the
|
|
47
|
+
* describe side and the parse side read it, so adding an origin is one union member
|
|
48
|
+
* plus one line here — not the six edit sites the per-origin recorder functions used
|
|
49
|
+
* to cost. The label text is user-facing (it lands in the final gate's report and in
|
|
50
|
+
* the FAIL picker), so these strings are byte-frozen.
|
|
51
|
+
*/
|
|
52
|
+
const DEBT_LABELS = {
|
|
53
|
+
accepted: 'accepted despite verify-FAIL',
|
|
54
|
+
'enforce-revert': 'enforce re-verify FAILED then the edits were reverted (defect indicts the ORIGINAL work, still shipped)',
|
|
55
|
+
'enforce-kept': 'enforce re-verify FAILED on a check the enforce diff cannot reach — the guideline edits were KEPT (reverting them could not fix it) and the defect indicts the ORIGINAL work, still shipped',
|
|
56
|
+
'frozen-blocked': 'repo health blocked by a spec-frozen path (cross-task contradiction — no task may perform the fixing edit)',
|
|
57
|
+
'cross-task-deletion': "a sibling task's committed deliverable was DELETED by this task's work and the deletion was accepted (still missing from the tree)",
|
|
58
|
+
'yolo-accepted': 'auto-ACCEPTED by YOLO mode despite verify-FAIL (unattended — no human weighed this)',
|
|
59
|
+
'final-gate': 'final-gate check DEMOTED to UNOBSERVED (identical failure across two tree-changing fix attempts — unfalsifiable in that environment, never proven passing)',
|
|
60
|
+
'root-cause': "verify FAILed on a PRE-EXISTING defect in another task's file that this task never touched (this task's work was kept; a scoped repair task was queued for the root cause)"
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* A stored origin field is honoured only when it is a REGISTERED origin — anything
|
|
64
|
+
* else (a hand-edited line, a field from a newer build) falls back to the 'accepted'
|
|
65
|
+
* class rather than being trusted or dropped.
|
|
66
|
+
*/
|
|
67
|
+
function isKnownOrigin(origin) {
|
|
68
|
+
return origin !== undefined && Object.hasOwn(DEBT_LABELS, origin);
|
|
69
|
+
}
|
|
43
70
|
export function acceptDebtFile(cwd) {
|
|
44
71
|
return path.join(tasksDir(cwd), ACCEPT_DEBT_FILE);
|
|
45
72
|
}
|
|
@@ -78,15 +105,10 @@ export function parseAcceptDebts(raw) {
|
|
|
78
105
|
out.push({
|
|
79
106
|
taskId: parts[0].trim(),
|
|
80
107
|
reason: parts[1].trim(),
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|| origin === 'yolo-accepted'
|
|
86
|
-
|| origin === 'final-gate'
|
|
87
|
-
|| origin === 'root-cause') ?
|
|
88
|
-
{ origin: origin }
|
|
89
|
-
: {}),
|
|
108
|
+
// 'accepted' is deliberately NOT carried: it is the legacy 2-field shape's
|
|
109
|
+
// implicit class, so an absent origin and a spelled-out 'accepted' must
|
|
110
|
+
// parse to the same record.
|
|
111
|
+
...(isKnownOrigin(origin) && origin !== 'accepted' ? { origin } : {}),
|
|
90
112
|
...(verifyCommand !== undefined && verifyCommand.length > 0 ? { verifyCommand } : {})
|
|
91
113
|
});
|
|
92
114
|
}
|
|
@@ -147,112 +169,44 @@ async function appendDebt(cwd, entry) {
|
|
|
147
169
|
// best-effort ledger
|
|
148
170
|
}
|
|
149
171
|
}
|
|
150
|
-
/** Record a user-ACCEPTED-despite-verify-FAIL debt. */
|
|
151
|
-
export async function recordAcceptDebt(cwd, taskId, reason) {
|
|
152
|
-
await appendDebt(cwd, { taskId: taskId.trim(), reason: normaliseReason(reason) });
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Record an ENFORCE-REVERT debt (mx5 run 10 item 3): an enforce re-verify FAILED and
|
|
156
|
-
* the enforce edits were reverted, but the FAIL indicted the ORIGINAL work — so the
|
|
157
|
-
* defect is still in the shipped tree. Durable so the final gate re-checks/surfaces it
|
|
158
|
-
* rather than letting it die with the revert.
|
|
159
|
-
*/
|
|
160
|
-
export async function recordEnforceRevertDebt(cwd, taskId, reason) {
|
|
161
|
-
await appendDebt(cwd, {
|
|
162
|
-
taskId: taskId.trim(),
|
|
163
|
-
reason: normaliseReason(reason),
|
|
164
|
-
origin: 'enforce-revert'
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Record an ENFORCE-KEPT debt (mx5 run 18 / nexttask 4): an enforce re-verify FAILED
|
|
169
|
-
* on a check whose named files are DISJOINT from the enforce commit's own diff, so
|
|
170
|
-
* the edits were kept — discarding them could not have repaired a failure they cannot
|
|
171
|
-
* reach. The defect is real and still in the shipped tree, so it is recorded with the
|
|
172
|
-
* same durability as an enforce-revert; only the disposition of the edits differs.
|
|
173
|
-
*/
|
|
174
|
-
export async function recordEnforceKeptDebt(cwd, taskId, reason) {
|
|
175
|
-
await appendDebt(cwd, {
|
|
176
|
-
taskId: taskId.trim(),
|
|
177
|
-
reason: normaliseReason(reason),
|
|
178
|
-
origin: 'enforce-kept'
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Record a FROZEN-BLOCKED debt (mx5 run 12 / PROMPT 1 layer B): a repo-health FAIL
|
|
183
|
-
* whose static findings can only be fixed by editing a path this task's spec froze —
|
|
184
|
-
* a cross-task contradiction no unattended re-run may resolve. Recorded when the gate
|
|
185
|
-
* loop routes to the picker (regardless of what the human then picks), so the final
|
|
186
|
-
* gate re-checks it at run end. Static-class by reason prefix (`repo health: …`), so
|
|
187
|
-
* it auto-closes iff the final gate's own static check passes.
|
|
188
|
-
*/
|
|
189
|
-
export async function recordFrozenBlockedDebt(cwd, taskId, reason) {
|
|
190
|
-
await appendDebt(cwd, {
|
|
191
|
-
taskId: taskId.trim(),
|
|
192
|
-
reason: normaliseReason(reason),
|
|
193
|
-
origin: 'frozen-blocked'
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Record a CROSS-TASK DELETION debt (mx5 run 12 PROMPT 2): the task's work deleted a
|
|
198
|
-
* file a DIFFERENT task's commit introduced, verify FAILed, and the user ACCEPTed —
|
|
199
|
-
* so the deletion survives into the next commit. The reason is a fixed machine-
|
|
200
|
-
* parseable shape (`deleted \`<path>\` …`) so the final gate's re-check can extract
|
|
201
|
-
* the path and prove the debt resolved iff the file is back in the tree.
|
|
202
|
-
*/
|
|
203
|
-
export async function recordCrossTaskDeletionDebt(cwd, taskId, deletion) {
|
|
204
|
-
await appendDebt(cwd, {
|
|
205
|
-
taskId: taskId.trim(),
|
|
206
|
-
reason: normaliseReason(`deleted \`${deletion.path}\` — ${deletion.owner}'s committed deliverable, removed by this task's work`),
|
|
207
|
-
origin: 'cross-task-deletion'
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Record a YOLO-ACCEPTED debt: unattended auto-pick took the verify-FAIL picker's
|
|
212
|
-
* ACCEPT branch because there was nobody to ask (yolo.ts). Its own origin — and
|
|
213
|
-
* therefore its own line in the final gate's surfaced report — so a later audit
|
|
214
|
-
* reading only the artifacts can never read it as "a human decided this".
|
|
215
|
-
*/
|
|
216
|
-
export async function recordYoloAcceptDebt(cwd, taskId, reason) {
|
|
217
|
-
await appendDebt(cwd, {
|
|
218
|
-
taskId: taskId.trim(),
|
|
219
|
-
reason: normaliseReason(reason),
|
|
220
|
-
origin: 'yolo-accepted'
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
172
|
/**
|
|
224
|
-
* Record
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
173
|
+
* Record one durable defect against the run ledger, stamped with the DebtOrigin that
|
|
174
|
+
* says how it was reached (see DebtOrigin's doc for what each class asserts).
|
|
175
|
+
*
|
|
176
|
+
* This was eight exported wrappers that differed only in that one string literal, so
|
|
177
|
+
* a new class cost six edit sites. The origin is not cosmetic: the final gate
|
|
178
|
+
* re-checks and reports BY class, and an unattended auto-pick may never be recorded
|
|
179
|
+
* as the 'accepted' class — that one asserts a human weighed the failing artifact.
|
|
180
|
+
*
|
|
181
|
+
* Best-effort by construction (appendDebt swallows its own faults): the ledger is an
|
|
182
|
+
* auditing aid and must never break the gate sequence that calls it.
|
|
183
|
+
*
|
|
184
|
+
* `origin` is REQUIRED and deliberately has no default. It used to default to
|
|
185
|
+
* 'accepted' (the legacy 2-field on-disk shape), and that default silently absorbed
|
|
186
|
+
* a dropped argument: the eight-recorder collapse migrated one call in
|
|
187
|
+
* scripts/ignored-writes-ab.ts without its origin, so a run-level 'final-gate'
|
|
188
|
+
* demotion was stamped as a human 'accepted'. The wrappers each carried their class
|
|
189
|
+
* in the NAME, so no migration of them could lose it; a defaulted parameter can.
|
|
190
|
+
* Making it explicit turns that whole class of slip into a compile error.
|
|
230
191
|
*/
|
|
231
|
-
export async function
|
|
232
|
-
await appendDebt(cwd, {
|
|
233
|
-
taskId: taskId.trim(),
|
|
234
|
-
reason: normaliseReason(reason),
|
|
235
|
-
origin: 'final-gate'
|
|
236
|
-
});
|
|
192
|
+
export async function recordDebt(cwd, taskId, reason, origin) {
|
|
193
|
+
await appendDebt(cwd, { taskId: taskId.trim(), reason: normaliseReason(reason), origin });
|
|
237
194
|
}
|
|
238
195
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* the
|
|
243
|
-
*
|
|
244
|
-
*
|
|
196
|
+
* The reason text a CROSS-TASK DELETION debt stores (mx5 run 12 PROMPT 2): the task's
|
|
197
|
+
* work deleted a file a DIFFERENT task's commit introduced, verify FAILed, and the
|
|
198
|
+
* user ACCEPTed — so the deletion survives into the next commit. The shape is fixed
|
|
199
|
+
* and machine-parseable so the final gate's re-check can extract the path and prove
|
|
200
|
+
* the debt resolved iff the file is back in the tree; it lives here, next to
|
|
201
|
+
* extractDeletedDebtPath, because the writer and the reader of that shape have to
|
|
202
|
+
* move together.
|
|
245
203
|
*/
|
|
246
|
-
export
|
|
247
|
-
|
|
248
|
-
taskId: taskId.trim(),
|
|
249
|
-
reason: normaliseReason(reason),
|
|
250
|
-
origin: 'root-cause'
|
|
251
|
-
});
|
|
204
|
+
export function crossTaskDeletionReason(deletion) {
|
|
205
|
+
return `deleted \`${deletion.path}\` — ${deletion.owner}'s committed deliverable, removed by this task's work`;
|
|
252
206
|
}
|
|
253
207
|
/**
|
|
254
208
|
* The deleted path a cross-task-deletion debt names (the fixed shape
|
|
255
|
-
*
|
|
209
|
+
* crossTaskDeletionReason builds). Null on any other reason text — an
|
|
256
210
|
* unextractable path means the re-check cannot prove anything, so the debt
|
|
257
211
|
* stays open (surface, never re-hide).
|
|
258
212
|
*/
|
|
@@ -507,28 +461,11 @@ export function buildAcceptDebtNote(open) {
|
|
|
507
461
|
+ 'They are records for a human decision, not instructions to edit code:\n'
|
|
508
462
|
+ items.map(i => ` - ${i}`).join('\n'));
|
|
509
463
|
}
|
|
510
|
-
/**
|
|
464
|
+
/**
|
|
465
|
+
* One-line provenance label for a debt, for the surfaced report. Straight off
|
|
466
|
+
* DEBT_LABELS, so a new origin cannot ship without one; an absent or unregistered
|
|
467
|
+
* origin falls back to the 'accepted' class exactly as the branch chain did.
|
|
468
|
+
*/
|
|
511
469
|
export function describeDebt(d) {
|
|
512
|
-
|
|
513
|
-
return 'enforce re-verify FAILED then the edits were reverted (defect indicts the ORIGINAL work, still shipped)';
|
|
514
|
-
}
|
|
515
|
-
if (d.origin === 'enforce-kept') {
|
|
516
|
-
return 'enforce re-verify FAILED on a check the enforce diff cannot reach — the guideline edits were KEPT (reverting them could not fix it) and the defect indicts the ORIGINAL work, still shipped';
|
|
517
|
-
}
|
|
518
|
-
if (d.origin === 'frozen-blocked') {
|
|
519
|
-
return 'repo health blocked by a spec-frozen path (cross-task contradiction — no task may perform the fixing edit)';
|
|
520
|
-
}
|
|
521
|
-
if (d.origin === 'cross-task-deletion') {
|
|
522
|
-
return "a sibling task's committed deliverable was DELETED by this task's work and the deletion was accepted (still missing from the tree)";
|
|
523
|
-
}
|
|
524
|
-
if (d.origin === 'yolo-accepted') {
|
|
525
|
-
return 'auto-ACCEPTED by YOLO mode despite verify-FAIL (unattended — no human weighed this)';
|
|
526
|
-
}
|
|
527
|
-
if (d.origin === 'root-cause') {
|
|
528
|
-
return "verify FAILed on a PRE-EXISTING defect in another task's file that this task never touched (this task's work was kept; a scoped repair task was queued for the root cause)";
|
|
529
|
-
}
|
|
530
|
-
if (d.origin === 'final-gate') {
|
|
531
|
-
return 'final-gate check DEMOTED to UNOBSERVED (identical failure across two tree-changing fix attempts — unfalsifiable in that environment, never proven passing)';
|
|
532
|
-
}
|
|
533
|
-
return 'accepted despite verify-FAIL';
|
|
470
|
+
return isKnownOrigin(d.origin) ? DEBT_LABELS[d.origin] : DEBT_LABELS.accepted;
|
|
534
471
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
import { type FinalGateFixFn } from './gate-deps.js';
|
|
3
2
|
import { type GateDeps } from './task-gates.js';
|
|
4
|
-
import { type
|
|
3
|
+
import { type FinalGateStageDeps } from './run-final-gate.js';
|
|
5
4
|
/**
|
|
6
5
|
* Injectable seams so the planner and loop are testable without spawning pi.
|
|
7
|
-
* `runChild` is the planning-only seam used by planAuto; everything else
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* `runChild` is the planning-only seam used by planAuto; everything else is one of
|
|
7
|
+
* two shared gate surfaces — the per-task one ({@link GateDeps}, driven by
|
|
8
|
+
* runGatesForTask) and the run-end one ({@link FinalGateStageDeps}, driven by
|
|
9
|
+
* runFinalGateStage) — both built by {@link buildGateDeps} / defaultDeps. Only the
|
|
10
|
+
* loop's own repo-integrity probes are declared here.
|
|
10
11
|
*/
|
|
11
|
-
export interface AutoDeps extends GateDeps {
|
|
12
|
+
export interface AutoDeps extends GateDeps, FinalGateStageDeps {
|
|
12
13
|
runChild: (name: string, tools: string, prompt: string) => Promise<string>;
|
|
13
14
|
/**
|
|
14
15
|
* Paths with unmerged index entries (an in-progress merge conflict). The loop
|
|
@@ -24,57 +25,6 @@ export interface AutoDeps extends GateDeps {
|
|
|
24
25
|
* Absent (tests) → the check is skipped.
|
|
25
26
|
*/
|
|
26
27
|
stashRef?: (cwd: string) => Promise<string | null>;
|
|
27
|
-
/**
|
|
28
|
-
* Whole-repo FINAL integration gate, run once when every task is checked off
|
|
29
|
-
* and BEFORE the run is declared complete (see final-gate.ts): the project's
|
|
30
|
-
* own static checks plus its own test/build commands, unaided. Absent (tests /
|
|
31
|
-
* gate off) → the run completes as before.
|
|
32
|
-
*/
|
|
33
|
-
finalGate?: (cwd: string, planText?: string) => Promise<{
|
|
34
|
-
ok: boolean;
|
|
35
|
-
reason: string;
|
|
36
|
-
failures?: string[];
|
|
37
|
-
debtNote?: string;
|
|
38
|
-
openDebts?: AcceptDebt[];
|
|
39
|
-
/** Set ⇒ the gate observed nothing dynamic: UNOBSERVED, not PASS. */
|
|
40
|
-
unobserved?: string;
|
|
41
|
-
}>;
|
|
42
|
-
/**
|
|
43
|
-
* Bounded model-driven fix pass for a final-gate FAIL (see final-gate-fix.ts),
|
|
44
|
-
* offered as the picker's third option. Runs the fix child, applies the
|
|
45
|
-
* command-shrink guard, and re-runs the gate; the result's `ok` means the gate
|
|
46
|
-
* now passes. Absent (tests / no fix wiring) → the picker keeps only
|
|
47
|
-
* Leave-failed / Accept, exactly the pre-autofix behavior.
|
|
48
|
-
*/
|
|
49
|
-
finalGateFix?: FinalGateFixFn;
|
|
50
|
-
/**
|
|
51
|
-
* Paths currently uncommitted in the working tree (`git status` shape), used to
|
|
52
|
-
* detect SUB-FIXES a non-converging final-gate autofix left behind (mx5 run 13
|
|
53
|
-
* PROMPT 4 item 3). Every task is committed by the time the final gate runs, so
|
|
54
|
-
* anything dirty here is the fix pass's own work. Absent → the stranded-fix
|
|
55
|
-
* handling is skipped entirely (prior behavior).
|
|
56
|
-
*/
|
|
57
|
-
pendingChanges?: (cwd: string) => Promise<string[]>;
|
|
58
|
-
/**
|
|
59
|
-
* Re-derive the still-open ACCEPT-debt ledger against the tree AS IT IS NOW
|
|
60
|
-
* (final-gate.ts `deriveOpenDebts`). Needed because the run's "N recorded
|
|
61
|
-
* verify-FAIL defect(s) are STILL unresolved" report used to be built from the
|
|
62
|
-
* FIRST gate result and the converged-autofix path then rebuilt the gate
|
|
63
|
-
* outcome as a bare `{ok, reason}` — so `openDebts` was not merely un-actioned,
|
|
64
|
-
* it was GONE from the value, and no code path could ever clear, re-check or
|
|
65
|
-
* act on it (mx5 run 18: four defects reported STILL OPEN at 14:58, one of them
|
|
66
|
-
* fixed by the autofix that converged at 15:03, and the report never moved).
|
|
67
|
-
*
|
|
68
|
-
* `staticOk` is the caller's PROOF about the current statics, never a guess:
|
|
69
|
-
* pass true only where the gate itself just passed them. Absent (tests) → the
|
|
70
|
-
* post-autofix re-check is skipped and the pre-autofix report stands, exactly
|
|
71
|
-
* the prior behavior.
|
|
72
|
-
*/
|
|
73
|
-
recheckOpenDebts?: (cwd: string, staticOk: boolean) => Promise<{
|
|
74
|
-
openDebts: AcceptDebt[];
|
|
75
|
-
debtNote?: string;
|
|
76
|
-
trail?: string[];
|
|
77
|
-
}>;
|
|
78
28
|
}
|
|
79
29
|
/**
|
|
80
30
|
* Expand any @file references in the feature text by appending each referenced
|