agent-dag 1.34.6 → 1.35.0
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 +3 -1
- package/bin/deck.js +13 -3
- package/dist/web/assets/{index-G1Xt6iC6.js → index-CMZYhML1.js} +1 -1
- package/dist/web/index.html +1 -1
- package/hook/hook.js +64 -21
- package/package.json +1 -1
- package/src/server/index.mjs +32 -1
- package/src/server/log-writer.mjs +34 -19
package/dist/web/index.html
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
document.documentElement.setAttribute("data-theme", stored === "light" ? "light" : "dark");
|
|
41
41
|
})();
|
|
42
42
|
</script>
|
|
43
|
-
<script type="module" crossorigin src="/assets/index-
|
|
43
|
+
<script type="module" crossorigin src="/assets/index-CMZYhML1.js"></script>
|
|
44
44
|
<link rel="stylesheet" crossorigin href="/assets/index-CRPobBZf.css">
|
|
45
45
|
</head>
|
|
46
46
|
<body>
|
package/hook/hook.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// agent-dag hook forwarder. Invoked by Claude Code or Codex CLI as a command
|
|
3
3
|
// hook. Reads stdin (event JSON), tags it with the provider passed via
|
|
4
|
-
// `--provider <name>`, finds
|
|
5
|
-
// discovery files in <claude config dir>/agent-dag
|
|
6
|
-
// is the deck
|
|
7
|
-
// cleaned up.
|
|
4
|
+
// `--provider <name>`, finds every agent-dag server whose workspace contains the
|
|
5
|
+
// session — via the discovery files in <claude config dir>/agent-dag/ — makes
|
|
6
|
+
// each one prove it is the deck its file describes, and POSTs the payload. Dead
|
|
7
|
+
// instances are cleaned up.
|
|
8
8
|
"use strict";
|
|
9
9
|
|
|
10
10
|
const fs = require("fs");
|
|
@@ -86,6 +86,36 @@ function cwdInWorkspace(cwd, workspace, platform = process.platform) {
|
|
|
86
86
|
return a.startsWith(b.endsWith(p.sep) ? b : b + p.sep);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Does a deck scoped to `workspace` capture a session running in `cwd`? This is
|
|
91
|
+
* the whole of what `--workspace` means, and it is a question about ONE deck: it
|
|
92
|
+
* asks nothing about the others that may also be up, so a deck's answer never
|
|
93
|
+
* depends on who else is running.
|
|
94
|
+
*
|
|
95
|
+
* An empty workspace is the default — machine-wide — and captures everything.
|
|
96
|
+
* It is answered before cwdInWorkspace rather than passed to it because
|
|
97
|
+
* p.resolve("") is the resolving process's own cwd, which here is the agent's,
|
|
98
|
+
* so an unscoped deck would be silently scoped to whatever directory the user
|
|
99
|
+
* happened to run their agent in.
|
|
100
|
+
*
|
|
101
|
+
* A session that never said where it runs is inside no workspace, so only an
|
|
102
|
+
* unscoped deck sees it. Unreachable from main(), which exits before this on a
|
|
103
|
+
* payload with no cwd — it is here because the rule has to be stated the same
|
|
104
|
+
* way on both sides to be pinned against the other one.
|
|
105
|
+
*
|
|
106
|
+
* src/server/log-writer.mjs answers this same question, for the sessions the
|
|
107
|
+
* server builds itself out of Codex's rollout files, under the name
|
|
108
|
+
* codexCwdInWorkspace — this script is copied out of the package and run
|
|
109
|
+
* standalone, so it cannot import that copy. A test walks one table of paths
|
|
110
|
+
* through both: a disagreement between them is `--workspace` meaning two
|
|
111
|
+
* different things depending on which CLI produced the session.
|
|
112
|
+
*/
|
|
113
|
+
function capturesSession(cwd, workspace, platform = process.platform) {
|
|
114
|
+
if (!workspace || typeof workspace !== "string") return true;
|
|
115
|
+
if (!cwd || typeof cwd !== "string") return false;
|
|
116
|
+
return cwdInWorkspace(cwd, workspace, platform);
|
|
117
|
+
}
|
|
118
|
+
|
|
89
119
|
function isAlive(pid) {
|
|
90
120
|
try { process.kill(pid, 0); return true; }
|
|
91
121
|
catch (e) { return e && e.code === "EPERM"; }
|
|
@@ -315,7 +345,24 @@ function main() {
|
|
|
315
345
|
} catch { return process.exit(0); }
|
|
316
346
|
if (!files.length) return process.exit(0);
|
|
317
347
|
|
|
318
|
-
|
|
348
|
+
// Every deck whose workspace contains this cwd, and nothing else decides it.
|
|
349
|
+
//
|
|
350
|
+
// This used to sort the matches by how long each deck's workspace path was
|
|
351
|
+
// and deliver only to the longest — so a deck scoped to /Users/x/proj TOOK
|
|
352
|
+
// that tree's sessions away from a machine-wide deck, which then sat there
|
|
353
|
+
// showing nothing while `--all` promised it captured every session on this
|
|
354
|
+
// machine. Nothing documented that, and the server's own Codex capture never
|
|
355
|
+
// did it: each deck tails the rollout files itself and evaluates its own
|
|
356
|
+
// workspace, so a Codex session inside a scoped tree appeared on both decks
|
|
357
|
+
// while the Claude session beside it appeared on one. One flag, one path,
|
|
358
|
+
// two answers.
|
|
359
|
+
//
|
|
360
|
+
// The fan-out is the documented meaning and the one kept: `--workspace` says
|
|
361
|
+
// which sessions a deck captures, not which sessions it takes from the decks
|
|
362
|
+
// around it. It is also what electWriters below already assumes — several
|
|
363
|
+
// decks drawing one event is the case it exists to keep from being written
|
|
364
|
+
// to one log several times.
|
|
365
|
+
const targets = [];
|
|
319
366
|
for (const file of files) {
|
|
320
367
|
let d;
|
|
321
368
|
try { d = JSON.parse(fs.readFileSync(path.join(DIR, file), "utf8")); } catch { continue; }
|
|
@@ -329,29 +376,25 @@ function main() {
|
|
|
329
376
|
continue;
|
|
330
377
|
}
|
|
331
378
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
379
|
+
// "" is machine-wide and must never reach normPath: resolving it would
|
|
380
|
+
// produce this hook's own cwd — the agent's — and scope a deck that asked
|
|
381
|
+
// for no scope at all. Any other spelling is canonicalized here, which is
|
|
382
|
+
// now a second pass over a path bin/deck.js already canonicalized before
|
|
383
|
+
// publishing it — kept because a deck old enough to have published a
|
|
384
|
+
// relative one is still entitled to its events.
|
|
385
|
+
const ws = d.workspace === "" ? "" : normPath(d.workspace);
|
|
386
|
+
if (capturesSession(resolvedCwd, ws)) targets.push(d);
|
|
340
387
|
}
|
|
341
388
|
|
|
342
|
-
if (!
|
|
343
|
-
|
|
344
|
-
matches.sort((a, b) => b.wsLen - a.wsLen);
|
|
345
|
-
const bestLen = matches[0].wsLen;
|
|
346
|
-
const targets = matches.filter(m => m.wsLen === bestLen);
|
|
389
|
+
if (!targets.length) return process.exit(0);
|
|
347
390
|
|
|
348
391
|
// One deck per events log records this event; the others only draw it.
|
|
349
|
-
const writers = electWriters(targets
|
|
392
|
+
const writers = electWriters(targets);
|
|
350
393
|
|
|
351
394
|
let pending = targets.length;
|
|
352
395
|
const done = () => { if (--pending <= 0) process.exit(0); };
|
|
353
396
|
|
|
354
|
-
for (const
|
|
397
|
+
for (const d of targets) deliver(d, taggedInput, writers.has(d), done);
|
|
355
398
|
});
|
|
356
399
|
}
|
|
357
400
|
|
|
@@ -360,5 +403,5 @@ function main() {
|
|
|
360
403
|
// require() it exports the rules it decides by — matching, election, the
|
|
361
404
|
// handshake — and starts nothing, which is what lets them be tested without a
|
|
362
405
|
// 1.5s exit timer in the test runner.
|
|
363
|
-
module.exports = { cwdInWorkspace, foldsCase, electWriters, challengeProof, requiresProof };
|
|
406
|
+
module.exports = { capturesSession, cwdInWorkspace, foldsCase, electWriters, challengeProof, requiresProof };
|
|
364
407
|
if (require.main === module) main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dag",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.0",
|
|
4
4
|
"description": "Live deck of Claude Code and Codex agents — watch parallel subagents fork, call tools, and return on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/server/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Single-file pure Node HTTP server, zero deps.
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { readFile, stat, mkdir, appendFile, open, truncate, readdir, unlink } from "node:fs/promises";
|
|
5
|
-
import { createReadStream, existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { createReadStream, existsSync, readFileSync, realpathSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { extname, join, resolve, dirname as pdirname } from "node:path";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -1918,6 +1918,37 @@ async function handleSoundHookSet(req, res) {
|
|
|
1918
1918
|
// can read before a single event has arrived.
|
|
1919
1919
|
let _workspace = "";
|
|
1920
1920
|
|
|
1921
|
+
/**
|
|
1922
|
+
* The one spelling of `--workspace` everything downstream compares against.
|
|
1923
|
+
* Empty — including a value that is nothing but spaces — stays empty, which is
|
|
1924
|
+
* how every reader of it says "machine-wide".
|
|
1925
|
+
*
|
|
1926
|
+
* Called once, in bin/deck.js, where the flag arrives: that process's cwd is the
|
|
1927
|
+
* shell the user typed the command in, and it is the only process on either
|
|
1928
|
+
* capture path whose cwd is the one a relative `--workspace ./sub` was written
|
|
1929
|
+
* against. The raw string used to go straight into the discovery file, and
|
|
1930
|
+
* hook.js resolved it inside its own process — which the host CLI runs with the
|
|
1931
|
+
* AGENT's cwd — so `--workspace ./sub` scoped Claude sessions to a `sub`
|
|
1932
|
+
* directory under whatever the agent happened to be working in, a different
|
|
1933
|
+
* directory per agent and none of them the one asked for. The Codex path
|
|
1934
|
+
* resolved the same string in the server process and was right. Resolving here
|
|
1935
|
+
* makes both of them right, and for the same reason bin/deck.js already resolves
|
|
1936
|
+
* the events log before publishing it: what goes in the discovery file is read
|
|
1937
|
+
* by other processes that cannot reconstruct the context it was written in.
|
|
1938
|
+
*
|
|
1939
|
+
* Symlinks are resolved too, because a process's cwd — which is what both
|
|
1940
|
+
* providers report — comes from getcwd() and has none left in it. Without this,
|
|
1941
|
+
* `--workspace /tmp/proj` on a Mac is scoped to /tmp/proj while every session
|
|
1942
|
+
* inside it reports /private/tmp/proj, and the deck stays empty. A path that
|
|
1943
|
+
* does not exist yet keeps its resolved form rather than failing: scoping a deck
|
|
1944
|
+
* to a directory you are about to create is not an error.
|
|
1945
|
+
*/
|
|
1946
|
+
export function canonicalWorkspace(raw) {
|
|
1947
|
+
if (typeof raw !== "string" || raw.trim() === "") return "";
|
|
1948
|
+
const abs = resolve(raw);
|
|
1949
|
+
try { return realpathSync(abs); } catch { return abs; }
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1921
1952
|
function handleHealth(_req, res) {
|
|
1922
1953
|
send(res, 200, {
|
|
1923
1954
|
ok: true,
|
|
@@ -73,17 +73,29 @@ export function electWriters(decks, platform = process.platform) {
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* Would a deck scoped to `workspace` capture a rollout running in `cwd`? An
|
|
76
|
-
* empty workspace is unscoped and captures every session
|
|
76
|
+
* empty workspace is unscoped and captures every session; a rollout that never
|
|
77
|
+
* said where it runs is inside no workspace, so only an unscoped deck draws it.
|
|
77
78
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
79
|
+
* This answers two questions with one function — whether THIS deck tails a
|
|
80
|
+
* rollout, and whether another deck tails it too — and that is only sound while
|
|
81
|
+
* the rule below is the rule every deck actually runs. Model another deck's
|
|
82
|
+
* capture with anything else and the election covers the wrong set: a deck that
|
|
83
|
+
* writes without being elected, or an elected deck that never opened the file.
|
|
84
|
+
*
|
|
85
|
+
* It is also the rule hook/hook.js runs for the sessions it delivers, under the
|
|
86
|
+
* name capturesSession — that script is copied out of the package and run
|
|
87
|
+
* standalone, so the two are written twice and pinned equal by a test walking
|
|
88
|
+
* one table of paths through both. They were not equal: case was folded here on
|
|
89
|
+
* every platform, so on Linux a deck scoped to /srv/proj captured Codex sessions
|
|
90
|
+
* from /srv/Proj and Claude sessions from neither. Those are two real
|
|
91
|
+
* directories there, and the hook's own comment says what folding them together
|
|
92
|
+
* costs — a deck handed the events of a tree it was not scoped to. So the fold
|
|
93
|
+
* is per-platform on both sides now, and `--workspace` means one thing.
|
|
94
|
+
*
|
|
95
|
+
* (The narrow window that opens: two decks on Linux whose workspaces differ only
|
|
96
|
+
* in case, one of them old enough to still fold, both containing one rollout's
|
|
97
|
+
* cwd. Each models the other as tailing the file; one of them is wrong, and the
|
|
98
|
+
* cost is a single log line written twice.)
|
|
87
99
|
*
|
|
88
100
|
* The platform is a parameter, following the hook's cwdInWorkspace and
|
|
89
101
|
* spawnSpec in src/server/exec.mjs, so the Windows separator is testable from a
|
|
@@ -93,8 +105,9 @@ export function codexCwdInWorkspace(cwd, workspace, platform = process.platform)
|
|
|
93
105
|
if (!workspace || typeof workspace !== "string") return true;
|
|
94
106
|
if (!cwd || typeof cwd !== "string") return false;
|
|
95
107
|
const p = platform === "win32" ? win32 : posix;
|
|
96
|
-
const
|
|
97
|
-
const
|
|
108
|
+
const fold = s => (foldsCase(platform) ? s.toLowerCase() : s);
|
|
109
|
+
const a = fold(p.resolve(cwd));
|
|
110
|
+
const b = fold(p.resolve(workspace));
|
|
98
111
|
if (a === b) return true;
|
|
99
112
|
// A root ("C:\", "/") already ends in the separator; appending a second one
|
|
100
113
|
// would match nothing.
|
|
@@ -106,13 +119,15 @@ export function codexCwdInWorkspace(cwd, workspace, platform = process.platform)
|
|
|
106
119
|
* it? `decks` is every deck registered right now, `pid` identifies this one
|
|
107
120
|
* among them, and `cwd` is the workspace the rollout is running in.
|
|
108
121
|
*
|
|
109
|
-
* The group is every deck that tails this same rollout
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
122
|
+
* The group is every deck that tails this same rollout: each deck decides for
|
|
123
|
+
* itself, so all of them whose workspace contains the cwd read the file and all
|
|
124
|
+
* of them would write it. The hook builds the same group the same way for the
|
|
125
|
+
* events it delivers — it used to narrow them to the longest workspace match
|
|
126
|
+
* first, which is the asymmetry the predicate above describes the end of. A deck
|
|
127
|
+
* started with `--no-codex` tails nothing and is left out; electing it would
|
|
128
|
+
* mean the rollout's events reach no log at all. A deck too old to say either
|
|
129
|
+
* way is assumed to be tailing, which is what it was doing before this field
|
|
130
|
+
* existed.
|
|
116
131
|
*/
|
|
117
132
|
export function writesCodexLog({ decks, pid, cwd, platform = process.platform }) {
|
|
118
133
|
const live = Array.isArray(decks) ? decks : [];
|