@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.
Files changed (123) hide show
  1. package/README.md +7 -4
  2. package/dist/config/config.d.ts +61 -18
  3. package/dist/config/config.js +50 -6
  4. package/dist/config/register.js +18 -5
  5. package/dist/shared/content-tokens.d.ts +16 -0
  6. package/dist/shared/content-tokens.js +137 -0
  7. package/dist/task/accept-debt.d.ts +15 -1
  8. package/dist/task/accept-debt.js +4 -0
  9. package/dist/task/artifact-closure.js +7 -1
  10. package/dist/task/auto-io.d.ts +59 -7
  11. package/dist/task/auto-io.js +140 -47
  12. package/dist/task/auto-orchestrator.d.ts +22 -19
  13. package/dist/task/auto-orchestrator.js +126 -112
  14. package/dist/task/child-runner.d.ts +18 -0
  15. package/dist/task/child-runner.js +7 -8
  16. package/dist/task/command-run.d.ts +4 -0
  17. package/dist/task/command-run.js +4 -2
  18. package/dist/task/constraint-policy.d.ts +52 -0
  19. package/dist/task/constraint-policy.js +66 -0
  20. package/dist/task/context-silence.d.ts +5 -4
  21. package/dist/task/context-silence.js +13 -5
  22. package/dist/task/coverage-loop.d.ts +7 -4
  23. package/dist/task/coverage-loop.js +16 -138
  24. package/dist/task/debug-log.d.ts +25 -2
  25. package/dist/task/debug-log.js +64 -10
  26. package/dist/task/decompose-fidelity.d.ts +17 -4
  27. package/dist/task/decompose-fidelity.js +95 -55
  28. package/dist/task/decompose-granularity.d.ts +7 -7
  29. package/dist/task/decompose-granularity.js +8 -8
  30. package/dist/task/env-notes.d.ts +65 -19
  31. package/dist/task/env-notes.js +152 -50
  32. package/dist/task/external-context.js +25 -5
  33. package/dist/task/fix-context.d.ts +39 -0
  34. package/dist/task/fix-context.js +58 -0
  35. package/dist/task/gate-deps.d.ts +49 -0
  36. package/dist/task/gate-deps.js +199 -38
  37. package/dist/task/gate-evidence.d.ts +51 -0
  38. package/dist/task/gate-evidence.js +90 -0
  39. package/dist/task/gate-resolution.d.ts +119 -0
  40. package/dist/task/gate-resolution.js +120 -0
  41. package/dist/task/git-state-guard.js +3 -26
  42. package/dist/task/handoff.d.ts +35 -0
  43. package/dist/task/handoff.js +100 -0
  44. package/dist/task/health-baseline.d.ts +87 -0
  45. package/dist/task/health-baseline.js +158 -0
  46. package/dist/task/ledger.d.ts +10 -0
  47. package/dist/task/ledger.js +22 -11
  48. package/dist/task/lint-fix.d.ts +11 -0
  49. package/dist/task/lint-fix.js +14 -8
  50. package/dist/task/loop-detector.d.ts +52 -3
  51. package/dist/task/loop-detector.js +81 -5
  52. package/dist/task/mentions.d.ts +17 -0
  53. package/dist/task/mentions.js +86 -0
  54. package/dist/task/orchestrator.d.ts +53 -9
  55. package/dist/task/orchestrator.js +104 -28
  56. package/dist/task/orientation.d.ts +135 -29
  57. package/dist/task/orientation.js +259 -67
  58. package/dist/task/owned-freeze-reassign.d.ts +7 -3
  59. package/dist/task/owned-freeze-reassign.js +13 -5
  60. package/dist/task/parsers.d.ts +6 -1
  61. package/dist/task/parsers.js +21 -4
  62. package/dist/task/phases.d.ts +1 -1
  63. package/dist/task/phases.js +125 -153
  64. package/dist/task/plan-orchestrator.js +5 -6
  65. package/dist/task/plan-rounds.d.ts +1 -0
  66. package/dist/task/plan-rounds.js +8 -1
  67. package/dist/task/prohibition-probe.d.ts +6 -1
  68. package/dist/task/prohibition-probe.js +26 -6
  69. package/dist/task/prompts.js +13 -2
  70. package/dist/task/qa-transcript.d.ts +17 -0
  71. package/dist/task/qa-transcript.js +28 -0
  72. package/dist/task/question-source.d.ts +16 -2
  73. package/dist/task/question-source.js +21 -8
  74. package/dist/task/repo-health-check.d.ts +27 -0
  75. package/dist/task/repo-health-check.js +39 -5
  76. package/dist/task/requirements.d.ts +67 -13
  77. package/dist/task/requirements.js +129 -67
  78. package/dist/task/research-worker.d.ts +43 -2
  79. package/dist/task/research-worker.js +103 -8
  80. package/dist/task/run-bracket.d.ts +9 -4
  81. package/dist/task/run-bracket.js +9 -0
  82. package/dist/task/run-context.d.ts +152 -0
  83. package/dist/task/run-context.js +277 -0
  84. package/dist/task/spec-doc.d.ts +78 -0
  85. package/dist/task/spec-doc.js +205 -0
  86. package/dist/task/spec-model.d.ts +70 -0
  87. package/dist/task/spec-model.js +139 -0
  88. package/dist/task/stall-detector.d.ts +1 -1
  89. package/dist/task/stall-detector.js +9 -5
  90. package/dist/task/state-dir.d.ts +29 -0
  91. package/dist/task/state-dir.js +80 -0
  92. package/dist/task/suppression-probe.d.ts +67 -0
  93. package/dist/task/suppression-probe.js +78 -0
  94. package/dist/task/task-gates.d.ts +39 -57
  95. package/dist/task/task-gates.js +115 -195
  96. package/dist/task/task-io.d.ts +10 -0
  97. package/dist/task/task-io.js +69 -18
  98. package/dist/task/task-parsers.js +6 -6
  99. package/dist/task/task-types.d.ts +7 -0
  100. package/dist/task/timings.d.ts +11 -0
  101. package/dist/task/timings.js +21 -0
  102. package/dist/task/tree-hash.d.ts +8 -0
  103. package/dist/task/tree-hash.js +55 -0
  104. package/dist/task/verify-reconcile.d.ts +4 -3
  105. package/dist/task/verify-reconcile.js +7 -8
  106. package/dist/task/verify-resolution.d.ts +28 -5
  107. package/dist/task/verify-resolution.js +70 -13
  108. package/dist/task/verify-work.d.ts +98 -35
  109. package/dist/task/verify-work.js +227 -60
  110. package/dist/task/yolo.d.ts +0 -13
  111. package/dist/task/yolo.js +9 -21
  112. package/dist/workers/pi-worker-core.d.ts +20 -0
  113. package/dist/workers/pi-worker-core.js +16 -11
  114. package/dist/workers/pi-worker-docs.d.ts +6 -5
  115. package/dist/workers/pi-worker-docs.js +8 -7
  116. package/dist/workers/pi-worker-fetch.d.ts +3 -2
  117. package/dist/workers/pi-worker-fetch.js +5 -4
  118. package/dist/workers/pi-worker-search.js +5 -4
  119. package/dist/workers/research-cache.d.ts +23 -1
  120. package/dist/workers/research-cache.js +63 -3
  121. package/dist/workers/shared.d.ts +18 -0
  122. package/dist/workers/shared.js +38 -22
  123. 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 { formatTimings } from './timings.js';
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
- _fixInstruction;
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._fixInstruction = opts.fixInstruction;
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
- await updateTaskFrontMatter(cwd, id, { state: 'in_progress' });
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
- // Wire up per-task debug log (<cwd>/.pi-tasks/TASK_XXXX-debug.log).
246
- const debugLogPath = path.join(tasksDir(cwd), `${id}-debug.log`);
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
- const line = `${new Date().toISOString()} ${msg}\n`;
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 setTaskSection(cwd, id, 'phase timings', formatTimings(this._timings));
335
- await setTaskSection(cwd, id, 'handoff', `handoff_at: ${new Date().toISOString()}`);
336
- await this._deliverSpec(ctx);
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 setTaskSection(cwd, id, 'phase timings', formatTimings(this._timings));
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 _deliverSpec(_ctx) {
397
- const spec = await this._specForDelivery();
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._fixInstruction && this._fixInstruction.trim().length > 0) {
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
- fixInstruction: opts.fixInstruction,
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
- fixInstruction: opts?.fixInstruction
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" (manifest, config, domain types,
4
- * schema, entrypoints, API surface). The four research workers are separate child
5
- * processes with no shared memory, so without this each one reads the same hot
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 a tier-0/1 subset, and phases.ts prepends the full block to
11
- * the two READ-HEAVY research workers (`worker:files`, `worker:apis`) — not to all
12
- * four. It is purely additive: nothing is blocked, so a worker can still read
13
- * anything it wants; orientation only removes the need to.
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 (one huge file can't eat
17
- * the budget), and a candidate cap. Files that don't fit are simply not
18
- * pre-supplied; the worker reads them as before. Selection is the pure, tested
19
- * core; reading takes an injectable reader so it can be exercised without a repo.
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 read-heavy worker it also caps the prefill those workers pay.
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
- * Priority tier for an orientation candidate lower is more fundamental. The
39
- * tiers mirror the questions a worker re-derives on every task: what is this
40
- * project (manifest) → how is it built (config) → what is its domain model
41
- * (types/schema) → where does it start (entrypoints) → what is its surface (api)
42
- * → what does it say about itself (docs). Returns null for paths that aren't
43
- * orientation material, so they're dropped entirely.
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): number | null;
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[]): 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 the per-file cap and the remaining total budget;
64
- * skip the rest. Returns an empty block (and empty set) when nothing qualifies,
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>;