@afokapu/atdd-flow 0.3.2 → 0.3.4
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. 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. 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";
|
|
@@ -27,6 +28,7 @@ Usage:
|
|
|
27
28
|
atdd-flow describe <address> --purpose <one-line responsibility>
|
|
28
29
|
atdd-flow checkpoint <address> --summary <text> --next <text> [--status active|standby|blocked|complete|unverified]
|
|
29
30
|
atdd-flow task add <project> <task-id> --title <text> --coordinator <address> [--assignee <address>] [--body <text>] [--source <reference>] [--depends-on <task-id,...>] --done-when <text> [--done-when <text> ...]
|
|
31
|
+
atdd-flow task assign <project> <task-id> --assignee <address> --by <coordinator-address>
|
|
30
32
|
atdd-flow task amend <project> <task-id> [--title <text>] [--body <text>] [--source <reference>] [--depends-on <task-id,...>]
|
|
31
33
|
atdd-flow task import <project> <task-id> --proof <reference> [--proof <reference> ...] [--done-when <text> ...] [--source <reference>]
|
|
32
34
|
atdd-flow task start|review|return <project> <task-id> --by <address>
|
|
@@ -48,6 +50,7 @@ Usage:
|
|
|
48
50
|
atdd-flow behavioral-review record <project> <task-id> --by <reviewer-address> --file <result-yaml>
|
|
49
51
|
atdd-flow behavioral-review open <project> <task-id>
|
|
50
52
|
atdd-flow open <address>
|
|
53
|
+
atdd-flow address-book
|
|
51
54
|
|
|
52
55
|
Global:
|
|
53
56
|
atdd-flow --root <desk-directory> <command>
|
|
@@ -88,6 +91,7 @@ async function main() {
|
|
|
88
91
|
task: async () => {
|
|
89
92
|
const [subcommand, projectName, taskId, ...tail] = rest;
|
|
90
93
|
if (subcommand === "add") return tasks.add(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
94
|
+
if (subcommand === "assign") return tasks.assign(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
91
95
|
if (subcommand === "amend") return tasks.amend(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
92
96
|
if (subcommand === "import") return tasks.importCompleted(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
93
97
|
if (subcommand === "start") return tasks.start(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
@@ -98,7 +102,7 @@ async function main() {
|
|
|
98
102
|
if (subcommand === "block") return tasks.block(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
99
103
|
if (subcommand === "list") return tasks.list(root, required(projectName, "project"), rest.slice(2));
|
|
100
104
|
if (subcommand === "open") return tasks.open(root, required(projectName, "project"), required(taskId, "task id"));
|
|
101
|
-
throw new Error("Use `atdd-flow task add|amend|import|start|prove|review|return|done|block|list|open`.");
|
|
105
|
+
throw new Error("Use `atdd-flow task add|assign|amend|import|start|prove|review|return|done|block|list|open`.");
|
|
102
106
|
},
|
|
103
107
|
thread: async () => {
|
|
104
108
|
const [subcommand, ...tail] = rest;
|
|
@@ -130,6 +134,7 @@ async function main() {
|
|
|
130
134
|
throw new Error("Use `atdd-flow behavioral-review launch|record|open <project> <task-id>`.");
|
|
131
135
|
},
|
|
132
136
|
open: () => openSeat(root, required(rest[0], "address")),
|
|
137
|
+
"address-book": () => addressBook(root),
|
|
133
138
|
};
|
|
134
139
|
const action = commands[command];
|
|
135
140
|
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
|
@@ -116,6 +116,19 @@ export async function add(root: string, projectName: string, id: string, args: s
|
|
|
116
116
|
console.log(id);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
export async function assign(root: string, projectName: string, id: string, args: string[]) {
|
|
120
|
+
const task = await readTask(root, projectName, taskId(id));
|
|
121
|
+
const actor = await canonicalAddress(root, required(words(args, "--by"), "--by"));
|
|
122
|
+
if (actor !== task.coordinator) throw new Error(`Only ${task.coordinator} may assign task ${id}.`);
|
|
123
|
+
if (task.status !== "todo") throw new Error(`Task ${id} can only be assigned while todo.`);
|
|
124
|
+
if (task.assignee) throw new Error(`Task ${id} is already assigned to ${task.assignee}.`);
|
|
125
|
+
const assignee = await canonicalAddress(root, required(words(args, "--assignee"), "--assignee"));
|
|
126
|
+
await seat(root, assignee);
|
|
127
|
+
task.assignee = assignee;
|
|
128
|
+
await writeTask(root, projectName, id, task);
|
|
129
|
+
console.log(`${id} assigned ${assignee}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
119
132
|
export async function amend(root: string, projectName: string, id: string, args: string[]) {
|
|
120
133
|
const task = await readTask(root, projectName, taskId(id));
|
|
121
134
|
const title = words(args, "--title");
|
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) {
|