@mjasnikovs/pi-task 0.13.27 → 0.13.28
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/README.md +2 -1
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.js +2 -1
- package/dist/config/register.js +6 -1
- package/dist/task/_ab.d.ts +1 -0
- package/dist/task/_ab.js +68 -0
- package/dist/task/orientation.d.ts +76 -0
- package/dist/task/orientation.js +198 -0
- package/dist/task/phases.js +39 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ pi install npm:@mjasnikovs/pi-task
|
|
|
49
49
|
| `/task-auto <feature>` | Plan a feature into a task list and run each title through `/task` in order (resumable). |
|
|
50
50
|
| `/task-auto-resume` | Resume the active `/task-auto` run at the next unfinished task. |
|
|
51
51
|
| `/task-auto-cancel` | Stop the `/task-auto` loop after the current task (still resumable). |
|
|
52
|
-
| `/task-config` | Toggle pi-task settings in an editor dialog: remote server, compress reasoning,
|
|
52
|
+
| `/task-config` | Toggle pi-task settings in an editor dialog: remote server, compress reasoning, auto-commit, and orientation. |
|
|
53
53
|
| `/remote` | Show the QR code & URLs for the web view (`/remote stop` to stop). Answer grill questions, start tasks, and watch progress from your phone. |
|
|
54
54
|
|
|
55
55
|
## The pipeline
|
|
@@ -147,6 +147,7 @@ Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings pe
|
|
|
147
147
|
| **remote** | The remote UI server (QR code, phone access). Turn off to never start it. |
|
|
148
148
|
| **compress reasoning** | After each message, compresses the model's `<think>` blocks down to the decisions/constraints/facts that matter later — keeping long local-model runs from drowning their own context in self-talk. |
|
|
149
149
|
| **auto-commit** | Snapshots the working tree into one git commit per `/task-auto` sub-task (see above). |
|
|
150
|
+
| **orientation** | Pre-reads the project's core files (manifest, config, domain types, schema, entrypoints, API surface) once and hands the contents to the read-heavy research workers, so they skip re-discovering the same files cold. Bounded by a hard byte budget; applied only where it helps (FILES/APIS workers). |
|
|
150
151
|
|
|
151
152
|
## Configuration
|
|
152
153
|
|
package/dist/config/config.d.ts
CHANGED
package/dist/config/config.js
CHANGED
|
@@ -5,7 +5,8 @@ import * as os from 'node:os';
|
|
|
5
5
|
const DEFAULTS = {
|
|
6
6
|
remote: true,
|
|
7
7
|
compressReasoning: true,
|
|
8
|
-
autoCommit: true
|
|
8
|
+
autoCommit: true,
|
|
9
|
+
orientation: true
|
|
9
10
|
};
|
|
10
11
|
const CONFIG_PATH = path.join(os.homedir(), '.config', 'pi-task', 'config.json');
|
|
11
12
|
const _g = globalThis;
|
package/dist/config/register.js
CHANGED
|
@@ -12,6 +12,11 @@ const ITEMS = [
|
|
|
12
12
|
id: 'autoCommit',
|
|
13
13
|
label: 'auto-commit',
|
|
14
14
|
description: 'git commit around each /task-auto sub-task (checkpoint before, snapshot after)'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'orientation',
|
|
18
|
+
label: 'orientation',
|
|
19
|
+
description: 'Pre-supply the project core (manifest, types, schema…) to the read-heavy research workers'
|
|
15
20
|
}
|
|
16
21
|
];
|
|
17
22
|
function makeTheme(theme) {
|
|
@@ -48,7 +53,7 @@ async function handleTaskConfig(_args, ctx) {
|
|
|
48
53
|
}
|
|
49
54
|
export function registerConfig(pi) {
|
|
50
55
|
registerBridgeCommand(pi, 'task-config', {
|
|
51
|
-
description: 'Configure pi-task settings (remote, compress reasoning, auto-commit).',
|
|
56
|
+
description: 'Configure pi-task settings (remote, compress reasoning, auto-commit, orientation).',
|
|
52
57
|
handler: handleTaskConfig
|
|
53
58
|
});
|
|
54
59
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/task/_ab.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* Live A/B: orientation OFF vs ON, real pi + local model, real mx5 repo. */
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { runWorker } from '../workers/pi-worker-core.js';
|
|
5
|
+
import { getFileInventory } from './file-inventory.js';
|
|
6
|
+
import { buildOrientation } from './orientation.js';
|
|
7
|
+
import { appendNoThink, RESEARCH_FILES_PROMPT, RESEARCH_CONTEXT_PROMPT, RESEARCH_APIS_PROMPT, RESEARCH_TOOLING_PROMPT } from './prompts.js';
|
|
8
|
+
import { scopedToolingGoal } from './phases.js';
|
|
9
|
+
const DOCS_EXT = new URL('../workers/docs-extension.js', import.meta.url).pathname;
|
|
10
|
+
const SINGLE_READ_EXT = new URL('../workers/single-read-extension.js', import.meta.url).pathname;
|
|
11
|
+
const CWD = '/home/edgars/hub/mx5';
|
|
12
|
+
process.env.PI_BIN ??= '/home/edgars/.local/share/mise/installs/node/26.2.0/bin/pi';
|
|
13
|
+
const refined = await readFile('/tmp/refined.txt', 'utf8');
|
|
14
|
+
const inventoryRaw = await getFileInventory(CWD);
|
|
15
|
+
const invPaths = inventoryRaw.split('\n').filter(l => l.trim());
|
|
16
|
+
const inventoryHeader = `PROJECT FILE INVENTORY\n${inventoryRaw}\n\n`;
|
|
17
|
+
const orientation = await buildOrientation(invPaths, async (p) => {
|
|
18
|
+
try {
|
|
19
|
+
return await readFile(resolve(CWD, p), 'utf8');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
console.error(`orientation: ${orientation.supplied.size} files, ${(orientation.block.length / 1024).toFixed(1)}KB`);
|
|
26
|
+
const suppliedRel = new Set([...orientation.supplied].map(p => resolve(CWD, p)));
|
|
27
|
+
const workers = {
|
|
28
|
+
FILES: { build: RESEARCH_FILES_PROMPT, tools: undefined, ext: [] },
|
|
29
|
+
APIS: {
|
|
30
|
+
build: RESEARCH_APIS_PROMPT,
|
|
31
|
+
tools: 'read,grep,find,ls,pi-worker-docs',
|
|
32
|
+
ext: [DOCS_EXT]
|
|
33
|
+
},
|
|
34
|
+
CONTEXT: { build: RESEARCH_CONTEXT_PROMPT, tools: 'read,grep', ext: [] },
|
|
35
|
+
TOOLING: {
|
|
36
|
+
build: (r) => RESEARCH_TOOLING_PROMPT(scopedToolingGoal(r)),
|
|
37
|
+
tools: undefined,
|
|
38
|
+
ext: [SINGLE_READ_EXT]
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
async function run(worker, on) {
|
|
42
|
+
const header = (on ? orientation.block : '') + inventoryHeader;
|
|
43
|
+
const prompt = appendNoThink(header + workers[worker].build(refined));
|
|
44
|
+
const reads = [];
|
|
45
|
+
const r = await runWorker({
|
|
46
|
+
prompt,
|
|
47
|
+
cwd: CWD,
|
|
48
|
+
...(workers[worker].tools ? { tools: workers[worker].tools } : {}),
|
|
49
|
+
...(workers[worker].ext.length ? { extensions: workers[worker].ext } : {}),
|
|
50
|
+
onLine: line => {
|
|
51
|
+
const m = /read: (\S+)/.exec(line);
|
|
52
|
+
if (m)
|
|
53
|
+
reads.push(resolve(CWD, m[1]));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
const reReadSupplied = reads.filter(p => suppliedRel.has(p)).length;
|
|
57
|
+
const label = `${worker} ${on ? 'ON ' : 'OFF'}`;
|
|
58
|
+
console.log(`${label} exit=${r.exitCode} loop=${r.loopHit ? 'Y' : 'n'} time=${r.loopHit || r.timedOut ? '(runaway)' : ''}` +
|
|
59
|
+
` workMs=${r.workMs} reads=${reads.length} reads_of_core=${reReadSupplied} answerChars=${r.text.trim().length}`);
|
|
60
|
+
return { reads: reads.length, reReadSupplied, workMs: r.workMs, exit: r.exitCode };
|
|
61
|
+
}
|
|
62
|
+
for (const w of ['FILES', 'APIS', 'CONTEXT', 'TOOLING']) {
|
|
63
|
+
const off = await run(w, false);
|
|
64
|
+
const on = await run(w, true);
|
|
65
|
+
const saved = off.reads - on.reads;
|
|
66
|
+
console.log(` -> ${w}: reads ${off.reads}->${on.reads} (${saved >= 0 ? '-' : '+'}${Math.abs(saved)}), ` +
|
|
67
|
+
`workMs ${off.workMs}->${on.workMs}, core re-read while ON: ${on.reReadSupplied}\n`);
|
|
68
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project orientation core — a bounded snapshot of the few files every research
|
|
3
|
+
* worker re-reads cold to learn "what is this project" (manifest, config, domain
|
|
4
|
+
* types, schema, entrypoints, API surface). The four workers run as separate
|
|
5
|
+
* child processes with no shared memory, so today each one independently
|
|
6
|
+
* `read()`s the same hot files: across the recorded mx5 run package.json was
|
|
7
|
+
* read 87×, src/types/index.ts 82×, and 55% of all worker reads were repeats of
|
|
8
|
+
* a file another worker in the same task had already read.
|
|
9
|
+
*
|
|
10
|
+
* This module picks that orientation core from the file inventory (repo-agnostic,
|
|
11
|
+
* by path convention), reads it ONCE in the parent, and the caller folds it into
|
|
12
|
+
* the shared prompt header — so every worker already has those files and skips
|
|
13
|
+
* the cold read. It is purely additive: nothing is blocked, so a worker can still
|
|
14
|
+
* read anything it wants; orientation only removes the need to.
|
|
15
|
+
*
|
|
16
|
+
* Bounded by design — the snapshot can never overflow the prompt regardless of
|
|
17
|
+
* repo size: a hard total byte budget, a per-file cap (one huge file can't eat
|
|
18
|
+
* the budget), and a candidate cap. Files that don't fit are simply not
|
|
19
|
+
* pre-supplied; the worker reads them as before. Selection is the pure, tested
|
|
20
|
+
* core; reading takes an injectable reader so it can be exercised without a repo.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Total bytes of file content the orientation block may contain (~10k tokens).
|
|
24
|
+
* This is the real overflow guard: it bounds the block regardless of repo size,
|
|
25
|
+
* and because the snapshot is prepended to each read-heavy worker it also caps
|
|
26
|
+
* the prefill those workers pay. Selection stops as soon as adding a file would
|
|
27
|
+
* exceed it.
|
|
28
|
+
*/
|
|
29
|
+
export declare const ORIENTATION_BYTE_BUDGET: number;
|
|
30
|
+
/** A single file larger than this is skipped (read by the worker as before). */
|
|
31
|
+
export declare const ORIENTATION_PER_FILE_MAX: number;
|
|
32
|
+
/**
|
|
33
|
+
* Backstop file-count cap — a guard against a pathological repo with hundreds of
|
|
34
|
+
* tiny core files packing the byte budget into noise, NOT a normal-case limit.
|
|
35
|
+
* It is deliberately set high enough that the byte budget binds first on real
|
|
36
|
+
* repos (verified: mx5/aiz-server hit the budget at ~16 files; a 5000-file repo
|
|
37
|
+
* yields only ~6 orientation candidates because leaf source is filtered out). An
|
|
38
|
+
* earlier cap of 16 wrongly bound before the budget — e.g. it dropped 5 core
|
|
39
|
+
* files on a 165-file repo while leaving 17KB of budget unused.
|
|
40
|
+
*/
|
|
41
|
+
export declare const ORIENTATION_MAX_FILES = 40;
|
|
42
|
+
/**
|
|
43
|
+
* Priority tier for an orientation candidate — lower is more fundamental. The
|
|
44
|
+
* tiers mirror the questions a worker re-derives on every task: what is this
|
|
45
|
+
* project (manifest) → how is it built (config) → what is its domain model
|
|
46
|
+
* (types/schema) → where does it start (entrypoints) → what is its surface (api)
|
|
47
|
+
* → what does it say about itself (docs). Returns null for paths that aren't
|
|
48
|
+
* orientation material, so they're dropped entirely.
|
|
49
|
+
*/
|
|
50
|
+
export declare function orientationTier(path: string): number | null;
|
|
51
|
+
/**
|
|
52
|
+
* Rank inventory paths into orientation priority order: by tier, then shallower
|
|
53
|
+
* paths first (a root entrypoint beats a deeply-nested one), then alphabetical
|
|
54
|
+
* for a fully deterministic order. Non-orientation paths are dropped. The result
|
|
55
|
+
* is the *candidate* order; the byte budget is applied later when reading.
|
|
56
|
+
*/
|
|
57
|
+
export declare function selectOrientationFiles(inventoryPaths: string[]): string[];
|
|
58
|
+
export interface OrientationResult {
|
|
59
|
+
/** Formatted PROJECT ORIENTATION block to prepend to the worker header, or ''. */
|
|
60
|
+
block: string;
|
|
61
|
+
/** Resolved-relative paths whose full content was pre-supplied in the block. */
|
|
62
|
+
supplied: Set<string>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Read the orientation core within the byte budget and format it as a header
|
|
66
|
+
* block. `readFile` returns a file's text or null (missing/unreadable/binary) —
|
|
67
|
+
* a null or over-cap file is skipped, not fatal. Greedy in priority order: take
|
|
68
|
+
* each file whose content fits the per-file cap and the remaining total budget;
|
|
69
|
+
* skip the rest. Returns an empty block (and empty set) when nothing qualifies,
|
|
70
|
+
* so the caller falls back to today's behavior.
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildOrientation(inventoryPaths: string[], readFile: (path: string) => Promise<string | null>, opts?: {
|
|
73
|
+
byteBudget?: number;
|
|
74
|
+
perFileMax?: number;
|
|
75
|
+
maxFiles?: number;
|
|
76
|
+
}): Promise<OrientationResult>;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project orientation core — a bounded snapshot of the few files every research
|
|
3
|
+
* worker re-reads cold to learn "what is this project" (manifest, config, domain
|
|
4
|
+
* types, schema, entrypoints, API surface). The four workers run as separate
|
|
5
|
+
* child processes with no shared memory, so today each one independently
|
|
6
|
+
* `read()`s the same hot files: across the recorded mx5 run package.json was
|
|
7
|
+
* read 87×, src/types/index.ts 82×, and 55% of all worker reads were repeats of
|
|
8
|
+
* a file another worker in the same task had already read.
|
|
9
|
+
*
|
|
10
|
+
* This module picks that orientation core from the file inventory (repo-agnostic,
|
|
11
|
+
* by path convention), reads it ONCE in the parent, and the caller folds it into
|
|
12
|
+
* the shared prompt header — so every worker already has those files and skips
|
|
13
|
+
* the cold read. It is purely additive: nothing is blocked, so a worker can still
|
|
14
|
+
* read anything it wants; orientation only removes the need to.
|
|
15
|
+
*
|
|
16
|
+
* Bounded by design — the snapshot can never overflow the prompt regardless of
|
|
17
|
+
* repo size: a hard total byte budget, a per-file cap (one huge file can't eat
|
|
18
|
+
* the budget), and a candidate cap. Files that don't fit are simply not
|
|
19
|
+
* pre-supplied; the worker reads them as before. Selection is the pure, tested
|
|
20
|
+
* core; reading takes an injectable reader so it can be exercised without a repo.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Total bytes of file content the orientation block may contain (~10k tokens).
|
|
24
|
+
* This is the real overflow guard: it bounds the block regardless of repo size,
|
|
25
|
+
* and because the snapshot is prepended to each read-heavy worker it also caps
|
|
26
|
+
* the prefill those workers pay. Selection stops as soon as adding a file would
|
|
27
|
+
* exceed it.
|
|
28
|
+
*/
|
|
29
|
+
export const ORIENTATION_BYTE_BUDGET = 40 * 1024;
|
|
30
|
+
/** A single file larger than this is skipped (read by the worker as before). */
|
|
31
|
+
export const ORIENTATION_PER_FILE_MAX = 12 * 1024;
|
|
32
|
+
/**
|
|
33
|
+
* Backstop file-count cap — a guard against a pathological repo with hundreds of
|
|
34
|
+
* tiny core files packing the byte budget into noise, NOT a normal-case limit.
|
|
35
|
+
* It is deliberately set high enough that the byte budget binds first on real
|
|
36
|
+
* repos (verified: mx5/aiz-server hit the budget at ~16 files; a 5000-file repo
|
|
37
|
+
* yields only ~6 orientation candidates because leaf source is filtered out). An
|
|
38
|
+
* earlier cap of 16 wrongly bound before the budget — e.g. it dropped 5 core
|
|
39
|
+
* files on a 165-file repo while leaving 17KB of budget unused.
|
|
40
|
+
*/
|
|
41
|
+
export const ORIENTATION_MAX_FILES = 40;
|
|
42
|
+
/** Code/config/doc extensions worth pre-reading; everything else is ignored. */
|
|
43
|
+
const ORIENTATION_EXTENSIONS = new Set([
|
|
44
|
+
'ts',
|
|
45
|
+
'tsx',
|
|
46
|
+
'js',
|
|
47
|
+
'jsx',
|
|
48
|
+
'mjs',
|
|
49
|
+
'cjs',
|
|
50
|
+
'json',
|
|
51
|
+
'sql',
|
|
52
|
+
'prisma',
|
|
53
|
+
'go',
|
|
54
|
+
'rs',
|
|
55
|
+
'py',
|
|
56
|
+
'rb',
|
|
57
|
+
'java',
|
|
58
|
+
'kt',
|
|
59
|
+
'md',
|
|
60
|
+
'toml',
|
|
61
|
+
'graphql',
|
|
62
|
+
'gql'
|
|
63
|
+
]);
|
|
64
|
+
/** Root manifests — the single highest-signal "what is this project" file. */
|
|
65
|
+
const MANIFEST_BASENAMES = new Set([
|
|
66
|
+
'package.json',
|
|
67
|
+
'go.mod',
|
|
68
|
+
'cargo.toml',
|
|
69
|
+
'pyproject.toml',
|
|
70
|
+
'requirements.txt',
|
|
71
|
+
'pom.xml',
|
|
72
|
+
'build.gradle',
|
|
73
|
+
'gemfile',
|
|
74
|
+
'composer.json',
|
|
75
|
+
'deno.json'
|
|
76
|
+
]);
|
|
77
|
+
function basename(p) {
|
|
78
|
+
const i = p.lastIndexOf('/');
|
|
79
|
+
return i === -1 ? p : p.slice(i + 1);
|
|
80
|
+
}
|
|
81
|
+
function extension(p) {
|
|
82
|
+
const b = basename(p);
|
|
83
|
+
const i = b.lastIndexOf('.');
|
|
84
|
+
return i === -1 ? '' : b.slice(i + 1).toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
function depth(p) {
|
|
87
|
+
let n = 0;
|
|
88
|
+
for (const c of p)
|
|
89
|
+
if (c === '/')
|
|
90
|
+
n++;
|
|
91
|
+
return n;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Priority tier for an orientation candidate — lower is more fundamental. The
|
|
95
|
+
* tiers mirror the questions a worker re-derives on every task: what is this
|
|
96
|
+
* project (manifest) → how is it built (config) → what is its domain model
|
|
97
|
+
* (types/schema) → where does it start (entrypoints) → what is its surface (api)
|
|
98
|
+
* → what does it say about itself (docs). Returns null for paths that aren't
|
|
99
|
+
* orientation material, so they're dropped entirely.
|
|
100
|
+
*/
|
|
101
|
+
export function orientationTier(path) {
|
|
102
|
+
const lower = path.toLowerCase();
|
|
103
|
+
// Never orient on vendored/build output or tests: a dependency's manifest or
|
|
104
|
+
// a big *.test.ts would otherwise outrank the project's own files and eat the
|
|
105
|
+
// budget (validated against the mx5 traces — node_modules/hono/package.json
|
|
106
|
+
// and zod-schemas.test.ts both crowded out genuinely hot project files). A
|
|
107
|
+
// git-ls-files inventory won't list these anyway; this is belt-and-braces.
|
|
108
|
+
if (/(^|\/)(node_modules|dist|build|out|vendor|\.git|coverage)\//.test(lower))
|
|
109
|
+
return null;
|
|
110
|
+
if (/\.(test|spec)\.[a-z]+$/.test(lower) || /(^|\/)(__tests__|e2e)\//.test(lower))
|
|
111
|
+
return null;
|
|
112
|
+
if (path.startsWith('/'))
|
|
113
|
+
return null; // absolute path escaped the repo root
|
|
114
|
+
const base = basename(lower);
|
|
115
|
+
const ext = extension(lower);
|
|
116
|
+
if (MANIFEST_BASENAMES.has(base))
|
|
117
|
+
return 0;
|
|
118
|
+
if (!ORIENTATION_EXTENSIONS.has(ext))
|
|
119
|
+
return null;
|
|
120
|
+
if (base === 'tsconfig.json' || /\.config\.(ts|js|mjs|cjs|json)$/.test(base))
|
|
121
|
+
return 1;
|
|
122
|
+
const segs = lower.split('/');
|
|
123
|
+
const stem = base.replace(/\.[^.]+$/, '');
|
|
124
|
+
if (ext === 'sql'
|
|
125
|
+
|| ext === 'prisma'
|
|
126
|
+
|| ext === 'graphql'
|
|
127
|
+
|| ext === 'gql'
|
|
128
|
+
|| base.endsWith('.d.ts')
|
|
129
|
+
|| segs.includes('types')
|
|
130
|
+
|| stem === 'types'
|
|
131
|
+
|| /schema|zod/.test(lower))
|
|
132
|
+
return 2;
|
|
133
|
+
if (['index', 'main', 'app', 'server', 'mod'].includes(stem))
|
|
134
|
+
return 3;
|
|
135
|
+
if (/(^|\/)api(\/|s?\.|$)/.test(lower) || segs.includes('api'))
|
|
136
|
+
return 4;
|
|
137
|
+
if (stem === 'readme')
|
|
138
|
+
return 5;
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Rank inventory paths into orientation priority order: by tier, then shallower
|
|
143
|
+
* paths first (a root entrypoint beats a deeply-nested one), then alphabetical
|
|
144
|
+
* for a fully deterministic order. Non-orientation paths are dropped. The result
|
|
145
|
+
* is the *candidate* order; the byte budget is applied later when reading.
|
|
146
|
+
*/
|
|
147
|
+
export function selectOrientationFiles(inventoryPaths) {
|
|
148
|
+
return inventoryPaths
|
|
149
|
+
.map(p => ({ p, tier: orientationTier(p) }))
|
|
150
|
+
.filter((x) => x.tier !== null)
|
|
151
|
+
.sort((a, b) => a.tier - b.tier || depth(a.p) - depth(b.p) || (a.p < b.p ? -1 : 1))
|
|
152
|
+
.map(x => x.p);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Read the orientation core within the byte budget and format it as a header
|
|
156
|
+
* block. `readFile` returns a file's text or null (missing/unreadable/binary) —
|
|
157
|
+
* a null or over-cap file is skipped, not fatal. Greedy in priority order: take
|
|
158
|
+
* each file whose content fits the per-file cap and the remaining total budget;
|
|
159
|
+
* skip the rest. Returns an empty block (and empty set) when nothing qualifies,
|
|
160
|
+
* so the caller falls back to today's behavior.
|
|
161
|
+
*/
|
|
162
|
+
export async function buildOrientation(inventoryPaths, readFile, opts = {}) {
|
|
163
|
+
const byteBudget = opts.byteBudget ?? ORIENTATION_BYTE_BUDGET;
|
|
164
|
+
const perFileMax = opts.perFileMax ?? ORIENTATION_PER_FILE_MAX;
|
|
165
|
+
const maxFiles = opts.maxFiles ?? ORIENTATION_MAX_FILES;
|
|
166
|
+
const candidates = selectOrientationFiles(inventoryPaths).slice(0, maxFiles * 4);
|
|
167
|
+
const supplied = new Set();
|
|
168
|
+
const parts = [];
|
|
169
|
+
// Budget the *emitted* block, not just the raw content: the per-file fence
|
|
170
|
+
// (`--- path ---\n` + the `\n\n` join) and the fixed wrapper add up across
|
|
171
|
+
// many files and would otherwise let the block overrun the budget (a 27-file
|
|
172
|
+
// repo overran a content-only budget by >1KB). Seed `used` with the wrapper
|
|
173
|
+
// so the final block is guaranteed ≤ byteBudget.
|
|
174
|
+
const HEADER = `PROJECT ORIENTATION (full contents of the project's core files — `
|
|
175
|
+
+ `already provided, do not re-read these)\n`;
|
|
176
|
+
const TRAILER = '\n\n';
|
|
177
|
+
let used = Buffer.byteLength(HEADER + TRAILER, 'utf8');
|
|
178
|
+
for (const path of candidates) {
|
|
179
|
+
if (supplied.size >= maxFiles)
|
|
180
|
+
break;
|
|
181
|
+
const text = await readFile(path);
|
|
182
|
+
if (text === null)
|
|
183
|
+
continue;
|
|
184
|
+
if (Buffer.byteLength(text, 'utf8') > perFileMax)
|
|
185
|
+
continue;
|
|
186
|
+
const piece = `--- ${path} ---\n${text.replace(/\n+$/, '')}`;
|
|
187
|
+
// Each piece after the first is preceded by the `\n\n` join separator.
|
|
188
|
+
const pieceBytes = Buffer.byteLength(piece, 'utf8') + (parts.length > 0 ? 2 : 0);
|
|
189
|
+
if (used + pieceBytes > byteBudget)
|
|
190
|
+
continue;
|
|
191
|
+
used += pieceBytes;
|
|
192
|
+
supplied.add(path);
|
|
193
|
+
parts.push(piece);
|
|
194
|
+
}
|
|
195
|
+
if (parts.length === 0)
|
|
196
|
+
return { block: '', supplied };
|
|
197
|
+
return { block: HEADER + parts.join('\n\n') + TRAILER, supplied };
|
|
198
|
+
}
|
package/dist/task/phases.js
CHANGED
|
@@ -9,6 +9,10 @@ import { runWorker } from '../workers/pi-worker-core.js';
|
|
|
9
9
|
import { search as defaultSearch } from '../workers/search-core.js';
|
|
10
10
|
import { extractEnrichTargets } from './enrichment.js';
|
|
11
11
|
import { getFileInventory } from './file-inventory.js';
|
|
12
|
+
import { buildOrientation } from './orientation.js';
|
|
13
|
+
import { getConfig } from '../config/config.js';
|
|
14
|
+
import { readFile } from 'node:fs/promises';
|
|
15
|
+
import { resolve } from 'node:path';
|
|
12
16
|
import { formatServiceBlock, formatFreshnessSkippedBlock } from './service-blocks.js';
|
|
13
17
|
import { gatherExternalContext } from './external-context.js';
|
|
14
18
|
import { REFINE_PROMPT, RESEARCH_FILES_PROMPT, RESEARCH_APIS_PROMPT, RESEARCH_CONTEXT_PROMPT, RESEARCH_TOOLING_PROMPT, GRILL_GEN_PROMPT, GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT, COMPOSE_PROMPT, CRITIQUE_PROMPT, CRITIQUE_TRIAGE_PROMPT, VERIFY_TOOLING_PROMPT, MAX_GRILL_QUESTIONS, appendNoThink } from './prompts.js';
|
|
@@ -194,6 +198,37 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
194
198
|
// behavior.
|
|
195
199
|
const inventoryRaw = await fileInventoryFn(deps.cwd, deps.signal).catch(() => '');
|
|
196
200
|
const inventoryHeader = inventoryRaw.length > 0 ? `PROJECT FILE INVENTORY\n${inventoryRaw}\n\n` : '';
|
|
201
|
+
// Pre-read the project's orientation core (manifest, config, domain types,
|
|
202
|
+
// schema, entrypoints, API surface) ONCE and hand the full contents to the
|
|
203
|
+
// READ-HEAVY workers in their header. The workers run as separate child
|
|
204
|
+
// processes, so without this each one that explores re-reads the same hot
|
|
205
|
+
// files cold. Bounded by a hard byte budget so it can't overflow on a large
|
|
206
|
+
// repo; purely additive (nothing is blocked) so it can only remove a
|
|
207
|
+
// redundant read, never hide a file.
|
|
208
|
+
//
|
|
209
|
+
// Applied to FILES and APIS only — NOT CONTEXT/TOOLING. Verified with a live
|
|
210
|
+
// A/B on the local model (real pi, real repo): FILES and APIS are bimodal —
|
|
211
|
+
// they sometimes answer from the inventory but sometimes spiral into heavy
|
|
212
|
+
// reads (APIS hit 37 reads / 221s, a near-runaway), and pre-supplying the core
|
|
213
|
+
// collapses that to 0 reads / ~3.5s with the model honoring "do not re-read"
|
|
214
|
+
// (0 core re-reads in every ON run). CONTEXT works from inventory+grep and
|
|
215
|
+
// reads ~0 files regardless, so the block was pure prefill and made it slower
|
|
216
|
+
// in 5/5 reps; TOOLING is already scoped + single-read-guarded and saw no
|
|
217
|
+
// benefit. So orientation only goes where reads actually happen.
|
|
218
|
+
const orientationPaths = getConfig().orientation && inventoryRaw.length > 0 ?
|
|
219
|
+
inventoryRaw.split('\n').filter(l => l.trim().length > 0)
|
|
220
|
+
: [];
|
|
221
|
+
const orientation = await buildOrientation(orientationPaths, async (path) => {
|
|
222
|
+
try {
|
|
223
|
+
return await readFile(resolve(deps.cwd, path), 'utf8');
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}).catch(() => ({ block: '', supplied: new Set() }));
|
|
229
|
+
if (orientation.supplied.size > 0) {
|
|
230
|
+
deps.logDebug?.(`orientation: pre-supplied ${orientation.supplied.size} core files`);
|
|
231
|
+
}
|
|
197
232
|
const promptHeader = externalContext + inventoryHeader;
|
|
198
233
|
let doneCount = 0;
|
|
199
234
|
const updateProgress = () => {
|
|
@@ -235,12 +270,14 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
235
270
|
{
|
|
236
271
|
section: 'FILES',
|
|
237
272
|
label: 'worker:files',
|
|
238
|
-
|
|
273
|
+
// Read-heavy: gets the orientation core (see note above).
|
|
274
|
+
prompt: appendNoThink(orientation.block + promptHeader + RESEARCH_FILES_PROMPT(refined))
|
|
239
275
|
},
|
|
240
276
|
{
|
|
241
277
|
section: 'APIS',
|
|
242
278
|
label: 'worker:apis',
|
|
243
|
-
|
|
279
|
+
// Read-heavy: gets the orientation core (see note above).
|
|
280
|
+
prompt: appendNoThink(orientation.block + promptHeader + RESEARCH_APIS_PROMPT(refined)),
|
|
244
281
|
tools: 'read,grep,find,ls,pi-worker-docs',
|
|
245
282
|
extensions: [DOCS_EXTENSION_PATH]
|
|
246
283
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.28",
|
|
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",
|