@mjasnikovs/pi-task 0.18.22 → 0.18.23
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/task/accept-debt.d.ts +31 -4
- package/dist/task/accept-debt.js +50 -4
- package/dist/task/final-gate.d.ts +2 -9
- package/dist/task/final-gate.js +10 -22
- package/dist/task/gate-deps.d.ts +8 -0
- package/dist/task/gate-deps.js +35 -3
- package/dist/task/lint-fix.d.ts +11 -0
- package/dist/task/lint-fix.js +62 -0
- package/dist/task/task-gates.d.ts +11 -0
- package/dist/task/task-gates.js +12 -0
- package/dist/task/task-provenance.d.ts +45 -0
- package/dist/task/task-provenance.js +98 -0
- package/dist/task/verify-work.d.ts +17 -1
- package/dist/task/verify-work.js +55 -4
- package/dist/task/write-guard.d.ts +10 -0
- package/dist/task/write-guard.js +40 -0
- package/package.json +1 -1
|
@@ -11,8 +11,14 @@
|
|
|
11
11
|
* created files need a tsconfig registration every spec forbids). Cross-task
|
|
12
12
|
* contradiction: no unattended re-run can converge, so the gate loop records the
|
|
13
13
|
* defect and routes to the human picker instead of burning AUTOFIX rounds.
|
|
14
|
+
* - 'cross-task-deletion' — the task's work DELETED a sibling task's committed
|
|
15
|
+
* deliverable (mx5 run 12 PROMPT 2: a fix child deleted TASK_0020's playwright ct
|
|
16
|
+
* files to green a lint) and the user ACCEPTed the verify-FAIL anyway, so the
|
|
17
|
+
* deletion ships in the next commit. Recorded so the final gate re-checks it:
|
|
18
|
+
* resolved iff the named file is back in the tree (a later task restored it),
|
|
19
|
+
* otherwise surfaced.
|
|
14
20
|
*/
|
|
15
|
-
export type DebtOrigin = 'accepted' | 'enforce-revert' | 'frozen-blocked';
|
|
21
|
+
export type DebtOrigin = 'accepted' | 'enforce-revert' | 'frozen-blocked' | 'cross-task-deletion';
|
|
16
22
|
/** One recorded defect: the task, why its VERIFY failed, and how it was recorded. */
|
|
17
23
|
export interface AcceptDebt {
|
|
18
24
|
taskId: string;
|
|
@@ -60,6 +66,24 @@ export declare function recordEnforceRevertDebt(cwd: string, taskId: string, rea
|
|
|
60
66
|
* it auto-closes iff the final gate's own static check passes.
|
|
61
67
|
*/
|
|
62
68
|
export declare function recordFrozenBlockedDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Record a CROSS-TASK DELETION debt (mx5 run 12 PROMPT 2): the task's work deleted a
|
|
71
|
+
* file a DIFFERENT task's commit introduced, verify FAILed, and the user ACCEPTed —
|
|
72
|
+
* so the deletion survives into the next commit. The reason is a fixed machine-
|
|
73
|
+
* parseable shape (`deleted \`<path>\` …`) so the final gate's re-check can extract
|
|
74
|
+
* the path and prove the debt resolved iff the file is back in the tree.
|
|
75
|
+
*/
|
|
76
|
+
export declare function recordCrossTaskDeletionDebt(cwd: string, taskId: string, deletion: {
|
|
77
|
+
path: string;
|
|
78
|
+
owner: string;
|
|
79
|
+
}): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* The deleted path a cross-task-deletion debt names (the fixed shape
|
|
82
|
+
* recordCrossTaskDeletionDebt writes). Null on any other reason text — an
|
|
83
|
+
* unextractable path means the re-check cannot prove anything, so the debt
|
|
84
|
+
* stays open (surface, never re-hide).
|
|
85
|
+
*/
|
|
86
|
+
export declare function extractDeletedDebtPath(reason: string): string | null;
|
|
63
87
|
/** Overwrite the ledger with exactly these records (used to prune resolved debts). */
|
|
64
88
|
export declare function writeAcceptDebts(cwd: string, debts: AcceptDebt[]): Promise<void>;
|
|
65
89
|
/**
|
|
@@ -73,12 +97,15 @@ export declare function writeAcceptDebts(cwd: string, debts: AcceptDebt[]): Prom
|
|
|
73
97
|
export declare function isStaticClassDebt(reason: string): boolean;
|
|
74
98
|
/**
|
|
75
99
|
* Re-check the ledger against the current run state. A static-class debt is RESOLVED
|
|
76
|
-
* iff the final gate's own static check now passes (`staticOk`);
|
|
77
|
-
*
|
|
78
|
-
*
|
|
100
|
+
* iff the final gate's own static check now passes (`staticOk`); a cross-task-deletion
|
|
101
|
+
* debt is RESOLVED iff the file it names is back in the tree (`fileExists` — a later
|
|
102
|
+
* task or a human restored it, so the deletion no longer holds); every other debt
|
|
103
|
+
* stays OPEN (unprovable ⇒ surface, never re-hide). FP-safe: the only auto-closes are
|
|
104
|
+
* ones a deterministic check can stand behind.
|
|
79
105
|
*/
|
|
80
106
|
export declare function recheckAcceptDebts(debts: AcceptDebt[], opts: {
|
|
81
107
|
staticOk: boolean;
|
|
108
|
+
fileExists?: (rel: string) => boolean;
|
|
82
109
|
}): {
|
|
83
110
|
open: AcceptDebt[];
|
|
84
111
|
resolved: AcceptDebt[];
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -74,7 +74,9 @@ export function parseAcceptDebts(raw) {
|
|
|
74
74
|
out.push({
|
|
75
75
|
taskId: parts[0].trim(),
|
|
76
76
|
reason: parts[1].trim(),
|
|
77
|
-
...(origin === 'enforce-revert'
|
|
77
|
+
...((origin === 'enforce-revert'
|
|
78
|
+
|| origin === 'frozen-blocked'
|
|
79
|
+
|| origin === 'cross-task-deletion') ?
|
|
78
80
|
{ origin: origin }
|
|
79
81
|
: {})
|
|
80
82
|
});
|
|
@@ -157,6 +159,30 @@ export async function recordFrozenBlockedDebt(cwd, taskId, reason) {
|
|
|
157
159
|
origin: 'frozen-blocked'
|
|
158
160
|
});
|
|
159
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Record a CROSS-TASK DELETION debt (mx5 run 12 PROMPT 2): the task's work deleted a
|
|
164
|
+
* file a DIFFERENT task's commit introduced, verify FAILed, and the user ACCEPTed —
|
|
165
|
+
* so the deletion survives into the next commit. The reason is a fixed machine-
|
|
166
|
+
* parseable shape (`deleted \`<path>\` …`) so the final gate's re-check can extract
|
|
167
|
+
* the path and prove the debt resolved iff the file is back in the tree.
|
|
168
|
+
*/
|
|
169
|
+
export async function recordCrossTaskDeletionDebt(cwd, taskId, deletion) {
|
|
170
|
+
await appendDebt(cwd, {
|
|
171
|
+
taskId: taskId.trim(),
|
|
172
|
+
reason: normaliseReason(`deleted \`${deletion.path}\` — ${deletion.owner}'s committed deliverable, removed by this task's work`),
|
|
173
|
+
origin: 'cross-task-deletion'
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The deleted path a cross-task-deletion debt names (the fixed shape
|
|
178
|
+
* recordCrossTaskDeletionDebt writes). Null on any other reason text — an
|
|
179
|
+
* unextractable path means the re-check cannot prove anything, so the debt
|
|
180
|
+
* stays open (surface, never re-hide).
|
|
181
|
+
*/
|
|
182
|
+
export function extractDeletedDebtPath(reason) {
|
|
183
|
+
const m = /^deleted `([^`]+)`/.exec(reason.trim());
|
|
184
|
+
return m ? m[1] : null;
|
|
185
|
+
}
|
|
160
186
|
/** Overwrite the ledger with exactly these records (used to prune resolved debts). */
|
|
161
187
|
export async function writeAcceptDebts(cwd, debts) {
|
|
162
188
|
try {
|
|
@@ -184,14 +210,31 @@ export function isStaticClassDebt(reason) {
|
|
|
184
210
|
}
|
|
185
211
|
/**
|
|
186
212
|
* Re-check the ledger against the current run state. A static-class debt is RESOLVED
|
|
187
|
-
* iff the final gate's own static check now passes (`staticOk`);
|
|
188
|
-
*
|
|
189
|
-
*
|
|
213
|
+
* iff the final gate's own static check now passes (`staticOk`); a cross-task-deletion
|
|
214
|
+
* debt is RESOLVED iff the file it names is back in the tree (`fileExists` — a later
|
|
215
|
+
* task or a human restored it, so the deletion no longer holds); every other debt
|
|
216
|
+
* stays OPEN (unprovable ⇒ surface, never re-hide). FP-safe: the only auto-closes are
|
|
217
|
+
* ones a deterministic check can stand behind.
|
|
190
218
|
*/
|
|
191
219
|
export function recheckAcceptDebts(debts, opts) {
|
|
192
220
|
const open = [];
|
|
193
221
|
const resolved = [];
|
|
194
222
|
for (const d of debts) {
|
|
223
|
+
if (d.origin === 'cross-task-deletion') {
|
|
224
|
+
const p = extractDeletedDebtPath(d.reason);
|
|
225
|
+
let restored;
|
|
226
|
+
try {
|
|
227
|
+
restored = p !== null && opts.fileExists?.(p) === true;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
restored = false; // an existence-check fault is inconclusive, not proof
|
|
231
|
+
}
|
|
232
|
+
if (restored)
|
|
233
|
+
resolved.push(d);
|
|
234
|
+
else
|
|
235
|
+
open.push(d);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
195
238
|
if (opts.staticOk && isStaticClassDebt(d.reason))
|
|
196
239
|
resolved.push(d);
|
|
197
240
|
else
|
|
@@ -278,5 +321,8 @@ export function describeDebt(d) {
|
|
|
278
321
|
if (d.origin === 'frozen-blocked') {
|
|
279
322
|
return 'repo health blocked by a spec-frozen path (cross-task contradiction — no task may perform the fixing edit)';
|
|
280
323
|
}
|
|
324
|
+
if (d.origin === 'cross-task-deletion') {
|
|
325
|
+
return "a sibling task's committed deliverable was DELETED by this task's work and the deletion was accepted (still missing from the tree)";
|
|
326
|
+
}
|
|
281
327
|
return 'accepted despite verify-FAIL';
|
|
282
328
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type HealthCommand } from './repo-health-check.js';
|
|
2
2
|
import { type AcceptDebt } from './accept-debt.js';
|
|
3
3
|
import { type RenderOutcome } from './render-check.js';
|
|
4
|
+
import { taskThatIntroduced } from './task-provenance.js';
|
|
4
5
|
export interface FinalGateOutcome {
|
|
5
6
|
/** true → statics and every runnable integration command passed (or nothing to run). */
|
|
6
7
|
ok: boolean;
|
|
@@ -140,14 +141,7 @@ export declare function discoverGateCommandLabels(cwd: string): string[];
|
|
|
140
141
|
* this box; the same wording in a `test` run is a real failure the suite must own).
|
|
141
142
|
*/
|
|
142
143
|
export declare const INFRA_GAP_OUTPUT_RE: RegExp;
|
|
143
|
-
|
|
144
|
-
* The task whose commit INTRODUCED `rel` (oldest `--diff-filter=A` commit whose
|
|
145
|
-
* subject carries the pi-task `(TASK_nnnn)` suffix — both the task snapshot and the
|
|
146
|
-
* ENFORCE commit shapes match). Null when the file predates the run, was never
|
|
147
|
-
* committed, git is unavailable, or the adding commit is not a task commit — every
|
|
148
|
-
* unknown degrades to "no conflict claim".
|
|
149
|
-
*/
|
|
150
|
-
export declare function taskThatIntroduced(cwd: string, rel: string): string | null;
|
|
144
|
+
export { taskThatIntroduced };
|
|
151
145
|
/**
|
|
152
146
|
* Run the final gate: static analysis first, then the lockfile consistency
|
|
153
147
|
* checks, then the discovered integration commands, then one boot exercise of
|
|
@@ -155,4 +149,3 @@ export declare function taskThatIntroduced(cwd: string, rel: string): string | n
|
|
|
155
149
|
* First real failure wins.
|
|
156
150
|
*/
|
|
157
151
|
export declare function runFinalIntegrationGate(cwd: string, timeoutMs?: number, bootGraceMs?: number, bootDeps?: BootDeps, planText?: string): Promise<FinalGateOutcome>;
|
|
158
|
-
export {};
|
package/dist/task/final-gate.js
CHANGED
|
@@ -46,6 +46,7 @@ import { readAcceptDebts, recheckAcceptDebts, writeAcceptDebts, buildAcceptDebtN
|
|
|
46
46
|
import { readDeclaredScripts, missingDeclaredScripts, runnableDeclaredScripts } from './launch-contract.js';
|
|
47
47
|
import { readEnvNotes, parseEnvNotes, isExcuseNote } from './env-notes.js';
|
|
48
48
|
import { runRenderCheck } from './render-check.js';
|
|
49
|
+
import { taskThatIntroduced } from './task-provenance.js';
|
|
49
50
|
function packageScripts(cwd) {
|
|
50
51
|
try {
|
|
51
52
|
const j = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
@@ -586,27 +587,10 @@ async function recoverOrphanPort(cwd, boot, first, bootGraceMs, deps, expectServ
|
|
|
586
587
|
await new Promise(r => setTimeout(r, 1_500));
|
|
587
588
|
return runBootCheck(cwd, boot, bootGraceMs, { expectServer, deps });
|
|
588
589
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
* committed, git is unavailable, or the adding commit is not a task commit — every
|
|
594
|
-
* unknown degrades to "no conflict claim".
|
|
595
|
-
*/
|
|
596
|
-
export function taskThatIntroduced(cwd, rel) {
|
|
597
|
-
const r = spawnSync('git', ['log', '--diff-filter=A', '--format=%s', '--', rel], {
|
|
598
|
-
cwd,
|
|
599
|
-
encoding: 'utf8'
|
|
600
|
-
});
|
|
601
|
-
if (r.error || r.status !== 0 || !r.stdout)
|
|
602
|
-
return null;
|
|
603
|
-
const subjects = r.stdout.trim().split('\n').filter(Boolean);
|
|
604
|
-
// Newest-first output; the LAST line is the original introduction (a
|
|
605
|
-
// delete-and-re-add later in history must not reattribute the file).
|
|
606
|
-
const first = subjects[subjects.length - 1] ?? '';
|
|
607
|
-
const m = /\((TASK_\d+)\)\s*$/.exec(first);
|
|
608
|
-
return m ? m[1] : null;
|
|
609
|
-
}
|
|
590
|
+
// File → introducing-task provenance moved to task-provenance.ts (mx5 run-12
|
|
591
|
+
// PROMPT 2 extracted it for the cross-task deletion guards); re-exported so
|
|
592
|
+
// existing importers keep working.
|
|
593
|
+
export { taskThatIntroduced };
|
|
610
594
|
/**
|
|
611
595
|
* Run the final gate: static analysis first, then the lockfile consistency
|
|
612
596
|
* checks, then the discovered integration commands, then one boot exercise of
|
|
@@ -624,7 +608,11 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
624
608
|
// construction (see accept-debt.ts). Best-effort: a ledger read/write failure
|
|
625
609
|
// must never break the gate.
|
|
626
610
|
const { open: openRaw, resolved } = recheckAcceptDebts(await readAcceptDebts(cwd), {
|
|
627
|
-
staticOk: stat.ok
|
|
611
|
+
staticOk: stat.ok,
|
|
612
|
+
// Cross-task-deletion debts auto-close iff the deleted file is back in the
|
|
613
|
+
// tree — a deterministic existence check, corroborating the per-file
|
|
614
|
+
// provenance the record already carries.
|
|
615
|
+
fileExists: rel => existsSync(path.join(cwd, rel))
|
|
628
616
|
});
|
|
629
617
|
if (resolved.length > 0)
|
|
630
618
|
await writeAcceptDebts(cwd, openRaw);
|
package/dist/task/gate-deps.d.ts
CHANGED
|
@@ -40,6 +40,14 @@ export declare function collectAddedLines(cwd: string, signal?: AbortSignal): Pr
|
|
|
40
40
|
* Failures degrade to an empty summary — the guard then has nothing to reject.
|
|
41
41
|
*/
|
|
42
42
|
export declare function collectTreeChanges(cwd: string, signal?: AbortSignal): Promise<TreeChangeSummary>;
|
|
43
|
+
/**
|
|
44
|
+
* The task's changes for the cross-task deletion probe: the working tree's status
|
|
45
|
+
* when the work is uncommitted (pre-commit verify), else the LAST COMMIT's
|
|
46
|
+
* name-status diff (the post-enforce re-verify runs on a clean tree, where that
|
|
47
|
+
* commit IS the task's work). Failures degrade to an empty summary — the probe is
|
|
48
|
+
* a sharpener, never a blocker. Same fallback discipline as collectChangedFiles.
|
|
49
|
+
*/
|
|
50
|
+
export declare function collectTaskTreeChanges(cwd: string, signal?: AbortSignal): Promise<TreeChangeSummary>;
|
|
43
51
|
/**
|
|
44
52
|
* Build the gate deps for one command run. `runTask` is the orchestrator's
|
|
45
53
|
* implementation re-runner, injected by the caller. The returned object also drives
|
package/dist/task/gate-deps.js
CHANGED
|
@@ -21,7 +21,7 @@ import { runGuidelineEnforcement, classifyEnforceChildFailure } from './enforce-
|
|
|
21
21
|
import { runWorkVerification, extractSpecForVerification } from './verify-work.js';
|
|
22
22
|
import { readEnvNotes, appendEnvNotes } from './env-notes.js';
|
|
23
23
|
import { readContracts } from './contracts.js';
|
|
24
|
-
import { recordAcceptDebt, recordEnforceRevertDebt, recordFrozenBlockedDebt } from './accept-debt.js';
|
|
24
|
+
import { recordAcceptDebt, recordEnforceRevertDebt, recordFrozenBlockedDebt, recordCrossTaskDeletionDebt } from './accept-debt.js';
|
|
25
25
|
import { runRepoHealthCheck } from './repo-health-check.js';
|
|
26
26
|
import { runFinalIntegrationGate, discoverGateCommandLabels } from './final-gate.js';
|
|
27
27
|
import { runFinalGateAutofix } from './final-gate-fix.js';
|
|
@@ -30,7 +30,8 @@ import { extractProhibitions, findProhibitionViolations } from './prohibition-pr
|
|
|
30
30
|
import { frozenPathsFromSpec, revertFrozenPaths } from './frozen-path-guard.js';
|
|
31
31
|
import { findProbeGaming, parseAddedLines } from './probe-gaming.js';
|
|
32
32
|
import { findSubstitutionSuspects, isTestFile } from './substitution-probe.js';
|
|
33
|
-
import { parseTreeChanges, formatTreeChanges } from './write-guard.js';
|
|
33
|
+
import { parseTreeChanges, parseNameStatusChanges, formatTreeChanges } from './write-guard.js';
|
|
34
|
+
import { taskThatIntroduced, findCrossTaskDeletions } from './task-provenance.js';
|
|
34
35
|
import { findTestRebuiltAssemblies, testAssemblyVerifyFindings } from './test-assembly.js';
|
|
35
36
|
import { runBoundedLintFix } from './lint-fix.js';
|
|
36
37
|
import { captureGitState, reconcileGitState } from './git-state-guard.js';
|
|
@@ -131,6 +132,20 @@ export async function collectTreeChanges(cwd, signal) {
|
|
|
131
132
|
const r = await git(cwd, ['status', '--porcelain', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
132
133
|
return r.exitCode === 0 ? parseTreeChanges(r.stdout) : { modified: [], deleted: [], added: [] };
|
|
133
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The task's changes for the cross-task deletion probe: the working tree's status
|
|
137
|
+
* when the work is uncommitted (pre-commit verify), else the LAST COMMIT's
|
|
138
|
+
* name-status diff (the post-enforce re-verify runs on a clean tree, where that
|
|
139
|
+
* commit IS the task's work). Failures degrade to an empty summary — the probe is
|
|
140
|
+
* a sharpener, never a blocker. Same fallback discipline as collectChangedFiles.
|
|
141
|
+
*/
|
|
142
|
+
export async function collectTaskTreeChanges(cwd, signal) {
|
|
143
|
+
const now = await collectTreeChanges(cwd, signal);
|
|
144
|
+
if (now.modified.length + now.deleted.length + now.added.length > 0)
|
|
145
|
+
return now;
|
|
146
|
+
const last = await git(cwd, ['diff', '--name-status', 'HEAD~1..HEAD', '--', '.', EXCLUDE_TASKS_DIR], signal);
|
|
147
|
+
return last.exitCode === 0 ? parseNameStatusChanges(last.stdout) : now;
|
|
148
|
+
}
|
|
134
149
|
/** Source extensions whose relative imports the test-assembly probe reasons over. */
|
|
135
150
|
const SOURCE_EXT_RE = /\.(?:[cm]?[jt]sx?)$/;
|
|
136
151
|
/** Bounds so the probe stays cheap on large repos (it reads file text). */
|
|
@@ -318,6 +333,10 @@ export function buildGateDeps(params) {
|
|
|
318
333
|
// FAIL whose only fix is an edit to a path this task's spec froze — recorded
|
|
319
334
|
// when the gate loop routes it to the picker, re-checked by the final gate.
|
|
320
335
|
recordFrozenBlockedDebt: (cwd2, taskId, reason) => recordFrozenBlockedDebt(cwd2, taskId, reason),
|
|
336
|
+
// Durable cross-task-deletion ledger (PROMPT 2): a sibling's committed
|
|
337
|
+
// deliverable this task's diff deletes, ACCEPTed into a commit anyway —
|
|
338
|
+
// the final gate re-checks it (resolved iff the file is back in the tree).
|
|
339
|
+
recordCrossTaskDeletionDebt: (cwd2, taskId, deletion) => recordCrossTaskDeletionDebt(cwd2, taskId, deletion),
|
|
321
340
|
// Frozen-path write-deny (see frozen-path-guard.ts): the concrete paths this
|
|
322
341
|
// task's spec forbids modifying, so the gate sequence can UNDO any edit the
|
|
323
342
|
// enforce EDIT pass makes to them before those edits are committed. Reads the
|
|
@@ -457,6 +476,12 @@ export function buildGateDeps(params) {
|
|
|
457
476
|
// ("return 401 so the verification test passes") become rule-4c
|
|
458
477
|
// findings so the child verifies the real requirement, not the check.
|
|
459
478
|
probeGamingProbe: () => collectAddedLines(cwd2, signal).then(findProbeGaming),
|
|
479
|
+
// Deterministic cross-task deletion probe (mx5 run 12 PROMPT 2):
|
|
480
|
+
// tracked files this task's diff DELETES whose introducing commit
|
|
481
|
+
// belongs to a DIFFERENT task — a sibling's committed deliverable
|
|
482
|
+
// destroyed (typically to green a check). Injected under rule 4d and
|
|
483
|
+
// carried on a FAIL so an ACCEPT records durable debts.
|
|
484
|
+
crossTaskDeletionProbe: () => collectTaskTreeChanges(cwd2, signal).then(changes => findCrossTaskDeletions(changes, taskId, rel => taskThatIntroduced(cwd2, rel))),
|
|
460
485
|
// Deterministic prohibition probe: paths the spec forbids modifying
|
|
461
486
|
// that the task's diff modified anyway become prompt-level findings
|
|
462
487
|
// under the no-waiver rule — the child otherwise rarely runs `git
|
|
@@ -520,7 +545,14 @@ export function buildGateDeps(params) {
|
|
|
520
545
|
const r = await git(cwd2, args, signal);
|
|
521
546
|
return { exitCode: r.exitCode, stdout: r.stdout };
|
|
522
547
|
},
|
|
523
|
-
frozenPaths
|
|
548
|
+
frozenPaths,
|
|
549
|
+
// Cross-task deletion guard (mx5 run 12 PROMPT 2): the revert-guard
|
|
550
|
+
// is blind to a clean tracked sibling deliverable, so deleting one
|
|
551
|
+
// greens the lint and the pass returns ok. Provenance is the
|
|
552
|
+
// discriminator — the guard restores a sibling's deleted file and
|
|
553
|
+
// reports not-applied naming the owner.
|
|
554
|
+
currentTaskId: taskId,
|
|
555
|
+
introducedBy: rel => Promise.resolve(taskThatIntroduced(cwd2, rel))
|
|
524
556
|
});
|
|
525
557
|
},
|
|
526
558
|
// Deterministic static check + tree helpers for the enforce pre-commit gate.
|
package/dist/task/lint-fix.d.ts
CHANGED
|
@@ -31,6 +31,17 @@ export interface LintFixDeps {
|
|
|
31
31
|
* and the fix reported not-applied. Absent/empty → no-op, prior behavior.
|
|
32
32
|
*/
|
|
33
33
|
frozenPaths?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* The CURRENT task's id, for the cross-task deletion guard's provenance
|
|
36
|
+
* discriminator. Absent → the guard is disarmed (prior behavior).
|
|
37
|
+
*/
|
|
38
|
+
currentTaskId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* file → introducing task id (git provenance, see task-provenance.ts); null on
|
|
41
|
+
* any unknown — inconclusive is never evidence. Absent → the cross-task
|
|
42
|
+
* deletion guard is disarmed (prior behavior).
|
|
43
|
+
*/
|
|
44
|
+
introducedBy?: (rel: string) => Promise<string | null>;
|
|
34
45
|
}
|
|
35
46
|
/** The fix child edits and runs the checker; bash exists to RUN the check, not git. */
|
|
36
47
|
export declare const LINT_FIX_TOOLS = "read,edit,bash";
|
package/dist/task/lint-fix.js
CHANGED
|
@@ -43,8 +43,23 @@
|
|
|
43
43
|
* that were CLEAN before the child ran are reverted — a frozen path already
|
|
44
44
|
* dirty with (possibly task) work is left alone, in the guard's safe direction:
|
|
45
45
|
* cost time, never work.
|
|
46
|
+
*
|
|
47
|
+
* CROSS-TASK DELETION GUARD (mx5 run 12 PROMPT 2, verify-debug.log 17:53:44): the
|
|
48
|
+
* revert-guard above is outcome-based but blind to a CLEAN tracked file — a
|
|
49
|
+
* sibling task's committed deliverable is neither pre-dirty nor pre-untracked, so
|
|
50
|
+
* DELETING it slips both checks, and if the lint converges the pass returns ok
|
|
51
|
+
* (confirmed live: the child deleted TASK_0020's playwright ct files to green a
|
|
52
|
+
* typed-lint it was frozen out of fixing properly). The discriminator is
|
|
53
|
+
* PROVENANCE, not deletion per se: a tracked file the CHILD deleted whose
|
|
54
|
+
* introducing task (per `introducedBy`, git history) differs from the CURRENT
|
|
55
|
+
* task is restored from HEAD and the fix reported not-applied naming the owner.
|
|
56
|
+
* Same-task deletions, unknown provenance, relocations, and any git error all
|
|
57
|
+
* step aside — the guard may only cost time, never work. Armed only when both
|
|
58
|
+
* `currentTaskId` and `introducedBy` are wired.
|
|
46
59
|
*/
|
|
47
60
|
import { parseChangedFrozenFiles, pathNamedIn, revertFrozenPaths } from './frozen-path-guard.js';
|
|
61
|
+
import { parseTreeChanges } from './write-guard.js';
|
|
62
|
+
import { findCrossTaskDeletions } from './task-provenance.js';
|
|
48
63
|
/** The fix child edits and runs the checker; bash exists to RUN the check, not git. */
|
|
49
64
|
export const LINT_FIX_TOOLS = 'read,edit,bash';
|
|
50
65
|
/**
|
|
@@ -121,6 +136,17 @@ async function dirtyFiles(deps) {
|
|
|
121
136
|
.map(l => l.trim())
|
|
122
137
|
.filter(l => l.length > 0);
|
|
123
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* The whole tree's current changes (`git status --porcelain` shape), or null when
|
|
141
|
+
* git itself failed — inconclusive, not evidence (same discipline as dirtyFiles).
|
|
142
|
+
* Feeds the cross-task deletion guard's pre/post comparison.
|
|
143
|
+
*/
|
|
144
|
+
async function treeChanges(deps) {
|
|
145
|
+
const r = await deps.git(['status', '--porcelain', '--', '.', EXCLUDE_TASKS_DIR]);
|
|
146
|
+
if (r.exitCode !== 0)
|
|
147
|
+
return null;
|
|
148
|
+
return parseTreeChanges(r.stdout);
|
|
149
|
+
}
|
|
124
150
|
/**
|
|
125
151
|
* Which spec-frozen paths currently show a change in `git status`? Null when git
|
|
126
152
|
* itself failed — inconclusive, not evidence (same discipline as dirtyFiles).
|
|
@@ -172,6 +198,12 @@ export async function runBoundedLintFix(deps) {
|
|
|
172
198
|
// here disables the guard for this run (inconclusive ≠ license to revert).
|
|
173
199
|
const frozen = deps.frozenPaths ?? [];
|
|
174
200
|
const preFrozenDirty = await frozenDirtySet(deps, frozen);
|
|
201
|
+
// CROSS-TASK DELETION baseline: deletions already in the tree pre-child are
|
|
202
|
+
// the task's own (uncommitted) work, not the child's — only deletions the
|
|
203
|
+
// CHILD introduces on top are candidates. A git failure here disarms the
|
|
204
|
+
// guard for this run (inconclusive ≠ evidence).
|
|
205
|
+
const deletionGuardArmed = Boolean(deps.currentTaskId && deps.introducedBy);
|
|
206
|
+
const preChanges = deletionGuardArmed ? await treeChanges(deps) : null;
|
|
175
207
|
try {
|
|
176
208
|
await deps.runChild(LINT_FIX_TOOLS, buildLintFixPrompt(deps.failReason, frozen), deps.signal);
|
|
177
209
|
}
|
|
@@ -224,6 +256,36 @@ export async function runBoundedLintFix(deps) {
|
|
|
224
256
|
+ `${violations.length > 3 ? ', …' : ''}) — fix ${snapshot ? 'rolled back' : 'REJECTED but no snapshot to restore'}`
|
|
225
257
|
};
|
|
226
258
|
}
|
|
259
|
+
// CROSS-TASK DELETION GUARD: a tracked file the CHILD deleted whose
|
|
260
|
+
// introducing task differs from the current task is a sibling's committed
|
|
261
|
+
// deliverable destroyed to go green (mx5 run 12: the child deleted TASK_0020's
|
|
262
|
+
// playwright ct files and the pass returned ok — the revert-guard above only
|
|
263
|
+
// watches pre-dirty and pre-untracked files, and a clean tracked sibling file
|
|
264
|
+
// is neither). Restore JUST those paths from HEAD (they were clean pre-child,
|
|
265
|
+
// so nothing of the task's work can be lost) and report not-applied naming the
|
|
266
|
+
// owner. Inconclusive on any side — git error, unknown provenance, same-task
|
|
267
|
+
// deletion, relocation — steps aside.
|
|
268
|
+
if (deletionGuardArmed && preChanges !== null) {
|
|
269
|
+
const postChanges = await treeChanges(deps);
|
|
270
|
+
if (postChanges !== null) {
|
|
271
|
+
const preDeleted = new Set(preChanges.deleted);
|
|
272
|
+
const crossDeletions = await findCrossTaskDeletions({
|
|
273
|
+
modified: postChanges.modified,
|
|
274
|
+
added: postChanges.added,
|
|
275
|
+
deleted: postChanges.deleted.filter(p => !preDeleted.has(p))
|
|
276
|
+
}, deps.currentTaskId, deps.introducedBy);
|
|
277
|
+
if (crossDeletions.length > 0) {
|
|
278
|
+
await deps.git(['checkout', '-f', 'HEAD', '--', ...crossDeletions.map(d => d.path)]);
|
|
279
|
+
const named = crossDeletions.map(d => `${d.path} — ${d.owner}'s deliverable`);
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
reason: `cross-task-deletion: fix child DELETED sibling task deliverable(s) `
|
|
283
|
+
+ `(${named.slice(0, 3).join('; ')}${crossDeletions.length > 3 ? '; …' : ''}) `
|
|
284
|
+
+ `— restored from HEAD; deleting another task's committed work is not a fix`
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
227
289
|
// FROZEN-PATH GUARD: a frozen path that was clean pre-child and is changed
|
|
228
290
|
// now is the child's edit — the exact write verify's rule-4b prohibition
|
|
229
291
|
// probe is guaranteed to fail the TASK for (mx5 run 12: ESLint's own error
|
|
@@ -143,6 +143,17 @@ export interface GateDeps {
|
|
|
143
143
|
* it), so the final gate re-checks it at run end. Best-effort; absent in tests.
|
|
144
144
|
*/
|
|
145
145
|
recordFrozenBlockedDebt?: (cwd: string, taskId: string, reason: string) => Promise<void>;
|
|
146
|
+
/**
|
|
147
|
+
* Record a durable CROSS-TASK DELETION debt (mx5 run 12 PROMPT 2): the task's
|
|
148
|
+
* work deleted a file a DIFFERENT task's commit introduced, verify FAILed with
|
|
149
|
+
* the deterministic finding attached, and the user ACCEPTed anyway — the
|
|
150
|
+
* deletion ships in the next commit, so the final gate must re-check it
|
|
151
|
+
* (resolved iff the file is back in the tree). Best-effort; absent in tests.
|
|
152
|
+
*/
|
|
153
|
+
recordCrossTaskDeletionDebt?: (cwd: string, taskId: string, deletion: {
|
|
154
|
+
path: string;
|
|
155
|
+
owner: string;
|
|
156
|
+
}) => Promise<void>;
|
|
146
157
|
/**
|
|
147
158
|
* The concrete paths this task's spec forbids modifying (its `Do NOT modify`
|
|
148
159
|
* CONSTRAINTS — see frozen-path-guard.ts / prohibition-probe.ts). Used to
|
package/dist/task/task-gates.js
CHANGED
|
@@ -208,6 +208,18 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
208
208
|
// recording must never break the gate sequence
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
+
// Cross-task deletions the verify probe detected ship in the next
|
|
212
|
+
// commit with this ACCEPT — record each as its own durable debt so
|
|
213
|
+
// the final gate re-checks them (mx5 run 12 PROMPT 2). Best-effort.
|
|
214
|
+
for (const del of verified.crossTaskDeletions ?? []) {
|
|
215
|
+
try {
|
|
216
|
+
await deps.recordCrossTaskDeletionDebt?.(p.cwd, p.taskId, del);
|
|
217
|
+
await rec(`accept-debt: cross-task deletion recorded — ${del.path} (${del.owner}'s deliverable)`);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// recording must never break the gate sequence
|
|
221
|
+
}
|
|
222
|
+
}
|
|
211
223
|
active.ui.notify(`${p.tag}: accepted "${p.title}" despite verify FAIL (${failReason.slice(0, 120)}) — proceeding.`, 'warning');
|
|
212
224
|
break;
|
|
213
225
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type TreeChangeSummary } from './write-guard.js';
|
|
2
|
+
/** Run a git subcommand; only stdout + exit code are read (injectable for tests). */
|
|
3
|
+
export type ProvenanceGit = (args: string[]) => Promise<{
|
|
4
|
+
stdout: string;
|
|
5
|
+
exitCode: number;
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* The task id carried by the OLDEST commit subject in `git log --diff-filter=A
|
|
9
|
+
* --format=%s` output (newest-first, so the LAST line is the original
|
|
10
|
+
* introduction — a delete-and-re-add later in history must not reattribute the
|
|
11
|
+
* file). Both the task snapshot and the ENFORCE commit shapes match the
|
|
12
|
+
* `(TASK_nnnn)` suffix. Null when the output is empty or the adding commit is
|
|
13
|
+
* not a task commit.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseIntroducingTask(logSubjects: string): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* The task whose commit INTRODUCED `rel`. Null when the file predates the run,
|
|
18
|
+
* was never committed, git is unavailable, or the adding commit is not a task
|
|
19
|
+
* commit — every unknown degrades to "no provenance claim".
|
|
20
|
+
*/
|
|
21
|
+
export declare function taskThatIntroduced(cwd: string, rel: string): string | null;
|
|
22
|
+
/** Same lookup through an injected async git (the lint-fix deps shape). */
|
|
23
|
+
export declare function taskThatIntroducedVia(git: ProvenanceGit, rel: string): Promise<string | null>;
|
|
24
|
+
/** One detected cross-task deletion: the file and the task whose commit introduced it. */
|
|
25
|
+
export interface CrossTaskDeletion {
|
|
26
|
+
path: string;
|
|
27
|
+
owner: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Deterministic cross-task deletion detection: a tracked file DELETED in the
|
|
31
|
+
* given change summary whose introducing task differs from the CURRENT task is a
|
|
32
|
+
* finding. Everything else steps aside by construction — same-task deletions and
|
|
33
|
+
* unknown provenance are never findings (a task refactoring its own work must not
|
|
34
|
+
* be blocked), an unknown CURRENT task id disables the check ("differs" cannot be
|
|
35
|
+
* established), and a relocation (the same file name reappears among the added
|
|
36
|
+
* files — write-guard's allowance) is not a deletion. `introducedBy` may be sync
|
|
37
|
+
* or async and may throw; a throw reads as unknown provenance.
|
|
38
|
+
*/
|
|
39
|
+
export declare function findCrossTaskDeletions(changes: TreeChangeSummary, currentTaskId: string, introducedBy: (rel: string) => string | null | Promise<string | null>): Promise<CrossTaskDeletion[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Finding lines for the verify prompt (the substitution-probe injection pattern:
|
|
42
|
+
* the rule alone is A/B-proven ignored, the rule plus a concrete finding naming
|
|
43
|
+
* the file is what lands).
|
|
44
|
+
*/
|
|
45
|
+
export declare function crossTaskDeletionVerifyFindings(found: CrossTaskDeletion[]): string[];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-provenance — file → introducing-task lookup, and the cross-task deletion
|
|
3
|
+
* detection built on it (mx5 run-12 PROMPT 2).
|
|
4
|
+
*
|
|
5
|
+
* The failure class: write-enabled gate/fix children can DELETE a sibling task's
|
|
6
|
+
* committed deliverable to turn a red check green. Live (mx5 2026-07-16,
|
|
7
|
+
* verify-debug.log 17:53:44): `bun run lint` was permanently red because
|
|
8
|
+
* committed playwright ct files were not registered in a spec-frozen tsconfig —
|
|
9
|
+
* so a lint-fix child deleted TASK_0020's verified deliverables and the pass
|
|
10
|
+
* returned ok. Every existing guard missed the class:
|
|
11
|
+
*
|
|
12
|
+
* - the lint-fix revert-guard checks only pre-DIRTY files reverted and
|
|
13
|
+
* pre-UNTRACKED files gone — a CLEAN tracked sibling file is in neither set;
|
|
14
|
+
* - the frozen-path guard covers only paths THIS task's spec froze;
|
|
15
|
+
* - the run-11 deletion guard covers only the final-fix child;
|
|
16
|
+
* - the impl turn legitimately deletes files (its own refactors), so a blanket
|
|
17
|
+
* ban is wrong — the discriminator must be PROVENANCE: who introduced the file.
|
|
18
|
+
*
|
|
19
|
+
* The provenance primitive (extracted from final-gate.ts, where it fed the
|
|
20
|
+
* conflicting-claim annotation) resolves a file to the task whose commit ADDED it
|
|
21
|
+
* (`git log --diff-filter=A` → oldest task-subject commit). Every unknown — a file
|
|
22
|
+
* predating the run, a non-task commit, any git error — degrades to null:
|
|
23
|
+
* inconclusive is never evidence, so the guards built on this may only cost time,
|
|
24
|
+
* never work.
|
|
25
|
+
*/
|
|
26
|
+
import { spawnSync } from 'node:child_process';
|
|
27
|
+
import { findForbiddenDeletions } from './write-guard.js';
|
|
28
|
+
/**
|
|
29
|
+
* The task id carried by the OLDEST commit subject in `git log --diff-filter=A
|
|
30
|
+
* --format=%s` output (newest-first, so the LAST line is the original
|
|
31
|
+
* introduction — a delete-and-re-add later in history must not reattribute the
|
|
32
|
+
* file). Both the task snapshot and the ENFORCE commit shapes match the
|
|
33
|
+
* `(TASK_nnnn)` suffix. Null when the output is empty or the adding commit is
|
|
34
|
+
* not a task commit.
|
|
35
|
+
*/
|
|
36
|
+
export function parseIntroducingTask(logSubjects) {
|
|
37
|
+
const subjects = logSubjects.trim().split('\n').filter(Boolean);
|
|
38
|
+
const first = subjects[subjects.length - 1] ?? '';
|
|
39
|
+
const m = /\((TASK_\d+)\)\s*$/.exec(first);
|
|
40
|
+
return m ? m[1] : null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The task whose commit INTRODUCED `rel`. Null when the file predates the run,
|
|
44
|
+
* was never committed, git is unavailable, or the adding commit is not a task
|
|
45
|
+
* commit — every unknown degrades to "no provenance claim".
|
|
46
|
+
*/
|
|
47
|
+
export function taskThatIntroduced(cwd, rel) {
|
|
48
|
+
const r = spawnSync('git', ['log', '--diff-filter=A', '--format=%s', '--', rel], {
|
|
49
|
+
cwd,
|
|
50
|
+
encoding: 'utf8'
|
|
51
|
+
});
|
|
52
|
+
if (r.error || r.status !== 0 || !r.stdout)
|
|
53
|
+
return null;
|
|
54
|
+
return parseIntroducingTask(r.stdout);
|
|
55
|
+
}
|
|
56
|
+
/** Same lookup through an injected async git (the lint-fix deps shape). */
|
|
57
|
+
export async function taskThatIntroducedVia(git, rel) {
|
|
58
|
+
const r = await git(['log', '--diff-filter=A', '--format=%s', '--', rel]);
|
|
59
|
+
if (r.exitCode !== 0 || !r.stdout)
|
|
60
|
+
return null;
|
|
61
|
+
return parseIntroducingTask(r.stdout);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Deterministic cross-task deletion detection: a tracked file DELETED in the
|
|
65
|
+
* given change summary whose introducing task differs from the CURRENT task is a
|
|
66
|
+
* finding. Everything else steps aside by construction — same-task deletions and
|
|
67
|
+
* unknown provenance are never findings (a task refactoring its own work must not
|
|
68
|
+
* be blocked), an unknown CURRENT task id disables the check ("differs" cannot be
|
|
69
|
+
* established), and a relocation (the same file name reappears among the added
|
|
70
|
+
* files — write-guard's allowance) is not a deletion. `introducedBy` may be sync
|
|
71
|
+
* or async and may throw; a throw reads as unknown provenance.
|
|
72
|
+
*/
|
|
73
|
+
export async function findCrossTaskDeletions(changes, currentTaskId, introducedBy) {
|
|
74
|
+
const current = currentTaskId.trim();
|
|
75
|
+
if (current.length === 0)
|
|
76
|
+
return [];
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const p of findForbiddenDeletions(changes)) {
|
|
79
|
+
let owner;
|
|
80
|
+
try {
|
|
81
|
+
owner = await introducedBy(p);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
owner = null;
|
|
85
|
+
}
|
|
86
|
+
if (owner && owner !== current)
|
|
87
|
+
out.push({ path: p, owner });
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Finding lines for the verify prompt (the substitution-probe injection pattern:
|
|
93
|
+
* the rule alone is A/B-proven ignored, the rule plus a concrete finding naming
|
|
94
|
+
* the file is what lands).
|
|
95
|
+
*/
|
|
96
|
+
export function crossTaskDeletionVerifyFindings(found) {
|
|
97
|
+
return found.map(f => `\`${f.path}\` — introduced and committed by ${f.owner}, DELETED by this task's work`);
|
|
98
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CrossTaskDeletion } from './task-provenance.js';
|
|
1
2
|
/**
|
|
2
3
|
* The verification child gets exactly two tools: `read` and `bash`.
|
|
3
4
|
*
|
|
@@ -27,6 +28,12 @@ export interface VerifyOutcome {
|
|
|
27
28
|
* AUTOFIX re-run, which cannot provision a missing tool. Only meaningful when
|
|
28
29
|
* ok === false. */
|
|
29
30
|
unobserved?: boolean;
|
|
31
|
+
/** The deterministic cross-task deletion findings (see task-provenance.ts) that
|
|
32
|
+
* were live when this verdict was produced: sibling tasks' committed
|
|
33
|
+
* deliverables this task's diff deletes. Carried on a FAIL so the gate loop
|
|
34
|
+
* can record each as a durable debt if the user ACCEPTs anyway — the deletion
|
|
35
|
+
* then ships in the next commit and the final gate must re-check it. */
|
|
36
|
+
crossTaskDeletions?: CrossTaskDeletion[];
|
|
30
37
|
}
|
|
31
38
|
/**
|
|
32
39
|
* Slice the delivered spec (GOAL / CONSTRAINTS / ACCEPTANCE / VERIFY) out of a
|
|
@@ -61,7 +68,7 @@ export declare function extractSpecForVerification(taskBody: string): string | n
|
|
|
61
68
|
* Guard: honest-clean fixture (prohibition in spec, probe silent) 5/5 PASS — no
|
|
62
69
|
* paranoia. Reverted-violation ≡ clean at the diff level (no entry → no finding).
|
|
63
70
|
*/
|
|
64
|
-
export declare function buildVerifyPrompt(spec: string, probeFindings?: string[], envNotes?: string, prohibitionFindings?: string[], skipEscapeFindings?: string[], contracts?: string, testAssemblyFindings?: string[], probeGamingFindings?: string[]): string;
|
|
71
|
+
export declare function buildVerifyPrompt(spec: string, probeFindings?: string[], envNotes?: string, prohibitionFindings?: string[], skipEscapeFindings?: string[], contracts?: string, testAssemblyFindings?: string[], probeGamingFindings?: string[], crossTaskDeletionFindings?: string[]): string;
|
|
65
72
|
/**
|
|
66
73
|
* Parse the child's verdict. Scans for the LAST `WORK-VERIFIED: PASS|FAIL|UNOBSERVED`
|
|
67
74
|
* marker (the model discusses before concluding, and bash output may echo the word
|
|
@@ -130,6 +137,15 @@ export interface VerificationDeps {
|
|
|
130
137
|
* underlying requirement is genuinely met rather than trusting the green check.
|
|
131
138
|
* Pure diff-text analysis; ABSENT or empty → no probe block. */
|
|
132
139
|
probeGamingProbe?: () => Promise<string[]>;
|
|
140
|
+
/**
|
|
141
|
+
* DETERMINISTIC cross-task deletion probe (see task-provenance.ts, mx5 run 12
|
|
142
|
+
* PROMPT 2): tracked files the task's diff DELETES whose introducing task (git
|
|
143
|
+
* provenance) differs from the current task — a sibling's committed deliverable
|
|
144
|
+
* destroyed, typically to green a check. Injected as prompt findings under rule
|
|
145
|
+
* 4d (MANDATORY + verdict-gating, the A/B-proven shape — buried rules score
|
|
146
|
+
* 0/5), and carried structurally on a FAIL outcome so an ACCEPT records each as
|
|
147
|
+
* a durable debt. ABSENT or empty → no block. */
|
|
148
|
+
crossTaskDeletionProbe?: () => Promise<CrossTaskDeletion[]>;
|
|
133
149
|
/**
|
|
134
150
|
* Result of the git-state guard for the MOST RECENT runChild call (see
|
|
135
151
|
* git-state-guard.ts): did the child mutate repo state (stash/checkout/file
|
package/dist/task/verify-work.js
CHANGED
|
@@ -71,6 +71,7 @@ import { USER_CANCELLED } from './child-runner.js';
|
|
|
71
71
|
import { buildEnvNotesBlock, ENV_NOTE_EMIT_INSTRUCTION, extractEnvNotes } from './env-notes.js';
|
|
72
72
|
import { buildContractsVerifyBlock } from './contracts.js';
|
|
73
73
|
import { findSkipEscapes, skipEscapeVerifyFindings } from './skip-escape.js';
|
|
74
|
+
import { crossTaskDeletionVerifyFindings } from './task-provenance.js';
|
|
74
75
|
/**
|
|
75
76
|
* The verification child gets exactly two tools: `read` and `bash`.
|
|
76
77
|
*
|
|
@@ -139,7 +140,7 @@ export function extractSpecForVerification(taskBody) {
|
|
|
139
140
|
* Guard: honest-clean fixture (prohibition in spec, probe silent) 5/5 PASS — no
|
|
140
141
|
* paranoia. Reverted-violation ≡ clean at the diff level (no entry → no finding).
|
|
141
142
|
*/
|
|
142
|
-
export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFindings, skipEscapeFindings, contracts, testAssemblyFindings, probeGamingFindings) {
|
|
143
|
+
export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFindings, skipEscapeFindings, contracts, testAssemblyFindings, probeGamingFindings, crossTaskDeletionFindings) {
|
|
143
144
|
const probeBlock = probeFindings && probeFindings.length > 0 ?
|
|
144
145
|
[
|
|
145
146
|
'SELF-VERIFICATION NOTICE (deterministic, computed by the orchestrator from the diff):',
|
|
@@ -166,6 +167,22 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
|
|
|
166
167
|
''
|
|
167
168
|
]
|
|
168
169
|
: [];
|
|
170
|
+
const crossTaskDeletionBlock = crossTaskDeletionFindings && crossTaskDeletionFindings.length > 0 ?
|
|
171
|
+
[
|
|
172
|
+
'CROSS-TASK DELETION NOTICE (deterministic, computed by the orchestrator from',
|
|
173
|
+
"git provenance over the task's diff): this task's work DELETED committed",
|
|
174
|
+
'deliverable(s) that a DIFFERENT, already-completed task introduced:',
|
|
175
|
+
...crossTaskDeletionFindings.map(f => `- ${f}`),
|
|
176
|
+
"A sibling task's verified, committed deliverable is not this task's to remove.",
|
|
177
|
+
'Deleting the file a checker complains about is the cheapest way to turn a red',
|
|
178
|
+
'check green — the finding vanishes with the file — and it destroys finished',
|
|
179
|
+
'work (rule 4d). A green check suite on the shrunken tree is NOT a waiver.',
|
|
180
|
+
"Unless THIS task's spec explicitly REQUIRES removing exactly that file, the",
|
|
181
|
+
'verdict is FAIL naming the deleted file and its owning task — even if every',
|
|
182
|
+
'check passes and the deletion made them pass.',
|
|
183
|
+
''
|
|
184
|
+
]
|
|
185
|
+
: [];
|
|
169
186
|
const probeGamingBlock = probeGamingFindings && probeGamingFindings.length > 0 ?
|
|
170
187
|
[
|
|
171
188
|
'CHECK-GAMING NOTICE (deterministic, computed by the orchestrator from the',
|
|
@@ -235,6 +252,7 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
|
|
|
235
252
|
...contractsBlock,
|
|
236
253
|
...probeBlock,
|
|
237
254
|
...prohibitionBlock,
|
|
255
|
+
...crossTaskDeletionBlock,
|
|
238
256
|
...probeGamingBlock,
|
|
239
257
|
...skipEscapeBlock,
|
|
240
258
|
...testAssemblyBlock,
|
|
@@ -359,6 +377,17 @@ export function buildVerifyPrompt(spec, probeFindings, envNotes, prohibitionFind
|
|
|
359
377
|
' check, not implementing the behavior). If the requirement is genuinely unmet while',
|
|
360
378
|
' the check passes, the verdict is FAIL naming the gamed check and the real gap.',
|
|
361
379
|
'',
|
|
380
|
+
"4d. A SIBLING TASK'S COMMITTED DELIVERABLE IS NOT THIS TASK'S TO DELETE — check the",
|
|
381
|
+
" task's own diff (git) for DELETED tracked files. A file that a DIFFERENT completed",
|
|
382
|
+
" task introduced and committed, deleted by this task's work, is finished work",
|
|
383
|
+
' destroyed — usually to make a red check go green (the file the checker complains',
|
|
384
|
+
' about simply disappears; that is quieting the messenger, rule 4c, not fixing the',
|
|
385
|
+
' defect). A passing check suite on the shrunken tree is not a waiver. Only two',
|
|
386
|
+
" outcomes are not a FAIL: THIS task's spec explicitly requires that removal, or",
|
|
387
|
+
' the deletion is a genuine relocation (the same file lives on elsewhere in the',
|
|
388
|
+
' tree). Otherwise the verdict is FAIL naming the deleted file and the task that',
|
|
389
|
+
' owns it.',
|
|
390
|
+
'',
|
|
362
391
|
'5. The ONLY thing you may assume is already provided is a genuinely EXTERNAL running',
|
|
363
392
|
' service or network resource (a database server, an API host) that the project',
|
|
364
393
|
' documents as a prerequisite. Before you rely on that assumption, PROBE for the',
|
|
@@ -531,6 +560,18 @@ export async function runWorkVerification(deps) {
|
|
|
531
560
|
probeGaming = [];
|
|
532
561
|
}
|
|
533
562
|
}
|
|
563
|
+
// Cross-task deletion findings feed the prompt (rule 4d) AND ride on a FAIL
|
|
564
|
+
// outcome (structured) so an ACCEPT can record them as durable debts. A probe
|
|
565
|
+
// failure must never block verification.
|
|
566
|
+
let crossDeletions = [];
|
|
567
|
+
if (deps.crossTaskDeletionProbe) {
|
|
568
|
+
try {
|
|
569
|
+
crossDeletions = await deps.crossTaskDeletionProbe();
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
crossDeletions = [];
|
|
573
|
+
}
|
|
574
|
+
}
|
|
534
575
|
// Environment facts from earlier gate children (best-effort; a cache failure
|
|
535
576
|
// must never block verification).
|
|
536
577
|
let envNotes = '';
|
|
@@ -566,7 +607,7 @@ export async function runWorkVerification(deps) {
|
|
|
566
607
|
for (let attempt = 1;; attempt++) {
|
|
567
608
|
let text;
|
|
568
609
|
try {
|
|
569
|
-
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, envNotes, prohibitions, skipEscapes, contracts, testAssembly, probeGaming), deps.signal);
|
|
610
|
+
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, envNotes, prohibitions, skipEscapes, contracts, testAssembly, probeGaming, crossTaskDeletionVerifyFindings(crossDeletions)), deps.signal);
|
|
570
611
|
}
|
|
571
612
|
catch (err) {
|
|
572
613
|
if (err instanceof Error && err.message === USER_CANCELLED)
|
|
@@ -603,15 +644,25 @@ export async function runWorkVerification(deps) {
|
|
|
603
644
|
return { ok: true };
|
|
604
645
|
if (verdict.detail === 'no verdict emitted' && attempt === 1)
|
|
605
646
|
continue;
|
|
647
|
+
// Structured cross-task deletion findings ride on every FAIL outcome: if the
|
|
648
|
+
// human ACCEPTs the failing artifact, the deletions ship in the next commit
|
|
649
|
+
// and the gate loop must record each as a durable debt (see task-gates.ts).
|
|
650
|
+
const deletions = crossDeletions.length > 0 ? { crossTaskDeletions: crossDeletions } : {};
|
|
606
651
|
// UNOBSERVED (rule 5c): a spec-required behavioral check could not run because
|
|
607
652
|
// its tooling is absent. Block like any FAIL, but flag it so the gate hands it
|
|
608
653
|
// straight to the human — re-running the impl turn cannot install a missing tool.
|
|
609
654
|
if (verdict.unobserved) {
|
|
610
|
-
return {
|
|
655
|
+
return {
|
|
656
|
+
ok: false,
|
|
657
|
+
unobserved: true,
|
|
658
|
+
reason: `work unobserved: ${verdict.detail}`,
|
|
659
|
+
...deletions
|
|
660
|
+
};
|
|
611
661
|
}
|
|
612
662
|
return {
|
|
613
663
|
ok: false,
|
|
614
|
-
reason: `work did not verify: ${verdict.detail}${verdict.detail === 'no verdict emitted' ? ' (after verify retry)' : ''}
|
|
664
|
+
reason: `work did not verify: ${verdict.detail}${verdict.detail === 'no verdict emitted' ? ' (after verify retry)' : ''}`,
|
|
665
|
+
...deletions
|
|
615
666
|
};
|
|
616
667
|
}
|
|
617
668
|
}
|
|
@@ -36,6 +36,16 @@ export interface TreeChangeSummary {
|
|
|
36
36
|
* without a repo.
|
|
37
37
|
*/
|
|
38
38
|
export declare function parseTreeChanges(porcelain: string): TreeChangeSummary;
|
|
39
|
+
/**
|
|
40
|
+
* Parse `git diff --name-status` output (tab-separated: `M\tpath`, `A\tpath`,
|
|
41
|
+
* `D\tpath`, `R100\told\tnew`, `C75\told\tnew`) into the same change summary. Used
|
|
42
|
+
* where the working tree is already clean and the last COMMIT's diff is the record
|
|
43
|
+
* of the task's changes (the post-enforce re-verify fallback). A rename entry
|
|
44
|
+
* contributes its source to `deleted` and its target to `added`, matching
|
|
45
|
+
* parseTreeChanges so the relocation allowance below reads both shapes identically;
|
|
46
|
+
* a copy's source still exists, so only its target counts (as added).
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseNameStatusChanges(nameStatus: string): TreeChangeSummary;
|
|
39
49
|
/**
|
|
40
50
|
* The deletions a fix pass may NOT make: every deleted tracked file whose name does
|
|
41
51
|
* not reappear among the added files (a relocation keeps the file, under the same
|
package/dist/task/write-guard.js
CHANGED
|
@@ -75,6 +75,46 @@ export function parseTreeChanges(porcelain) {
|
|
|
75
75
|
}
|
|
76
76
|
return { modified: [...modified], deleted: [...deleted], added: [...added] };
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Parse `git diff --name-status` output (tab-separated: `M\tpath`, `A\tpath`,
|
|
80
|
+
* `D\tpath`, `R100\told\tnew`, `C75\told\tnew`) into the same change summary. Used
|
|
81
|
+
* where the working tree is already clean and the last COMMIT's diff is the record
|
|
82
|
+
* of the task's changes (the post-enforce re-verify fallback). A rename entry
|
|
83
|
+
* contributes its source to `deleted` and its target to `added`, matching
|
|
84
|
+
* parseTreeChanges so the relocation allowance below reads both shapes identically;
|
|
85
|
+
* a copy's source still exists, so only its target counts (as added).
|
|
86
|
+
*/
|
|
87
|
+
export function parseNameStatusChanges(nameStatus) {
|
|
88
|
+
const modified = new Set();
|
|
89
|
+
const deleted = new Set();
|
|
90
|
+
const added = new Set();
|
|
91
|
+
for (const raw of nameStatus.split('\n')) {
|
|
92
|
+
const parts = raw.split('\t').map(s => s.trim());
|
|
93
|
+
const code = parts[0] ?? '';
|
|
94
|
+
if (code.length === 0 || !parts[1])
|
|
95
|
+
continue;
|
|
96
|
+
if (code.startsWith('R')) {
|
|
97
|
+
deleted.add(parts[1]);
|
|
98
|
+
if (parts[2])
|
|
99
|
+
added.add(parts[2]);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (code.startsWith('C')) {
|
|
103
|
+
added.add(parts[2] ?? parts[1]);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (code.startsWith('D')) {
|
|
107
|
+
deleted.add(parts[1]);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (code.startsWith('A')) {
|
|
111
|
+
added.add(parts[1]);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
modified.add(parts[1]);
|
|
115
|
+
}
|
|
116
|
+
return { modified: [...modified], deleted: [...deleted], added: [...added] };
|
|
117
|
+
}
|
|
78
118
|
const basename = (p) => {
|
|
79
119
|
const i = p.lastIndexOf('/');
|
|
80
120
|
return i === -1 ? p : p.slice(i + 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.23",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|