@dpeek/codeless 0.1.4 → 0.1.5
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 +17 -14
- package/extension/planner.js +120 -66
- package/package.json +1 -1
- package/spec/workflow.md +33 -20
package/README.md
CHANGED
|
@@ -100,17 +100,20 @@ Review and commit those local prompt edits in the invoking checkout, then bring
|
|
|
100
100
|
that commit onto the configured integration branch before creating streams.
|
|
101
101
|
Required prompts and directions must exist for stream creation and opening.
|
|
102
102
|
|
|
103
|
-
The package-owned planner extension
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
103
|
+
The package-owned planner extension admits each newly launched planner using
|
|
104
|
+
its exact `<slug>-planner` Pi name, `<slug>_planner` Herdr identity, managed
|
|
105
|
+
readiness, canonical worktree, and official Pi lifecycle integration. It checks
|
|
106
|
+
that all five planner tools are active before sending `/change` and binds the
|
|
107
|
+
admitted process, pane, worktree, name, and live session ID.
|
|
108
|
+
|
|
109
|
+
After landing, replacement activation uses a one-use ticket held in that Pi
|
|
110
|
+
process, bound to the new conversation's session ID. It verifies the local
|
|
111
|
+
identity, effective model/thinking selection, and tools without querying Herdr's
|
|
112
|
+
asynchronous status. Saved session entries cannot replay a handoff. Codeless
|
|
113
|
+
requires activation acknowledgement before submitting `/change` exactly once;
|
|
114
|
+
failed or cancelled handoffs never retry automatically. After a failed
|
|
115
|
+
replacement, exit Pi and reopen the stream from another Herdr shell. Herdr's
|
|
116
|
+
integration still supplies monitoring and external agent control. Global
|
|
114
117
|
installation of Codeless's extension is unnecessary.
|
|
115
118
|
The approval tool has no arguments. Its extension derives the active
|
|
116
119
|
`<slug>-planner` Pi session and passes it to the backing CLI, which requires it
|
|
@@ -235,9 +238,9 @@ only when absent, and uses `herdr agent start` for named, readiness-checked Pi
|
|
|
235
238
|
startup. It verifies the result before sending activation. An occupied or
|
|
236
239
|
mismatched pane, unmanaged agent, ambiguous layout, or failed startup stops;
|
|
237
240
|
Codeless never takes over an existing agent. Pi's display name is separate from
|
|
238
|
-
Herdr's managed agent name.
|
|
239
|
-
authority, and worktree
|
|
240
|
-
|
|
241
|
+
Herdr's managed agent name. Launch admission verifies both names, lifecycle
|
|
242
|
+
authority, and worktree; subsequent conversation replacements use the local
|
|
243
|
+
handoff described above. Codeless never renames an unmanaged process. There is
|
|
241
244
|
no direct `planner` command.
|
|
242
245
|
|
|
243
246
|
Dispatch validates the implementer selection before touching the planner's
|
package/extension/planner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
-
import {
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { validAttempt } from "../src/attempt.ts";
|
|
4
5
|
|
|
5
6
|
const codeless = fileURLToPath(new URL("../bin/codeless", import.meta.url));
|
|
@@ -28,32 +29,66 @@ function selection(value) {
|
|
|
28
29
|
return { provider: value.provider, model: value.model, thinking: value.thinking };
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// Pi reloads extension modules when replacing a session. Keep only in-flight
|
|
33
|
+
// tickets on the process global; neither persisted entries nor a module cache
|
|
34
|
+
// can establish handoff provenance. Every ticket is removed on consume/finally.
|
|
35
|
+
const handoffKey = Symbol.for("@dpeek/codeless/planner-handoffs");
|
|
36
|
+
const handoffs = (globalThis[handoffKey] ??= new Map());
|
|
37
|
+
|
|
38
|
+
function worktree(cwd) {
|
|
39
|
+
try {
|
|
40
|
+
return realpathSync(cwd);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error("Codeless could not resolve the planner worktree");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
export default function plannerExtension(pi) {
|
|
32
47
|
let registered = false;
|
|
33
48
|
let pending;
|
|
49
|
+
let admitted;
|
|
34
50
|
|
|
35
51
|
pi.registerCommand("streams-activate", {
|
|
36
52
|
description: "Verify this planner session before starting its project prompt",
|
|
37
53
|
handler: async (args, ctx) => {
|
|
38
|
-
let
|
|
54
|
+
let input;
|
|
39
55
|
try {
|
|
40
|
-
|
|
56
|
+
input = JSON.parse(args);
|
|
41
57
|
} catch {
|
|
42
|
-
throw new Error("Codeless activation requires
|
|
58
|
+
throw new Error("Codeless activation requires a JSON prompt or handoff ticket");
|
|
43
59
|
}
|
|
44
|
-
|
|
60
|
+
const replacement = typeof input === "object" && input !== null;
|
|
61
|
+
const ticket = replacement ? handoffs.get(input.handoff) : undefined;
|
|
62
|
+
if (replacement) {
|
|
63
|
+
handoffs.delete(input.handoff);
|
|
64
|
+
if (!ticket) throw new Error("Codeless handoff is missing, expired, or already consumed");
|
|
65
|
+
} else if (typeof input !== "string" || !input.startsWith("/change ")) {
|
|
45
66
|
throw new Error("Codeless activation requires a /change project prompt");
|
|
46
67
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
if (admitted) throw new Error("Codeless planner session is already activated");
|
|
69
|
+
const sessionName = pi.getSessionName();
|
|
70
|
+
const match = /^([a-z][a-z0-9-]{0,23})-planner$/.exec(sessionName ?? "");
|
|
71
|
+
if (!match)
|
|
72
|
+
throw new Error("Codeless activation requires an exact <slug>-planner Pi session name");
|
|
73
|
+
const expectedPlanner = `${match[1].replaceAll("-", "_")}_planner`;
|
|
74
|
+
const pane = process.env.HERDR_PANE_ID;
|
|
75
|
+
if (!pane) throw new Error("Codeless activation requires a Herdr-managed planner pane");
|
|
76
|
+
const cwd = worktree(ctx.cwd);
|
|
77
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
78
|
+
if (replacement) {
|
|
79
|
+
if (
|
|
80
|
+
ticket.binding.pid !== process.pid ||
|
|
81
|
+
ticket.binding.pane !== pane ||
|
|
82
|
+
ticket.binding.cwd !== cwd ||
|
|
83
|
+
ticket.binding.sessionName !== sessionName ||
|
|
84
|
+
ticket.sessionId !== sessionId ||
|
|
85
|
+
ticket.binding.sessionId === sessionId
|
|
86
|
+
) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"Codeless handoff does not match this planner process, pane, worktree, or session",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const requested = ticket.selection;
|
|
57
92
|
const reference = `${requested.provider}/${requested.model}`;
|
|
58
93
|
const model = ctx.modelRegistry.find(requested.provider, requested.model);
|
|
59
94
|
if (!model) throw new Error(`Planner requested ${reference}, but Pi could not find it`);
|
|
@@ -66,56 +101,46 @@ export default function plannerExtension(pi) {
|
|
|
66
101
|
ctx.model.id !== requested.model ||
|
|
67
102
|
pi.getThinkingLevel() !== requested.thinking
|
|
68
103
|
) {
|
|
69
|
-
const effective = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "no model";
|
|
70
104
|
throw new Error(
|
|
71
|
-
`
|
|
105
|
+
`Codeless planner could not apply ${reference} at thinking level ${requested.thinking}`,
|
|
72
106
|
);
|
|
73
107
|
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
108
|
+
} else {
|
|
109
|
+
const plannerIdentity = async () => {
|
|
110
|
+
const identity = await pi.exec("herdr", ["agent", "get", pane], { timeout: 30_000 });
|
|
111
|
+
if (identity.code !== 0) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
identity.stderr.trim() ||
|
|
114
|
+
identity.stdout.trim() ||
|
|
115
|
+
"Codeless could not verify Herdr planner identity",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(identity.stdout).result?.agent;
|
|
120
|
+
} catch {
|
|
121
|
+
throw new Error("Herdr returned an invalid planner identity response");
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const agent = await plannerIdentity();
|
|
125
|
+
if (agent?.name !== expectedPlanner) {
|
|
85
126
|
throw new Error(
|
|
86
|
-
identity
|
|
87
|
-
identity.stdout.trim() ||
|
|
88
|
-
"Codeless could not verify Herdr planner identity",
|
|
127
|
+
`Codeless planner identity is ${agent?.name ?? "missing"}, expected ${expectedPlanner}; exit this agent and run codeless open ${match[1]} from another Herdr shell`,
|
|
89
128
|
);
|
|
90
129
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
130
|
+
if (agent.agent !== "pi" || agent.interactive_ready !== true) {
|
|
131
|
+
throw new Error("Codeless requires a Herdr-managed Pi planner started by codeless open");
|
|
132
|
+
}
|
|
133
|
+
if (typeof agent.foreground_cwd !== "string" || worktree(agent.foreground_cwd) !== cwd) {
|
|
134
|
+
throw new Error("Codeless planner worktree does not match Herdr's foreground cwd");
|
|
135
|
+
}
|
|
136
|
+
const session = agent.agent_session;
|
|
137
|
+
if (
|
|
138
|
+
agent.screen_detection_skipped !== true ||
|
|
139
|
+
session?.source !== "herdr:pi" ||
|
|
140
|
+
session.agent !== "pi"
|
|
141
|
+
) {
|
|
142
|
+
throw new Error("Codeless planner requires Herdr's Pi lifecycle integration");
|
|
95
143
|
}
|
|
96
|
-
};
|
|
97
|
-
const agent = await plannerIdentity();
|
|
98
|
-
if (agent?.name !== expectedPlanner) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
`Codeless planner identity is ${agent?.name ?? "missing"}, expected ${expectedPlanner}; exit this agent and run codeless open ${match[1]} from another Herdr shell`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
if (agent.agent !== "pi" || agent.interactive_ready !== true) {
|
|
104
|
-
throw new Error("Codeless requires a Herdr-managed Pi planner started by codeless open");
|
|
105
|
-
}
|
|
106
|
-
if (
|
|
107
|
-
typeof agent.foreground_cwd !== "string" ||
|
|
108
|
-
resolve(agent.foreground_cwd) !== resolve(ctx.cwd)
|
|
109
|
-
) {
|
|
110
|
-
throw new Error("Codeless planner worktree does not match Herdr's foreground cwd");
|
|
111
|
-
}
|
|
112
|
-
const session = agent.agent_session;
|
|
113
|
-
if (
|
|
114
|
-
agent.screen_detection_skipped !== true ||
|
|
115
|
-
session?.source !== "herdr:pi" ||
|
|
116
|
-
session.agent !== "pi"
|
|
117
|
-
) {
|
|
118
|
-
throw new Error("Codeless planner requires Herdr's Pi lifecycle integration");
|
|
119
144
|
}
|
|
120
145
|
const activeTools = ctx.getSystemPromptOptions().selectedTools ?? [];
|
|
121
146
|
const missing = requiredTools.filter((tool) => !activeTools.includes(tool));
|
|
@@ -124,8 +149,10 @@ export default function plannerExtension(pi) {
|
|
|
124
149
|
`Codeless planner activation is missing required tools: ${missing.join(", ")}`,
|
|
125
150
|
);
|
|
126
151
|
}
|
|
152
|
+
admitted = { pid: process.pid, pane, cwd, sessionName, sessionId };
|
|
127
153
|
ctx.ui.notify(`Planner activated: ${sessionName} / ${expectedPlanner}`, "info");
|
|
128
|
-
|
|
154
|
+
if (ticket) ticket.activated = true;
|
|
155
|
+
else pi.sendUserMessage(input, { expandPromptTemplates: true });
|
|
129
156
|
},
|
|
130
157
|
});
|
|
131
158
|
|
|
@@ -135,8 +162,18 @@ export default function plannerExtension(pi) {
|
|
|
135
162
|
if (!pending || pending.running) throw new Error("No pending Codeless handoff");
|
|
136
163
|
const request = pending;
|
|
137
164
|
request.running = true;
|
|
165
|
+
let token;
|
|
138
166
|
try {
|
|
139
167
|
await ctx.waitForIdle();
|
|
168
|
+
if (
|
|
169
|
+
!admitted ||
|
|
170
|
+
admitted.pid !== process.pid ||
|
|
171
|
+
admitted.pane !== process.env.HERDR_PANE_ID ||
|
|
172
|
+
admitted.cwd !== worktree(ctx.cwd) ||
|
|
173
|
+
admitted.sessionId !== ctx.sessionManager.getSessionId() ||
|
|
174
|
+
admitted.sessionName !== pi.getSessionName()
|
|
175
|
+
)
|
|
176
|
+
throw new Error("Codeless handoff requires the activated planner session");
|
|
140
177
|
const execution = await pi.exec(
|
|
141
178
|
"bun",
|
|
142
179
|
[codeless, "next", request.changePath, request.landedCommit],
|
|
@@ -160,23 +197,39 @@ export default function plannerExtension(pi) {
|
|
|
160
197
|
throw new Error("Codeless returned an invalid planner handoff");
|
|
161
198
|
}
|
|
162
199
|
const requested = selection(requestedValue);
|
|
200
|
+
token = randomUUID();
|
|
201
|
+
const ticket = {
|
|
202
|
+
binding: admitted,
|
|
203
|
+
selection: requested,
|
|
204
|
+
sessionId: undefined,
|
|
205
|
+
activated: false,
|
|
206
|
+
};
|
|
207
|
+
handoffs.set(token, ticket);
|
|
163
208
|
const result = await ctx.newSession({
|
|
164
209
|
setup: async (sm) => {
|
|
165
210
|
sm.appendSessionInfo(sessionName);
|
|
166
|
-
sm.
|
|
167
|
-
role: "planner",
|
|
168
|
-
selection: requested,
|
|
169
|
-
});
|
|
211
|
+
ticket.sessionId = sm.getSessionId();
|
|
170
212
|
},
|
|
171
213
|
withSession: async (replacement) => {
|
|
172
|
-
await replacement.sendUserMessage(
|
|
173
|
-
|
|
174
|
-
|
|
214
|
+
await replacement.sendUserMessage(
|
|
215
|
+
`/streams-activate ${JSON.stringify({ handoff: token })}`,
|
|
216
|
+
{
|
|
217
|
+
expandPromptTemplates: true,
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
// Pi displays command errors instead of rejecting sendUserMessage.
|
|
221
|
+
// Require an explicit receipt before submitting the project prompt.
|
|
222
|
+
if (!ticket.activated)
|
|
223
|
+
throw new Error(
|
|
224
|
+
"Codeless replacement activation failed; exit Pi and reopen the stream",
|
|
225
|
+
);
|
|
226
|
+
await replacement.sendUserMessage(prompt, { expandPromptTemplates: true });
|
|
175
227
|
},
|
|
176
228
|
});
|
|
177
229
|
if (result.cancelled)
|
|
178
230
|
throw new Error("Codeless handoff cancelled; the current session was retained");
|
|
179
231
|
} finally {
|
|
232
|
+
if (token) handoffs.delete(token);
|
|
180
233
|
pending = undefined;
|
|
181
234
|
}
|
|
182
235
|
},
|
|
@@ -212,6 +265,7 @@ export default function plannerExtension(pi) {
|
|
|
212
265
|
additionalProperties: false,
|
|
213
266
|
},
|
|
214
267
|
async execute(_toolCallId, params) {
|
|
268
|
+
if (!admitted) throw new Error("Codeless handoff requires an activated planner");
|
|
215
269
|
if (pending) throw new Error("A Codeless handoff is already pending");
|
|
216
270
|
pending = {
|
|
217
271
|
changePath: params.changePath.replace(/^@/, ""),
|
package/package.json
CHANGED
package/spec/workflow.md
CHANGED
|
@@ -137,26 +137,39 @@ the stream worktree; occupied, mismatched, or ambiguous layouts stop unchanged.
|
|
|
137
137
|
The operator must invoke opening from outside those target panes. Reopening an
|
|
138
138
|
existing managed planner focuses it without installation or another prompt.
|
|
139
139
|
|
|
140
|
-
The package-owned extension
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
name,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
140
|
+
The package-owned extension admits a newly launched planner after checking its
|
|
141
|
+
exact `<slug>-planner` Pi name, `<slug-with-hyphens-replaced>_planner` Herdr
|
|
142
|
+
name, managed interactive readiness, canonical foreground worktree, official Pi
|
|
143
|
+
lifecycle authority and reporter, and active planner tools. Admission binds the
|
|
144
|
+
process ID, pane, canonical worktree, planner name, and native Pi session ID.
|
|
145
|
+
Activation never repairs names or submits the initial prompt twice. The direct
|
|
146
|
+
`planner` command is removed; recovery exits Pi deliberately and reopens from
|
|
147
|
+
another Herdr shell.
|
|
148
|
+
|
|
149
|
+
Post-landing conversation replacement uses that admission, without querying
|
|
150
|
+
Herdr. After idle and workflow validation, Codeless creates a one-use in-memory
|
|
151
|
+
handoff ticket for the admitted parent and requested model/thinking selection.
|
|
152
|
+
Pi's `newSession` setup binds it to the newly allocated native session ID;
|
|
153
|
+
`withSession` invokes activation in the fresh extension. Activation consumes the
|
|
154
|
+
ticket before checking the same process, pane, canonical worktree and planner
|
|
155
|
+
name, a distinct matching replacement session, effective selection, and active
|
|
156
|
+
tools. Saved session entries cannot authorize a handoff. The ticket registry
|
|
157
|
+
survives Pi's extension module reload within the process, but no ticket survives
|
|
158
|
+
consumption, cancellation, failure, or process exit.
|
|
159
|
+
|
|
160
|
+
Pi displays extension-command errors without rejecting the command submission.
|
|
161
|
+
Codeless therefore requires an explicit activation acknowledgement before
|
|
162
|
+
`withSession` submits the project prompt exactly once through the replacement
|
|
163
|
+
context. Missing acknowledgement stops visibly. Neither old Pi contexts nor
|
|
164
|
+
Herdr's asynchronous lifecycle or native-session snapshots participate in
|
|
165
|
+
replacement activation. Delayed, missing, or rejected lifecycle reports cannot
|
|
166
|
+
block this local handoff. Cancellation retains the old conversation; failed
|
|
167
|
+
replacement requires deliberate exit and reopen, with no automatic retry.
|
|
168
|
+
|
|
169
|
+
Herdr's official Pi integration remains responsible for lifecycle monitoring,
|
|
170
|
+
external agent control, and native-session restore reporting. Implementers use
|
|
171
|
+
the corresponding `_impl` and `-impl` names. Codeless loads its own extension
|
|
172
|
+
explicitly. This boundary was checked against Herdr 0.8.2 and Pi 0.85.1.
|
|
160
173
|
|
|
161
174
|
## Dispatch and review
|
|
162
175
|
|