@afokapu/atdd-flow 0.3.32 → 0.3.33

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/seats.ts +30 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-flow",
3
- "version": "0.3.32",
3
+ "version": "0.3.33",
4
4
  "description": "Filesystem-first Desk registry for projects, seats, tasks, threads, and handoffs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/seats.ts CHANGED
@@ -218,10 +218,37 @@ export function resolveModelCommand(config: Desk, candidate: ModelCandidate) {
218
218
  return { agent: resolveExecutable(config, candidate.executable), args: candidate.args ?? [] };
219
219
  }
220
220
 
221
- async function launchEligibleTasks(root: string, record: Seat) {
221
+ async function isExactNamedCoordinator(root: string, record: Seat, address: string) {
222
+ if (record.address !== address || record.role !== "coordinator") return false;
223
+ const config = await project(root, record.project);
224
+ const role = config.roles.coordinator;
225
+ const suffix = `@${config.project}`;
226
+ const local = record.address.endsWith(suffix) ? record.address.slice(0, -suffix.length) : "";
227
+ const match = local.match(/^coordinator\.([a-z0-9_-]+)$/);
228
+ if (!config.repository || role?.address !== "coordinator.{name}@{project}" || role.branch !== "integration/{name}" || !role.worktree || !match) return false;
229
+ const stream = match[1]!;
230
+ const expectedWorktree = resolve(fill(role.worktree, {
231
+ project: config.project, name: stream, worktree_root: config.worktree_root ?? "", repository: config.repository,
232
+ }));
233
+ if (record.branch !== `integration/${stream}` || resolve(record.worktree) !== expectedWorktree || expectedWorktree === resolve(config.repository)) return false;
234
+
235
+ try {
236
+ const peers = await Promise.all((await readdir(paths(root).seats(config.project))).map(async (name) => {
237
+ try { return await readYaml<Seat>(join(paths(root).seats(config.project), name, "seat.yaml")); }
238
+ catch { return undefined; }
239
+ }));
240
+ return !peers.some((peer) => peer && peer.address !== record.address && peer.role === "coordinator"
241
+ && (peer.branch === record.branch || resolve(peer.worktree) === expectedWorktree));
242
+ } catch { return false; }
243
+ }
244
+
245
+ async function launchEligibleTasks(root: string, record: Seat, address: string) {
246
+ const coordinator = await isExactNamedCoordinator(root, record, address);
222
247
  const tasks = await seatTasks(root, record.project, record.address);
223
248
  const eligible = await Promise.all(tasks.map(async (entry) => {
224
- if (entry.task.assignee !== record.address || entry.task.blocker) return undefined;
249
+ const accountable = record.role === "driver" ? entry.task.assignee === record.address
250
+ : coordinator && entry.task.coordinator === record.address;
251
+ if (!accountable || entry.task.blocker) return undefined;
225
252
  if (entry.task.status === "in_progress") return entry;
226
253
  if (entry.task.status !== "todo") return undefined;
227
254
  const dependencies = await Promise.all((entry.task.depends_on ?? []).map(async (id) => {
@@ -357,7 +384,7 @@ export async function launchPiRuntime(root: string, address: string, args: strin
357
384
  if (resume && ((requestedPane && requestedPane !== existingPane) || herdrSession !== existingSession)) {
358
385
  throw new Error("Resume cannot relocate a Pi session across a Herdr session or pane; use explicit exact-session adoption.");
359
386
  }
360
- const active = await launchEligibleTasks(root, record);
387
+ const active = await launchEligibleTasks(root, record, resolved);
361
388
  if (!active.length) throw new Error(`${resolved} has no launch-eligible task; refusing runtime launch.`);
362
389
  const selection = await chooseLaunchModel(root, config, record, required(await modelPortfolio(root), "models.yaml for Pi runtime launch"), dependencies.select);
363
390
  const model = resolveModelCommand(config, selection.candidate);