@mjasnikovs/pi-task 0.40.50 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/config/config.d.ts +61 -18
- package/dist/config/config.js +50 -6
- package/dist/config/register.js +18 -5
- package/dist/shared/content-tokens.d.ts +16 -0
- package/dist/shared/content-tokens.js +137 -0
- package/dist/task/accept-debt.d.ts +15 -1
- package/dist/task/accept-debt.js +4 -0
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +59 -7
- package/dist/task/auto-io.js +140 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +126 -112
- package/dist/task/child-runner.d.ts +18 -0
- package/dist/task/child-runner.js +7 -8
- package/dist/task/command-run.d.ts +4 -0
- package/dist/task/command-run.js +4 -2
- package/dist/task/constraint-policy.d.ts +52 -0
- package/dist/task/constraint-policy.js +66 -0
- package/dist/task/context-silence.d.ts +5 -4
- package/dist/task/context-silence.js +13 -5
- package/dist/task/coverage-loop.d.ts +7 -4
- package/dist/task/coverage-loop.js +16 -138
- package/dist/task/debug-log.d.ts +25 -2
- package/dist/task/debug-log.js +64 -10
- package/dist/task/decompose-fidelity.d.ts +17 -4
- package/dist/task/decompose-fidelity.js +95 -55
- package/dist/task/decompose-granularity.d.ts +7 -7
- package/dist/task/decompose-granularity.js +8 -8
- package/dist/task/env-notes.d.ts +65 -19
- package/dist/task/env-notes.js +152 -50
- package/dist/task/external-context.js +25 -5
- package/dist/task/fix-context.d.ts +39 -0
- package/dist/task/fix-context.js +58 -0
- package/dist/task/gate-deps.d.ts +49 -0
- package/dist/task/gate-deps.js +199 -38
- package/dist/task/gate-evidence.d.ts +51 -0
- package/dist/task/gate-evidence.js +90 -0
- package/dist/task/gate-resolution.d.ts +119 -0
- package/dist/task/gate-resolution.js +120 -0
- package/dist/task/git-state-guard.js +3 -26
- package/dist/task/handoff.d.ts +35 -0
- package/dist/task/handoff.js +100 -0
- package/dist/task/health-baseline.d.ts +87 -0
- package/dist/task/health-baseline.js +158 -0
- package/dist/task/ledger.d.ts +10 -0
- package/dist/task/ledger.js +22 -11
- package/dist/task/lint-fix.d.ts +11 -0
- package/dist/task/lint-fix.js +14 -8
- package/dist/task/loop-detector.d.ts +52 -3
- package/dist/task/loop-detector.js +81 -5
- package/dist/task/mentions.d.ts +17 -0
- package/dist/task/mentions.js +86 -0
- package/dist/task/orchestrator.d.ts +53 -9
- package/dist/task/orchestrator.js +104 -28
- package/dist/task/orientation.d.ts +135 -29
- package/dist/task/orientation.js +259 -67
- package/dist/task/owned-freeze-reassign.d.ts +7 -3
- package/dist/task/owned-freeze-reassign.js +13 -5
- package/dist/task/parsers.d.ts +6 -1
- package/dist/task/parsers.js +21 -4
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +125 -153
- package/dist/task/plan-orchestrator.js +5 -6
- package/dist/task/plan-rounds.d.ts +1 -0
- package/dist/task/plan-rounds.js +8 -1
- package/dist/task/prohibition-probe.d.ts +6 -1
- package/dist/task/prohibition-probe.js +26 -6
- package/dist/task/prompts.js +13 -2
- package/dist/task/qa-transcript.d.ts +17 -0
- package/dist/task/qa-transcript.js +28 -0
- package/dist/task/question-source.d.ts +16 -2
- package/dist/task/question-source.js +21 -8
- package/dist/task/repo-health-check.d.ts +27 -0
- package/dist/task/repo-health-check.js +39 -5
- package/dist/task/requirements.d.ts +67 -13
- package/dist/task/requirements.js +129 -67
- package/dist/task/research-worker.d.ts +43 -2
- package/dist/task/research-worker.js +103 -8
- package/dist/task/run-bracket.d.ts +9 -4
- package/dist/task/run-bracket.js +9 -0
- package/dist/task/run-context.d.ts +152 -0
- package/dist/task/run-context.js +277 -0
- package/dist/task/spec-doc.d.ts +78 -0
- package/dist/task/spec-doc.js +205 -0
- package/dist/task/spec-model.d.ts +70 -0
- package/dist/task/spec-model.js +139 -0
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +9 -5
- package/dist/task/state-dir.d.ts +29 -0
- package/dist/task/state-dir.js +80 -0
- package/dist/task/suppression-probe.d.ts +67 -0
- package/dist/task/suppression-probe.js +78 -0
- package/dist/task/task-gates.d.ts +39 -57
- package/dist/task/task-gates.js +115 -195
- package/dist/task/task-io.d.ts +10 -0
- package/dist/task/task-io.js +69 -18
- package/dist/task/task-parsers.js +6 -6
- package/dist/task/task-types.d.ts +7 -0
- package/dist/task/timings.d.ts +11 -0
- package/dist/task/timings.js +21 -0
- package/dist/task/tree-hash.d.ts +8 -0
- package/dist/task/tree-hash.js +55 -0
- package/dist/task/verify-reconcile.d.ts +4 -3
- package/dist/task/verify-reconcile.js +7 -8
- package/dist/task/verify-resolution.d.ts +28 -5
- package/dist/task/verify-resolution.js +70 -13
- package/dist/task/verify-work.d.ts +98 -35
- package/dist/task/verify-work.js +227 -60
- package/dist/task/yolo.d.ts +0 -13
- package/dist/task/yolo.js +9 -21
- package/dist/workers/pi-worker-core.d.ts +20 -0
- package/dist/workers/pi-worker-core.js +16 -11
- package/dist/workers/pi-worker-docs.d.ts +6 -5
- package/dist/workers/pi-worker-docs.js +8 -7
- package/dist/workers/pi-worker-fetch.d.ts +3 -2
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +5 -4
- package/dist/workers/research-cache.d.ts +23 -1
- package/dist/workers/research-cache.js +63 -3
- package/dist/workers/shared.d.ts +18 -0
- package/dist/workers/shared.js +38 -22
- package/package.json +1 -1
package/dist/task/timings.js
CHANGED
|
@@ -36,3 +36,24 @@ export function formatTimings(entries) {
|
|
|
36
36
|
lines.push(`${'total'.padEnd(TOP_LABEL_WIDTH)}${formatMs(total).padStart(MS_COLUMN_WIDTH)}`);
|
|
37
37
|
return lines.join('\n');
|
|
38
38
|
}
|
|
39
|
+
/** Heading of one attempt's block inside the `## phase timings` section. */
|
|
40
|
+
const ATTEMPT_HEADING_RE = /^attempt (\d+)$/gm;
|
|
41
|
+
/**
|
|
42
|
+
* The `## phase timings` body once this attempt's phases are added to it.
|
|
43
|
+
*
|
|
44
|
+
* A run re-entered for an autofix resumes at `phase: done` and therefore runs no
|
|
45
|
+
* phases at all, so rendering only what IT timed wrote an empty block over the
|
|
46
|
+
* five phases the first attempt measured — the timings vanished for exactly the
|
|
47
|
+
* tasks that took longest. Each attempt gets its own labelled block instead, and
|
|
48
|
+
* an unlabelled body written before this grammar is adopted as attempt 1 rather
|
|
49
|
+
* than discarded.
|
|
50
|
+
*/
|
|
51
|
+
export function mergeTimings(prev, next) {
|
|
52
|
+
const before = (prev ?? '').trim();
|
|
53
|
+
if (before.length === 0)
|
|
54
|
+
return `attempt 1\n\n${formatTimings(next)}`;
|
|
55
|
+
const seen = [...before.matchAll(ATTEMPT_HEADING_RE)];
|
|
56
|
+
const body = seen.length === 0 ? `attempt 1\n\n${before}` : before;
|
|
57
|
+
const last = seen.length === 0 ? 1 : parseInt(seen[seen.length - 1][1], 10);
|
|
58
|
+
return `${body}\n\nattempt ${last + 1}\n\n${formatTimings(next)}`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GitRunner } from '../shared/git-runner.js';
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot the worktree content into a tree object. Returns null when git cannot
|
|
4
|
+
* build the tree (an unborn HEAD, a non-repo cwd, a missing binary).
|
|
5
|
+
*/
|
|
6
|
+
export declare function worktreeTreeHash(git: GitRunner): Promise<string | null>;
|
|
7
|
+
/** The tree object a commit-ish points at — the committed twin of the above. */
|
|
8
|
+
export declare function commitTreeHash(git: GitRunner, revision: string): Promise<string | null>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tree-hash — the content identity of the working tree, computed without touching
|
|
3
|
+
* the real index or the working tree.
|
|
4
|
+
*
|
|
5
|
+
* `git add -A` against a THROWAWAY `GIT_INDEX_FILE` is the whole technique: the
|
|
6
|
+
* staged state a user (or a half-finished gate) left in the real index survives,
|
|
7
|
+
* nothing is stashed, and no file moves. Every consumer that asks "is this the
|
|
8
|
+
* same tree as before?" — the git-state guard's reconcile, the health baseline,
|
|
9
|
+
* and the evidence cache when it lands — must ask it the same way, or two answers
|
|
10
|
+
* disagree about the same tree.
|
|
11
|
+
*
|
|
12
|
+
* `.pi-tasks` is excluded because the gate machinery writes its own trail there
|
|
13
|
+
* DURING the work being hashed; including it would make every hash differ from
|
|
14
|
+
* every other one for reasons that have nothing to do with the code.
|
|
15
|
+
*
|
|
16
|
+
* Null means "git could not tell us", never "empty tree": a caller that treats an
|
|
17
|
+
* unreadable tree as a match would call two different trees identical.
|
|
18
|
+
*/
|
|
19
|
+
import * as fsp from 'node:fs/promises';
|
|
20
|
+
import * as os from 'node:os';
|
|
21
|
+
import * as path from 'node:path';
|
|
22
|
+
/** Keep the gate machinery's own artifacts out of the hash. */
|
|
23
|
+
const EXCLUDE_TASKS_DIR = ':(exclude).pi-tasks';
|
|
24
|
+
function throwawayIndexPath() {
|
|
25
|
+
return path.join(os.tmpdir(), `pi-task-index-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Snapshot the worktree content into a tree object. Returns null when git cannot
|
|
29
|
+
* build the tree (an unborn HEAD, a non-repo cwd, a missing binary).
|
|
30
|
+
*/
|
|
31
|
+
export async function worktreeTreeHash(git) {
|
|
32
|
+
const tmpIndex = throwawayIndexPath();
|
|
33
|
+
const env = { GIT_INDEX_FILE: tmpIndex };
|
|
34
|
+
try {
|
|
35
|
+
const empty = await git(['read-tree', '--empty'], env);
|
|
36
|
+
if (empty.exitCode !== 0)
|
|
37
|
+
return null;
|
|
38
|
+
const add = await git(['add', '-A', '--', '.', EXCLUDE_TASKS_DIR], env);
|
|
39
|
+
if (add.exitCode !== 0)
|
|
40
|
+
return null;
|
|
41
|
+
const tree = await git(['write-tree'], env);
|
|
42
|
+
return tree.exitCode === 0 ? tree.stdout.trim() : null;
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await fsp.rm(tmpIndex, { force: true }).catch(() => { });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** The tree object a commit-ish points at — the committed twin of the above. */
|
|
49
|
+
export async function commitTreeHash(git, revision) {
|
|
50
|
+
const r = await git(['rev-parse', `${revision}^{tree}`]);
|
|
51
|
+
if (r.exitCode !== 0)
|
|
52
|
+
return null;
|
|
53
|
+
const sha = r.stdout.trim();
|
|
54
|
+
return sha.length > 0 ? sha : null;
|
|
55
|
+
}
|
|
@@ -35,7 +35,8 @@ export declare function findAbsenceConflicts(spec: string, ctx: AbsenceConflictC
|
|
|
35
35
|
* each conflict, why it is one, and the reconciliation to apply. */
|
|
36
36
|
export declare function absenceProbeText(conflicts: AbsenceConflict[]): string;
|
|
37
37
|
/** Parse SIBLING titles out of `buildScopeFence`'s listing, whose rows read
|
|
38
|
-
* `[N]
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* `[N] [done|this|later] head`. The `[this]` row is dropped — a task may
|
|
39
|
+
* legitimately assert about its own deliverables; a `[done]` sibling is kept,
|
|
40
|
+
* since its deliverable is in the tree and asserting its ABSENCE is the very
|
|
41
|
+
* defect this feeds. '' and undefined (a bare /task with no plan) yield none. */
|
|
41
42
|
export declare function siblingTitlesFromPlanContext(planContext: string | undefined): string[];
|
|
@@ -184,20 +184,19 @@ export function absenceProbeText(conflicts) {
|
|
|
184
184
|
].join('\n');
|
|
185
185
|
}
|
|
186
186
|
/** Parse SIBLING titles out of `buildScopeFence`'s listing, whose rows read
|
|
187
|
-
* `[N]
|
|
188
|
-
*
|
|
189
|
-
*
|
|
187
|
+
* `[N] [done|this|later] head`. The `[this]` row is dropped — a task may
|
|
188
|
+
* legitimately assert about its own deliverables; a `[done]` sibling is kept,
|
|
189
|
+
* since its deliverable is in the tree and asserting its ABSENCE is the very
|
|
190
|
+
* defect this feeds. '' and undefined (a bare /task with no plan) yield none. */
|
|
190
191
|
export function siblingTitlesFromPlanContext(planContext) {
|
|
191
192
|
if (!planContext)
|
|
192
193
|
return [];
|
|
193
194
|
const out = [];
|
|
194
195
|
for (const line of planContext.split('\n')) {
|
|
195
|
-
const m = /^\[\d+\](.*)$/.exec(line.trim());
|
|
196
|
-
if (!m)
|
|
196
|
+
const m = /^\[\d+\] \[(done|this|later)\] (.*)$/.exec(line.trim());
|
|
197
|
+
if (!m || m[1] === 'this')
|
|
197
198
|
continue;
|
|
198
|
-
|
|
199
|
-
continue;
|
|
200
|
-
const title = m[1].trim();
|
|
199
|
+
const title = m[2].trim();
|
|
201
200
|
if (title.length > 0)
|
|
202
201
|
out.push(title);
|
|
203
202
|
}
|
|
@@ -1,30 +1,50 @@
|
|
|
1
|
+
import type { SpecContradiction } from './gate-resolution.js';
|
|
1
2
|
/** The observe-only contract handed to the child. The same string as
|
|
2
3
|
* verify-work's VERIFY_TOOLS, so both passes are told the same thing. */
|
|
3
4
|
declare const RESOLUTION_TOOLS = "read,bash";
|
|
4
5
|
/** Which card the picker tints RECOMMENDED. */
|
|
5
6
|
export type ResolutionRecommendation = 'autofix' | 'accept';
|
|
7
|
+
/**
|
|
8
|
+
* The marker a judge emits INSTEAD of a recommendation when it finds the two
|
|
9
|
+
* gates contradict each other: the acceptance criterion can only be met by
|
|
10
|
+
* editing a path the same spec freezes.
|
|
11
|
+
*
|
|
12
|
+
* It exists because the judge was previously offered AUTOFIX or ACCEPT and
|
|
13
|
+
* nothing else, so a contradiction it had already diagnosed came back as
|
|
14
|
+
* "AUTOFIX" and the loop spent its whole budget re-deriving it (0053).
|
|
15
|
+
*/
|
|
16
|
+
export declare const BLOCKED_BY_FROZEN = "BLOCKED-BY-FROZEN";
|
|
6
17
|
export interface ResolutionOutcome {
|
|
7
18
|
/** The card to recommend. Defaults to 'autofix' (conservative: re-do, don't
|
|
8
19
|
* bless) when the research could not run or emitted no verdict. */
|
|
9
20
|
recommend: ResolutionRecommendation;
|
|
10
21
|
/** Short, human-readable rationale shown under the picker. Always set. */
|
|
11
22
|
rationale: string;
|
|
23
|
+
/** Set only by the BLOCKED-BY-FROZEN marker: the criterion the spec's own
|
|
24
|
+
* freeze makes unreachable. Outranks `recommend` in the gate's decision
|
|
25
|
+
* table — no re-run and no picker answer can resolve a contradiction. */
|
|
26
|
+
contradiction?: SpecContradiction;
|
|
12
27
|
}
|
|
13
28
|
/**
|
|
14
29
|
* Build the resolution research child's prompt. Pure, so the wording is
|
|
15
30
|
* unit-tested without spawning pi. The contract it states: investigate the real
|
|
16
31
|
* workspace and end on exactly one `VERIFY-RESOLUTION:` line.
|
|
17
32
|
*/
|
|
18
|
-
export declare function buildResolutionPrompt(spec: string, failReason: string): string;
|
|
33
|
+
export declare function buildResolutionPrompt(spec: string, failReason: string, qaRecord?: string): string;
|
|
19
34
|
/**
|
|
20
|
-
* Parse the child's
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
35
|
+
* Parse the child's verdict out of the LAST `VERIFY-RESOLUTION:` marker,
|
|
36
|
+
* case-insensitively. Last match wins: the model reasons before concluding, and a
|
|
37
|
+
* bash command it runs can print the token back, so an earlier occurrence is not
|
|
38
|
+
* the verdict.
|
|
24
39
|
*
|
|
25
40
|
* No marker → AUTOFIX (re-do rather than bless), with a rationale saying the
|
|
26
41
|
* research was inconclusive. A marker with no trailing sentence gets a stock
|
|
27
42
|
* rationale, so `rationale` is never empty.
|
|
43
|
+
*
|
|
44
|
+
* A BLOCKED-BY-FROZEN marker yields a CONTRADICTION plus an `accept`
|
|
45
|
+
* recommendation, which is what the gate does with it once the contradiction is
|
|
46
|
+
* recorded as debt. A marker that names no path is not a usable contradiction and
|
|
47
|
+
* degrades to that recommendation alone.
|
|
28
48
|
*/
|
|
29
49
|
export declare function parseResolutionVerdict(text: string): ResolutionOutcome;
|
|
30
50
|
export declare const ACCEPT_VALUE = "accept";
|
|
@@ -62,6 +82,9 @@ export interface ResolutionDeps {
|
|
|
62
82
|
signal?: AbortSignal;
|
|
63
83
|
/** The composed spec the task was verified against. */
|
|
64
84
|
spec: string;
|
|
85
|
+
/** The task's rendered Q&A, which resolves each constraint's provenance tag —
|
|
86
|
+
* the same input the verify child got, so both weigh the spec alike. */
|
|
87
|
+
qaRecord?: string;
|
|
65
88
|
/** The verify gate's FAIL reason (VerifyOutcome.reason). */
|
|
66
89
|
failReason: string;
|
|
67
90
|
/** Runs the research child and returns its assistant text. Injected so the
|
|
@@ -14,10 +14,9 @@
|
|
|
14
14
|
* INVESTIGATE the real workspace: a genuine defect in the work (→ AUTOFIX), or
|
|
15
15
|
* an artifact the gate misjudged (→ ACCEPT).
|
|
16
16
|
*
|
|
17
|
-
* The recommendation
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* bound is spent, or when the FAIL is UNOBSERVED or frozen-blocked.
|
|
17
|
+
* The recommendation is one input to gate-resolution.ts's decision table, which
|
|
18
|
+
* alone decides whether a human is asked. This module never overrides itself: an
|
|
19
|
+
* ACCEPT reached here stands.
|
|
21
20
|
*
|
|
22
21
|
* Tools: `read` + `bash`, the same string as verify-work's VERIFY_TOOLS. That is
|
|
23
22
|
* a CONTRACT, not a capability — `bash` can write. gate-child.ts marks the
|
|
@@ -28,15 +27,28 @@
|
|
|
28
27
|
* rather than blessing it.
|
|
29
28
|
*/
|
|
30
29
|
import { USER_CANCELLED } from './child-runner.js';
|
|
30
|
+
import { annotateConstraints, renderConstraintPolicy } from './constraint-policy.js';
|
|
31
|
+
import { specValue } from './verify-work.js';
|
|
31
32
|
/** The observe-only contract handed to the child. The same string as
|
|
32
33
|
* verify-work's VERIFY_TOOLS, so both passes are told the same thing. */
|
|
33
34
|
const RESOLUTION_TOOLS = 'read,bash';
|
|
35
|
+
/**
|
|
36
|
+
* The marker a judge emits INSTEAD of a recommendation when it finds the two
|
|
37
|
+
* gates contradict each other: the acceptance criterion can only be met by
|
|
38
|
+
* editing a path the same spec freezes.
|
|
39
|
+
*
|
|
40
|
+
* It exists because the judge was previously offered AUTOFIX or ACCEPT and
|
|
41
|
+
* nothing else, so a contradiction it had already diagnosed came back as
|
|
42
|
+
* "AUTOFIX" and the loop spent its whole budget re-deriving it (0053).
|
|
43
|
+
*/
|
|
44
|
+
export const BLOCKED_BY_FROZEN = 'BLOCKED-BY-FROZEN';
|
|
34
45
|
/**
|
|
35
46
|
* Build the resolution research child's prompt. Pure, so the wording is
|
|
36
47
|
* unit-tested without spawning pi. The contract it states: investigate the real
|
|
37
48
|
* workspace and end on exactly one `VERIFY-RESOLUTION:` line.
|
|
38
49
|
*/
|
|
39
|
-
export function buildResolutionPrompt(spec, failReason) {
|
|
50
|
+
export function buildResolutionPrompt(spec, failReason, qaRecord) {
|
|
51
|
+
const weights = annotateConstraints(specValue(spec, qaRecord).constraints);
|
|
40
52
|
return [
|
|
41
53
|
"A strict verification gate just FAILED an AI coding agent's task and the",
|
|
42
54
|
'run paused. You are the reviewer who decides what to recommend next. The',
|
|
@@ -49,6 +61,13 @@ export function buildResolutionPrompt(spec, failReason) {
|
|
|
49
61
|
'THE TASK SPEC (its ACCEPTANCE criteria and VERIFY block were the contract):',
|
|
50
62
|
spec.trim(),
|
|
51
63
|
'',
|
|
64
|
+
...(weights === null ?
|
|
65
|
+
[]
|
|
66
|
+
: [
|
|
67
|
+
"THE SPEC'S CONSTRAINTS, BY WEIGHT (rule 5 below says what each weight is worth):",
|
|
68
|
+
weights,
|
|
69
|
+
''
|
|
70
|
+
]),
|
|
52
71
|
"THE GATE'S FAILURE REASON:",
|
|
53
72
|
failReason.trim(),
|
|
54
73
|
'',
|
|
@@ -86,25 +105,46 @@ export function buildResolutionPrompt(spec, failReason) {
|
|
|
86
105
|
' when you run it — that is AUTOFIX. When you cannot positively confirm the',
|
|
87
106
|
' artifact works, recommend AUTOFIX: re-doing the work is the safe default;',
|
|
88
107
|
' blessing broken work is the failure mode this whole gate exists to stop.',
|
|
108
|
+
'4. If the ONLY edit that could satisfy the failing criterion is to a file the',
|
|
109
|
+
" spec's CONSTRAINTS forbid this task from modifying, neither verdict above",
|
|
110
|
+
' is true: a re-run happens under the same freeze and cannot converge. Report',
|
|
111
|
+
' the contradiction instead, naming the frozen path and the criterion.',
|
|
112
|
+
// The SAME policy the verify child was held to (constraint-policy.ts). Two
|
|
113
|
+
// children reading one spec must not weigh its constraints differently —
|
|
114
|
+
// that disagreement is what made a grill-invented constraint unwaivable.
|
|
115
|
+
...renderConstraintPolicy('5.'),
|
|
89
116
|
'',
|
|
90
117
|
'When done, output EXACTLY ONE of these as the final line:',
|
|
91
118
|
' VERIFY-RESOLUTION: AUTOFIX <one sentence why the work must be re-done>',
|
|
92
119
|
' VERIFY-RESOLUTION: ACCEPT <one sentence why the artifact is good as-is>',
|
|
120
|
+
` VERIFY-RESOLUTION: ${BLOCKED_BY_FROZEN} <frozen path> — <the criterion it blocks>`,
|
|
93
121
|
'Output the verdict line verbatim — it is parsed mechanically.'
|
|
94
122
|
].join('\n');
|
|
95
123
|
}
|
|
96
124
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
125
|
+
* The path/criterion separator the marker's contract states. An em dash is what
|
|
126
|
+
* the prompt shows; a model that retypes it as an en dash or a spaced hyphen
|
|
127
|
+
* means the same thing, and losing a contradiction over a dash costs the whole
|
|
128
|
+
* autofix budget.
|
|
129
|
+
*/
|
|
130
|
+
const CONTRADICTION_SEP = /\s+[—–]\s+|\s+-{1,2}\s+/;
|
|
131
|
+
/**
|
|
132
|
+
* Parse the child's verdict out of the LAST `VERIFY-RESOLUTION:` marker,
|
|
133
|
+
* case-insensitively. Last match wins: the model reasons before concluding, and a
|
|
134
|
+
* bash command it runs can print the token back, so an earlier occurrence is not
|
|
135
|
+
* the verdict.
|
|
101
136
|
*
|
|
102
137
|
* No marker → AUTOFIX (re-do rather than bless), with a rationale saying the
|
|
103
138
|
* research was inconclusive. A marker with no trailing sentence gets a stock
|
|
104
139
|
* rationale, so `rationale` is never empty.
|
|
140
|
+
*
|
|
141
|
+
* A BLOCKED-BY-FROZEN marker yields a CONTRADICTION plus an `accept`
|
|
142
|
+
* recommendation, which is what the gate does with it once the contradiction is
|
|
143
|
+
* recorded as debt. A marker that names no path is not a usable contradiction and
|
|
144
|
+
* degrades to that recommendation alone.
|
|
105
145
|
*/
|
|
106
146
|
export function parseResolutionVerdict(text) {
|
|
107
|
-
const re =
|
|
147
|
+
const re = new RegExp(`VERIFY-RESOLUTION:\\s*(AUTOFIX|ACCEPT|${BLOCKED_BY_FROZEN})\\b[ \\t]*(.*)`, 'gi');
|
|
108
148
|
let last = null;
|
|
109
149
|
for (let m = re.exec(text); m !== null; m = re.exec(text))
|
|
110
150
|
last = m;
|
|
@@ -114,11 +154,28 @@ export function parseResolutionVerdict(text) {
|
|
|
114
154
|
rationale: 'research inconclusive — defaulting to re-doing the work'
|
|
115
155
|
};
|
|
116
156
|
}
|
|
117
|
-
const
|
|
118
|
-
const
|
|
157
|
+
const verdict = last[1].toUpperCase();
|
|
158
|
+
const tail = last[2].trim();
|
|
159
|
+
if (verdict === BLOCKED_BY_FROZEN)
|
|
160
|
+
return blockedVerdict(tail);
|
|
161
|
+
const recommend = verdict === 'ACCEPT' ? 'accept' : 'autofix';
|
|
162
|
+
const rationale = tail
|
|
119
163
|
|| (recommend === 'accept' ? 'artifact judged acceptable' : 'work judged to need fixing');
|
|
120
164
|
return { recommend, rationale };
|
|
121
165
|
}
|
|
166
|
+
function blockedVerdict(tail) {
|
|
167
|
+
const sep = CONTRADICTION_SEP.exec(tail);
|
|
168
|
+
const frozenPath = (sep ? tail.slice(0, sep.index) : (tail.split(/\s+/)[0] ?? '')).trim();
|
|
169
|
+
const criterion = sep ? tail.slice(sep.index + sep[0].length).trim() : '';
|
|
170
|
+
const rationale = frozenPath.length > 0 ?
|
|
171
|
+
`blocked by spec-frozen \`${frozenPath}\`${criterion ? `: ${criterion}` : ''}`
|
|
172
|
+
: 'judged blocked by a spec-frozen path, but the marker named none';
|
|
173
|
+
return {
|
|
174
|
+
recommend: 'accept',
|
|
175
|
+
rationale,
|
|
176
|
+
...(frozenPath.length > 0 ? { contradiction: { criterion, frozenPath } } : {})
|
|
177
|
+
};
|
|
178
|
+
}
|
|
122
179
|
// ─── Picker plumbing ─────────────────────────────────────────────────────────
|
|
123
180
|
//
|
|
124
181
|
// These pure helpers shape the boxed two-choice picker (task-gates.ts
|
|
@@ -179,7 +236,7 @@ export function classifyResolutionAnswer(answer) {
|
|
|
179
236
|
export async function researchResolution(deps) {
|
|
180
237
|
let text;
|
|
181
238
|
try {
|
|
182
|
-
text = await deps.runChild(RESOLUTION_TOOLS, buildResolutionPrompt(deps.spec, deps.failReason), deps.signal);
|
|
239
|
+
text = await deps.runChild(RESOLUTION_TOOLS, buildResolutionPrompt(deps.spec, deps.failReason, deps.qaRecord), deps.signal);
|
|
183
240
|
}
|
|
184
241
|
catch (err) {
|
|
185
242
|
if (err instanceof Error && err.message === USER_CANCELLED)
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { type EmittedNote } from './env-notes.js';
|
|
1
2
|
import { type CrossTaskDeletion } from './task-provenance.js';
|
|
3
|
+
import { type HealthBaseline, type HealthSignal } from './health-baseline.js';
|
|
4
|
+
import { type Spec } from './spec-model.js';
|
|
5
|
+
import { type SuppressionHit } from './suppression-probe.js';
|
|
2
6
|
/**
|
|
3
7
|
* The verification child gets exactly two tools: `read` and `bash`.
|
|
4
8
|
*
|
|
@@ -14,19 +18,43 @@ import { type CrossTaskDeletion } from './task-provenance.js';
|
|
|
14
18
|
* mutated tree. The enforce pass, whose job IS editing, runs `read,edit`.
|
|
15
19
|
*/
|
|
16
20
|
declare const VERIFY_TOOLS = "read,bash";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The work verified, or there was nothing to verify.
|
|
23
|
+
*
|
|
24
|
+
* The FAIL-only fields are declared `?: undefined` rather than omitted: a pass may
|
|
25
|
+
* not carry them (`{ok: true, failClass: …}` is still a compile error), but a
|
|
26
|
+
* caller holding the bare union can still READ them without narrowing first, which
|
|
27
|
+
* is how every consumer asks "was this unobserved?" in one expression.
|
|
28
|
+
*/
|
|
29
|
+
export interface VerifyPass {
|
|
30
|
+
ok: true;
|
|
31
|
+
/** The NO-OP cause ('disabled', 'no spec to verify'). Absent on a genuine
|
|
32
|
+
* pass, which is the only signal the enforce pass may edit against. */
|
|
24
33
|
reason?: string;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
failClass?: undefined;
|
|
35
|
+
unobserved?: undefined;
|
|
36
|
+
crossTaskDeletions?: undefined;
|
|
37
|
+
inheritedHealth?: string;
|
|
38
|
+
probes?: ProbeFindings;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The work did NOT satisfy its spec, or the pass could not run.
|
|
42
|
+
*
|
|
43
|
+
* `failClass` is REQUIRED. Optional, it travelled as the prefix of `reason` and
|
|
44
|
+
* every downstream classifier recovered it by re-typing that literal, so a reword
|
|
45
|
+
* of the mint disarmed them with no compile error — and one site (the
|
|
46
|
+
* mutation-guard FAIL) simply never set it, which left the decision table with no
|
|
47
|
+
* class to route on. `VERIFY_FAIL_PREFIX` owns the display strings so mint and
|
|
48
|
+
* match cannot drift.
|
|
49
|
+
*/
|
|
50
|
+
export interface VerifyFail {
|
|
51
|
+
ok: false;
|
|
52
|
+
failClass: VerifyFailClass;
|
|
53
|
+
/** Short, human-readable reason, minted under the class's prefix. */
|
|
54
|
+
reason: string;
|
|
55
|
+
/** Rule 5c: a spec-required behavioral check could not run because its tooling
|
|
56
|
+
* is absent. Sets the AUTOFIX budget to zero on any class — an unattended
|
|
57
|
+
* re-run cannot provision a missing tool. */
|
|
30
58
|
unobserved?: boolean;
|
|
31
59
|
/** The deterministic cross-task deletion findings (see task-provenance.ts) that
|
|
32
60
|
* were live when this verdict was produced: sibling tasks' committed
|
|
@@ -34,17 +62,20 @@ export interface VerifyOutcome {
|
|
|
34
62
|
* can record each as a durable debt if the user ACCEPTs anyway — the deletion
|
|
35
63
|
* then ships in the next commit and the final gate must re-check it. */
|
|
36
64
|
crossTaskDeletions?: CrossTaskDeletion[];
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
* reword of the mint would then disarm them with no compile error.
|
|
43
|
-
* `VERIFY_FAIL_PREFIX` owns the strings so mint and match cannot drift, and
|
|
44
|
-
* `verifyFailClass` prefers this field over the prefix.
|
|
45
|
-
*/
|
|
46
|
-
failClass?: VerifyFailClass;
|
|
65
|
+
inheritedHealth?: string;
|
|
66
|
+
/** What the deterministic probes found for THIS verdict, carried out so an
|
|
67
|
+
* AUTOFIX re-run is told what the gate already knows (see fix-context.ts)
|
|
68
|
+
* instead of re-deriving it from the one-line reason. */
|
|
69
|
+
probes?: ProbeFindings;
|
|
47
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* `inheritedHealth` is declared on BOTH halves: the repo's own static checks that
|
|
73
|
+
* were already failing the same way before this task ran (health-baseline.ts).
|
|
74
|
+
* A task can verify perfectly in a repo that arrived red, so a PASS carries it
|
|
75
|
+
* too — dropping it on the pass path is where a sibling's defect disappears until
|
|
76
|
+
* the run-end gate rediscovers it with nobody's name on it.
|
|
77
|
+
*/
|
|
78
|
+
export type VerifyOutcome = VerifyPass | VerifyFail;
|
|
48
79
|
/**
|
|
49
80
|
* The kinds of verify FAIL. `VERIFY_FAIL_PREFIX` below is a
|
|
50
81
|
* `Record<VerifyFailClass, string>`, so a new member here is a compile error
|
|
@@ -72,18 +103,18 @@ export declare const VERIFY_FAIL_PREFIX: Record<VerifyFailClass, string>;
|
|
|
72
103
|
* mints its `static checks:` line through a different path entirely. This is the
|
|
73
104
|
* only place in src/ that tests a reason against a prefix.
|
|
74
105
|
*/
|
|
75
|
-
export declare function verifyFailClass(o:
|
|
106
|
+
export declare function verifyFailClass(o: {
|
|
107
|
+
failClass?: VerifyFailClass;
|
|
108
|
+
reason?: string;
|
|
109
|
+
}): VerifyFailClass | undefined;
|
|
76
110
|
/** The class a recorded reason STRING belongs to, by its minted prefix. */
|
|
77
111
|
export declare function failClassOfReason(reason: string): VerifyFailClass | undefined;
|
|
78
112
|
/** Does this class name a deterministic whole-repo static check, at either altitude? */
|
|
79
113
|
export declare function isStaticClass(cls: VerifyFailClass | undefined): boolean;
|
|
80
114
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* `## ` plus non-space (`## phase timings`); a `### ` subheading stays inside.
|
|
85
|
-
* Returns null when there is no spec section or it is blank (a task that never
|
|
86
|
-
* reached compose), which the caller treats as a pass — nothing to verify against.
|
|
115
|
+
* The delivered spec's TEXT, for the children that must read its prose verbatim.
|
|
116
|
+
* The slicing itself lives in spec-model.ts beside the parser, so "the spec
|
|
117
|
+
* section" is one definition rather than one per consumer.
|
|
87
118
|
*/
|
|
88
119
|
export declare function extractSpecForVerification(taskBody: string): string | null;
|
|
89
120
|
/**
|
|
@@ -115,6 +146,7 @@ export declare function extractSpecForVerification(taskBody: string): string | n
|
|
|
115
146
|
* 'VerifyProbes'").
|
|
116
147
|
*/
|
|
117
148
|
export interface ProbeRaw {
|
|
149
|
+
evidence: string[];
|
|
118
150
|
substitution: string[];
|
|
119
151
|
prohibition: string[];
|
|
120
152
|
crossTaskDeletion: CrossTaskDeletion[];
|
|
@@ -124,15 +156,19 @@ export interface ProbeRaw {
|
|
|
124
156
|
scriptEscape: string[];
|
|
125
157
|
runnerGlob: string[];
|
|
126
158
|
testAssembly: string[];
|
|
159
|
+
repoHealth: string[];
|
|
160
|
+
suppressionWidening: SuppressionHit[];
|
|
127
161
|
}
|
|
128
162
|
/** Stable identity of one probe channel: table row ↔ findings-bag key. */
|
|
129
163
|
export type ProbeKey = keyof ProbeRaw;
|
|
130
164
|
/**
|
|
131
|
-
* The channels a CALLER binds
|
|
132
|
-
* analysis of `deps.spec` and is sourced inside its own table row,
|
|
133
|
-
*
|
|
165
|
+
* The channels a CALLER binds. Two keys are not among them: `skipEscape`, which is
|
|
166
|
+
* pure text analysis of `deps.spec` and is sourced inside its own table row, and
|
|
167
|
+
* `repoHealth`, whose findings are the classified DELTA of a check
|
|
168
|
+
* `runWorkVerification` has already run — only that function holds both the
|
|
169
|
+
* baseline and the result, so only it can source the row.
|
|
134
170
|
*/
|
|
135
|
-
export type BoundProbeKey = Exclude<ProbeKey, 'skipEscape'>;
|
|
171
|
+
export type BoundProbeKey = Exclude<ProbeKey, 'skipEscape' | 'repoHealth'>;
|
|
136
172
|
/**
|
|
137
173
|
* The bound probes: one optional thunk per channel, typed to that channel's raw
|
|
138
174
|
* shape. Built in ONE place (gate-deps' `buildVerifyProbes`); an absent key means
|
|
@@ -150,6 +186,12 @@ export type ProbeFindings = Partial<Record<ProbeKey, string[]>>;
|
|
|
150
186
|
* checked against the table rather than against a hand-kept list.
|
|
151
187
|
*/
|
|
152
188
|
export declare const BOUND_PROBE_KEYS: readonly BoundProbeKey[];
|
|
189
|
+
/**
|
|
190
|
+
* The spec as a VALUE, with each constraint's `[from:]` tag resolved against the
|
|
191
|
+
* task's own Q&A record. Shared by the verify prompt and the resolution judge, so
|
|
192
|
+
* one constraint cannot carry two weights.
|
|
193
|
+
*/
|
|
194
|
+
export declare function specValue(spec: string, qaRecord?: string): Spec;
|
|
153
195
|
/**
|
|
154
196
|
* Build the verification child's prompt. Kept pure so the wording is unit-tested
|
|
155
197
|
* without spawning pi. The contract: run the spec's own verification in the real
|
|
@@ -162,8 +204,14 @@ export declare const BOUND_PROBE_KEYS: readonly BoundProbeKey[];
|
|
|
162
204
|
export declare function buildVerifyPrompt(spec: string, findings?: ProbeFindings, context?: {
|
|
163
205
|
/** Environment facts earlier gate children discovered — see env-notes.ts. */
|
|
164
206
|
envNotes?: string;
|
|
207
|
+
/** This run's id: the notes it recorded lead, older runs' follow. */
|
|
208
|
+
envRunId?: string;
|
|
165
209
|
/** Cross-slice interface facts the design pins — see contracts.ts. */
|
|
166
210
|
contracts?: string;
|
|
211
|
+
/** The task file's rendered Q&A, which is what resolves a constraint's
|
|
212
|
+
* `[from: Q<n>]` tag to the kind of answer that produced it. Absent ⇒
|
|
213
|
+
* every tagged constraint reads as `derived`, which is advisory. */
|
|
214
|
+
qaRecord?: string;
|
|
167
215
|
}): string;
|
|
168
216
|
/**
|
|
169
217
|
* Parse the child's verdict out of the LAST `WORK-VERIFIED: PASS|FAIL|UNOBSERVED`
|
|
@@ -206,7 +254,15 @@ export interface VerificationDeps {
|
|
|
206
254
|
repoHealth?: () => Promise<{
|
|
207
255
|
ok: boolean;
|
|
208
256
|
reason: string;
|
|
209
|
-
}>;
|
|
257
|
+
} & HealthSignal>;
|
|
258
|
+
/**
|
|
259
|
+
* What those same checks said BEFORE this task ran (see health-baseline.ts).
|
|
260
|
+
* Consulted only when `repoHealth` comes back red, and it is what decides
|
|
261
|
+
* whose defect that is: a check failing the same way before is `pre-existing`
|
|
262
|
+
* and becomes a probe row, not a FAIL. ABSENT, or null, → every red check is
|
|
263
|
+
* this task's, which is the behavior that shipped before the baseline existed.
|
|
264
|
+
*/
|
|
265
|
+
healthBaseline?: () => Promise<HealthBaseline | null>;
|
|
210
266
|
/**
|
|
211
267
|
* Progress hook for the DETERMINISTIC stage — the repo-health run plus the
|
|
212
268
|
* `probes` below, all of which run BEFORE the child (and therefore before the
|
|
@@ -245,7 +301,10 @@ export interface VerificationDeps {
|
|
|
245
301
|
*/
|
|
246
302
|
envNotes?: {
|
|
247
303
|
read: () => Promise<string>;
|
|
248
|
-
append: (notes:
|
|
304
|
+
append: (notes: readonly EmittedNote[]) => Promise<void>;
|
|
305
|
+
/** The run these notes belong to, so the child's prompt can lead with the
|
|
306
|
+
* facts measured against the tree it is standing in. */
|
|
307
|
+
runId?: string;
|
|
249
308
|
};
|
|
250
309
|
/**
|
|
251
310
|
* Per-run cross-slice contract registry (see contracts.ts): `read` supplies the
|
|
@@ -255,6 +314,10 @@ export interface VerificationDeps {
|
|
|
255
314
|
* `/task` runs, or a design pinning no shared boundary).
|
|
256
315
|
*/
|
|
257
316
|
contracts?: () => Promise<string>;
|
|
317
|
+
/** The task file's `## grill Q&A` text, which resolves each constraint's
|
|
318
|
+
* provenance tag (see `specValue`). ABSENT ⇒ every tagged constraint is
|
|
319
|
+
* advisory, which is the safe reading of a provenance nobody can confirm. */
|
|
320
|
+
qaRecord?: string;
|
|
258
321
|
}
|
|
259
322
|
/**
|
|
260
323
|
* Run the verification pass for one task. `repoHealth` runs first and can FAIL on
|