@ai-codebot/daemon 0.1.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 (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/dist/broker/handoff.d.ts +57 -0
  4. package/dist/broker/handoff.js +87 -0
  5. package/dist/broker/handoff.js.map +1 -0
  6. package/dist/broker/server.d.ts +34 -0
  7. package/dist/broker/server.js +204 -0
  8. package/dist/broker/server.js.map +1 -0
  9. package/dist/broker/token.d.ts +36 -0
  10. package/dist/broker/token.js +48 -0
  11. package/dist/broker/token.js.map +1 -0
  12. package/dist/cli.d.ts +15 -0
  13. package/dist/cli.js +120 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/config.d.ts +33 -0
  16. package/dist/config.js +132 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/frames.d.ts +311 -0
  19. package/dist/frames.js +137 -0
  20. package/dist/frames.js.map +1 -0
  21. package/dist/index.d.ts +9 -0
  22. package/dist/index.js +19 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/launch.d.ts +40 -0
  25. package/dist/launch.js +163 -0
  26. package/dist/launch.js.map +1 -0
  27. package/dist/logging.d.ts +27 -0
  28. package/dist/logging.js +91 -0
  29. package/dist/logging.js.map +1 -0
  30. package/dist/spawn/cli-runner.d.ts +91 -0
  31. package/dist/spawn/cli-runner.js +180 -0
  32. package/dist/spawn/cli-runner.js.map +1 -0
  33. package/dist/spawn/detect.d.ts +18 -0
  34. package/dist/spawn/detect.js +46 -0
  35. package/dist/spawn/detect.js.map +1 -0
  36. package/dist/spawn/disallowed-tools.d.ts +15 -0
  37. package/dist/spawn/disallowed-tools.js +30 -0
  38. package/dist/spawn/disallowed-tools.js.map +1 -0
  39. package/dist/spawn/git.d.ts +19 -0
  40. package/dist/spawn/git.js +56 -0
  41. package/dist/spawn/git.js.map +1 -0
  42. package/dist/spawn/llm-client.d.ts +52 -0
  43. package/dist/spawn/llm-client.js +61 -0
  44. package/dist/spawn/llm-client.js.map +1 -0
  45. package/dist/spawn/n2-runner.d.ts +33 -0
  46. package/dist/spawn/n2-runner.js +176 -0
  47. package/dist/spawn/n2-runner.js.map +1 -0
  48. package/dist/spawn/n2-tools.d.ts +44 -0
  49. package/dist/spawn/n2-tools.js +374 -0
  50. package/dist/spawn/n2-tools.js.map +1 -0
  51. package/dist/spawn/result-map.d.ts +40 -0
  52. package/dist/spawn/result-map.js +68 -0
  53. package/dist/spawn/result-map.js.map +1 -0
  54. package/dist/tunnel.d.ts +61 -0
  55. package/dist/tunnel.js +205 -0
  56. package/dist/tunnel.js.map +1 -0
  57. package/package.json +60 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * PRIMARY executor: spawn a real coding-agent CLI (Claude Code / Codex) in
3
+ * `cwd=repo`, headless, talking to the broker, with the disallowed-tools blacklist
4
+ * applied (design §4 — R1-NEW / DR-6).
5
+ *
6
+ * ─────────────────────────────────────────────────────────────────────────────
7
+ * OQ-3: the headless / JSON-output / disallowed-tools flag contract per shipped
8
+ * CLI. `claude` (Claude Code 2.1.169) flag NAMES are VERIFIED against the real
9
+ * binary (see the contract entry below); `codex` (codex-cli 0.137.0) is left
10
+ * `verified:false` because it has NO disallowed-tools concept — its sandbox /
11
+ * approval model does not map to the C5 tool blacklist, so enabling PRIMARY-codex
12
+ * needs a design decision and until then the runner FAILS FAST → N2. The runner
13
+ * NEVER emits a silent wrong invocation. The N2 fallback (n2-runner.ts) is the
14
+ * fully-tested path that covers "no verified CLI".
15
+ *
16
+ * NOTE: the live-model E2E spawn proof remains the MANUAL `verify-daemon.mjs`
17
+ * step — these unit tests pin the argv shape, not a real spawn against a model.
18
+ * ─────────────────────────────────────────────────────────────────────────────
19
+ *
20
+ * To enable a CLI here: verify the real flag names against the shipped binary,
21
+ * flip the `verified: true` contract entry, and add an integration proof in
22
+ * `verify-daemon.mjs`. Until then this runner throws `CliContractUnverifiedError`
23
+ * and `launch.ts` routes to N2.
24
+ */
25
+ import { spawn } from "node:child_process";
26
+ import { log } from "../logging.js";
27
+ import { DISALLOWED_TOOLS } from "./disallowed-tools.js";
28
+ import { captureDiff, headSha } from "./git.js";
29
+ const CLI_FLAG_CONTRACT = {
30
+ // Claude Code 2.1.169 — flag NAMES VERIFIED against the real binary. `--print`
31
+ // is headless; `--output-format stream-json`; `--permission-mode
32
+ // bypassPermissions`; `--disallowedTools <tools...>` is VARIADIC (one flag,
33
+ // space-separated tool list). Because the variadic list would greedily eat the
34
+ // trailing positional prompt as another tool name, buildCliArgv emits a `--`
35
+ // separator before the prompt (see below).
36
+ claude: {
37
+ verified: true,
38
+ subcommand: null,
39
+ headlessFlag: "--print",
40
+ jsonOutputFlag: "--output-format=stream-json",
41
+ bypassFlag: "--permission-mode=bypassPermissions",
42
+ disallowStyle: "variadic",
43
+ disallowFlag: "--disallowedTools",
44
+ },
45
+ // codex-cli 0.137.0 — non-interactive mode is the `exec` SUBCOMMAND
46
+ // (`codex exec [OPTIONS] [PROMPT]`); JSON via `--json`; sandbox via
47
+ // `-s/--sandbox <mode>` or `--dangerously-bypass-approvals-and-sandbox`.
48
+ // codex has NO disallowed-tools flag: it governs tools via sandbox/approval,
49
+ // which does NOT map to a tool blacklist. So `disallowStyle: "none"` and we
50
+ // KEEP `verified: false` — enabling PRIMARY-codex needs a design decision for
51
+ // how the C5 blacklist maps; until then the runner fails fast → N2.
52
+ codex: {
53
+ verified: false,
54
+ subcommand: "exec",
55
+ headlessFlag: "",
56
+ jsonOutputFlag: "--json",
57
+ bypassFlag: "--dangerously-bypass-approvals-and-sandbox",
58
+ disallowStyle: "none",
59
+ disallowFlag: "",
60
+ },
61
+ };
62
+ export class CliContractUnverifiedError extends Error {
63
+ constructor(cli) {
64
+ super(`CLI '${cli}' flag contract is UNVERIFIED (OQ-3) — refusing to spawn a possibly-wrong ` +
65
+ `invocation. Falling back to N2. Verify the real flag names and set verified:true.`);
66
+ this.name = "CliContractUnverifiedError";
67
+ }
68
+ }
69
+ /**
70
+ * Build the argv for a CLI invocation. PUBLIC for unit testing the argv shape
71
+ * (disallowed-tools present, bypass flag, headless+json) without spawning.
72
+ */
73
+ export function buildCliArgv(cli, prompt) {
74
+ const contract = CLI_FLAG_CONTRACT[cli];
75
+ const args = [];
76
+ // Some CLIs put non-interactive mode behind a subcommand (codex's `exec`).
77
+ if (contract.subcommand !== null) {
78
+ args.push(contract.subcommand);
79
+ }
80
+ // headlessFlag may be empty when the subcommand itself IS the headless mode.
81
+ if (contract.headlessFlag !== "") {
82
+ args.push(contract.headlessFlag);
83
+ }
84
+ args.push(contract.jsonOutputFlag, contract.bypassFlag);
85
+ // The C5 blacklist mapping is per-CLI (see CliFlagContract.disallowStyle).
86
+ let emittedVariadic = false;
87
+ switch (contract.disallowStyle) {
88
+ case "variadic":
89
+ // ONE flag followed by every tool as a separate arg (claude). The list is
90
+ // variadic, so it would greedily consume the trailing positional prompt as
91
+ // another tool — guarded by the `--` separator emitted below.
92
+ args.push(contract.disallowFlag, ...DISALLOWED_TOOLS);
93
+ emittedVariadic = true;
94
+ break;
95
+ case "repeated":
96
+ for (const tool of DISALLOWED_TOOLS) {
97
+ args.push(contract.disallowFlag, tool);
98
+ }
99
+ break;
100
+ case "csv":
101
+ args.push(contract.disallowFlag, DISALLOWED_TOOLS.join(","));
102
+ break;
103
+ case "none":
104
+ // The CLI has no disallowed-tools concept (codex); nothing to emit.
105
+ break;
106
+ }
107
+ // Prompt passed positionally last. When a variadic flag precedes it, emit `--`
108
+ // first so the variadic list stops and the prompt is not eaten as a tool name.
109
+ if (emittedVariadic) {
110
+ args.push("--");
111
+ }
112
+ args.push(prompt);
113
+ return { command: cli, args, contract };
114
+ }
115
+ /**
116
+ * Run the PRIMARY CLI. Throws `CliContractUnverifiedError` if the flag contract is
117
+ * not verified (OQ-3 fail-fast) so the caller routes to N2 — NEVER a silent wrong
118
+ * invocation. On a verified contract, spawns, captures the BASE..HEAD diff, and
119
+ * maps the exit code to an outcome.
120
+ */
121
+ export async function runCli(params) {
122
+ const { cli, repo, prompt, env, signal } = params;
123
+ const { command, args, contract } = buildCliArgv(cli, prompt);
124
+ if (!contract.verified) {
125
+ // OQ-3 fail-fast: do not spawn a possibly-wrong invocation.
126
+ throw new CliContractUnverifiedError(cli);
127
+ }
128
+ const base = await headSha(repo);
129
+ if (base === null) {
130
+ return { kind: "failed", error: `repo '${repo}' is not a git repository` };
131
+ }
132
+ log.info("spawn.cli.start", { cli, repo });
133
+ return new Promise((resolvePromise) => {
134
+ const child = spawn(command, args, {
135
+ cwd: repo,
136
+ env,
137
+ stdio: ["ignore", "pipe", "pipe"],
138
+ signal,
139
+ });
140
+ let stderrTail = "";
141
+ let stdoutTail = "";
142
+ child.stdout?.on("data", (chunk) => {
143
+ stdoutTail = (stdoutTail + chunk.toString("utf8")).slice(-65536);
144
+ });
145
+ child.stderr?.on("data", (chunk) => {
146
+ stderrTail = (stderrTail + chunk.toString("utf8")).slice(-65536);
147
+ });
148
+ child.on("error", (err) => {
149
+ if (err.name === "AbortError") {
150
+ resolvePromise({ kind: "timed_out" });
151
+ return;
152
+ }
153
+ resolvePromise({
154
+ kind: "failed",
155
+ error: `failed to spawn ${cli}: ${err.message}`,
156
+ });
157
+ });
158
+ child.on("close", async (code, sigterm) => {
159
+ if (signal.aborted) {
160
+ resolvePromise({ kind: "timed_out" });
161
+ return;
162
+ }
163
+ const { diff, files } = await captureDiff(repo, base);
164
+ if (code === 0) {
165
+ resolvePromise({
166
+ kind: "success",
167
+ text: stdoutTail.trim() || null,
168
+ diff: diff || null,
169
+ files,
170
+ });
171
+ return;
172
+ }
173
+ resolvePromise({
174
+ kind: "failed",
175
+ error: `${cli} exited with code ${code ?? sigterm}: ${stderrTail.trim()}`,
176
+ });
177
+ });
178
+ });
179
+ }
180
+ //# sourceMappingURL=cli-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-runner.js","sourceRoot":"","sources":["../../src/spawn/cli-runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * CLI detection (design §4). Decides whether a real coding-agent CLI is available
3
+ * on PATH for the requested `--cli` mode, or whether to fall back to N2.
4
+ *
5
+ * Detection is a `which`-style PATH probe (no execution of the CLI), so it is cheap
6
+ * and side-effect-free. The PRIMARY runner does the real fail-fast contract check
7
+ * (cli-runner.ts).
8
+ */
9
+ import type { CliKind } from "../config.js";
10
+ export type DetectedCli = "claude" | "codex" | null;
11
+ /** True if `name` is found on PATH (POSIX `which` equivalent, no execution). */
12
+ export declare function isOnPath(name: string, env?: NodeJS.ProcessEnv): boolean;
13
+ /**
14
+ * Resolve which CLI to use for the requested mode, or null to force N2.
15
+ * - `auto`: prefer claude, then codex, else null.
16
+ * - `claude` / `codex`: that one if present, else null (→ N2, never the wrong CLI).
17
+ */
18
+ export declare function detectCli(mode: CliKind, env?: NodeJS.ProcessEnv): DetectedCli;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * CLI detection (design §4). Decides whether a real coding-agent CLI is available
3
+ * on PATH for the requested `--cli` mode, or whether to fall back to N2.
4
+ *
5
+ * Detection is a `which`-style PATH probe (no execution of the CLI), so it is cheap
6
+ * and side-effect-free. The PRIMARY runner does the real fail-fast contract check
7
+ * (cli-runner.ts).
8
+ */
9
+ import { existsSync } from "node:fs";
10
+ import { delimiter, join } from "node:path";
11
+ const CLI_BINARIES = {
12
+ // On Windows these would be `.cmd`; the daemon targets Node 20 on POSIX hosts.
13
+ claude: ["claude"],
14
+ codex: ["codex"],
15
+ };
16
+ /** True if `name` is found on PATH (POSIX `which` equivalent, no execution). */
17
+ export function isOnPath(name, env = process.env) {
18
+ const pathVar = env["PATH"] ?? "";
19
+ for (const dir of pathVar.split(delimiter)) {
20
+ if (!dir)
21
+ continue;
22
+ if (existsSync(join(dir, name)))
23
+ return true;
24
+ }
25
+ return false;
26
+ }
27
+ /**
28
+ * Resolve which CLI to use for the requested mode, or null to force N2.
29
+ * - `auto`: prefer claude, then codex, else null.
30
+ * - `claude` / `codex`: that one if present, else null (→ N2, never the wrong CLI).
31
+ */
32
+ export function detectCli(mode, env = process.env) {
33
+ if (mode === "claude") {
34
+ return CLI_BINARIES.claude.some((b) => isOnPath(b, env)) ? "claude" : null;
35
+ }
36
+ if (mode === "codex") {
37
+ return CLI_BINARIES.codex.some((b) => isOnPath(b, env)) ? "codex" : null;
38
+ }
39
+ // auto
40
+ if (CLI_BINARIES.claude.some((b) => isOnPath(b, env)))
41
+ return "claude";
42
+ if (CLI_BINARIES.codex.some((b) => isOnPath(b, env)))
43
+ return "codex";
44
+ return null;
45
+ }
46
+ //# sourceMappingURL=detect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect.js","sourceRoot":"","sources":["../../src/spawn/detect.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAM5C,MAAM,YAAY,GAAyC;IACzD,+EAA+E;IAC/E,MAAM,EAAE,CAAC,QAAQ,CAAC;IAClB,KAAK,EAAE,CAAC,OAAO,CAAC;CACjB,CAAC;AAEF,gFAAgF;AAChF,MAAM,UAAU,QAAQ,CACtB,IAAY,EACZ,MAAyB,OAAO,CAAC,GAAG;IAEpC,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAClC,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3C,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;IAC/C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CACvB,IAAa,EACb,MAAyB,OAAO,CAAC,GAAG;IAEpC,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,OAAO,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7E,CAAC;IACD,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACrB,OAAO,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3E,CAAC;IACD,OAAO;IACP,IAAI,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAAE,OAAO,QAAQ,CAAC;IACvE,IAAI,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAAE,OAAO,OAAO,CAAC;IACrE,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Disallowed-tools blacklist (design §4 — C5). SINGLE SOURCE OF TRUTH.
3
+ *
4
+ * These tool names must never be available to the spawned agent: for the real CLI
5
+ * they become `--disallowedTools` flags (stripped before spawn); for the N2 local
6
+ * tool-loop they are simply never registered.
7
+ *
8
+ * Rationale: the local runner executes one bounded launch. Tools that schedule
9
+ * future work, spawn sub-agents, or dispatch tasks would let the agent escape that
10
+ * bound (persistence / fan-out / lateral movement). The path allowlist + broker are
11
+ * the credential boundary; this list is the capability boundary.
12
+ */
13
+ export declare const DISALLOWED_TOOLS: readonly string[];
14
+ /** True if a tool name is on the blacklist (case-sensitive, exact match). */
15
+ export declare function isDisallowed(toolName: string): boolean;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Disallowed-tools blacklist (design §4 — C5). SINGLE SOURCE OF TRUTH.
3
+ *
4
+ * These tool names must never be available to the spawned agent: for the real CLI
5
+ * they become `--disallowedTools` flags (stripped before spawn); for the N2 local
6
+ * tool-loop they are simply never registered.
7
+ *
8
+ * Rationale: the local runner executes one bounded launch. Tools that schedule
9
+ * future work, spawn sub-agents, or dispatch tasks would let the agent escape that
10
+ * bound (persistence / fan-out / lateral movement). The path allowlist + broker are
11
+ * the credential boundary; this list is the capability boundary.
12
+ */
13
+ export const DISALLOWED_TOOLS = [
14
+ "EnterPlanMode",
15
+ "ExitPlanMode",
16
+ "ScheduleWakeup",
17
+ "Cron",
18
+ "CronCreate",
19
+ "CronDelete",
20
+ "CronList",
21
+ "Task",
22
+ "Agent",
23
+ "SpawnAgent",
24
+ "Dispatch",
25
+ ];
26
+ /** True if a tool name is on the blacklist (case-sensitive, exact match). */
27
+ export function isDisallowed(toolName) {
28
+ return DISALLOWED_TOOLS.includes(toolName);
29
+ }
30
+ //# sourceMappingURL=disallowed-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disallowed-tools.js","sourceRoot":"","sources":["../../src/spawn/disallowed-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IACjD,eAAe;IACf,cAAc;IACd,gBAAgB;IAChB,MAAM;IACN,YAAY;IACZ,YAAY;IACZ,UAAU;IACV,MAAM;IACN,OAAO;IACP,YAAY;IACZ,UAAU;CACF,CAAC;AAEX,6EAA6E;AAC7E,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,OAAO,gBAAgB,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Minimal git helpers for diff capture (design §4 — DR-6, mirrors the Python
3
+ * `agentic_backend` A2 contract: capture BASE..HEAD, the workspace is a real repo,
4
+ * never `git init`).
5
+ *
6
+ * All git runs via `execFile` (argv only, `shell=false`) — no model-supplied string
7
+ * ever reaches a shell.
8
+ */
9
+ export interface GitDiff {
10
+ diff: string;
11
+ files: string[];
12
+ }
13
+ /** Resolve the current HEAD sha (the diff base, captured before the agent runs). */
14
+ export declare function headSha(repo: string): Promise<string | null>;
15
+ /**
16
+ * Capture the working-tree diff against `baseSha`. Stages all changes first (to
17
+ * include new files), then diffs `baseSha..` against the index + worktree.
18
+ */
19
+ export declare function captureDiff(repo: string, baseSha: string): Promise<GitDiff>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Minimal git helpers for diff capture (design §4 — DR-6, mirrors the Python
3
+ * `agentic_backend` A2 contract: capture BASE..HEAD, the workspace is a real repo,
4
+ * never `git init`).
5
+ *
6
+ * All git runs via `execFile` (argv only, `shell=false`) — no model-supplied string
7
+ * ever reaches a shell.
8
+ */
9
+ import { execFile } from "node:child_process";
10
+ import { promisify } from "node:util";
11
+ const execFileAsync = promisify(execFile);
12
+ async function git(repo, args) {
13
+ try {
14
+ const { stdout } = await execFileAsync("git", args, {
15
+ cwd: repo,
16
+ maxBuffer: 64 * 1024 * 1024,
17
+ });
18
+ return { code: 0, stdout };
19
+ }
20
+ catch (err) {
21
+ const e = err;
22
+ return {
23
+ code: typeof e.code === "number" ? e.code : 1,
24
+ stdout: e.stdout ?? "",
25
+ };
26
+ }
27
+ }
28
+ /** Resolve the current HEAD sha (the diff base, captured before the agent runs). */
29
+ export async function headSha(repo) {
30
+ const { code, stdout } = await git(repo, ["rev-parse", "HEAD"]);
31
+ if (code !== 0)
32
+ return null;
33
+ const sha = stdout.trim();
34
+ return sha || null;
35
+ }
36
+ /**
37
+ * Capture the working-tree diff against `baseSha`. Stages all changes first (to
38
+ * include new files), then diffs `baseSha..` against the index + worktree.
39
+ */
40
+ export async function captureDiff(repo, baseSha) {
41
+ // `git add -A` so new/untracked files show in the diff; this does not commit.
42
+ await git(repo, ["add", "-A"]);
43
+ const diffRes = await git(repo, ["diff", "--cached", baseSha]);
44
+ const filesRes = await git(repo, [
45
+ "diff",
46
+ "--cached",
47
+ "--name-only",
48
+ baseSha,
49
+ ]);
50
+ const files = filesRes.stdout
51
+ .split("\n")
52
+ .map((f) => f.trim())
53
+ .filter((f) => f.length > 0);
54
+ return { diff: diffRes.stdout, files };
55
+ }
56
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/spawn/git.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAO1C,KAAK,UAAU,GAAG,CAChB,IAAY,EACZ,IAAc;IAEd,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE;YAClD,GAAG,EAAE,IAAI;YACT,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;SAC5B,CAAC,CAAC;QACH,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,GAAyC,CAAC;QACpD,OAAO;YACL,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC7C,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE;SACvB,CAAC;IACJ,CAAC;AACH,CAAC;AAED,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAY;IACxC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAChE,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC1B,OAAO,GAAG,IAAI,IAAI,CAAC;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAY,EACZ,OAAe;IAEf,8EAA8E;IAC9E,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;IAC/B,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/D,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE;QAC/B,MAAM;QACN,UAAU;QACV,aAAa;QACb,OAAO;KACR,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM;SAC1B,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC/B,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;AACzC,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Minimal OpenAI-compatible chat-completions client for the N2 tool-loop.
3
+ *
4
+ * It talks ONLY to the broker (`baseUrl` = the 127.0.0.1 broker URL) with the
5
+ * per-launch capability token — never the real provider directly. The broker
6
+ * injects the real key and forwards upstream. The model id is supplied by the
7
+ * launch / config; the daemon does not pick a provider.
8
+ *
9
+ * Kept deliberately tiny (one POST, tool-calls in/out) — the broker is the security
10
+ * boundary; this is just request shaping.
11
+ */
12
+ export interface ChatMessage {
13
+ role: "system" | "user" | "assistant" | "tool";
14
+ content: string;
15
+ tool_calls?: ToolCall[];
16
+ tool_call_id?: string;
17
+ name?: string;
18
+ }
19
+ export interface ToolCall {
20
+ id: string;
21
+ type: "function";
22
+ function: {
23
+ name: string;
24
+ arguments: string;
25
+ };
26
+ }
27
+ export interface ToolDef {
28
+ type: "function";
29
+ function: {
30
+ name: string;
31
+ description: string;
32
+ parameters: Record<string, unknown>;
33
+ };
34
+ }
35
+ export interface CompletionResult {
36
+ content: string;
37
+ toolCalls: ToolCall[];
38
+ finishReason: string;
39
+ model: string;
40
+ }
41
+ export interface LlmClientOptions {
42
+ /** Broker base URL (`http://127.0.0.1:<port>`). */
43
+ readonly baseUrl: string;
44
+ /** Per-launch capability token (NOT the real key). */
45
+ readonly token: string;
46
+ readonly model: string;
47
+ readonly maxTokens: number;
48
+ }
49
+ export declare class LlmClientError extends Error {
50
+ }
51
+ /** One chat-completion round-trip through the broker. */
52
+ export declare function complete(opts: LlmClientOptions, messages: ChatMessage[], tools: ToolDef[], signal: AbortSignal): Promise<CompletionResult>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Minimal OpenAI-compatible chat-completions client for the N2 tool-loop.
3
+ *
4
+ * It talks ONLY to the broker (`baseUrl` = the 127.0.0.1 broker URL) with the
5
+ * per-launch capability token — never the real provider directly. The broker
6
+ * injects the real key and forwards upstream. The model id is supplied by the
7
+ * launch / config; the daemon does not pick a provider.
8
+ *
9
+ * Kept deliberately tiny (one POST, tool-calls in/out) — the broker is the security
10
+ * boundary; this is just request shaping.
11
+ */
12
+ export class LlmClientError extends Error {
13
+ }
14
+ /** One chat-completion round-trip through the broker. */
15
+ export async function complete(opts, messages, tools, signal) {
16
+ const url = `${opts.baseUrl.replace(/\/$/, "")}/v1/chat/completions`;
17
+ let res;
18
+ try {
19
+ res = await fetch(url, {
20
+ method: "POST",
21
+ headers: {
22
+ "content-type": "application/json",
23
+ authorization: `Bearer ${opts.token}`,
24
+ },
25
+ body: JSON.stringify({
26
+ model: opts.model,
27
+ messages,
28
+ tools,
29
+ tool_choice: "auto",
30
+ max_tokens: opts.maxTokens,
31
+ }),
32
+ signal,
33
+ });
34
+ }
35
+ catch (err) {
36
+ throw new LlmClientError(`model request failed: ${err.message}`);
37
+ }
38
+ if (!res.ok) {
39
+ throw new LlmClientError(`model returned HTTP ${res.status}`);
40
+ }
41
+ let body;
42
+ try {
43
+ body = await res.json();
44
+ }
45
+ catch (err) {
46
+ throw new LlmClientError(`model returned invalid JSON: ${err.message}`);
47
+ }
48
+ const choice = body?.choices?.[0];
49
+ if (!choice || !choice.message) {
50
+ throw new LlmClientError("model response missing choices[0].message");
51
+ }
52
+ return {
53
+ content: choice.message.content ?? "",
54
+ toolCalls: Array.isArray(choice.message.tool_calls)
55
+ ? choice.message.tool_calls
56
+ : [],
57
+ finishReason: choice.finish_reason ?? "stop",
58
+ model: body.model ?? opts.model,
59
+ };
60
+ }
61
+ //# sourceMappingURL=llm-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-client.js","sourceRoot":"","sources":["../../src/spawn/llm-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAyCH,MAAM,OAAO,cAAe,SAAQ,KAAK;CAAG;AAE5C,yDAAyD;AACzD,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,IAAsB,EACtB,QAAuB,EACvB,KAAgB,EAChB,MAAmB;IAEnB,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,sBAAsB,CAAC;IACrE,IAAI,GAAa,CAAC;IAClB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YACrB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,IAAI,CAAC,KAAK,EAAE;aACtC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,QAAQ;gBACR,KAAK;gBACL,WAAW,EAAE,MAAM;gBACnB,UAAU,EAAE,IAAI,CAAC,SAAS;aAC3B,CAAC;YACF,MAAM;SACP,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,cAAc,CAAC,yBAA0B,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,cAAc,CAAC,uBAAuB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAChE,CAAC;IAED,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,cAAc,CACtB,gCAAiC,GAAa,CAAC,OAAO,EAAE,CACzD,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAI,IAAgC,EAAE,OAAO,EAAE,CAAC,CAAC,CAKhD,CAAC;IACd,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QAC/B,MAAM,IAAI,cAAc,CAAC,2CAA2C,CAAC,CAAC;IACxE,CAAC;IACD,OAAO;QACL,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE;QACrC,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC;YACjD,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU;YAC3B,CAAC,CAAC,EAAE;QACN,YAAY,EAAE,MAAM,CAAC,aAAa,IAAI,MAAM;QAC5C,KAAK,EAAG,IAA2B,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK;KACxD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * FALLBACK executor: the N2 local tool-loop (design §4 — ports
3
+ * `agentic_backend.py` A1/A2/A4/A5/A9 to TS). The fully-working, fully-tested path
4
+ * used when no verified CLI is available or `--cli` forces N2.
5
+ *
6
+ * Loop protocol (mirrors the Python backend):
7
+ * 1. Record HEAD as the diff base (A2 — workspace is a real repo, no `git init`).
8
+ * 2. system + user messages → model (through the broker) with the closed toolset.
9
+ * 3. Append ALL tool results before honoring `finish` (A4); `finish` ends the loop.
10
+ * 4. On `finish`: capture BASE..worktree diff; map to an outcome.
11
+ *
12
+ * `success` (Python) → `succeeded` (wire) happens in result-map.ts; this returns the
13
+ * internal `ExecOutcome`.
14
+ */
15
+ import type { ExecOutcome } from "./result-map.js";
16
+ export interface N2RunParams {
17
+ readonly repo: string;
18
+ readonly prompt: string;
19
+ readonly model: string;
20
+ /** Broker base URL. */
21
+ readonly brokerBaseUrl: string;
22
+ /** Per-launch capability token. */
23
+ readonly token: string;
24
+ readonly env: NodeJS.ProcessEnv;
25
+ readonly testTimeoutMs: number;
26
+ readonly signal: AbortSignal;
27
+ readonly maxTurns?: number;
28
+ }
29
+ /**
30
+ * Run the N2 tool-loop. Returns an internal `ExecOutcome`. NEVER throws on a
31
+ * model/tool error — that becomes a `failed` outcome (零静默: exactly one outcome).
32
+ */
33
+ export declare function runN2(params: N2RunParams): Promise<ExecOutcome>;