@mjasnikovs/pi-task 0.40.50 → 0.41.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.
- package/README.md +7 -4
- package/dist/config/config.d.ts +61 -18
- package/dist/config/config.js +50 -6
- package/dist/config/register.js +18 -5
- package/dist/shared/content-tokens.d.ts +16 -0
- package/dist/shared/content-tokens.js +137 -0
- package/dist/task/accept-debt.d.ts +15 -1
- package/dist/task/accept-debt.js +4 -0
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +59 -7
- package/dist/task/auto-io.js +140 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +126 -112
- package/dist/task/child-runner.d.ts +18 -0
- package/dist/task/child-runner.js +7 -8
- package/dist/task/command-run.d.ts +4 -0
- package/dist/task/command-run.js +4 -2
- package/dist/task/constraint-policy.d.ts +52 -0
- package/dist/task/constraint-policy.js +66 -0
- package/dist/task/context-silence.d.ts +5 -4
- package/dist/task/context-silence.js +13 -5
- package/dist/task/coverage-loop.d.ts +7 -4
- package/dist/task/coverage-loop.js +16 -138
- package/dist/task/debug-log.d.ts +25 -2
- package/dist/task/debug-log.js +64 -10
- package/dist/task/decompose-fidelity.d.ts +17 -4
- package/dist/task/decompose-fidelity.js +95 -55
- package/dist/task/decompose-granularity.d.ts +7 -7
- package/dist/task/decompose-granularity.js +8 -8
- package/dist/task/env-notes.d.ts +65 -19
- package/dist/task/env-notes.js +152 -50
- package/dist/task/external-context.js +25 -5
- package/dist/task/fix-context.d.ts +39 -0
- package/dist/task/fix-context.js +58 -0
- package/dist/task/gate-deps.d.ts +49 -0
- package/dist/task/gate-deps.js +199 -38
- package/dist/task/gate-evidence.d.ts +51 -0
- package/dist/task/gate-evidence.js +90 -0
- package/dist/task/gate-resolution.d.ts +119 -0
- package/dist/task/gate-resolution.js +120 -0
- package/dist/task/git-state-guard.js +3 -26
- package/dist/task/handoff.d.ts +35 -0
- package/dist/task/handoff.js +100 -0
- package/dist/task/health-baseline.d.ts +87 -0
- package/dist/task/health-baseline.js +158 -0
- package/dist/task/ledger.d.ts +10 -0
- package/dist/task/ledger.js +22 -11
- package/dist/task/lint-fix.d.ts +11 -0
- package/dist/task/lint-fix.js +14 -8
- package/dist/task/loop-detector.d.ts +52 -3
- package/dist/task/loop-detector.js +81 -5
- package/dist/task/mentions.d.ts +17 -0
- package/dist/task/mentions.js +86 -0
- package/dist/task/orchestrator.d.ts +53 -9
- package/dist/task/orchestrator.js +104 -28
- package/dist/task/orientation.d.ts +135 -29
- package/dist/task/orientation.js +259 -67
- package/dist/task/owned-freeze-reassign.d.ts +7 -3
- package/dist/task/owned-freeze-reassign.js +13 -5
- package/dist/task/parsers.d.ts +6 -1
- package/dist/task/parsers.js +21 -4
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +125 -153
- package/dist/task/plan-orchestrator.js +5 -6
- package/dist/task/plan-rounds.d.ts +1 -0
- package/dist/task/plan-rounds.js +8 -1
- package/dist/task/prohibition-probe.d.ts +6 -1
- package/dist/task/prohibition-probe.js +26 -6
- package/dist/task/prompts.js +13 -2
- package/dist/task/qa-transcript.d.ts +17 -0
- package/dist/task/qa-transcript.js +28 -0
- package/dist/task/question-source.d.ts +16 -2
- package/dist/task/question-source.js +21 -8
- package/dist/task/repo-health-check.d.ts +27 -0
- package/dist/task/repo-health-check.js +39 -5
- package/dist/task/requirements.d.ts +67 -13
- package/dist/task/requirements.js +129 -67
- package/dist/task/research-worker.d.ts +43 -2
- package/dist/task/research-worker.js +103 -8
- package/dist/task/run-bracket.d.ts +9 -4
- package/dist/task/run-bracket.js +9 -0
- package/dist/task/run-context.d.ts +152 -0
- package/dist/task/run-context.js +277 -0
- package/dist/task/spec-doc.d.ts +78 -0
- package/dist/task/spec-doc.js +205 -0
- package/dist/task/spec-model.d.ts +70 -0
- package/dist/task/spec-model.js +139 -0
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +9 -5
- package/dist/task/state-dir.d.ts +29 -0
- package/dist/task/state-dir.js +80 -0
- package/dist/task/suppression-probe.d.ts +67 -0
- package/dist/task/suppression-probe.js +78 -0
- package/dist/task/task-gates.d.ts +39 -57
- package/dist/task/task-gates.js +115 -195
- package/dist/task/task-io.d.ts +10 -0
- package/dist/task/task-io.js +69 -18
- package/dist/task/task-parsers.js +6 -6
- package/dist/task/task-types.d.ts +7 -0
- package/dist/task/timings.d.ts +11 -0
- package/dist/task/timings.js +21 -0
- package/dist/task/tree-hash.d.ts +8 -0
- package/dist/task/tree-hash.js +55 -0
- package/dist/task/verify-reconcile.d.ts +4 -3
- package/dist/task/verify-reconcile.js +7 -8
- package/dist/task/verify-resolution.d.ts +28 -5
- package/dist/task/verify-resolution.js +70 -13
- package/dist/task/verify-work.d.ts +98 -35
- package/dist/task/verify-work.js +227 -60
- package/dist/task/yolo.d.ts +0 -13
- package/dist/task/yolo.js +9 -21
- package/dist/workers/pi-worker-core.d.ts +20 -0
- package/dist/workers/pi-worker-core.js +16 -11
- package/dist/workers/pi-worker-docs.d.ts +6 -5
- package/dist/workers/pi-worker-docs.js +8 -7
- package/dist/workers/pi-worker-fetch.d.ts +3 -2
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +5 -4
- package/dist/workers/research-cache.d.ts +23 -1
- package/dist/workers/research-cache.js +63 -3
- package/dist/workers/shared.d.ts +18 -0
- package/dist/workers/shared.js +38 -22
- package/package.json +1 -1
|
@@ -23,15 +23,18 @@ import { handleFailure } from './failure-classifier.js';
|
|
|
23
23
|
import { PHASE_INDEX, PHASE_ORDER, RESUMABLE_STATES } from './task-types.js';
|
|
24
24
|
import { normaliseTaskId, parseFrontMatter, extractSection } from './task-parsers.js';
|
|
25
25
|
import { readTextFile } from '../shared/fs-text.js';
|
|
26
|
-
import { allocateTaskId, ensureTasksDir, readSection, readTaskFile, setTaskSection, taskFilePath, tasksDir, updateTaskFrontMatter, writeTaskFile } from './task-io.js';
|
|
26
|
+
import { allocateTaskId, ensureTasksDir, mergeTaskSection, readSection, readTaskFile, setTaskSection, taskFilePath, tasksDir, updateTaskFrontMatter, writeTaskFile } from './task-io.js';
|
|
27
27
|
import { startWidget } from './widget.js';
|
|
28
28
|
import { setupImplWidget } from './impl-widget.js';
|
|
29
29
|
import { enterImplementationTurn } from './implementation-scope.js';
|
|
30
30
|
import { SessionUI, publishNotify, registerBridgeCommand, getBridge, notifyBoth, isRemoteOrigin } from '../remote/bridge.js';
|
|
31
31
|
import { pushNotify } from '../remote/push.js';
|
|
32
32
|
import { getConfig } from '../config/config.js';
|
|
33
|
-
import { gateDebugWriter } from './debug-log.js';
|
|
33
|
+
import { appendDebugLine, gateDebugWriter } from './debug-log.js';
|
|
34
|
+
import { runLogPath } from './state-dir.js';
|
|
34
35
|
import { buildGateDeps } from './gate-deps.js';
|
|
36
|
+
import { formatHealthBaseline, HEALTH_BASELINE_SECTION } from './health-baseline.js';
|
|
37
|
+
import { formatFixBanner } from './fix-context.js';
|
|
35
38
|
import { runGatesForTask } from './task-gates.js';
|
|
36
39
|
import { parseVerifyBlock } from './spec-validation.js';
|
|
37
40
|
import { findDeliveryPhantoms, formatApiOverrideBanner } from '../workers/phantom-imports.js';
|
|
@@ -42,8 +45,10 @@ import { holdImplementation, liveModelControl } from './implementation-hold.js';
|
|
|
42
45
|
import { rearmCancelListener } from './cancel-input.js';
|
|
43
46
|
import { takeHeldInput, isRunActive } from './mid-run-input.js';
|
|
44
47
|
import { withRun, announceTerminal } from './run-bracket.js';
|
|
48
|
+
import { currentRunContext } from './run-context.js';
|
|
45
49
|
import { RUN_END_POLICY, runSucceeded } from './run-end.js';
|
|
46
|
-
import {
|
|
50
|
+
import { mergeTimings } from './timings.js';
|
|
51
|
+
import { appendHandoff, parseHandoff, specHash, summariseFixContext } from './handoff.js';
|
|
47
52
|
import { getParentContextWindow, resolveContextUsage } from './context-usage.js';
|
|
48
53
|
import { superviseImplementation } from './implementation-turn.js';
|
|
49
54
|
import { TERMINAL_OUTCOMES, formatAt, formatWhy } from './terminal-outcome.js';
|
|
@@ -102,7 +107,9 @@ export class TaskRunner {
|
|
|
102
107
|
_sendSpec;
|
|
103
108
|
_onStart;
|
|
104
109
|
_planContext;
|
|
105
|
-
|
|
110
|
+
_planKey;
|
|
111
|
+
_fixContext;
|
|
112
|
+
_healthBaseline;
|
|
106
113
|
/** See {@link TaskRunnerOptions.implAwaited}. */
|
|
107
114
|
_implAwaited;
|
|
108
115
|
_abort = new AbortController();
|
|
@@ -120,6 +127,12 @@ export class TaskRunner {
|
|
|
120
127
|
*/
|
|
121
128
|
_timings = [];
|
|
122
129
|
_currentPhaseChildren = null;
|
|
130
|
+
/**
|
|
131
|
+
* Whether this run has already appended its timings block. The success path
|
|
132
|
+
* writes, and a cancel raised AFTER it still lands in the catch — which would
|
|
133
|
+
* otherwise append a second block for the same attempt.
|
|
134
|
+
*/
|
|
135
|
+
_timingsWritten = false;
|
|
123
136
|
constructor(opts) {
|
|
124
137
|
const { ctx, cwd, rawPrompt } = opts;
|
|
125
138
|
this._ctx = ctx;
|
|
@@ -129,7 +142,9 @@ export class TaskRunner {
|
|
|
129
142
|
this._sendSpec = opts.sendSpec;
|
|
130
143
|
this._onStart = opts.onStart;
|
|
131
144
|
this._planContext = opts.planContext;
|
|
132
|
-
this.
|
|
145
|
+
this._planKey = opts.planKey;
|
|
146
|
+
this._fixContext = opts.fixContext;
|
|
147
|
+
this._healthBaseline = opts.healthBaseline;
|
|
133
148
|
this._implAwaited = opts.implAwaited ?? false;
|
|
134
149
|
this._startedAt = Date.now();
|
|
135
150
|
// Placeholder: id/title/phase are only known once run() has allocated or
|
|
@@ -207,9 +222,34 @@ export class TaskRunner {
|
|
|
207
222
|
async run() {
|
|
208
223
|
return withRun(this._ctx, {}, () => this._run());
|
|
209
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Store the task's repo-health baseline, ONCE. A re-entry — a resume, an
|
|
227
|
+
* autofix re-run — must keep the original: the baseline answers "what was
|
|
228
|
+
* already broken when this task started", and re-taking it on a tree the task
|
|
229
|
+
* has since edited answers a different question with the same name.
|
|
230
|
+
*/
|
|
231
|
+
async _writeHealthBaseline(cwd, id) {
|
|
232
|
+
if (!this._healthBaseline)
|
|
233
|
+
return;
|
|
234
|
+
try {
|
|
235
|
+
if ((await readSection(cwd, id, HEALTH_BASELINE_SECTION)) !== null)
|
|
236
|
+
return;
|
|
237
|
+
const baseline = await this._healthBaseline(this._ctx);
|
|
238
|
+
if (!baseline)
|
|
239
|
+
return;
|
|
240
|
+
await setTaskSection(cwd, id, HEALTH_BASELINE_SECTION, formatHealthBaseline(baseline));
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// The gate establishes one lazily; never fail a run over the baseline.
|
|
244
|
+
}
|
|
245
|
+
}
|
|
210
246
|
async _run() {
|
|
211
247
|
const cwd = this._cwd;
|
|
212
248
|
const ctx = this._ctx;
|
|
249
|
+
// Resolved HERE, not in the constructor: the run bracket that owns the
|
|
250
|
+
// per-run facts is opened by `run()`, one statement before this. A
|
|
251
|
+
// caller-supplied seam wins, like `logDebug` below.
|
|
252
|
+
this._deps.runContext ??= currentRunContext(cwd);
|
|
213
253
|
// Initialise or resume the TASK file.
|
|
214
254
|
let id;
|
|
215
255
|
let title;
|
|
@@ -221,7 +261,13 @@ export class TaskRunner {
|
|
|
221
261
|
title = frontMatter.title;
|
|
222
262
|
label = frontMatter.label;
|
|
223
263
|
resumePhase = frontMatter.phase;
|
|
224
|
-
|
|
264
|
+
// A re-entry backfills the plan key onto a file written before this
|
|
265
|
+
// run knew one (an older task, or a resume that reaches a task the
|
|
266
|
+
// plan has since keyed); it never clears one already on disk.
|
|
267
|
+
await updateTaskFrontMatter(cwd, id, {
|
|
268
|
+
state: 'in_progress',
|
|
269
|
+
...(this._planKey !== undefined && { plan_key: this._planKey })
|
|
270
|
+
});
|
|
225
271
|
}
|
|
226
272
|
else {
|
|
227
273
|
id = await allocateTaskId(cwd);
|
|
@@ -233,7 +279,8 @@ export class TaskRunner {
|
|
|
233
279
|
phase: 'refine',
|
|
234
280
|
created_at: now,
|
|
235
281
|
updated_at: now,
|
|
236
|
-
title
|
|
282
|
+
title,
|
|
283
|
+
...(this._planKey !== undefined && { plan_key: this._planKey })
|
|
237
284
|
};
|
|
238
285
|
await writeTaskFile(cwd, fm, `\n## raw prompt\n\n${this._rawPrompt.trim() || '(none)'}\n`);
|
|
239
286
|
}
|
|
@@ -242,8 +289,9 @@ export class TaskRunner {
|
|
|
242
289
|
// any phase work — and recover it if the session dies mid-pipeline.
|
|
243
290
|
if (this._onStart)
|
|
244
291
|
await this._onStart(id);
|
|
245
|
-
|
|
246
|
-
|
|
292
|
+
await this._writeHealthBaseline(cwd, id);
|
|
293
|
+
// Wire up the per-task debug log in this run's state dir (state-dir.ts).
|
|
294
|
+
const debugLogPath = runLogPath(cwd, `${id}-debug.log`);
|
|
247
295
|
// `gateDebugWriter` returns undefined at level `off`, so every
|
|
248
296
|
// `logDebug?.(…)` site downstream short-circuits before it formats a string
|
|
249
297
|
// and the file is never created. A caller-supplied `logDebug` seam WINS
|
|
@@ -251,10 +299,7 @@ export class TaskRunner {
|
|
|
251
299
|
// runner-driven test, and production never sets one, so the file writer is
|
|
252
300
|
// unaffected.
|
|
253
301
|
this._deps.logDebug ??= gateDebugWriter((msg) => {
|
|
254
|
-
|
|
255
|
-
fsp.appendFile(debugLogPath, line).catch(() => {
|
|
256
|
-
/* ignore */
|
|
257
|
-
});
|
|
302
|
+
appendDebugLine(debugLogPath, msg);
|
|
258
303
|
});
|
|
259
304
|
this._deps.logDebug?.(`run: start phase=${resumePhase}`);
|
|
260
305
|
// Register as active.
|
|
@@ -331,9 +376,10 @@ export class TaskRunner {
|
|
|
331
376
|
throw new Error('no_verify_block');
|
|
332
377
|
await updateTaskFrontMatter(cwd, id, { state: 'completed', phase: 'done' });
|
|
333
378
|
this._disposeWidget();
|
|
334
|
-
await
|
|
335
|
-
|
|
336
|
-
await this.
|
|
379
|
+
await this._writeTimings(cwd, id);
|
|
380
|
+
const spec = await this._specForDelivery();
|
|
381
|
+
await this._recordHandoff(cwd, id, spec);
|
|
382
|
+
await this._deliverSpec(spec);
|
|
337
383
|
// SAFE CHECKPOINT (post implementation turn): every phase section is
|
|
338
384
|
// on disk and the turn has ENDED. Its edits are uncommitted, so a
|
|
339
385
|
// resume re-delivers the spec onto the partly-edited tree — the same
|
|
@@ -367,7 +413,7 @@ export class TaskRunner {
|
|
|
367
413
|
// useful for analysis. Best-effort — never mask the original error.
|
|
368
414
|
if (this._timings.length > 0) {
|
|
369
415
|
try {
|
|
370
|
-
await
|
|
416
|
+
await this._writeTimings(cwd, id);
|
|
371
417
|
}
|
|
372
418
|
catch {
|
|
373
419
|
/* ignore — preserve original failure */
|
|
@@ -393,8 +439,30 @@ export class TaskRunner {
|
|
|
393
439
|
this._stopWidget?.();
|
|
394
440
|
this._stopWidget = null;
|
|
395
441
|
}
|
|
396
|
-
async
|
|
397
|
-
|
|
442
|
+
async _writeTimings(cwd, id) {
|
|
443
|
+
if (this._timingsWritten)
|
|
444
|
+
return;
|
|
445
|
+
this._timingsWritten = true;
|
|
446
|
+
await mergeTaskSection(cwd, id, 'phase timings', prev => mergeTimings(prev, this._timings));
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Append this delivery to the `## handoff` ledger. The attempt number is the
|
|
450
|
+
* count of deliveries already recorded rather than anything this process
|
|
451
|
+
* carries: a re-entry is a FRESH runner and knows nothing of the one before it.
|
|
452
|
+
*/
|
|
453
|
+
async _recordHandoff(cwd, id, spec) {
|
|
454
|
+
const fix = this._fixContext;
|
|
455
|
+
await mergeTaskSection(cwd, id, 'handoff', prev => {
|
|
456
|
+
const record = {
|
|
457
|
+
attempt: parseHandoff(prev).length + 1,
|
|
458
|
+
specHash: specHash(spec),
|
|
459
|
+
delivered: fix ? 'reattempt' : 'fresh',
|
|
460
|
+
...(fix ? { fixContext: summariseFixContext(fix) } : {})
|
|
461
|
+
};
|
|
462
|
+
return appendHandoff(prev, record);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
async _deliverSpec(spec) {
|
|
398
466
|
// Keep the rich status block alive across the implementation turn (the phase
|
|
399
467
|
// widget was disposed at handoff). Awaited (/task-auto) stays armed across all
|
|
400
468
|
// sub-turns and is disarmed here; fire-and-forget (/task) arms one-shot and its
|
|
@@ -463,13 +531,9 @@ export class TaskRunner {
|
|
|
463
531
|
// specific failure and re-satisfies the VERIFY block, rather than redoing
|
|
464
532
|
// the task from scratch (or repeating the same mistake).
|
|
465
533
|
let fixBanner = '';
|
|
466
|
-
if (this.
|
|
534
|
+
if (this._fixContext) {
|
|
467
535
|
this._deps.logDebug?.('impl-handoff RE-ATTEMPT banner prepended (verify FAIL fix)');
|
|
468
|
-
fixBanner =
|
|
469
|
-
'RE-ATTEMPT — your previous implementation of this task FAILED verification.\n'
|
|
470
|
-
+ "Fix the cause below, then make the spec's VERIFY block pass. Do NOT start over;\n"
|
|
471
|
-
+ 'change only what is needed to resolve the failure.\n\n'
|
|
472
|
-
+ `VERIFICATION FAILURE:\n${this._fixInstruction.trim()}`;
|
|
536
|
+
fixBanner = formatFixBanner(this._fixContext);
|
|
473
537
|
}
|
|
474
538
|
const banners = [fixBanner, apiBanner].filter(b => b && b.length > 0).join('\n\n');
|
|
475
539
|
return banners ? `${banners}\n\n${this._pc.spec}` : this._pc.spec;
|
|
@@ -547,7 +611,9 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
547
611
|
seams: opts.seams,
|
|
548
612
|
onStart: opts.onStart,
|
|
549
613
|
planContext: opts.planContext,
|
|
550
|
-
|
|
614
|
+
planKey: opts.planKey,
|
|
615
|
+
fixContext: opts.fixContext,
|
|
616
|
+
healthBaseline: opts.healthBaseline,
|
|
551
617
|
implAwaited: opts.waitForImplementation
|
|
552
618
|
});
|
|
553
619
|
runEnd = await runner.run();
|
|
@@ -593,7 +659,9 @@ export const gateRunTask = (c, cwd, t, opts) => runSingleTask(c, cwd, t, {
|
|
|
593
659
|
resumeId: opts?.resumeId,
|
|
594
660
|
onStart: opts?.onStart,
|
|
595
661
|
planContext: opts?.planContext,
|
|
596
|
-
|
|
662
|
+
planKey: opts?.planKey,
|
|
663
|
+
fixContext: opts?.fixContext,
|
|
664
|
+
healthBaseline: opts?.healthBaseline
|
|
597
665
|
// NO `seams` here, deliberately. Threading them would need a field on
|
|
598
666
|
// `GateParams` and another on `GateDeps`, and nothing — production or
|
|
599
667
|
// test — would set either: the gate is reached through two orchestrators
|
|
@@ -650,8 +718,16 @@ async function runGatedTaskInner(ctx, cwd, raw, opts = {}) {
|
|
|
650
718
|
// notifyFinish push the fire-and-forget path emits via runSingleTask).
|
|
651
719
|
// Bound late to `active`: a gate autofix can replace the live session.
|
|
652
720
|
const announce = (msg, level) => announceTerminal(active, msg, level);
|
|
721
|
+
// The health baseline is taken at /task's OWN start, this task's equivalent of
|
|
722
|
+
// /task-auto's pre-task checkpoint: the last moment the tree is the one the
|
|
723
|
+
// task inherited. Lazy — a resumed task file already carries its original and
|
|
724
|
+
// never pays for the run. There is no composed title yet, so the loader is
|
|
725
|
+
// labelled with what the user typed, or the id being resumed.
|
|
726
|
+
const label = opts.resumeId ?? raw;
|
|
727
|
+
const capture = deps.captureHealthBaseline;
|
|
728
|
+
const healthBaseline = capture ? { healthBaseline: (c) => capture(c, cwd, label) } : {};
|
|
653
729
|
// First implementation run (blocking).
|
|
654
|
-
const res = await deps.runTask(active, cwd, raw, { resumeId: opts.resumeId });
|
|
730
|
+
const res = await deps.runTask(active, cwd, raw, { resumeId: opts.resumeId, ...healthBaseline });
|
|
655
731
|
active = res.ctx ?? active;
|
|
656
732
|
const tag = res.taskId || 'Task';
|
|
657
733
|
// One dispatch over the named ending, so a cancel can never be reported as a
|
|
@@ -1,32 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project orientation core — a bounded snapshot of the few files a research worker
|
|
3
|
-
* re-reads cold to learn "what is this project" (
|
|
4
|
-
* schema, entrypoints, API surface). The four
|
|
5
|
-
* processes with no shared memory, so without
|
|
6
|
-
* files for itself.
|
|
3
|
+
* re-reads cold to learn "what is this project" (the doc the task cites, manifest,
|
|
4
|
+
* config, project rules, domain types, schema, entrypoints, API surface). The four
|
|
5
|
+
* research workers are separate child processes with no shared memory, so without
|
|
6
|
+
* this each one reads the same hot files for itself.
|
|
7
7
|
*
|
|
8
8
|
* This module picks that core from the file inventory (repo-agnostic, by path
|
|
9
9
|
* convention) and reads it ONCE in the parent. The caller folds the block into a
|
|
10
|
-
* prompt: refine gets
|
|
11
|
-
* the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* prompt: refine gets the manifest/config tiers, and phases.ts prepends the full
|
|
11
|
+
* block to the three EXPLORING research workers — not to TOOLING. It is purely
|
|
12
|
+
* additive: nothing is blocked, so a worker can still read anything it wants;
|
|
13
|
+
* orientation only removes the need to.
|
|
14
|
+
*
|
|
15
|
+
* ELIGIBILITY IS A REGISTRY, not a ladder of regexes: {@link ORIENTATION_RULES} is
|
|
16
|
+
* an ordered list of `{id, tier, match}` rows over an {@link OrientationCandidate},
|
|
17
|
+
* so "does this file orient" and "how fundamental is it" are one row each, and the
|
|
18
|
+
* two facts a path cannot carry on its own — whether the task CITES it, and whether
|
|
19
|
+
* it is VENDORED — are fields on the candidate rather than more pattern-matching.
|
|
14
20
|
*
|
|
15
21
|
* Bounded by design — the snapshot can never overflow the prompt regardless of
|
|
16
|
-
* repo size: a hard total byte budget, a per-file cap
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
22
|
+
* repo size: a hard total byte budget, a per-file cap so one huge file can't eat
|
|
23
|
+
* it, a candidate cap, and a second budget of the same size that only cited
|
|
24
|
+
* documents may draw on. Files that don't fit are simply not pre-supplied; the worker reads
|
|
25
|
+
* them as before. Selection is the pure, tested core; reading takes an injectable
|
|
26
|
+
* reader so it can be exercised without a repo.
|
|
20
27
|
*/
|
|
21
28
|
/**
|
|
22
29
|
* Total bytes the emitted orientation block may occupy. This is the real overflow
|
|
23
30
|
* guard: it bounds the block regardless of repo size, and because the snapshot is
|
|
24
|
-
* prepended to each
|
|
31
|
+
* prepended to each exploring worker it also caps the prefill those workers pay.
|
|
25
32
|
* Selection stops as soon as adding a file would exceed it.
|
|
26
33
|
*/
|
|
27
34
|
export declare const ORIENTATION_BYTE_BUDGET: number;
|
|
28
35
|
/** A single file larger than this is skipped (read by the worker as before). */
|
|
29
36
|
export declare const ORIENTATION_PER_FILE_MAX: number;
|
|
37
|
+
/**
|
|
38
|
+
* A SECOND purse, spent only on cited documents.
|
|
39
|
+
*
|
|
40
|
+
* A design doc is routinely most of the core budget on its own — the recorded mx5
|
|
41
|
+
* run's is 29 KB of 40 KB — so charging it to the shared purse buys the one file
|
|
42
|
+
* the task cites by dropping five files every task needs. Equal to the core
|
|
43
|
+
* budget, so the block can at most double and the bound stays a stated one.
|
|
44
|
+
*/
|
|
45
|
+
export declare const ORIENTATION_CITED_BYTE_BUDGET: number;
|
|
30
46
|
/**
|
|
31
47
|
* Backstop file-count cap — a guard against a pathological repo with hundreds of
|
|
32
48
|
* tiny core files packing the byte budget into noise, NOT a normal-case limit. Set
|
|
@@ -35,37 +51,127 @@ export declare const ORIENTATION_PER_FILE_MAX: number;
|
|
|
35
51
|
*/
|
|
36
52
|
export declare const ORIENTATION_MAX_FILES = 40;
|
|
37
53
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* project (manifest) → how is it built (config) → what
|
|
41
|
-
* (
|
|
42
|
-
* → what
|
|
43
|
-
*
|
|
54
|
+
* The priority ladder, lowest first. The tiers mirror the questions a worker
|
|
55
|
+
* re-derives on every task: what am I being asked about (the cited doc) → what is
|
|
56
|
+
* this project (manifest) → how is it built (config) → what rules does it set for
|
|
57
|
+
* itself (guidelines) → what is its domain model (types/schema) → where does it
|
|
58
|
+
* start (entrypoints) → what is its surface (api) → what does it say about itself
|
|
59
|
+
* (docs).
|
|
60
|
+
*
|
|
61
|
+
* Named rather than numbered at the call sites: refine asks for "manifest and
|
|
62
|
+
* config", not for "0 and 1", and a tier inserted in the middle must not silently
|
|
63
|
+
* change what refine gets.
|
|
64
|
+
*/
|
|
65
|
+
export declare const ORIENTATION_TIERS: {
|
|
66
|
+
readonly cited: 0;
|
|
67
|
+
readonly manifest: 1;
|
|
68
|
+
readonly config: 2;
|
|
69
|
+
readonly guidelines: 3;
|
|
70
|
+
readonly domain: 4;
|
|
71
|
+
readonly entrypoint: 5;
|
|
72
|
+
readonly api: 6;
|
|
73
|
+
readonly docs: 7;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Directories whose contents are never this project's own source: vendored
|
|
77
|
+
* dependencies, build output, and the agent's own scaffolding. A dependency's
|
|
78
|
+
* `package.json` is a manifest by basename and `.pi/skills/**` is a wall of
|
|
79
|
+
* markdown — either would outrank the project's real files and eat the budget.
|
|
80
|
+
*
|
|
81
|
+
* A registry rather than one regex so a project can be given more through
|
|
82
|
+
* `.gitignore` and `orientationExclude` (config.ts) without editing a pattern.
|
|
83
|
+
*/
|
|
84
|
+
export declare const VENDORED_DIRS: readonly string[];
|
|
85
|
+
/** What a rule decides on, beyond the path itself. */
|
|
86
|
+
export interface OrientationCandidate {
|
|
87
|
+
path: string;
|
|
88
|
+
/** The task prompt names this file (a readable @-mention): its spec document. */
|
|
89
|
+
cited: boolean;
|
|
90
|
+
/** Not this project's own source — see {@link VENDORED_DIRS}. */
|
|
91
|
+
vendored: boolean;
|
|
92
|
+
}
|
|
93
|
+
/** One eligibility rule: the tier a matching candidate lands in. */
|
|
94
|
+
export interface OrientationRule {
|
|
95
|
+
id: string;
|
|
96
|
+
tier: number;
|
|
97
|
+
match: (c: OrientationCandidate) => boolean;
|
|
98
|
+
/**
|
|
99
|
+
* This row identifies a file by its exact name (or because the task cites
|
|
100
|
+
* it), so {@link ORIENTATION_EXTENSIONS} does not apply. That filter exists to
|
|
101
|
+
* stop a row from GUESSING at a `.png` or a `.lock`; `Gemfile` and
|
|
102
|
+
* `requirements.txt` are not guesses.
|
|
103
|
+
*/
|
|
104
|
+
byName?: true;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Eligibility, in priority order — FIRST MATCH WINS, and a candidate no row
|
|
108
|
+
* matches is not orientation material and is dropped entirely.
|
|
109
|
+
*
|
|
110
|
+
* The three rejecting rows come first and carry no tier of their own: a vendored
|
|
111
|
+
* file, a test and an absolute path are excluded whatever else they look like.
|
|
112
|
+
* They are rows rather than early returns so the whole policy is one list.
|
|
113
|
+
*/
|
|
114
|
+
export declare const ORIENTATION_RULES: ReadonlyArray<OrientationRule>;
|
|
115
|
+
/**
|
|
116
|
+
* Priority tier for an orientation candidate — lower is more fundamental, null
|
|
117
|
+
* for a path that is not orientation material.
|
|
44
118
|
*/
|
|
45
|
-
export declare function orientationTier(path: string
|
|
119
|
+
export declare function orientationTier(path: string, opts?: {
|
|
120
|
+
cited?: boolean;
|
|
121
|
+
vendored?: boolean;
|
|
122
|
+
}): number | null;
|
|
123
|
+
/**
|
|
124
|
+
* Is this path outside the project's own source? `VENDORED_DIRS` plus whatever
|
|
125
|
+
* `.gitignore` and `orientationExclude` add (see {@link parseIgnorePatterns}).
|
|
126
|
+
*/
|
|
127
|
+
export declare function isVendored(path: string, patterns?: readonly string[]): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* The usable patterns in a `.gitignore`.
|
|
130
|
+
*
|
|
131
|
+
* Negations (`!kept.md`) are DROPPED rather than honoured: un-ignoring is a
|
|
132
|
+
* whole-file-order semantics this does not implement, and the inventory a real
|
|
133
|
+
* run orients over is `git ls-files`, which has already applied the real rules.
|
|
134
|
+
* What is left here only has to keep bulk out.
|
|
135
|
+
*/
|
|
136
|
+
export declare function parseIgnorePatterns(gitignore: string): string[];
|
|
137
|
+
/** One selected file and the tier it was selected at. */
|
|
138
|
+
export interface OrientationPick {
|
|
139
|
+
path: string;
|
|
140
|
+
tier: number;
|
|
141
|
+
}
|
|
142
|
+
export interface OrientationSelectOptions {
|
|
143
|
+
/** Paths the task prompt cites — its spec documents. */
|
|
144
|
+
cited?: readonly string[];
|
|
145
|
+
/** `.gitignore` lines plus `orientationExclude`, on top of `VENDORED_DIRS`. */
|
|
146
|
+
excludePatterns?: readonly string[];
|
|
147
|
+
}
|
|
46
148
|
/**
|
|
47
149
|
* Rank inventory paths into orientation priority order: by tier, then shallower
|
|
48
150
|
* paths first (a root entrypoint beats a deeply-nested one), then alphabetical
|
|
49
151
|
* for a fully deterministic order. Non-orientation paths are dropped. The result
|
|
50
152
|
* is the *candidate* order; the byte budget is applied later when reading.
|
|
51
153
|
*/
|
|
52
|
-
export declare function selectOrientationFiles(inventoryPaths: string[]):
|
|
154
|
+
export declare function selectOrientationFiles(inventoryPaths: string[], opts?: OrientationSelectOptions): OrientationPick[];
|
|
53
155
|
export interface OrientationResult {
|
|
54
156
|
/** Formatted PROJECT ORIENTATION block to prepend to the worker header, or ''. */
|
|
55
157
|
block: string;
|
|
56
158
|
/** Resolved-relative paths whose full content was pre-supplied in the block. */
|
|
57
159
|
supplied: Set<string>;
|
|
58
160
|
}
|
|
161
|
+
export interface OrientationBuildOptions extends OrientationSelectOptions {
|
|
162
|
+
byteBudget?: number;
|
|
163
|
+
/** The separate purse cited documents are charged to — see
|
|
164
|
+
* {@link ORIENTATION_CITED_BYTE_BUDGET}. */
|
|
165
|
+
citedByteBudget?: number;
|
|
166
|
+
perFileMax?: number;
|
|
167
|
+
maxFiles?: number;
|
|
168
|
+
}
|
|
59
169
|
/**
|
|
60
170
|
* Read the orientation core within the byte budget and format it as a header
|
|
61
171
|
* block. `readFile` returns a file's text or null (missing/unreadable/binary) —
|
|
62
172
|
* a null or over-cap file is skipped, not fatal. Greedy in priority order: take
|
|
63
|
-
* each file whose content fits
|
|
64
|
-
* skip the rest. Returns an empty block (and empty set) when nothing
|
|
65
|
-
* so the caller falls back to today's behavior.
|
|
173
|
+
* each file whose content fits its tier's per-file cap and the remaining total
|
|
174
|
+
* budget; skip the rest. Returns an empty block (and empty set) when nothing
|
|
175
|
+
* qualifies, so the caller falls back to today's behavior.
|
|
66
176
|
*/
|
|
67
|
-
export declare function buildOrientation(inventoryPaths: string[], readFile: (path: string) => Promise<string | null>, opts?:
|
|
68
|
-
byteBudget?: number;
|
|
69
|
-
perFileMax?: number;
|
|
70
|
-
maxFiles?: number;
|
|
71
|
-
}): Promise<OrientationResult>;
|
|
177
|
+
export declare function buildOrientation(inventoryPaths: string[], readFile: (path: string) => Promise<string | null>, opts?: OrientationBuildOptions): Promise<OrientationResult>;
|