@mjasnikovs/pi-task 0.17.11 → 0.17.12
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/gate-deps.js +4 -1
- package/dist/task/task-gates.d.ts +10 -0
- package/dist/task/task-gates.js +36 -2
- package/dist/task/task-io.d.ts +9 -0
- package/dist/task/task-io.js +19 -0
- package/dist/task/verify-work.js +33 -6
- package/package.json +1 -1
package/dist/task/gate-deps.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import * as fsp from 'node:fs/promises';
|
|
17
17
|
import * as path from 'node:path';
|
|
18
|
-
import { tasksDir, readTaskFile } from './task-io.js';
|
|
18
|
+
import { tasksDir, readTaskFile, appendGateRecord } from './task-io.js';
|
|
19
19
|
import { gitCommitAll, gitDropLastCommit } from './auto-commit.js';
|
|
20
20
|
import { runGuidelineEnforcement, classifyEnforceChildFailure } from './enforce-guidelines.js';
|
|
21
21
|
import { runWorkVerification, extractSpecForVerification } from './verify-work.js';
|
|
@@ -96,6 +96,9 @@ export function buildGateDeps(params) {
|
|
|
96
96
|
};
|
|
97
97
|
return {
|
|
98
98
|
runTask,
|
|
99
|
+
// Durable per-task gate trail: every verdict/decision lands in the task
|
|
100
|
+
// file's `## gates` section so gate behavior is auditable from artifacts.
|
|
101
|
+
record: (cwd2, taskId, line) => appendGateRecord(cwd2, taskId, line),
|
|
99
102
|
commit: (cwd2, message) => getConfig().autoCommit ?
|
|
100
103
|
gitCommitAll(cwd2, message, signal)
|
|
101
104
|
: Promise.resolve({ committed: false, reason: 'auto-commit disabled' }),
|
|
@@ -77,6 +77,16 @@ export interface GateDeps {
|
|
|
77
77
|
* skips the revert and warns.
|
|
78
78
|
*/
|
|
79
79
|
revert?: (cwd: string) => Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Append one line to the task's durable gate trail (`## gates` in the task
|
|
82
|
+
* file). Every gate outcome — each verify verdict, the user's FAIL resolution,
|
|
83
|
+
* the commit result, enforce mode + verdict, the differential guard's decision —
|
|
84
|
+
* is recorded so the sequence is auditable from artifacts alone (the mx5 audit
|
|
85
|
+
* could not tell WHY 10 of 18 tasks show no enforce run: verdicts lived only in
|
|
86
|
+
* terminal notifies). Best-effort: absent in tests → skipped; failures are
|
|
87
|
+
* swallowed by the implementation, never by this sequence.
|
|
88
|
+
*/
|
|
89
|
+
record?: (cwd: string, taskId: string, line: string) => Promise<void>;
|
|
80
90
|
}
|
|
81
91
|
/** Inputs the sequence needs that vary per caller. */
|
|
82
92
|
export interface GateParams {
|
package/dist/task/task-gates.js
CHANGED
|
@@ -34,6 +34,21 @@ export async function askVerifyResolution(ctx, title, failReason, rec) {
|
|
|
34
34
|
*/
|
|
35
35
|
export async function runGatesForTask(ctxIn, deps, p) {
|
|
36
36
|
let active = ctxIn;
|
|
37
|
+
// Durable per-task gate trail — every outcome below is also appended to the
|
|
38
|
+
// task file so the sequence is auditable from artifacts alone. Best-effort.
|
|
39
|
+
const rec = async (line) => {
|
|
40
|
+
try {
|
|
41
|
+
await deps.record?.(p.cwd, p.taskId, line);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// recording must never break the gate sequence
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const verdictLine = (v) => v.ok ?
|
|
48
|
+
v.reason ?
|
|
49
|
+
`verify: PASS (${v.reason})`
|
|
50
|
+
: 'verify: PASS'
|
|
51
|
+
: `verify: FAIL — ${v.reason ?? 'did not verify'}`;
|
|
37
52
|
// GATE: actually RUN the task's verification against the just-finished work
|
|
38
53
|
// BEFORE it is checked off or committed. Whether this produced a GENUINE clean
|
|
39
54
|
// pass (a real signal ran and the work met it) also decides how the enforce pass
|
|
@@ -44,24 +59,29 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
44
59
|
if (deps.verify) {
|
|
45
60
|
active.ui.notify(`${p.tag}: verifying "${p.title}"…`, 'info');
|
|
46
61
|
let verified = await deps.verify(active, p.cwd, p.title, p.taskId);
|
|
62
|
+
await rec(verdictLine(verified));
|
|
47
63
|
// A FAIL no longer dead-stops: offer the boxed AUTOFIX / ACCEPT / dismiss
|
|
48
64
|
// picker. The USER always decides; AUTOFIX loops straight back to the gate
|
|
49
65
|
// as many times as they keep choosing it (no attempt cap).
|
|
50
66
|
while (!verified.ok) {
|
|
51
67
|
const failReason = verified.reason ?? 'did not verify';
|
|
52
|
-
const
|
|
68
|
+
const recOutcome = deps.recommend ?
|
|
53
69
|
await deps.recommend(active, p.cwd, p.title, p.taskId, failReason)
|
|
54
70
|
: { recommend: 'autofix', rationale: failReason };
|
|
55
|
-
|
|
71
|
+
await rec(`resolution: recommended ${recOutcome.recommend.toUpperCase()}`);
|
|
72
|
+
const choice = await askVerifyResolution(active, p.title, failReason, recOutcome);
|
|
56
73
|
if (choice.action === 'cancel') {
|
|
74
|
+
await rec('resolution: user dismissed the verify-FAIL picker — paused');
|
|
57
75
|
return { kind: 'paused', ctx: active, reason: failReason };
|
|
58
76
|
}
|
|
59
77
|
if (choice.action === 'accept') {
|
|
78
|
+
await rec('resolution: user ACCEPTED the work despite verify FAIL');
|
|
60
79
|
active.ui.notify(`${p.tag}: accepted "${p.title}" despite verify FAIL (${failReason.slice(0, 120)}) — proceeding.`, 'warning');
|
|
61
80
|
break;
|
|
62
81
|
}
|
|
63
82
|
// AUTOFIX: re-run the implementation turn with the failure (and any typed
|
|
64
83
|
// guidance) prepended as a RE-ATTEMPT banner, then re-verify.
|
|
84
|
+
await rec('resolution: user chose AUTOFIX — re-running the implementation turn');
|
|
65
85
|
active.ui.notify(`${p.tag}: autofixing "${p.title}"…`, 'info');
|
|
66
86
|
const fixInstruction = choice.guidance ? `${failReason}\n\nUser guidance: ${choice.guidance}` : failReason;
|
|
67
87
|
const fixRes = await deps.runTask(active, p.cwd, p.title, {
|
|
@@ -78,6 +98,7 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
78
98
|
return { kind: 'failed', ctx: active, reason: fixRes.reason };
|
|
79
99
|
// Resume reuses the same inner task id, so p.taskId is stable.
|
|
80
100
|
verified = await deps.verify(active, p.cwd, p.title, p.taskId);
|
|
101
|
+
await rec(verdictLine(verified));
|
|
81
102
|
}
|
|
82
103
|
// Loop exited because the work verified OR the user accepted the artifact. A
|
|
83
104
|
// genuine clean pass is ok===true with NO reason; a no-op pass or an
|
|
@@ -91,9 +112,11 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
91
112
|
// so a passing task is durably recorded no matter what enforcement later finds.
|
|
92
113
|
const commit = await deps.commit(p.cwd, `task: ${p.title} (${p.taskId})`);
|
|
93
114
|
if (commit.committed) {
|
|
115
|
+
await rec('commit: task snapshot committed');
|
|
94
116
|
active.ui.notify(`${p.tag}: committed "${p.title}".`, 'info');
|
|
95
117
|
}
|
|
96
118
|
else {
|
|
119
|
+
await rec(`commit: skipped (${commit.reason ?? 'unknown'})`);
|
|
97
120
|
active.ui.notify(`${p.tag}: not committed (${commit.reason ?? 'unknown'}) — continuing.`, 'warning');
|
|
98
121
|
}
|
|
99
122
|
// With the task committed, hold its work to AGENTS.md / CLAUDE.md — but as a step
|
|
@@ -106,6 +129,7 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
106
129
|
`${p.tag}: enforcing AGENTS.md/CLAUDE.md on "${p.title}"…`
|
|
107
130
|
: `${p.tag}: reviewing "${p.title}" against AGENTS.md/CLAUDE.md (no verify signal — report only)…`, 'info');
|
|
108
131
|
const verdict = await deps.enforce(active, p.cwd, p.title, mode);
|
|
132
|
+
await rec(`enforce(${mode}): ${verdict.ok ? `clean${verdict.reason ? ` (${verdict.reason})` : ''}` : (verdict.reason ?? 'not clean')}`);
|
|
109
133
|
if (!verdict.ok) {
|
|
110
134
|
active.ui.notify(`${p.tag}: guideline ${mode === 'edit' ? 'enforcement' : 'review'} on "${p.title}" — ${verdict.reason ?? 'not clean'} — continuing.`, 'warning');
|
|
111
135
|
}
|
|
@@ -122,14 +146,24 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
122
146
|
if (!after.ok) {
|
|
123
147
|
if (deps.revert)
|
|
124
148
|
await deps.revert(p.cwd);
|
|
149
|
+
await rec(`enforce: fixes committed but re-verify FAILED (${(after.reason ?? 'now fails').slice(0, 200)}) — ${deps.revert ? 'REVERTED' : 'left in place (no revert available)'}`);
|
|
125
150
|
active.ui.notify(`${p.tag}: guideline fixes regressed verification on "${p.title}" (${(after.reason ?? 'now fails').slice(0, 120)}) — ${deps.revert ? 'reverted them, kept the verified work' : 'left in place (no revert available)'}.`, 'warning');
|
|
126
151
|
}
|
|
127
152
|
else {
|
|
153
|
+
await rec('enforce: fixes committed — re-verify PASS, kept');
|
|
128
154
|
active.ui.notify(`${p.tag}: committed guideline fixes for "${p.title}".`, 'info');
|
|
129
155
|
}
|
|
130
156
|
}
|
|
157
|
+
else {
|
|
158
|
+
await rec('enforce(edit): no fixes to commit');
|
|
159
|
+
}
|
|
131
160
|
}
|
|
132
161
|
// 'flag' mode makes no edits — nothing to commit or revert.
|
|
133
162
|
}
|
|
163
|
+
else if (deps.enforce) {
|
|
164
|
+
// deps.enforce wired but nothing was committed this round — record the skip
|
|
165
|
+
// so a missing enforce run is explainable from the trail (mx5 audit gap).
|
|
166
|
+
await rec('enforce: skipped (nothing committed this round)');
|
|
167
|
+
}
|
|
134
168
|
return { kind: 'done', ctx: active };
|
|
135
169
|
}
|
package/dist/task/task-io.d.ts
CHANGED
|
@@ -21,5 +21,14 @@ export declare function writeTaskFile(cwd: string, fm: TaskFrontMatter, body: st
|
|
|
21
21
|
export declare function updateTaskFrontMatter(cwd: string, id: string, patch: Partial<TaskFrontMatter>): Promise<void>;
|
|
22
22
|
export declare function readSection(cwd: string, id: string, heading: string): Promise<string | null>;
|
|
23
23
|
export declare function setTaskSection(cwd: string, id: string, heading: string, content: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Append one timestamped line to the task's `## gates` section — the durable
|
|
26
|
+
* per-task trail of gate outcomes (verify verdicts, enforce mode/verdict, commit
|
|
27
|
+
* results). Motivated by the mx5 audit: verdict text lived only in memory and
|
|
28
|
+
* terminal notifies, so "did enforce run for this task, and in which mode?" was
|
|
29
|
+
* unanswerable from artifacts. Best-effort by design: a failure to record must
|
|
30
|
+
* never break the gate sequence, so all errors are swallowed.
|
|
31
|
+
*/
|
|
32
|
+
export declare function appendGateRecord(cwd: string, id: string, line: string): Promise<void>;
|
|
24
33
|
/** Remove a section (heading + body) if present; a no-op when it's absent. */
|
|
25
34
|
export declare function removeTaskSection(cwd: string, id: string, heading: string): Promise<void>;
|
package/dist/task/task-io.js
CHANGED
|
@@ -112,6 +112,25 @@ export async function setTaskSection(cwd, id, heading, content) {
|
|
|
112
112
|
}
|
|
113
113
|
await writeTaskFile(cwd, { ...frontMatter, updated_at: new Date().toISOString() }, next);
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Append one timestamped line to the task's `## gates` section — the durable
|
|
117
|
+
* per-task trail of gate outcomes (verify verdicts, enforce mode/verdict, commit
|
|
118
|
+
* results). Motivated by the mx5 audit: verdict text lived only in memory and
|
|
119
|
+
* terminal notifies, so "did enforce run for this task, and in which mode?" was
|
|
120
|
+
* unanswerable from artifacts. Best-effort by design: a failure to record must
|
|
121
|
+
* never break the gate sequence, so all errors are swallowed.
|
|
122
|
+
*/
|
|
123
|
+
export async function appendGateRecord(cwd, id, line) {
|
|
124
|
+
try {
|
|
125
|
+
const stamp = new Date().toISOString();
|
|
126
|
+
const entry = `- ${stamp} ${line.replace(/\s*\n\s*/g, ' ').trim()}`;
|
|
127
|
+
const existing = await readSection(cwd, id, 'gates');
|
|
128
|
+
await setTaskSection(cwd, id, 'gates', existing ? `${existing}\n${entry}` : entry);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Recording is observability, not control flow — never propagate.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
115
134
|
/** Remove a section (heading + body) if present; a no-op when it's absent. */
|
|
116
135
|
export async function removeTaskSection(cwd, id, heading) {
|
|
117
136
|
const { frontMatter, body } = await readTaskFile(cwd, id);
|
package/dist/task/verify-work.js
CHANGED
|
@@ -39,6 +39,20 @@
|
|
|
39
39
|
* genuinely-broken shipped build FAILs 3/3, and a genuine external-service gap — which
|
|
40
40
|
* the OLD prompt wrongly blamed on the code 3/3 — now correctly PASSes 3/3.
|
|
41
41
|
*
|
|
42
|
+
* The sibling failure class is GREP-THEATER (mx5 run 2, TASK_0002): the composed VERIFY
|
|
43
|
+
* block was grep-only, so the child "verified" a schema.sql containing INVALID SQL by
|
|
44
|
+
* grepping for its own broken text — while a live PostgreSQL sat reachable in the same
|
|
45
|
+
* container, never touched. The prompt now (a) says a grep-only VERIFY block does not cap
|
|
46
|
+
* the obligation to execute/apply an executable artifact, and (b) requires PROBING a
|
|
47
|
+
* declared external service before invoking the absent-service exception — reachable ⇒
|
|
48
|
+
* the real verification must run against it. A/B on the faithful fixture (invalid
|
|
49
|
+
* `generate always as` schema, grep-only VERIFY, unadvertised trust-auth PostgreSQL on
|
|
50
|
+
* the default port, DATABASE_URL unset — exactly the mx5 shape): old prompt false-PASSed
|
|
51
|
+
* 4/5; new prompt FAILed 5/5, each naming the real syntax error. Guards: explicit-URL
|
|
52
|
+
* variant old 5/5 / new 5/5 correct-FAIL (no regression), valid schema 3/3 PASS (no
|
|
53
|
+
* paranoia), unreachable-DB 2/3 PASS via the env-gap exception + 1 conservative FAIL
|
|
54
|
+
* that still named the genuine SQL defect (safe direction — only false-PASS trashes work).
|
|
55
|
+
*
|
|
42
56
|
* It runs as a GATE right after the implementation turn, BEFORE the task is
|
|
43
57
|
* checked off or committed. A FAIL stops the /task-auto run exactly like an
|
|
44
58
|
* implementation failure: the task is left unchecked and uncommitted so
|
|
@@ -138,18 +152,31 @@ export function buildVerifyPrompt(spec) {
|
|
|
138
152
|
'3. Presence of a token / directive / string in a SOURCE file is NOT verification.',
|
|
139
153
|
' Judge the produced artifact and the real runtime behavior. A raw build directive',
|
|
140
154
|
' that SURVIVES into the built output means the build never ran — that is a FAIL.',
|
|
155
|
+
" The spec's VERIFY block does not cap this obligation: if that block only greps or",
|
|
156
|
+
' reads source files but the deliverable can be EXECUTED or APPLIED on this machine',
|
|
157
|
+
' (a script, a schema, a server, a config a tool can load), you must ALSO execute or',
|
|
158
|
+
' apply it and judge the real result. Grep-only checks passing while the artifact was',
|
|
159
|
+
' never executed is NOT a PASS — a static match cannot prove an artifact works, only',
|
|
160
|
+
' running it can.',
|
|
141
161
|
'',
|
|
142
162
|
'4. Treat the ACCEPTANCE criteria as the bar. If a command fails, or its real output',
|
|
143
163
|
' contradicts an ACCEPTANCE criterion, the work has NOT verified.',
|
|
144
164
|
'',
|
|
145
165
|
'5. The ONLY thing you may assume is already provided is a genuinely EXTERNAL running',
|
|
146
166
|
' service or network resource (a database server, an API host) that the project',
|
|
147
|
-
' documents as a prerequisite.
|
|
148
|
-
' service
|
|
149
|
-
'
|
|
150
|
-
'
|
|
151
|
-
'
|
|
152
|
-
' run
|
|
167
|
+
' documents as a prerequisite. Before you rely on that assumption, PROBE for the',
|
|
168
|
+
' service with a cheap real check using the connection details the project/spec',
|
|
169
|
+
' already declares (attempt the connection: pg_isready, a client one-liner, curl',
|
|
170
|
+
' with a short timeout). If the probe shows the service IS reachable, the exception',
|
|
171
|
+
' does NOT apply — you must run the real verification against it (apply the schema,',
|
|
172
|
+
' run the suite, hit the endpoint) and judge the real result; falling back to static',
|
|
173
|
+
' checks with the service available is NOT verification. Only if a command fails',
|
|
174
|
+
' purely because such an external service is genuinely ABSENT from this machine —',
|
|
175
|
+
' and NOT because the project misconfigures how it connects — note that as an',
|
|
176
|
+
' environment gap and judge the rest; do not fail the code for it. But a command',
|
|
177
|
+
' that fails because of how the PROJECT ITSELF is wired (config it owns but does',
|
|
178
|
+
' not load, a wrong default, a command that cannot run unaided) is a defect, not an',
|
|
179
|
+
' environment gap.',
|
|
153
180
|
'',
|
|
154
181
|
'6. If the spec legitimately has no runnable verification (a pure docs / config change',
|
|
155
182
|
' with nothing to build or run), validating it cleanly is a PASS.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.12",
|
|
4
4
|
"description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|