@mjasnikovs/pi-task 0.39.4 → 0.40.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.
Files changed (45) hide show
  1. package/README.md +20 -5
  2. package/dist/task/auto-orchestrator.d.ts +36 -0
  3. package/dist/task/auto-orchestrator.js +43 -6
  4. package/dist/task/cancel-points.d.ts +34 -6
  5. package/dist/task/cancel-points.js +62 -10
  6. package/dist/task/child-status.js +13 -1
  7. package/dist/task/context-attribution.js +18 -6
  8. package/dist/task/external-context.d.ts +8 -1
  9. package/dist/task/external-context.js +52 -4
  10. package/dist/task/orchestrator.js +47 -5
  11. package/dist/task/phases.js +2 -1
  12. package/dist/task/plan-orchestrator.js +10 -1
  13. package/dist/task/prompts.d.ts +7 -1
  14. package/dist/task/prompts.js +14 -4
  15. package/dist/task/research-worker.js +11 -0
  16. package/dist/task/run-bracket.js +13 -1
  17. package/dist/task/task-gates.js +34 -0
  18. package/dist/workers/docs-cache.js +50 -3
  19. package/dist/workers/docs-chunk.d.ts +6 -3
  20. package/dist/workers/docs-chunk.js +8 -5
  21. package/dist/workers/docs-core.d.ts +27 -3
  22. package/dist/workers/docs-core.js +104 -41
  23. package/dist/workers/docs-ecosystems.d.ts +173 -0
  24. package/dist/workers/docs-ecosystems.js +449 -0
  25. package/dist/workers/docs-index.d.ts +2 -1
  26. package/dist/workers/docs-index.js +55 -27
  27. package/dist/workers/docs-project.d.ts +10 -0
  28. package/dist/workers/docs-project.js +86 -24
  29. package/dist/workers/docs-resolve.d.ts +6 -1
  30. package/dist/workers/docs-resolve.js +4 -3
  31. package/dist/workers/docs-retrieve.d.ts +2 -0
  32. package/dist/workers/docs-retrieve.js +11 -11
  33. package/dist/workers/eco-cargo.d.ts +115 -0
  34. package/dist/workers/eco-cargo.js +793 -0
  35. package/dist/workers/eco-hackage.d.ts +93 -0
  36. package/dist/workers/eco-hackage.js +508 -0
  37. package/dist/workers/npm-version.d.ts +5 -3
  38. package/dist/workers/npm-version.js +6 -4
  39. package/dist/workers/pi-worker-docs.d.ts +18 -4
  40. package/dist/workers/pi-worker-docs.js +57 -19
  41. package/dist/workers/research-cache.d.ts +2 -13
  42. package/dist/workers/research-cache.js +22 -46
  43. package/dist/workers/shared.d.ts +16 -5
  44. package/dist/workers/shared.js +0 -0
  45. package/package.json +1 -1
@@ -8,6 +8,7 @@ import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
8
8
  import { docsLookup } from './docs-lookup.js';
9
9
  import { projectCorpus } from './docs-project.js';
10
10
  import { docsRaw, packageCorpus, buildVersionBanner } from './docs-core.js';
11
+ import { ECOSYSTEMS } from './docs-ecosystems.js';
11
12
  import { npmVersionLookup as defaultNpmVersionLookup, formatNpmVersionSection } from './npm-version.js';
12
13
  import { childFailureReason, makeWorkerTool, workerAnswer, workerUnavailable } from './shared.js';
13
14
  import { isTypeOnlyAnswer } from '../task/type-only-answer.js';
@@ -20,11 +21,14 @@ import { groupChildArgs } from '../config/group-args.js';
20
21
  const RENDER_QUERY_MAX = 100;
21
22
  const Params = Type.Object({
22
23
  module: Type.String({
23
- description: 'Bare npm module name (e.g. "zod", "@scope/name", "react/jsx-runtime"), OR "." to look up the current project\'s own source code. npm packages must be installed in node_modules.'
24
+ description: 'Bare package name (e.g. "zod", "@scope/name", "react/jsx-runtime"), OR "." to look up the current project\'s own source code.'
24
25
  }),
25
26
  query: Type.String({
26
27
  description: 'What to extract from the docs. The child pi reads ranked chunks and returns ONLY content answering this.'
27
- })
28
+ }),
29
+ ecosystem: Type.Optional(Type.Union([Type.Literal('npm'), Type.Literal('cargo'), Type.Literal('hackage')], {
30
+ description: 'Which registry to read. Only needed in a repo holding more than one package manifest; otherwise the manifest decides.'
31
+ }))
28
32
  });
29
33
  /**
30
34
  * Pull `@see {@link https://…}` pointers out of retrieved .d.ts/README text.
@@ -90,18 +94,24 @@ export function registerPiWorkerDocs(pi, internals = {}) {
90
94
  makeWorkerTool(pi, {
91
95
  name: 'pi-worker-docs',
92
96
  label: 'Pi Worker Docs',
93
- description: 'Look up an INSTALLED npm package and return a focused, version-pinned '
94
- + 'answer from its .d.ts types and README, PLUS the latest published version '
95
- + 'from a live npm registry call. USE THIS BEFORE ANSWERING any question '
97
+ description: 'Look up an INSTALLED package and return a focused, version-pinned '
98
+ + 'answer from its type declarations and README, PLUS the latest published '
99
+ + 'version from a live registry call. USE THIS BEFORE ANSWERING any question '
96
100
  + 'about how to use a library, what it exports, its types/overloads/config, '
97
- + 'or the latest published version of an npm package. Do NOT answer package '
101
+ + 'or the latest published version of a package. Do NOT answer package '
98
102
  + 'APIs from memory, do NOT run `npm view`/bash to get a package version, and '
99
103
  + 'do NOT web-search for an installed package — this tool is the source of '
100
104
  + 'truth and is version-pinned to what is actually installed (training-data '
101
105
  + 'versions and APIs are typically months stale).\n'
106
+ + 'SUPPORTED ECOSYSTEMS: npm (package.json), cargo (Cargo.toml), hackage '
107
+ + '(*.cabal). The MANIFEST in the working '
108
+ + 'directory decides which registry a name is looked up in — you do not. If '
109
+ + 'the directory holds none of those manifests, this tool REFUSES and '
110
+ + 'installs nothing; use `pi-worker-search` or `pi-worker-fetch` for that '
111
+ + 'package instead.\n'
102
112
  + 'For a non-package framework/runtime version (e.g. Node.js, Ubuntu), use '
103
113
  + '`pi-worker-search` instead. If the package is not installed it is '
104
- + 'auto-installed via bun add or npm install. The cache lives at '
114
+ + "auto-installed from the project's own registry. The cache lives at "
105
115
  + '~/.cache/pi-worker/docs.sqlite, keyed by exact installed version; the '
106
116
  + 'registry lookup is best-effort and silently absent when offline.\n'
107
117
  + '\n'
@@ -168,7 +178,8 @@ export function registerPiWorkerDocs(pi, internals = {}) {
168
178
  }
169
179
  if (projectResult.kind === 'no_chunks') {
170
180
  // The project IS indexed and has nothing — a real answer.
171
- return workerAnswer(`Project "${projectResult.projectName}" has no .ts/.tsx files indexed.`, {
181
+ return workerAnswer(`Project "${projectResult.projectName}" has no `
182
+ + `${projectResult.sourceLabel} files indexed.`, {
172
183
  hitCache: projectResult.hitCache,
173
184
  indexedFiles: projectResult.filesIngested
174
185
  });
@@ -214,6 +225,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
214
225
  pkg: params.module,
215
226
  query: params.query,
216
227
  cwd: ctx.cwd,
228
+ ...(params.ecosystem ? { ecosystem: params.ecosystem } : {}),
217
229
  resolvePackage: internals.resolvePackage,
218
230
  ensureIndexed: internals.ensureIndexed,
219
231
  retrieveChunks: internals.retrieveChunks,
@@ -222,7 +234,9 @@ export function registerPiWorkerDocs(pi, internals = {}) {
222
234
  npmVersionLookup: internals.npmVersionLookup,
223
235
  signal
224
236
  });
225
- const npmHeader = rawResult.npmVersion ? `${formatNpmVersionSection(rawResult.npmVersion)}\n\n` : '';
237
+ const npmHeader = rawResult.npmVersion ?
238
+ `${formatNpmVersionSection(rawResult.npmVersion, rawResult.registryLabel)}\n\n`
239
+ : '';
226
240
  const npmDetails = rawResult.npmVersion ?
227
241
  {
228
242
  npmLatest: rawResult.npmVersion.latest,
@@ -248,13 +262,16 @@ export function registerPiWorkerDocs(pi, internals = {}) {
248
262
  return workerUnavailable(npmHeader + rawResult.message, details, 'docs-error');
249
263
  }
250
264
  if (rawResult.kind === 'no_chunks') {
251
- const banner = buildVersionBanner(rawResult.autoInstallPin, rawResult.pkg.name, rawResult.pkg.version, ctx.cwd);
265
+ const banner = buildVersionBanner(rawResult.autoInstallPin, rawResult.pkg.name, rawResult.pkg.version, ctx.cwd, ECOSYSTEMS[rawResult.pkg.ecosystem]);
252
266
  // The package resolved and genuinely ships nothing to read — an
253
267
  // answer, and a stable one for this run.
254
268
  return workerAnswer(banner
255
269
  + npmHeader
256
- + `Package ${rawResult.pkg.name}@${rawResult.pkg.version} has no .d.ts files or README. Use pi-worker to read source directly.`, {
270
+ + `Package ${rawResult.pkg.name}@${rawResult.pkg.version} has no `
271
+ + `${ECOSYSTEMS[rawResult.pkg.ecosystem].surfaceLabel}. Use pi-worker to `
272
+ + 'read source directly.', {
257
273
  version: rawResult.pkg.version,
274
+ ecosystem: rawResult.pkg.ecosystem,
258
275
  hitCache: rawResult.hitCache,
259
276
  indexedFiles: rawResult.indexedFiles ?? 0,
260
277
  cacheError: rawResult.cacheError,
@@ -264,9 +281,10 @@ export function registerPiWorkerDocs(pi, internals = {}) {
264
281
  });
265
282
  }
266
283
  const { pkg, chunks, hitCache, indexingMs, cacheError, autoInstalled } = rawResult;
267
- const versionBanner = buildVersionBanner(rawResult.autoInstallPin, pkg.name, pkg.version, ctx.cwd);
284
+ const versionBanner = buildVersionBanner(rawResult.autoInstallPin, pkg.name, pkg.version, ctx.cwd, ECOSYSTEMS[pkg.ecosystem]);
268
285
  const baseDetails = {
269
286
  version: pkg.version,
287
+ ecosystem: pkg.ecosystem,
270
288
  hitCache,
271
289
  chunksRetrieved: chunks.length,
272
290
  indexingMs,
@@ -387,14 +405,34 @@ export function docsCacheable(d, text) {
387
405
  /** The docs cache key: a package's answer is per (module, question), with the question
388
406
  * lowercased and its whitespace collapsed so phrasing variants share one entry. Returns
389
407
  * null for the project-source `.` lookup, which is never cached — the working tree
390
- * mutates as tasks implement. */
408
+ * mutates as tasks implement.
409
+ *
410
+ * The ecosystem joins the key only when the caller named one, so the keys of every
411
+ * call that lets the manifest decide are the ones they always were. */
391
412
  export function docsCacheKey(params) {
392
- return params.module === '.' ?
393
- null
394
- : `${normalizeQuery(params.module)}::${normalizeQuery(params.query)}`;
413
+ if (params.module === '.')
414
+ return null;
415
+ const scope = params.ecosystem ? `${params.ecosystem}::` : '';
416
+ return `${scope}${normalizeQuery(params.module)}::${normalizeQuery(params.query)}`;
395
417
  }
396
418
  /** Package provenance for per-entry resume invalidation: the package ROOT of the
397
- * specifier (`hono/client` → `hono`), and undefined for the project-source `.`. */
398
- export function docsCachePkg(params) {
399
- return params.module === '.' ? undefined : packageRootOf(params.module);
419
+ * specifier (`hono/client` → `hono`), and undefined for the project-source `.`.
420
+ *
421
+ * The ecosystem comes from the RESOLVED lookup, not from the optional argument.
422
+ * Reading the argument would record npm for every single-manifest cargo or cabal
423
+ * project — the ordinary case, since the argument only exists to disambiguate a
424
+ * polyglot repo — and the entry's version would then be looked for in a
425
+ * `package.json` that is not there, leaving it un-prunable forever. */
426
+ export function docsCachePkg(params, details) {
427
+ if (params.module === '.')
428
+ return undefined;
429
+ const ecosystem = details.ecosystem ?? 'npm';
430
+ // The ROW knows how its own specifiers nest: `hono/client` → `hono`, but
431
+ // `serde_json::Value` → `serde_json`. Splitting on `/` alone recorded the
432
+ // whole path, and no manifest names that — so the entry's version came back
433
+ // undefined and it was never invalidated again.
434
+ return {
435
+ pkg: ECOSYSTEMS[ecosystem].parentPackage(params.module),
436
+ ...(ecosystem !== 'npm' ? { ecosystem } : {})
437
+ };
400
438
  }
@@ -1,3 +1,4 @@
1
+ import { type EcosystemId } from './docs-ecosystems.js';
1
2
  /** The env var the orchestrator stamps with the per-run id children inherit. */
2
3
  export declare const RESEARCH_RUN_ID_ENV = "PI_TASK_RUN_ID";
3
4
  /** Is this a transient filesystem error worth retrying inside the caller's deadline? */
@@ -24,18 +25,6 @@ export declare function configureResearchRun(enabled: boolean): string | undefin
24
25
  * of asking the same thing resolve to the same (correct) result.
25
26
  */
26
27
  export declare function normalizeQuery(s: string): string;
27
- /**
28
- * The project's declared dependency surface as a name→range map, flattened across every
29
- * dependency block (a package listed in two blocks resolves to the first range seen, in
30
- * block order — a real manifest does not disagree with itself, and a disagreement can
31
- * only make the comparison stricter, i.e. re-fetch).
32
- *
33
- * Returns undefined when the manifest is missing or unparseable: the caller then cannot
34
- * prove any package's version and must treat every package-scoped entry as unprovable.
35
- * Deliberately NOT the lockfile: it is rewritten by installs that change no resolved
36
- * version, which would drop digests for no correctness gain.
37
- */
38
- export declare function depsMap(cwd: string): Promise<Record<string, string> | undefined>;
39
28
  /**
40
29
  * Resume hook: keep the interrupted run's cache id and PRUNE the entries the manifest
41
30
  * has invalidated, rather than discarding the whole cache because anything moved. Returns
@@ -73,4 +62,4 @@ export declare function lookupResearch(cwd: string, runId: string, key: string):
73
62
  *
74
63
  * Best-effort: any failure is swallowed, leaving the caller's live result untouched.
75
64
  */
76
- export declare function storeResearch(cwd: string, runId: string, key: string, text: string, details: unknown, pkg?: string): Promise<void>;
65
+ export declare function storeResearch(cwd: string, runId: string, key: string, text: string, details: unknown, pkg?: string, ecosystem?: EcosystemId): Promise<void>;
@@ -79,6 +79,7 @@
79
79
  import * as fsp from 'node:fs/promises';
80
80
  import * as path from 'node:path';
81
81
  import { tasksDir } from '../task/task-io.js';
82
+ import { ECOSYSTEMS } from './docs-ecosystems.js';
82
83
  const RESEARCH_CACHE_FILE = 'research-cache.json';
83
84
  /** The env var the orchestrator stamps with the per-run id children inherit. */
84
85
  export const RESEARCH_RUN_ID_ENV = 'PI_TASK_RUN_ID';
@@ -174,47 +175,13 @@ export function configureResearchRun(enabled) {
174
175
  export function normalizeQuery(s) {
175
176
  return s.replace(/\s+/g, ' ').trim().toLowerCase();
176
177
  }
177
- /**
178
- * The project's declared dependency surface as a name→range map, flattened across every
179
- * dependency block (a package listed in two blocks resolves to the first range seen, in
180
- * block order — a real manifest does not disagree with itself, and a disagreement can
181
- * only make the comparison stricter, i.e. re-fetch).
182
- *
183
- * Returns undefined when the manifest is missing or unparseable: the caller then cannot
184
- * prove any package's version and must treat every package-scoped entry as unprovable.
185
- * Deliberately NOT the lockfile: it is rewritten by installs that change no resolved
186
- * version, which would drop digests for no correctness gain.
187
- */
188
- export async function depsMap(cwd) {
189
- try {
190
- const raw = await fsp.readFile(path.join(cwd, 'package.json'), 'utf8');
191
- const pkg = JSON.parse(raw);
192
- const blocks = [
193
- 'dependencies',
194
- 'devDependencies',
195
- 'peerDependencies',
196
- 'optionalDependencies'
197
- ];
198
- const out = {};
199
- for (const block of blocks) {
200
- const deps = pkg[block];
201
- if (!deps || typeof deps !== 'object')
202
- continue;
203
- for (const [name, range] of Object.entries(deps)) {
204
- if (typeof range === 'string' && !(name in out))
205
- out[name] = range;
206
- }
207
- }
208
- // No dependency block at all is a real, stable state (a dependency-free repo).
209
- return out;
210
- }
211
- catch {
212
- return undefined;
213
- }
178
+ /** The same question for any ecosystem: what does its manifest pin, name to version. */
179
+ function depsMapFor(cwd, ecosystem) {
180
+ return ECOSYSTEMS[ecosystem].declaredDeps(cwd);
214
181
  }
215
182
  /** The version declared for `pkg`, or undefined when it is not in the manifest. */
216
- async function declaredVersion(cwd, pkg) {
217
- return (await depsMap(cwd))?.[pkg];
183
+ function declaredVersion(cwd, pkg, ecosystem) {
184
+ return depsMapFor(cwd, ecosystem)?.[pkg];
218
185
  }
219
186
  /**
220
187
  * Is this entry still current against the manifest as it stands now?
@@ -227,10 +194,10 @@ async function declaredVersion(cwd, pkg) {
227
194
  * - `pkg` whose version moved, which left the manifest, or an unreadable manifest
228
195
  * (no evidence either way): DROPPED.
229
196
  */
230
- function entryStillFresh(entry, deps) {
197
+ function entryStillFresh(entry, depsFor) {
231
198
  if (entry.pkg === undefined || entry.pkgVersion === undefined)
232
199
  return true;
233
- return deps?.[entry.pkg] === entry.pkgVersion;
200
+ return depsFor(entry.ecosystem ?? 'npm')?.[entry.pkg] === entry.pkgVersion;
234
201
  }
235
202
  /**
236
203
  * Resume hook: keep the interrupted run's cache id and PRUNE the entries the manifest
@@ -267,11 +234,17 @@ async function pruneCache(cwd) {
267
234
  const file = await readCacheFile(cwd);
268
235
  if (!file || file.pkgv !== PKG_PROVENANCE_VERSION)
269
236
  return null;
270
- const deps = await depsMap(cwd);
237
+ // Read each manifest at most once: a file can hold entries from several.
238
+ const byEcosystem = new Map();
239
+ const depsFor = (id) => {
240
+ if (!byEcosystem.has(id))
241
+ byEcosystem.set(id, depsMapFor(cwd, id));
242
+ return byEcosystem.get(id);
243
+ };
271
244
  const kept = {};
272
245
  let dropped = 0;
273
246
  for (const [key, entry] of Object.entries(file.entries)) {
274
- if (entryStillFresh(entry, deps))
247
+ if (entryStillFresh(entry, depsFor))
275
248
  kept[key] = entry;
276
249
  else
277
250
  dropped++;
@@ -449,12 +422,12 @@ async function withCacheLock(cwd, fn) {
449
422
  *
450
423
  * Best-effort: any failure is swallowed, leaving the caller's live result untouched.
451
424
  */
452
- export async function storeResearch(cwd, runId, key, text, details, pkg) {
425
+ export async function storeResearch(cwd, runId, key, text, details, pkg, ecosystem = 'npm') {
453
426
  try {
454
- // Resolved OUTSIDE the critical section: it reads package.json, which no other
427
+ // Resolved OUTSIDE the critical section: it reads the manifest, which no other
455
428
  // writer can be mutating, and keeping it out holds the lock for the file
456
429
  // read/write alone. The version is still the one current at store time.
457
- const pkgVersion = pkg === undefined ? undefined : await declaredVersion(cwd, pkg);
430
+ const pkgVersion = pkg === undefined ? undefined : declaredVersion(cwd, pkg, ecosystem);
458
431
  await withCacheLock(cwd, async () => {
459
432
  const existing = await readCacheFile(cwd);
460
433
  const entries = existing && existing.runId === runId ? existing.entries : {};
@@ -463,6 +436,9 @@ export async function storeResearch(cwd, runId, key, text, details, pkg) {
463
436
  details,
464
437
  at: Date.now(),
465
438
  ...(pkg === undefined ? {} : { pkg }),
439
+ // npm is the absent default, so an npm-only file is byte-identical
440
+ // to what earlier versions wrote.
441
+ ...(pkg !== undefined && ecosystem !== 'npm' ? { ecosystem } : {}),
466
442
  ...(pkgVersion === undefined ? {} : { pkgVersion })
467
443
  };
468
444
  // Evict oldest by write time if over the cap.
@@ -2,7 +2,13 @@ import type { Static, TSchema } from '@sinclair/typebox';
2
2
  import type { AgentToolResult } from '@earendil-works/pi-agent-core';
3
3
  import type { ExtensionAPI, ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';
4
4
  import type { Text } from '@earendil-works/pi-tui';
5
+ import type { EcosystemId } from './docs-ecosystems.js';
5
6
  import { type WorkerFailureInput } from './worker-failure.js';
7
+ /** Which package, in which registry, a cached answer is about. */
8
+ export interface CachePackage {
9
+ pkg: string;
10
+ ecosystem?: EcosystemId;
11
+ }
6
12
  /** Build a plain-text AgentToolResult. */
7
13
  export declare function textResult<T>(text: string, details: T): AgentToolResult<T>;
8
14
  /**
@@ -98,13 +104,18 @@ export interface WorkerToolSpec<TParams extends TSchema, TDetails> {
98
104
  */
99
105
  cacheKey?(params: Static<TParams>): string | null;
100
106
  /**
101
- * The npm package this call's answer is ABOUT, for a package-scoped tool (docs).
102
- * Recorded as structured provenance on the cache entry so a resume can drop just the
103
- * entries whose package moved version, instead of the whole run's cache. A
104
- * greenfield run adds packages every few tasks, so a whole-file gate never holds.
107
+ * The package this call's answer is ABOUT, and the registry it came from, for a
108
+ * package-scoped tool (docs). Recorded as structured provenance on the cache entry
109
+ * so a resume can drop just the entries whose package moved version, instead of
110
+ * the whole run's cache. A greenfield run adds packages every few tasks, so a
111
+ * whole-file gate never holds. The registry is part of it because `text` on npm
112
+ * and `text` on another registry are different packages that move separately.
105
113
  * Omit — or return undefined — and the entry survives every resume of its run.
114
+ *
115
+ * `details` is the finished call's own record, so the registry recorded here is
116
+ * the one the lookup RESOLVED to — not one the caller happened to name.
106
117
  */
107
- cachePkg?(params: Static<TParams>): string | undefined;
118
+ cachePkg?(params: Static<TParams>, details: TDetails): CachePackage | undefined;
108
119
  /**
109
120
  * Whether an ANSWER is safe to cache — a question about the answer's QUALITY
110
121
  * (type-only, an abstention, an unverified excerpt), never about process
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.39.4",
3
+ "version": "0.40.0",
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",