@afokapu/atdd-flow 0.3.3 → 0.3.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
|
@@ -130,7 +130,7 @@ atdd-flow behavioral-review launch resolver-os runtime-rollout --by coordinator@
|
|
|
130
130
|
atdd-flow task done resolver-os runtime-rollout --by coordinator@resolver-os
|
|
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. 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.
|
|
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.
|
|
134
134
|
|
|
135
135
|
## Communicate and hand over
|
|
136
136
|
|
|
@@ -62,6 +62,13 @@ content:
|
|
|
62
62
|
Start from the current seat, checkpoint, assigned tasks, and relevant thread records. A replacement
|
|
63
63
|
agent resumes from those records; it does not depend on a conversation with its predecessor.
|
|
64
64
|
|
|
65
|
+
`operator@desk` is the human authority, not an autonomous agent identity. Never post, approve,
|
|
66
|
+
or make a decision while claiming that address unless the human operator actually supplied it.
|
|
67
|
+
Main seats are the normal repository-main integration gateway to the operator. Coordinators may
|
|
68
|
+
escalate a concrete decision or incident directly to the operator. Drivers send normal work
|
|
69
|
+
messages to their coordinator; they do not directly message the operator or main seat. A receipt
|
|
70
|
+
or result addresses the seat that made the request, not every historical participant in the thread.
|
|
71
|
+
|
|
65
72
|
A driver starts only its assigned ready task. Treat the task's done_when criteria as the required
|
|
66
73
|
delivery scope. Prefer existing repository patterns, components, dependencies, contracts, and
|
|
67
74
|
abstractions. When multiple implementations are correct, choose the smallest effective change that
|
package/package.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { paths, readYaml, type Seat } from "./core";
|
|
3
|
+
|
|
4
|
+
function identity(address: string) {
|
|
5
|
+
return address === "operator@desk" ? "human" : "agent";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function authority(record: Seat) {
|
|
9
|
+
if (record.address === "operator@desk") return "Human authority; no autonomous agent may post as this address.";
|
|
10
|
+
if (record.role === "main") return "Repository-main integration gateway; may escalate to operator@desk.";
|
|
11
|
+
if (record.role === "coordinator") return "Coordinates assigned work; may escalate a decision or incident to operator@desk.";
|
|
12
|
+
if (record.role === "driver") return "Delivers assigned work; routes normal communication through its coordinator.";
|
|
13
|
+
return "See the seat purpose and current assignment.";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function addressBook(root: string) {
|
|
17
|
+
const projects = (await readdir(paths(root).work)).sort();
|
|
18
|
+
const records: Seat[] = [];
|
|
19
|
+
for (const project of projects) {
|
|
20
|
+
const folder = paths(root).seats(project);
|
|
21
|
+
let locals: string[];
|
|
22
|
+
try { locals = await readdir(folder); }
|
|
23
|
+
catch { continue; }
|
|
24
|
+
for (const local of locals.sort()) {
|
|
25
|
+
try { records.push(await readYaml<Seat>(`${folder}/${local}/seat.yaml`)); }
|
|
26
|
+
catch { /* A partially removed legacy seat is not an address-book entry. */ }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
console.log("# Desk address book\n");
|
|
30
|
+
console.log("`operator@desk` is the human authority. Drivers route normal work through coordinators; main seats are the normal technical gateway to the operator.\n");
|
|
31
|
+
console.log("| Address | Identity | Purpose / authority |\n| --- | --- | --- |");
|
|
32
|
+
for (const record of records.sort((left, right) => left.address.localeCompare(right.address))) {
|
|
33
|
+
const purpose = (record.purpose ?? authority(record)).replaceAll("|", "\\|");
|
|
34
|
+
console.log(`| ${record.address} | ${identity(record.address)} | ${purpose} |`);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { init, initProject, spawn, bind, useApplication, attach, launch, describ
|
|
|
5
5
|
import { addParticipant, openThread, post, receipt, result, startThread } from "./threads";
|
|
6
6
|
import * as tasks from "./tasks";
|
|
7
7
|
import { required, values, words } from "./core";
|
|
8
|
+
import { addressBook } from "./address-book";
|
|
8
9
|
import * as judgment from "./judgment";
|
|
9
10
|
import * as reviews from "./reviews";
|
|
10
11
|
import { status } from "./overview";
|
|
@@ -34,6 +35,7 @@ Usage:
|
|
|
34
35
|
atdd-flow task done <project> <task-id> --by <address> [--retire-assignee]
|
|
35
36
|
atdd-flow task prove <project> <task-id> --by <address> --item <number> --proof <reference>
|
|
36
37
|
atdd-flow task block <project> <task-id> --by <address> --reason <text>
|
|
38
|
+
atdd-flow task unblock <project> <task-id> --by <coordinator-address>
|
|
37
39
|
atdd-flow task list <project> [--coordinator <address>] [--assignee <address>]
|
|
38
40
|
atdd-flow task open <project> <task-id>
|
|
39
41
|
atdd-flow thread start --with <address,...> --subject <text> [--task <project/task-id>]
|
|
@@ -49,6 +51,7 @@ Usage:
|
|
|
49
51
|
atdd-flow behavioral-review record <project> <task-id> --by <reviewer-address> --file <result-yaml>
|
|
50
52
|
atdd-flow behavioral-review open <project> <task-id>
|
|
51
53
|
atdd-flow open <address>
|
|
54
|
+
atdd-flow address-book
|
|
52
55
|
|
|
53
56
|
Global:
|
|
54
57
|
atdd-flow --root <desk-directory> <command>
|
|
@@ -98,9 +101,10 @@ async function main() {
|
|
|
98
101
|
if (subcommand === "return") return tasks.returnToWork(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
99
102
|
if (subcommand === "prove") return tasks.prove(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
100
103
|
if (subcommand === "block") return tasks.block(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
104
|
+
if (subcommand === "unblock") return tasks.unblock(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
101
105
|
if (subcommand === "list") return tasks.list(root, required(projectName, "project"), rest.slice(2));
|
|
102
106
|
if (subcommand === "open") return tasks.open(root, required(projectName, "project"), required(taskId, "task id"));
|
|
103
|
-
throw new Error("Use `atdd-flow task add|assign|amend|import|start|prove|review|return|done|block|list|open`.");
|
|
107
|
+
throw new Error("Use `atdd-flow task add|assign|amend|import|start|prove|review|return|done|block|unblock|list|open`.");
|
|
104
108
|
},
|
|
105
109
|
thread: async () => {
|
|
106
110
|
const [subcommand, ...tail] = rest;
|
|
@@ -132,6 +136,7 @@ async function main() {
|
|
|
132
136
|
throw new Error("Use `atdd-flow behavioral-review launch|record|open <project> <task-id>`.");
|
|
133
137
|
},
|
|
134
138
|
open: () => openSeat(root, required(rest[0], "address")),
|
|
139
|
+
"address-book": () => addressBook(root),
|
|
135
140
|
};
|
|
136
141
|
const action = commands[command];
|
|
137
142
|
if (!action) throw new Error(`Unknown command: ${command}`);
|
package/src/seats.ts
CHANGED
|
@@ -179,7 +179,7 @@ async function chooseLaunchModel(root: string, config: Desk, record: Seat, portf
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
export const launchNotice = (address: string) =>
|
|
182
|
-
`SYSTEM: you are ${address}. Read your durable seat and assigned task with: atdd-flow open ${address}. Use the installed atdd-flow command; never use bunx to replace it or run atdd-flow init against an existing Desk.
|
|
182
|
+
`SYSTEM: you are ${address}. Read your durable seat and assigned task with: atdd-flow open ${address}. Use the installed atdd-flow command; never use bunx to replace it or run atdd-flow init against an existing Desk. Routing: operator@desk is the human authority; drivers report to coordinators, and main seats are the normal technical gateway to the operator. Continue assigned in_progress work until it is review-ready or explicitly blocked.`;
|
|
183
183
|
|
|
184
184
|
/** Pi loads this extension inside its own process, so it can wake without host text injection. */
|
|
185
185
|
export const piExtensionPath = () => join(import.meta.dir, "..", "extensions", "pi", "index.ts");
|
package/src/tasks.ts
CHANGED
|
@@ -240,6 +240,17 @@ export async function block(root: string, projectName: string, id: string, args:
|
|
|
240
240
|
console.log(`${id} blocked`);
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
export async function unblock(root: string, projectName: string, id: string, args: string[]) {
|
|
244
|
+
const task = await readTask(root, projectName, taskId(id));
|
|
245
|
+
const actor = await canonicalAddress(root, required(words(args, "--by"), "--by"));
|
|
246
|
+
if (actor !== task.coordinator) throw new Error(`Only ${task.coordinator} may unblock task ${id}.`);
|
|
247
|
+
if (task.status !== "in_progress") throw new Error(`Task ${id} can only be unblocked while in_progress.`);
|
|
248
|
+
if (!task.blocker) throw new Error(`Task ${id} has no blocker to clear.`);
|
|
249
|
+
delete task.blocker;
|
|
250
|
+
await writeTask(root, projectName, id, task);
|
|
251
|
+
console.log(`${id} unblocked`);
|
|
252
|
+
}
|
|
253
|
+
|
|
243
254
|
export async function list(root: string, projectName: string, args: string[]) {
|
|
244
255
|
const coordinator = words(args, "--coordinator") ? await canonicalAddress(root, required(words(args, "--coordinator"), "--coordinator")) : undefined;
|
|
245
256
|
const assignee = words(args, "--assignee") ? await canonicalAddress(root, required(words(args, "--assignee"), "--assignee")) : undefined;
|
package/src/threads.ts
CHANGED
|
@@ -36,6 +36,7 @@ async function replyTarget(root: string, threadId: string, messageId: string, fr
|
|
|
36
36
|
const recipients = target.to === "all" ? record.participants.filter((address) => address !== target.from) : target.to;
|
|
37
37
|
if (!recipients.includes(from)) throw new Error(`${from} was not a recipient of message ${messageId}.`);
|
|
38
38
|
if (needsResult && !target.expects_result) throw new Error(`Message ${messageId} does not expect a result.`);
|
|
39
|
+
return target;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
async function seatsByRole(root: string, projectName: string, role: string) {
|
|
@@ -61,6 +62,18 @@ async function resolveRecipients(root: string, value: string, participants: stri
|
|
|
61
62
|
return [...new Set(expanded.flat())];
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
async function assertRoute(root: string, from: string, recipients: string[]) {
|
|
66
|
+
if (from === "operator@desk") return;
|
|
67
|
+
const sender = await seat(root, from);
|
|
68
|
+
const targets = await Promise.all(recipients.map((address) => seat(root, address)));
|
|
69
|
+
if (targets.some((target) => target.address === "operator@desk") && !["main", "coordinator"].includes(sender.role)) {
|
|
70
|
+
throw new Error(`${from} may not directly address operator@desk; route through the responsible coordinator and main seat.`);
|
|
71
|
+
}
|
|
72
|
+
if (sender.role === "driver" && targets.some((target) => target.role !== "coordinator")) {
|
|
73
|
+
throw new Error(`${from} may message only a coordinator; coordinators and main seats handle further escalation.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
async function inject(root: string, address: string, message: Message, threadId: string) {
|
|
65
78
|
const target = await seat(root, address);
|
|
66
79
|
const runtime = target.runtime;
|
|
@@ -81,6 +94,7 @@ async function post(root: string, threadId: string, args: string[], overrides: P
|
|
|
81
94
|
const toValue = overrides.to ?? words(args, "--to") ?? "all";
|
|
82
95
|
const recipients = Array.isArray(toValue) ? toValue : await resolveRecipients(root, toValue, record.participants);
|
|
83
96
|
if (recipients.some((address) => !record.participants.includes(address))) throw new Error("Recipients must be thread participants.");
|
|
97
|
+
await assertRoute(root, from, recipients.filter((address) => address !== from));
|
|
84
98
|
const message: Message = {
|
|
85
99
|
schema: "atdd-workflow/message/v1", id: id("M"), from, to: toValue === "all" ? "all" : recipients,
|
|
86
100
|
kind: overrides.kind ?? "message",
|
|
@@ -120,14 +134,14 @@ export async function openThread(root: string, threadId: string) {
|
|
|
120
134
|
|
|
121
135
|
export async function receipt(root: string, threadId: string, messageId: string, args: string[]) {
|
|
122
136
|
const from = await canonicalAddress(root, required(words(args, "--from"), "--from"));
|
|
123
|
-
await replyTarget(root, threadId, messageId, from, false);
|
|
124
|
-
return post(root, threadId, args, { from, kind: "receipt", in_reply_to: messageId, body: words(args, "--body") ?? `Received ${messageId}.` });
|
|
137
|
+
const target = await replyTarget(root, threadId, messageId, from, false);
|
|
138
|
+
return post(root, threadId, args, { from, to: [target.from], kind: "receipt", in_reply_to: messageId, body: words(args, "--body") ?? `Received ${messageId}.` });
|
|
125
139
|
}
|
|
126
140
|
|
|
127
141
|
export async function result(root: string, threadId: string, messageId: string, args: string[]) {
|
|
128
142
|
const from = await canonicalAddress(root, required(words(args, "--from"), "--from"));
|
|
129
|
-
await replyTarget(root, threadId, messageId, from, true);
|
|
130
|
-
return post(root, threadId, args, { from, kind: "result", in_reply_to: messageId, body: required(words(args, "--body"), "--body") });
|
|
143
|
+
const target = await replyTarget(root, threadId, messageId, from, true);
|
|
144
|
+
return post(root, threadId, args, { from, to: [target.from], kind: "result", in_reply_to: messageId, body: required(words(args, "--body"), "--body") });
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
export async function status(root: string) {
|