@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.
- package/README.md +20 -5
- package/dist/task/auto-orchestrator.d.ts +36 -0
- package/dist/task/auto-orchestrator.js +43 -6
- package/dist/task/cancel-points.d.ts +34 -6
- package/dist/task/cancel-points.js +62 -10
- package/dist/task/child-status.js +13 -1
- package/dist/task/context-attribution.js +18 -6
- package/dist/task/external-context.d.ts +8 -1
- package/dist/task/external-context.js +52 -4
- package/dist/task/orchestrator.js +47 -5
- package/dist/task/phases.js +2 -1
- package/dist/task/plan-orchestrator.js +10 -1
- package/dist/task/prompts.d.ts +7 -1
- package/dist/task/prompts.js +14 -4
- package/dist/task/research-worker.js +11 -0
- package/dist/task/run-bracket.js +13 -1
- package/dist/task/task-gates.js +34 -0
- package/dist/workers/docs-cache.js +50 -3
- package/dist/workers/docs-chunk.d.ts +6 -3
- package/dist/workers/docs-chunk.js +8 -5
- package/dist/workers/docs-core.d.ts +27 -3
- package/dist/workers/docs-core.js +104 -41
- package/dist/workers/docs-ecosystems.d.ts +173 -0
- package/dist/workers/docs-ecosystems.js +449 -0
- package/dist/workers/docs-index.d.ts +2 -1
- package/dist/workers/docs-index.js +55 -27
- package/dist/workers/docs-project.d.ts +10 -0
- package/dist/workers/docs-project.js +86 -24
- package/dist/workers/docs-resolve.d.ts +6 -1
- package/dist/workers/docs-resolve.js +4 -3
- package/dist/workers/docs-retrieve.d.ts +2 -0
- package/dist/workers/docs-retrieve.js +11 -11
- package/dist/workers/eco-cargo.d.ts +115 -0
- package/dist/workers/eco-cargo.js +793 -0
- package/dist/workers/eco-hackage.d.ts +93 -0
- package/dist/workers/eco-hackage.js +508 -0
- package/dist/workers/npm-version.d.ts +5 -3
- package/dist/workers/npm-version.js +6 -4
- package/dist/workers/pi-worker-docs.d.ts +18 -4
- package/dist/workers/pi-worker-docs.js +57 -19
- package/dist/workers/research-cache.d.ts +2 -13
- package/dist/workers/research-cache.js +22 -46
- package/dist/workers/shared.d.ts +16 -5
- package/dist/workers/shared.js +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,10 +68,10 @@ A whole plan — `/task-auto` splits it into an ordered task list and runs each
|
|
|
68
68
|
| `/task-plan <prompt>` | Plan one task with the model — it asks, you answer, ask it something back, or proceed — then run it through `/task`. |
|
|
69
69
|
| `/task-list` | Open the task list in an editor dialog. |
|
|
70
70
|
| `/task-resume [id]` | Resume the most recent (or named) unfinished task. |
|
|
71
|
-
| `/task-cancel` |
|
|
71
|
+
| `/task-cancel` | Stop the running task at the next safe checkpoint (still resumable). Mid-phase it kills the running child; during the implementation turn it lets the turn finish and stops before the gates. |
|
|
72
72
|
| `/task-auto <feature>` | Plan a feature into a task list and run each title through `/task` in order (resumable). |
|
|
73
73
|
| `/task-auto-resume [--unattended]` | Resume the active `/task-auto` run at the next unfinished task. `--unattended` is the boot-hook form: in-flight runs only. |
|
|
74
|
-
| `/task-auto-cancel` | Stop the `/task-auto` loop
|
|
74
|
+
| `/task-auto-cancel` | Stop the `/task-auto` loop at the next safe checkpoint — the end of the current phase, research worker, implementation turn or gate, not the end of the task (still resumable). During planning it abandons the plan, which is not yet written. |
|
|
75
75
|
| `/task-config` | Toggle pi-task settings in an editor dialog: remote control, auto-commit, verify work, enforce guidelines, project tour, parallel research, research cache, search engine, command timeout, stuck reply retry, yolo mode, debug logs, one `watch:` toggle per live tool, and one `ext:` toggle per installed host extension. |
|
|
76
76
|
| `/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. |
|
|
77
77
|
|
|
@@ -191,12 +191,25 @@ Fetches a URL, cleans HTML to markdown ([Readability](https://github.com/mozilla
|
|
|
191
191
|
- The extraction child runs with `--no-tools` to mitigate visible-text prompt injection.
|
|
192
192
|
|
|
193
193
|
### `pi-worker-docs`
|
|
194
|
-
Resolves an installed
|
|
194
|
+
Resolves an installed package, indexes its API surface and README into a local SQLite cache, retrieves the most relevant chunks for your `query`, and passes them to an isolated child that extracts the focused answer. Version-pinned to whatever the project actually resolved.
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
**The manifest decides which registry, not the model.** `text`, `base`, `aeson`, `tokio` and `clap` are all real npm packages *and* real Rust/Haskell ones, so a name alone cannot say which was meant — and guessing npm returns a confident answer about an unrelated package. That is a wrong answer, not a miss.
|
|
197
|
+
|
|
198
|
+
| Ecosystem | Detected by | Surface read | Version comes from |
|
|
199
|
+
| --- | --- | --- | --- |
|
|
200
|
+
| `npm` | `package.json`, or a `node_modules/` directory | the `.d.ts` files the package ships, plus README | the installed `package.json` |
|
|
201
|
+
| `cargo` | `Cargo.toml`, at the directory or one level below it | `.rs` source reduced to public item heads, doc comments and attributes | `Cargo.lock` |
|
|
202
|
+
| `hackage` | `*.cabal`, `cabal.project`, `stack.yaml` or `package.yaml` | `.hs` source reduced to the export list, signatures and type declarations | `dist-newstyle/cache/plan.json`, then `cabal.project.freeze`, then `stack.yaml.lock` |
|
|
203
|
+
|
|
204
|
+
- **No manifest, no lookup.** In a directory with none of the above the tool refuses, spawns nothing and installs nothing, and points you at `pi-worker-search` / `pi-worker-fetch` instead.
|
|
205
|
+
- **Two manifests** (a Tauri app, say) are resolved by whichever registry already has the package on disk. If neither does, the call is refused as ambiguous and you pass `ecosystem: "cargo"` to say which.
|
|
206
|
+
- A package the project does not have is fetched once into a dedicated cache dir: `npm install --ignore-scripts` for npm, the `.crate` tarball for cargo, the Hackage tarball (or cabal's own cached copy) for hackage.
|
|
207
|
+
- A Haskell **module** name is refused by name — `Data.Aeson` is not a package, `aeson` is.
|
|
208
|
+
- The first call for a `(ecosystem, package, version)` triple pays a one-time ingestion cost; later calls are FTS-only.
|
|
198
209
|
- Cache lives at `${XDG_CACHE_HOME:-~/.cache}/pi-worker/docs.sqlite` — delete it to reset.
|
|
199
210
|
|
|
211
|
+
**Known gaps.** The rest of pi-task is still npm-shaped: the final gate, repo-health and orientation read the working directory first-wins, so a Tauri repo gets cargo docs answers while the gate runs `bun run test`, not `cargo test`. Phantom-import checking, dependency-name extraction for research enrichment, and the `@types/…` redirect chain are npm concepts and are no-ops elsewhere. Adding an ecosystem is one row in `src/workers/docs-ecosystems.ts` plus its parsers — profiles live in code and arrive as pull requests with tests, never as user configuration.
|
|
212
|
+
|
|
200
213
|
## Settings — `/task-config`
|
|
201
214
|
|
|
202
215
|
Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings persist to `~/.config/pi-task/config.json`.
|
|
@@ -226,6 +239,8 @@ Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings pe
|
|
|
226
239
|
| --- | --- | --- |
|
|
227
240
|
| `BRAVE_SEARCH_API_KEY` / `BRAVE_API_KEY` | `pi-worker-search`, research enrichment | Required only when the **Brave** search engine is selected in `/task-config`. |
|
|
228
241
|
| `XDG_CACHE_HOME` | `pi-worker-docs` | Overrides the docs cache location (defaults to `~/.cache`). |
|
|
242
|
+
| `CARGO_HOME` | `pi-worker-docs` | Where crate source checkouts are read from (defaults to `~/.cargo`). |
|
|
243
|
+
| `CABAL_DIR` | `pi-worker-docs` | Where cabal's downloaded package tarballs are read from (also checks `~/.cabal/packages` and `${XDG_CACHE_HOME:-~/.cache}/cabal/packages`). |
|
|
229
244
|
| `XDG_DATA_HOME` | remote push | Where the VAPID keypair is stored (defaults to `~/.local/share`). |
|
|
230
245
|
| `PI_REMOTE_PUSH_SUBJECT` | remote push | VAPID JWT `sub` contact. Defaults to the project URL; set your own `mailto:you@domain.com` or `https://…`. |
|
|
231
246
|
| `PI_REMOTE_PUSH_DEBUG` | remote push | When set (e.g. `1`), logs push delivery and push-service HTTP status. Off by default. |
|
|
@@ -162,6 +162,42 @@ export interface CoveredPlan {
|
|
|
162
162
|
export declare function coverPlan(ctx: ExtensionCommandContext, cwd: string, deps: AutoDeps, oriented: OrientedFeature, clarifications: string, decomposed: DecomposedPlan, specDangling: DanglingRef[]): Promise<CoveredPlan>;
|
|
163
163
|
/** Plan phase: clarify → decompose → write AUTO file. Returns the new id, or null. */
|
|
164
164
|
export declare function planAuto(ctx: ExtensionCommandContext, cwd: string, feature: string, deps: AutoDeps): Promise<string | null>;
|
|
165
|
+
/**
|
|
166
|
+
* One best-effort, HOST-GROUNDED extraction: ask a child to emit lines, drop
|
|
167
|
+
* every line the design does not literally contain, log the kept/emitted split,
|
|
168
|
+
* and append what survives to a run-level artifact.
|
|
169
|
+
*
|
|
170
|
+
* The grounding step is the reason this shape exists rather than a plain child
|
|
171
|
+
* call. A child asked for interface facts will paraphrase and occasionally invent
|
|
172
|
+
* them, and an invented fact in a run-level registry is read as
|
|
173
|
+
* authoritative by every downstream refine/compose/verify. So nothing the child
|
|
174
|
+
* says is trusted: `ground` re-checks each emitted line against the design text
|
|
175
|
+
* host-side, and only substrings survive.
|
|
176
|
+
*
|
|
177
|
+
* Best-effort by contract. These artifacts SHARPEN planning; none of them gates
|
|
178
|
+
* it, so a fault here is swallowed rather than failing a run that is otherwise
|
|
179
|
+
* fine — which is why the whole body sits in one `catch {}`.
|
|
180
|
+
*
|
|
181
|
+
* The two call sites (contracts, launch scripts) were byte-identical apart from
|
|
182
|
+
* the four values this row carries, and the contracts copy parsed its child's
|
|
183
|
+
* output twice — once for the artifact and once for the log count — because the
|
|
184
|
+
* duplication made the second parse easy to miss.
|
|
185
|
+
*/
|
|
186
|
+
export declare function runGroundedExtraction<T>(row: {
|
|
187
|
+
cwd: string;
|
|
188
|
+
runChild: (name: string, tools: string, prompt: string) => Promise<string>;
|
|
189
|
+
/** Child name — also the key AUTO_PLAN_STEPS renders in the loader. */
|
|
190
|
+
child: string;
|
|
191
|
+
/** Singular noun for the log line ("contract", "script"). */
|
|
192
|
+
noun: string;
|
|
193
|
+
/** Log prefix naming the step. */
|
|
194
|
+
label: string;
|
|
195
|
+
prompt: string;
|
|
196
|
+
parse: (raw: string) => T[];
|
|
197
|
+
/** Keep only what the design itself backs. Runs host-side, never the child. */
|
|
198
|
+
ground: (emitted: T[]) => T[];
|
|
199
|
+
append: (cwd: string, kept: T[]) => Promise<void>;
|
|
200
|
+
}): Promise<void>;
|
|
165
201
|
export declare function requestAutoCancel(): void;
|
|
166
202
|
export declare function runAutoLoop(ctx: ExtensionCommandContext, cwd: string, id: string, deps: AutoDeps): Promise<void>;
|
|
167
203
|
export declare function registerTaskAuto(pi: ExtensionAPI): void;
|
|
@@ -773,7 +773,8 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
|
|
|
773
773
|
try {
|
|
774
774
|
verdict = parseCoverageVerdict(await deps.runChild('decompose-coverage', '', DECOMPOSE_COVERAGE_PROMPT(featureForModel, clarifications, titles)));
|
|
775
775
|
}
|
|
776
|
-
catch {
|
|
776
|
+
catch (err) {
|
|
777
|
+
rethrowIfCancelled(err);
|
|
777
778
|
verdict = null;
|
|
778
779
|
}
|
|
779
780
|
const verdictMissing = verdict?.kind === 'incomplete' ? verdict.missing : [];
|
|
@@ -796,7 +797,8 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
|
|
|
796
797
|
+ `${acc.crossCutting.length} cross-cutting, ${acc.unmapped.length} unmapped; `
|
|
797
798
|
+ `${covered.size} requirement(s) title-grounded`);
|
|
798
799
|
}
|
|
799
|
-
catch {
|
|
800
|
+
catch (err) {
|
|
801
|
+
rethrowIfCancelled(err);
|
|
800
802
|
// mapping fault — Fix A accounting degrades; the grounded owned-set
|
|
801
803
|
// above still guards against drops.
|
|
802
804
|
}
|
|
@@ -1080,7 +1082,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
1080
1082
|
* output twice — once for the artifact and once for the log count — because the
|
|
1081
1083
|
* duplication made the second parse easy to miss.
|
|
1082
1084
|
*/
|
|
1083
|
-
async function runGroundedExtraction(row) {
|
|
1085
|
+
export async function runGroundedExtraction(row) {
|
|
1084
1086
|
try {
|
|
1085
1087
|
const emitted = row.parse(await row.runChild(row.child, '', row.prompt));
|
|
1086
1088
|
const grounded = row.ground(emitted);
|
|
@@ -1088,10 +1090,32 @@ async function runGroundedExtraction(row) {
|
|
|
1088
1090
|
+ ` from ${emitted.length} emitted`);
|
|
1089
1091
|
await row.append(row.cwd, grounded);
|
|
1090
1092
|
}
|
|
1091
|
-
catch {
|
|
1093
|
+
catch (err) {
|
|
1094
|
+
rethrowIfCancelled(err);
|
|
1092
1095
|
// best-effort artifact — never a planning blocker
|
|
1093
1096
|
}
|
|
1094
1097
|
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Let a cancel through a best-effort catch.
|
|
1100
|
+
*
|
|
1101
|
+
* Planning's degrade-quietly catches exist so one weak child cannot sink a plan,
|
|
1102
|
+
* and every one of them predates the `plan:` checkpoint. That checkpoint throws
|
|
1103
|
+
* USER_CANCELLED from inside `runPlanningChild`, so without this the throw reads
|
|
1104
|
+
* as "that extraction failed": planning carries on, writes the AUTO file, and
|
|
1105
|
+
* leaves a resumable run whose contracts silently lost the entries the cancelled
|
|
1106
|
+
* child would have grounded. Cancel means abandon the plan, so it is not a
|
|
1107
|
+
* degradable fault.
|
|
1108
|
+
*
|
|
1109
|
+
* The requirement-extraction catch already gets this right through
|
|
1110
|
+
* `isFatalChildCause`, which covers USER_CANCELLED as well as the fatal kills.
|
|
1111
|
+
* This is the narrow half of the same rule for the catches that guard only an
|
|
1112
|
+
* optional artifact, where promoting every fatal kill would be a behaviour change
|
|
1113
|
+
* this fix has no evidence for.
|
|
1114
|
+
*/
|
|
1115
|
+
function rethrowIfCancelled(err) {
|
|
1116
|
+
if (err instanceof Error && err.message === USER_CANCELLED)
|
|
1117
|
+
throw err;
|
|
1118
|
+
}
|
|
1095
1119
|
/** The two feature-level planning children, shown as steps in the loader. */
|
|
1096
1120
|
const AUTO_PLAN_STEPS = {
|
|
1097
1121
|
'auto-clarify': { step: 'clarify', stepNum: 1 },
|
|
@@ -1470,7 +1494,12 @@ async function handleTaskAuto(args, ctx) {
|
|
|
1470
1494
|
catch (err) {
|
|
1471
1495
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1472
1496
|
if (msg === USER_CANCELLED) {
|
|
1473
|
-
|
|
1497
|
+
// Say what was thrown away. Planning writes nothing until it
|
|
1498
|
+
// finishes, so there is no half-plan to resume and no
|
|
1499
|
+
// /task-auto-resume to offer — the user has to start over,
|
|
1500
|
+
// and a bare "cancelled" would leave them looking for one.
|
|
1501
|
+
announceDone(ctx, '/task-auto cancelled — the plan was discarded. Nothing was written; '
|
|
1502
|
+
+ 'run /task-auto again to re-plan.', 'warning');
|
|
1474
1503
|
return;
|
|
1475
1504
|
}
|
|
1476
1505
|
announceDone(ctx, `/task-auto planning failed: ${msg}`, 'error');
|
|
@@ -1559,8 +1588,16 @@ async function handleTaskAutoCancel(_args, ctx) {
|
|
|
1559
1588
|
* promise "after the current task": the request is now honoured at the next safe
|
|
1560
1589
|
* checkpoint (see cancel-points.ts), which mid-spec-pipeline is the end of the
|
|
1561
1590
|
* current phase, not the end of the task.
|
|
1591
|
+
*
|
|
1592
|
+
* It names the seam set rather than the one seam the run happens to be between,
|
|
1593
|
+
* because that seam is not knowable from here — the checkpoint trail says where
|
|
1594
|
+
* the run has BEEN. Naming the set is what stops "next safe checkpoint" reading
|
|
1595
|
+
* as "some time before the run ends", which is how a wait that is really one
|
|
1596
|
+
* research worker gets mistaken for a wait of the whole task.
|
|
1562
1597
|
*/
|
|
1563
|
-
const CANCEL_ACK = 'Stopping /task-auto at the next safe checkpoint
|
|
1598
|
+
const CANCEL_ACK = 'Stopping /task-auto at the next safe checkpoint — the next planning child, or the '
|
|
1599
|
+
+ 'end of the current phase, research worker, implementation turn or gate. The model '
|
|
1600
|
+
+ 'call already in flight finishes first.';
|
|
1564
1601
|
/**
|
|
1565
1602
|
* Deliver a /task-auto-cancel typed in the terminal while a run owns the main
|
|
1566
1603
|
* loop — the run bracket's `onCancel`. The armed listener watches raw stdin, so
|
|
@@ -27,18 +27,42 @@
|
|
|
27
27
|
* pre-final-gate (run-final-gate) every task is checked off and committed; the
|
|
28
28
|
* whole-repo gate has not started. A resume re-enters the same
|
|
29
29
|
* branch.
|
|
30
|
+
* plan:<child> (child-status, runPlanningChild — the one funnel BOTH
|
|
31
|
+
* /task-auto's planning and /task-plan go through) DURABLE BY
|
|
32
|
+
* DISCARD, and the one seam here that is not durable by writing.
|
|
33
|
+
* Nothing planning produces reaches disk until planAuto's final
|
|
34
|
+
* writeTaskFile, so there is no partial plan to resume and no
|
|
35
|
+
* half-state to repair — stopping abandons the whole plan. That
|
|
36
|
+
* is the point: the alternative measured at 13+ minutes of
|
|
37
|
+
* running after the user asked to stop.
|
|
38
|
+
* research:<w> (research-worker) after persistSection. The four workers run
|
|
39
|
+
* serially by default and each one's section is read back by
|
|
40
|
+
* readCached, so a resume skips every worker already on disk.
|
|
41
|
+
* Costs nothing and repeats nothing — the only seam here that is
|
|
42
|
+
* free in both directions.
|
|
43
|
+
* impl:post-turn (orchestrator) the implementation turn has ENDED and the spec
|
|
44
|
+
* sections are all on disk. The turn's edits are uncommitted, so
|
|
45
|
+
* a resume re-delivers the spec onto the partly-edited tree —
|
|
46
|
+
* identical to the shipped ESC-then-decline-steer ending.
|
|
47
|
+
* gate:post-commit (task-gates) the task is checked off and its snapshot is in
|
|
48
|
+
* HEAD. Only the enforce pass is skipped, and enforce is
|
|
49
|
+
* re-runnable.
|
|
50
|
+
* gate:pre-resolution (task-gates) at the top of the verify-resolution loop, before
|
|
51
|
+
* a round spends a lint fix, a research child or a whole
|
|
52
|
+
* implementation re-run. The task file is demoted there: it
|
|
53
|
+
* still reads `completed` from spec handoff, and the work is
|
|
54
|
+
* neither verified nor committed.
|
|
30
55
|
*
|
|
31
|
-
* DELIBERATELY NOT
|
|
32
|
-
* - mid implementation turn
|
|
56
|
+
* DELIBERATELY NOT a checkpoint — stopping here is not safe:
|
|
57
|
+
* - mid implementation turn. The turn is a host-session turn, not a child, so
|
|
58
|
+
* stopping it means abandoning a half-applied edit set with no commit behind
|
|
59
|
+
* it. `impl:post-turn` waits for the turn to end instead. The user's ESC
|
|
33
60
|
* (declined steer) path already covers "stop now, I accept a partial tree".
|
|
34
|
-
* - between the implementation turn and the gates, or inside the gates: the
|
|
35
|
-
* work is written but unverified and uncommitted; the gates are what make it
|
|
36
|
-
* durable. Cancel is observed on the far side, at loop-top.
|
|
37
61
|
*/
|
|
38
62
|
/** Every place the cancel flag is polled. A closed union so the tests enumerate
|
|
39
63
|
* the same set the loop does — and they do: cancel-points.test.ts asserts on the
|
|
40
64
|
* recorded trail rather than on the loop's own bookkeeping. */
|
|
41
|
-
export type CancelCheckpoint = 'loop-top' | 'pre-task' | 'pre-final-gate' | `phase:${string}`;
|
|
65
|
+
export type CancelCheckpoint = 'loop-top' | 'pre-task' | 'pre-final-gate' | 'impl:post-turn' | 'gate:post-commit' | 'gate:pre-resolution' | `phase:${string}` | `plan:${string}` | `research:${string}`;
|
|
42
66
|
export declare function requestCancel(): void;
|
|
43
67
|
export declare function isCancelRequested(): boolean;
|
|
44
68
|
/**
|
|
@@ -61,5 +85,9 @@ export declare function resetCheckpointTrail(): void;
|
|
|
61
85
|
* @returns true when the caller must stop here.
|
|
62
86
|
*/
|
|
63
87
|
export declare function cancelCheckpoint(where: CancelCheckpoint): boolean;
|
|
88
|
+
/** Let only the seams this answers true for fire. Tests only. */
|
|
89
|
+
export declare function onlyCheckpoints(predicate: (where: CancelCheckpoint) => boolean): void;
|
|
90
|
+
/** Back to production: every seam fires. */
|
|
91
|
+
export declare function clearCheckpointSuppression(): void;
|
|
64
92
|
/** Checkpoints crossed since the last reset. */
|
|
65
93
|
export declare function checkpointsCrossed(): readonly CancelCheckpoint[];
|
|
@@ -27,13 +27,37 @@
|
|
|
27
27
|
* pre-final-gate (run-final-gate) every task is checked off and committed; the
|
|
28
28
|
* whole-repo gate has not started. A resume re-enters the same
|
|
29
29
|
* branch.
|
|
30
|
+
* plan:<child> (child-status, runPlanningChild — the one funnel BOTH
|
|
31
|
+
* /task-auto's planning and /task-plan go through) DURABLE BY
|
|
32
|
+
* DISCARD, and the one seam here that is not durable by writing.
|
|
33
|
+
* Nothing planning produces reaches disk until planAuto's final
|
|
34
|
+
* writeTaskFile, so there is no partial plan to resume and no
|
|
35
|
+
* half-state to repair — stopping abandons the whole plan. That
|
|
36
|
+
* is the point: the alternative measured at 13+ minutes of
|
|
37
|
+
* running after the user asked to stop.
|
|
38
|
+
* research:<w> (research-worker) after persistSection. The four workers run
|
|
39
|
+
* serially by default and each one's section is read back by
|
|
40
|
+
* readCached, so a resume skips every worker already on disk.
|
|
41
|
+
* Costs nothing and repeats nothing — the only seam here that is
|
|
42
|
+
* free in both directions.
|
|
43
|
+
* impl:post-turn (orchestrator) the implementation turn has ENDED and the spec
|
|
44
|
+
* sections are all on disk. The turn's edits are uncommitted, so
|
|
45
|
+
* a resume re-delivers the spec onto the partly-edited tree —
|
|
46
|
+
* identical to the shipped ESC-then-decline-steer ending.
|
|
47
|
+
* gate:post-commit (task-gates) the task is checked off and its snapshot is in
|
|
48
|
+
* HEAD. Only the enforce pass is skipped, and enforce is
|
|
49
|
+
* re-runnable.
|
|
50
|
+
* gate:pre-resolution (task-gates) at the top of the verify-resolution loop, before
|
|
51
|
+
* a round spends a lint fix, a research child or a whole
|
|
52
|
+
* implementation re-run. The task file is demoted there: it
|
|
53
|
+
* still reads `completed` from spec handoff, and the work is
|
|
54
|
+
* neither verified nor committed.
|
|
30
55
|
*
|
|
31
|
-
* DELIBERATELY NOT
|
|
32
|
-
* - mid implementation turn
|
|
56
|
+
* DELIBERATELY NOT a checkpoint — stopping here is not safe:
|
|
57
|
+
* - mid implementation turn. The turn is a host-session turn, not a child, so
|
|
58
|
+
* stopping it means abandoning a half-applied edit set with no commit behind
|
|
59
|
+
* it. `impl:post-turn` waits for the turn to end instead. The user's ESC
|
|
33
60
|
* (declined steer) path already covers "stop now, I accept a partial tree".
|
|
34
|
-
* - between the implementation turn and the gates, or inside the gates: the
|
|
35
|
-
* work is written but unverified and uncommitted; the gates are what make it
|
|
36
|
-
* durable. Cancel is observed on the far side, at loop-top.
|
|
37
61
|
*/
|
|
38
62
|
let requested = false;
|
|
39
63
|
/** Checkpoints actually reached since the last reset, in order. Instrumentation
|
|
@@ -71,14 +95,42 @@ export function resetCheckpointTrail() {
|
|
|
71
95
|
*/
|
|
72
96
|
export function cancelCheckpoint(where) {
|
|
73
97
|
crossed.push(where);
|
|
74
|
-
|
|
75
|
-
// so the two arms differ in exactly one thing. Nothing in src/ ever sets it;
|
|
76
|
-
// the only writer in the tree is cancel-points.test.ts, which uses it to pin
|
|
77
|
-
// that the extra checkpoints — and only they — are what the flag gates.
|
|
78
|
-
if (process.env.CANCEL_AB_ARM === 'baseline' && where !== 'loop-top')
|
|
98
|
+
if (isSuppressed(where))
|
|
79
99
|
return false;
|
|
80
100
|
return requested;
|
|
81
101
|
}
|
|
102
|
+
// ─── Suppression (negative controls) ─────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Which seams may fire. `null` is production: all of them.
|
|
105
|
+
*
|
|
106
|
+
* This is what makes the seam matrix falsifiable, and a predicate rather than a
|
|
107
|
+
* name list because three of the seams are open-ended (`phase:`, `plan:`,
|
|
108
|
+
* `research:`) and a list could not name them all.
|
|
109
|
+
*
|
|
110
|
+
* The matrix asks one seam at a time — `onlyCheckpoints(w => w === 'pre-task')`
|
|
111
|
+
* for the assertion, `onlyCheckpoints(() => false)` for its control — so the two
|
|
112
|
+
* runs differ by exactly that seam and nothing else. Without the control, "the
|
|
113
|
+
* run stopped here" also passes against a loop that stops everywhere, or one that
|
|
114
|
+
* stopped for an unrelated reason: several seams see the same raised flag, and
|
|
115
|
+
* whichever comes first is the one that stops the run.
|
|
116
|
+
*/
|
|
117
|
+
let allowed = null;
|
|
118
|
+
function isSuppressed(where) {
|
|
119
|
+
// CANCEL_AB_ARM=baseline collapses the set back to loop-top alone — the
|
|
120
|
+
// original two-arm control, kept because it is the coarse "does the extra
|
|
121
|
+
// checkpoint set do anything at all" question, which no per-seam control asks.
|
|
122
|
+
if (process.env.CANCEL_AB_ARM === 'baseline' && where !== 'loop-top')
|
|
123
|
+
return true;
|
|
124
|
+
return allowed !== null && !allowed(where);
|
|
125
|
+
}
|
|
126
|
+
/** Let only the seams this answers true for fire. Tests only. */
|
|
127
|
+
export function onlyCheckpoints(predicate) {
|
|
128
|
+
allowed = predicate;
|
|
129
|
+
}
|
|
130
|
+
/** Back to production: every seam fires. */
|
|
131
|
+
export function clearCheckpointSuppression() {
|
|
132
|
+
allowed = null;
|
|
133
|
+
}
|
|
82
134
|
/** Checkpoints crossed since the last reset. */
|
|
83
135
|
export function checkpointsCrossed() {
|
|
84
136
|
return crossed;
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
* gate-wide loader over a child that renders none (`frame: null`, reached when
|
|
20
20
|
* `deps.loader === false` in gate-child), so both must see the same object.
|
|
21
21
|
*/
|
|
22
|
-
import { runPhaseChild } from './child-runner.js';
|
|
22
|
+
import { runPhaseChild, USER_CANCELLED } from './child-runner.js';
|
|
23
23
|
import { resolveContextUsage } from './context-usage.js';
|
|
24
24
|
import { startAutoLoader } from './widget.js';
|
|
25
|
+
import { cancelCheckpoint } from './cancel-points.js';
|
|
25
26
|
export class ChildStatus {
|
|
26
27
|
_lastLine;
|
|
27
28
|
_contextUsage;
|
|
@@ -95,6 +96,17 @@ export class ChildStatus {
|
|
|
95
96
|
*/
|
|
96
97
|
export async function runPlanningChild(opts) {
|
|
97
98
|
const { ctx, status, phaseDeps, name, tools, prompt, loader } = opts;
|
|
99
|
+
// SAFE CHECKPOINT (planning): the one funnel every planning child goes through
|
|
100
|
+
// — orient, clarify, decompose, each coverage round, each grounded extraction,
|
|
101
|
+
// and /task-plan's children too. Safe by DISCARD rather than by writing:
|
|
102
|
+
// planning puts nothing on disk before planAuto's closing writeTaskFile, so
|
|
103
|
+
// there is no partial plan to resume and none to repair. Without it the flag
|
|
104
|
+
// was read only after planning RETURNED — measured at 13+ minutes of planning
|
|
105
|
+
// still running after the user asked to stop.
|
|
106
|
+
if (cancelCheckpoint(`plan:${name}`)) {
|
|
107
|
+
phaseDeps.logDebug?.(`cancel: abandoning the plan before ${name}`);
|
|
108
|
+
throw new Error(USER_CANCELLED);
|
|
109
|
+
}
|
|
98
110
|
const startedAt = Date.now();
|
|
99
111
|
return status.track(ctx, () => ({
|
|
100
112
|
...(loader.command === undefined ? {} : { command: loader.command }),
|
|
@@ -154,17 +154,29 @@ export function splitBulletSpans(context) {
|
|
|
154
154
|
export function splitBullets(context) {
|
|
155
155
|
return splitBulletSpans(context).map(s => s.text);
|
|
156
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* A version block is headed by its REGISTRY, so the heading is open-ended
|
|
159
|
+
* (`crates.io`, `hackage`). Anything else is one: the widened pattern also reads
|
|
160
|
+
* headings out of a retrieved README or page BODY, and calling those `docs` would
|
|
161
|
+
* let a package's own text forge the block that sources a semantics claim.
|
|
162
|
+
*/
|
|
163
|
+
function blockKind(heading) {
|
|
164
|
+
if (heading === 'freshness-check')
|
|
165
|
+
return 'freshness-skipped';
|
|
166
|
+
if (heading === 'docs' || heading === 'url' || heading === 'service')
|
|
167
|
+
return heading;
|
|
168
|
+
return 'npm';
|
|
169
|
+
}
|
|
157
170
|
/** Parse the `### npm:` / `### docs:` / `### url:` / `### service:` blocks out of an EXTERNAL CONTEXT header. */
|
|
158
171
|
export function parseContextBlocks(externalContext) {
|
|
159
172
|
const out = [];
|
|
160
|
-
|
|
173
|
+
// A version block is headed by its REGISTRY, so `npm` is one of several
|
|
174
|
+
// (`crates.io`, `hackage`). Matching only the fixed words made every non-npm
|
|
175
|
+
// block invisible to the parser.
|
|
176
|
+
const re = /^###\s+([A-Za-z][\w.-]*)\s*:?\s*(.*)$/gim;
|
|
161
177
|
let m;
|
|
162
178
|
while ((m = re.exec(externalContext)) !== null) {
|
|
163
|
-
|
|
164
|
-
out.push({
|
|
165
|
-
kind: kind === 'freshness-check' ? 'freshness-skipped' : kind,
|
|
166
|
-
subject: m[2].trim()
|
|
167
|
-
});
|
|
179
|
+
out.push({ kind: blockKind(m[1].toLowerCase()), subject: m[2].trim() });
|
|
168
180
|
}
|
|
169
181
|
return out;
|
|
170
182
|
}
|
|
@@ -36,6 +36,8 @@ type GatherDeps = Pick<PhaseDeps, 'cwd' | 'signal' | 'recordSubStep' | 'docsRaw'
|
|
|
36
36
|
export interface ExternalTargetResult {
|
|
37
37
|
/** Emitted as an `### npm:` block ahead of every body. Absent for url targets. */
|
|
38
38
|
npmVersion?: NpmVersionInfo | null;
|
|
39
|
+
/** Which registry the version block came from; npm when absent. */
|
|
40
|
+
registryLabel?: string;
|
|
39
41
|
/**
|
|
40
42
|
* The `### docs:`/`### url:` body. `undefined` means "this target contributes no
|
|
41
43
|
* body block" — and the two call paths draw that line differently ON PURPOSE:
|
|
@@ -68,6 +70,11 @@ export interface ExternalContextLookups {
|
|
|
68
70
|
* question, in front of a waiting user, while research is uncapped and trails its
|
|
69
71
|
* sub-step.
|
|
70
72
|
*/
|
|
73
|
+
/** A live version answer and the registry it came from, for the block heading. */
|
|
74
|
+
export interface VersionBlock {
|
|
75
|
+
info: NpmVersionInfo;
|
|
76
|
+
label: string;
|
|
77
|
+
}
|
|
71
78
|
export interface ExternalContextPolicy {
|
|
72
79
|
/**
|
|
73
80
|
* Max combined docs+url targets fanned out, packages first. Omit for uncapped
|
|
@@ -83,7 +90,7 @@ export interface ExternalContextPolicy {
|
|
|
83
90
|
* version at all, and a "which version?" question falls back to whatever the
|
|
84
91
|
* model remembers — which is how a dependency gets pinned to a stale major.
|
|
85
92
|
*/
|
|
86
|
-
versionLookup?: (pkg: string) => Promise<
|
|
93
|
+
versionLookup?: (pkg: string) => Promise<VersionBlock | null>;
|
|
87
94
|
/** Sub-step label recorded via `deps.recordSubStep`. Omit to record nothing. */
|
|
88
95
|
subStepLabel?: string;
|
|
89
96
|
/**
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* Run against a mixed source, the emitted headings come out in exactly that order:
|
|
23
23
|
* `### npm:` then `### docs:` then `### url:` then `### service:`.
|
|
24
24
|
*/
|
|
25
|
+
import { chooseEcosystem, defaultEcosystemIo } from '../workers/docs-ecosystems.js';
|
|
25
26
|
import { docsRaw } from '../workers/docs-core.js';
|
|
26
27
|
import { fetchRaw } from '../workers/fetch-core.js';
|
|
27
28
|
import { formatNpmVersionSection, npmVersionLookup } from '../workers/npm-version.js';
|
|
@@ -61,6 +62,14 @@ export async function buildExternalContext(source, deps, lookups, policy = {}) {
|
|
|
61
62
|
}).catch(() => null))),
|
|
62
63
|
Promise.all(versionLookup ? extraVersionPkgs.map(pkg => versionLookup(pkg).catch(() => null)) : [])
|
|
63
64
|
]);
|
|
65
|
+
// A docs target that came back WITHOUT a version — a refused ecosystem, a dead
|
|
66
|
+
// registry — was dropped from `extraVersionPkgs` on the assumption docs would
|
|
67
|
+
// supply it. Ask for it now, or its block is silently lost.
|
|
68
|
+
const missedVersionPkgs = versionLookup ?
|
|
69
|
+
enrichTargets.versionPackages.filter(p => docsTargets.has(p)
|
|
70
|
+
&& !targets.some((t, i) => t.name === p && targetResults[i]?.npmVersion))
|
|
71
|
+
: [];
|
|
72
|
+
const missedVersionResults = await Promise.all(missedVersionPkgs.map(pkg => versionLookup(pkg).catch(() => null)));
|
|
64
73
|
const sections = [];
|
|
65
74
|
// npm version blocks lead the section so the model anchors on live version
|
|
66
75
|
// data before reading any docs body. The docs-fetched packages carry their
|
|
@@ -69,11 +78,11 @@ export async function buildExternalContext(source, deps, lookups, policy = {}) {
|
|
|
69
78
|
// named dep.
|
|
70
79
|
for (const r of targetResults) {
|
|
71
80
|
if (r?.npmVersion)
|
|
72
|
-
sections.push(formatNpmVersionSection(r.npmVersion));
|
|
81
|
+
sections.push(formatNpmVersionSection(r.npmVersion, r.registryLabel));
|
|
73
82
|
}
|
|
74
|
-
for (const v of extraVersionResults) {
|
|
83
|
+
for (const v of [...extraVersionResults, ...missedVersionResults]) {
|
|
75
84
|
if (v)
|
|
76
|
-
sections.push(formatNpmVersionSection(v));
|
|
85
|
+
sections.push(formatNpmVersionSection(v.info, v.label));
|
|
77
86
|
}
|
|
78
87
|
for (let i = 0; i < targets.length; i++) {
|
|
79
88
|
const body = targetResults[i]?.body;
|
|
@@ -118,6 +127,44 @@ export async function gatherExternalContext(refined, deps) {
|
|
|
118
127
|
const fetchRawFn = deps.fetchRaw ?? fetchRaw;
|
|
119
128
|
const npmVersionFn = deps.npmVersionLookup ?? npmVersionLookup;
|
|
120
129
|
const docsQuery = refined.split('\n').find(l => l.trim()) ?? refined;
|
|
130
|
+
// Which registry to ask, decided PER PACKAGE.
|
|
131
|
+
//
|
|
132
|
+
// A cargo-only project's dependency names are crate names, and asking npm
|
|
133
|
+
// about them returns a real but unrelated package's versions — the exact
|
|
134
|
+
// confusion the docs tool refuses. But deciding once for the whole repo makes
|
|
135
|
+
// every polyglot project ambiguous, and then NO version block is emitted at
|
|
136
|
+
// all: worse than the old always-npm, because the prompts tell the model to
|
|
137
|
+
// quote a block that will never be there. Each name carries its own evidence,
|
|
138
|
+
// so each name gets its own answer.
|
|
139
|
+
const io = defaultEcosystemIo(deps.signal ? { signal: deps.signal } : {});
|
|
140
|
+
const askNpm = async (pkg) => {
|
|
141
|
+
const info = await npmVersionFn(pkg, { signal: deps.signal });
|
|
142
|
+
return info ? { info, label: 'npm' } : null;
|
|
143
|
+
};
|
|
144
|
+
const versionLookup = async (pkg) => {
|
|
145
|
+
const choice = chooseEcosystem({
|
|
146
|
+
cwd: deps.cwd,
|
|
147
|
+
declaresPackage: p => p.declaredRange(p.parentPackage(pkg), deps.cwd) !== null,
|
|
148
|
+
resolvesLocally: p => {
|
|
149
|
+
try {
|
|
150
|
+
p.resolve(pkg, deps.cwd, io);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
// No manifest at all keeps npm, which is what a bare directory has always
|
|
159
|
+
// done. A name genuinely ambiguous between two registries gets no block —
|
|
160
|
+
// a version from the wrong registry is worse than none.
|
|
161
|
+
if (!choice.ok)
|
|
162
|
+
return choice.reason === 'none' ? askNpm(pkg) : null;
|
|
163
|
+
if (choice.profile.id === 'npm')
|
|
164
|
+
return askNpm(pkg);
|
|
165
|
+
const info = await choice.profile.latest(pkg, io);
|
|
166
|
+
return info ? { info, label: choice.profile.registryLabel } : null;
|
|
167
|
+
};
|
|
121
168
|
return buildExternalContext(refined, deps, {
|
|
122
169
|
docs: async (pkg) => {
|
|
123
170
|
const r = await docsRawFn({
|
|
@@ -128,6 +175,7 @@ export async function gatherExternalContext(refined, deps) {
|
|
|
128
175
|
});
|
|
129
176
|
return {
|
|
130
177
|
npmVersion: r.npmVersion,
|
|
178
|
+
...(r.registryLabel ? { registryLabel: r.registryLabel } : {}),
|
|
131
179
|
body: r.kind === 'ok' && r.chunks.length > 0 ?
|
|
132
180
|
r.chunks
|
|
133
181
|
.map(c => c.content)
|
|
@@ -142,7 +190,7 @@ export async function gatherExternalContext(refined, deps) {
|
|
|
142
190
|
},
|
|
143
191
|
search: deps.searchFn
|
|
144
192
|
}, {
|
|
145
|
-
versionLookup
|
|
193
|
+
versionLookup,
|
|
146
194
|
subStepLabel: 'enrichment',
|
|
147
195
|
earlyReturnOnNoTargets: true
|
|
148
196
|
});
|
|
@@ -37,10 +37,10 @@ import { parseVerifyBlock } from './spec-validation.js';
|
|
|
37
37
|
import { findDeliveryPhantoms, formatApiOverrideBanner } from '../workers/phantom-imports.js';
|
|
38
38
|
import { titleForDisplay } from './parsers.js';
|
|
39
39
|
import { USER_CANCELLED } from './child-runner.js';
|
|
40
|
-
import { cancelCheckpoint } from './cancel-points.js';
|
|
40
|
+
import { cancelCheckpoint, requestCancel } from './cancel-points.js';
|
|
41
41
|
import { holdImplementation, liveModelControl } from './implementation-hold.js';
|
|
42
42
|
import { rearmCancelListener } from './cancel-input.js';
|
|
43
|
-
import { takeHeldInput } from './mid-run-input.js';
|
|
43
|
+
import { takeHeldInput, isRunActive } from './mid-run-input.js';
|
|
44
44
|
import { withRun, announceTerminal } from './run-bracket.js';
|
|
45
45
|
import { RUN_END_POLICY, runSucceeded } from './run-end.js';
|
|
46
46
|
import { formatTimings } from './timings.js';
|
|
@@ -334,6 +334,31 @@ export class TaskRunner {
|
|
|
334
334
|
await setTaskSection(cwd, id, 'phase timings', formatTimings(this._timings));
|
|
335
335
|
await setTaskSection(cwd, id, 'handoff', `handoff_at: ${new Date().toISOString()}`);
|
|
336
336
|
await this._deliverSpec(ctx);
|
|
337
|
+
// SAFE CHECKPOINT (post implementation turn): every phase section is
|
|
338
|
+
// on disk and the turn has ENDED. Its edits are uncommitted, so a
|
|
339
|
+
// resume re-delivers the spec onto the partly-edited tree — the same
|
|
340
|
+
// ending the ESC-then-decline-steer path already produces. Front
|
|
341
|
+
// matter reads `phase: done`, and PHASE_INDEX.done is past every row,
|
|
342
|
+
// so the resumed run restores all five sections and falls straight
|
|
343
|
+
// through to _deliverSpec.
|
|
344
|
+
//
|
|
345
|
+
// This is also what makes /task-cancel work here at all: the turn
|
|
346
|
+
// runs in the host session, not as a child, so aborting this runner's
|
|
347
|
+
// signal never reached it. The command raises the cooperative flag
|
|
348
|
+
// and this is where the flag is read.
|
|
349
|
+
//
|
|
350
|
+
// AWAITED ONLY. On the fire-and-forget /task path _deliverSpec returns
|
|
351
|
+
// as soon as the spec is sent, so the turn is STARTING, not finished,
|
|
352
|
+
// and firing here would write `cancelled` over a task the agent then
|
|
353
|
+
// goes on to implement in full — reported and recorded as stopped
|
|
354
|
+
// while it runs, and left in a resumable state that re-delivers the
|
|
355
|
+
// same spec on top of the finished work. Nothing is lost by staying
|
|
356
|
+
// out: that path has no gates and no loop to stop, the run just ends,
|
|
357
|
+
// and ESC is what interrupts the turn itself.
|
|
358
|
+
if (this._implAwaited && cancelCheckpoint('impl:post-turn')) {
|
|
359
|
+
this._deps.logDebug?.('cancel: stopping after the implementation turn');
|
|
360
|
+
throw new Error(USER_CANCELLED);
|
|
361
|
+
}
|
|
337
362
|
return { kind: 'completed' };
|
|
338
363
|
}
|
|
339
364
|
catch (err) {
|
|
@@ -795,12 +820,29 @@ async function handleTaskResume(args, ctx) {
|
|
|
795
820
|
}
|
|
796
821
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
797
822
|
async function handleTaskCancel(_args, ctx) {
|
|
798
|
-
|
|
823
|
+
// `activeTask` covers the spec phases and the implementation turn — it is set
|
|
824
|
+
// in TaskRunner._run and cleared in that run's `finally`. The GATES run after
|
|
825
|
+
// that, so for the whole verify/autofix/enforce stretch there is no runner to
|
|
826
|
+
// abort and the honest answer is not "No task is running."
|
|
827
|
+
const runner = activeTask;
|
|
828
|
+
if (!runner && !isRunActive()) {
|
|
799
829
|
notifyBoth(ctx, 'No task is running.', 'info');
|
|
800
830
|
return;
|
|
801
831
|
}
|
|
802
|
-
|
|
803
|
-
|
|
832
|
+
// The cooperative flag is what gives the command one meaning wherever it is
|
|
833
|
+
// typed: stop at the next point the run can be resumed from. Without it a
|
|
834
|
+
// cancel during the gates, or during the implementation turn, had nothing to
|
|
835
|
+
// observe it. The run bracket scopes the flag to the run that raised it.
|
|
836
|
+
requestCancel();
|
|
837
|
+
if (!runner) {
|
|
838
|
+
notifyBoth(ctx, 'Stopping at the next safe checkpoint…', 'warning');
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
// Abort as well: during the spec phases this kills the running child outright
|
|
842
|
+
// instead of waiting out its remaining minutes, and the phase already on disk
|
|
843
|
+
// is what a resume starts from.
|
|
844
|
+
runner.cancel();
|
|
845
|
+
notifyBoth(ctx, `Cancelling ${runner.taskId}…`, 'warning');
|
|
804
846
|
}
|
|
805
847
|
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
806
848
|
export function registerTask(pi) {
|
package/dist/task/phases.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { docsFocused } from '../workers/docs-core.js';
|
|
7
|
+
import { detectEcosystems } from '../workers/docs-ecosystems.js';
|
|
7
8
|
import { fetchFocused } from '../workers/fetch-core.js';
|
|
8
9
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
9
10
|
import { findPhantomImports, formatApiCorrections, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
|
|
@@ -550,7 +551,7 @@ export async function phaseResearch(deps, refined) {
|
|
|
550
551
|
// queries the FILES worker just answered.
|
|
551
552
|
prompt: prior => orientation.block
|
|
552
553
|
+ promptHeader
|
|
553
|
-
+ RESEARCH_APIS_PROMPT(refined, prior.find(s => s.name === 'FILES')?.text || undefined)
|
|
554
|
+
+ RESEARCH_APIS_PROMPT(refined, prior.find(s => s.name === 'FILES')?.text || undefined, detectEcosystems(deps.cwd))
|
|
554
555
|
+ (searchConfigured() ? RESEARCH_SEARCH_HINT : '')
|
|
555
556
|
// Empty unless PI_TASK_PROJECT_DOCS_BUDGET is set. The tool-side
|
|
556
557
|
// half lives in pi-worker-docs.ts; a budget enforced without being
|