@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 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 activates every planner session. Before its
104
- first project prompt, it requires the exact `<slug>-planner` Pi name and verifies
105
- the `<slug>_planner` Herdr identity, managed interactive readiness, foreground
106
- worktree, and Herdr's official Pi lifecycle authority and session reporter. It confirms
107
- `approve_stream_change`, `dispatch_stream_implementer`,
108
- `rework_stream_implementer`, `finish_stream_implementer`, and
109
- `next_stream_change` are active. Missing or incompatible activation, identity
110
- mismatch, or inactive tools stops before `/change`. Pi's active command context
111
- owns the live conversation identity. Codeless requires Herdr's official reporter
112
- but treats its native session-reference value as asynchronous restore metadata,
113
- not an activation prerequisite. Global
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. Activation verifies the names, process, lifecycle
239
- authority, and worktree without correlating Herdr's asynchronous restore
240
- reference to Pi's live session; it never renames an unmanaged process. There is
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
@@ -1,5 +1,6 @@
1
1
  import { fileURLToPath } from "node:url";
2
- import { resolve } from "node:path";
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 prompt;
54
+ let input;
39
55
  try {
40
- prompt = JSON.parse(args);
56
+ input = JSON.parse(args);
41
57
  } catch {
42
- throw new Error("Codeless activation requires one JSON-quoted project prompt");
58
+ throw new Error("Codeless activation requires a JSON prompt or handoff ticket");
43
59
  }
44
- if (typeof prompt !== "string" || !prompt.startsWith("/change ")) {
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
- const activation = ctx.sessionManager
48
- .getEntries()
49
- .findLast(
50
- (entry) =>
51
- entry.type === "custom" &&
52
- entry.customType === "streams-role-selection" &&
53
- entry.data?.role === "planner",
54
- );
55
- if (activation) {
56
- const requested = selection(activation.data.selection);
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
- `Planner requested ${reference} at thinking level ${requested.thinking}, but Pi applied ${effective} at ${pi.getThinkingLevel()}`,
105
+ `Codeless planner could not apply ${reference} at thinking level ${requested.thinking}`,
72
106
  );
73
107
  }
74
- }
75
- const sessionName = pi.getSessionName();
76
- const match = /^([a-z][a-z0-9-]{0,23})-planner$/.exec(sessionName ?? "");
77
- if (!match)
78
- throw new Error("Codeless activation requires an exact <slug>-planner Pi session name");
79
- const expectedPlanner = `${match[1].replaceAll("-", "_")}_planner`;
80
- const pane = process.env.HERDR_PANE_ID;
81
- if (!pane) throw new Error("Codeless activation requires a Herdr-managed planner pane");
82
- const plannerIdentity = async () => {
83
- const identity = await pi.exec("herdr", ["agent", "get", pane], { timeout: 30_000 });
84
- if (identity.code !== 0) {
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.stderr.trim() ||
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
- try {
92
- return JSON.parse(identity.stdout).result?.agent;
93
- } catch {
94
- throw new Error("Herdr returned an invalid planner identity response");
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
- pi.sendUserMessage(prompt, { expandPromptTemplates: true });
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.appendCustomEntry("streams-role-selection", {
167
- role: "planner",
168
- selection: requested,
169
- });
211
+ ticket.sessionId = sm.getSessionId();
170
212
  },
171
213
  withSession: async (replacement) => {
172
- await replacement.sendUserMessage(`/streams-activate ${JSON.stringify(prompt)}`, {
173
- expandPromptTemplates: true,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dpeek/codeless",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "An attended planner and implementer workflow for parallel capability development",
5
5
  "homepage": "https://github.com/dpeek/codeless#readme",
6
6
  "bugs": {
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 activates creation, reopening, and post-landing
141
- replacement. Before the first project prompt it requires the exact
142
- `<slug>-planner` Pi name, `<slug-with-hyphens-replaced>_planner` Herdr name,
143
- managed interactive readiness, matching foreground worktree, Herdr's official Pi
144
- lifecycle authority and reporter, and the active planner tools. A wrong name,
145
- process, lifecycle source, worktree, or tool set stops visibly before `/change`.
146
- Activation never repairs names. Pi's active command context owns the live
147
- conversation identity. Codeless requires Herdr's official session reporter but
148
- treats its native session-reference value as asynchronous restore metadata; it
149
- does not correlate that value with the active Pi session or wait for a changed
150
- value. The direct `planner` command is removed; recovery exits Pi
151
- deliberately and reopens from another Herdr shell.
152
-
153
- Pi session replacement keeps the managed process and Herdr name while changing
154
- its native conversation reference. Codeless revalidates the replacement's Pi
155
- name, selection, tools, and Herdr process/lifecycle/worktree binding before
156
- prompting it. Implementers use the corresponding `_impl` and `-impl` names.
157
- Codeless loads its own extension explicitly; Herdr's official Pi integration
158
- supplies lifecycle and native-session reporting. This boundary was verified
159
- against Herdr 0.8.2 and Pi 0.85.1.
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