@mjasnikovs/pi-task 0.17.10 → 0.17.11
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
CHANGED
|
@@ -19,6 +19,7 @@ import { tasksDir, readTaskFile } 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';
|
|
22
|
+
import { runRepoHealthCheck } from './repo-health-check.js';
|
|
22
23
|
import { researchResolution } from './verify-resolution.js';
|
|
23
24
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
24
25
|
import { formatLoopHint } from './child-runner.js';
|
|
@@ -195,7 +196,11 @@ export function buildGateDeps(params) {
|
|
|
195
196
|
cwd: cwd2,
|
|
196
197
|
signal,
|
|
197
198
|
spec,
|
|
198
|
-
runChild: makeGateChild(verifyCtx, cwd2, taskTitle, 'verify', 'verify-debug.log')
|
|
199
|
+
runChild: makeGateChild(verifyCtx, cwd2, taskTitle, 'verify', 'verify-debug.log'),
|
|
200
|
+
// Deterministic whole-repo static-analysis gate — runs the project's
|
|
201
|
+
// own lint/typecheck and fails on a real non-zero exit, independent of
|
|
202
|
+
// the model-authored VERIFY block (which may not lint at all).
|
|
203
|
+
repoHealth: () => Promise.resolve(runRepoHealthCheck(cwd2))
|
|
199
204
|
});
|
|
200
205
|
},
|
|
201
206
|
recommend: async (recCtx, cwd2, taskTitle, taskId, failReason) => {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface HealthOutcome {
|
|
2
|
+
/** true → every discovered static check passed, or there was nothing to run.
|
|
3
|
+
* false → a discovered command actually ran and exited non-zero. */
|
|
4
|
+
ok: boolean;
|
|
5
|
+
/** Human-readable reason. On a fail, names the exact command and exit code. */
|
|
6
|
+
reason: string;
|
|
7
|
+
/** Which manifest drove discovery, or null when none was found. */
|
|
8
|
+
ecosystem: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** One discovered command: the binary and its args, run from the repo root. */
|
|
11
|
+
export type HealthCommand = [bin: string, args: string[]];
|
|
12
|
+
/**
|
|
13
|
+
* Discover the project's OWN whole-repo static-analysis commands. First manifest
|
|
14
|
+
* that exists wins; returns only the STATIC commands actually available for that
|
|
15
|
+
* ecosystem (never test/build/run). An empty list means "nothing static to run".
|
|
16
|
+
*/
|
|
17
|
+
export declare function discoverHealthCommands(cwd: string): {
|
|
18
|
+
ecosystem: string | null;
|
|
19
|
+
cmds: HealthCommand[];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Run the discovered static checks whole-repo and let the real exit codes decide.
|
|
23
|
+
* Deterministic and synchronous under the hood (a wrapper keeps the caller async).
|
|
24
|
+
*
|
|
25
|
+
* - No manifest / no static command → ok (nothing can regress).
|
|
26
|
+
* - A command that CANNOT run (ENOENT / null exit — tool not installed) → skipped,
|
|
27
|
+
* treated as an environment gap, not a fault.
|
|
28
|
+
* - A command that ran and exited non-zero → the first such failure is returned.
|
|
29
|
+
*
|
|
30
|
+
* A generous per-command timeout guards against a wedged tool; a timeout is treated
|
|
31
|
+
* as an inconclusive skip, not a fault (it is an environment problem, not the code's).
|
|
32
|
+
*/
|
|
33
|
+
export declare function runRepoHealthCheck(cwd: string, timeoutMs?: number): HealthOutcome;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* repo-health-check — the deterministic, whole-repo half of the verify gate.
|
|
3
|
+
*
|
|
4
|
+
* The failure this closes (proven on the mx5 run): the verify gate ran only the
|
|
5
|
+
* task's OWN composed VERIFY block, and that block is authored per-task by the local
|
|
6
|
+
* model — so it is inconsistent. Some tasks lint the whole repo, some (TASK_0021)
|
|
7
|
+
* ship a VERIFY of `tsc --noEmit` ONLY, with no lint at all. A/B on the live model,
|
|
8
|
+
* 5 runs/arm on the real dirty tree: the tsc-only task false-PASSed 5/5 while
|
|
9
|
+
* `bun run lint` reported 11 errors. The model gate is only ever as good as the
|
|
10
|
+
* VERIFY block it happened to be handed.
|
|
11
|
+
*
|
|
12
|
+
* This check does NOT depend on that block. It discovers the project's OWN
|
|
13
|
+
* whole-repo static-analysis command (the one a fresh checkout / CI would run) and
|
|
14
|
+
* lets its REAL exit code decide — no model, so there is no per-file narrowing and
|
|
15
|
+
* no "those errors aren't in my files" gray area. A non-zero exit is a FAIL that the
|
|
16
|
+
* caller turns into the existing verify-FAIL outcome (→ the AUTOFIX / ACCEPT /
|
|
17
|
+
* dismiss picker; the user decides).
|
|
18
|
+
*
|
|
19
|
+
* Scope is deliberately STATIC ANALYSIS ONLY (lint / typecheck / clippy / vet), never
|
|
20
|
+
* `test`, `build`, `run`, or anything that boots a server or needs a database. Those
|
|
21
|
+
* depend on external services the verify prompt already carves out as an environment
|
|
22
|
+
* gap — running them here would re-introduce the false-FAIL that once wrongly blamed
|
|
23
|
+
* code for a missing DB. Static analysis is hermetic: it needs no network, no service,
|
|
24
|
+
* no fixtures, and it is exactly the class of the reported defect.
|
|
25
|
+
*
|
|
26
|
+
* Absence is a PASS, two ways: (1) no recognised manifest at all (a pure-docs or
|
|
27
|
+
* config-only repo has nothing that can regress); (2) a manifest with no static-check
|
|
28
|
+
* command wired up. A tool that is simply not installed (ENOENT / null exit) is an
|
|
29
|
+
* environment gap, not a code fault, so that command is SKIPPED — only a command that
|
|
30
|
+
* actually ran and returned non-zero fails the check.
|
|
31
|
+
*/
|
|
32
|
+
import { spawnSync } from 'node:child_process';
|
|
33
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
34
|
+
import * as path from 'node:path';
|
|
35
|
+
function packageScripts(cwd) {
|
|
36
|
+
try {
|
|
37
|
+
const j = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
38
|
+
return j.scripts ?? {};
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Does the Makefile define a `<target>:` rule? (so `make <target>` won't error out) */
|
|
45
|
+
function makeHasTarget(cwd, target) {
|
|
46
|
+
try {
|
|
47
|
+
const mk = readFileSync(path.join(cwd, 'Makefile'), 'utf8');
|
|
48
|
+
return new RegExp(`^${target}:`, 'm').test(mk);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Discover the project's OWN whole-repo static-analysis commands. First manifest
|
|
56
|
+
* that exists wins; returns only the STATIC commands actually available for that
|
|
57
|
+
* ecosystem (never test/build/run). An empty list means "nothing static to run".
|
|
58
|
+
*/
|
|
59
|
+
export function discoverHealthCommands(cwd) {
|
|
60
|
+
if (existsSync(path.join(cwd, 'package.json'))) {
|
|
61
|
+
const s = packageScripts(cwd);
|
|
62
|
+
const cmds = [];
|
|
63
|
+
// Only static-analysis scripts. `lint` commonly chains tsc (as in mx5's
|
|
64
|
+
// `prettier && eslint && tsc`), so a single `bun run lint` covers both.
|
|
65
|
+
for (const name of ['lint', 'typecheck']) {
|
|
66
|
+
if (s[name])
|
|
67
|
+
cmds.push(['bun', ['run', name]]);
|
|
68
|
+
}
|
|
69
|
+
return { ecosystem: 'package.json', cmds };
|
|
70
|
+
}
|
|
71
|
+
if (existsSync(path.join(cwd, 'Makefile'))) {
|
|
72
|
+
const cmds = [];
|
|
73
|
+
if (makeHasTarget(cwd, 'lint'))
|
|
74
|
+
cmds.push(['make', ['lint']]);
|
|
75
|
+
return { ecosystem: 'Makefile', cmds };
|
|
76
|
+
}
|
|
77
|
+
if (existsSync(path.join(cwd, 'Cargo.toml'))) {
|
|
78
|
+
return { ecosystem: 'Cargo.toml', cmds: [['cargo', ['clippy', '--quiet']]] };
|
|
79
|
+
}
|
|
80
|
+
if (existsSync(path.join(cwd, 'pyproject.toml'))) {
|
|
81
|
+
return { ecosystem: 'pyproject.toml', cmds: [['ruff', ['check', '.']]] };
|
|
82
|
+
}
|
|
83
|
+
if (existsSync(path.join(cwd, 'deno.json')) || existsSync(path.join(cwd, 'deno.jsonc'))) {
|
|
84
|
+
return { ecosystem: 'deno', cmds: [['deno', ['lint']]] };
|
|
85
|
+
}
|
|
86
|
+
if (existsSync(path.join(cwd, 'go.mod'))) {
|
|
87
|
+
return { ecosystem: 'go.mod', cmds: [['go', ['vet', './...']]] };
|
|
88
|
+
}
|
|
89
|
+
return { ecosystem: null, cmds: [] };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Run the discovered static checks whole-repo and let the real exit codes decide.
|
|
93
|
+
* Deterministic and synchronous under the hood (a wrapper keeps the caller async).
|
|
94
|
+
*
|
|
95
|
+
* - No manifest / no static command → ok (nothing can regress).
|
|
96
|
+
* - A command that CANNOT run (ENOENT / null exit — tool not installed) → skipped,
|
|
97
|
+
* treated as an environment gap, not a fault.
|
|
98
|
+
* - A command that ran and exited non-zero → the first such failure is returned.
|
|
99
|
+
*
|
|
100
|
+
* A generous per-command timeout guards against a wedged tool; a timeout is treated
|
|
101
|
+
* as an inconclusive skip, not a fault (it is an environment problem, not the code's).
|
|
102
|
+
*/
|
|
103
|
+
export function runRepoHealthCheck(cwd, timeoutMs = 600_000) {
|
|
104
|
+
const { ecosystem, cmds } = discoverHealthCommands(cwd);
|
|
105
|
+
if (!ecosystem || cmds.length === 0) {
|
|
106
|
+
return { ok: true, reason: 'no repo-wide static-analysis command found', ecosystem };
|
|
107
|
+
}
|
|
108
|
+
for (const [bin, args] of cmds) {
|
|
109
|
+
const r = spawnSync(bin, args, { cwd, encoding: 'utf8', timeout: timeoutMs });
|
|
110
|
+
// Tool missing (ENOENT) or killed by timeout → cannot conclude; skip it.
|
|
111
|
+
if (r.error || r.status === null)
|
|
112
|
+
continue;
|
|
113
|
+
if (r.status !== 0) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
reason: `\`${bin} ${args.join(' ')}\` exited ${r.status}`,
|
|
117
|
+
ecosystem
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { ok: true, reason: `${ecosystem}: static checks passed`, ecosystem };
|
|
122
|
+
}
|
|
@@ -57,6 +57,18 @@ export interface VerificationDeps {
|
|
|
57
57
|
/** Runs the verification child and returns its assistant text. Injected so
|
|
58
58
|
* the orchestrator wires the real child runner and tests use a fake. */
|
|
59
59
|
runChild: (tools: string, prompt: string, signal?: AbortSignal) => Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* DETERMINISTIC whole-repo static-analysis gate (see repo-health-check.ts). Runs
|
|
62
|
+
* the project's OWN lint/typecheck command and lets its real exit code decide —
|
|
63
|
+
* independent of the model-authored VERIFY block, so a repo-wide lint failure can
|
|
64
|
+
* never be narrowed away to "not my files". A non-zero result short-circuits to a
|
|
65
|
+
* FAIL (the existing verify-FAIL outcome → the AUTOFIX/ACCEPT/dismiss picker).
|
|
66
|
+
* Injected so tests fake it; ABSENT → skipped (a pass), keeping the pass path a
|
|
67
|
+
* pure no-op for callers/tests that do not wire it. */
|
|
68
|
+
repoHealth?: () => Promise<{
|
|
69
|
+
ok: boolean;
|
|
70
|
+
reason: string;
|
|
71
|
+
}>;
|
|
60
72
|
}
|
|
61
73
|
/**
|
|
62
74
|
* Run the verification pass for one task. A missing spec is a pass. Otherwise run
|
package/dist/task/verify-work.js
CHANGED
|
@@ -187,6 +187,17 @@ export function parseVerifyVerdict(text) {
|
|
|
187
187
|
* USER_CANCELLED handler reports a clean "cancelled — resume").
|
|
188
188
|
*/
|
|
189
189
|
export async function runWorkVerification(deps) {
|
|
190
|
+
// DETERMINISTIC gate FIRST: run the project's own whole-repo static analysis and
|
|
191
|
+
// let its exit code decide, before spending a model turn. This catches the class
|
|
192
|
+
// the model gate misses — a task whose composed VERIFY block never lints (proven
|
|
193
|
+
// 5/5 false-PASS live) — because it does not depend on that block. A fail is the
|
|
194
|
+
// ordinary verify-FAIL outcome, so it flows into the existing resolution picker.
|
|
195
|
+
// Absent dep, or a no-op result (no tooling to run), falls through to the model.
|
|
196
|
+
if (deps.repoHealth) {
|
|
197
|
+
const h = await deps.repoHealth();
|
|
198
|
+
if (!h.ok)
|
|
199
|
+
return { ok: false, reason: `repo health: ${h.reason}` };
|
|
200
|
+
}
|
|
190
201
|
if (!deps.spec || deps.spec.trim().length === 0) {
|
|
191
202
|
return { ok: true, reason: 'no spec to verify' };
|
|
192
203
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.11",
|
|
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",
|