@mjasnikovs/pi-task 0.18.3 → 0.18.5
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/config/config.d.ts +11 -0
- package/dist/config/config.js +4 -1
- package/dist/config/register.js +5 -0
- package/dist/task/accept-debt.d.ts +52 -0
- package/dist/task/accept-debt.js +0 -0
- package/dist/task/auto-orchestrator.d.ts +2 -0
- package/dist/task/auto-orchestrator.js +20 -0
- package/dist/task/enforce-guidelines.d.ts +2 -2
- package/dist/task/enforce-guidelines.js +36 -3
- package/dist/task/env-notes.d.ts +24 -8
- package/dist/task/env-notes.js +124 -24
- package/dist/task/final-gate.d.ts +8 -0
- package/dist/task/final-gate.js +27 -7
- package/dist/task/frozen-path-guard.d.ts +39 -0
- package/dist/task/frozen-path-guard.js +116 -0
- package/dist/task/gate-deps.d.ts +10 -0
- package/dist/task/gate-deps.js +122 -3
- package/dist/task/probe-gaming.d.ts +60 -0
- package/dist/task/probe-gaming.js +0 -0
- package/dist/task/repo-health-check.d.ts +11 -0
- package/dist/task/repo-health-check.js +26 -3
- package/dist/task/task-gates.d.ts +24 -0
- package/dist/task/task-gates.js +78 -8
- package/dist/task/test-assembly.d.ts +87 -0
- package/dist/task/test-assembly.js +163 -0
- package/dist/task/verify-work.d.ts +17 -1
- package/dist/task/verify-work.js +87 -2
- package/dist/workers/pi-worker-docs.js +13 -1
- package/dist/workers/pi-worker-fetch.js +10 -1
- package/dist/workers/pi-worker-search.js +9 -1
- package/dist/workers/research-cache.d.ts +39 -0
- package/dist/workers/research-cache.js +140 -0
- package/dist/workers/shared.d.ts +17 -0
- package/dist/workers/shared.js +0 -0
- package/package.json +2 -2
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* research-cache — a per-run cache of docs/search/fetch worker RESULTS, shared
|
|
3
|
+
* across the sibling task pipelines of one /task-auto run.
|
|
4
|
+
*
|
|
5
|
+
* The failure this serves (mx5 run 8, F10): the research phase alone burned 75 of
|
|
6
|
+
* 363 minutes because ~20 sibling task pipelines each re-fetched the SAME external
|
|
7
|
+
* docs and re-ran the SAME searches (the tailwind CLI docs fetched anew for task
|
|
8
|
+
* after task). Each of those worker results is a deterministic function of (tool,
|
|
9
|
+
* package/url, query) that does not change within a run — so the first pipeline to
|
|
10
|
+
* ask a question can answer every later one from a shared digest instead of a fresh
|
|
11
|
+
* network round-trip plus child-summariser spawn.
|
|
12
|
+
*
|
|
13
|
+
* SCOPE — stable external lookups only: npm-package docs, web search, web fetch. A
|
|
14
|
+
* PROJECT-SOURCE (`.`) docs lookup is deliberately NOT cached: the working tree
|
|
15
|
+
* mutates as tasks implement, so a `.` answer from an early task can be stale by a
|
|
16
|
+
* later one (the docs SQLite index already keys those on file mtime). Only a result
|
|
17
|
+
* the tool marks successful is cached — an error, an empty result, or an abort is
|
|
18
|
+
* never memoised, so a transient failure cannot poison the run.
|
|
19
|
+
*
|
|
20
|
+
* PER-RUN ISOLATION: the orchestrator stamps a FRESH run id into the environment
|
|
21
|
+
* (PI_TASK_RUN_ID) at the start of every /task-auto invocation; the research-worker
|
|
22
|
+
* children inherit it. The cache file records the run id it was written for, and any
|
|
23
|
+
* read or write for a different id discards the stale contents. So a long-lived host
|
|
24
|
+
* process running many /task-auto runs never serves one run's digest to another, and
|
|
25
|
+
* a run started with the feature flag OFF (no id in the environment) does not cache
|
|
26
|
+
* at all — the cache is inert unless the orchestrator turned it on for this run.
|
|
27
|
+
*
|
|
28
|
+
* Stored under `.pi-tasks/` (sibling of env-notes.md / contracts.md), which the
|
|
29
|
+
* git-state guard and discardEdits both exclude. Best-effort throughout: any I/O or
|
|
30
|
+
* parse failure falls back to a live fetch — the cache only ever saves time, it can
|
|
31
|
+
* never change an answer or block a worker.
|
|
32
|
+
*/
|
|
33
|
+
import * as fsp from 'node:fs/promises';
|
|
34
|
+
import * as path from 'node:path';
|
|
35
|
+
import { tasksDir } from '../task/task-io.js';
|
|
36
|
+
const RESEARCH_CACHE_FILE = 'research-cache.json';
|
|
37
|
+
/** The env var the orchestrator stamps with the per-run id children inherit. */
|
|
38
|
+
export const RESEARCH_RUN_ID_ENV = 'PI_TASK_RUN_ID';
|
|
39
|
+
/**
|
|
40
|
+
* Cap stored entries so a chatty run cannot grow the file unboundedly; the newest
|
|
41
|
+
* (by write time) are kept. Sized well above a 20-task run's distinct external
|
|
42
|
+
* lookups (dozens), so a real run never evicts a still-useful digest.
|
|
43
|
+
*/
|
|
44
|
+
const MAX_ENTRIES = 250;
|
|
45
|
+
export function researchCacheFile(cwd) {
|
|
46
|
+
return path.join(tasksDir(cwd), RESEARCH_CACHE_FILE);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The current run's id, or undefined when caching is off (the orchestrator did not
|
|
50
|
+
* stamp one for this run). A worker treats undefined as "do not cache".
|
|
51
|
+
*/
|
|
52
|
+
export function researchRunId() {
|
|
53
|
+
const v = process.env[RESEARCH_RUN_ID_ENV]?.trim();
|
|
54
|
+
return v && v.length > 0 ? v : undefined;
|
|
55
|
+
}
|
|
56
|
+
/** A fresh, per-invocation run token — stable within one run, unique across runs. */
|
|
57
|
+
export function newRunToken() {
|
|
58
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Orchestrator hook: called once at the start of every /task-auto invocation. When
|
|
62
|
+
* caching is enabled it stamps a FRESH token (so a long-lived host never reuses a
|
|
63
|
+
* prior run's token, and planAuto + the task loop of THIS run share one id); when
|
|
64
|
+
* disabled it clears any token a prior run left, so the workers cache nothing.
|
|
65
|
+
*/
|
|
66
|
+
export function configureResearchRun(enabled) {
|
|
67
|
+
if (!enabled) {
|
|
68
|
+
delete process.env[RESEARCH_RUN_ID_ENV];
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const token = newRunToken();
|
|
72
|
+
process.env[RESEARCH_RUN_ID_ENV] = token;
|
|
73
|
+
return token;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Normalise a query/module string for the cache KEY: collapse whitespace and
|
|
77
|
+
* lowercase, so trivially-varied phrasings of the same question share a digest. The
|
|
78
|
+
* stored value is the real answer, so a case/spacing collision only means two ways
|
|
79
|
+
* of asking the same thing resolve to the same (correct) result.
|
|
80
|
+
*/
|
|
81
|
+
export function normalizeQuery(s) {
|
|
82
|
+
return s.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
async function readCacheFile(cwd) {
|
|
85
|
+
try {
|
|
86
|
+
const raw = await fsp.readFile(researchCacheFile(cwd), 'utf8');
|
|
87
|
+
const parsed = JSON.parse(raw);
|
|
88
|
+
if (parsed
|
|
89
|
+
&& typeof parsed === 'object'
|
|
90
|
+
&& typeof parsed.runId === 'string'
|
|
91
|
+
&& typeof parsed.entries === 'object'
|
|
92
|
+
&& parsed.entries !== null) {
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// missing or corrupt ⇒ treated as empty
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Look up a cached result for `key` in the current run. Returns undefined on a miss,
|
|
103
|
+
* a stale-run file (different id ⇒ another run's digest, ignored), or any failure.
|
|
104
|
+
*/
|
|
105
|
+
export async function lookupResearch(cwd, runId, key) {
|
|
106
|
+
const file = await readCacheFile(cwd);
|
|
107
|
+
if (!file || file.runId !== runId)
|
|
108
|
+
return undefined;
|
|
109
|
+
const entry = file.entries[key];
|
|
110
|
+
return entry ? { text: entry.text, details: entry.details } : undefined;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Store a successful result under `key` for the current run. A file written for a
|
|
114
|
+
* different run id is discarded and started fresh (first write of a new run drops the
|
|
115
|
+
* prior run's contents — self-healing per-run isolation without an explicit clear).
|
|
116
|
+
* Best-effort: any failure is swallowed, leaving the caller's live result untouched.
|
|
117
|
+
*/
|
|
118
|
+
export async function storeResearch(cwd, runId, key, text, details) {
|
|
119
|
+
try {
|
|
120
|
+
const existing = await readCacheFile(cwd);
|
|
121
|
+
const entries = existing && existing.runId === runId ? existing.entries : {};
|
|
122
|
+
entries[key] = { text, details, at: Date.now() };
|
|
123
|
+
// Evict oldest by write time if over the cap.
|
|
124
|
+
const keys = Object.keys(entries);
|
|
125
|
+
if (keys.length > MAX_ENTRIES) {
|
|
126
|
+
const ordered = keys.sort((a, b) => entries[a].at - entries[b].at);
|
|
127
|
+
for (const k of ordered.slice(0, keys.length - MAX_ENTRIES))
|
|
128
|
+
delete entries[k];
|
|
129
|
+
}
|
|
130
|
+
const out = { runId, entries };
|
|
131
|
+
await fsp.mkdir(tasksDir(cwd), { recursive: true });
|
|
132
|
+
// Atomic-ish write so a concurrent reader never sees a half-written file.
|
|
133
|
+
const tmp = `${researchCacheFile(cwd)}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
134
|
+
await fsp.writeFile(tmp, JSON.stringify(out), 'utf8');
|
|
135
|
+
await fsp.rename(tmp, researchCacheFile(cwd));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// best-effort cache
|
|
139
|
+
}
|
|
140
|
+
}
|
package/dist/workers/shared.d.ts
CHANGED
|
@@ -38,6 +38,23 @@ export interface WorkerToolSpec<TParams extends TSchema, TDetails> {
|
|
|
38
38
|
details: TDetails;
|
|
39
39
|
}>;
|
|
40
40
|
renderCall(args: Static<TParams>, theme: Theme): Text;
|
|
41
|
+
/**
|
|
42
|
+
* Per-run research-cache policy (F10). Return a stable cache key for this call —
|
|
43
|
+
* a result keyed on it is a deterministic function of the inputs that does not
|
|
44
|
+
* change within a run, so a later sibling task can reuse it instead of re-running
|
|
45
|
+
* the network fetch + child summariser. Return `null` to opt a particular call
|
|
46
|
+
* OUT of caching (e.g. a project-source `.` lookup, whose answer the working tree
|
|
47
|
+
* mutates within a run). Omit entirely and the tool is never cached. The stored
|
|
48
|
+
* key is namespaced by tool name, so keys need only be unique within a tool.
|
|
49
|
+
*/
|
|
50
|
+
cacheKey?(params: Static<TParams>): string | null;
|
|
51
|
+
/**
|
|
52
|
+
* Whether a produced result is safe to cache. Only a SUCCESS is memoised — an
|
|
53
|
+
* error, empty, or aborted result must fall through so a transient failure never
|
|
54
|
+
* poisons the run. Defaults to always-cacheable when omitted (but a tool with a
|
|
55
|
+
* cacheKey should always supply this).
|
|
56
|
+
*/
|
|
57
|
+
cacheable?(details: TDetails, text: string): boolean;
|
|
41
58
|
}
|
|
42
59
|
/** Register a worker tool from its spec, supplying the shared registration ritual. */
|
|
43
60
|
export declare function makeWorkerTool<TParams extends TSchema, TDetails>(pi: ExtensionAPI, spec: WorkerToolSpec<TParams, TDetails>): void;
|
package/dist/workers/shared.js
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.5",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc -p tsconfig.build.json",
|
|
16
16
|
"lint": "prettier --log-level warn --write 'src/**/*.ts' && eslint --fix . && tsc --noEmit",
|
|
17
|
-
"test": "bun test src/",
|
|
17
|
+
"test": "AGENT=1 bun test src/",
|
|
18
18
|
"prepublishOnly": "bun run build"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|