@afokapu/atdd-flow 0.3.3 → 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.
|
@@ -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";
|
|
@@ -49,6 +50,7 @@ Usage:
|
|
|
49
50
|
atdd-flow behavioral-review record <project> <task-id> --by <reviewer-address> --file <result-yaml>
|
|
50
51
|
atdd-flow behavioral-review open <project> <task-id>
|
|
51
52
|
atdd-flow open <address>
|
|
53
|
+
atdd-flow address-book
|
|
52
54
|
|
|
53
55
|
Global:
|
|
54
56
|
atdd-flow --root <desk-directory> <command>
|
|
@@ -132,6 +134,7 @@ async function main() {
|
|
|
132
134
|
throw new Error("Use `atdd-flow behavioral-review launch|record|open <project> <task-id>`.");
|
|
133
135
|
},
|
|
134
136
|
open: () => openSeat(root, required(rest[0], "address")),
|
|
137
|
+
"address-book": () => addressBook(root),
|
|
135
138
|
};
|
|
136
139
|
const action = commands[command];
|
|
137
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/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) {
|