@mjasnikovs/pi-task 0.18.3 → 0.18.5
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/config/config.d.ts +11 -0
- package/dist/config/config.js +4 -1
- package/dist/config/register.js +5 -0
- package/dist/task/accept-debt.d.ts +52 -0
- package/dist/task/accept-debt.js +0 -0
- package/dist/task/auto-orchestrator.d.ts +2 -0
- package/dist/task/auto-orchestrator.js +20 -0
- package/dist/task/enforce-guidelines.d.ts +2 -2
- package/dist/task/enforce-guidelines.js +36 -3
- package/dist/task/env-notes.d.ts +24 -8
- package/dist/task/env-notes.js +124 -24
- package/dist/task/final-gate.d.ts +8 -0
- package/dist/task/final-gate.js +27 -7
- package/dist/task/frozen-path-guard.d.ts +39 -0
- package/dist/task/frozen-path-guard.js +116 -0
- package/dist/task/gate-deps.d.ts +10 -0
- package/dist/task/gate-deps.js +122 -3
- package/dist/task/probe-gaming.d.ts +60 -0
- package/dist/task/probe-gaming.js +0 -0
- package/dist/task/repo-health-check.d.ts +11 -0
- package/dist/task/repo-health-check.js +26 -3
- package/dist/task/task-gates.d.ts +24 -0
- package/dist/task/task-gates.js +78 -8
- package/dist/task/test-assembly.d.ts +87 -0
- package/dist/task/test-assembly.js +163 -0
- package/dist/task/verify-work.d.ts +17 -1
- package/dist/task/verify-work.js +87 -2
- package/dist/workers/pi-worker-docs.js +13 -1
- package/dist/workers/pi-worker-fetch.js +10 -1
- package/dist/workers/pi-worker-search.js +9 -1
- package/dist/workers/research-cache.d.ts +39 -0
- package/dist/workers/research-cache.js +140 -0
- package/dist/workers/shared.d.ts +17 -0
- package/dist/workers/shared.js +0 -0
- package/package.json +2 -2
package/dist/config/config.d.ts
CHANGED
|
@@ -12,6 +12,17 @@ export interface PiTaskConfig {
|
|
|
12
12
|
* phases.ts). Turn on only for a parallel-capable backend.
|
|
13
13
|
*/
|
|
14
14
|
parallelResearchWorkers: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Cache docs/search/fetch worker RESULTS for the duration of one /task-auto run
|
|
17
|
+
* so sibling tasks re-asking the same (package/url, query) reuse the first
|
|
18
|
+
* pipeline's digest instead of re-fetching (mx5 run-8 F10: the research phase
|
|
19
|
+
* burned 75 of 363 min largely re-fetching the same external docs across ~20
|
|
20
|
+
* siblings). Per-run isolated, external-only (project-source `.` lookups excluded),
|
|
21
|
+
* success-only. DEFAULT ON — the F10 live A/B showed no answer-quality regression
|
|
22
|
+
* (a cache hit serves byte-identical text to the first fetch; distinct queries never
|
|
23
|
+
* collide).
|
|
24
|
+
*/
|
|
25
|
+
researchCache: boolean;
|
|
15
26
|
}
|
|
16
27
|
export declare function getConfig(): PiTaskConfig;
|
|
17
28
|
export declare function saveConfig(config: PiTaskConfig): Promise<void>;
|
package/dist/config/config.js
CHANGED
|
@@ -9,7 +9,10 @@ const DEFAULTS = {
|
|
|
9
9
|
orientation: true,
|
|
10
10
|
enforceGuidelines: true,
|
|
11
11
|
verifyWork: true,
|
|
12
|
-
parallelResearchWorkers: false
|
|
12
|
+
parallelResearchWorkers: false,
|
|
13
|
+
// ON: the F10 live A/B showed no answer-quality regression (fidelity 3/3, quality
|
|
14
|
+
// 3/3, 0 collisions; ~14.5s of repeated docs lookups collapse to 0ms on a hit).
|
|
15
|
+
researchCache: true
|
|
13
16
|
};
|
|
14
17
|
const CONFIG_PATH = path.join(os.homedir(), '.config', 'pi-task', 'config.json');
|
|
15
18
|
const _g = globalThis;
|
package/dist/config/register.js
CHANGED
|
@@ -77,6 +77,11 @@ const ITEMS = [
|
|
|
77
77
|
id: 'parallelResearchWorkers',
|
|
78
78
|
label: 'parallel research',
|
|
79
79
|
description: 'Run the 4 research workers concurrently. Leave OFF on a single-GPU local server (serial is measurably faster there); turn on only for a parallel-capable model backend'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'researchCache',
|
|
83
|
+
label: 'research cache',
|
|
84
|
+
description: 'Cache docs/search/fetch results within one /task-auto run so sibling tasks reuse the first pipeline’s digest instead of re-fetching the same external docs. Per-run isolated, external-only, success-only'
|
|
80
85
|
}
|
|
81
86
|
];
|
|
82
87
|
function makeTheme(theme) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** One accepted-despite-FAIL record: the task and why its VERIFY failed. */
|
|
2
|
+
export interface AcceptDebt {
|
|
3
|
+
taskId: string;
|
|
4
|
+
reason: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function acceptDebtFile(cwd: string): string;
|
|
7
|
+
/** The raw stored ledger ('' when none recorded yet). Parse with parseAcceptDebts. */
|
|
8
|
+
export declare function readAcceptDebtsRaw(cwd: string): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Parse the stored ledger into records. A line without the separator (a reason but
|
|
11
|
+
* no id, e.g. hand-edited) parses with an empty taskId rather than being dropped —
|
|
12
|
+
* a recorded debt is never silently lost.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseAcceptDebts(raw: string): AcceptDebt[];
|
|
15
|
+
/** Read + parse in one step. */
|
|
16
|
+
export declare function readAcceptDebts(cwd: string): Promise<AcceptDebt[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Append one accepted-despite-FAIL record, deduplicated against what is already
|
|
19
|
+
* stored (case-insensitive on task id + reason), keeping the newest MAX_DEBTS.
|
|
20
|
+
* Failures are swallowed — the ledger is an auditing aid, never a blocker of the
|
|
21
|
+
* gate sequence that calls it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function recordAcceptDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
24
|
+
/** Overwrite the ledger with exactly these records (used to prune resolved debts). */
|
|
25
|
+
export declare function writeAcceptDebts(cwd: string, debts: AcceptDebt[]): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* STATIC-CLASS debt: one whose accepted FAIL was the deterministic whole-repo static
|
|
28
|
+
* health check (`repo health: …`, the prefix runWorkVerification's repoHealth branch
|
|
29
|
+
* emits). This is the ONE class a deterministic re-check can prove resolved
|
|
30
|
+
* stack-agnostically — the final gate runs the same static check, so a later task
|
|
31
|
+
* that fixed the statics resolves the debt. Every other reason is model-judged or
|
|
32
|
+
* behavioral and cannot be proven resolved without re-running the model.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isStaticClassDebt(reason: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Re-check the ledger against the current run state. A static-class debt is RESOLVED
|
|
37
|
+
* iff the final gate's own static check now passes (`staticOk`); every other debt
|
|
38
|
+
* stays OPEN (unprovable ⇒ surface, never re-hide). FP-safe: the only auto-close is
|
|
39
|
+
* the one a deterministic check can stand behind.
|
|
40
|
+
*/
|
|
41
|
+
export declare function recheckAcceptDebts(debts: AcceptDebt[], opts: {
|
|
42
|
+
staticOk: boolean;
|
|
43
|
+
}): {
|
|
44
|
+
open: AcceptDebt[];
|
|
45
|
+
resolved: AcceptDebt[];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* A one-line-per-debt suffix appended to the final gate's report reason so the still
|
|
49
|
+
* -open accepted defects surface in the gate outcome the user sees (and in the fail
|
|
50
|
+
* picker). Empty when nothing is open.
|
|
51
|
+
*/
|
|
52
|
+
export declare function buildAcceptDebtNote(open: AcceptDebt[]): string;
|
|
Binary file
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { type FinalGateFixFn } from './gate-deps.js';
|
|
3
3
|
import { type GateDeps } from './task-gates.js';
|
|
4
|
+
import type { AcceptDebt } from './accept-debt.js';
|
|
4
5
|
/**
|
|
5
6
|
* Injectable seams so the planner and loop are testable without spawning pi.
|
|
6
7
|
* `runChild` is the planning-only seam used by planAuto; everything else (runTask,
|
|
@@ -32,6 +33,7 @@ export interface AutoDeps extends GateDeps {
|
|
|
32
33
|
finalGate?: (cwd: string) => Promise<{
|
|
33
34
|
ok: boolean;
|
|
34
35
|
reason: string;
|
|
36
|
+
openDebts?: AcceptDebt[];
|
|
35
37
|
}>;
|
|
36
38
|
/**
|
|
37
39
|
* Bounded model-driven fix pass for a final-gate FAIL (see final-gate-fix.ts),
|
|
@@ -29,6 +29,7 @@ import { gitUnmergedPaths, gitStashRef } from './auto-commit.js';
|
|
|
29
29
|
import { runFinalIntegrationGate } from './final-gate.js';
|
|
30
30
|
import { classifyFinalGateAnswer, MAX_FINAL_GATE_AUTOFIX, FINAL_LEAVE_LABEL, FINAL_LEAVE_VALUE, FINAL_ACCEPT_LABEL, FINAL_ACCEPT_VALUE, FINAL_AUTOFIX_LABEL, FINAL_AUTOFIX_VALUE } from './final-gate-fix.js';
|
|
31
31
|
import { getConfig } from '../config/config.js';
|
|
32
|
+
import { configureResearchRun } from '../workers/research-cache.js';
|
|
32
33
|
import { CONTRACT_EXTRACT_PROMPT, parseContractLines, keepGroundedContracts, appendContracts } from './contracts.js';
|
|
33
34
|
// Hard ceiling on clarify questions per feature. The loop is open-ended (it stops
|
|
34
35
|
// when the model emits NONE), but a model that never says NONE would otherwise
|
|
@@ -681,6 +682,18 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
681
682
|
let fin = await deps.finalGate(cwd);
|
|
682
683
|
if (!fin.ok)
|
|
683
684
|
await recGate(`final-gate: FAIL — ${fin.reason.slice(0, 300)}`);
|
|
685
|
+
// ACCEPT-debt re-check surfacing (mx5 run 4 B3 / run 8 TASK_0012):
|
|
686
|
+
// tasks the user accepted despite a verify-FAIL that the gate could
|
|
687
|
+
// not prove resolved against the current tree. Surface them at the
|
|
688
|
+
// gate moment — on PASS or FAIL — so a run never completes silently
|
|
689
|
+
// carrying an accepted defect. Informational: the per-task ACCEPT was
|
|
690
|
+
// already a human decision, so this reports, it does not re-fail.
|
|
691
|
+
if (fin.openDebts && fin.openDebts.length > 0) {
|
|
692
|
+
for (const d of fin.openDebts) {
|
|
693
|
+
await recGate(`accept-debt STILL OPEN — ${d.taskId || '(unknown task)'} was ACCEPTED despite verify-FAIL: ${d.reason.slice(0, 240)}`);
|
|
694
|
+
}
|
|
695
|
+
active.ui.notify(`${id}: ${fin.openDebts.length} task(s) accepted despite verify-FAIL are STILL unresolved at run end — see the gate trail.`, 'warning');
|
|
696
|
+
}
|
|
684
697
|
// Resolution loop: Leave-failed (recommended) / Autofix (bounded,
|
|
685
698
|
// model-driven fix pass + gate re-run — run 7's gap: the picker
|
|
686
699
|
// had NO automated fix path) / Accept. The user always decides;
|
|
@@ -916,6 +929,10 @@ async function handleTaskAuto(args, ctx) {
|
|
|
916
929
|
return;
|
|
917
930
|
}
|
|
918
931
|
autoRunning = true;
|
|
932
|
+
// Stamp a fresh per-run research-cache id (F10) BEFORE planning so enrichment and
|
|
933
|
+
// every task's research phase share one run's cache; disabled ⇒ clears any token a
|
|
934
|
+
// prior run left, so nothing is cached.
|
|
935
|
+
configureResearchRun(getConfig().researchCache);
|
|
919
936
|
const abort = new AbortController();
|
|
920
937
|
const deps = defaultDeps(ctx, cwd, abort.signal, deriveTitle(raw));
|
|
921
938
|
let id;
|
|
@@ -958,6 +975,9 @@ async function handleTaskAutoResume(_args, ctx) {
|
|
|
958
975
|
ctx.ui.notify(`Resuming ${id}…`, 'info');
|
|
959
976
|
await updateTaskFrontMatter(cwd, id, { state: 'in_progress' });
|
|
960
977
|
autoRunning = true;
|
|
978
|
+
// Fresh per-run research-cache id for the resumed run (F10); a resume re-fetches
|
|
979
|
+
// rather than reusing the interrupted run's digest — safe, only slightly less reuse.
|
|
980
|
+
configureResearchRun(getConfig().researchCache);
|
|
961
981
|
const abort = new AbortController();
|
|
962
982
|
// Resume only runs the loop (runTask); no planning children, so the loader
|
|
963
983
|
// title is unused here — pass the id for clarity if that ever changes.
|
|
@@ -61,14 +61,14 @@ export declare function discoverGuidelines(cwd: string, readFile?: (p: string) =
|
|
|
61
61
|
* done, and the contract for the final verdict line. Kept pure so the wording
|
|
62
62
|
* is unit-tested without spawning pi.
|
|
63
63
|
*/
|
|
64
|
-
export declare function buildEnforcePrompt(rulesText: string, diff: string): string;
|
|
64
|
+
export declare function buildEnforcePrompt(rulesText: string, diff: string, probeGamingFindings?: string[]): string;
|
|
65
65
|
/**
|
|
66
66
|
* Build the FLAG-ONLY enforcement prompt: same rules + diff, but the child has a
|
|
67
67
|
* `read` tool only and is told to REPORT violations, not fix them. Kept pure so
|
|
68
68
|
* the wording is unit-tested without spawning pi. Used when there is no
|
|
69
69
|
* verification signal to guard a destructive edit (see ENFORCE_FLAG_TOOLS).
|
|
70
70
|
*/
|
|
71
|
-
export declare function buildEnforceFlagPrompt(rulesText: string, diff: string): string;
|
|
71
|
+
export declare function buildEnforceFlagPrompt(rulesText: string, diff: string, probeGamingFindings?: string[]): string;
|
|
72
72
|
/**
|
|
73
73
|
* Parse the child's final verdict. Scans for the LAST `ENFORCE: CLEAN` /
|
|
74
74
|
* `ENFORCE: VIOLATION` marker (the model may discuss before concluding).
|
|
@@ -26,6 +26,7 @@ import * as path from 'node:path';
|
|
|
26
26
|
import { runChildDefault } from '../shared/child-process.js';
|
|
27
27
|
import { USER_CANCELLED } from './child-runner.js';
|
|
28
28
|
import { TASKS_DIR_NAME } from './task-types.js';
|
|
29
|
+
import { findProbeGamingInDiff } from './probe-gaming.js';
|
|
29
30
|
/** Filenames discovered in the working directory (cwd only — no tree walk). */
|
|
30
31
|
export const GUIDELINE_FILENAMES = ['AGENTS.md', 'CLAUDE.md'];
|
|
31
32
|
/**
|
|
@@ -87,12 +88,33 @@ export async function discoverGuidelines(cwd, readFile = p => fsp.readFile(p, 'u
|
|
|
87
88
|
return null;
|
|
88
89
|
return { files, text: sections.join('\n\n') };
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Render the deterministic probe-gaming findings (run-8 F6) as a prompt block, or
|
|
93
|
+
* an empty array when there are none. Shared by the edit and flag prompts: the
|
|
94
|
+
* finding is a concrete diff line whose stated purpose is to make a check pass
|
|
95
|
+
* rather than meet the requirement — the reliable lever the prompt rule leans on.
|
|
96
|
+
* The `action` line differs (fix vs report) between the two capability modes.
|
|
97
|
+
*/
|
|
98
|
+
function probeGamingEnforceBlock(findings, action) {
|
|
99
|
+
if (findings.length === 0)
|
|
100
|
+
return [];
|
|
101
|
+
return [
|
|
102
|
+
'CHECK-GAMING NOTICE (deterministic, computed from the diff): these added lines',
|
|
103
|
+
'state their own purpose is to make a CHECK pass (a test / verification / lint /',
|
|
104
|
+
'gate), not to meet the requirement the check stands for:',
|
|
105
|
+
...findings.map(f => `- ${f}`),
|
|
106
|
+
'A check is a MESSENGER for a requirement; code written only to quiet the messenger',
|
|
107
|
+
'is a defect even when the check is green (run-8 F6: a handler returned 401 "so the',
|
|
108
|
+
`verification test passes" while the real route stayed dead). ${action}`,
|
|
109
|
+
''
|
|
110
|
+
];
|
|
111
|
+
}
|
|
90
112
|
/**
|
|
91
113
|
* Build the enforcement child's prompt: the rules, the diff of the work just
|
|
92
114
|
* done, and the contract for the final verdict line. Kept pure so the wording
|
|
93
115
|
* is unit-tested without spawning pi.
|
|
94
116
|
*/
|
|
95
|
-
export function buildEnforcePrompt(rulesText, diff) {
|
|
117
|
+
export function buildEnforcePrompt(rulesText, diff, probeGamingFindings = []) {
|
|
96
118
|
return [
|
|
97
119
|
'You are a strict guideline-enforcement pass running right after an AI coding',
|
|
98
120
|
'agent finished a task and committed it. The agent is known to skip project',
|
|
@@ -111,6 +133,9 @@ export function buildEnforcePrompt(rulesText, diff) {
|
|
|
111
133
|
'CHANGES IN THE LAST COMMIT (verify these specifically against the rules):',
|
|
112
134
|
diff.trim().length > 0 ? diff : '(no textual diff captured — nothing to verify)',
|
|
113
135
|
'',
|
|
136
|
+
...probeGamingEnforceBlock(probeGamingFindings, 'Treat this as a violation: replace the check-gaming code with a real'
|
|
137
|
+
+ ' implementation of the requirement, or if you cannot, report it as a'
|
|
138
|
+
+ ' VIOLATION naming the gamed check.'),
|
|
114
139
|
'Your job:',
|
|
115
140
|
'1. Read each changed file and check it against EVERY rule above.',
|
|
116
141
|
'2. For each violation you find, FIX it directly with your `edit` tool, then',
|
|
@@ -130,7 +155,7 @@ export function buildEnforcePrompt(rulesText, diff) {
|
|
|
130
155
|
* the wording is unit-tested without spawning pi. Used when there is no
|
|
131
156
|
* verification signal to guard a destructive edit (see ENFORCE_FLAG_TOOLS).
|
|
132
157
|
*/
|
|
133
|
-
export function buildEnforceFlagPrompt(rulesText, diff) {
|
|
158
|
+
export function buildEnforceFlagPrompt(rulesText, diff, probeGamingFindings = []) {
|
|
134
159
|
return [
|
|
135
160
|
'You are a strict guideline-enforcement REVIEW pass running right after an AI',
|
|
136
161
|
'coding agent finished a task and committed it. The agent is known to skip',
|
|
@@ -146,6 +171,8 @@ export function buildEnforceFlagPrompt(rulesText, diff) {
|
|
|
146
171
|
'CHANGES IN THE LAST COMMIT (review these specifically against the rules):',
|
|
147
172
|
diff.trim().length > 0 ? diff : '(no textual diff captured — nothing to verify)',
|
|
148
173
|
'',
|
|
174
|
+
...probeGamingEnforceBlock(probeGamingFindings, 'Treat this as a violation and REPORT it (do not fix it): name the gamed check'
|
|
175
|
+
+ ' and the requirement left unmet.'),
|
|
149
176
|
'Read each changed file and check it against EVERY rule above. Do NOT attempt',
|
|
150
177
|
'to fix anything — only report what you find.',
|
|
151
178
|
'',
|
|
@@ -279,9 +306,15 @@ export async function runGuidelineEnforcement(deps) {
|
|
|
279
306
|
if (!doc)
|
|
280
307
|
return { ok: true, reason: 'no guideline files' };
|
|
281
308
|
const diff = await getDiff(deps.cwd, deps.signal);
|
|
309
|
+
// Deterministic probe-gaming findings (F6) straight from the captured diff — no
|
|
310
|
+
// extra git call, the diff is already in hand. Injected under the CHECK-GAMING
|
|
311
|
+
// rule so the child acts on a concrete line, not on self-discovered intent.
|
|
312
|
+
const probeGaming = findProbeGamingInDiff(diff);
|
|
282
313
|
const flagOnly = deps.mode === 'flag';
|
|
283
314
|
const tools = flagOnly ? ENFORCE_FLAG_TOOLS : ENFORCE_TOOLS;
|
|
284
|
-
const prompt = flagOnly ?
|
|
315
|
+
const prompt = flagOnly ?
|
|
316
|
+
buildEnforceFlagPrompt(doc.text, diff, probeGaming)
|
|
317
|
+
: buildEnforcePrompt(doc.text, diff, probeGaming);
|
|
285
318
|
let text;
|
|
286
319
|
try {
|
|
287
320
|
text = await deps.runChild(tools, prompt, deps.signal);
|
package/dist/task/env-notes.d.ts
CHANGED
|
@@ -1,23 +1,39 @@
|
|
|
1
1
|
export declare function envNotesFile(cwd: string): string;
|
|
2
|
-
/** The
|
|
2
|
+
/** The raw stored file ('' when none were recorded yet). Parse with parseEnvNotes. */
|
|
3
3
|
export declare function readEnvNotes(cwd: string): Promise<string>;
|
|
4
|
+
/** One recorded fact plus the origin task that established it (may be ''). */
|
|
5
|
+
export interface EnvNote {
|
|
6
|
+
fact: string;
|
|
7
|
+
origin: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parse the stored file into fact+origin records. Legacy lines written before
|
|
11
|
+
* provenance (no separator) parse with an empty origin, so old caches still read.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseEnvNotes(raw: string): EnvNote[];
|
|
4
14
|
/**
|
|
5
15
|
* Pull `ENV-NOTE: <fact>` lines out of a child's answer text. Deduplicated,
|
|
6
16
|
* length-capped; verdict markers can never match (different prefix).
|
|
7
17
|
*/
|
|
8
18
|
export declare function extractEnvNotes(text: string): string[];
|
|
19
|
+
/** True when a fact reads like a standing excuse (see EXCUSE_PATTERNS). */
|
|
20
|
+
export declare function isExcuseNote(fact: string): boolean;
|
|
9
21
|
/**
|
|
10
22
|
* Append newly discovered facts to the cache, deduplicated against what is
|
|
11
|
-
* already there (case-insensitive
|
|
12
|
-
*
|
|
23
|
+
* already there (case-insensitive fact match), keeping the newest MAX_NOTES.
|
|
24
|
+
* Each new fact is stamped with the `origin` task that recorded it; a fact
|
|
25
|
+
* already present keeps its ORIGINAL origin (provenance traces to who first
|
|
26
|
+
* established it). Failures are swallowed — the cache is a sharpener, never a
|
|
13
27
|
* blocker.
|
|
14
28
|
*/
|
|
15
|
-
export declare function appendEnvNotes(cwd: string, notes: string[]): Promise<void>;
|
|
29
|
+
export declare function appendEnvNotes(cwd: string, notes: string[], origin?: string): Promise<void>;
|
|
16
30
|
/**
|
|
17
|
-
* The prompt block a gate child receives when notes exist.
|
|
18
|
-
* load-bearing: facts save re-discovery time but grant no
|
|
19
|
-
*
|
|
31
|
+
* The prompt block a gate child receives when notes exist. Two things are
|
|
32
|
+
* load-bearing: the no-waiver caveat (facts save re-discovery time but grant no
|
|
33
|
+
* license to prepare/repair) and the trust discipline (a note is second-hand
|
|
34
|
+
* until re-validated; an EXCUSE-CLASS note may not wave off a failure without a
|
|
35
|
+
* live re-check; a grep of a generated artifact is not evidence of absence).
|
|
20
36
|
*/
|
|
21
|
-
export declare function buildEnvNotesBlock(
|
|
37
|
+
export declare function buildEnvNotesBlock(raw: string): string;
|
|
22
38
|
/** The emit instruction appended to bash-capable gate-child prompts. */
|
|
23
39
|
export declare const ENV_NOTE_EMIT_INSTRUCTION: string;
|
package/dist/task/env-notes.js
CHANGED
|
@@ -18,6 +18,19 @@
|
|
|
18
18
|
* project setup: the verify-as-shipped rule ("any prep you needed IS the
|
|
19
19
|
* defect") still governs every verdict — the block injected into prompts says
|
|
20
20
|
* so explicitly. The cache only kills re-discovery time.
|
|
21
|
+
*
|
|
22
|
+
* PROVENANCE + RE-VALIDATION (run 8, F7): a verify child once grepped component
|
|
23
|
+
* names in a MINIFIED bundle (identifiers mangled ⇒ 0 hits by construction),
|
|
24
|
+
* wrote "build tree-shakes ALL route components — pre-existing issue" to the
|
|
25
|
+
* cache, and ten later tasks inherited it verbatim as a standing "pre-existing,
|
|
26
|
+
* unrelated" excuse to wave off a genuinely broken deliverable — nobody
|
|
27
|
+
* re-checked. Two guards close that class: (a) each note is stamped host-side
|
|
28
|
+
* with the ORIGIN task that recorded it (a note is second-hand hearsay, not the
|
|
29
|
+
* reader's own observation); (b) the injected block demands the reader
|
|
30
|
+
* RE-VALIDATE a note in the CURRENT tree before citing it to excuse a failure,
|
|
31
|
+
* flags EXCUSE-CLASS notes ("pre-existing", "unrelated", "tree-shaken") for
|
|
32
|
+
* exactly that scrutiny, and forbids treating a grep of a generated artifact as
|
|
33
|
+
* evidence of absence. Provenance is mechanical; re-validation is prompt-level.
|
|
21
34
|
*/
|
|
22
35
|
import * as fsp from 'node:fs/promises';
|
|
23
36
|
import * as path from 'node:path';
|
|
@@ -27,10 +40,16 @@ const ENV_NOTES_FILE = 'env-notes.md';
|
|
|
27
40
|
const MAX_NOTES = 40;
|
|
28
41
|
/** A single fact is one line; anything longer is prose, not a fact. */
|
|
29
42
|
const MAX_NOTE_LENGTH = 240;
|
|
43
|
+
/**
|
|
44
|
+
* Field separator between a fact and its origin in the stored file. A tab never
|
|
45
|
+
* occurs in a one-line fact (facts are prose), so it round-trips cleanly and any
|
|
46
|
+
* stray tab in an emitted fact is normalised to a space before storage.
|
|
47
|
+
*/
|
|
48
|
+
const ORIGIN_SEP = '\t';
|
|
30
49
|
export function envNotesFile(cwd) {
|
|
31
50
|
return path.join(tasksDir(cwd), ENV_NOTES_FILE);
|
|
32
51
|
}
|
|
33
|
-
/** The
|
|
52
|
+
/** The raw stored file ('' when none were recorded yet). Parse with parseEnvNotes. */
|
|
34
53
|
export async function readEnvNotes(cwd) {
|
|
35
54
|
try {
|
|
36
55
|
return (await fsp.readFile(envNotesFile(cwd), 'utf8')).trim();
|
|
@@ -39,6 +58,27 @@ export async function readEnvNotes(cwd) {
|
|
|
39
58
|
return '';
|
|
40
59
|
}
|
|
41
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Parse the stored file into fact+origin records. Legacy lines written before
|
|
63
|
+
* provenance (no separator) parse with an empty origin, so old caches still read.
|
|
64
|
+
*/
|
|
65
|
+
export function parseEnvNotes(raw) {
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const line of raw.split('\n')) {
|
|
68
|
+
const t = line.trim();
|
|
69
|
+
if (t.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
const i = t.indexOf(ORIGIN_SEP);
|
|
72
|
+
if (i === -1)
|
|
73
|
+
out.push({ fact: t, origin: '' });
|
|
74
|
+
else
|
|
75
|
+
out.push({ fact: t.slice(0, i).trim(), origin: t.slice(i + 1).trim() });
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function serializeNote(n) {
|
|
80
|
+
return n.origin ? `${n.fact}${ORIGIN_SEP}${n.origin}` : n.fact;
|
|
81
|
+
}
|
|
42
82
|
/**
|
|
43
83
|
* Pull `ENV-NOTE: <fact>` lines out of a child's answer text. Deduplicated,
|
|
44
84
|
* length-capped; verdict markers can never match (different prefix).
|
|
@@ -58,54 +98,110 @@ export function extractEnvNotes(text) {
|
|
|
58
98
|
}
|
|
59
99
|
return notes;
|
|
60
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* EXCUSE-CLASS wording: a note that waves a problem off as someone else's or a
|
|
103
|
+
* prior condition ("pre-existing … mismatch", "unrelated to this task",
|
|
104
|
+
* "tree-shaken", "not applicable"). These are the notes that propagate across
|
|
105
|
+
* slices as standing excuses (mx5 run-8 F7) — every one of the run-8 cache's
|
|
106
|
+
* dozen such notes was either the false tree-shake fact or the schema mismatch
|
|
107
|
+
* the final gate later proved was a REAL defect. Pure text, stack-agnostic; the
|
|
108
|
+
* flag never drops or fails a note, it only marks it as needing live
|
|
109
|
+
* re-validation before it may EXCUSE a failure. FP is harmless by construction:
|
|
110
|
+
* a benign fact re-validates and is used, the marker only bites a citation-to-
|
|
111
|
+
* wave-off. Benign status facts ("5 pre-existing warnings") do not match — a
|
|
112
|
+
* "pre-existing" match requires a co-located problem word.
|
|
113
|
+
*/
|
|
114
|
+
const EXCUSE_PATTERNS = [
|
|
115
|
+
/\bunrelated\b/i,
|
|
116
|
+
/\bnot (?:my|our|this) (?:task|concern|deliverable|slice|problem)\b/i,
|
|
117
|
+
/\boutside (?:the|this) deliverable\b/i,
|
|
118
|
+
/\bnot applicable\b/i,
|
|
119
|
+
/\bnot specific to\b/i,
|
|
120
|
+
/\btree[-\s]?shak/i,
|
|
121
|
+
/\bpre-?existing\b[^.\n]*\b(?:fail|issue|mismatch|bug|error|broken|problem|affect)/i,
|
|
122
|
+
/\baffect(?:ing|s)? all\b/i
|
|
123
|
+
];
|
|
124
|
+
/** True when a fact reads like a standing excuse (see EXCUSE_PATTERNS). */
|
|
125
|
+
export function isExcuseNote(fact) {
|
|
126
|
+
return EXCUSE_PATTERNS.some(re => re.test(fact));
|
|
127
|
+
}
|
|
61
128
|
/**
|
|
62
129
|
* Append newly discovered facts to the cache, deduplicated against what is
|
|
63
|
-
* already there (case-insensitive
|
|
64
|
-
*
|
|
130
|
+
* already there (case-insensitive fact match), keeping the newest MAX_NOTES.
|
|
131
|
+
* Each new fact is stamped with the `origin` task that recorded it; a fact
|
|
132
|
+
* already present keeps its ORIGINAL origin (provenance traces to who first
|
|
133
|
+
* established it). Failures are swallowed — the cache is a sharpener, never a
|
|
65
134
|
* blocker.
|
|
66
135
|
*/
|
|
67
|
-
export async function appendEnvNotes(cwd, notes) {
|
|
136
|
+
export async function appendEnvNotes(cwd, notes, origin = '') {
|
|
68
137
|
if (notes.length === 0)
|
|
69
138
|
return;
|
|
70
139
|
try {
|
|
71
|
-
const existing = (await readEnvNotes(cwd))
|
|
72
|
-
const seen = new Set(existing.map(
|
|
140
|
+
const existing = parseEnvNotes(await readEnvNotes(cwd));
|
|
141
|
+
const seen = new Set(existing.map(n => n.fact.toLowerCase()));
|
|
73
142
|
const merged = [...existing];
|
|
74
143
|
for (const note of notes) {
|
|
75
|
-
const
|
|
76
|
-
|
|
144
|
+
const fact = note.trim().replace(/\t/g, ' ');
|
|
145
|
+
const key = fact.toLowerCase();
|
|
146
|
+
if (key.length === 0 || seen.has(key))
|
|
77
147
|
continue;
|
|
78
148
|
seen.add(key);
|
|
79
|
-
merged.push(
|
|
149
|
+
merged.push({ fact, origin: origin.trim() });
|
|
80
150
|
}
|
|
81
151
|
const kept = merged.slice(-MAX_NOTES);
|
|
82
152
|
await fsp.mkdir(tasksDir(cwd), { recursive: true });
|
|
83
|
-
await fsp.writeFile(envNotesFile(cwd), kept.join('\n') + '\n', 'utf8');
|
|
153
|
+
await fsp.writeFile(envNotesFile(cwd), kept.map(serializeNote).join('\n') + '\n', 'utf8');
|
|
84
154
|
}
|
|
85
155
|
catch {
|
|
86
156
|
// best-effort cache
|
|
87
157
|
}
|
|
88
158
|
}
|
|
89
159
|
/**
|
|
90
|
-
* The prompt block a gate child receives when notes exist.
|
|
91
|
-
* load-bearing: facts save re-discovery time but grant no
|
|
92
|
-
*
|
|
160
|
+
* The prompt block a gate child receives when notes exist. Two things are
|
|
161
|
+
* load-bearing: the no-waiver caveat (facts save re-discovery time but grant no
|
|
162
|
+
* license to prepare/repair) and the trust discipline (a note is second-hand
|
|
163
|
+
* until re-validated; an EXCUSE-CLASS note may not wave off a failure without a
|
|
164
|
+
* live re-check; a grep of a generated artifact is not evidence of absence).
|
|
93
165
|
*/
|
|
94
|
-
export function buildEnvNotesBlock(
|
|
95
|
-
|
|
166
|
+
export function buildEnvNotesBlock(raw) {
|
|
167
|
+
const notes = parseEnvNotes(raw);
|
|
168
|
+
if (notes.length === 0)
|
|
96
169
|
return '';
|
|
170
|
+
const lines = notes.map(n => {
|
|
171
|
+
const origin = n.origin ? ` — recorded by ${n.origin}` : ' — origin unrecorded';
|
|
172
|
+
const flag = isExcuseNote(n.fact) ?
|
|
173
|
+
' [EXCUSE-CLASS — re-validate in the CURRENT tree before citing this to wave off any failure]'
|
|
174
|
+
: '';
|
|
175
|
+
return `- ${n.fact}${origin}${flag}`;
|
|
176
|
+
});
|
|
97
177
|
return [
|
|
98
|
-
'KNOWN ENVIRONMENT FACTS —
|
|
99
|
-
'(
|
|
100
|
-
...
|
|
101
|
-
|
|
102
|
-
.split('\n')
|
|
103
|
-
.map(n => `- ${n}`),
|
|
178
|
+
'KNOWN ENVIRONMENT FACTS — recorded by earlier verification passes in this run',
|
|
179
|
+
'(second-hand, may be stale or WRONG):',
|
|
180
|
+
...lines,
|
|
181
|
+
'',
|
|
104
182
|
'These facts only save you re-discovery time (where credentials/config live, which',
|
|
105
183
|
'tools are installed, which services are reachable). They are NOT a license to',
|
|
106
184
|
'prepare or repair the run: the verify-as-shipped rules below still govern the',
|
|
107
185
|
'verdict — if the project needs something its own committed files do not provide,',
|
|
108
186
|
'that remains the defect no matter what is listed here.',
|
|
187
|
+
'',
|
|
188
|
+
'TRUST DISCIPLINE — a false "pre-existing, unrelated" note once masked a real shipped',
|
|
189
|
+
'defect across many tasks in this exact pipeline; do not repeat it:',
|
|
190
|
+
'- A note above is second-hand hearsay from another task, not your own observation.',
|
|
191
|
+
' You may CITE one to EXCUSE, wave off, or down-grade a failure ONLY IF you',
|
|
192
|
+
' RE-VALIDATE its claim in the CURRENT tree right now and state the command or',
|
|
193
|
+
' observation you used to reconfirm it.',
|
|
194
|
+
'- If a note fails re-validation (its claim is not true in the current tree), do NOT',
|
|
195
|
+
" inherit it: treat the underlying problem as UNexcused and report it. Don't silently",
|
|
196
|
+
' carry a stale fact forward.',
|
|
197
|
+
'- EVIDENCE HYGIENE: string-searching a MINIFIED, bundled, or otherwise generated or',
|
|
198
|
+
' compiled artifact is NOT evidence that something is absent — identifiers there are',
|
|
199
|
+
' renamed or stripped by construction, so a zero-hit grep proves nothing. Derive',
|
|
200
|
+
' presence/absence only from SOURCE files or by EXECUTING the artifact and observing.',
|
|
201
|
+
'- A claim that a defect is "pre-existing", "unrelated", or "not this task" is an',
|
|
202
|
+
' EXCUSE, not a fact (the ones above are marked): re-establish it live, and if it',
|
|
203
|
+
' actually holds as a real defect, escalate it (report FAIL) rather than passing it',
|
|
204
|
+
' on as a standing waiver.',
|
|
109
205
|
''
|
|
110
206
|
].join('\n');
|
|
111
207
|
}
|
|
@@ -115,7 +211,11 @@ export const ENV_NOTE_EMIT_INSTRUCTION = [
|
|
|
115
211
|
'THIS MACHINE or the project environment (a service reachable/absent at an address, where',
|
|
116
212
|
'credentials/config live, a tool or runtime present/missing and its version), emit a line',
|
|
117
213
|
' ENV-NOTE: <one-line fact>',
|
|
118
|
-
'anywhere in your answer, one per fact.
|
|
119
|
-
'
|
|
120
|
-
'
|
|
214
|
+
'anywhere in your answer, one per fact. A fact is something you OBSERVED to be true of the',
|
|
215
|
+
'machine or environment — never a task verdict, never spec content, never a judgment about',
|
|
216
|
+
'the code. In particular do NOT record an absence you inferred from grepping a built or',
|
|
217
|
+
'minified artifact (identifiers there are mangled — a zero-hit grep proves nothing), and',
|
|
218
|
+
'do NOT record "X is pre-existing / unrelated / not my task": that is a verdict, not an',
|
|
219
|
+
'environment fact. These are cached for later verification passes in this run so they do',
|
|
220
|
+
'not re-discover the same things.'
|
|
121
221
|
].join('\n');
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { type HealthCommand } from './repo-health-check.js';
|
|
2
|
+
import { type AcceptDebt } from './accept-debt.js';
|
|
2
3
|
export interface FinalGateOutcome {
|
|
3
4
|
/** true → statics and every runnable integration command passed (or nothing to run). */
|
|
4
5
|
ok: boolean;
|
|
5
6
|
/** On a fail: the exact command, its exit code, and the tail of its output. */
|
|
6
7
|
reason: string;
|
|
8
|
+
/**
|
|
9
|
+
* ACCEPT-despite-verify-FAIL debts still open at run end (mx5 run 4 B3 / run 8
|
|
10
|
+
* TASK_0012): tasks the user blessed as-is despite a verify-FAIL that a
|
|
11
|
+
* deterministic re-check could not prove resolved. The caller surfaces them so a
|
|
12
|
+
* run never completes silently carrying an accepted defect. Empty/absent = none.
|
|
13
|
+
*/
|
|
14
|
+
openDebts?: AcceptDebt[];
|
|
7
15
|
}
|
|
8
16
|
/**
|
|
9
17
|
* The project's OWN whole-repo integration commands (test, then build — test
|
package/dist/task/final-gate.js
CHANGED
|
@@ -42,6 +42,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
42
42
|
import { existsSync, readFileSync } from 'node:fs';
|
|
43
43
|
import * as path from 'node:path';
|
|
44
44
|
import { runRepoHealthCheck, discoverHealthCommands } from './repo-health-check.js';
|
|
45
|
+
import { readAcceptDebts, recheckAcceptDebts, writeAcceptDebts, buildAcceptDebtNote } from './accept-debt.js';
|
|
45
46
|
function packageScripts(cwd) {
|
|
46
47
|
try {
|
|
47
48
|
const j = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
@@ -280,13 +281,32 @@ function runGateCommand(cwd, [bin, args], timeoutMs) {
|
|
|
280
281
|
*/
|
|
281
282
|
export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGraceMs = 10_000) {
|
|
282
283
|
const stat = runRepoHealthCheck(cwd);
|
|
284
|
+
// ACCEPT-debt re-check (mx5 run 4 B3 / run 8 TASK_0012): read the ledger of tasks
|
|
285
|
+
// the user accepted despite a verify-FAIL and re-check each against the current
|
|
286
|
+
// tree. A static-class debt whose statics now pass is provably RESOLVED (a later
|
|
287
|
+
// task fixed it) and pruned; every other debt cannot be proven resolved
|
|
288
|
+
// deterministically, so it stays OPEN and is surfaced in this gate's report — a
|
|
289
|
+
// run may not complete silently carrying an accepted defect. FP-safe by
|
|
290
|
+
// construction (see accept-debt.ts). Best-effort: a ledger read/write failure
|
|
291
|
+
// must never break the gate.
|
|
292
|
+
const { open: openDebts, resolved } = recheckAcceptDebts(await readAcceptDebts(cwd), {
|
|
293
|
+
staticOk: stat.ok
|
|
294
|
+
});
|
|
295
|
+
if (resolved.length > 0)
|
|
296
|
+
await writeAcceptDebts(cwd, openDebts);
|
|
297
|
+
const debtNote = buildAcceptDebtNote(openDebts);
|
|
298
|
+
const withDebts = (o) => ({
|
|
299
|
+
...o,
|
|
300
|
+
reason: `${o.reason}${debtNote}`,
|
|
301
|
+
openDebts
|
|
302
|
+
});
|
|
283
303
|
if (!stat.ok)
|
|
284
|
-
return { ok: false, reason: `static checks: ${stat.reason}` };
|
|
304
|
+
return withDebts({ ok: false, reason: `static checks: ${stat.reason}` });
|
|
285
305
|
const lockCmds = discoverLockfileChecks(cwd);
|
|
286
306
|
const { cmds } = discoverIntegrationCommands(cwd);
|
|
287
307
|
const boot = discoverBootCommand(cwd);
|
|
288
308
|
if (lockCmds.length === 0 && cmds.length === 0 && !boot) {
|
|
289
|
-
return { ok: true, reason: 'no integration command found (statics passed)' };
|
|
309
|
+
return withDebts({ ok: true, reason: 'no integration command found (statics passed)' });
|
|
290
310
|
}
|
|
291
311
|
const ran = [];
|
|
292
312
|
for (const { prefix, list } of [
|
|
@@ -299,10 +319,10 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
299
319
|
if (r.outcome === 'skip')
|
|
300
320
|
continue;
|
|
301
321
|
if (r.outcome === 'fail') {
|
|
302
|
-
return {
|
|
322
|
+
return withDebts({
|
|
303
323
|
ok: false,
|
|
304
324
|
reason: `${prefix}\`${label}\` exited ${r.status}${r.tail ? ` — ${r.tail}` : ''}`
|
|
305
|
-
};
|
|
325
|
+
});
|
|
306
326
|
}
|
|
307
327
|
ran.push(label);
|
|
308
328
|
}
|
|
@@ -311,15 +331,15 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
311
331
|
const label = `${boot[0]} ${boot[1].join(' ')}`;
|
|
312
332
|
const b = await runBootCheck(cwd, boot, bootGraceMs);
|
|
313
333
|
if (b.outcome === 'fail') {
|
|
314
|
-
return { ok: false, reason: `boot check: \`${label}\` ${b.detail}` };
|
|
334
|
+
return withDebts({ ok: false, reason: `boot check: \`${label}\` ${b.detail}` });
|
|
315
335
|
}
|
|
316
336
|
if (b.outcome === 'pass')
|
|
317
337
|
ran.push(label);
|
|
318
338
|
}
|
|
319
|
-
return {
|
|
339
|
+
return withDebts({
|
|
320
340
|
ok: true,
|
|
321
341
|
reason: ran.length > 0 ?
|
|
322
342
|
`statics + ${ran.map(c => `\`${c}\``).join(', ')} passed`
|
|
323
343
|
: 'statics passed (integration commands not runnable here)'
|
|
324
|
-
};
|
|
344
|
+
});
|
|
325
345
|
}
|