@dpeek/codeless 0.1.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 +242 -0
- package/bin/codeless +10 -0
- package/extension/planner.js +336 -0
- package/package.json +47 -0
- package/spec/workflow.md +200 -0
- package/src/cli.ts +924 -0
- package/src/metrics.ts +155 -0
- package/src/pi.ts +174 -0
- package/src/project.ts +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Codeless
|
|
2
|
+
|
|
3
|
+
Codeless is an attended workflow for parallel capability development. Each stream has one
|
|
4
|
+
branch/worktree, one planner, an implementer shell, and at most one approved
|
|
5
|
+
change. Work within a stream is sequential: propose, approve, implement, review,
|
|
6
|
+
commit, land.
|
|
7
|
+
|
|
8
|
+
This project owns the executable, Pi extension, Git/Herdr mechanics, and tests.
|
|
9
|
+
Project instructions and prompts belong to the consuming project. The package
|
|
10
|
+
has its own dependencies, TypeScript configuration, and checks; it imports no
|
|
11
|
+
consumer application code. Its tests use independent temporary repositories.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Codeless requires Bun, Git, Herdr, and Pi. The scoped package is configured for
|
|
16
|
+
public npm access. Once published, install it with either package manager:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
bun add --global @dpeek/codeless
|
|
20
|
+
# or
|
|
21
|
+
npm install --global @dpeek/codeless
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The package installs the `codeless` executable:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
codeless --help
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The implemented workflow contract is in [spec/workflow.md](./spec/workflow.md).
|
|
31
|
+
Proposed and missing behavior is kept in [todo/workflow.md](./todo/workflow.md).
|
|
32
|
+
|
|
33
|
+
## Project configuration
|
|
34
|
+
|
|
35
|
+
Run `codeless` inside the intended Git checkout. The runner discovers the project
|
|
36
|
+
from the current working directory, including when invoked from a subdirectory
|
|
37
|
+
or through a linked executable. Its installation location does not select the
|
|
38
|
+
project. Help also works outside a repository.
|
|
39
|
+
|
|
40
|
+
Each project commits `.codeless/config.json`:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"integrationBranch": "main",
|
|
45
|
+
"directions": "todo",
|
|
46
|
+
"prompts": ".codeless/prompts",
|
|
47
|
+
"install": ["bun", "install", "--frozen-lockfile"],
|
|
48
|
+
"check": ["bun", "run", "check"],
|
|
49
|
+
"planner": {
|
|
50
|
+
"provider": "openai-codex",
|
|
51
|
+
"model": "gpt-5.6-sol",
|
|
52
|
+
"thinking": "high"
|
|
53
|
+
},
|
|
54
|
+
"implementer": {
|
|
55
|
+
"provider": "openai-codex",
|
|
56
|
+
"model": "gpt-5.6-terra",
|
|
57
|
+
"thinking": "medium"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
All fields are required. Directory paths are relative to the checkout and must
|
|
63
|
+
stay within it. Commands are nonempty argument arrays, executed in the target
|
|
64
|
+
worktree without shell interpolation. Each role requires an exact Pi provider
|
|
65
|
+
and model ID plus one of `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or
|
|
66
|
+
`max`. Configuration is trusted project code.
|
|
67
|
+
|
|
68
|
+
Before a role session starts, Codeless launches a short-lived, promptless Pi RPC
|
|
69
|
+
process in the target worktree. It requires the exact provider/model in Pi's
|
|
70
|
+
available-model response, selects it, requires the configured level in that
|
|
71
|
+
model's available-thinking response, and verifies the resulting state. Failure
|
|
72
|
+
identifies the role and requested selection; Codeless does not start agent work
|
|
73
|
+
with Pi's fallback model or a clamped thinking level. Successful launch output
|
|
74
|
+
shows the effective role selection, and the Pi process receives the same exact
|
|
75
|
+
model and level.
|
|
76
|
+
|
|
77
|
+
The invoking checkout selects the integration branch. Keep that setting aligned
|
|
78
|
+
across participating worktrees. The runner discovers its checkout from Git's
|
|
79
|
+
worktree list. Directions, prompts, and install commands come from the target
|
|
80
|
+
worktree; landing reads the check command after rebasing. Creating a stream
|
|
81
|
+
first verifies its direction and prompts in the integration checkout.
|
|
82
|
+
|
|
83
|
+
## Project prompt contract
|
|
84
|
+
|
|
85
|
+
The configured prompt directory provides `change.md`, `implement.md`,
|
|
86
|
+
`review.md`, and `commit.md`, using Pi prompt-template syntax. The runner supplies:
|
|
87
|
+
|
|
88
|
+
- `/change <stream-directory> <direction-file>` to the planner;
|
|
89
|
+
- `/implement <numbered-change-file>` to the implementer;
|
|
90
|
+
- `/review <numbered-change-file>` to the planner after successful dispatch.
|
|
91
|
+
|
|
92
|
+
Path arguments are JSON-quoted. Each direction is `<directions>/<slug>.md`.
|
|
93
|
+
The project owns prompt contents, approval rules, review criteria, and the
|
|
94
|
+
transition from review to commit. Required prompts and directions must exist;
|
|
95
|
+
the runner does not generate or copy project instructions.
|
|
96
|
+
|
|
97
|
+
The package-owned planner extension activates every planner session. Before its
|
|
98
|
+
first project prompt, it requires the exact `<slug>-planner` Pi name, establishes
|
|
99
|
+
and verifies the `<slug>_planner` Herdr identity, and confirms `approve_stream_change`,
|
|
100
|
+
`dispatch_stream_implementer`, and `next_stream_change` are active. Missing or
|
|
101
|
+
incompatible activation, identity mismatch, or inactive tools stops before
|
|
102
|
+
`/change`; global Pi extension installation is unnecessary.
|
|
103
|
+
The approval tool has no arguments. Its extension derives the active
|
|
104
|
+
`<slug>-planner` Pi session and passes it to the backing CLI, which requires it
|
|
105
|
+
to match the worktree and branch. The CLI validates the clean current-integration
|
|
106
|
+
baseline and proposal, exclusively creates the next monotonic `changes/NNN.md`,
|
|
107
|
+
and records one hash-backed journal approval. Exact retries reconcile a missing
|
|
108
|
+
file or journal step; conflicting state stops. Approval returns the numbered path
|
|
109
|
+
but does not dispatch it. Dispatch uses the package executable, inherits the
|
|
110
|
+
planner's worktree, and queues review only on success. Pi loads the extension
|
|
111
|
+
explicitly at planner launch.
|
|
112
|
+
|
|
113
|
+
After landing and recording the full landed commit hash in `planner.md`, the
|
|
114
|
+
project's commit prompt calls `next_stream_change` with the completed
|
|
115
|
+
`changePath` and `landedCommit`. The tool requests `/streams-next`, an extension
|
|
116
|
+
command that waits for the current turn to settle, prepares the next loop, and
|
|
117
|
+
replaces the Pi session in the same pane. It preserves the planner name, applies
|
|
118
|
+
the planner selection read and validated after the stream fast-forwards to the
|
|
119
|
+
captured integration commit, then activates and verifies the replacement before
|
|
120
|
+
sending `/change` after resources reload. The previous conversation is not copied; the
|
|
121
|
+
journal and project files carry context.
|
|
122
|
+
|
|
123
|
+
This uses Pi's `newSession({ setup, withSession })` command API, verified with
|
|
124
|
+
Pi 0.84.3. Only the replacement context activates the selection and sends the
|
|
125
|
+
new prompt. Configuration changes take effect at the next role-session boundary,
|
|
126
|
+
not during an active planner or implementer. Review and remediation therefore
|
|
127
|
+
continue with their existing session's selection. Duplicate requests
|
|
128
|
+
while a handoff is pending are rejected. Ordinary session startup, waiting for
|
|
129
|
+
approval, failed validation, and cancelled replacement do not schedule another
|
|
130
|
+
loop. The project prompt still owns approval and stopping when no work remains.
|
|
131
|
+
|
|
132
|
+
## Shared local state
|
|
133
|
+
|
|
134
|
+
Worktrees, proposals, numbered changes, and journals live in one shared local
|
|
135
|
+
workspace. By default it is `.codeless/state/` in the primary checkout. That
|
|
136
|
+
directory is Git-ignored while `.codeless/config.json`, prompts, and guidance
|
|
137
|
+
remain tracked. Every linked worktree resolves the same primary-checkout state.
|
|
138
|
+
|
|
139
|
+
To put the state elsewhere, configure an absolute path once from any checkout:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
git config --local codeless.workspaceRoot /absolute/path/to/workspace
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
This local Git setting is shared by all worktrees. Keep the path stable while
|
|
146
|
+
planners or worktree shells are running.
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
<primary-checkout>/.codeless/state/
|
|
150
|
+
stream/<slug>/
|
|
151
|
+
planner.md # decisions and outcomes
|
|
152
|
+
change.md # editable current proposal
|
|
153
|
+
changes/NNN.md # approved proposals
|
|
154
|
+
worktree/<slug>/ # stream/<slug> branch
|
|
155
|
+
worktree/main/ # example integration checkout location
|
|
156
|
+
.land-lock/ # shared landing owner and recorded integration commit
|
|
157
|
+
metrics/<slug>/NNN.json # first dispatch, landing time, and landed commit
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Normal `git clean -fd` preserves ignored state. `git clean -fdx` removes ignored
|
|
161
|
+
files and can therefore destroy local Codeless journals, metrics, and worktrees;
|
|
162
|
+
inspect its targets before using it.
|
|
163
|
+
|
|
164
|
+
## Commands
|
|
165
|
+
|
|
166
|
+
Run creation, opening, and planner launch from a Herdr-managed shell. Landing
|
|
167
|
+
needs no Herdr session.
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
codeless create <slug>
|
|
171
|
+
codeless open <slug>
|
|
172
|
+
codeless planner <slug>
|
|
173
|
+
codeless approve <planner-session>
|
|
174
|
+
codeless dispatch <numbered-change-file>
|
|
175
|
+
codeless land <slug>
|
|
176
|
+
codeless next <numbered-change-file> <landed-commit>
|
|
177
|
+
codeless metrics
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Slugs are lowercase kebab-case, at most 24 characters. `create` starts
|
|
181
|
+
`stream/<slug>` from the integration branch and creates its local documents;
|
|
182
|
+
it refuses existing streams. `open` resumes a stream. Both run the configured
|
|
183
|
+
install command, then validate and open a planner beside an idle shell. `planner`
|
|
184
|
+
starts Pi in an existing stream's lone shell after the same role preflight. Its
|
|
185
|
+
activation establishes the same identity as creation and reopening.
|
|
186
|
+
Dispatch validates the implementer selection before touching the planner's
|
|
187
|
+
right-hand pane, starts a fresh ephemeral implementer, and waits for completion.
|
|
188
|
+
|
|
189
|
+
The first valid dispatch creates one atomic local JSON metric record for its
|
|
190
|
+
stream and numbered change. Retries preserve its original dispatch time. After a
|
|
191
|
+
successful integration fast-forward, Codeless records the landed time and commit
|
|
192
|
+
on that change's canonical record, creating a landed record without elapsed time
|
|
193
|
+
when dispatch collection was unavailable; collection warnings never alter
|
|
194
|
+
dispatch or landing.
|
|
195
|
+
`codeless metrics` prints every recorded stream and a project total. Its elapsed
|
|
196
|
+
columns are dispatch-to-land wall-clock time; among landed changes, records
|
|
197
|
+
without a measured duration are explicitly unavailable. Dispatched-but-unlanded
|
|
198
|
+
changes remain a separate count. Metrics are prospective local observations, not
|
|
199
|
+
journal state or a recovery mechanism.
|
|
200
|
+
|
|
201
|
+
Landing requires clean stream and integration worktrees and exactly one stream
|
|
202
|
+
commit outside their merge base. It acquires `.land-lock` atomically, recording
|
|
203
|
+
the owner and integration commit. It rebases if necessary, runs the configured
|
|
204
|
+
check command, requires the checked worktree to remain clean, and fast-forwards
|
|
205
|
+
the integration checkout. Success releases the lock. Other branches and
|
|
206
|
+
checkouts are untouched; no push is performed.
|
|
207
|
+
|
|
208
|
+
Another lock owner causes a stop, without queuing or polling. Rebase conflicts
|
|
209
|
+
or failed checks retain ownership. Resolve the existing failure, then rerun
|
|
210
|
+
`land`; it verifies the recorded integration commit has not changed. To abandon
|
|
211
|
+
a landing, inspect the owner/base and Git state before manually removing the
|
|
212
|
+
lock. There is no automatic stale-lock removal or retry.
|
|
213
|
+
|
|
214
|
+
`next` is the session handoff's preparation command. It requires the stream's
|
|
215
|
+
own clean worktree and latest numbered change, a full commit hash present in its
|
|
216
|
+
journal, that commit in the stream's history, and all stream commits included
|
|
217
|
+
in the configured integration branch. A landing lock owned by this stream or
|
|
218
|
+
with an unknown owner stops preparation; another stream's lock does not block
|
|
219
|
+
planning. It fast-forwards to a captured integration commit, verifies project
|
|
220
|
+
prompts, direction, and planner selection, and returns JSON containing
|
|
221
|
+
`sessionName`, `prompt`, and the validated `selection`. It does not modify
|
|
222
|
+
journals, allocate a change, approve implementation, or
|
|
223
|
+
control Pi itself. Preparation can be repeated safely after inspecting a failure.
|
|
224
|
+
|
|
225
|
+
If validation or session replacement fails, the planner stops for operator
|
|
226
|
+
attention. Landing is already complete, and any preparation fast-forward remains
|
|
227
|
+
applied. There is no background retry; restarting the planner recovers from the
|
|
228
|
+
journal and Git state.
|
|
229
|
+
|
|
230
|
+
## Package development
|
|
231
|
+
|
|
232
|
+
From this package directory, run `bun run check` for formatting, lint, types,
|
|
233
|
+
and tests, or `bun run test` for tests alone. The integration tests use real Git
|
|
234
|
+
worktrees and mock Herdr/Pi commands; they never launch actual agents.
|
|
235
|
+
|
|
236
|
+
`npm publish` runs the full check through `prepublishOnly` and publishes
|
|
237
|
+
`@dpeek/codeless` with public access.
|
|
238
|
+
|
|
239
|
+
Keep source, tests, executable, extension, and dependencies inside this project.
|
|
240
|
+
Keep project policies and real prompts outside it. Add automation only for
|
|
241
|
+
concrete needs; this package has no supervisor, project registry, queue, or
|
|
242
|
+
automatic recovery service.
|
package/bin/codeless
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
|
|
3
|
+
const codeless = fileURLToPath(new URL("../bin/codeless", import.meta.url));
|
|
4
|
+
const thinkingLevels = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
5
|
+
const requiredTools = [
|
|
6
|
+
"approve_stream_change",
|
|
7
|
+
"dispatch_stream_implementer",
|
|
8
|
+
"next_stream_change",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function selection(value) {
|
|
12
|
+
if (
|
|
13
|
+
typeof value !== "object" ||
|
|
14
|
+
value === null ||
|
|
15
|
+
Array.isArray(value) ||
|
|
16
|
+
typeof value.provider !== "string" ||
|
|
17
|
+
value.provider.length === 0 ||
|
|
18
|
+
typeof value.model !== "string" ||
|
|
19
|
+
value.model.length === 0 ||
|
|
20
|
+
!thinkingLevels.has(value.thinking)
|
|
21
|
+
) {
|
|
22
|
+
throw new Error("Codeless returned an invalid planner selection");
|
|
23
|
+
}
|
|
24
|
+
return { provider: value.provider, model: value.model, thinking: value.thinking };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function plannerExtension(pi) {
|
|
28
|
+
let registered = false;
|
|
29
|
+
let pending;
|
|
30
|
+
|
|
31
|
+
pi.registerCommand("streams-activate", {
|
|
32
|
+
description: "Verify this planner session before starting its project prompt",
|
|
33
|
+
handler: async (args, ctx) => {
|
|
34
|
+
let prompt;
|
|
35
|
+
try {
|
|
36
|
+
prompt = JSON.parse(args);
|
|
37
|
+
} catch {
|
|
38
|
+
throw new Error("Codeless activation requires one JSON-quoted project prompt");
|
|
39
|
+
}
|
|
40
|
+
if (typeof prompt !== "string" || !prompt.startsWith("/change ")) {
|
|
41
|
+
throw new Error("Codeless activation requires a /change project prompt");
|
|
42
|
+
}
|
|
43
|
+
const activation = ctx.sessionManager
|
|
44
|
+
.getEntries()
|
|
45
|
+
.findLast(
|
|
46
|
+
(entry) =>
|
|
47
|
+
entry.type === "custom" &&
|
|
48
|
+
entry.customType === "streams-role-selection" &&
|
|
49
|
+
entry.data?.role === "planner",
|
|
50
|
+
);
|
|
51
|
+
if (activation) {
|
|
52
|
+
const requested = selection(activation.data.selection);
|
|
53
|
+
const reference = `${requested.provider}/${requested.model}`;
|
|
54
|
+
const model = ctx.modelRegistry.find(requested.provider, requested.model);
|
|
55
|
+
if (!model) throw new Error(`Planner requested ${reference}, but Pi could not find it`);
|
|
56
|
+
if (!(await pi.setModel(model))) {
|
|
57
|
+
throw new Error(`Planner requested ${reference}, but Pi authentication is unavailable`);
|
|
58
|
+
}
|
|
59
|
+
pi.setThinkingLevel(requested.thinking);
|
|
60
|
+
if (
|
|
61
|
+
ctx.model?.provider !== requested.provider ||
|
|
62
|
+
ctx.model.id !== requested.model ||
|
|
63
|
+
pi.getThinkingLevel() !== requested.thinking
|
|
64
|
+
) {
|
|
65
|
+
const effective = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "no model";
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Planner requested ${reference} at thinking level ${requested.thinking}, but Pi applied ${effective} at ${pi.getThinkingLevel()}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const sessionName = pi.getSessionName();
|
|
72
|
+
const match = /^([a-z][a-z0-9-]{0,23})-planner$/.exec(sessionName ?? "");
|
|
73
|
+
if (!match)
|
|
74
|
+
throw new Error("Codeless activation requires an exact <slug>-planner Pi session name");
|
|
75
|
+
const expectedPlanner = `${match[1].replaceAll("-", "_")}_planner`;
|
|
76
|
+
const pane = process.env.HERDR_PANE_ID;
|
|
77
|
+
if (!pane) throw new Error("Codeless activation requires a Herdr-managed planner pane");
|
|
78
|
+
const plannerIdentity = async () => {
|
|
79
|
+
const identity = await pi.exec("herdr", ["agent", "get", pane], { timeout: 30_000 });
|
|
80
|
+
if (identity.code !== 0) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
identity.stderr.trim() ||
|
|
83
|
+
identity.stdout.trim() ||
|
|
84
|
+
"Codeless could not verify Herdr planner identity",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(identity.stdout).result?.agent;
|
|
89
|
+
} catch {
|
|
90
|
+
throw new Error("Herdr returned an invalid planner identity response");
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
let agent = await plannerIdentity();
|
|
94
|
+
if (agent?.name === "pi") {
|
|
95
|
+
const renamed = await pi.exec("herdr", ["agent", "rename", pane, expectedPlanner], {
|
|
96
|
+
timeout: 30_000,
|
|
97
|
+
});
|
|
98
|
+
if (renamed.code !== 0) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
renamed.stderr.trim() ||
|
|
101
|
+
renamed.stdout.trim() ||
|
|
102
|
+
"Codeless could not establish Herdr planner identity",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
agent = await plannerIdentity();
|
|
106
|
+
}
|
|
107
|
+
if (agent?.name !== expectedPlanner) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Codeless planner identity is ${agent?.name ?? "missing"}, expected ${expectedPlanner}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const activeTools = ctx.getSystemPromptOptions().selectedTools ?? [];
|
|
113
|
+
const missing = requiredTools.filter((tool) => !activeTools.includes(tool));
|
|
114
|
+
if (missing.length > 0) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Codeless planner activation is missing required tools: ${missing.join(", ")}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
ctx.ui.notify(`Planner activated: ${sessionName} / ${expectedPlanner}`, "info");
|
|
120
|
+
pi.sendUserMessage(prompt, { expandPromptTemplates: true });
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
pi.registerCommand("streams-next", {
|
|
125
|
+
description: "Finish a requested stream handoff and start a fresh planner session",
|
|
126
|
+
handler: async (_args, ctx) => {
|
|
127
|
+
if (!pending || pending.running) throw new Error("No pending Codeless handoff");
|
|
128
|
+
const request = pending;
|
|
129
|
+
request.running = true;
|
|
130
|
+
try {
|
|
131
|
+
await ctx.waitForIdle();
|
|
132
|
+
const execution = await pi.exec(
|
|
133
|
+
"bun",
|
|
134
|
+
[codeless, "next", request.changePath, request.landedCommit],
|
|
135
|
+
{
|
|
136
|
+
timeout: 30_000,
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
if (execution.code !== 0) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
execution.stderr.trim() ||
|
|
142
|
+
execution.stdout.trim() ||
|
|
143
|
+
"Codeless handoff validation failed",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const { sessionName, prompt, selection: requestedValue } = JSON.parse(execution.stdout);
|
|
147
|
+
if (
|
|
148
|
+
sessionName !== pi.getSessionName() ||
|
|
149
|
+
typeof prompt !== "string" ||
|
|
150
|
+
!prompt.startsWith("/change ")
|
|
151
|
+
) {
|
|
152
|
+
throw new Error("Codeless returned an invalid planner handoff");
|
|
153
|
+
}
|
|
154
|
+
const requested = selection(requestedValue);
|
|
155
|
+
const result = await ctx.newSession({
|
|
156
|
+
setup: async (sm) => {
|
|
157
|
+
sm.appendSessionInfo(sessionName);
|
|
158
|
+
sm.appendCustomEntry("streams-role-selection", {
|
|
159
|
+
role: "planner",
|
|
160
|
+
selection: requested,
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
withSession: async (replacement) => {
|
|
164
|
+
await replacement.sendUserMessage(`/streams-activate ${JSON.stringify(prompt)}`, {
|
|
165
|
+
expandPromptTemplates: true,
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
if (result.cancelled)
|
|
170
|
+
throw new Error("Codeless handoff cancelled; the current session was retained");
|
|
171
|
+
} finally {
|
|
172
|
+
pending = undefined;
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
pi.on("session_start", () => {
|
|
178
|
+
if (registered || !pi.getSessionName()?.endsWith("-planner")) return;
|
|
179
|
+
registered = true;
|
|
180
|
+
|
|
181
|
+
pi.registerTool({
|
|
182
|
+
name: "next_stream_change",
|
|
183
|
+
label: "Start next stream change",
|
|
184
|
+
description:
|
|
185
|
+
"After a successful landing and journal update, start a fresh planner session to propose the next change.",
|
|
186
|
+
promptSnippet: "Start the next planning loop after recording a successful landing",
|
|
187
|
+
promptGuidelines: [
|
|
188
|
+
"Call next_stream_change exactly once after landing the latest approved change and recording its full commit hash in the planner journal.",
|
|
189
|
+
"Finish this turn after requesting the handoff. The replacement planner proposes the next change and waits for approval. Stop on failure or cancellation; do not retry automatically.",
|
|
190
|
+
],
|
|
191
|
+
parameters: {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: {
|
|
194
|
+
changePath: {
|
|
195
|
+
type: "string",
|
|
196
|
+
description: "Absolute path to the completed changes/NNN.md file",
|
|
197
|
+
},
|
|
198
|
+
landedCommit: {
|
|
199
|
+
type: "string",
|
|
200
|
+
description: "Full landed commit hash recorded in planner.md",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
required: ["changePath", "landedCommit"],
|
|
204
|
+
additionalProperties: false,
|
|
205
|
+
},
|
|
206
|
+
async execute(_toolCallId, params) {
|
|
207
|
+
if (pending) throw new Error("A Codeless handoff is already pending");
|
|
208
|
+
pending = {
|
|
209
|
+
changePath: params.changePath.replace(/^@/, ""),
|
|
210
|
+
landedCommit: params.landedCommit,
|
|
211
|
+
running: false,
|
|
212
|
+
};
|
|
213
|
+
try {
|
|
214
|
+
pi.sendUserMessage("/streams-next", {
|
|
215
|
+
deliverAs: "followUp",
|
|
216
|
+
expandPromptTemplates: true,
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
pending = undefined;
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: "text",
|
|
226
|
+
text: "Requested the next planning loop. Finish this turn so the handoff can validate the landing and start a fresh session.",
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
pi.registerTool({
|
|
234
|
+
name: "approve_stream_change",
|
|
235
|
+
label: "Approve stream change",
|
|
236
|
+
description:
|
|
237
|
+
"Promote this planner's current proposal exactly once into an immutable numbered change and record its approval before dispatch.",
|
|
238
|
+
promptSnippet: "Approve the current stream proposal after the operator says go",
|
|
239
|
+
promptGuidelines: [
|
|
240
|
+
"After an operator says go, call approve_stream_change exactly once. It allocates and records the approval but does not dispatch implementation.",
|
|
241
|
+
"Use the returned changePath for one explicit dispatch_stream_implementer call. Keep rejection and revision conversational.",
|
|
242
|
+
],
|
|
243
|
+
parameters: {
|
|
244
|
+
type: "object",
|
|
245
|
+
properties: {},
|
|
246
|
+
additionalProperties: false,
|
|
247
|
+
},
|
|
248
|
+
async execute(_toolCallId, _params, signal) {
|
|
249
|
+
const sessionName = pi.getSessionName();
|
|
250
|
+
const session = /^([a-z][a-z0-9-]{0,23})-planner$/.exec(sessionName ?? "");
|
|
251
|
+
if (!session) throw new Error("Approval requires an active <slug>-planner Pi session");
|
|
252
|
+
const execution = await pi.exec("bun", [codeless, "approve", sessionName], {
|
|
253
|
+
signal,
|
|
254
|
+
timeout: 30_000,
|
|
255
|
+
});
|
|
256
|
+
const output = [execution.stdout.trim(), execution.stderr.trim()]
|
|
257
|
+
.filter(Boolean)
|
|
258
|
+
.join("\n");
|
|
259
|
+
if (execution.code !== 0) {
|
|
260
|
+
throw new Error(output || `codeless approve failed with exit code ${execution.code}`);
|
|
261
|
+
}
|
|
262
|
+
let approval;
|
|
263
|
+
try {
|
|
264
|
+
approval = JSON.parse(execution.stdout);
|
|
265
|
+
} catch {
|
|
266
|
+
throw new Error("Codeless returned an invalid approval");
|
|
267
|
+
}
|
|
268
|
+
if (
|
|
269
|
+
typeof approval !== "object" ||
|
|
270
|
+
approval === null ||
|
|
271
|
+
typeof approval.number !== "string" ||
|
|
272
|
+
!/^\d{3}$/.test(approval.number) ||
|
|
273
|
+
typeof approval.changePath !== "string" ||
|
|
274
|
+
!approval.changePath.startsWith("/") ||
|
|
275
|
+
typeof approval.title !== "string" ||
|
|
276
|
+
!approval.title
|
|
277
|
+
) {
|
|
278
|
+
throw new Error("Codeless returned an invalid approval");
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
content: [
|
|
282
|
+
{
|
|
283
|
+
type: "text",
|
|
284
|
+
text: `Approved ${approval.number}: ${approval.title}. Dispatch requires a separate explicit call.`,
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
details: approval,
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
pi.registerTool({
|
|
293
|
+
name: "dispatch_stream_implementer",
|
|
294
|
+
label: "Dispatch stream implementer",
|
|
295
|
+
description:
|
|
296
|
+
"Create or reuse this stream planner's right-hand Herdr pane, start a fresh Pi implementer in the correct worktree, submit one approved numbered change, wait for it to settle, and queue this planner's review.",
|
|
297
|
+
promptSnippet: "Dispatch one approved stream change to a fresh Pi implementer",
|
|
298
|
+
promptGuidelines: [
|
|
299
|
+
"Use dispatch_stream_implementer exactly once after the operator approves and the numbered stream change file is created; never reproduce its Herdr pane or agent commands with bash.",
|
|
300
|
+
"After successful dispatch, the tool queues the expanded review prompt for the planner; follow that queued instruction instead of ending the workflow after reporting implementation completion.",
|
|
301
|
+
],
|
|
302
|
+
parameters: {
|
|
303
|
+
type: "object",
|
|
304
|
+
properties: {
|
|
305
|
+
changePath: {
|
|
306
|
+
type: "string",
|
|
307
|
+
description: "Absolute path to an approved changes/NNN.md stream change file",
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
required: ["changePath"],
|
|
311
|
+
additionalProperties: false,
|
|
312
|
+
},
|
|
313
|
+
async execute(_toolCallId, params, signal) {
|
|
314
|
+
const changePath = params.changePath.replace(/^@/, "");
|
|
315
|
+
const execution = await pi.exec("bun", [codeless, "dispatch", changePath], {
|
|
316
|
+
signal,
|
|
317
|
+
timeout: 3_700_000,
|
|
318
|
+
});
|
|
319
|
+
const output = [execution.stdout.trim(), execution.stderr.trim()]
|
|
320
|
+
.filter(Boolean)
|
|
321
|
+
.join("\n");
|
|
322
|
+
if (execution.code !== 0) {
|
|
323
|
+
throw new Error(output || `codeless dispatch failed with exit code ${execution.code}`);
|
|
324
|
+
}
|
|
325
|
+
pi.sendUserMessage(`/review ${JSON.stringify(changePath)}`, {
|
|
326
|
+
deliverAs: "steer",
|
|
327
|
+
expandPromptTemplates: true,
|
|
328
|
+
});
|
|
329
|
+
return {
|
|
330
|
+
content: [{ type: "text", text: output || "Implementer settled." }],
|
|
331
|
+
details: { changePath },
|
|
332
|
+
};
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dpeek/codeless",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An attended planner and implementer workflow for parallel capability development",
|
|
5
|
+
"homepage": "https://github.com/dpeek/codeless#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/dpeek/codeless/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "UNLICENSED",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/dpeek/codeless.git"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"codeless": "bin/codeless"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin",
|
|
19
|
+
"extension",
|
|
20
|
+
"src",
|
|
21
|
+
"README.md",
|
|
22
|
+
"spec"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"check": "oxfmt --config .oxfmtrc.json --write . && oxlint --config .oxlintrc.json --fix --type-aware --type-check . && bun run test",
|
|
30
|
+
"prepublishOnly": "bun run check",
|
|
31
|
+
"test": "bun test ./test --dots"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"effect": "4.0.0-rc.112"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/bun": "1.4.0",
|
|
38
|
+
"oxfmt": "0.64.0",
|
|
39
|
+
"oxlint": "1.79.0",
|
|
40
|
+
"oxlint-tsgolint": "7.0.2001",
|
|
41
|
+
"typescript": "^7"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"bun": ">=1.4.0"
|
|
45
|
+
},
|
|
46
|
+
"packageManager": "bun@1.4.0"
|
|
47
|
+
}
|