@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.
Files changed (35) hide show
  1. package/dist/config/config.d.ts +11 -0
  2. package/dist/config/config.js +4 -1
  3. package/dist/config/register.js +5 -0
  4. package/dist/task/accept-debt.d.ts +52 -0
  5. package/dist/task/accept-debt.js +0 -0
  6. package/dist/task/auto-orchestrator.d.ts +2 -0
  7. package/dist/task/auto-orchestrator.js +20 -0
  8. package/dist/task/enforce-guidelines.d.ts +2 -2
  9. package/dist/task/enforce-guidelines.js +36 -3
  10. package/dist/task/env-notes.d.ts +24 -8
  11. package/dist/task/env-notes.js +124 -24
  12. package/dist/task/final-gate.d.ts +8 -0
  13. package/dist/task/final-gate.js +27 -7
  14. package/dist/task/frozen-path-guard.d.ts +39 -0
  15. package/dist/task/frozen-path-guard.js +116 -0
  16. package/dist/task/gate-deps.d.ts +10 -0
  17. package/dist/task/gate-deps.js +122 -3
  18. package/dist/task/probe-gaming.d.ts +60 -0
  19. package/dist/task/probe-gaming.js +0 -0
  20. package/dist/task/repo-health-check.d.ts +11 -0
  21. package/dist/task/repo-health-check.js +26 -3
  22. package/dist/task/task-gates.d.ts +24 -0
  23. package/dist/task/task-gates.js +78 -8
  24. package/dist/task/test-assembly.d.ts +87 -0
  25. package/dist/task/test-assembly.js +163 -0
  26. package/dist/task/verify-work.d.ts +17 -1
  27. package/dist/task/verify-work.js +87 -2
  28. package/dist/workers/pi-worker-docs.js +13 -1
  29. package/dist/workers/pi-worker-fetch.js +10 -1
  30. package/dist/workers/pi-worker-search.js +9 -1
  31. package/dist/workers/research-cache.d.ts +39 -0
  32. package/dist/workers/research-cache.js +140 -0
  33. package/dist/workers/shared.d.ts +17 -0
  34. package/dist/workers/shared.js +0 -0
  35. 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
+ }
@@ -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;
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.18.3",
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": {