@afokapu/atdd-flow 0.3.39 → 0.3.40

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
@@ -130,7 +130,7 @@ atdd-flow task review example-app runtime-rollout --by driver.runtime@example-ap
130
130
  atdd-flow task done example-app runtime-rollout --by coordinator@example-app
131
131
  ```
132
132
 
133
- Proof is a compact PR, CI run, report, commit range, deployment, or thread reference. Dependencies gate prerequisites; independent tasks are parallel-ready. A coordinator can staff an unassigned ready task with `atdd-flow task assign <project> <task-id> --assignee <address> --by <coordinator-address>`; assignment is allowed only once while the task is `todo`. Use `task block` only for a real external blocker, then checkpoint exact state and next action. Once the blocker is resolved, only that coordinator can clear it with `atdd-flow task unblock <project> <task-id> --by <coordinator-address>` while the task remains `in_progress`. For deliveries explicitly governed by the `workflow` ATDD Bun profile, `review → done` additionally requires a durable final behavioral-review result with decision `APPROVE` for the current clean delivery commit. For an idle driver’s final task, `task done ... --retire-assignee` delegates clean-and-merged worktree retirement to ATDD Bun. A driver whose assigned tasks are all already `done` is retired with `atdd-flow seat retire <address> --by <coordinator-or-main>`, using the same checks. When every assigned task is `done` and the recorded worktree is absent from disk and from `git worktree list`, `seat retire` records the retirement without running worktree finish, deleting the branch, or recreating the worktree; a missing worktree that Git still registers fails closed. Both paths fail closed on unfinished tasks or a dirty or unmerged worktree; on success they record `retired` and a checkpoint, then close the driver's Herdr workspace when `close_inactive` is enabled.
133
+ Proof is a compact PR, CI run, report, commit range, deployment, or thread reference. Dependencies gate prerequisites; independent tasks are parallel-ready. A coordinator can staff an unassigned ready task with `atdd-flow task assign <project> <task-id> --assignee <address> --by <coordinator-address>`; assignment is allowed only once while the task is `todo`. Use `task block` only for a real external blocker, then checkpoint exact state and next action. Once the blocker is resolved, only that coordinator can clear it with `atdd-flow task unblock <project> <task-id> --by <coordinator-address>` while the task remains `in_progress`. For deliveries explicitly governed by the `workflow` ATDD Bun profile, `review → done` additionally requires a durable final behavioral-review result with decision `APPROVE` for the current clean delivery commit. For an idle driver’s final task, `task done ... --retire-assignee` delegates clean-and-merged worktree retirement to ATDD Bun. A driver whose assigned tasks are all already `done` is retired with `atdd-flow seat retire <address> --by <coordinator-or-main>`, using the same checks. When every assigned task is `done` and the recorded worktree is absent from disk and from `git worktree list`, `seat retire` records the retirement without running worktree finish, deleting the branch, or recreating the worktree; a missing worktree that Git still registers fails closed. Both paths fail closed on unfinished tasks or a dirty or unmerged worktree; on success they record `retired` and a checkpoint, then close the driver's Herdr workspace when `close_inactive` is enabled. `main@<project>` may likewise retire a coordinator seat (never main, the operator, or a seat on the primary checkout) once every task it coordinates or is assigned is `done`, through the same worktree-finish, absent-and-unregistered, and fail-closed paths. Retirement runs in the seat worktree with ATDD Bun from the primary checkout's `node_modules/.bin`, else the primary checkout's own source when it is the ATDD Bun package, else `PATH`; never the seat worktree's copy, which may pin an older toolkit.
134
134
 
135
135
  At 80% context usage a seat writes a structured handover (template and process rules in the lifecycle convention), checks it with `atdd-flow handover <address> --check --file <path>`, and records it with `atdd-flow handover <address> --file <path>`. The seat keeps one durable `handover.yaml` beside its unchanged checkpoint. Recording rejects a missing header, Runtime line, or section, sections out of order, more than 150 lines, vague references such as `latest` or `that PR` without an exact ID, and a `supersedes` value that does not match the recorded handover id. A fresh session bound to the same seat reads it with `atdd-flow handover show <address>`.
136
136
 
@@ -246,8 +246,9 @@ content:
246
246
  review and writes a blocked checkpoint for investigation.
247
247
 
248
248
  Before working in or retiring a driver worktree, install that repository's local dependencies there.
249
- Retirement resolves ATDD Bun from the worktree's local node_modules/.bin so it does not depend on a
250
- developer's global executable installation.
249
+ Retirement runs in the seat worktree but resolves ATDD Bun from the primary checkout's local
250
+ node_modules/.bin, then the primary checkout's own source when it is the ATDD Bun package, then PATH.
251
+ It never uses the seat worktree's copy, which may pin an older toolkit that refuses its own retirement.
251
252
 
252
253
  Use the atdd-flow CLI to mutate workflow records. Read task, seat, checkpoint, and thread YAML
253
254
  before acting; do not treat terminal pane history, a multiplexer, an optional judgment helper, or an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-flow",
3
- "version": "0.3.39",
3
+ "version": "0.3.40",
4
4
  "description": "Filesystem-first Desk registry for projects, seats, tasks, threads, and handoffs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/tasks.ts CHANGED
@@ -172,9 +172,26 @@ async function absentUnregisteredWorktree(root: string, projectName: string, own
172
172
  return `Worktree ${owner.worktree} is absent and unregistered; retired without worktree finish; branch ${owner.branch} retained.`;
173
173
  }
174
174
 
175
+ /**
176
+ * Resolves the ATDD Bun that retires a seat: the primary checkout's local copy, then the primary
177
+ * checkout's package source when it is ATDD Bun itself, then PATH. A seat worktree copy is never used:
178
+ * a finished branch may pin an older toolkit that refuses the seat being retired.
179
+ */
180
+ async function retirementBun(root: string, projectName: string) {
181
+ const primary = resolve(required((await project(root, projectName)).repository, `a repository for project ${projectName}`));
182
+ const local = join(primary, "node_modules", ".bin", "atdd-bun");
183
+ if (await exists(local)) return [local];
184
+ const source = join(primary, "src", "cli.ts");
185
+ try {
186
+ if ((await Bun.file(join(primary, "package.json")).json()).name === "@afokapu/atdd-bun" && await exists(source)) return [process.execPath, source];
187
+ } catch { /* no readable package manifest; fall through to PATH */ }
188
+ // Absolute so command execution never substitutes the seat worktree's local copy.
189
+ return [required(Bun.which("atdd-bun") ?? undefined, "atdd-bun in the primary checkout node_modules/.bin or on PATH")];
190
+ }
191
+
175
192
  async function retireDriver(root: string, projectName: string, assignee: string, id: string, allowAbsentWorktree = false) {
176
193
  const unfinished = (await allTasks(root, projectName))
177
- .filter((entry) => entry.id !== id && entry.task.assignee === assignee && entry.task.status !== "done")
194
+ .filter((entry) => entry.id !== id && (entry.task.assignee === assignee || entry.task.coordinator === assignee) && entry.task.status !== "done")
178
195
  .map((entry) => entry.id);
179
196
  if (unfinished.length) throw new Error(`Cannot retire ${assignee}; it still owns unfinished tasks: ${unfinished.join(", ")}.`);
180
197
 
@@ -182,7 +199,7 @@ async function retireDriver(root: string, projectName: string, assignee: string,
182
199
  if (owner.retired) throw new Error(`Seat ${assignee} is already retired.`);
183
200
  try {
184
201
  const summary = (allowAbsentWorktree ? await absentUnregisteredWorktree(root, projectName, owner) : undefined)
185
- ?? await runOutput(["atdd-bun", "worktree", "finish", "--delete-branch"], owner.worktree);
202
+ ?? await runOutput([...await retirementBun(root, projectName), "worktree", "finish", "--delete-branch"], owner.worktree);
186
203
  owner.retired = { task: `${projectName}/${id}`, completed_at: now(), summary };
187
204
  await atomicYaml(paths(root).seatFile(assignee), owner);
188
205
  const checkpoint: Checkpoint = {
@@ -206,20 +223,40 @@ async function retireDriver(root: string, projectName: string, assignee: string,
206
223
  await closeRetiredDriverWorkspace(root, owner);
207
224
  }
208
225
 
209
- /** Retires a driver whose assigned tasks are all already done. */
226
+ /**
227
+ * Only main retires a coordinator, and never one seated on the primary checkout; a driver may also be
228
+ * retired by the coordinator of every task assigned to it.
229
+ */
230
+ async function assertRetireAuthority(root: string, owner: Seat, actor: string, owned: ListedTask[]) {
231
+ const main = `main@${owner.project}`;
232
+ if (owner.role === "coordinator") {
233
+ if (actor !== main) throw new Error(`Only ${main} may retire coordinator ${owner.address}.`);
234
+ const primary = (await project(root, owner.project)).repository;
235
+ if (primary && await canonicalPath(owner.worktree) === await canonicalPath(primary)) {
236
+ throw new Error(`Cannot retire ${owner.address}; its worktree is the primary checkout.`);
237
+ }
238
+ } else if (actor !== main && owned.some((entry) => entry.task.coordinator !== actor)) {
239
+ throw new Error(`Only ${main} or the coordinator of every task assigned to ${owner.address} may retire it.`);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Retires a driver whose assigned tasks are all already done, or, for main only, a coordinator
245
+ * (never main or the primary checkout) whose coordinated and assigned tasks are all done.
246
+ */
210
247
  export async function retireSeat(root: string, address: string, args: string[]) {
211
248
  const resolved = await canonicalAddress(root, address);
212
249
  const actor = await canonicalAddress(root, required(words(args, "--by"), "--by"));
213
250
  const owner = await seat(root, resolved);
214
- if (owner.role !== "driver") throw new Error(`Only driver seats can be retired; ${resolved} is ${owner.role}.`);
251
+ const coordinator = owner.role === "coordinator";
252
+ if (owner.role !== "driver" && !coordinator) throw new Error(`Only driver or coordinator seats can be retired; ${resolved} is ${owner.role}.`);
215
253
  if (owner.retired) throw new Error(`Seat ${resolved} is already retired.`);
216
- const owned = (await allTasks(root, owner.project)).filter((entry) => entry.task.assignee === resolved);
217
- if (!owned.length) throw new Error(`Cannot retire ${resolved}; it has no assigned tasks.`);
254
+ const owned = (await allTasks(root, owner.project))
255
+ .filter((entry) => entry.task.assignee === resolved || (coordinator && entry.task.coordinator === resolved));
256
+ if (!owned.length) throw new Error(`Cannot retire ${resolved}; it has no ${coordinator ? "coordinated or " : ""}assigned tasks.`);
218
257
  const unfinished = owned.filter((entry) => entry.task.status !== "done").map((entry) => entry.id);
219
258
  if (unfinished.length) throw new Error(`Cannot retire ${resolved}; it still owns unfinished tasks: ${unfinished.join(", ")}.`);
220
- if (actor !== `main@${owner.project}` && owned.some((entry) => entry.task.coordinator !== actor)) {
221
- throw new Error(`Only main@${owner.project} or the coordinator of every task assigned to ${resolved} may retire it.`);
222
- }
259
+ await assertRetireAuthority(root, owner, actor, owned);
223
260
  await retireDriver(root, owner.project, resolved, owned.at(-1)!.id, true);
224
261
  console.log(`${resolved} retired`);
225
262
  }