@mjasnikovs/pi-task 0.18.21 → 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 +45 -4
- package/dist/task/accept-debt.js +76 -9
- package/dist/task/final-gate.d.ts +2 -9
- package/dist/task/final-gate.js +10 -22
- package/dist/task/frozen-conflict.d.ts +36 -0
- package/dist/task/frozen-conflict.js +206 -0
- package/dist/task/frozen-path-guard.d.ts +9 -0
- package/dist/task/frozen-path-guard.js +15 -0
- package/dist/task/gate-deps.d.ts +8 -0
- package/dist/task/gate-deps.js +39 -3
- package/dist/task/lint-fix.d.ts +15 -1
- package/dist/task/lint-fix.js +82 -1
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +31 -9
- package/dist/task/prohibition-probe.d.ts +7 -0
- package/dist/task/prohibition-probe.js +1 -1
- package/dist/task/task-gates.d.ts +19 -0
- package/dist/task/task-gates.js +71 -8
- 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
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 } 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). */
|
|
@@ -314,6 +329,14 @@ export function buildGateDeps(params) {
|
|
|
314
329
|
// discardEdits): the final integration gate re-checks each debt at run end.
|
|
315
330
|
recordAcceptDebt: (cwd2, taskId, reason) => recordAcceptDebt(cwd2, taskId, reason),
|
|
316
331
|
recordEnforceRevertDebt: (cwd2, taskId, reason) => recordEnforceRevertDebt(cwd2, taskId, reason),
|
|
332
|
+
// Durable cross-task-contradiction ledger (PROMPT 1 layer B): a repo-health
|
|
333
|
+
// FAIL whose only fix is an edit to a path this task's spec froze — recorded
|
|
334
|
+
// when the gate loop routes it to the picker, re-checked by the final gate.
|
|
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),
|
|
317
340
|
// Frozen-path write-deny (see frozen-path-guard.ts): the concrete paths this
|
|
318
341
|
// task's spec forbids modifying, so the gate sequence can UNDO any edit the
|
|
319
342
|
// enforce EDIT pass makes to them before those edits are committed. Reads the
|
|
@@ -453,6 +476,12 @@ export function buildGateDeps(params) {
|
|
|
453
476
|
// ("return 401 so the verification test passes") become rule-4c
|
|
454
477
|
// findings so the child verifies the real requirement, not the check.
|
|
455
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))),
|
|
456
485
|
// Deterministic prohibition probe: paths the spec forbids modifying
|
|
457
486
|
// that the task's diff modified anyway become prompt-level findings
|
|
458
487
|
// under the no-waiver rule — the child otherwise rarely runs `git
|
|
@@ -516,7 +545,14 @@ export function buildGateDeps(params) {
|
|
|
516
545
|
const r = await git(cwd2, args, signal);
|
|
517
546
|
return { exitCode: r.exitCode, stdout: r.stdout };
|
|
518
547
|
},
|
|
519
|
-
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))
|
|
520
556
|
});
|
|
521
557
|
},
|
|
522
558
|
// Deterministic static check + tree helpers for the enforce pre-commit gate.
|
package/dist/task/lint-fix.d.ts
CHANGED
|
@@ -11,10 +11,13 @@ export interface LintFixDeps {
|
|
|
11
11
|
failReason: string;
|
|
12
12
|
/** Run the fix child; same closure shape the other gate children use. */
|
|
13
13
|
runChild: (tools: string, prompt: string, signal?: AbortSignal) => Promise<string>;
|
|
14
|
-
/** The deterministic whole-repo static check to converge against.
|
|
14
|
+
/** The deterministic whole-repo static check to converge against. `output`
|
|
15
|
+
* (the failing command's captured text, when the impl provides it) lets the
|
|
16
|
+
* non-convergence path trace the findings to a spec-frozen path. */
|
|
15
17
|
repoHealth: () => Promise<{
|
|
16
18
|
ok: boolean;
|
|
17
19
|
reason: string;
|
|
20
|
+
output?: string;
|
|
18
21
|
}>;
|
|
19
22
|
/** Run git in cwd; injected so the guard logic is unit-testable. */
|
|
20
23
|
git: (args: string[]) => Promise<{
|
|
@@ -28,6 +31,17 @@ export interface LintFixDeps {
|
|
|
28
31
|
* and the fix reported not-applied. Absent/empty → no-op, prior behavior.
|
|
29
32
|
*/
|
|
30
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>;
|
|
31
45
|
}
|
|
32
46
|
/** The fix child edits and runs the checker; bash exists to RUN the check, not git. */
|
|
33
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
|
-
import { parseChangedFrozenFiles, revertFrozenPaths } from './frozen-path-guard.js';
|
|
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
|
|
@@ -251,6 +313,25 @@ export async function runBoundedLintFix(deps) {
|
|
|
251
313
|
}
|
|
252
314
|
const health = await deps.repoHealth();
|
|
253
315
|
if (!health.ok) {
|
|
316
|
+
// FROZEN-PATH TRACE on non-convergence (PROMPT 1 layer B): when the child
|
|
317
|
+
// was honest — it did NOT touch the frozen path, so the guard above never
|
|
318
|
+
// tripped — but the check is still red and its own output NAMES a frozen
|
|
319
|
+
// path (typed ESLint: "playwright/index.ts was not found by the project …
|
|
320
|
+
// consider including it in the tsconfig.json"), the findings can only be
|
|
321
|
+
// fixed by an edit this task's spec forbids. Report it under the same
|
|
322
|
+
// `frozen-path:` prefix as the guard trip, so the gate loop can route
|
|
323
|
+
// straight to the human picker instead of burning unattended AUTOFIX
|
|
324
|
+
// rounds an impl re-run under the same freeze cannot converge out of.
|
|
325
|
+
const implicated = frozen.filter(p => pathNamedIn(`${health.reason}\n${health.output ?? ''}`, p));
|
|
326
|
+
if (implicated.length > 0) {
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
reason: `frozen-path: static findings implicate spec-frozen path(s) `
|
|
330
|
+
+ `(${implicated.slice(0, 3).join(', ')}`
|
|
331
|
+
+ `${implicated.length > 3 ? ', …' : ''}) — did not converge `
|
|
332
|
+
+ `(${health.reason}); a fix under this task's constraints cannot converge`
|
|
333
|
+
};
|
|
334
|
+
}
|
|
254
335
|
return { ok: false, reason: `did not converge: ${health.reason}` };
|
|
255
336
|
}
|
|
256
337
|
return { ok: true, reason: guardNote };
|
package/dist/task/phases.d.ts
CHANGED
|
@@ -117,7 +117,7 @@ export interface PhaseAutoAnswerDeps {
|
|
|
117
117
|
export declare function phaseAutoAnswer(deps: PhaseDeps, refined: string, research: string, question: string, autoDeps?: PhaseAutoAnswerDeps): Promise<AutoAnswer>;
|
|
118
118
|
export declare function phaseGrill(deps: PhaseDeps, ctx: ExtensionCommandContext, widgetState: WidgetState, refined: string, research: string): Promise<string>;
|
|
119
119
|
export declare function phaseCompose(deps: PhaseDeps, refined: string, research: string, qa: string): Promise<string>;
|
|
120
|
-
export declare function phaseCritique(deps: PhaseDeps, spec: string, refined: string, qa: string, planContext?: string): Promise<string>;
|
|
120
|
+
export declare function phaseCritique(deps: PhaseDeps, spec: string, refined: string, qa: string, planContext?: string, research?: string): Promise<string>;
|
|
121
121
|
export declare function critiqueWithFallback(d: PhaseDeps, p: PhaseContext): Promise<string>;
|
|
122
122
|
export declare const PHASES: PhaseConfig[];
|
|
123
123
|
export declare function postCommitPhase(phase: PhaseConfig, deps: PhaseDeps, pc: PhaseContext, out: string): Promise<void>;
|
package/dist/task/phases.js
CHANGED
|
@@ -29,6 +29,7 @@ import { parseVerifyBlock, validateSpecShape, stripSpecPreamble, isCritiqueClean
|
|
|
29
29
|
import { findSkipEscapes, skipEscapeDefectText } from './skip-escape.js';
|
|
30
30
|
import { findSynthesizedWiring, wiringProbeText, readReferencedDocs } from './wiring-claims.js';
|
|
31
31
|
import { findAbsenceConflicts, absenceProbeText, siblingTitlesFromPlanContext } from './verify-reconcile.js';
|
|
32
|
+
import { findFrozenPathConflicts, frozenConflictProbeText } from './frozen-conflict.js';
|
|
32
33
|
import { existsSync } from 'node:fs';
|
|
33
34
|
import { readContracts, buildContractsBlock, buildContractsVerifyBlock } from './contracts.js';
|
|
34
35
|
import { readRequirements, buildRequirementsBlock } from './requirements.js';
|
|
@@ -791,7 +792,7 @@ export async function phaseCompose(deps, refined, research, qa) {
|
|
|
791
792
|
return { ok: true, value: stripped };
|
|
792
793
|
}, problem => new Error(`compose_invalid: ${problem}`));
|
|
793
794
|
}
|
|
794
|
-
export async function phaseCritique(deps, spec, refined, qa, planContext) {
|
|
795
|
+
export async function phaseCritique(deps, spec, refined, qa, planContext, research) {
|
|
795
796
|
// Fast triage before the expensive full rewrite. The rewrite regenerates
|
|
796
797
|
// the entire spec from scratch and is the costliest tail of the pipeline
|
|
797
798
|
// (observed up to ~240s). Most compose drafts are already good, so we first
|
|
@@ -850,6 +851,21 @@ export async function phaseCritique(deps, spec, refined, qa, planContext) {
|
|
|
850
851
|
deps.logDebug?.('plan-contradiction flagged in VERIFY: '
|
|
851
852
|
+ absenceConflicts.map(c => `${c.assertion.target} (${c.against})`).join(' | '));
|
|
852
853
|
}
|
|
854
|
+
// DETERMINISTIC unsatisfiable-pair probe (mx5 run 12 root cause): a blanket
|
|
855
|
+
// frozen path ("Do NOT modify `tsconfig.json` … handled in steps 1–2") whose
|
|
856
|
+
// registration edit the spec's OWN body — or the task's RESEARCH the spec was
|
|
857
|
+
// composed from (live drafts sometimes drop the nuance while shipping the
|
|
858
|
+
// freeze and the creation) — says the deliverable requires ("must also be
|
|
859
|
+
// included …"). Shipped as-is, the created files turn the repo-wide static
|
|
860
|
+
// check permanently red and no task is allowed to fix it — every later task
|
|
861
|
+
// burns its AUTOFIX rounds on it. Forced into the rewrite like the other
|
|
862
|
+
// probes: the rewrite must grant scoped ownership or drop the creation.
|
|
863
|
+
const frozenConflicts = findFrozenPathConflicts(spec, research);
|
|
864
|
+
const frozenProbe = frozenConflicts.length > 0 ? frozenConflictProbeText(frozenConflicts) : null;
|
|
865
|
+
if (frozenProbe) {
|
|
866
|
+
deps.logDebug?.('unsatisfiable freeze/requires-edit pair flagged in spec: '
|
|
867
|
+
+ frozenConflicts.map(c => c.path).join(' | '));
|
|
868
|
+
}
|
|
853
869
|
let triageDefects = null;
|
|
854
870
|
if (parseVerifyBlock(spec) !== null) {
|
|
855
871
|
const tTriage = Date.now();
|
|
@@ -866,12 +882,15 @@ export async function phaseCritique(deps, spec, refined, qa, planContext) {
|
|
|
866
882
|
}
|
|
867
883
|
deps.recordSubStep?.('triage', Date.now() - tTriage);
|
|
868
884
|
if (verdict !== null) {
|
|
869
|
-
// A deterministic skip-escape, synthesized-wiring,
|
|
870
|
-
// finding overrides a CLEAN triage: the draft must
|
|
871
|
-
// it even if the model judged the rest clean
|
|
872
|
-
// self-discover any of them reliably).
|
|
885
|
+
// A deterministic skip-escape, synthesized-wiring, plan-contradiction,
|
|
886
|
+
// or unsatisfiable-pair finding overrides a CLEAN triage: the draft must
|
|
887
|
+
// be rewritten to resolve it even if the model judged the rest clean
|
|
888
|
+
// (the model does not self-discover any of them reliably).
|
|
873
889
|
if (isCritiqueClean(verdict)) {
|
|
874
|
-
if (skipDefects === null
|
|
890
|
+
if (skipDefects === null
|
|
891
|
+
&& wiringProbe === null
|
|
892
|
+
&& absenceProbe === null
|
|
893
|
+
&& frozenProbe === null) {
|
|
875
894
|
return spec;
|
|
876
895
|
}
|
|
877
896
|
}
|
|
@@ -881,8 +900,11 @@ export async function phaseCritique(deps, spec, refined, qa, planContext) {
|
|
|
881
900
|
}
|
|
882
901
|
}
|
|
883
902
|
// Merge the deterministic skip-escape + synthesized-wiring + plan-contradiction
|
|
884
|
-
// defects with any triage defects for the rewrite (all are
|
|
885
|
-
|
|
903
|
+
// + unsatisfiable-pair defects with any triage defects for the rewrite (all are
|
|
904
|
+
// forced FOCUS items).
|
|
905
|
+
const rewriteDefects = [skipDefects, wiringProbe, absenceProbe, frozenProbe, triageDefects]
|
|
906
|
+
.filter(Boolean)
|
|
907
|
+
.join('\n\n') || null;
|
|
886
908
|
const tRewrite = Date.now();
|
|
887
909
|
try {
|
|
888
910
|
return await runWithEmphasisRetry(deps, 'critique', 'read', problem => CRITIQUE_PROMPT(spec, refined, qa, problem !== null, rewriteDefects, contractsBlock), text => {
|
|
@@ -902,7 +924,7 @@ export async function phaseCritique(deps, spec, refined, qa, planContext) {
|
|
|
902
924
|
// ─── Critique with fallback ──────────────────────────────────────────────────
|
|
903
925
|
export async function critiqueWithFallback(d, p) {
|
|
904
926
|
try {
|
|
905
|
-
return await phaseCritique(d, p.spec, p.refined, p.qa, p.planContext);
|
|
927
|
+
return await phaseCritique(d, p.spec, p.refined, p.qa, p.planContext, p.research);
|
|
906
928
|
}
|
|
907
929
|
catch (err) {
|
|
908
930
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -36,6 +36,13 @@ export interface Prohibition {
|
|
|
36
36
|
* against the EXACT wording — including any exception clause it states. */
|
|
37
37
|
constraint: string;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Does this line express a modification ban? Matches the active forms ("do not
|
|
41
|
+
* modify", "must not touch", "never edit", "don't change") and the passive form
|
|
42
|
+
* ("must not be modified"). Deliberately verb-scoped to modification — a "do not
|
|
43
|
+
* add a dependency" style rule names no path and is the prompt rule's job.
|
|
44
|
+
*/
|
|
45
|
+
export declare const PROHIBITION_RE: RegExp;
|
|
39
46
|
/**
|
|
40
47
|
* Extract the concrete paths the spec explicitly forbids modifying: every
|
|
41
48
|
* backtick-quoted path-like token on a line that expresses a modification ban.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* ("must not be modified"). Deliberately verb-scoped to modification — a "do not
|
|
5
5
|
* add a dependency" style rule names no path and is the prompt rule's job.
|
|
6
6
|
*/
|
|
7
|
-
const PROHIBITION_RE = /\b(?:do\s+not|don'?t|must\s+not|never)\s+(?:be\s+)?(?:modify|modified|touch|touched|edit|edited|change|changed|alter|altered|rewrite|rewritten|overwrite|overwritten|delete|deleted|remove|removed)\b/i;
|
|
7
|
+
export const PROHIBITION_RE = /\b(?:do\s+not|don'?t|must\s+not|never)\s+(?:be\s+)?(?:modify|modified|touch|touched|edit|edited|change|changed|alter|altered|rewrite|rewritten|overwrite|overwritten|delete|deleted|remove|removed)\b/i;
|
|
8
8
|
/**
|
|
9
9
|
* A backtick token counts as a path only when it is whitespace-free, uses path
|
|
10
10
|
* characters, and either contains a directory separator, has a file extension,
|
|
@@ -135,6 +135,25 @@ export interface GateDeps {
|
|
|
135
135
|
* gate re-checks and surfaces it like an accept-debt. Best-effort; absent in tests.
|
|
136
136
|
*/
|
|
137
137
|
recordEnforceRevertDebt?: (cwd: string, taskId: string, reason: string) => Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* Record a durable FROZEN-BLOCKED debt (mx5 run 12 / PROMPT 1 layer B): a
|
|
140
|
+
* repo-health FAIL whose only fix is an edit to a path THIS task's spec froze —
|
|
141
|
+
* a cross-task contradiction. Recorded when the loop routes such a FAIL to the
|
|
142
|
+
* picker (whatever the human then picks, the defect is real and no task may fix
|
|
143
|
+
* it), so the final gate re-checks it at run end. Best-effort; absent in tests.
|
|
144
|
+
*/
|
|
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>;
|
|
138
157
|
/**
|
|
139
158
|
* The concrete paths this task's spec forbids modifying (its `Do NOT modify`
|
|
140
159
|
* CONSTRAINTS — see frozen-path-guard.ts / prohibition-probe.ts). Used to
|
package/dist/task/task-gates.js
CHANGED
|
@@ -89,6 +89,15 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
89
89
|
// auto attempts still FAIL, the picker returns so a person can break the loop.
|
|
90
90
|
let lintFixAttempted = false;
|
|
91
91
|
let autoFixCount = 0;
|
|
92
|
+
// Set when the bounded lint-fix reports the `frozen-path:` rejection: the
|
|
93
|
+
// repo-health FAIL can only be fixed by editing a path THIS task's spec
|
|
94
|
+
// froze (mx5 run 12's unsatisfiable registration pair). An impl re-run is
|
|
95
|
+
// under the same freeze — rule 4b fails the task if it complies with the
|
|
96
|
+
// linter — so unattended AUTOFIX rounds CANNOT converge and are skipped;
|
|
97
|
+
// the picker is forced with the cross-task contradiction named, and the
|
|
98
|
+
// defect is recorded as a durable debt for the final gate.
|
|
99
|
+
let frozenContradiction = null;
|
|
100
|
+
let frozenDebtRecorded = false;
|
|
92
101
|
while (!verified.ok) {
|
|
93
102
|
const failReason = verified.reason ?? 'did not verify';
|
|
94
103
|
// GRADUATED resolution: a repo-health FAIL (pure static findings) gets ONE
|
|
@@ -107,6 +116,9 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
107
116
|
await rec(verdictLine(verified));
|
|
108
117
|
continue;
|
|
109
118
|
}
|
|
119
|
+
if ((fix.reason ?? '').startsWith('frozen-path:')) {
|
|
120
|
+
frozenContradiction = fix.reason ?? null;
|
|
121
|
+
}
|
|
110
122
|
}
|
|
111
123
|
// UNOBSERVED (rule 5c): a spec-required behavioral check could not run because
|
|
112
124
|
// its observation tooling is absent. An unattended AUTOFIX re-run cannot install
|
|
@@ -114,14 +126,48 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
114
126
|
// decision (provision the tool, or accept the unproven behavior) is the human's.
|
|
115
127
|
// Skip the (moot) recommendation research and force the picker.
|
|
116
128
|
const isUnobserved = verified.unobserved === true;
|
|
129
|
+
// FROZEN-BLOCKED (cross-task contradiction): skip the recommendation
|
|
130
|
+
// research too — it would only re-derive what the deterministic lint-fix
|
|
131
|
+
// rejection already proved. The picker shows the contradiction; ACCEPT
|
|
132
|
+
// records the (already-recorded) defect as the human's call. Applies
|
|
133
|
+
// only while the FAIL is still the repo-health one the contradiction
|
|
134
|
+
// explains — a later, different FAIL gets the ordinary resolution path.
|
|
135
|
+
const isFrozenBlocked = frozenContradiction !== null && failReason.startsWith('repo health:');
|
|
117
136
|
const recOutcome = isUnobserved ? { recommend: 'autofix', rationale: failReason }
|
|
118
|
-
:
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
: isFrozenBlocked ?
|
|
138
|
+
{
|
|
139
|
+
recommend: 'accept',
|
|
140
|
+
rationale: `${frozenContradiction} — the failing static check can only be fixed by `
|
|
141
|
+
+ `editing a path this task's spec freezes (a cross-task contradiction: `
|
|
142
|
+
+ `the spec forbids the very edit the repo needs; the "owning" earlier `
|
|
143
|
+
+ `step already completed). An implementation re-run under the same `
|
|
144
|
+
+ `freeze cannot converge. ACCEPT records it as durable debt the final `
|
|
145
|
+
+ `gate re-checks; fixing it needs a plan-level change, not a re-run.`
|
|
146
|
+
}
|
|
147
|
+
: deps.recommend ?
|
|
148
|
+
await deps.recommend(active, p.cwd, p.title, p.taskId, failReason)
|
|
149
|
+
: { recommend: 'autofix', rationale: failReason };
|
|
121
150
|
await rec(isUnobserved ?
|
|
122
151
|
'resolution: verify UNOBSERVED — spec-required check could not run (tooling absent); '
|
|
123
152
|
+ 'forcing the human picker, an unattended re-run cannot provision it'
|
|
124
|
-
:
|
|
153
|
+
: isFrozenBlocked ?
|
|
154
|
+
'resolution: repo-health FAIL is blocked by spec-frozen path(s) — cross-task '
|
|
155
|
+
+ 'contradiction; unattended AUTOFIX skipped (an impl re-run under the same '
|
|
156
|
+
+ 'freeze cannot converge), forcing the human picker'
|
|
157
|
+
: `resolution: recommended ${recOutcome.recommend.toUpperCase()}`);
|
|
158
|
+
if (isFrozenBlocked && !frozenDebtRecorded) {
|
|
159
|
+
frozenDebtRecorded = true;
|
|
160
|
+
// Durable regardless of what the human picks next: the contradiction
|
|
161
|
+
// is real, cross-task, and outside this task's power to fix — the
|
|
162
|
+
// final gate must surface it at run end (static-class: it auto-closes
|
|
163
|
+
// iff the run-end static check passes).
|
|
164
|
+
try {
|
|
165
|
+
await deps.recordFrozenBlockedDebt?.(p.cwd, p.taskId, `${failReason} — ${frozenContradiction}`);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// recording must never break the gate sequence
|
|
169
|
+
}
|
|
170
|
+
}
|
|
125
171
|
// AUTO-RESOLVE the AUTOFIX path: when the research says the work is
|
|
126
172
|
// genuinely wrong, re-run the fix WITHOUT prompting the user. The picker is
|
|
127
173
|
// reserved for the ACCEPT recommendation (the human decides whether to bless
|
|
@@ -129,6 +175,7 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
129
175
|
// MAX_AUTO_AUTOFIX consecutive unattended attempts that still FAIL, hand
|
|
130
176
|
// control back so a person can break a non-converging loop.
|
|
131
177
|
const autoFixNow = !isUnobserved
|
|
178
|
+
&& !isFrozenBlocked
|
|
132
179
|
&& recOutcome.recommend === 'autofix'
|
|
133
180
|
&& autoFixCount < MAX_AUTO_AUTOFIX;
|
|
134
181
|
let choice;
|
|
@@ -151,11 +198,27 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
151
198
|
// defect ships and nothing else revisits it (mx5 run 4 B3 / run 8
|
|
152
199
|
// TASK_0012). Record it to the run ledger; the final integration gate
|
|
153
200
|
// re-checks it at run end and surfaces it if still open. Best-effort.
|
|
154
|
-
|
|
155
|
-
|
|
201
|
+
// The frozen-blocked routing above already recorded this defect (with
|
|
202
|
+
// the contradiction named) — don't double-enter it in the ledger.
|
|
203
|
+
if (!frozenDebtRecorded) {
|
|
204
|
+
try {
|
|
205
|
+
await deps.recordAcceptDebt?.(p.cwd, p.taskId, failReason);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// recording must never break the gate sequence
|
|
209
|
+
}
|
|
156
210
|
}
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
}
|
|
159
222
|
}
|
|
160
223
|
active.ui.notify(`${p.tag}: accepted "${p.title}" despite verify FAIL (${failReason.slice(0, 120)}) — proceeding.`, 'warning');
|
|
161
224
|
break;
|
|
@@ -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
|
+
}
|