@tduyng/pi-prewalk 0.0.0-stage → 0.0.2
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/LICENSE +178 -0
- package/README.md +143 -2
- package/package.json +42 -4
- package/src/bash-writes.ts +42 -0
- package/src/config.ts +108 -0
- package/src/index.ts +934 -0
- package/src/logic.ts +384 -0
- package/src/outcomes.ts +127 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
Use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
Copyright 2026 tduyng
|
package/README.md
CHANGED
|
@@ -1,3 +1,144 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tduyng/pi-prewalk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A task goes to the frontier planner, the planner gives the cheap executor a plan to carry out, and the executor hands the work back if it hits a wall. The phase decides every boundary, and no model is asked to.
|
|
4
|
+
|
|
5
|
+
## The problem
|
|
6
|
+
|
|
7
|
+
Stencil measured 1.81B tokens over ~2M tool calls and found edits are about 9% of them. Reads are the rest.
|
|
8
|
+
|
|
9
|
+
A plan document does not help. Whoever executes it opens the same files again.
|
|
10
|
+
|
|
11
|
+
prewalk keeps one context instead. The planner explores and makes the first edit, then the cheap model carries on from the same history. On SWE-bench it reached 92 to 97% of the frontier pass rate for 53 to 61% of its cost.
|
|
12
|
+
|
|
13
|
+
## The router
|
|
14
|
+
|
|
15
|
+
prewalk registers one virtual model, `prewalk/auto`. Select it with `/prewalk` or from `/model`. Pi calls the router before every request, so prewalk never moves the session's model itself and no `setModel` runs in the middle of a task. Every model change in the session file is one prewalk made.
|
|
16
|
+
|
|
17
|
+
Each route returns one request's model, so the planner and the executor can differ per request inside one task.
|
|
18
|
+
|
|
19
|
+
## A task boundary
|
|
20
|
+
|
|
21
|
+
prewalk asks one question, and only when a task is about to start. The boundary is the gap between one settled run and the next prompt typed after it, so a task is routed once and the turns inside it are never touched.
|
|
22
|
+
|
|
23
|
+
Pi's own `reason: "user"` cannot carry that boundary. It fires for a steer and a follow-up too, because both arrive as a user message after the last assistant message. prewalk keeps its own flag, set from the `input` event, which reads `streamingBehavior` as the three cases it actually is: undefined when the session was idle, `steer` when the text joins the turn in flight, and `followUp` when it waits for that turn to settle and then opens a task of its own. The first router call that sees the flag consumes it, so one prompt routes once. The source does not matter: a message from another extension opens a task the same way, and the only text that opens nothing is prewalk's own nudge, recognized by its `[prewalk]` prefix.
|
|
24
|
+
|
|
25
|
+
**A prompt inside the task in flight.** The model stays where it is, the prompt runs as written, and no routing call is made. This is the common case, and it is the one that makes the bill linear in the turn count rather than quadratic.
|
|
26
|
+
|
|
27
|
+
**A question typed while the executor is in play.** The text used to be dropped on the way in, so a mid-task question could never reach the planner. It does now: a steer long enough to be a question moves the task back to the planner, whatever reason pi carries the request under, and the planner stays in play until the next edit hands it to the executor again, and the session says so. No routing call is spent, because the phase answers it. A one-word steer is a redirect rather than a question and stays on the model in play.
|
|
28
|
+
|
|
29
|
+
**A task routed to the planner.** The session moves to the planner before the first token. The planner explores, then must commit a structured plan through the `prewalk_plan` tool before it can edit: the first edit is blocked once with a message naming the tool, and a planner that ignores the block degrades to a plain edit-only swap. The first `edit`, `write`, `multiedit` or `notebookedit` after that swaps the session to the executor, and so does a bash command that writes a file outside the temp dir (a redirect, `sed -i`, `tee`, `patch`, `git apply`, or an inline script that writes), which also demands the plan first. The third tool call after the plan still hands over for a planner that writes in a way the check cannot see. An edit under the temp dir is scratch and swaps nothing. The plan tool returns the handoff brief itself: the goal, the facts with file:line, each step with file, anchor, change and verify command, the out-of-scope list, and the rules of engagement. Both roles read the same bytes, so neither cache is rewritten.
|
|
30
|
+
|
|
31
|
+
**The cheap tier.** A task plans: the planner opens it, and the executor joins from the first edit on and runs the steps of the plan the planner gave, and nothing else. A prompt too short to be a task, under eight characters, is not a task: it holds whatever is in play rather than opening one. `/prewalk off` puts every task on the cheap model with no plan, and `/prewalk run <task>` does it for one task.
|
|
32
|
+
|
|
33
|
+
**A task the executor is stuck on.** Three failed tool results in a row are a wall. One miss does not hand the task back, and any success resets the count. The third failure appends a brief naming the step, and the planner takes the task back on the next request until its next edit. The task buys two upgrades, because each swap re-reads the whole prefix at the new model's rate, so a task cannot ping-pong between the models.
|
|
34
|
+
|
|
35
|
+
**The review.** When the executor finishes the plan, or stalls after its nudge, the planner takes the task back once per plan. It reads the edits already in the transcript, runs the final check, and fixes what is wrong. Work the plan did not cover gets a new plan, which is how a long task runs in phases of up to 20 steps. `"review": false` in `prewalk.json` turns it off.
|
|
36
|
+
|
|
37
|
+
That is the one place a stuck cheap model hands the task over by itself: the wall is the evidence. The failing result carries a brief with the step and the failure count, and the planner decides whether to fix it here or commit the steps that are left. The session badge reads `prewalk replan` while it happens.
|
|
38
|
+
|
|
39
|
+
## The score that is gone
|
|
40
|
+
|
|
41
|
+
The routing call used to ask three questions over the prompt text and gate the boundary on the answer. `difficulty` asked how much a stronger model would improve the answer, scored across four levels from 0 to 3. `tool` asked whether the fix follows mechanically from what the code already says, and `needsTools` whether the work needs tools and live code, or only the repository's contents.
|
|
42
|
+
|
|
43
|
+
It did not separate. The difficulty expectation read 0.548 AUC against how long the turn ran, and the three tail readouts read 0.527 to 0.537, so the distribution shape held no more than its mean. Against the label that matters for routing, which model actually ran the turn, it read 0.755 AUC over 20 turns on the frontier model and 255 on the free one, and the four live prompts that mattered came out the way round: a bug analysis at 0.907 and a variable rename at 1.514.
|
|
44
|
+
|
|
45
|
+
Two cuts were fitted on it and both came out wrong, one above the range and one with a hold band that put every task on the cheap model. The score is deleted, and so is the call that produced it, because a router that cannot tell a bug from a rename is not a router. The phase already answers the same question: a task plans, and the cheap tier runs the steps of the plan it was given.
|
|
46
|
+
|
|
47
|
+
## Grading the routes
|
|
48
|
+
|
|
49
|
+
The route is kept in the session's own state entry, so a session grades itself and the check needs no model. `just routes` reads every session under `sessions/`, splits the tasks by the route each one took, and prints what they cost, how many landed an edit, and how many ran the plan's own `check`.
|
|
50
|
+
|
|
51
|
+
Read it as a survey. The log grows while the run reads it, so the numbers move under you.
|
|
52
|
+
|
|
53
|
+
A wrong swap costs a whole cache re-read at the new model's input rate, so a prompt too short to be a task keeps the model already in play.
|
|
54
|
+
|
|
55
|
+
## Cache and context
|
|
56
|
+
|
|
57
|
+
A prompt cache belongs to one model. The swap is the only thing that has to be paid for, and a task pays it at most once: the executor meets a prefix it has never seen, at the cheap model's input rate.
|
|
58
|
+
|
|
59
|
+
Prompts are never rewritten. There is no wrapper on your message and no message is edited after the fact, so both models read the transcript you wrote and each keeps its cache warm across its own turns. The single message prewalk writes is the plan tool's result, and it is written once at the swap. The `prewalk_plan` tool stays in the active set for as long as prewalk is armed in plan mode, because a toolset change rewrites the system prompt and the tool list, and the claude provider answers that by opening a new session that replays the whole transcript.
|
|
60
|
+
|
|
61
|
+
## Model chains
|
|
62
|
+
|
|
63
|
+
Each role is an ordered list, walked until one model resolves against pi's registry and has credentials. Two ordinary things break a single model: a provider with no API key, and a frontier model that runs out of its usage window part way through the day. The chain absorbs both without a config edit.
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"planner": ["opencode-go/mimo-v2.6-pro", "anthropic/claude-opus-4-6"],
|
|
68
|
+
"executor": ["opencode-z/space-bunny-free", "z-ai/glm-5.3-flash"]
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`/prewalk models` prints both chains and marks each entry `router`, `ready`, or `unavailable` against the registry pi's `/model` reads, with the context window and input price the picker shows, and the mode underneath.
|
|
73
|
+
|
|
74
|
+
## Progress tracking
|
|
75
|
+
|
|
76
|
+
After each edit, the extension matches the edited file path against the plan steps and auto-advances a step pointer. The match walks forward from the current step, so batched edits and missed results catch up instead of freezing the count. A short footer is appended to the edit result: what was done, what comes next, the anchor, and the verify command. No model cooperation is required.
|
|
77
|
+
|
|
78
|
+
The status badge reads `prewalk route` between tasks, `prewalk plan` or `prewalk run` on the first turn of one, `prewalk exec 3/7` while the plan runs, and `prewalk done 7/7` at the end.
|
|
79
|
+
|
|
80
|
+
If the planner commits a plan but the turn ends without an edit, the extension swaps anyway and nudges the executor to start step 1. When the executor settles with steps open, one steer resumes the plan. User input or progress re-arms it, so at most one nudge is spent per stall.
|
|
81
|
+
|
|
82
|
+
## The plan tool
|
|
83
|
+
|
|
84
|
+
`prewalk_plan` is in the active set while prewalk is armed in plan mode, and the executor only meets it through tool search. Its description carries the planning brief, which is why nothing is prepended to your prompt. Its schema:
|
|
85
|
+
|
|
86
|
+
- `summary`: one-line goal
|
|
87
|
+
- `facts`: findings from exploration the executor cannot re-derive, each with file:line
|
|
88
|
+
- `steps`: `{ file, change, anchor?, verify? }` in order, max 20
|
|
89
|
+
- `check`: final verification command
|
|
90
|
+
- `outOfScope`: files the executor must not touch
|
|
91
|
+
|
|
92
|
+
## Commands
|
|
93
|
+
|
|
94
|
+
| Command | Effect |
|
|
95
|
+
| -------------------------------------- | --------------------------------------------------------- |
|
|
96
|
+
| `/prewalk` | status while armed, arm from `prewalk.json` when not |
|
|
97
|
+
| `/prewalk off` | disarm |
|
|
98
|
+
| `/prewalk status` | both chains, the mode, and where the task is |
|
|
99
|
+
| `/prewalk models` | both chains, what resolves, what it costs, and the mode |
|
|
100
|
+
| `/prewalk mode off` | nothing happens unless you ask |
|
|
101
|
+
| `/prewalk mode plan` | every task plans (the default) |
|
|
102
|
+
| `/prewalk planner <model>...` | replace the planner chain, or pick one from a list |
|
|
103
|
+
| `/prewalk executor <model>...` | replace the executor chain, or pick one from a list |
|
|
104
|
+
| `/prewalk plan [task]` | the next task plans, decided by hand |
|
|
105
|
+
| `/prewalk run [task]` | the next task runs on the cheap model, decided by hand |
|
|
106
|
+
| `/prewalk <executor> [task]` | set the executor. A task given here starts on the planner |
|
|
107
|
+
| `/prewalk <planner> <executor> [task]` | set both roles and, with a task, start it |
|
|
108
|
+
|
|
109
|
+
`plan` and `run` spend one routing decision, so they work with nothing else running. Slash commands and skill blocks (`<skill `) are left alone, so skill expansion still works.
|
|
110
|
+
|
|
111
|
+
A role named with no model opens the same dialog pi's own model list is built from, `ctx.ui.select`, since pi keeps its selector private and no extension API opens it. Each line carries what a role choice turns on, `provider/id`, the context window, and the input price per million, sorted by price because the executor is the tier you pay for. A chain of several models still takes arguments, because a dialog picks one.
|
|
112
|
+
|
|
113
|
+
An armed prewalk owns the session model, because every task routes through the router. A switch away from it is a session where nothing routes at all, for the rest of its life, and one press of the model cycle key used to make it silently: the `12:20` model change in the session that reported the router stuck on `mimo-v2.6-flash` left 130 assistant messages on `space-bunny-free` with no routing at all. prewalk now takes the router back on `model_select` and on every session start and branch switch, and says which model it took. `/prewalk off` is the mode that plans nothing and takes no model, so that is how a physical model is kept.
|
|
114
|
+
|
|
115
|
+
Model naming is a bare id (`deepseek-v4-flash`) or provider/id (`google/gemini-3-flash`). Resolution is exact provider/id, then exact bare id, then unique id suffix. A bare id that several providers carry is an error rather than a guess.
|
|
116
|
+
|
|
117
|
+
## Configuration
|
|
118
|
+
|
|
119
|
+
`~/.pi/agent/prewalk.json` holds the machine-wide settings. A project's `.pi/prewalk.json` overrides `planner`, `executor` and `mode` field by field. The commands write to the project file when one exists, otherwise to the agent dir. Edit either for anything they do not cover.
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"planner": ["opencode-go/mimo-v2.6-pro"],
|
|
124
|
+
"executor": ["opencode-go/mimo-v2.6-flash"],
|
|
125
|
+
"mode": "plan",
|
|
126
|
+
"review": true
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`mode` is how a task gets its model. `plan` is the default: every task starts on the frontier model and the executor carries the steps of the plan it was given. `off` plans nothing unless you ask, so every task runs on the executor. `/prewalk run [task]` still picks a single task for the cheap model. A file written before the score was removed, with `mode: "auto"`, reads as `plan`.
|
|
131
|
+
|
|
132
|
+
The router must be selected for any of this to run. An armed session takes it back on session start and on a branch switch, and says which model it took. A session that was never armed stays on whatever model you chose.
|
|
133
|
+
|
|
134
|
+
A string is still accepted for either role, so a file written before the chains existed reads the same way. A missing, unparseable, or wrongly shaped file costs its settings and not the session.
|
|
135
|
+
|
|
136
|
+
Mode is per project file, and `mode: "off"` still lets `/prewalk plan`, `/prewalk run`, and `/prewalk <planner> <executor> [task]` work.
|
|
137
|
+
|
|
138
|
+
## Install
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
pi install npm:@tduyng/pi-prewalk
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Local checkout: `pi install /path/to/dotfiles/pi/agent/extensions/pi-prewalk`
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tduyng/pi-prewalk",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Plan on the frontier model, execute on the cheap one. The session model swaps the moment the first edit lands.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi",
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"pi-coding-agent",
|
|
10
|
+
"cost",
|
|
11
|
+
"model-routing",
|
|
12
|
+
"planning"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://gitlab.com/tduyng/pi-agent/-/tree/main/extensions/pi-prewalk#readme",
|
|
15
|
+
"bugs": "https://gitlab.com/tduyng/pi-agent/-/issues",
|
|
16
|
+
"license": "Apache-2.0",
|
|
17
|
+
"author": "tduyng",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://gitlab.com/tduyng/pi-agent.git",
|
|
21
|
+
"directory": "extensions/pi-prewalk"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"!src/**/*.test.ts",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"pi": {
|
|
37
|
+
"extensions": [
|
|
38
|
+
"./src/index.ts"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"ata-validator": "^1.62.1"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isScratchPath } from './logic.js'
|
|
2
|
+
|
|
3
|
+
const HEREDOC = /(?<!<)<<(?!<)-?\s*(['"]?)(\w+)\1/
|
|
4
|
+
const QUOTED = /'[^']*'|"(?:[^"\\]|\\.)*"/g
|
|
5
|
+
const REDIRECT = />>?\s*([^\s;&|()<>]+)/g
|
|
6
|
+
const WRITER_VERB =
|
|
7
|
+
/(^|[\s;&|(])(sed\b[^|;&\n]*\s(-[a-zA-Z]*i[a-zA-Z]*|--in-place)|perl\b[^|;&\n]*\s-[a-zA-Z]*i|git\s+(apply|am))(?![\w-])|(^|[;&|(]\s*)(tee|patch)(?![\w-])/
|
|
8
|
+
const INTERPRETER = /(^|[;&|(]\s*)(python3?|node|bun|deno|ruby|perl)(?![\w-])/
|
|
9
|
+
const SCRIPT_WRITE =
|
|
10
|
+
/\b(writeFileSync|appendFileSync|write_text|write_bytes|Bun\.write)\b|\bopen\([^)]*,\s*['"][wax]\+?b?['"]/
|
|
11
|
+
|
|
12
|
+
function withoutInertText(command: string): string {
|
|
13
|
+
const kept: string[] = []
|
|
14
|
+
let end: string | null = null
|
|
15
|
+
for (const line of command.split('\n')) {
|
|
16
|
+
if (end !== null) {
|
|
17
|
+
if (line.trim() === end) end = null
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
kept.push(line)
|
|
21
|
+
end = HEREDOC.exec(line)?.[2] ?? null
|
|
22
|
+
}
|
|
23
|
+
return kept.join('\n').replace(QUOTED, "''")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function redirectsToWorkspace(command: string, cwd: string): boolean {
|
|
27
|
+
const text = withoutInertText(command)
|
|
28
|
+
for (const match of text.matchAll(REDIRECT)) {
|
|
29
|
+
const before = text[match.index - 1]
|
|
30
|
+
const target = match[1] ?? ''
|
|
31
|
+
if (before === '-' || before === '=' || target.startsWith('=')) continue
|
|
32
|
+
if (target.startsWith('/dev/') || isScratchPath(target, cwd)) continue
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function bashWritesFiles(command: string, cwd: string): boolean {
|
|
39
|
+
if (redirectsToWorkspace(command, cwd)) return true
|
|
40
|
+
const text = withoutInertText(command)
|
|
41
|
+
return WRITER_VERB.test(text) || (INTERPRETER.test(text) && SCRIPT_WRITE.test(command))
|
|
42
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { getAgentDir } from '@earendil-works/pi-coding-agent'
|
|
2
|
+
import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { Validator, type Infer } from 'ata-validator'
|
|
5
|
+
import { t } from 'ata-validator/t'
|
|
6
|
+
|
|
7
|
+
export const PREWALK_CONFIG_FILE = 'prewalk.json'
|
|
8
|
+
|
|
9
|
+
export function prewalkConfigPath(): string {
|
|
10
|
+
return join(getAgentDir(), PREWALK_CONFIG_FILE)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function prewalkProjectConfigPath(): string {
|
|
14
|
+
return join(process.cwd(), '.pi', PREWALK_CONFIG_FILE)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A project that carries the file owns later writes. Otherwise they stay in the agent dir, which
|
|
18
|
+
* is the file every session had before the project one existed. */
|
|
19
|
+
export function prewalkWritePath(): string {
|
|
20
|
+
const project = prewalkProjectConfigPath()
|
|
21
|
+
return existsSync(project) ? project : prewalkConfigPath()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type PrewalkMode = 'plan' | 'off'
|
|
25
|
+
|
|
26
|
+
export type PrewalkConfig = {
|
|
27
|
+
planner: string[]
|
|
28
|
+
executor: string[]
|
|
29
|
+
mode: PrewalkMode
|
|
30
|
+
review: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `plan` by default, and not because routing is free.
|
|
35
|
+
*
|
|
36
|
+
* The score that used to pick the planner or the cheap tier read 0.548 AUC against how long a
|
|
37
|
+
* turn ran, so it did not separate, and the phase already decides the rest: a task plans and the
|
|
38
|
+
* cheap tier runs the steps of the plan it was given. `off` runs everything on the cheap tier with
|
|
39
|
+
* no plan at all.
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_PREWALK_CONFIG: PrewalkConfig = {
|
|
42
|
+
planner: [],
|
|
43
|
+
executor: [],
|
|
44
|
+
mode: 'plan',
|
|
45
|
+
review: true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const RoleChain = t.union([t.string(), t.array(t.string())])
|
|
49
|
+
|
|
50
|
+
const PrewalkSettings = t.object({
|
|
51
|
+
planner: t.optional(RoleChain),
|
|
52
|
+
executor: t.optional(RoleChain),
|
|
53
|
+
mode: t.optional(t.union([t.literal('plan'), t.literal('off'), t.literal('auto')])),
|
|
54
|
+
review: t.optional(t.boolean())
|
|
55
|
+
})
|
|
56
|
+
const isPrewalkSettings = new Validator(PrewalkSettings).isValidObject
|
|
57
|
+
|
|
58
|
+
/** The shape a `prewalk.json` may hold, before defaults fill it in. */
|
|
59
|
+
export type PrewalkSettings = Infer<typeof PrewalkSettings>
|
|
60
|
+
|
|
61
|
+
function modelChain(value: string | string[] | undefined): string[] {
|
|
62
|
+
if (value === undefined) return []
|
|
63
|
+
const raw = Array.isArray(value) ? value : [value]
|
|
64
|
+
return raw.filter((entry) => entry.trim() !== '')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function prewalkConfigFrom(settings: PrewalkSettings): PrewalkConfig {
|
|
68
|
+
return {
|
|
69
|
+
planner: modelChain(settings.planner),
|
|
70
|
+
executor: modelChain(settings.executor),
|
|
71
|
+
mode: settings.mode === 'off' ? 'off' : 'plan',
|
|
72
|
+
review: settings.review !== false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readSettingsFile(path: string): PrewalkSettings {
|
|
77
|
+
try {
|
|
78
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
|
79
|
+
return isPrewalkSettings(parsed) ? parsed : {}
|
|
80
|
+
} catch {
|
|
81
|
+
return {}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function readPrewalkConfig(path: string = prewalkConfigPath()): PrewalkConfig {
|
|
86
|
+
return prewalkConfigFrom(readSettingsFile(path))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The project file overrides the agent dir one field by field, so a checkout can pin its own
|
|
90
|
+
* models without the machine losing the pair it ships with. */
|
|
91
|
+
export function readEffectivePrewalkConfig(
|
|
92
|
+
agentPath: string = prewalkConfigPath(),
|
|
93
|
+
projectPath: string = prewalkProjectConfigPath()
|
|
94
|
+
): PrewalkConfig {
|
|
95
|
+
return prewalkConfigFrom({ ...readSettingsFile(agentPath), ...readSettingsFile(projectPath) })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function writePrewalkConfig(config: PrewalkConfig, path: string = prewalkWritePath()): boolean {
|
|
99
|
+
try {
|
|
100
|
+
const target = existsSync(path) ? realpathSync(path) : path
|
|
101
|
+
const temporary = `${target}.${process.pid}.tmp`
|
|
102
|
+
writeFileSync(temporary, `${JSON.stringify(config, null, 2)}\n`)
|
|
103
|
+
renameSync(temporary, target)
|
|
104
|
+
return true
|
|
105
|
+
} catch {
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
}
|