@afokapu/atdd-flow 0.3.33 → 0.3.34
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.
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
schema_version: 1.0.0
|
|
2
|
+
rule_id: atdd-flow.collaboration
|
|
3
|
+
kind: policy
|
|
4
|
+
status: active
|
|
5
|
+
name: Collaborate through durable accountable delegation
|
|
6
|
+
statement: >-
|
|
7
|
+
Agents use accountable delegation, evidence-based escalation, economical delivery,
|
|
8
|
+
and bounded two-way resolution while keeping work within its assigned project and task.
|
|
9
|
+
content:
|
|
10
|
+
canonical_policy: >-
|
|
11
|
+
Delegation flows operator → main → named coordinator → driver. Each level proactively
|
|
12
|
+
dispatches safe independent work, verifies durable delivery and visible execution, and
|
|
13
|
+
escalates material blockers upward with exact evidence, an economical recommendation,
|
|
14
|
+
viable alternatives and tradeoffs, and safe work that can continue. Choose the smallest
|
|
15
|
+
non-duplicative compliant implementation; do not skip lifecycle or gates. Use bounded
|
|
16
|
+
two-way evidence dialogue with the responsible actor to resolve assumptions or
|
|
17
|
+
disagreement; time-box it and escalate only unresolved decisions. Distinguish persisted,
|
|
18
|
+
delivered, acknowledged, awaiting actor, and executing. Never infer authorization or
|
|
19
|
+
restart productive sessions merely to relay policy. Proactive next-task discovery stays
|
|
20
|
+
within the same project. Keep one bounded active task and worktree per driver; assign
|
|
21
|
+
adjacent work separately. Cross-project dialogue is only for a real dependency or
|
|
22
|
+
interface through counterpart authority. Prefer atomic assignment; unassigned work needs
|
|
23
|
+
a project-main decision proposal with purpose, priority, recommended assignee,
|
|
24
|
+
alternatives, consequences, dependencies, and safe work meanwhile. Promptly complete,
|
|
25
|
+
supersede, reject, or time-bound defer work. Coordinating actors reconcile their durable
|
|
26
|
+
inbox as the first action at every session, turn, and task resumption, and after every
|
|
27
|
+
bounded action, phase completion, or wait before the next action. An actionable
|
|
28
|
+
expects_result remains visible until answered, executed, delegated with verified delivery
|
|
29
|
+
and follow-up, or explicitly deferred, superseded, or closed. Messages only communicate,
|
|
30
|
+
evidence, consensus, authorization, or status; they never request executable work unless it
|
|
31
|
+
references an existing assigned task. Tasks define, assign, execute, evidence, and complete
|
|
32
|
+
work; every code or repository mutation requires that task first. The operator accepts and
|
|
33
|
+
receipts inbound communication from any seat, but sends substantive direction, results, and
|
|
34
|
+
wakes only to main@project. Only main@project executes that project repository merge; the
|
|
35
|
+
operator may authorize, relay, or inspect but never merges.
|
|
36
|
+
projection: >-
|
|
37
|
+
Keep this full policy only in this convention. Managed AGENTS.md and CLAUDE.md blocks
|
|
38
|
+
contain only its canonical pointer and preserve all user-authored content outside the
|
|
39
|
+
marked block.
|
|
40
|
+
metadata:
|
|
41
|
+
aliases:
|
|
42
|
+
- ATDD-FLOW-COLLABORATION
|
|
43
|
+
implementation:
|
|
44
|
+
type: source-resolved
|
package/extensions/pi/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
6
6
|
import { clearStaleRuntimeState, clearRuntimeState, heartbeatRuntimeState, ownsRuntimeState, registerRuntimeState, runtimeOwnerToken, withOwnedRuntimeState } from "../../src/runtime-state";
|
|
7
7
|
import { paths, readYaml, type Seat } from "../../src/core";
|
|
8
8
|
import { directiveNotice, nextDirective } from "../../src/tasks";
|
|
9
|
+
import { runtimeStartupInstruction } from "../../src/collaboration-policy";
|
|
9
10
|
|
|
10
11
|
type Thread = { id?: unknown; participants?: unknown; subject?: unknown };
|
|
11
12
|
export type Mail = { id?: unknown; from?: unknown; to?: unknown; subject?: unknown; created_at?: unknown };
|
|
@@ -63,7 +64,6 @@ const defaultHeartbeatMs = 15_000;
|
|
|
63
64
|
const defaultStaleAfterMs = 60_000;
|
|
64
65
|
const defaultActivationIntervalMs = 25;
|
|
65
66
|
const defaultActivationTimeoutMs = 60_000;
|
|
66
|
-
const lifecycleConventionPath = "conventions/atdd-workflow.workflow/atdd-workflow.workflow.lifecycle.convention.yaml";
|
|
67
67
|
|
|
68
68
|
function participants(thread: Thread) {
|
|
69
69
|
return Array.isArray(thread.participants) && thread.participants.every((entry) => typeof entry === "string") ? thread.participants as string[] : [];
|
|
@@ -358,7 +358,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
358
358
|
root, seat, piSession, herdrSession, pane,
|
|
359
359
|
activate: async () => {
|
|
360
360
|
if (!await current.activate()) return;
|
|
361
|
-
pi.sendMessage({ customType: "atdd-flow-start", content:
|
|
361
|
+
pi.sendMessage({ customType: "atdd-flow-start", content: runtimeStartupInstruction(seat), display: true, details: { seat, root } }, { triggerTurn: true, deliverAs: "followUp" });
|
|
362
362
|
await sendTaskDirective(pi, root, seat);
|
|
363
363
|
if (ctx.hasUI) ctx.ui.notify(`ATDD Flow native mail active for ${seat}`, "info");
|
|
364
364
|
},
|
package/package.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parse } from "yaml";
|
|
5
|
+
|
|
6
|
+
export const collaborationConventionPath = "conventions/atdd-flow.collaboration/atdd-flow.collaboration.convention.yaml";
|
|
7
|
+
const startMarker = "<!-- atdd-flow:collaboration-policy:start -->";
|
|
8
|
+
const endMarker = "<!-- atdd-flow:collaboration-policy:end -->";
|
|
9
|
+
export const collaborationPolicyPointer = `ATDD Flow collaboration policy: node_modules/@afokapu/atdd-flow/${collaborationConventionPath} (source checkout: ${collaborationConventionPath})`;
|
|
10
|
+
|
|
11
|
+
type Convention = { content?: { canonical_policy?: unknown } };
|
|
12
|
+
|
|
13
|
+
function conventionFile() {
|
|
14
|
+
return join(import.meta.dir, "..", collaborationConventionPath);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The convention is the sole policy text; projections and runtimes resolve it from here. */
|
|
18
|
+
export function canonicalCollaborationPolicy() {
|
|
19
|
+
const convention = parse(readFileSync(conventionFile(), "utf8")) as Convention;
|
|
20
|
+
const policy = convention.content?.canonical_policy;
|
|
21
|
+
if (typeof policy !== "string" || !policy.trim()) throw new Error("Collaboration convention requires content.canonical_policy.");
|
|
22
|
+
return policy.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function block() {
|
|
26
|
+
return `${startMarker}\n${collaborationPolicyPointer}\n${endMarker}\n`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function escaped(value: string) {
|
|
30
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const existingBlock = new RegExp(`${escaped(startMarker)}\\r?\\n[\\s\\S]*?${escaped(endMarker)}(?:\\r?\\n)?`, "g");
|
|
34
|
+
|
|
35
|
+
/** Replaces every prior managed block with one canonical pointer while retaining all other bytes. */
|
|
36
|
+
export function projectCollaborationPolicyText(source: string) {
|
|
37
|
+
const matches = [...source.matchAll(existingBlock)];
|
|
38
|
+
if (matches.length) {
|
|
39
|
+
const first = matches[0]!;
|
|
40
|
+
const before = source.slice(0, first.index);
|
|
41
|
+
const after = source.slice((first.index ?? 0) + first[0].length).replace(existingBlock, "");
|
|
42
|
+
return `${before}${block()}${after}`;
|
|
43
|
+
}
|
|
44
|
+
if (!source) return block();
|
|
45
|
+
return `${source}${source.endsWith("\n") ? "\n" : "\n\n"}${block()}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function lineCount(text: string) {
|
|
49
|
+
return text ? text.replace(/\r?\n$/, "").split(/\r?\n/).length : 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Idempotently projects the canonical pointer while preserving user content and refusing oversized instruction files. */
|
|
53
|
+
export async function projectCollaborationPolicy(root: string) {
|
|
54
|
+
const projections = await Promise.all(["AGENTS.md", "CLAUDE.md"].map(async (name) => {
|
|
55
|
+
const file = join(root, name);
|
|
56
|
+
let source = "";
|
|
57
|
+
try { source = await readFile(file, "utf8"); }
|
|
58
|
+
catch (error) {
|
|
59
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
60
|
+
}
|
|
61
|
+
return { name, file, source, next: projectCollaborationPolicyText(source) };
|
|
62
|
+
}));
|
|
63
|
+
for (const { name, source, next } of projections) {
|
|
64
|
+
if (lineCount(source) > 20 || lineCount(next) > 20) {
|
|
65
|
+
throw new Error(`${name} exceeds the 20-line cap; refusing projection without changing user content.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
await Promise.all(projections.map(async ({ file, source, next }) => {
|
|
69
|
+
if (next !== source) await writeFile(file, next, "utf8");
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The one startup handoff for supported Flow-managed runtimes, resolved from the canonical convention. */
|
|
74
|
+
export function runtimeStartupInstruction(seat: string) {
|
|
75
|
+
return `SYSTEM: you are ${seat}. Collaboration policy: ${canonicalCollaborationPolicy()} Canonical convention: ${collaborationConventionPath}. Read your durable seat and assigned work with: atdd-flow open ${seat}. Continue assigned in_progress work until it is review-ready or explicitly blocked.`;
|
|
76
|
+
}
|
package/src/seats.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { seatTasks } from "./tasks";
|
|
|
13
13
|
import { misplacedSeats, projectHerdrSeat } from "./multiplexer";
|
|
14
14
|
import { isRuntimeStateStale, readRuntimeState } from "./runtime-state";
|
|
15
15
|
import { resolveExactSessionAdoptionAuthorization } from "./adoption-authorizations";
|
|
16
|
+
import { projectCollaborationPolicy } from "./collaboration-policy";
|
|
16
17
|
|
|
17
18
|
const lifecycleConventionPath = "conventions/atdd-workflow.workflow/atdd-workflow.workflow.lifecycle.convention.yaml";
|
|
18
19
|
|
|
@@ -51,6 +52,7 @@ export async function init(root: string, name: string, args: string[]) {
|
|
|
51
52
|
const config = { schema: "atdd-workflow/desk/v1" as const, desk: name, application: "herdr", executables: defaultExecutables() };
|
|
52
53
|
await Promise.all([mkdir(paths(root).work, { recursive: true }), mkdir(paths(root).threads, { recursive: true })]);
|
|
53
54
|
await Promise.all([atomicYaml(paths(root).desk, config), atomicYaml(paths(root).models, defaultModels())]);
|
|
55
|
+
await projectCollaborationPolicy(root);
|
|
54
56
|
if (args.includes("--git") && !await exists(join(root, ".git"))) await run(["git", "init", "--initial-branch=main", root]);
|
|
55
57
|
console.log(`Initialized Desk ${root}`);
|
|
56
58
|
}
|
|
@@ -58,6 +60,7 @@ export async function init(root: string, name: string, args: string[]) {
|
|
|
58
60
|
export async function migrate(root: string) {
|
|
59
61
|
if (await migrateDesk(root)) console.log("Migrated legacy coordination registry to desk.yaml");
|
|
60
62
|
else console.log("Desk registry already exists");
|
|
63
|
+
await projectCollaborationPolicy(root);
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
export async function initProject(root: string, name: string) {
|