@mjasnikovs/pi-task 0.30.0 → 0.31.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 +29 -0
- package/dist/index.js +2 -0
- package/dist/remote/bridge.d.ts +18 -0
- package/dist/remote/bridge.js +2 -0
- package/dist/remote/protocol.d.ts +9 -0
- package/dist/remote/ui-script.js +20 -0
- package/dist/task/accept-debt.d.ts +51 -2
- package/dist/task/accept-debt.js +140 -7
- package/dist/task/auto-orchestrator.d.ts +1 -0
- package/dist/task/auto-orchestrator.js +10 -1
- package/dist/task/final-gate.d.ts +54 -1
- package/dist/task/final-gate.js +115 -3
- package/dist/task/plan-io.d.ts +55 -0
- package/dist/task/plan-io.js +94 -0
- package/dist/task/plan-orchestrator.d.ts +52 -0
- package/dist/task/plan-orchestrator.js +234 -0
- package/dist/task/plan-prompts.d.ts +40 -0
- package/dist/task/plan-prompts.js +138 -0
- package/dist/task/plan-session.d.ts +184 -0
- package/dist/task/plan-session.js +373 -0
- package/dist/task/question-box.d.ts +8 -0
- package/dist/task/question-box.js +1 -1
- package/dist/task/spec-validation.d.ts +14 -0
- package/dist/task/spec-validation.js +21 -1
- package/dist/task/widget.d.ts +4 -0
- package/dist/task/widget.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,12 @@ One change — `/task` runs it through the full pipeline and hands the finished
|
|
|
46
46
|
/task add rate limiting to the /api/upload endpoint
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
One change, but you want a say in HOW — `/task-plan` talks it through with you first, one question at a time, then hands the decisions to `/task`:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
/task-plan add rate limiting to the /api/upload endpoint
|
|
53
|
+
```
|
|
54
|
+
|
|
49
55
|
A whole plan — `/task-auto` splits it into an ordered task list and runs each one through `/task`:
|
|
50
56
|
|
|
51
57
|
```
|
|
@@ -59,6 +65,7 @@ A whole plan — `/task-auto` splits it into an ordered task list and runs each
|
|
|
59
65
|
| Command | What it does |
|
|
60
66
|
| --- | --- |
|
|
61
67
|
| `/task <prompt>` | Start a new task and run it through the full pipeline. |
|
|
68
|
+
| `/task-plan <prompt>` | Plan one task with the model — it asks, you answer, ask it something back, or proceed — then run it through `/task`. |
|
|
62
69
|
| `/task-list` | Open the task list in an editor dialog. |
|
|
63
70
|
| `/task-resume [id]` | Resume the most recent (or named) unfinished task. |
|
|
64
71
|
| `/task-cancel` | Cancel the running task (soft-terminal — still resumable). |
|
|
@@ -86,6 +93,28 @@ A whole plan — `/task-auto` splits it into an ordered task list and runs each
|
|
|
86
93
|
|
|
87
94
|
The finished spec is delivered to your main `pi` conversation via `sendUserMessage`, so you keep working in the same chat — no context handoff, no copy-paste.
|
|
88
95
|
|
|
96
|
+
## Planning one task — `/task-plan`
|
|
97
|
+
|
|
98
|
+
`/task` decides most things for you: refine sharpens the ask, research gathers context, and grill only surfaces the questions its research could not settle. That is the right trade when you want the change done. When you want a say in **how** it gets done, `/task-plan` puts the conversation first.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
/task-plan add rate limiting to the /api/upload endpoint
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It works like the clarify step you already know — **one question at a time, each one shaped by your last answer** — with a recommendation you can take with one keystroke. The difference is that three moves are on screen at *every* prompt, so the conversation is yours to steer:
|
|
105
|
+
|
|
106
|
+
| Move | What it does |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| **Answer** | Take the recommendation (or the `B` option), or type your own answer. Empty submit = accept the recommendation. |
|
|
109
|
+
| **❓ Ask the model a question** | You ask, it answers — grounded in the repo, with the read tool. The answer is recorded as a **note**, and the same question you were being asked comes straight back. Notes do not decide anything; they are context, and they ride into the next question. |
|
|
110
|
+
| **▶ Proceed to execution** | Stop planning and hand what you have to `/task`. Available from the very first prompt — you are never forced through a question you do not care about. |
|
|
111
|
+
|
|
112
|
+
When the model runs out of questions it says so and offers the same three moves rather than proceeding behind your back. Answering something new re-opens it: a decision you volunteer can make a fresh question worth asking.
|
|
113
|
+
|
|
114
|
+
Everything lands in `.pi-tasks/TASK_PLAN_NNNN.md` — the task prompt, the `## decisions` transcript, and a separate `## notes` section for what you asked. Only the decisions are handed to `/task`, as an authoritative block ahead of your original prompt; the notes stay behind, because an answer you read is not a decision you made. From there it is an ordinary `/task` run — same pipeline, same gates.
|
|
115
|
+
|
|
116
|
+
It works from the browser too (see [Remote](#remote--drive-a-task-from-your-phone)): the prompt card grows an **Ask the model** and a **Proceed to execution** button alongside the usual Accept / Manual answer.
|
|
117
|
+
|
|
89
118
|
## Orchestrating multiple tasks — `/task-auto`
|
|
90
119
|
|
|
91
120
|
A real feature is usually several tasks, not one. `/task-auto` is a thin planner on top of the single-task pipeline:
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { registerConfig } from './config/register.js';
|
|
2
2
|
import { registerTask } from './task/orchestrator.js';
|
|
3
3
|
import { registerTaskAuto } from './task/auto-orchestrator.js';
|
|
4
|
+
import { registerTaskPlan } from './task/plan-orchestrator.js';
|
|
4
5
|
import { registerWorkers } from './workers/index.js';
|
|
5
6
|
import { registerRemote } from './remote/register.js';
|
|
6
7
|
import { registerThinkingCompression } from './thinking/compress.js';
|
|
@@ -10,6 +11,7 @@ export default function (pi) {
|
|
|
10
11
|
registerConfig(pi);
|
|
11
12
|
registerTask(pi);
|
|
12
13
|
registerTaskAuto(pi);
|
|
14
|
+
registerTaskPlan(pi);
|
|
13
15
|
registerWorkers(pi);
|
|
14
16
|
registerRemote(pi);
|
|
15
17
|
registerThinkingCompression(pi);
|
package/dist/remote/bridge.d.ts
CHANGED
|
@@ -52,6 +52,24 @@ export interface AskSpec {
|
|
|
52
52
|
label: string;
|
|
53
53
|
value: string;
|
|
54
54
|
}[];
|
|
55
|
+
/**
|
|
56
|
+
* Label for the local picker's trailing free-text card (see
|
|
57
|
+
* {@link AskQuestionBoxSpec.manualLabel}). Ignored without `options`.
|
|
58
|
+
*/
|
|
59
|
+
manualLabel?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Extra buttons the BROWSER card shows alongside the recommendation, each
|
|
62
|
+
* answering with its own `value`. Unlike `options` — which are answers, and
|
|
63
|
+
* which the remote already covers with the recommended/recommended2 buttons —
|
|
64
|
+
* these are ACTIONS that mean something other than "here is my answer", so
|
|
65
|
+
* the remote cannot express them by any existing field. /task-plan's "ask the
|
|
66
|
+
* model" and "proceed to execution" are the only producers; every other call
|
|
67
|
+
* site omits this and the card is byte-for-byte what it was.
|
|
68
|
+
*/
|
|
69
|
+
actions?: {
|
|
70
|
+
label: string;
|
|
71
|
+
value: string;
|
|
72
|
+
}[];
|
|
55
73
|
}
|
|
56
74
|
/** Wraps a live command ctx and fans interactions out to local TUI + browsers. */
|
|
57
75
|
export declare class SessionUI {
|
package/dist/remote/bridge.js
CHANGED
|
@@ -54,6 +54,7 @@ export class SessionUI {
|
|
|
54
54
|
question: spec.question,
|
|
55
55
|
recommended: spec.recommended,
|
|
56
56
|
...(spec.recommended2 !== undefined && { recommended2: spec.recommended2 }),
|
|
57
|
+
...(spec.actions !== undefined && spec.actions.length > 0 && { actions: spec.actions }),
|
|
57
58
|
allowSkip: spec.allowSkip
|
|
58
59
|
};
|
|
59
60
|
setPrompt(prompt);
|
|
@@ -99,6 +100,7 @@ export class SessionUI {
|
|
|
99
100
|
value: o.value,
|
|
100
101
|
recommended: i === 0
|
|
101
102
|
})),
|
|
103
|
+
...(spec.manualLabel !== undefined && { manualLabel: spec.manualLabel }),
|
|
102
104
|
signal
|
|
103
105
|
});
|
|
104
106
|
}
|
|
@@ -4,6 +4,15 @@ export interface PromptMessage {
|
|
|
4
4
|
question: string;
|
|
5
5
|
recommended?: string;
|
|
6
6
|
recommended2?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Buttons that answer with their own `value` instead of with an answer to the
|
|
9
|
+
* question — /task-plan's "ask the model" and "proceed to execution". Absent
|
|
10
|
+
* on every other prompt, so an older card renders exactly as before.
|
|
11
|
+
*/
|
|
12
|
+
actions?: {
|
|
13
|
+
label: string;
|
|
14
|
+
value: string;
|
|
15
|
+
}[];
|
|
7
16
|
allowSkip: boolean;
|
|
8
17
|
}
|
|
9
18
|
export interface PromptResolvedMessage {
|
package/dist/remote/ui-script.js
CHANGED
|
@@ -109,6 +109,7 @@ export function clientScript(wsUrl) {
|
|
|
109
109
|
let activePromptId = null;
|
|
110
110
|
let activeRecommended = '';
|
|
111
111
|
let activeRecommended2 = '';
|
|
112
|
+
let activeActions = [];
|
|
112
113
|
let cancelArmTimer = null;
|
|
113
114
|
const toolCallMap = {};
|
|
114
115
|
let currentBubble = null;
|
|
@@ -144,6 +145,7 @@ export function clientScript(wsUrl) {
|
|
|
144
145
|
|
|
145
146
|
const COMMANDS = [
|
|
146
147
|
{ name: '/task', desc: 'Start a new task' },
|
|
148
|
+
{ name: '/task-plan', desc: 'Plan one task with the model, then run it' },
|
|
147
149
|
{ name: '/task-list', desc: 'List tasks in this project' },
|
|
148
150
|
{ name: '/task-resume', desc: 'Resume a task' },
|
|
149
151
|
{ name: '/task-cancel', desc: 'Cancel the currently running task' },
|
|
@@ -782,6 +784,7 @@ export function clientScript(wsUrl) {
|
|
|
782
784
|
promptInput.style.display = 'none';
|
|
783
785
|
promptRec.style.display = 'none';
|
|
784
786
|
activeRecommended2 = '';
|
|
787
|
+
activeActions = [];
|
|
785
788
|
if (cancelArmTimer) { clearTimeout(cancelArmTimer); cancelArmTimer = null; }
|
|
786
789
|
refreshComposer();
|
|
787
790
|
}
|
|
@@ -815,10 +818,26 @@ export function clientScript(wsUrl) {
|
|
|
815
818
|
return btn;
|
|
816
819
|
}
|
|
817
820
|
|
|
821
|
+
// Action buttons (/task-plan's "ask the model" / "proceed to execution").
|
|
822
|
+
// They answer with their own sentinel value rather than with an answer to the
|
|
823
|
+
// question, so they sit between the answer buttons and Cancel. Empty — and
|
|
824
|
+
// therefore invisible — for every prompt that carries no actions.
|
|
825
|
+
function makeActionBtns() {
|
|
826
|
+
const out = [];
|
|
827
|
+
for (let i = 0; i < activeActions.length; i++) {
|
|
828
|
+
(function (a) {
|
|
829
|
+
out.push(makeBtn(a.label, 'secondary', function () { answer(a.value); }));
|
|
830
|
+
})(activeActions[i]);
|
|
831
|
+
}
|
|
832
|
+
return out;
|
|
833
|
+
}
|
|
834
|
+
|
|
818
835
|
function renderButtons(buttons, stacked) {
|
|
819
836
|
promptButtons.className = stacked ? 'row stacked' : 'row';
|
|
820
837
|
promptButtons.innerHTML = '';
|
|
821
838
|
for (let i = 0; i < buttons.length; i++) promptButtons.appendChild(buttons[i]);
|
|
839
|
+
const actions = makeActionBtns();
|
|
840
|
+
for (let i = 0; i < actions.length; i++) promptButtons.appendChild(actions[i]);
|
|
822
841
|
promptButtons.appendChild(makeCancelBtn());
|
|
823
842
|
}
|
|
824
843
|
|
|
@@ -861,6 +880,7 @@ export function clientScript(wsUrl) {
|
|
|
861
880
|
promptQ.textContent = msg.question;
|
|
862
881
|
activeRecommended = msg.recommended || '';
|
|
863
882
|
activeRecommended2 = msg.recommended2 || '';
|
|
883
|
+
activeActions = msg.actions || [];
|
|
864
884
|
if (msg.recommended) {
|
|
865
885
|
// Mode A: recommendation(s) present. Render markdown in the panel so a
|
|
866
886
|
// recommendation with code/emphasis reads the same as an assistant bubble.
|
|
@@ -64,6 +64,19 @@ export interface AcceptDebt {
|
|
|
64
64
|
* child `rm`'d TASK_0008's verified admin page to satisfy exactly such a claim.
|
|
65
65
|
*/
|
|
66
66
|
conflict?: string;
|
|
67
|
+
/**
|
|
68
|
+
* The ONE command this debt's own reason NAMES, quoted verbatim in backticks and
|
|
69
|
+
* present byte-identically in the owning task's VERIFY block (nexttask 5, mx5 run
|
|
70
|
+
* 19 TASK_0009). Set at record time by classifyVerifyCommand; absent whenever the
|
|
71
|
+
* reason names no such command — which is most of them (measured: 2 of 19
|
|
72
|
+
* PROJECT-pool debts, `scripts/debt-verify-class-baserate.ts`).
|
|
73
|
+
*
|
|
74
|
+
* It exists so the run-end re-check can settle the debt the way the debt was
|
|
75
|
+
* created: by RUNNING the command and reading its exit status. Never
|
|
76
|
+
* synthesised, never paraphrased, never reconstructed from prose — the stored
|
|
77
|
+
* string is the VERIFY-block line itself (`inv-command-provenance`).
|
|
78
|
+
*/
|
|
79
|
+
verifyCommand?: string;
|
|
67
80
|
}
|
|
68
81
|
export declare function acceptDebtFile(cwd: string): string;
|
|
69
82
|
/** The raw stored ledger ('' when none recorded yet). Parse with parseAcceptDebts. */
|
|
@@ -159,20 +172,56 @@ export declare function writeAcceptDebts(cwd: string, debts: AcceptDebt[]): Prom
|
|
|
159
172
|
* behavioral and cannot be proven resolved without re-running the model.
|
|
160
173
|
*/
|
|
161
174
|
export declare function isStaticClassDebt(reason: string): boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Read the owning task's spec and return the VERIFY command its FAIL reason names,
|
|
177
|
+
* or null. Best-effort by design: a missing task file, an unparseable or UNCLOSED
|
|
178
|
+
* VERIFY fence, or a reason that quotes nothing all yield null, and null simply
|
|
179
|
+
* means the debt keeps today's behaviour (surfaced, never auto-closed).
|
|
180
|
+
*
|
|
181
|
+
* The STRICT parse matters here. mx5 run 19's `TASK_0001.md` opens ```sh and never
|
|
182
|
+
* closes it, so the lenient parser hands back the phase-timings table and every
|
|
183
|
+
* appended gate-trail line as "VERIFY commands" — including sentences that quote
|
|
184
|
+
* `bun run lint`. Matching a reason against that would mint a stored, re-runnable
|
|
185
|
+
* command with fabricated provenance, which is the one thing this class may not do.
|
|
186
|
+
*/
|
|
187
|
+
export declare function classifyVerifyCommand(cwd: string, taskId: string, reason: string): Promise<string | null>;
|
|
188
|
+
export declare function verifyCommandFromReason(reason: string, verifyCommands: readonly string[]): string | null;
|
|
189
|
+
/**
|
|
190
|
+
* How a re-run of a debt's stored VERIFY command ended, as the re-check sees it.
|
|
191
|
+
* `pass` is the ONLY conclusive outcome: everything else — a real failure, a missing
|
|
192
|
+
* tool, unreachable infrastructure, a timeout, a tree the run mutated — leaves the
|
|
193
|
+
* debt exactly as open as it was.
|
|
194
|
+
*/
|
|
195
|
+
export interface VerifyRerunResult {
|
|
196
|
+
outcome: 'pass' | 'fail' | 'gap';
|
|
197
|
+
detail?: string;
|
|
198
|
+
}
|
|
162
199
|
/**
|
|
163
200
|
* Re-check the ledger against the current run state. A static-class debt is RESOLVED
|
|
164
201
|
* iff the final gate's own static check now passes (`staticOk`); a cross-task-deletion
|
|
165
202
|
* debt is RESOLVED iff the file it names is back in the tree (`fileExists` — a later
|
|
166
203
|
* task or a human restored it, so the deletion no longer holds); every other debt
|
|
167
|
-
* stays OPEN (unprovable ⇒ surface, never re-hide)
|
|
168
|
-
*
|
|
204
|
+
* stays OPEN (unprovable ⇒ surface, never re-hide) — unless it carries a stored
|
|
205
|
+
* `verifyCommand` and `rerunVerify` re-runs that command to a ZERO exit, which is the
|
|
206
|
+
* third class (nexttask 5): the debt named a command, the command was run, and it
|
|
207
|
+
* passed. FP-safe: the only auto-closes are ones a deterministic check can stand
|
|
208
|
+
* behind, and here the check is the task spec's own command.
|
|
169
209
|
*/
|
|
170
210
|
export declare function recheckAcceptDebts(debts: AcceptDebt[], opts: {
|
|
171
211
|
staticOk: boolean;
|
|
172
212
|
fileExists?: (rel: string) => boolean;
|
|
213
|
+
/**
|
|
214
|
+
* Re-run a debt's stored VERIFY command (nexttask 5). Absent ⇒ the class is
|
|
215
|
+
* inert and every debt behaves exactly as it did before it existed. Only
|
|
216
|
+
* `pass` may close a debt; `fail` and `gap` both leave it open, and the
|
|
217
|
+
* caller is expected to have made `pass` mean "ran, exited 0, and changed
|
|
218
|
+
* nothing tracked" (`inv-no-write`).
|
|
219
|
+
*/
|
|
220
|
+
rerunVerify?: (command: string, debt: AcceptDebt) => VerifyRerunResult;
|
|
173
221
|
}): {
|
|
174
222
|
open: AcceptDebt[];
|
|
175
223
|
resolved: AcceptDebt[];
|
|
224
|
+
trail: string[];
|
|
176
225
|
};
|
|
177
226
|
/**
|
|
178
227
|
* Extract the paths whose EXISTENCE the reason asserts as the failure — a path
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import * as fsp from 'node:fs/promises';
|
|
29
29
|
import * as path from 'node:path';
|
|
30
|
-
import {
|
|
30
|
+
import { parseVerifyBlockStrict } from './spec-validation.js';
|
|
31
|
+
import { taskFilePath, tasksDir } from './task-io.js';
|
|
31
32
|
const ACCEPT_DEBT_FILE = 'accept-debt.md';
|
|
32
33
|
/** Cap kept records so a run that accepts many FAILs cannot grow the report unboundedly. */
|
|
33
34
|
const MAX_DEBTS = 60;
|
|
@@ -71,6 +72,9 @@ export function parseAcceptDebts(raw) {
|
|
|
71
72
|
continue;
|
|
72
73
|
}
|
|
73
74
|
const origin = parts[2]?.trim();
|
|
75
|
+
// 4th field: the verbatim VERIFY command the reason names (nexttask 5).
|
|
76
|
+
// Absent in every legacy record, and absent in most new ones.
|
|
77
|
+
const verifyCommand = parts[3]?.trim();
|
|
74
78
|
out.push({
|
|
75
79
|
taskId: parts[0].trim(),
|
|
76
80
|
reason: parts[1].trim(),
|
|
@@ -82,7 +86,8 @@ export function parseAcceptDebts(raw) {
|
|
|
82
86
|
|| origin === 'final-gate'
|
|
83
87
|
|| origin === 'root-cause') ?
|
|
84
88
|
{ origin: origin }
|
|
85
|
-
: {})
|
|
89
|
+
: {}),
|
|
90
|
+
...(verifyCommand !== undefined && verifyCommand.length > 0 ? { verifyCommand } : {})
|
|
86
91
|
});
|
|
87
92
|
}
|
|
88
93
|
return out;
|
|
@@ -101,6 +106,11 @@ function normaliseReason(reason) {
|
|
|
101
106
|
function serialize(d) {
|
|
102
107
|
// Legacy 2-field shape for 'accepted' (backward compatible); a 3rd origin field
|
|
103
108
|
// only for the non-accepted classes, so old readers/files round-trip unchanged.
|
|
109
|
+
// The 4th verify-command field forces the origin field to be written (positional
|
|
110
|
+
// format) — 'accepted' spelled out there parses back to the same absent origin.
|
|
111
|
+
if (d.verifyCommand !== undefined && d.verifyCommand.length > 0) {
|
|
112
|
+
return [d.taskId, d.reason, d.origin ?? 'accepted', d.verifyCommand].join(FIELD_SEP);
|
|
113
|
+
}
|
|
104
114
|
return d.origin && d.origin !== 'accepted' ?
|
|
105
115
|
`${d.taskId}${FIELD_SEP}${d.reason}${FIELD_SEP}${d.origin}`
|
|
106
116
|
: `${d.taskId}${FIELD_SEP}${d.reason}`;
|
|
@@ -119,6 +129,12 @@ async function appendDebt(cwd, entry) {
|
|
|
119
129
|
if (entry.reason.length === 0)
|
|
120
130
|
return;
|
|
121
131
|
try {
|
|
132
|
+
// Classify AT RECORD TIME, against the spec as it stands when the defect is
|
|
133
|
+
// recorded (nexttask 5). Doing it later would read a spec a subsequent task
|
|
134
|
+
// may have rewritten — the provenance claim has to be made where it is true.
|
|
135
|
+
const verifyCommand = entry.verifyCommand ?? (await classifyVerifyCommand(cwd, entry.taskId, entry.reason));
|
|
136
|
+
if (verifyCommand !== null && verifyCommand !== undefined)
|
|
137
|
+
entry = { ...entry, verifyCommand };
|
|
122
138
|
const existing = parseAcceptDebts(await readAcceptDebtsRaw(cwd));
|
|
123
139
|
const seen = new Set(existing.map(debtKey));
|
|
124
140
|
if (seen.has(debtKey(entry)))
|
|
@@ -269,17 +285,101 @@ export async function writeAcceptDebts(cwd, debts) {
|
|
|
269
285
|
export function isStaticClassDebt(reason) {
|
|
270
286
|
return /^\s*repo health:/i.test(reason);
|
|
271
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* The VERIFY-COMMAND class (nexttask 5): the ONE command a recorded reason itself
|
|
290
|
+
* NAMES, quoted verbatim in backticks, and present byte-identically in the owning
|
|
291
|
+
* task's VERIFY block.
|
|
292
|
+
*
|
|
293
|
+
* mx5 run 19 recorded `work did not verify: The VERIFY block command \`AGENT=1 bun
|
|
294
|
+
* test test/listings.test.ts\` fails unaided …`; eleven minutes later the final-gate
|
|
295
|
+
* autofix fixed exactly that, the gate's own re-run printed `121 pass 0 fail`, and
|
|
296
|
+
* the run still ended reporting the debt STILL OPEN — because no reachable code path
|
|
297
|
+
* could ever have closed it (see recheckAcceptDebts: two classes, neither reachable
|
|
298
|
+
* for a `work did not verify:` reason).
|
|
299
|
+
*
|
|
300
|
+
* Deliberately NOT fuzzy: a backticked span is accepted only when it equals a parsed
|
|
301
|
+
* VERIFY line exactly (after trimming). No paraphrase, no reconstruction, no prefix
|
|
302
|
+
* match — the returned string is the VERIFY-block entry itself, so anything stored
|
|
303
|
+
* or re-run carries the task spec's own provenance (`inv-command-provenance`). A
|
|
304
|
+
* reason that quotes a path, a symbol or a truncated command matches nothing and the
|
|
305
|
+
* debt stays unclassified, i.e. exactly as un-closable as it is today.
|
|
306
|
+
*/
|
|
307
|
+
/**
|
|
308
|
+
* A stored command must survive the ledger's own storage format and stay something
|
|
309
|
+
* a shell can be handed verbatim. A tab would break the positional record, a newline
|
|
310
|
+
* would split it into two, and an over-long line is a heredoc/prose artefact rather
|
|
311
|
+
* than a command. Any of those ⇒ not stored ⇒ the debt is simply unclassified, i.e.
|
|
312
|
+
* exactly as un-closable as it is today.
|
|
313
|
+
*/
|
|
314
|
+
function isStorableCommand(cmd) {
|
|
315
|
+
return cmd.length > 0 && cmd.length <= MAX_REASON_LENGTH && !/[\t\n\r]/.test(cmd);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Read the owning task's spec and return the VERIFY command its FAIL reason names,
|
|
319
|
+
* or null. Best-effort by design: a missing task file, an unparseable or UNCLOSED
|
|
320
|
+
* VERIFY fence, or a reason that quotes nothing all yield null, and null simply
|
|
321
|
+
* means the debt keeps today's behaviour (surfaced, never auto-closed).
|
|
322
|
+
*
|
|
323
|
+
* The STRICT parse matters here. mx5 run 19's `TASK_0001.md` opens ```sh and never
|
|
324
|
+
* closes it, so the lenient parser hands back the phase-timings table and every
|
|
325
|
+
* appended gate-trail line as "VERIFY commands" — including sentences that quote
|
|
326
|
+
* `bun run lint`. Matching a reason against that would mint a stored, re-runnable
|
|
327
|
+
* command with fabricated provenance, which is the one thing this class may not do.
|
|
328
|
+
*/
|
|
329
|
+
export async function classifyVerifyCommand(cwd, taskId, reason) {
|
|
330
|
+
if (taskId.trim().length === 0)
|
|
331
|
+
return null;
|
|
332
|
+
try {
|
|
333
|
+
const spec = await fsp.readFile(taskFilePath(cwd, taskId.trim()), 'utf8');
|
|
334
|
+
const cmds = parseVerifyBlockStrict(spec);
|
|
335
|
+
if (cmds === null || cmds.length === 0)
|
|
336
|
+
return null;
|
|
337
|
+
const hit = verifyCommandFromReason(reason, cmds.map(c => c.raw));
|
|
338
|
+
return hit !== null && isStorableCommand(hit) ? hit : null;
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
export function verifyCommandFromReason(reason, verifyCommands) {
|
|
345
|
+
const byText = new Map();
|
|
346
|
+
for (const c of verifyCommands) {
|
|
347
|
+
const t = c.trim();
|
|
348
|
+
if (t.length > 0 && !byText.has(t))
|
|
349
|
+
byText.set(t, t);
|
|
350
|
+
}
|
|
351
|
+
if (byText.size === 0)
|
|
352
|
+
return null;
|
|
353
|
+
for (const m of reason.matchAll(/`([^`]+)`/g)) {
|
|
354
|
+
const hit = byText.get(m[1].trim());
|
|
355
|
+
if (hit !== undefined)
|
|
356
|
+
return hit;
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Re-runs allowed per re-check. `inv-bounded`: a run that accepted many command-shaped
|
|
362
|
+
* FAILs must not turn its own report into an unbounded second test suite. Three covers
|
|
363
|
+
* every recorded run in the corpus (max classified per run: 1) with room to spare, and
|
|
364
|
+
* anything past it stays open with the budget stated in the trail — never closed.
|
|
365
|
+
*/
|
|
366
|
+
const MAX_VERIFY_RERUNS = 3;
|
|
272
367
|
/**
|
|
273
368
|
* Re-check the ledger against the current run state. A static-class debt is RESOLVED
|
|
274
369
|
* iff the final gate's own static check now passes (`staticOk`); a cross-task-deletion
|
|
275
370
|
* debt is RESOLVED iff the file it names is back in the tree (`fileExists` — a later
|
|
276
371
|
* task or a human restored it, so the deletion no longer holds); every other debt
|
|
277
|
-
* stays OPEN (unprovable ⇒ surface, never re-hide)
|
|
278
|
-
*
|
|
372
|
+
* stays OPEN (unprovable ⇒ surface, never re-hide) — unless it carries a stored
|
|
373
|
+
* `verifyCommand` and `rerunVerify` re-runs that command to a ZERO exit, which is the
|
|
374
|
+
* third class (nexttask 5): the debt named a command, the command was run, and it
|
|
375
|
+
* passed. FP-safe: the only auto-closes are ones a deterministic check can stand
|
|
376
|
+
* behind, and here the check is the task spec's own command.
|
|
279
377
|
*/
|
|
280
378
|
export function recheckAcceptDebts(debts, opts) {
|
|
281
379
|
const open = [];
|
|
282
380
|
const resolved = [];
|
|
381
|
+
const trail = [];
|
|
382
|
+
let rerunsLeft = MAX_VERIFY_RERUNS;
|
|
283
383
|
for (const d of debts) {
|
|
284
384
|
if (d.origin === 'cross-task-deletion') {
|
|
285
385
|
const p = extractDeletedDebtPath(d.reason);
|
|
@@ -296,12 +396,45 @@ export function recheckAcceptDebts(debts, opts) {
|
|
|
296
396
|
open.push(d);
|
|
297
397
|
continue;
|
|
298
398
|
}
|
|
299
|
-
if (opts.staticOk && isStaticClassDebt(d.reason))
|
|
399
|
+
if (opts.staticOk && isStaticClassDebt(d.reason)) {
|
|
300
400
|
resolved.push(d);
|
|
301
|
-
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
// VERIFY-COMMAND class, LAST so the two older classes decide exactly what they
|
|
404
|
+
// decided before (`inv-existing-classes-kept`) and nothing is re-run that was
|
|
405
|
+
// already settled without running anything.
|
|
406
|
+
const cmd = d.verifyCommand;
|
|
407
|
+
if (opts.rerunVerify === undefined || cmd === undefined || !isStorableCommand(cmd)) {
|
|
408
|
+
open.push(d);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (rerunsLeft <= 0) {
|
|
412
|
+
trail.push(`${d.taskId}: NOT re-checked — the per-run re-run budget `
|
|
413
|
+
+ `(${MAX_VERIFY_RERUNS}) is spent; the debt stays open`);
|
|
302
414
|
open.push(d);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
rerunsLeft -= 1;
|
|
418
|
+
let r;
|
|
419
|
+
try {
|
|
420
|
+
r = opts.rerunVerify(cmd, d);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// A harness fault observes nothing, so it proves nothing.
|
|
424
|
+
r = { outcome: 'gap', detail: 're-run harness fault' };
|
|
425
|
+
}
|
|
426
|
+
if (r.outcome === 'pass') {
|
|
427
|
+
resolved.push(d);
|
|
428
|
+
trail.push(`${d.taskId}: RESOLVED — re-ran \`${cmd}\` and it exited 0`);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
trail.push(`${d.taskId}: still open — re-ran \`${cmd}\`: `
|
|
432
|
+
+ (r.outcome === 'fail' ?
|
|
433
|
+
`it FAILED${r.detail ? ` (${r.detail})` : ''}`
|
|
434
|
+
: `INCONCLUSIVE${r.detail ? ` (${r.detail})` : ''}, nothing was observed`));
|
|
435
|
+
open.push(d);
|
|
303
436
|
}
|
|
304
|
-
return { open, resolved };
|
|
437
|
+
return { open, resolved, trail };
|
|
305
438
|
}
|
|
306
439
|
// ─── Conflicting-claim classification (mx5 run 11) ──────────────────────────
|
|
307
440
|
//
|
|
@@ -1290,6 +1290,15 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1290
1290
|
openDebts: fresh.openDebts,
|
|
1291
1291
|
...(fresh.debtNote ? { debtNote: fresh.debtNote } : {})
|
|
1292
1292
|
};
|
|
1293
|
+
// Per-debt evidence from the VERIFY-COMMAND re-check
|
|
1294
|
+
// (nexttask 5): which command was re-run and what it did. A
|
|
1295
|
+
// close that cannot be read back from the trail is a close
|
|
1296
|
+
// nobody can audit, and an INCONCLUSIVE re-run is worth
|
|
1297
|
+
// saying out loud — it is the difference between "still
|
|
1298
|
+
// broken" and "nothing could observe it".
|
|
1299
|
+
for (const line of fresh.trail ?? []) {
|
|
1300
|
+
await recGate(`defect re-check: ${line}`);
|
|
1301
|
+
}
|
|
1293
1302
|
// Identity is (task, origin, reason), but a RESOLUTION claim
|
|
1294
1303
|
// needs more than a key miss: a ledger entry whose TEXT changed
|
|
1295
1304
|
// is the same defect re-recorded, never a fix. So a debt counts
|
|
@@ -1309,7 +1318,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
1309
1318
|
return;
|
|
1310
1319
|
}
|
|
1311
1320
|
for (const d of closed) {
|
|
1312
|
-
await recGate(`defect RESOLVED
|
|
1321
|
+
await recGate(`defect RESOLVED — ${d.taskId || '(unknown task)'}: `
|
|
1313
1322
|
+ `${d.reason.slice(0, 240)}`);
|
|
1314
1323
|
}
|
|
1315
1324
|
await surfaceOpenDebts(added);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type HealthCommand } from './repo-health-check.js';
|
|
2
|
-
import { type AcceptDebt } from './accept-debt.js';
|
|
2
|
+
import { type AcceptDebt, type VerifyRerunResult } from './accept-debt.js';
|
|
3
3
|
import { type RenderOutcome } from './render-check.js';
|
|
4
4
|
import { type DeepRenderOutcome } from './deep-render-check.js';
|
|
5
5
|
import { taskThatIntroduced } from './task-provenance.js';
|
|
@@ -344,6 +344,40 @@ export declare function discoverGateCommandBodies(cwd: string): Record<string, s
|
|
|
344
344
|
* this box; the same wording in a `test` run is a real failure the suite must own).
|
|
345
345
|
*/
|
|
346
346
|
export declare const INFRA_GAP_OUTPUT_RE: RegExp;
|
|
347
|
+
/**
|
|
348
|
+
* How a re-run of ONE recorded VERIFY command line ended.
|
|
349
|
+
* pass — it ran and exited 0. The ONLY outcome that may close a debt.
|
|
350
|
+
* fail — it ran and exited non-zero for a real reason. Debt stays open.
|
|
351
|
+
* gap — nothing was observed: the shell/runner never spawned, 127 inside the
|
|
352
|
+
* chain, a timeout, a missing browser, or absent external infrastructure.
|
|
353
|
+
* INCONCLUSIVE, so the debt stays open (surface, never re-hide).
|
|
354
|
+
*/
|
|
355
|
+
export type VerifyRerunOutcome = {
|
|
356
|
+
outcome: 'pass';
|
|
357
|
+
} | {
|
|
358
|
+
outcome: 'fail';
|
|
359
|
+
status: number;
|
|
360
|
+
tail: string;
|
|
361
|
+
} | {
|
|
362
|
+
outcome: 'gap';
|
|
363
|
+
detail: string;
|
|
364
|
+
};
|
|
365
|
+
/**
|
|
366
|
+
* Re-run one VERIFY-block command line (nexttask 5) under the gate's existing
|
|
367
|
+
* env-gap contract, so a debt whose reason NAMES that command can be closed by the
|
|
368
|
+
* command itself rather than by a judgement about it.
|
|
369
|
+
*
|
|
370
|
+
* Runs through `sh -c` because a VERIFY line is a shell line, not an argv: run 19's
|
|
371
|
+
* is `AGENT=1 bun test test/listings.test.ts`, and env prefixes, `&&` and redirects
|
|
372
|
+
* are all ordinary there. The leading command word is still resolved through
|
|
373
|
+
* runner-resolve so a login-shell-stripped PATH cannot make every re-run look like a
|
|
374
|
+
* gap (mx5 run 16's blindness, one level down).
|
|
375
|
+
*
|
|
376
|
+
* The asymmetry is the point: only exit 0 is conclusive. Every other ending — real
|
|
377
|
+
* failure, missing tool, unreachable database, timeout, no POSIX shell — leaves the
|
|
378
|
+
* debt exactly as open as it was.
|
|
379
|
+
*/
|
|
380
|
+
export declare function runVerifyCommandLine(cwd: string, line: string, timeoutMs: number, extraGapRe?: RegExp): VerifyRerunOutcome;
|
|
347
381
|
/**
|
|
348
382
|
* The full-skip blindness guard (mx5 run 16, validated): dynamic commands were
|
|
349
383
|
* DISCOVERED but every single one skipped as an environment gap, so the gate
|
|
@@ -484,7 +518,26 @@ export { taskThatIntroduced };
|
|
|
484
518
|
export declare function deriveOpenDebts(cwd: string, staticOk: boolean): Promise<{
|
|
485
519
|
openDebts: AcceptDebt[];
|
|
486
520
|
debtNote?: string;
|
|
521
|
+
trail?: string[];
|
|
487
522
|
}>;
|
|
523
|
+
/**
|
|
524
|
+
* Re-run ONE debt's stored VERIFY command for the re-check, with the no-write guard
|
|
525
|
+
* (`inv-no-write`) wrapped around it.
|
|
526
|
+
*
|
|
527
|
+
* A VERIFY command is the project's own command and may legitimately write (a build
|
|
528
|
+
* emits `dist/`, a suite writes a snapshot). What it may NOT do is turn the tree into
|
|
529
|
+
* a passing tree and have that count as the debt being fixed — the run would then be
|
|
530
|
+
* certifying its own side effect. So tracked state is captured before and after, and
|
|
531
|
+
* a pass that came with a tracked change is downgraded to INCONCLUSIVE with the
|
|
532
|
+
* change named. Untracked output is left alone: it is what a build legitimately
|
|
533
|
+
* produces, and `git status --porcelain` in a repo with the usual ignores does not
|
|
534
|
+
* see it.
|
|
535
|
+
*
|
|
536
|
+
* A repository the guard cannot read (no git, git absent) is not a licence to skip
|
|
537
|
+
* the guard: the re-run is INCONCLUSIVE there, because "nothing changed" would be an
|
|
538
|
+
* assumption rather than an observation.
|
|
539
|
+
*/
|
|
540
|
+
export declare function rerunDebtVerifyCommand(cwd: string, command: string): VerifyRerunResult;
|
|
488
541
|
/**
|
|
489
542
|
* Run the final gate: static analysis first, then the lockfile consistency
|
|
490
543
|
* checks, then the discovered integration commands, then one boot exercise of
|