@afokapu/atdd-flow 0.3.29 → 0.3.31
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/package.json +1 -1
- package/src/multiplexer.ts +15 -7
- package/src/threads.ts +8 -0
package/package.json
CHANGED
package/src/multiplexer.ts
CHANGED
|
@@ -191,15 +191,23 @@ async function liveWorkspaces(session: string, command: HerdrCommand) {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
/** A live workspace without a checkout path never matches: resolve("") would be the process cwd. */
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Herdr may list a primary workspace created for a non-code checkout with no checkout path. Such a
|
|
196
|
+
* workspace matches only the primary target, and only by its exact project label (so it is never
|
|
197
|
+
* renamed); linked seat targets always require a checkout path.
|
|
198
|
+
*/
|
|
199
|
+
function matchWorkspace(workspaces: Workspace[], target: Target) {
|
|
200
|
+
const checkoutOf = (entry: Workspace) => {
|
|
196
201
|
const checkout = entry.worktree?.checkout_path;
|
|
197
|
-
return typeof checkout === "string" && checkout !== ""
|
|
198
|
-
}
|
|
202
|
+
return typeof checkout === "string" && checkout !== "" ? checkout : undefined;
|
|
203
|
+
};
|
|
204
|
+
const byCheckout = workspaces.find((entry) => { const checkout = checkoutOf(entry); return checkout !== undefined && resolve(checkout) === target.worktree; });
|
|
205
|
+
if (byCheckout || target.workspaceLabel !== target.project) return byCheckout;
|
|
206
|
+
return workspaces.find((entry) => checkoutOf(entry) === undefined && entry.label === target.project);
|
|
199
207
|
}
|
|
200
208
|
|
|
201
209
|
async function ensureWorkspace(session: string, target: Target, workspaces: Workspace[], command: HerdrCommand, primary?: WorkspaceState) {
|
|
202
|
-
let current = matchWorkspace(workspaces, target
|
|
210
|
+
let current = matchWorkspace(workspaces, target);
|
|
203
211
|
if (!current) {
|
|
204
212
|
const args = target.workspaceLabel === target.project
|
|
205
213
|
? ["workspace", "create", "--cwd", target.worktree, "--label", target.workspaceLabel, "--no-focus"]
|
|
@@ -297,9 +305,9 @@ export async function multiplexer(root: string, args: string[]) {
|
|
|
297
305
|
const command: HerdrCommand = herdr;
|
|
298
306
|
const workspaces = await liveWorkspaces(session, command);
|
|
299
307
|
if (action === "status") {
|
|
300
|
-
const present = desired.filter((target) => matchWorkspace(workspaces, target
|
|
308
|
+
const present = desired.filter((target) => matchWorkspace(workspaces, target));
|
|
301
309
|
const topology = await Promise.all(desired.map(async (target) => {
|
|
302
|
-
const workspace = matchWorkspace(workspaces, target
|
|
310
|
+
const workspace = matchWorkspace(workspaces, target);
|
|
303
311
|
return workspace ? topologyCompliant(session, target, workspace, command) : false;
|
|
304
312
|
}));
|
|
305
313
|
console.log(JSON.stringify({ schema: "atdd-workflow/multiplexer-status/v1", application: "herdr", session, desired: desired.length, present: present.length, topology_compliant: topology.filter(Boolean).length, unprojected }, null, 2));
|
package/src/threads.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readdir, rm, stat } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { notify } from "./adapters";
|
|
4
|
+
import { seatTasks } from "./tasks";
|
|
4
5
|
import {
|
|
5
6
|
atomicYaml, canonicalAddress, desk, has, id, now, paths, project, readYaml,
|
|
6
7
|
required, runtimeAddress, seat, words,
|
|
@@ -82,6 +83,13 @@ async function assertRoute(root: string, from: string, recipients: string[]) {
|
|
|
82
83
|
throw new Error(`${from} may not directly address operator@desk; route through the responsible coordinator and main seat.`);
|
|
83
84
|
}
|
|
84
85
|
if (sender.role === "driver" && targets.some((target) => target.role !== "coordinator")) {
|
|
86
|
+
// A driver may also reach the actual coordinator of its own non-done task in the same project (e.g. main).
|
|
87
|
+
const owners = new Set((await seatTasks(root, sender.project, from))
|
|
88
|
+
.filter(({ task }) => task.assignee === from && task.status !== "done")
|
|
89
|
+
.map(({ task }) => task.coordinator));
|
|
90
|
+
const permitted = (target: { address: string; role: string; project: string }) =>
|
|
91
|
+
target.role === "coordinator" || (target.project === sender.project && owners.has(target.address));
|
|
92
|
+
if (targets.every(permitted)) return;
|
|
85
93
|
throw new Error(`${from} may message only a coordinator; coordinators and main seats handle further escalation.`);
|
|
86
94
|
}
|
|
87
95
|
}
|