@afokapu/atdd-flow 0.3.24 → 0.3.26

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
@@ -16,7 +16,7 @@ Desk (private Git repository) Code repositories / worktrees
16
16
  - A **seat** owns responsibility, branch, worktree, checkpoint, and host addresses; a **task** owns its brief, ownership, dependencies, criteria, and proof.
17
17
  - A **thread** owns messages, receipts, results, and decisions; a **host** owns panes and notifications—never durable state.
18
18
 
19
- A stable address, such as `driver.runtime@resolver-os`, survives a different pane, host, model, or replacement agent. A replacement reads its durable seat, task, checkpoint, and threads rather than predecessor-private context.
19
+ A stable address, such as `driver.runtime@example-app`, survives a different pane, host, model, or replacement agent. A replacement reads its durable seat, task, checkpoint, and threads rather than predecessor-private context.
20
20
 
21
21
  Workflow is a YAML protocol and CLI—not a database, daemon, agent runtime, or task-management SaaS. Git supplies history and replication; a multiplexer may wake an agent, but never owns state.
22
22
 
@@ -76,15 +76,25 @@ model. Final behavioral review has its own bounded routing step described below.
76
76
 
77
77
  ## Configure worktrees and seats
78
78
 
79
- Create a project, then set its policy in `work/<project>/project.yaml`:
79
+ Create a project, declare its primary checkout and linked-worktree root, then adjust its policy in `work/<project>/project.yaml`:
80
80
 
81
81
  ```sh
82
- atdd-flow project init resolver-os
82
+ atdd-flow project init example-app
83
+ atdd-flow project configure example-app --repository /Users/you/Github/example-app --worktree-root /Users/you/Github/worktrees/example-app
83
84
  ```
84
85
 
86
+ `project configure` sets only `repository` and `worktree_root`. It fails closed, leaving the project
87
+ unchanged, when `--repository` is not the top level of a Git repository or a flag is unknown, repeated,
88
+ or missing its value. It never changes seats, runtimes, tasks, branches, worktrees, or repository
89
+ content; it lists each seat whose recorded placement violates the topology as `unprojected`.
90
+
91
+ A misplaced seat is left unprojected: `multiplexer status` reports it under `unprojected`, and
92
+ `multiplexer apply` and launch projection of other seats continue. Projecting that seat explicitly
93
+ still rejects until its placement is corrected.
94
+
85
95
  ```yaml
86
- repository: /Users/you/Github/resolver-os
87
- worktree_root: /Users/you/Github/worktrees/resolver-os
96
+ repository: /Users/you/Github/example-app
97
+ worktree_root: /Users/you/Github/worktrees/example-app
88
98
  roles:
89
99
  coordinator: { address: coordinator@{project}, branch: main, worktree: '{repository}' }
90
100
  driver: { address: driver.{name}@{project}, branch: delivery/{name}, base: main, worktree: '{worktree_root}/{name}' }
@@ -93,11 +103,11 @@ roles:
93
103
  The operator or coordinator creates seats; drivers do not choose their policy:
94
104
 
95
105
  ```sh
96
- atdd-flow spawn resolver-os coordinator main --worktree /Users/you/Github/resolver-os
97
- atdd-flow spawn resolver-os driver runtime
106
+ atdd-flow spawn example-app coordinator main --worktree /Users/you/Github/example-app
107
+ atdd-flow spawn example-app driver runtime
98
108
  ```
99
109
 
100
- `spawn` creates missing driver worktrees through Git: this example creates `/Users/you/Github/worktrees/resolver-os/runtime` on `delivery/runtime`. ATDD Bun owns safe retirement, not creation. A seat can own several tasks.
110
+ `spawn` creates missing driver worktrees through Git: this example creates `/Users/you/Github/worktrees/example-app/runtime` on `delivery/runtime`. ATDD Bun owns safe retirement, not creation. A seat can own several tasks.
101
111
 
102
112
  ## Deliver work
103
113
 
@@ -108,14 +118,14 @@ todo → in_progress → review → done
108
118
  The coordinator assigns; the driver implements, proves each criterion, and submits for review; only the coordinator marks the task done.
109
119
 
110
120
  ```sh
111
- atdd-flow task add resolver-os runtime-rollout --title 'Complete runtime rollout' \
112
- --coordinator coordinator@resolver-os --assignee driver.runtime@resolver-os \
121
+ atdd-flow task add example-app runtime-rollout --title 'Complete runtime rollout' \
122
+ --coordinator coordinator@example-app --assignee driver.runtime@example-app \
113
123
  --done-when 'Checks pass'
114
- atdd-flow task start resolver-os runtime-rollout --by driver.runtime@resolver-os
115
- atdd-flow task prove resolver-os runtime-rollout --by driver.runtime@resolver-os --item 1 --proof 'CI run 42'
116
- atdd-flow task review resolver-os runtime-rollout --by driver.runtime@resolver-os
124
+ atdd-flow task start example-app runtime-rollout --by driver.runtime@example-app
125
+ atdd-flow task prove example-app runtime-rollout --by driver.runtime@example-app --item 1 --proof 'CI run 42'
126
+ atdd-flow task review example-app runtime-rollout --by driver.runtime@example-app
117
127
  # An independently attached reviewer persists APPROVE, RETURN, or ESCALATE through behavioral-review record
118
- atdd-flow task done resolver-os runtime-rollout --by coordinator@resolver-os
128
+ atdd-flow task done example-app runtime-rollout --by coordinator@example-app
119
129
  ```
120
130
 
121
131
  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.
@@ -127,11 +137,11 @@ For topology normalization, transfer an active task through the narrow durable c
127
137
  Threads are the durable inbox/outbox. Workflow persists a message before a best-effort host notification, so a closed pane, rate limit, or missed prompt cannot lose it.
128
138
 
129
139
  ```sh
130
- atdd-flow thread start --with coordinator@resolver-os,driver.runtime@resolver-os \
131
- --subject 'Runtime rollout' --task resolver-os/runtime-rollout
132
- atdd-flow post T-... --from coordinator@resolver-os --to driver.runtime@resolver-os \
140
+ atdd-flow thread start --with coordinator@example-app,driver.runtime@example-app \
141
+ --subject 'Runtime rollout' --task example-app/runtime-rollout
142
+ atdd-flow post T-... --from coordinator@example-app --to driver.runtime@example-app \
133
143
  --label 'implementation request' --expects-result --body 'Implement the task and return proof references.'
134
- atdd-flow result T-... M-... --from driver.runtime@resolver-os --label 'proof returned' --body 'CI run 42; PR #81.'
144
+ atdd-flow result T-... M-... --from driver.runtime@example-app --label 'proof returned' --body 'CI run 42; PR #81.'
135
145
  ```
136
146
 
137
147
  New durable thread and message IDs are human-readable: `T-` or `M-`, a UTC-second
@@ -149,14 +159,14 @@ For a shared boundary: driver → coordinator → affected coordinator(s) → mi
149
159
 
150
160
  ```sh
151
161
  atdd-flow status
152
- atdd-flow status seat driver.runtime@resolver-os
153
- atdd-flow open driver.runtime@resolver-os
162
+ atdd-flow status seat driver.runtime@example-app
163
+ atdd-flow open driver.runtime@example-app
154
164
  ```
155
165
 
156
166
  Herdr and tmux can notify an already attached host pane. Flow does not create host panes: the operator starts the agent in its declared worktree with `ATDD_WORKFLOW_ROOT` and `ATDD_WORKFLOW_SEAT`, then attaches that pane deterministically.
157
167
 
158
168
  ```sh
159
- atdd-flow attach driver.runtime@resolver-os --application herdr
169
+ atdd-flow attach driver.runtime@example-app --application herdr
160
170
  ```
161
171
 
162
172
  ### Optional Herdr worktree projection
@@ -213,10 +223,10 @@ To host Pi in Herdr, create a pane with the durable Desk and seat identity, then
213
223
  PI_EXTENSION="$(atdd-flow pi extension-path)"
214
224
  herdr pane split --current --direction right --cwd /path/to/worktree --no-focus \
215
225
  --env ATDD_WORKFLOW_ROOT=/path/to/desk \
216
- --env ATDD_WORKFLOW_SEAT=driver.runtime@resolver-os
226
+ --env ATDD_WORKFLOW_SEAT=driver.runtime@example-app
217
227
  # Use the pane id returned above.
218
228
  herdr agent start pi-runtime --kind pi --pane <pane-id> -- --extension "$PI_EXTENSION"
219
- atdd-flow --root /path/to/desk attach driver.runtime@resolver-os --application herdr --wake native
229
+ atdd-flow --root /path/to/desk attach driver.runtime@example-app --application herdr --wake native
220
230
  ```
221
231
 
222
232
  Pi receives its identity and startup task from the extension itself. Its normal TUI remains visible and manually usable; incoming Desk mail wakes it through Pi's native message API. The runtime lifecycle has one internal bounded inbox transport; it does not add a second watcher, registry, mailbox, reservation system, or routing path.
@@ -265,7 +275,7 @@ Jev is read-only; it cannot mutate state, approve proof, or override ATDD Bun.
265
275
 
266
276
  ```sh
267
277
  atdd-flow scout --goal 'Fix payment retry behavior' --path src/payment/retry.ts --path src/profile/avatar.ts
268
- atdd-flow focus-check resolver-os runtime-rollout --action 'Add a generic retry orchestration service'
278
+ atdd-flow focus-check example-app runtime-rollout --action 'Add a generic retry orchestration service'
269
279
  ```
270
280
 
271
281
  `scout` selects likely files. `focus-check` returns `REQUIRED`, `USEFUL_BUT_NOT_REQUIRED`, or `SPECULATIVE`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-flow",
3
- "version": "0.3.24",
3
+ "version": "0.3.26",
4
4
  "description": "Filesystem-first Desk registry for projects, seats, tasks, threads, and handoffs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { basename, resolve } from "node:path";
4
- import { init, initProject, spawn, bind, useApplication, attach, describe, checkpoint, launchPiRuntime, migrate, openSeat, piExtensionPath } from "./seats";
4
+ import { init, initProject, configureProject, spawn, bind, useApplication, attach, describe, checkpoint, launchPiRuntime, migrate, openSeat, piExtensionPath } from "./seats";
5
5
  import { createExactSessionAdoptionAuthorization } from "./adoption-authorizations";
6
6
  import { addParticipant, openThread, post, readMessage, receipt, result, startThread } from "./threads";
7
7
  import * as tasks from "./tasks";
@@ -23,6 +23,7 @@ Usage:
23
23
  atdd-flow init <desk-directory> [--git]
24
24
  atdd-flow desk migrate
25
25
  atdd-flow project init <project>
26
+ atdd-flow project configure <project> [--repository <primary-checkout>] [--worktree-root <directory>]
26
27
  atdd-flow spawn <project> <role> <name> [--worktree <path>] [--branch <branch>] [--agent <legacy-executable>]
27
28
  atdd-flow bind <address> [--application <application>] --address <native-address> [--session <name>] [--agent <executable>] [--worktree <path>] [--wake host|native]
28
29
  atdd-flow attach <address> [--application <application>] [--wake host|native]
@@ -87,7 +88,8 @@ async function main() {
87
88
  },
88
89
  project: async () => {
89
90
  if (rest[0] === "init") return initProject(root, required(rest[1], "project"));
90
- throw new Error("Use `atdd-flow project init <project>`.");
91
+ if (rest[0] === "configure") return configureProject(root, required(rest[1], "project"), rest.slice(2));
92
+ throw new Error("Use `atdd-flow project init <project>` or `atdd-flow project configure <project> [--repository <primary-checkout>] [--worktree-root <directory>]`.");
91
93
  },
92
94
  spawn: () => spawn(root, required(rest[0], "project"), required(rest[1], "role"), required(rest[2], "name"), rest.slice(3)),
93
95
  bind: () => bind(root, required(rest[0], "address"), rest.slice(1)),
@@ -89,6 +89,21 @@ async function projectSeats(root: string, projectName: string) {
89
89
  return seats.filter((entry): entry is Seat => Boolean(entry && entry.schema === "atdd-workflow/seat/v2" && !entry.retired));
90
90
  }
91
91
 
92
+ export type UnprojectedSeat = { address: string; reason: string };
93
+
94
+ /** Seats whose recorded placement violates the project topology; they are never projected. */
95
+ export async function misplacedSeats(root: string, projectName: string): Promise<UnprojectedSeat[]> {
96
+ const config = await project(root, projectName);
97
+ if (!config.repository) return [];
98
+ const primary = resolve(config.repository);
99
+ const result: UnprojectedSeat[] = [];
100
+ for (const entry of await projectSeats(root, projectName)) {
101
+ try { assertNewTopologyPlacement(config, entry, primary); }
102
+ catch (error) { result.push({ address: entry.address, reason: (error as Error).message }); }
103
+ }
104
+ return result;
105
+ }
106
+
92
107
  function assertNewTopologyPlacement(config: Project, entry: Seat, primary: string) {
93
108
  const coordinator = config.roles.coordinator;
94
109
  const namedCoordinatorDefaults = coordinator?.address === "coordinator.{name}@{project}" && coordinator.branch === "integration/{name}";
@@ -111,7 +126,11 @@ function assertNewTopologyPlacement(config: Project, entry: Seat, primary: strin
111
126
  }
112
127
  }
113
128
 
114
- async function targets(root: string): Promise<Target[]> {
129
+ /**
130
+ * A misplaced seat is skipped and recorded in `unprojected` so it never aborts projection of other
131
+ * seats; requesting that seat explicitly (`strict`) still rejects.
132
+ */
133
+ async function targets(root: string, options: { strict?: string; unprojected?: UnprojectedSeat[] } = {}): Promise<Target[]> {
115
134
  let projects: string[];
116
135
  try { projects = await readdir(paths(root).work); }
117
136
  catch { return []; }
@@ -124,8 +143,13 @@ async function targets(root: string): Promise<Target[]> {
124
143
  const primary = resolve(config.repository);
125
144
  result.push({ project: name, worktree: primary, workspaceLabel: name });
126
145
  for (const entry of await projectSeats(root, name)) {
127
- assertNewTopologyPlacement(config, entry, primary);
128
- if ((entry.role === "main" || entry.role === "coordinator") && resolve(entry.worktree) === primary) {
146
+ try { assertNewTopologyPlacement(config, entry, primary); }
147
+ catch (error) {
148
+ if (entry.address === options.strict) throw error;
149
+ options.unprojected?.push({ address: entry.address, reason: (error as Error).message });
150
+ continue;
151
+ }
152
+ if ((entry.role === "main" || entry.role === "coordinator" || entry.role === "operator") && resolve(entry.worktree) === primary) {
129
153
  result.push({ project: name, worktree: primary, workspaceLabel: name, seat: entry });
130
154
  continue;
131
155
  }
@@ -166,8 +190,16 @@ async function liveWorkspaces(session: string, command: HerdrCommand) {
166
190
  return value.workspaces as Workspace[];
167
191
  }
168
192
 
193
+ /** A live workspace without a checkout path never matches: resolve("") would be the process cwd. */
194
+ function matchWorkspace(workspaces: Workspace[], worktree: string) {
195
+ return workspaces.find((entry) => {
196
+ const checkout = entry.worktree?.checkout_path;
197
+ return typeof checkout === "string" && checkout !== "" && resolve(checkout) === worktree;
198
+ });
199
+ }
200
+
169
201
  async function ensureWorkspace(session: string, target: Target, workspaces: Workspace[], command: HerdrCommand, primary?: WorkspaceState) {
170
- let current = workspaces.find((entry) => resolve(entry.worktree?.checkout_path ?? "") === target.worktree);
202
+ let current = matchWorkspace(workspaces, target.worktree);
171
203
  if (!current) {
172
204
  const args = target.workspaceLabel === target.project
173
205
  ? ["workspace", "create", "--cwd", target.worktree, "--label", target.workspaceLabel, "--no-focus"]
@@ -229,7 +261,7 @@ async function ensureTarget(session: string, target: Target, workspaces: Workspa
229
261
  /** Reconciles exactly one Desk seat into a labeled Herdr workspace/tab/pane. */
230
262
  export async function projectHerdrSeat(root: string, address: string, session: string, command: HerdrCommand = herdr): Promise<HerdrSeatProjection> {
231
263
  await herdrPolicy();
232
- const desired = await targets(root);
264
+ const desired = await targets(root, { strict: address });
233
265
  const target = desired.find((entry) => entry.seat?.address === address);
234
266
  if (!target?.seat) throw new Error(`No active Desk projection target exists for ${address}.`);
235
267
  const primaryTarget = desired.find((entry) => entry.project === target.project && entry.workspaceLabel === target.project && !entry.seat);
@@ -260,16 +292,17 @@ export async function multiplexer(root: string, args: string[]) {
260
292
  console.log("No Herdr session selected; projection was not run.");
261
293
  return;
262
294
  }
263
- const desired = await targets(root);
295
+ const unprojected: UnprojectedSeat[] = [];
296
+ const desired = await targets(root, { unprojected });
264
297
  const command: HerdrCommand = herdr;
265
298
  const workspaces = await liveWorkspaces(session, command);
266
299
  if (action === "status") {
267
- const present = desired.filter((target) => workspaces.some((entry) => resolve(entry.worktree?.checkout_path ?? "") === target.worktree));
300
+ const present = desired.filter((target) => matchWorkspace(workspaces, target.worktree));
268
301
  const topology = await Promise.all(desired.map(async (target) => {
269
- const workspace = workspaces.find((entry) => resolve(entry.worktree?.checkout_path ?? "") === target.worktree);
302
+ const workspace = matchWorkspace(workspaces, target.worktree);
270
303
  return workspace ? topologyCompliant(session, target, workspace, command) : false;
271
304
  }));
272
- console.log(JSON.stringify({ schema: "atdd-workflow/multiplexer-status/v1", application: "herdr", session, desired: desired.length, present: present.length, topology_compliant: topology.filter(Boolean).length }, null, 2));
305
+ console.log(JSON.stringify({ schema: "atdd-workflow/multiplexer-status/v1", application: "herdr", session, desired: desired.length, present: present.length, topology_compliant: topology.filter(Boolean).length, unprojected }, null, 2));
273
306
  return;
274
307
  }
275
308
  const primary = new Map<string, WorkspaceState>();
@@ -283,5 +316,6 @@ export async function multiplexer(root: string, args: string[]) {
283
316
  }
284
317
  await ensureTarget(session, target, workspaces, command, projectPrimary);
285
318
  }
319
+ for (const entry of unprojected) console.log(`unprojected ${entry.address}: ${entry.reason}`);
286
320
  console.log(`Projected ${desired.length} Desk seat worktree(s) into Herdr session ${session}.`);
287
321
  }
package/src/seats.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, realpathSync } from "node:fs";
2
2
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { createHash } from "node:crypto";
4
4
  import { basename, dirname, join, resolve } from "node:path";
@@ -10,7 +10,7 @@ import {
10
10
  } from "./core";
11
11
  import { type ModelSelectionInput, type ModelSelectionResponse, selectModel } from "./judgment";
12
12
  import { seatTasks } from "./tasks";
13
- import { projectHerdrSeat } from "./multiplexer";
13
+ import { misplacedSeats, projectHerdrSeat } from "./multiplexer";
14
14
  import { isRuntimeStateStale, readRuntimeState } from "./runtime-state";
15
15
  import { resolveExactSessionAdoptionAuthorization } from "./adoption-authorizations";
16
16
 
@@ -68,6 +68,35 @@ export async function initProject(root: string, name: string) {
68
68
  console.log(`Initialized project ${name}`);
69
69
  }
70
70
 
71
+ const configureUsage = "Use `atdd-flow project configure <project> [--repository <primary-checkout>] [--worktree-root <directory>]`.";
72
+
73
+ /**
74
+ * The supported way to declare an existing project's primary checkout and linked-worktree root.
75
+ * Everything is validated before one atomic write; only those two project fields change.
76
+ */
77
+ export async function configureProject(root: string, name: string, args: string[]) {
78
+ const config = await project(root, name);
79
+ const values = new Map<string, string>();
80
+ for (let index = 0; index < args.length; index += 2) {
81
+ const flag = args[index], value = args[index + 1];
82
+ if (!["--repository", "--worktree-root"].includes(flag) || values.has(flag) || !value || value.startsWith("--")) throw new Error(configureUsage);
83
+ values.set(flag, value);
84
+ }
85
+ if (!values.size) throw new Error(configureUsage);
86
+ const repository = values.get("--repository"), worktreeRoot = values.get("--worktree-root");
87
+ if (repository) {
88
+ const path = resolve(repository);
89
+ const real = (value: string) => { try { return realpathSync(value); } catch { return value; } };
90
+ const top = existsSync(path) ? Bun.spawnSync(["git", "-C", path, "rev-parse", "--show-toplevel"], { stdout: "pipe", stderr: "pipe" }) : undefined;
91
+ if (!top || top.exitCode !== 0 || real(top.stdout.toString().trim()) !== real(path)) throw new Error(`${path} is not the top level of a Git repository.`);
92
+ config.repository = path;
93
+ }
94
+ if (worktreeRoot) config.worktree_root = resolve(worktreeRoot);
95
+ await atomicYaml(paths(root).projectFile(name), config);
96
+ for (const entry of await misplacedSeats(root, name)) console.log(`unprojected ${entry.address}: ${entry.reason}`);
97
+ console.log(`${name} configured`);
98
+ }
99
+
71
100
  async function ensureWorktree(config: Project, role: Role, worktree: string, branch: string) {
72
101
  if (!config.repository || worktree === resolve(config.repository) || await exists(worktree)) return;
73
102
  await mkdir(dirname(worktree), { recursive: true });