@afokapu/atdd-flow 0.3.4 → 0.3.6
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 +10 -2
- package/extensions/pi/index.ts +12 -3
- package/package.json +1 -1
- package/src/cli.ts +9 -2
- package/src/tasks.ts +11 -0
- package/src/threads.ts +26 -2
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
|
|
|
@@ -178,7 +178,15 @@ runtime:
|
|
|
178
178
|
wake: native
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
`application` identifies the visible pane. `wake: native` tells Flow not to also send a host prompt; the Pi extension reads `ATDD_WORKFLOW_ROOT` and `ATDD_WORKFLOW_SEAT`, observes final `M-*.yaml` files, filters recipients, and queues a follow-up
|
|
181
|
+
`application` identifies the visible pane. `wake: native` tells Flow not to also send a host prompt; the Pi extension reads `ATDD_WORKFLOW_ROOT` and `ATDD_WORKFLOW_SEAT`, observes final `M-*.yaml` files, filters recipients, and queues a compact follow-up containing the thread subject, sender, recipients, thread/message IDs, and `atdd-flow message read <message-id>`. It never embeds the message body. Other agents keep `wake: host` and receive the same compact notification through their normal host adapter.
|
|
182
|
+
|
|
183
|
+
Read one durable message without loading its complete thread:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
atdd-flow message read M-...
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The output includes only the message and its thread ID/subject; Flow rejects missing or ambiguous message IDs.
|
|
182
190
|
|
|
183
191
|
Pi’s native wake-up is intentionally lightweight: no daemon, duplicate mailbox, or separate extension installation. The extension is shipped inside the Flow package. Its in-memory message-id guard tolerates duplicate filesystem events; the Desk thread files remain the source of truth.
|
|
184
192
|
|
package/extensions/pi/index.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { join } from "node:path";
|
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
|
|
7
|
-
type Thread = { participants?: unknown };
|
|
8
|
-
type Mail = { id?: unknown; from?: unknown; to?: unknown };
|
|
7
|
+
type Thread = { participants?: unknown; subject?: unknown };
|
|
8
|
+
type Mail = { id?: unknown; from?: unknown; to?: unknown; subject?: unknown };
|
|
9
9
|
|
|
10
10
|
const finalMail = /^M-[^.]+\.yaml$/;
|
|
11
11
|
|
|
@@ -21,6 +21,15 @@ export function addressedTo(mail: Mail, thread: Thread, seat: string) {
|
|
|
21
21
|
return mail.to === "all" && participants(thread).includes(seat);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** A compact wake notice; the durable message body is read only on demand. */
|
|
25
|
+
export function mailNotice(threadId: string, mail: Mail) {
|
|
26
|
+
const id = typeof mail.id === "string" ? mail.id : "unknown";
|
|
27
|
+
const subject = typeof mail.subject === "string" ? mail.subject : undefined;
|
|
28
|
+
const from = typeof mail.from === "string" ? mail.from : "unknown";
|
|
29
|
+
const recipients = mail.to === "all" ? "all" : Array.isArray(mail.to) ? mail.to.join(", ") : "unknown";
|
|
30
|
+
return `SYSTEM: Flow mail ${id} | thread ${threadId}${subject ? ` (${subject})` : ""} | ${from} → ${recipients}. Read: atdd-flow message read ${id}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
/**
|
|
25
34
|
* A Pi-native wake adapter. The Desk remains authoritative: this extension
|
|
26
35
|
* only observes final immutable mail files and tells Pi where to read them.
|
|
@@ -55,7 +64,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
55
64
|
delivered.add(key);
|
|
56
65
|
pi.sendMessage({
|
|
57
66
|
customType: "atdd-flow-mail",
|
|
58
|
-
content:
|
|
67
|
+
content: mailNotice(threadId, { ...mail, id, subject: typeof thread.subject === "string" ? thread.subject : undefined }),
|
|
59
68
|
display: true,
|
|
60
69
|
details: { thread: threadId, message: id, path: file },
|
|
61
70
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { basename, resolve } from "node:path";
|
|
4
4
|
import { init, initProject, spawn, bind, useApplication, attach, launch, describe, checkpoint, migrate, openSeat, piExtensionPath } from "./seats";
|
|
5
|
-
import { addParticipant, openThread, post, receipt, result, startThread } from "./threads";
|
|
5
|
+
import { addParticipant, openThread, post, readMessage, receipt, result, startThread } from "./threads";
|
|
6
6
|
import * as tasks from "./tasks";
|
|
7
7
|
import { required, values, words } from "./core";
|
|
8
8
|
import { addressBook } from "./address-book";
|
|
@@ -35,11 +35,13 @@ Usage:
|
|
|
35
35
|
atdd-flow task done <project> <task-id> --by <address> [--retire-assignee]
|
|
36
36
|
atdd-flow task prove <project> <task-id> --by <address> --item <number> --proof <reference>
|
|
37
37
|
atdd-flow task block <project> <task-id> --by <address> --reason <text>
|
|
38
|
+
atdd-flow task unblock <project> <task-id> --by <coordinator-address>
|
|
38
39
|
atdd-flow task list <project> [--coordinator <address>] [--assignee <address>]
|
|
39
40
|
atdd-flow task open <project> <task-id>
|
|
40
41
|
atdd-flow thread start --with <address,...> --subject <text> [--task <project/task-id>]
|
|
41
42
|
atdd-flow thread add <thread-id> <address>
|
|
42
43
|
atdd-flow thread open <thread-id>
|
|
44
|
+
atdd-flow message read <message-id>
|
|
43
45
|
atdd-flow post <thread-id> --from <address> --to <all|address,...> --body <text> [--expects-result]
|
|
44
46
|
atdd-flow receipt <thread-id> <message-id> --from <address> [--body <text>]
|
|
45
47
|
atdd-flow result <thread-id> <message-id> --from <address> --body <text>
|
|
@@ -100,9 +102,10 @@ async function main() {
|
|
|
100
102
|
if (subcommand === "return") return tasks.returnToWork(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
101
103
|
if (subcommand === "prove") return tasks.prove(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
102
104
|
if (subcommand === "block") return tasks.block(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
105
|
+
if (subcommand === "unblock") return tasks.unblock(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
103
106
|
if (subcommand === "list") return tasks.list(root, required(projectName, "project"), rest.slice(2));
|
|
104
107
|
if (subcommand === "open") return tasks.open(root, required(projectName, "project"), required(taskId, "task id"));
|
|
105
|
-
throw new Error("Use `atdd-flow task add|assign|amend|import|start|prove|review|return|done|block|list|open`.");
|
|
108
|
+
throw new Error("Use `atdd-flow task add|assign|amend|import|start|prove|review|return|done|block|unblock|list|open`.");
|
|
106
109
|
},
|
|
107
110
|
thread: async () => {
|
|
108
111
|
const [subcommand, ...tail] = rest;
|
|
@@ -111,6 +114,10 @@ async function main() {
|
|
|
111
114
|
if (subcommand === "open") return openThread(root, required(tail[0], "thread id"));
|
|
112
115
|
throw new Error("Use `atdd-flow thread start|add|open`.");
|
|
113
116
|
},
|
|
117
|
+
message: async () => {
|
|
118
|
+
if (rest[0] === "read") return readMessage(root, required(rest[1], "message id"));
|
|
119
|
+
throw new Error("Use `atdd-flow message read <message-id>`.");
|
|
120
|
+
},
|
|
114
121
|
post: () => post(root, required(rest[0], "thread id"), rest.slice(1)),
|
|
115
122
|
receipt: () => receipt(root, required(rest[0], "thread id"), required(rest[1], "message id"), rest.slice(2)),
|
|
116
123
|
result: () => result(root, required(rest[0], "thread id"), required(rest[1], "message id"), rest.slice(2)),
|
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
|
@@ -81,8 +81,10 @@ async function inject(root: string, address: string, message: Message, threadId:
|
|
|
81
81
|
if (runtime.wake === "native") return;
|
|
82
82
|
const nativeAddress = runtime.addresses[runtime.application];
|
|
83
83
|
if (!nativeAddress) return;
|
|
84
|
-
const
|
|
85
|
-
const
|
|
84
|
+
const record = await thread(root, threadId);
|
|
85
|
+
const recipients = message.to === "all" ? "all" : message.to.join(", ");
|
|
86
|
+
const subject = record.subject.replace(/\s+/g, " ").trim();
|
|
87
|
+
const notice = `SYSTEM: Flow mail ${message.id} | thread ${threadId} (${subject}) | ${message.from} → ${recipients}. Read: atdd-flow message read ${message.id}`;
|
|
86
88
|
try { await notify(runtime.application, nativeAddress, notice, (await desk(root)).herdr_session); }
|
|
87
89
|
catch (error) { console.warn(`Notification for ${address} was not delivered: ${(error as Error).message}`); }
|
|
88
90
|
}
|
|
@@ -132,6 +134,28 @@ export async function openThread(root: string, threadId: string) {
|
|
|
132
134
|
console.log(Bun.YAML.stringify({ ...record, messages: all }));
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
/** Reads one durable message without loading or printing its whole thread. */
|
|
138
|
+
export async function readMessage(root: string, messageId: string) {
|
|
139
|
+
if (!/^M-[A-Za-z0-9_-]+$/.test(messageId)) throw new Error(`Invalid message id: ${messageId}.`);
|
|
140
|
+
const folders = await readdir(paths(root).threads, { withFileTypes: true });
|
|
141
|
+
const matches = await Promise.all(folders
|
|
142
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("T-"))
|
|
143
|
+
.map(async (entry) => {
|
|
144
|
+
const file = paths(root).message(entry.name, messageId);
|
|
145
|
+
try { return { thread: await thread(root, entry.name), message: await readYaml<Message>(file) }; }
|
|
146
|
+
catch { return undefined; }
|
|
147
|
+
}));
|
|
148
|
+
const found = matches.filter((entry): entry is NonNullable<typeof entry> => Boolean(entry));
|
|
149
|
+
if (!found.length) throw new Error(`Message ${messageId} does not exist in this Desk.`);
|
|
150
|
+
if (found.length > 1) throw new Error(`Message ${messageId} is ambiguous across threads: ${found.map((entry) => entry.thread.id).join(", ")}.`);
|
|
151
|
+
const entry = found[0];
|
|
152
|
+
console.log(Bun.YAML.stringify({
|
|
153
|
+
schema: "atdd-workflow/message-read/v1",
|
|
154
|
+
thread: { id: entry.thread.id, subject: entry.thread.subject },
|
|
155
|
+
message: entry.message,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
|
|
135
159
|
export async function receipt(root: string, threadId: string, messageId: string, args: string[]) {
|
|
136
160
|
const from = await canonicalAddress(root, required(words(args, "--from"), "--from"));
|
|
137
161
|
const target = await replyTarget(root, threadId, messageId, from, false);
|