pi-daddy 0.14.0 → 0.16.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +37 -14
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +10 -4
  5. package/dist/cli.js.map +1 -1
  6. package/dist/executor.d.ts +38 -0
  7. package/dist/executor.d.ts.map +1 -0
  8. package/dist/executor.js +93 -0
  9. package/dist/executor.js.map +1 -0
  10. package/dist/grant-store.d.ts +68 -0
  11. package/dist/grant-store.d.ts.map +1 -0
  12. package/dist/grant-store.js +142 -0
  13. package/dist/grant-store.js.map +1 -0
  14. package/dist/herdr-cli.d.ts +78 -0
  15. package/dist/herdr-cli.d.ts.map +1 -0
  16. package/dist/herdr-cli.js +113 -0
  17. package/dist/herdr-cli.js.map +1 -0
  18. package/dist/herdr-name.d.ts +37 -0
  19. package/dist/herdr-name.d.ts.map +1 -0
  20. package/dist/herdr-name.js +59 -0
  21. package/dist/herdr-name.js.map +1 -0
  22. package/dist/herdr-poll.d.ts +104 -0
  23. package/dist/herdr-poll.d.ts.map +1 -0
  24. package/dist/herdr-poll.js +150 -0
  25. package/dist/herdr-poll.js.map +1 -0
  26. package/dist/herdr-stage.d.ts +40 -0
  27. package/dist/herdr-stage.d.ts.map +1 -0
  28. package/dist/herdr-stage.js +54 -0
  29. package/dist/herdr-stage.js.map +1 -0
  30. package/dist/ledger-report.d.ts +18 -0
  31. package/dist/ledger-report.d.ts.map +1 -1
  32. package/dist/ledger-report.js +10 -0
  33. package/dist/ledger-report.js.map +1 -1
  34. package/dist/ledger.d.ts +17 -0
  35. package/dist/ledger.d.ts.map +1 -1
  36. package/dist/ledger.js +1 -0
  37. package/dist/ledger.js.map +1 -1
  38. package/dist/pane-reaper.d.ts +66 -4
  39. package/dist/pane-reaper.d.ts.map +1 -1
  40. package/dist/pane-reaper.js +131 -9
  41. package/dist/pane-reaper.js.map +1 -1
  42. package/dist/progress.d.ts +96 -0
  43. package/dist/progress.d.ts.map +1 -0
  44. package/dist/progress.js +167 -0
  45. package/dist/progress.js.map +1 -0
  46. package/dist/run-child.d.ts +27 -0
  47. package/dist/run-child.d.ts.map +1 -1
  48. package/dist/run-child.js +84 -7
  49. package/dist/run-child.js.map +1 -1
  50. package/dist/run-herdr.d.ts +41 -28
  51. package/dist/run-herdr.d.ts.map +1 -1
  52. package/dist/run-herdr.js +150 -167
  53. package/dist/run-herdr.js.map +1 -1
  54. package/dist/skill-packages.d.ts +16 -0
  55. package/dist/skill-packages.d.ts.map +1 -1
  56. package/dist/skill-packages.js +39 -10
  57. package/dist/skill-packages.js.map +1 -1
  58. package/extensions/delegation.ts +94 -2
  59. package/extensions/grants-command.ts +57 -1
  60. package/extensions/grants.ts +109 -165
  61. package/extensions/init-command.ts +132 -0
  62. package/extensions/run-delegation.ts +70 -8
  63. package/extensions/session-report.ts +231 -0
  64. package/extensions/session.ts +138 -17
  65. package/extensions/tripwire.ts +44 -0
  66. package/package.json +17 -1
  67. package/src/cli.ts +10 -4
  68. package/src/executor.ts +122 -0
  69. package/src/grant-store.ts +151 -0
  70. package/src/herdr-cli.ts +125 -0
  71. package/src/herdr-name.ts +61 -0
  72. package/src/herdr-poll.ts +185 -0
  73. package/src/herdr-stage.ts +55 -0
  74. package/src/ledger-report.ts +21 -0
  75. package/src/ledger.ts +18 -0
  76. package/src/pane-reaper.ts +147 -9
  77. package/src/progress.ts +206 -0
  78. package/src/run-child.ts +96 -7
  79. package/src/run-herdr.ts +170 -174
  80. package/src/skill-packages.ts +41 -10
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The grant for a directory, stored **outside** it.
3
+ *
4
+ * `PI_GRANTS_GRANT` is the propagation channel to children and stays exactly as it was — a parent writes
5
+ * it once per session and every child inherits it. What this adds is a second *source* for the root
6
+ * session's own grant, so an operator does not have to `source` a file and restart pi to be governed.
7
+ *
8
+ * **Outside the workspace, and that is the whole design.** A grant is a ceiling; a ceiling a governed child
9
+ * can rewrite is not a ceiling. `<cwd>/.pi/grants.env` is writable by any child holding `tool:write`, so
10
+ * storing the live grant there would let a child widen the *next* session's ceiling — ADR-0014's
11
+ * self-defeating case verbatim, which is why persisted approvals were moved out of the workspace in the
12
+ * first place. This reuses that pattern exactly: `$PI_CODING_AGENT_DIR/grants/<slug>-<hash>.json`, keyed by
13
+ * the directory, unwritable by a narrowed child because a narrowed child holds no write access to `$HOME`.
14
+ *
15
+ * **It does not defend against a child holding `bash`** (ADR-0012). Nothing here does.
16
+ *
17
+ * `.pi/grants.env` is still written by `init` and is still worth committing — it is the *reviewable record*
18
+ * of the decision, diffable in a PR. It is simply no longer the thing the enforcer reads.
19
+ */
20
+ import { createHash } from "node:crypto";
21
+ import { readFileSync } from "node:fs";
22
+ import { mkdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
23
+ import { randomUUID } from "node:crypto";
24
+ import { homedir } from "node:os";
25
+ import { basename, dirname, join } from "node:path";
26
+ import { withFileLock, LockTimeoutError } from "./file-lock.js";
27
+ /** `$PI_CODING_AGENT_DIR`, or pi's default. Same resolution as the approval store. */
28
+ function agentDir() {
29
+ return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
30
+ }
31
+ /**
32
+ * Where this directory's grant lives.
33
+ *
34
+ * Slug plus a 64-bit hash, exactly as `approvalsPath` does it and for the same two reasons: the basename
35
+ * keeps the directory legible to a human reading it, and the hash is what makes it unambiguous, since two
36
+ * checkouts can share a basename.
37
+ */
38
+ export function grantStorePath(cwd) {
39
+ const slug = (basename(cwd) || "root").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40);
40
+ const hash = createHash("sha256").update(cwd, "utf8").digest("hex").slice(0, 16);
41
+ return join(agentDir(), "grants", `${slug}-${hash}.json`);
42
+ }
43
+ /**
44
+ * Parse a store file's text into a grant, or null.
45
+ *
46
+ * Split out from the readers so the **one** validation lives in one place: both the sync and async paths
47
+ * must agree, and two parsers is how they come to disagree.
48
+ *
49
+ * Fails closed on every doubt — a malformed file grants nothing rather than something. The `cwd` check is
50
+ * R-27's: a file copied to another machine or another checkout describes a directory that is not this one,
51
+ * and honouring it would let a grant travel somewhere nobody authorised it for.
52
+ */
53
+ export function parseGrantFile(text, cwd) {
54
+ try {
55
+ const parsed = JSON.parse(text);
56
+ if (parsed.version !== 1)
57
+ return null;
58
+ if (parsed.cwd !== cwd)
59
+ return null;
60
+ if (!Array.isArray(parsed.grant))
61
+ return null;
62
+ if (!parsed.grant.every((c) => typeof c === "string" && c.length > 0))
63
+ return null;
64
+ return parsed.grant;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ /**
71
+ * Read this directory's stored grant, synchronously.
72
+ *
73
+ * **Sync on purpose, and this is the constraint that shapes the feature.** Whether `delegate` is registered
74
+ * at all is decided when the extension factory runs (S-5: a session without `tool:delegate` must not be
75
+ * offered the tool), and that is before any `await` is possible. An async read would resolve after the
76
+ * decision it exists to inform, so the store would silently fail to grant delegation — the exact class of
77
+ * defect R-38 and R-39 were.
78
+ */
79
+ export function loadGrantSync(cwd) {
80
+ try {
81
+ return parseGrantFile(readFileSync(grantStorePath(cwd), "utf8"), cwd);
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ /** Async twin, for callers that already have one. Same parser, so they cannot disagree. */
88
+ export async function loadGrant(cwd) {
89
+ try {
90
+ return parseGrantFile(await readFile(grantStorePath(cwd), "utf8"), cwd);
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ /**
97
+ * Write this directory's grant.
98
+ *
99
+ * Locked with the same lock the ledger and the approval store use, for the same reason: two sessions in one
100
+ * directory running `/grants init` concurrently must not interleave. A lock this cannot take yields `busy`
101
+ * and changes nothing — the caller reports it and the operator retries, which is the honest outcome for a
102
+ * write that never looked at the file (R-68).
103
+ *
104
+ * `wx` on the temp file refuses to follow a pre-existing symlink, and `rename` is atomic within a
105
+ * filesystem, so a reader never sees a half-written grant. Both copied from `writeFileSafely`, deliberately
106
+ * — a second, subtly different atomic-write is how the two come to disagree about what "safe" meant.
107
+ */
108
+ export async function saveGrant(cwd, grant) {
109
+ const path = grantStorePath(cwd);
110
+ const file = { version: 1, cwd, grant: [...grant].sort(), writtenAt: new Date().toISOString() };
111
+ try {
112
+ await mkdir(dirname(path), { recursive: true });
113
+ return await withFileLock(path, "grant store", async () => {
114
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
115
+ try {
116
+ await writeFile(temp, `${JSON.stringify(file, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
117
+ await rename(temp, path);
118
+ return "saved";
119
+ }
120
+ catch {
121
+ await unlink(temp).catch(() => undefined);
122
+ return "failed";
123
+ }
124
+ });
125
+ }
126
+ catch (error) {
127
+ return error instanceof LockTimeoutError ? "busy" : "failed";
128
+ }
129
+ }
130
+ /** Remove this directory's stored grant. True when a file was there to remove. */
131
+ export async function clearGrant(cwd) {
132
+ const path = grantStorePath(cwd);
133
+ try {
134
+ await readFile(path, "utf8");
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ await rm(path, { force: true }).catch(() => undefined);
140
+ return true;
141
+ }
142
+ //# sourceMappingURL=grant-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grant-store.js","sourceRoot":"","sources":["../src/grant-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Talking to herdr: one command, one JSON envelope, plus the two questions ADR-0031 needs answered.
3
+ *
4
+ * Lifted out of `src/run-herdr.ts`, which was at 357 of the 400-line ceiling and gains output polling under
5
+ * ADR-0032. But the split is not only about lines: **the probe is not an executor concern**. It runs at session
6
+ * start, before any delegation exists, to decide *which* executor a session will use — so leaving it inside the
7
+ * herdr executor would mean the session imported the thing it was deciding whether to use.
8
+ *
9
+ * Every rule here is tested against an injected `exec`, so the suite stays fast, pi-free and herdr-free. The
10
+ * facts the fakes reproduce were measured against real herdr 0.7.5 (`docs/probes/g16-herdr`).
11
+ */
12
+ /** One herdr CLI invocation. Injectable so every rule below is testable without herdr installed. */
13
+ export type HerdrExec = (args: string[]) => Promise<{
14
+ code: number | null;
15
+ stdout: string;
16
+ stderr: string;
17
+ }>;
18
+ export declare const defaultExec: HerdrExec;
19
+ /**
20
+ * Parse herdr's JSON envelope. Every command replies `{id, result}` or `{id, error:{code,message}}`.
21
+ *
22
+ * `stderr` is folded into the message because the first end-to-end run failed with an EMPTY stdout and the
23
+ * real reason on stderr, producing the useless diagnostic "unparseable herdr reply: ". A wrapper that
24
+ * hides the substrate's own error message costs more time than it saves.
25
+ */
26
+ export declare function parseReply(reply: {
27
+ stdout: string;
28
+ stderr: string;
29
+ }): {
30
+ result?: Record<string, unknown>;
31
+ error?: string;
32
+ };
33
+ /** Bound on the session-start probe. Short: it sits in front of the operator's first prompt. */
34
+ export declare const PROBE_TIMEOUT_MS = 2000;
35
+ export interface HerdrProbe {
36
+ ok: boolean;
37
+ /** herdr's own words when it is not reachable. Carried so the disclosure line can name the reason. */
38
+ error?: string;
39
+ }
40
+ /**
41
+ * Is there a herdr server that will answer right now? — ADR-0031's selection input.
42
+ *
43
+ * **`tab list`, not `which herdr`.** ADR-0031 rejects `PATH` detection as option C by name: a binary on `PATH`
44
+ * with no server behind it would make every delegation fail at `tab create`, on a path the operator never
45
+ * chose, and the diagnostic would arrive at the first delegation rather than at startup. Only a parsed
46
+ * `result` envelope counts as reachable; an `error` envelope, a non-JSON reply, a timeout and a throwing
47
+ * `exec` are all "not reachable" with the reason preserved.
48
+ *
49
+ * **Zero tabs is a successful answer**, deliberately: a fresh herdr with nothing open is reachable.
50
+ *
51
+ * Never throws. A probe that threw out of `session_start` would cancel every control after it, which is
52
+ * R-60's shape exactly — and this one runs *before* the line that discloses what it decided.
53
+ */
54
+ export declare function probeHerdr(options?: {
55
+ exec?: HerdrExec;
56
+ timeoutMs?: number;
57
+ }): Promise<HerdrProbe>;
58
+ /** herdr's own variable, set in every pane it creates. Measured 2026-08-17; documented nowhere. */
59
+ export declare const ENV_PARENT_WORKSPACE = "HERDR_WORKSPACE_ID";
60
+ /** The operator's explicit override. Defined here because this is the only module that reads it. */
61
+ export declare const ENV_HERDR_WORKSPACE = "PI_GRANTS_HERDR_WORKSPACE";
62
+ /**
63
+ * Which herdr workspace a governed child's pane belongs in.
64
+ *
65
+ * **Defaults to the parent's own workspace.** herdr tells a pane which workspace it is in
66
+ * (`HERDR_WORKSPACE_ID`, alongside `HERDR_TAB_ID` and `HERDR_PANE_ID`), and a child placed in a *different*
67
+ * workspace from the pi session that spawned it turns "switch between them" into a workspace hop — which is
68
+ * the entire feature ADR-0032 exists to deliver. The previous behaviour was "omitted lets herdr choose",
69
+ * which is that failure by default on any machine with more than one workspace.
70
+ *
71
+ * `PI_GRANTS_HERDR_WORKSPACE` still wins: it is the operator saying so explicitly, and an explicit answer
72
+ * beating an inference is this package's standing rule (ADR-0030 says it about the grant itself).
73
+ *
74
+ * Blank is treated as absent rather than passed through — `--workspace ""` is not a workspace, and it would
75
+ * fail `tab create` on a path nobody chose.
76
+ */
77
+ export declare function resolveWorkspace(env: NodeJS.ProcessEnv): string | undefined;
78
+ //# sourceMappingURL=herdr-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"herdr-cli.d.ts","sourceRoot":"","sources":["../src/herdr-cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,oGAAoG;AACpG,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE7G,eAAO,MAAM,WAAW,EAAE,SAatB,CAAC;AAEL;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAW1H;AAED,gGAAgG;AAChG,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,sGAAsG;IACtG,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,UAAU,CAAC,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,SAAS,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAsB5G;AAED,mGAAmG;AACnG,eAAO,MAAM,oBAAoB,uBAAuB,CAAC;AAEzD,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAE/D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,GAAG,SAAS,CAI3E"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Talking to herdr: one command, one JSON envelope, plus the two questions ADR-0031 needs answered.
3
+ *
4
+ * Lifted out of `src/run-herdr.ts`, which was at 357 of the 400-line ceiling and gains output polling under
5
+ * ADR-0032. But the split is not only about lines: **the probe is not an executor concern**. It runs at session
6
+ * start, before any delegation exists, to decide *which* executor a session will use — so leaving it inside the
7
+ * herdr executor would mean the session imported the thing it was deciding whether to use.
8
+ *
9
+ * Every rule here is tested against an injected `exec`, so the suite stays fast, pi-free and herdr-free. The
10
+ * facts the fakes reproduce were measured against real herdr 0.7.5 (`docs/probes/g16-herdr`).
11
+ */
12
+ import { execFile } from "node:child_process";
13
+ export const defaultExec = (args) => new Promise((settle) => {
14
+ execFile("herdr", args, { maxBuffer: 32 * 1024 * 1024 }, (error, stdout, stderr) => {
15
+ const raw = error?.code;
16
+ const code = typeof raw === "number" ? raw : error ? 1 : 0;
17
+ // **A string `code` is a spawn failure, and it used to be thrown away.** `ENOENT` — herdr not installed —
18
+ // arrives as `code: "ENOENT"`, so the numeric test failed, the message was dropped, and an operator with
19
+ // `PI_GRANTS_HERDR=1` on a machine without herdr was told *"herdr is not answering (unparseable herdr
20
+ // reply: (no output))"* rather than that the binary is missing. Rule 8 wants the loud version, and this is
21
+ // the first diagnostic such an operator meets.
22
+ const spawnFailure = typeof raw === "string" ? `herdr could not be run (${raw}): ${error?.message ?? ""}` : "";
23
+ settle({ code, stdout: String(stdout), stderr: spawnFailure || String(stderr) });
24
+ });
25
+ });
26
+ /**
27
+ * Parse herdr's JSON envelope. Every command replies `{id, result}` or `{id, error:{code,message}}`.
28
+ *
29
+ * `stderr` is folded into the message because the first end-to-end run failed with an EMPTY stdout and the
30
+ * real reason on stderr, producing the useless diagnostic "unparseable herdr reply: ". A wrapper that
31
+ * hides the substrate's own error message costs more time than it saves.
32
+ */
33
+ export function parseReply(reply) {
34
+ try {
35
+ const parsed = JSON.parse(reply.stdout);
36
+ if (parsed.error)
37
+ return { error: parsed.error.message ?? parsed.error.code ?? "herdr reported an error" };
38
+ return { result: parsed.result };
39
+ }
40
+ catch {
41
+ // A non-JSON reply is a herdr-version or PATH problem, not a governance decision. Surfaced as a spawn
42
+ // error so the caller reports "could not start" rather than "the child produced nothing".
43
+ const detail = [reply.stdout.trim(), reply.stderr.trim()].filter((t) => t.length > 0).join(" | ");
44
+ return { error: `unparseable herdr reply: ${detail.slice(0, 300) || "(no output)"}` };
45
+ }
46
+ }
47
+ /** Bound on the session-start probe. Short: it sits in front of the operator's first prompt. */
48
+ export const PROBE_TIMEOUT_MS = 2000;
49
+ /**
50
+ * Is there a herdr server that will answer right now? — ADR-0031's selection input.
51
+ *
52
+ * **`tab list`, not `which herdr`.** ADR-0031 rejects `PATH` detection as option C by name: a binary on `PATH`
53
+ * with no server behind it would make every delegation fail at `tab create`, on a path the operator never
54
+ * chose, and the diagnostic would arrive at the first delegation rather than at startup. Only a parsed
55
+ * `result` envelope counts as reachable; an `error` envelope, a non-JSON reply, a timeout and a throwing
56
+ * `exec` are all "not reachable" with the reason preserved.
57
+ *
58
+ * **Zero tabs is a successful answer**, deliberately: a fresh herdr with nothing open is reachable.
59
+ *
60
+ * Never throws. A probe that threw out of `session_start` would cancel every control after it, which is
61
+ * R-60's shape exactly — and this one runs *before* the line that discloses what it decided.
62
+ */
63
+ export async function probeHerdr(options = {}) {
64
+ const exec = options.exec ?? defaultExec;
65
+ const timeoutMs = options.timeoutMs ?? PROBE_TIMEOUT_MS;
66
+ let timer;
67
+ try {
68
+ return await Promise.race([
69
+ exec(["tab", "list"]).then((reply) => {
70
+ const parsed = parseReply(reply);
71
+ return parsed.error ? { ok: false, error: parsed.error } : { ok: true };
72
+ }),
73
+ new Promise((settle) => {
74
+ timer = setTimeout(() => settle({ ok: false, error: `probe timed out after ${timeoutMs}ms` }), timeoutMs);
75
+ }),
76
+ ]);
77
+ }
78
+ catch (error) {
79
+ return { ok: false, error: String(error) };
80
+ }
81
+ finally {
82
+ // Cleared whichever branch won, so a fast probe does not hold the event loop open for the timeout's
83
+ // remainder — which would add up to two seconds to every `node --test` run of this file.
84
+ if (timer)
85
+ clearTimeout(timer);
86
+ }
87
+ }
88
+ /** herdr's own variable, set in every pane it creates. Measured 2026-08-17; documented nowhere. */
89
+ export const ENV_PARENT_WORKSPACE = "HERDR_WORKSPACE_ID";
90
+ /** The operator's explicit override. Defined here because this is the only module that reads it. */
91
+ export const ENV_HERDR_WORKSPACE = "PI_GRANTS_HERDR_WORKSPACE";
92
+ /**
93
+ * Which herdr workspace a governed child's pane belongs in.
94
+ *
95
+ * **Defaults to the parent's own workspace.** herdr tells a pane which workspace it is in
96
+ * (`HERDR_WORKSPACE_ID`, alongside `HERDR_TAB_ID` and `HERDR_PANE_ID`), and a child placed in a *different*
97
+ * workspace from the pi session that spawned it turns "switch between them" into a workspace hop — which is
98
+ * the entire feature ADR-0032 exists to deliver. The previous behaviour was "omitted lets herdr choose",
99
+ * which is that failure by default on any machine with more than one workspace.
100
+ *
101
+ * `PI_GRANTS_HERDR_WORKSPACE` still wins: it is the operator saying so explicitly, and an explicit answer
102
+ * beating an inference is this package's standing rule (ADR-0030 says it about the grant itself).
103
+ *
104
+ * Blank is treated as absent rather than passed through — `--workspace ""` is not a workspace, and it would
105
+ * fail `tab create` on a path nobody chose.
106
+ */
107
+ export function resolveWorkspace(env) {
108
+ const explicit = env[ENV_HERDR_WORKSPACE]?.trim();
109
+ if (explicit)
110
+ return explicit;
111
+ return env[ENV_PARENT_WORKSPACE]?.trim() || undefined;
112
+ }
113
+ //# sourceMappingURL=herdr-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"herdr-cli.js","sourceRoot":"","sources":["../src/herdr-cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Naming a herdr agent: the grammar herdr enforces, and uniqueness it does not.
3
+ *
4
+ * Split out of `src/run-herdr.ts` at the 400-line ceiling, and a real seam: both rules below come from **herdr's
5
+ * own validation and lifecycle**, not from anything this package decides. Two shipping defects lived here, and
6
+ * both were invisible to every test because the unit fake accepts whatever name it is handed and the integration
7
+ * suite never reaches a real herdr spawn. They surfaced from two real spawns against the live daemon.
8
+ */
9
+ /**
10
+ * Make a herdr agent name that is **valid** and cannot collide with a live one.
11
+ *
12
+ * **Validity is a separate, PRE-EXISTING defect, and it is the more serious half.** Callers build a name as
13
+ * `${definition}-${childId}`, and a ledger child id is hierarchical — `d0.1`, `d0.1.2` (ADR-0008/F8). Those dots
14
+ * are **not in herdr's grammar**, so `agent start review-d0.1 …` is rejected with `invalid_agent_name`. Every
15
+ * `delegate({agent})` on the herdr path has therefore failed at `agent start` since the executor was written.
16
+ *
17
+ * Nothing could see it. The unit fake accepts any name it is handed, and the integration suite never reaches a
18
+ * real herdr spawn — so both were green while the feature could not work. It surfaced only by running two real
19
+ * spawns against the live daemon, which is the argument for doing that at all.
20
+ *
21
+ * **Measured, and a shipping defect without it.** herdr binds an agent name to its **tab**, and only closing
22
+ * the tab frees the name: a second `agent start` with a name still held returns
23
+ * `agent_name_taken: agent <name> is already used; … tab_id=…`. `herdr agent stop` does not exist (see
24
+ * `cleanup`), so nothing else releases it.
25
+ *
26
+ * Callers build a name from the definition and the ledger child id — and for a plain blocking `delegate` that
27
+ * id is **constant** (`d0.1`, index 0 of the session), so every delegation in a session asked for the same
28
+ * name. That was harmless while the pane closed at the end of each call. Once ADR-0032 kept panes alive to
29
+ * `agent_settled`, the **first** delegation of a turn worked and every later one failed with
30
+ * `agent_name_taken`, on the executor ADR-0031 had just made the default.
31
+ *
32
+ * Uniquified HERE rather than at the call site, so no caller can forget: the constraint belongs to herdr, and
33
+ * this module is the only thing that talks to herdr. The suffix is a counter rather than a random token so a
34
+ * pane label stays readable and reproducible within a run.
35
+ */
36
+ export declare function uniqueAgentName(base: string): string;
37
+ //# sourceMappingURL=herdr-name.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"herdr-name.d.ts","sourceRoot":"","sources":["../src/herdr-name.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAapD"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Naming a herdr agent: the grammar herdr enforces, and uniqueness it does not.
3
+ *
4
+ * Split out of `src/run-herdr.ts` at the 400-line ceiling, and a real seam: both rules below come from **herdr's
5
+ * own validation and lifecycle**, not from anything this package decides. Two shipping defects lived here, and
6
+ * both were invisible to every test because the unit fake accepts whatever name it is handed and the integration
7
+ * suite never reaches a real herdr spawn. They surfaced from two real spawns against the live daemon.
8
+ */
9
+ /** Monotonic within this process. See `uniqueAgentName`. */
10
+ let spawnSeq = 0;
11
+ /**
12
+ * herdr's agent-name grammar, measured from its own rejection message.
13
+ *
14
+ * `agent name must start with a lowercase letter and contain only lowercase letters, digits, '-' or '_'
15
+ * (1-32 characters)`.
16
+ */
17
+ const AGENT_NAME_MAX = 32;
18
+ /**
19
+ * Make a herdr agent name that is **valid** and cannot collide with a live one.
20
+ *
21
+ * **Validity is a separate, PRE-EXISTING defect, and it is the more serious half.** Callers build a name as
22
+ * `${definition}-${childId}`, and a ledger child id is hierarchical — `d0.1`, `d0.1.2` (ADR-0008/F8). Those dots
23
+ * are **not in herdr's grammar**, so `agent start review-d0.1 …` is rejected with `invalid_agent_name`. Every
24
+ * `delegate({agent})` on the herdr path has therefore failed at `agent start` since the executor was written.
25
+ *
26
+ * Nothing could see it. The unit fake accepts any name it is handed, and the integration suite never reaches a
27
+ * real herdr spawn — so both were green while the feature could not work. It surfaced only by running two real
28
+ * spawns against the live daemon, which is the argument for doing that at all.
29
+ *
30
+ * **Measured, and a shipping defect without it.** herdr binds an agent name to its **tab**, and only closing
31
+ * the tab frees the name: a second `agent start` with a name still held returns
32
+ * `agent_name_taken: agent <name> is already used; … tab_id=…`. `herdr agent stop` does not exist (see
33
+ * `cleanup`), so nothing else releases it.
34
+ *
35
+ * Callers build a name from the definition and the ledger child id — and for a plain blocking `delegate` that
36
+ * id is **constant** (`d0.1`, index 0 of the session), so every delegation in a session asked for the same
37
+ * name. That was harmless while the pane closed at the end of each call. Once ADR-0032 kept panes alive to
38
+ * `agent_settled`, the **first** delegation of a turn worked and every later one failed with
39
+ * `agent_name_taken`, on the executor ADR-0031 had just made the default.
40
+ *
41
+ * Uniquified HERE rather than at the call site, so no caller can forget: the constraint belongs to herdr, and
42
+ * this module is the only thing that talks to herdr. The suffix is a counter rather than a random token so a
43
+ * pane label stays readable and reproducible within a run.
44
+ */
45
+ export function uniqueAgentName(base) {
46
+ spawnSeq += 1;
47
+ const suffix = `-${spawnSeq}`;
48
+ const cleaned = base
49
+ .toLowerCase()
50
+ .replace(/[^a-z0-9_-]+/g, "-") // dots from a child id, and anything else outside the grammar
51
+ .replace(/-{2,}/g, "-")
52
+ .replace(/^[^a-z]+/, ""); // must START with a lowercase letter, so a leading digit or dash goes
53
+ // Truncated so the whole name fits, and trimmed of a trailing separator so the join stays readable. The
54
+ // fallback covers a base that sanitises to nothing at all (a definition named entirely in non-Latin script).
55
+ const room = AGENT_NAME_MAX - suffix.length;
56
+ const head = cleaned.slice(0, room).replace(/[-_]+$/, "") || "agent";
57
+ return `${head}${suffix}`;
58
+ }
59
+ //# sourceMappingURL=herdr-name.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"herdr-name.js","sourceRoot":"","sources":["../src/herdr-name.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,4DAA4D;AAC5D,IAAI,QAAQ,GAAG,CAAC,CAAC;AAEjB;;;;;GAKG;AACH,MAAM,cAAc,GAAG,EAAE,CAAC;AAE1B;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,QAAQ,IAAI,CAAC,CAAC;IACd,MAAM,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;IAC9B,MAAM,OAAO,GAAG,IAAI;SACjB,WAAW,EAAE;SACb,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC,8DAA8D;SAC5F,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;SACtB,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,sEAAsE;IAClG,wGAAwG;IACxG,6GAA6G;IAC7G,MAAM,IAAI,GAAG,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC;IAC5C,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC;IACrE,OAAO,GAAG,IAAI,GAAG,MAAM,EAAE,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Waiting for a herdr agent to settle, and reading what it printed on the way.
3
+ *
4
+ * Split out of `src/run-herdr.ts` when that file hit **404 of the 400-line ceiling** adding ADR-0032's output
5
+ * polling. The seam was named in the plan before it was needed, and it is a real one: this module is about
6
+ * *observing* an agent, `run-herdr.ts` is about *starting and cleaning up after* one. Nothing here creates or
7
+ * destroys anything.
8
+ *
9
+ * The two facts it is built on were measured against real herdr 0.7.5 (`docs/probes/g16-herdr`) and both are
10
+ * counter-intuitive enough to be worth the module comment: `agent wait --until idle` matches the state the
11
+ * agent was **already** in, and `agent read` is the one command that does **not** return a JSON envelope.
12
+ */
13
+ import { type HerdrExec } from "./herdr-cli.ts";
14
+ /**
15
+ * What `waitForSettled` needs from a run request.
16
+ *
17
+ * Declared here rather than importing `HerdrRunRequest`, which would make the two modules mutually dependent
18
+ * for no benefit. `HerdrRunRequest` satisfies it structurally, so the call site needs no adapter.
19
+ */
20
+ export interface PollTarget {
21
+ /** herdr agent name. */
22
+ name: string;
23
+ signal?: AbortSignal;
24
+ /**
25
+ * The pane's last few lines, re-reported on every poll — a SNAPSHOT, not a stream (ADR-0032).
26
+ *
27
+ * The consumer must **replace** what it holds rather than append. `agent read` returns a snapshot of a
28
+ * bounded terminal, and treating it as append-only is what produced an 89,000× amplification; see
29
+ * `tailLines`.
30
+ */
31
+ onSnapshot?: (lines: string[]) => void;
32
+ /** How many lines the display wants. Bounds the per-poll cost regardless of how big the pane is. */
33
+ snapshotLines?: number;
34
+ /** Poll cadence override. Exists so tests do not wait `POLL_INTERVAL_MS` per state transition. */
35
+ pollIntervalMs?: number;
36
+ }
37
+ /** How often to poll `agent get` while waiting for the child to settle. */
38
+ export declare const POLL_INTERVAL_MS = 750;
39
+ /** Lines of pane tail reported per poll. Matches the status block's own tail, so nothing is fetched unused. */
40
+ export declare const DEFAULT_SNAPSHOT_LINES = 3;
41
+ /**
42
+ * Wait for the child to settle, without accepting the state it was already in.
43
+ *
44
+ * **R-33, measured.** `herdr agent wait --until idle` called right after `agent prompt` returned
45
+ * *immediately*, matching the agent's **pre-existing** idle state with `state_change_seq` unchanged — a
46
+ * reply indistinguishable from a completed run. For fan-out that is not an inconvenience but a
47
+ * correctness bug: an orchestrator would "collect" N children that never ran and merge N empty results
48
+ * into a confident summary (R-03 with a new cause).
49
+ *
50
+ * So this polls `agent get` and requires **both** that the status is terminal **and** that
51
+ * `state_change_seq` has advanced past the value observed before prompting. `agent wait` is deliberately
52
+ * not used at all: its contract cannot express "settled *after* this point".
53
+ */
54
+ export declare function waitForSettled(exec: HerdrExec, request: PollTarget, before: number, deadline: number, maxOutputBytes: number): Promise<{
55
+ status?: string;
56
+ timedOut?: boolean;
57
+ aborted?: boolean;
58
+ spawnError?: string;
59
+ }>;
60
+ /**
61
+ * The last `keep` non-blank lines of a pane snapshot — what the display actually needs.
62
+ *
63
+ * **This replaces a `newSuffix` diff, and the replacement is a correction rather than a tune-up.** The old
64
+ * design treated `agent read` as a *stream* and tried to report only what was new, by testing whether the new
65
+ * text extended the old. That is wrong about the substrate: `agent read` returns a **snapshot of a bounded
66
+ * terminal**, and a snapshot is not an append-only log. Two ordinary things break the prefix test forever —
67
+ * the pane **scrolling** (its top lines are gone, so the new text is not an extension of the old) and
68
+ * `readPane` **truncating to the tail** past `maxOutputBytes` (each read is a different window of a growing
69
+ * buffer). Once either happens, every poll reported the whole buffer.
70
+ *
71
+ * Measured before the fix: **51 MiB streamed for ~600 bytes of real output — 89,000× amplification** in 37
72
+ * seconds, per child, with a scrolling pane also delivering the same real lines three times each. The old
73
+ * docstring named that exact failure as the thing it prevented.
74
+ *
75
+ * So the herdr path now reports a **bounded snapshot** and the consumer *replaces* rather than appends. There
76
+ * is no diff to get wrong, the per-poll cost is `keep` lines regardless of buffer size, and a scrolling pane
77
+ * simply shows its current tail — which is what a human looking at that pane would see.
78
+ */
79
+ export declare function tailLines(snapshot: string, keep: number): string[];
80
+ /**
81
+ * Read the pane's contents.
82
+ *
83
+ * `agent read` is the ONE command that does not return herdr's JSON envelope — it writes the terminal's
84
+ * text straight to stdout. Running it through `parseReply` turned every successful read into
85
+ * "unparseable herdr reply", i.e. reported the child's actual answer as a failure to read it. Found by the
86
+ * end-to-end run; the unit fake had been written to the envelope shape and so agreed with the bug.
87
+ *
88
+ * A JSON envelope is still accepted first, because an `error` reply here IS JSON and must not be mistaken
89
+ * for terminal output.
90
+ *
91
+ * **`readFailed` is separate from `text`, and that separation is the fix for an R-03 defect.** A failed read
92
+ * used to return its own diagnostic *as* `text` — so `runHerdrPane` returned
93
+ * `[grants] could not read the agent pane: pane is gone` **as the child's answer, with `code: 0`**, and the
94
+ * orchestrator read a failure message as a completed sub-agent's report. Measured. It mattered little when this
95
+ * ran once per child; ADR-0032 made it run on every poll, up to 800 times for a ten-minute child, so a
96
+ * transient failure went from unlikely to expected. The caller must now decide, and it cannot do so by
97
+ * inspecting a string.
98
+ */
99
+ export declare function readPane(exec: HerdrExec, name: string, maxOutputBytes: number): Promise<{
100
+ text: string;
101
+ truncated: boolean;
102
+ readFailed?: string;
103
+ }>;
104
+ //# sourceMappingURL=herdr-poll.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"herdr-poll.d.ts","sourceRoot":"","sources":["../src/herdr-poll.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACvC,oGAAoG;IACpG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kGAAkG;IAClG,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAKD,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,+GAA+G;AAC/G,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC;;;;;;;;;;;;GAYG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAoC1F;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAalE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAuBpE"}