@ze-norm/cli 0.12.0 → 0.14.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAyBA,kFAAkF;AAClF,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA0BD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAS;IAIxB,OAAO,CAAC,aAAa,CAAgB;gBAEzB,IAAI,CAAC,EAAE,gBAAgB;IAKnC,wEAAwE;YAC1D,mBAAmB;IAMjC,OAAO,CAAC,YAAY;IA0BpB,OAAO,CAAC,cAAc;IAStB,2EAA2E;IAC3E,gBAAgB,IAAI,OAAO;IAI3B,uEAAuE;IACvE,qBAAqB,IAAI,OAAO;IAQhC,OAAO,CAAC,2BAA2B;YAIrB,QAAQ;YAqBR,OAAO;IAqEf,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIjD,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhD,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIlD,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAIzC;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC;CAG7C"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AA0BA,kFAAkF;AAClF,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAiCD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAS;IAIxB,OAAO,CAAC,aAAa,CAAgB;gBAEzB,IAAI,CAAC,EAAE,gBAAgB;IAKnC,wEAAwE;YAC1D,mBAAmB;IAMjC,OAAO,CAAC,YAAY;IA0BpB,OAAO,CAAC,cAAc;IAStB,2EAA2E;IAC3E,gBAAgB,IAAI,OAAO;IAI3B,uEAAuE;IACvE,qBAAqB,IAAI,OAAO;IAYhC,OAAO,CAAC,2BAA2B;YAIrB,QAAQ;YAqBR,OAAO;IAqEf,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIjD,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhD,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIlD,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAIzC;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC;CAG7C"}
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { ApiError, AuthError, UpgradeRequiredError } from "../util/errors.js";
4
4
  import { forceRefreshToken, hasRefreshableCredentials, resolveValidToken, } from "../auth/store.js";
5
5
  import { PRODUCTION_API_URL } from "../config/defaults.js";
6
+ import { resolveEnvironment, ZENORM_CLI_ENV_VAR } from "../config/environments.js";
6
7
  import { getCliVersion } from "../util/package.js";
7
8
  import { log } from "../util/logger.js";
8
9
  const UPGRADE_COMMAND = "npm install -g @ze-norm/cli@latest";
@@ -23,11 +24,17 @@ function resolveBaseUrl(explicit) {
23
24
  // 1. Explicit option
24
25
  if (explicit)
25
26
  return explicit;
26
- // 2. Environment variable
27
+ // 2. Named environment via ZENORM_CLI. A selected environment is an explicit
28
+ // "talk to this stack" intent, so its API URL wins over a stray
29
+ // ZENORM_API_URL / .zenorm.json left over from another workflow.
30
+ if (process.env[ZENORM_CLI_ENV_VAR]) {
31
+ return resolveEnvironment().apiUrl;
32
+ }
33
+ // 3. Environment variable
27
34
  const envUrl = process.env["ZENORM_API_URL"];
28
35
  if (envUrl)
29
36
  return envUrl;
30
- // 3. .zenorm.json config file in cwd
37
+ // 4. .zenorm.json config file in cwd
31
38
  const configPath = join(process.cwd(), ".zenorm.json");
32
39
  if (existsSync(configPath)) {
33
40
  const raw = readFileSync(configPath, "utf-8");
@@ -35,7 +42,7 @@ function resolveBaseUrl(explicit) {
35
42
  if (config.apiUrl)
36
43
  return config.apiUrl;
37
44
  }
38
- // 4. Public CLI default
45
+ // 5. Public CLI default
39
46
  return PRODUCTION_API_URL;
40
47
  }
41
48
  export class ZenormClient {
@@ -95,9 +102,12 @@ export class ZenormClient {
95
102
  }
96
103
  /** True when requests will intentionally ignore stored credentials. */
97
104
  isUsingLocalDevBypass() {
98
- return (this.isLocalBaseUrl() &&
99
- process.env["ZENORM_LOCAL_DEV_BYPASS_AUTH"] === "true" &&
100
- !process.env["ZENORM_API_TOKEN"]);
105
+ // Selecting the `local` environment implies dev-bypass — that is what the
106
+ // local stack is (no real Clerk sign-in, dev-bypass headers). An explicit
107
+ // ZENORM_LOCAL_DEV_BYPASS_AUTH still enables it for a raw localhost URL.
108
+ const bypassEnabled = process.env["ZENORM_LOCAL_DEV_BYPASS_AUTH"] === "true" ||
109
+ resolveEnvironment().name === "local";
110
+ return (this.isLocalBaseUrl() && bypassEnabled && !process.env["ZENORM_API_TOKEN"]);
101
111
  }
102
112
  shouldBypassAuthForLocalDev() {
103
113
  return this.isUsingLocalDevBypass();
@@ -1 +1 @@
1
- {"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAkUpD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,CAuBD;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAkDpE"}
1
+ {"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA6UpD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,CAuBD;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAkDpE"}
@@ -5,6 +5,7 @@ import { platform } from "node:os";
5
5
  import { URL, URLSearchParams } from "node:url";
6
6
  import { log } from "../util/logger.js";
7
7
  import { CLI_CALLBACK_PORTS, PRODUCTION_CLERK_CLIENT_ID, PRODUCTION_CLERK_ISSUER, } from "../config/defaults.js";
8
+ import { resolveEnvironment, ZENORM_CLI_ENV_VAR } from "../config/environments.js";
8
9
  /**
9
10
  * Find the first pre-registered callback port that is free to bind. Clerk
10
11
  * matches redirect_uris exactly, so the local listener must use one of the
@@ -26,6 +27,11 @@ async function pickFreeCallbackPort() {
26
27
  `Free one of them and run \`zenorm login\` again.`);
27
28
  }
28
29
  function getClerkIssuer() {
30
+ // A named environment (ZENORM_CLI) selects the whole Clerk instance, so it
31
+ // wins over a stray ZENORM_CLERK_ISSUER from another workflow.
32
+ if (process.env[ZENORM_CLI_ENV_VAR]) {
33
+ return resolveEnvironment().clerkIssuer;
34
+ }
29
35
  return process.env["ZENORM_CLERK_ISSUER"] ?? PRODUCTION_CLERK_ISSUER;
30
36
  }
31
37
  /** Escape user-influenced text before interpolating into the result page HTML. */
@@ -38,6 +44,11 @@ function escapeHtml(value) {
38
44
  .replace(/'/g, "'");
39
45
  }
40
46
  function getClerkClientId() {
47
+ // A named environment (ZENORM_CLI) selects the whole Clerk instance, so it
48
+ // wins over stray ZENORM_CLERK_CLIENT_ID / VITE_CLERK_PUBLISHABLE_KEY vars.
49
+ if (process.env[ZENORM_CLI_ENV_VAR]) {
50
+ return resolveEnvironment().clerkClientId;
51
+ }
41
52
  return (process.env["ZENORM_CLERK_CLIENT_ID"] ??
42
53
  process.env["VITE_CLERK_PUBLISHABLE_KEY"] ??
43
54
  PRODUCTION_CLERK_CLIENT_ID);
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/auth/store.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAaD,wBAAgB,eAAe,IAAI,iBAAiB,GAAG,IAAI,CAO1D;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAM9D;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAU5C;AAWD;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwBhE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,OAAO,CAInD;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAezD"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/auth/store.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAmBD,wBAAgB,eAAe,IAAI,iBAAiB,GAAG,IAAI,CAO1D;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAM9D;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAU5C;AAWD;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwBhE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,OAAO,CAInD;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAezD"}
@@ -2,12 +2,18 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { refreshAccessToken } from "./device-flow.js";
5
+ import { resolveEnvironment } from "../config/environments.js";
5
6
  import { log } from "../util/logger.js";
6
7
  // Refresh the access token this many ms before it actually expires, so an
7
8
  // in-flight request never races the expiry boundary.
8
9
  const EXPIRY_SKEW_MS = 60_000;
10
+ // Credentials are scoped per environment so a prod login and a dev login can
11
+ // coexist instead of overwriting each other. Prod keeps the historical
12
+ // `credentials.json` filename for back-compat; other stacks get a suffix.
9
13
  function getCredentialsPath() {
10
- return join(homedir(), ".zenorm", "credentials.json");
14
+ const env = resolveEnvironment();
15
+ const filename = env.name === "prod" ? "credentials.json" : `credentials.${env.name}.json`;
16
+ return join(homedir(), ".zenorm", filename);
11
17
  }
12
18
  function ensureDir() {
13
19
  const dir = join(homedir(), ".zenorm");
@@ -0,0 +1,92 @@
1
+ import type { ChildProcess, SpawnOptions } from "node:child_process";
2
+ /**
3
+ * Opt-in coding-agent mode for local `zenorm work` runs.
4
+ *
5
+ * When a user opts in (`--interactive` or `.zenorm.json` `interactive: true`),
6
+ * the daemon does NOT run the agent headlessly. Instead it launches the active
7
+ * coding agent — Claude Code by default, or Codex when the user runs with
8
+ * `--agent codex` — as a *live interactive session*, seeded with the
9
+ * `/zenorm <specId>` handoff. The agent owns the terminal directly (inherited
10
+ * stdio), so the operator works with it interactively until they close it; when
11
+ * the agent exits, the daemon releases the worker and moves on to the next task.
12
+ *
13
+ * Scope is deliberately narrow (single local operator): the agent is attached
14
+ * to *this* process's own stdio. There is no network listener, no second
15
+ * connection, nothing remote — `assertLocalAgentOptions` enforces those
16
+ * boundaries; this module just launches the local interactive agent.
17
+ */
18
+ /**
19
+ * The minimal surface we need from a TTY check — injectable so tests can drive
20
+ * both the "usable TTY" and "no TTY" branches without a real terminal.
21
+ *
22
+ * A usable interactive agent needs BOTH stdin and stdout to be TTYs: stdin so
23
+ * the operator can type into the agent's TUI, stdout so it can render. We read
24
+ * them through this indirection rather than touching `process.std*.isTTY`
25
+ * inline.
26
+ */
27
+ export interface TtyProbe {
28
+ stdinIsTty: boolean;
29
+ stdoutIsTty: boolean;
30
+ }
31
+ /** Read the real process TTY state. */
32
+ export declare function processTtyProbe(): TtyProbe;
33
+ /**
34
+ * Injectable spawn signature (mirrors `work.ts`'s `SpawnFn`) so the interactive
35
+ * agent can be unit-tested without launching a real `claude`/`codex` process.
36
+ */
37
+ export type AgentSpawnFn = (command: string, args: string[], options: SpawnOptions) => ChildProcess;
38
+ /**
39
+ * Assert a usable local TTY is present, or fail loud. This is the fail-fast
40
+ * gate the spec's "Fail-fast without usable TTY" constraint requires: an
41
+ * interactive run with no terminal to attach to must error immediately rather
42
+ * than hang waiting on input that can never arrive (or silently degrade to a
43
+ * non-interactive run, which would mask the misconfiguration).
44
+ */
45
+ export declare function assertUsableTty(probe: TtyProbe): void;
46
+ /**
47
+ * Local-only guardrails for the interactive agent (spec: "No remote or shared
48
+ * shells", "Local single-operator agent scope"). These are STRUCTURAL
49
+ * guardrails: the agent never exposes a remote/shared surface in the first
50
+ * place. The interactive agent is, by construction, the operator's own
51
+ * `claude`/`codex` attached to *this* process's own terminal — there is no
52
+ * socket, no listener, no second connection, and it is bound to this process
53
+ * group so it cannot outlive (or be detached from) the local task.
54
+ * `buildLocalAgentOptions` is the single source of truth for those spawn
55
+ * options, and `assertLocalAgentOptions` enforces them so a future edit cannot
56
+ * silently introduce a detached or non-local agent session.
57
+ */
58
+ export declare function buildLocalAgentOptions(): SpawnOptions;
59
+ /**
60
+ * Guard the spawn options against any non-local/shared configuration. Fails
61
+ * loud (no masked default) if a caller ever hands the agent options that would
62
+ * detach it from this process or route its IO anywhere but this terminal.
63
+ */
64
+ export declare function assertLocalAgentOptions(options: SpawnOptions): void;
65
+ /**
66
+ * A live, interactive coding-agent session that replaces a task's headless run.
67
+ * `open` spawns `claude`/`codex` with inherited stdio (so the operator works
68
+ * with it directly, in its own TUI); the returned child resolves the run when
69
+ * it exits, which releases the worker. Constructed only after `assertUsableTty`
70
+ * has passed.
71
+ *
72
+ * Single-operator invariant: an instance opens AT MOST one agent session, ever.
73
+ * A second `open()` — whether the first session is still live or already
74
+ * closed — is refused loudly, so there is never more than one interactive agent
75
+ * behind a single task.
76
+ */
77
+ export declare class InteractiveAgent {
78
+ private readonly command;
79
+ private readonly args;
80
+ private readonly spawnFn;
81
+ private child;
82
+ private opened;
83
+ constructor(command: string, args: string[], spawnFn?: AgentSpawnFn);
84
+ /**
85
+ * Spawn the coding agent with inherited stdio so it owns the terminal and the
86
+ * operator works with it live. Returns the child so the caller can await its
87
+ * exit (which releases the worker). The single-operator invariant is enforced
88
+ * here: one session per instance, period.
89
+ */
90
+ open(): ChildProcess;
91
+ }
92
+ //# sourceMappingURL=interactive-agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interactive-agent.d.ts","sourceRoot":"","sources":["../../src/commands/interactive-agent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIrE;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;;GAQG;AACH,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,uCAAuC;AACvC,wBAAgB,eAAe,IAAI,QAAQ,CAK1C;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,CACzB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,YAAY,KAClB,YAAY,CAAC;AAElB;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAarD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,IAAI,YAAY,CAiBrD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAcnE;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,gBAAgB;IAKzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,MAAM,CAAS;gBAGJ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,YAAoB;IAGhD;;;;;OAKG;IACH,IAAI,IAAI,YAAY;CAmBrB"}
@@ -0,0 +1,120 @@
1
+ import { spawn } from "node:child_process";
2
+ import { CliError } from "../util/errors.js";
3
+ import { log } from "../util/logger.js";
4
+ /** Read the real process TTY state. */
5
+ export function processTtyProbe() {
6
+ return {
7
+ stdinIsTty: process.stdin.isTTY === true,
8
+ stdoutIsTty: process.stdout.isTTY === true,
9
+ };
10
+ }
11
+ /**
12
+ * Assert a usable local TTY is present, or fail loud. This is the fail-fast
13
+ * gate the spec's "Fail-fast without usable TTY" constraint requires: an
14
+ * interactive run with no terminal to attach to must error immediately rather
15
+ * than hang waiting on input that can never arrive (or silently degrade to a
16
+ * non-interactive run, which would mask the misconfiguration).
17
+ */
18
+ export function assertUsableTty(probe) {
19
+ if (!probe.stdinIsTty || !probe.stdoutIsTty) {
20
+ throw new CliError("Interactive coding-agent mode requires a local terminal (TTY), but none is attached.\n\n" +
21
+ "`zenorm work --interactive` launches the coding agent as a live session, " +
22
+ "which needs an interactive stdin and stdout. This run has " +
23
+ `${probe.stdinIsTty ? "" : "no TTY stdin"}` +
24
+ `${!probe.stdinIsTty && !probe.stdoutIsTty ? " and " : ""}` +
25
+ `${probe.stdoutIsTty ? "" : "no TTY stdout"}` +
26
+ ".\n\nRun it from a real terminal, or drop `--interactive` (and the " +
27
+ '`interactive` config flag) to run non-interactively.');
28
+ }
29
+ }
30
+ /**
31
+ * Local-only guardrails for the interactive agent (spec: "No remote or shared
32
+ * shells", "Local single-operator agent scope"). These are STRUCTURAL
33
+ * guardrails: the agent never exposes a remote/shared surface in the first
34
+ * place. The interactive agent is, by construction, the operator's own
35
+ * `claude`/`codex` attached to *this* process's own terminal — there is no
36
+ * socket, no listener, no second connection, and it is bound to this process
37
+ * group so it cannot outlive (or be detached from) the local task.
38
+ * `buildLocalAgentOptions` is the single source of truth for those spawn
39
+ * options, and `assertLocalAgentOptions` enforces them so a future edit cannot
40
+ * silently introduce a detached or non-local agent session.
41
+ */
42
+ export function buildLocalAgentOptions() {
43
+ return {
44
+ cwd: process.cwd(),
45
+ // inherit: the agent owns THIS terminal directly (its TUI, raw-mode line
46
+ // editing, etc. all work). Crucially this is also the local/single-operator
47
+ // boundary — the agent reads/writes only this process's stdio, never a pipe
48
+ // we could forward elsewhere. We are explicitly NOT piping the agent's
49
+ // output through Node (that piping + re-render is the headless path), so
50
+ // there is no second stream a remote/shared peer could attach, and no raw
51
+ // text dump: the agent's own TUI renders directly to the terminal.
52
+ stdio: "inherit",
53
+ // detached:false keeps the agent in OUR process group so it dies with the
54
+ // local task process and can never become an orphaned, independently-reachable
55
+ // session. A detached agent would violate "no remote or shared shells" — so
56
+ // it is asserted false below.
57
+ detached: false,
58
+ };
59
+ }
60
+ /**
61
+ * Guard the spawn options against any non-local/shared configuration. Fails
62
+ * loud (no masked default) if a caller ever hands the agent options that would
63
+ * detach it from this process or route its IO anywhere but this terminal.
64
+ */
65
+ export function assertLocalAgentOptions(options) {
66
+ if (options.detached === true) {
67
+ throw new CliError("Refusing to launch a detached interactive agent: it must stay bound " +
68
+ "to the local task process (no remote or shared shells).");
69
+ }
70
+ if (options.stdio !== "inherit") {
71
+ throw new CliError("Refusing to launch the interactive agent on non-inherited stdio: it must " +
72
+ "attach to this local terminal only (no remote or shared shells, no raw " +
73
+ "text dumping).");
74
+ }
75
+ }
76
+ /**
77
+ * A live, interactive coding-agent session that replaces a task's headless run.
78
+ * `open` spawns `claude`/`codex` with inherited stdio (so the operator works
79
+ * with it directly, in its own TUI); the returned child resolves the run when
80
+ * it exits, which releases the worker. Constructed only after `assertUsableTty`
81
+ * has passed.
82
+ *
83
+ * Single-operator invariant: an instance opens AT MOST one agent session, ever.
84
+ * A second `open()` — whether the first session is still live or already
85
+ * closed — is refused loudly, so there is never more than one interactive agent
86
+ * behind a single task.
87
+ */
88
+ export class InteractiveAgent {
89
+ command;
90
+ args;
91
+ spawnFn;
92
+ child = null;
93
+ opened = false;
94
+ constructor(command, args, spawnFn = spawn) {
95
+ this.command = command;
96
+ this.args = args;
97
+ this.spawnFn = spawnFn;
98
+ }
99
+ /**
100
+ * Spawn the coding agent with inherited stdio so it owns the terminal and the
101
+ * operator works with it live. Returns the child so the caller can await its
102
+ * exit (which releases the worker). The single-operator invariant is enforced
103
+ * here: one session per instance, period.
104
+ */
105
+ open() {
106
+ if (this.opened) {
107
+ throw new CliError("Interactive agent already launched for this task — only a single local " +
108
+ "agent session is allowed (no second or shared session).");
109
+ }
110
+ this.opened = true;
111
+ const options = buildLocalAgentOptions();
112
+ // Belt-and-braces: prove the options are local-only before spawning, so a
113
+ // future change to buildLocalAgentOptions can't quietly launch a shared agent.
114
+ assertLocalAgentOptions(options);
115
+ log.plain(`Launching interactive ${this.command} session — work with it, then exit ` +
116
+ "the agent to continue to the next task.");
117
+ this.child = this.spawnFn(this.command, this.args, options);
118
+ return this.child;
119
+ }
120
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/commands/pull.ts"],"names":[],"mappings":"AAQA,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuE/D"}
1
+ {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/commands/pull.ts"],"names":[],"mappings":"AAcA,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA4F/D"}
@@ -3,7 +3,7 @@ import { writeFileSync } from "node:fs";
3
3
  import { ZenormClient } from "../api/client.js";
4
4
  import { CliError } from "../util/errors.js";
5
5
  import { log } from "../util/logger.js";
6
- import { blocksToMarkdown, specToMarkdown } from "../util/markdown.js";
6
+ import { attachmentsToMarkdown, blocksToMarkdown, specToMarkdown, } from "../util/markdown.js";
7
7
  export async function pullCommand(argv) {
8
8
  const { positionals, values } = parseArgs({
9
9
  args: argv,
@@ -56,7 +56,28 @@ export async function pullCommand(argv) {
56
56
  error: err instanceof Error ? err.message : String(err),
57
57
  });
58
58
  }
59
- const md = blocksMd ? `${specToMarkdown(spec)}\n\n${blocksMd}\n` : specToMarkdown(spec);
59
+ // Fetch the spec's READY attachments and append them to the work context,
60
+ // automatically (no flag, no per-run selection — AC "included automatically").
61
+ // The `?status=ready` filter is the SERVER-SIDE gate, so a worker run can only
62
+ // ever see ingested files (AC "ready files only"). Tolerate the endpoint 404ing
63
+ // on older servers — degrade silently like the blocks fetch above.
64
+ let attachmentsMd = "";
65
+ try {
66
+ const { attachments } = await client.get(`/v1/specs/${spec.id}/attachments?status=ready`);
67
+ attachmentsMd = attachmentsToMarkdown(attachments);
68
+ }
69
+ catch (err) {
70
+ log.debug("Attachment fetch failed, omitting attachments section", {
71
+ specId: spec.id,
72
+ error: err instanceof Error ? err.message : String(err),
73
+ });
74
+ }
75
+ const sections = [specToMarkdown(spec)];
76
+ if (blocksMd)
77
+ sections.push(blocksMd);
78
+ if (attachmentsMd)
79
+ sections.push(attachmentsMd);
80
+ const md = `${sections.join("\n\n")}\n`;
60
81
  if (outputPath) {
61
82
  writeFileSync(outputPath, md, "utf-8");
62
83
  log.success(`Wrote spec to ${outputPath}`);
@@ -83,6 +83,7 @@ export declare function splashLogo(): string;
83
83
  */
84
84
  export declare function daemonBanner(opts: {
85
85
  agent: string;
86
+ model?: string;
86
87
  repo: string;
87
88
  intervalSeconds: number;
88
89
  once: boolean;
@@ -112,6 +113,27 @@ export declare function failureBanner(opts: {
112
113
  title: string;
113
114
  error: string;
114
115
  }): string;
116
+ /**
117
+ * The ONE-LINE post-agent change summary printed when an interactive
118
+ * coding-agent session closes (operator exited the agent). It is the only
119
+ * read-out of an interactive session — the agent's own TUI owned the terminal,
120
+ * so there is no transcript to footer — and per the spec it must be a SINGLE
121
+ * line (constraint: "No raw text terminal dumping"): no multi-line dump, no raw
122
+ * diff. It reports the files the agent touched during the session and the
123
+ * claimed task's resulting status, then the daemon releases the worker.
124
+ *
125
+ * The file list is capped to `maxFiles` names with a `+N more` tail so a large
126
+ * change set never wraps the line. `status` is the task's server-side status
127
+ * (e.g. `done` once the agent ran `zenorm task complete`), or the literal
128
+ * `unknown` when the status read could not be made — the one sanctioned
129
+ * soft-fallback, since reading the status must never block worker release.
130
+ */
131
+ export declare function postAgentSummary(opts: {
132
+ filesTouched: string[];
133
+ status: string;
134
+ shortTaskId: string;
135
+ maxFiles?: number;
136
+ }): string;
115
137
  /**
116
138
  * The session summary printed when the daemon shuts down (Ctrl-C or `--once`
117
139
  * exhausted). Closes the run with a rule + a one-line tally so the user sees the
@@ -1 +1 @@
1
- {"version":3,"file":"work-render.d.ts","sourceRoot":"","sources":["../../src/commands/work-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAoBH,+DAA+D;AAC/D,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AA2DlC;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAM;IAEpB,6EAA6E;IAC7E,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAQ7B,wDAAwD;IACxD,KAAK,IAAI,MAAM,EAAE;CAMlB;AA+ED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,EAAE,CAkChE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,EAAE,CAoE/D;AAED,kEAAkE;AAClE,MAAM,MAAM,aAAa,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY,EAAE,CAAC;AAuBjE,eAAO,MAAM,cAAc,EAAE,aAA+C,CAAC;AAC7E,eAAO,MAAM,aAAa,EAAE,aAA8C,CAAC;AAE3E;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,YAAY,CAI/E;AAMD;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,MAAM,CAMR;AA2JD;;;;;;;;GAQG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAenC;AAoBD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,MAAM,CAsBT;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,MAAM,CAKT;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAMT;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,MAAM,CAKT"}
1
+ {"version":3,"file":"work-render.d.ts","sourceRoot":"","sources":["../../src/commands/work-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAoBH,+DAA+D;AAC/D,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AA2DlC;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAM;IAEpB,6EAA6E;IAC7E,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAQ7B,wDAAwD;IACxD,KAAK,IAAI,MAAM,EAAE;CAMlB;AA+ED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,EAAE,CAkChE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,EAAE,CAoE/D;AAED,kEAAkE;AAClE,MAAM,MAAM,aAAa,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY,EAAE,CAAC;AAuBjE,eAAO,MAAM,cAAc,EAAE,aAA+C,CAAC;AAC7E,eAAO,MAAM,aAAa,EAAE,aAA8C,CAAC;AAE3E;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,YAAY,CAI/E;AAMD;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,MAAM,CAMR;AA2JD;;;;;;;;GAQG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAenC;AAoBD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,MAAM,CAuBT;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,MAAM,CAKT;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAMT;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,CAiBT;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,MAAM,CAKT"}
@@ -537,6 +537,7 @@ export function daemonBanner(opts) {
537
537
  `${DIM}spec-authoring agent · claim-and-run daemon${RESET}`,
538
538
  "",
539
539
  `${DIM}agent ${RESET}${opts.agent}`,
540
+ ...(opts.model ? [`${DIM}model ${RESET}${opts.model}`] : []),
540
541
  `${DIM}repo ${RESET}${opts.repo}`,
541
542
  `${DIM}interval ${RESET}${opts.intervalSeconds}s`,
542
543
  ]);
@@ -576,6 +577,38 @@ export function failureBanner(opts) {
576
577
  `${DIM} released back to todo — it can be re-claimed.${RESET}`,
577
578
  ].join("\n");
578
579
  }
580
+ /**
581
+ * The ONE-LINE post-agent change summary printed when an interactive
582
+ * coding-agent session closes (operator exited the agent). It is the only
583
+ * read-out of an interactive session — the agent's own TUI owned the terminal,
584
+ * so there is no transcript to footer — and per the spec it must be a SINGLE
585
+ * line (constraint: "No raw text terminal dumping"): no multi-line dump, no raw
586
+ * diff. It reports the files the agent touched during the session and the
587
+ * claimed task's resulting status, then the daemon releases the worker.
588
+ *
589
+ * The file list is capped to `maxFiles` names with a `+N more` tail so a large
590
+ * change set never wraps the line. `status` is the task's server-side status
591
+ * (e.g. `done` once the agent ran `zenorm task complete`), or the literal
592
+ * `unknown` when the status read could not be made — the one sanctioned
593
+ * soft-fallback, since reading the status must never block worker release.
594
+ */
595
+ export function postAgentSummary(opts) {
596
+ const max = opts.maxFiles ?? 4;
597
+ const count = opts.filesTouched.length;
598
+ const noun = count === 1 ? "file" : "files";
599
+ let detail;
600
+ if (count === 0) {
601
+ detail = `${DIM}no files touched${RESET}`;
602
+ }
603
+ else {
604
+ const shown = opts.filesTouched.slice(0, max);
605
+ const overflow = count - shown.length;
606
+ const names = shown.join(", ") + (overflow > 0 ? `, +${overflow} more` : "");
607
+ detail = `${count} ${noun} touched ${DIM}(${clip(names)})${RESET}`;
608
+ }
609
+ return (`${BOLD}◣ agent closed${RESET} ${DIM}·${RESET} ${detail} ` +
610
+ `${DIM}·${RESET} task ${GREEN}${opts.status}${RESET} ${DIM}(${opts.shortTaskId})${RESET}`);
611
+ }
579
612
  /**
580
613
  * The session summary printed when the daemon shuts down (Ctrl-C or `--once`
581
614
  * exhausted). Closes the run with a rule + a one-line tally so the user sees the
@@ -1,6 +1,7 @@
1
1
  import type { SpawnOptions, ChildProcess } from "node:child_process";
2
2
  import { ZenormClient } from "../api/client.js";
3
3
  import type { SpecTaskRecord } from "../api/types.js";
4
+ import { type TtyProbe } from "./interactive-agent.js";
4
5
  /**
5
6
  * Agents whose subprocess runner slice 3 will wire up. Validated here so an
6
7
  * unknown/missing `--agent` fails loud rather than silently defaulting (project
@@ -35,6 +36,13 @@ export interface TaskRunner {
35
36
  * Exported for unit testing.
36
37
  */
37
38
  export declare function buildHandoffPrompt(task: SpecTaskRecord): string;
39
+ /**
40
+ * The interactive (coding-agent mode) handoff prompt. Same spec-mode handoff as
41
+ * the headless prompt, MINUS the "work non-interactively" instruction: in this
42
+ * mode the agent runs as a live session the operator drives, so it SHOULD pause
43
+ * for input. Exported for unit testing.
44
+ */
45
+ export declare function buildInteractiveHandoffPrompt(task: SpecTaskRecord): string;
38
46
  /**
39
47
  * The headless subprocess invocation for a given agent + task. Pure (no
40
48
  * side effects) so it can be unit-tested directly without spawning anything.
@@ -47,7 +55,37 @@ export declare function buildHandoffPrompt(task: SpecTaskRecord): string;
47
55
  *
48
56
  * Exported for unit testing.
49
57
  */
50
- export declare function buildAgentCommand(agent: SupportedAgent, task: SpecTaskRecord): {
58
+ export declare function buildAgentCommand(agent: SupportedAgent, task: SpecTaskRecord, model?: string): {
59
+ command: string;
60
+ args: string[];
61
+ };
62
+ /**
63
+ * The INTERACTIVE invocation for a given agent + task: launch the agent as a
64
+ * live session (NOT headless) seeded with the spec handoff, so the operator
65
+ * works with it directly in its own TUI. Pure (no side effects) so it can be
66
+ * unit-tested without spawning anything.
67
+ *
68
+ * Unlike `buildAgentCommand`, there is no `-p`/`exec`/`--json` and no
69
+ * stream-json: both agents start an interactive session by default, and the
70
+ * agent's own TUI renders straight to the inherited terminal (so there is no
71
+ * raw JSON event stream for us to re-render, and nothing to dump as raw text).
72
+ * - claude: `claude --permission-mode auto [--model <m>] "<prompt>"`
73
+ * (interactive by default; `-p` would make it headless, so it is deliberately
74
+ * absent). `--permission-mode auto` runs the background classifier that
75
+ * auto-approves safe actions and only blocks destructive ones, so the
76
+ * operator is not interrupted by a permission prompt on every gated tool call
77
+ * — same guardrail the headless path uses, just keeping the session live.
78
+ * - codex: `codex --ask-for-approval never --sandbox workspace-write [-m <m>]
79
+ * "<prompt>"` (interactive by default; the `exec` subcommand is what makes it
80
+ * non-interactive, so it is deliberately absent). `--ask-for-approval never`
81
+ * is codex's full-auto: the operator is never interrupted by an approval
82
+ * prompt, while `--sandbox workspace-write` still confines writes to the
83
+ * workspace (NOT `--dangerously-bypass-approvals-and-sandbox`, which would
84
+ * drop the sandbox entirely).
85
+ *
86
+ * Exported for unit testing.
87
+ */
88
+ export declare function buildInteractiveAgentCommand(agent: SupportedAgent, task: SpecTaskRecord, model?: string): {
51
89
  command: string;
52
90
  args: string[];
53
91
  };
@@ -56,6 +94,31 @@ export declare function buildAgentCommand(agent: SupportedAgent, task: SpecTaskR
56
94
  * unit-tested without launching a real agent.
57
95
  */
58
96
  export type SpawnFn = (command: string, args: string[], options: SpawnOptions) => ChildProcess;
97
+ /**
98
+ * Snapshot the working tree's changed-file state. Injectable so the interactive
99
+ * post-agent summary can be unit-tested without a real git repo; the default is
100
+ * a real `git status --porcelain` in the current repo. Returns the raw porcelain
101
+ * text (each line is a 3-char status prefix + path); `diffTouchedFiles` parses
102
+ * it into the touched-file set.
103
+ */
104
+ export type GitStatusFn = () => string;
105
+ /**
106
+ * Read the claimed task's CURRENT server-side status (e.g. `done` once the agent
107
+ * ran `zenorm task complete` inside the session). Injectable so the summary can
108
+ * be unit-tested without HTTP. Returns the status, or `"unknown"` when the read
109
+ * could not be made — the summary must never block worker release on a status
110
+ * read, so this is the one sanctioned soft-fallback (the caller also warns).
111
+ */
112
+ export type FetchTaskStatusFn = (task: SpecTaskRecord) => Promise<SpecTaskRecord["status"]>;
113
+ /**
114
+ * Diff two `git status --porcelain` snapshots into the set of files the agent
115
+ * touched during the session. We compare the porcelain LINE per path across
116
+ * before/after: a path is "touched" when its line differs (added, removed, or
117
+ * its status code changed). This deliberately IGNORES pre-existing dirt whose
118
+ * status did not change during the session (so unrelated stray files already in
119
+ * the tree don't pollute the summary). Returns a sorted, deduped path list.
120
+ */
121
+ export declare function diffTouchedFiles(before: string, after: string): string[];
59
122
  /**
60
123
  * The real runner: spawns the chosen coding agent headlessly against the
61
124
  * claimed task and awaits its exit.
@@ -69,9 +132,48 @@ export type SpawnFn = (command: string, args: string[], options: SpawnOptions) =
69
132
  export declare class AgentRunner implements TaskRunner {
70
133
  private readonly agent;
71
134
  private readonly spawnFn;
135
+ private readonly model?;
136
+ private readonly interactive;
137
+ private readonly gitStatus;
138
+ private readonly fetchTaskStatus?;
72
139
  readonly agentName: SupportedAgent;
73
- constructor(agent: SupportedAgent, spawnFn?: SpawnFn);
140
+ constructor(agent: SupportedAgent, spawnFn?: SpawnFn, model?: string | undefined, interactive?: boolean, gitStatus?: GitStatusFn, fetchTaskStatus?: FetchTaskStatusFn | undefined);
74
141
  run(task: SpecTaskRecord): Promise<void>;
142
+ /**
143
+ * Default (non-interactive) path — UNCHANGED behavior: spawn the agent
144
+ * headlessly, pipe + re-render its JSON event stream, await its exit.
145
+ */
146
+ private runHeadless;
147
+ /**
148
+ * Coding-agent (interactive) path. Launch the agent as a LIVE session via
149
+ * `InteractiveAgent`: inherited stdio so the agent's own TUI owns the terminal
150
+ * (no piping, no re-render — and therefore no raw text dumping), seeded with
151
+ * the spec handoff. The operator works with it; when they close the agent it
152
+ * exits, which resolves this run and RELEASES THE WORKER so the loop claims
153
+ * the next task. A clean operator exit (code 0) is success; a crash/non-zero
154
+ * exit is still a loud failure.
155
+ */
156
+ private runInteractive;
157
+ /**
158
+ * Print the ONE-LINE post-agent change summary, then RESOLVE (releasing the
159
+ * worker) — in that order, per the spec constraint "Summary before worker
160
+ * release". Computes the touched-file set from the before/after porcelain
161
+ * snapshots and reads the claimed task's current server-side status.
162
+ *
163
+ * Robustness: this runs after a CLEAN agent exit, so it must NEVER turn that
164
+ * success into a failure. The whole body is wrapped so `resolve()` always
165
+ * fires even if the summary work throws. The status read is the one sanctioned
166
+ * soft-fallback — a fetch failure warns and reports `unknown` rather than
167
+ * blocking the release; an absent status fetcher (no client wired, e.g. unit
168
+ * tests) is silently `unknown`.
169
+ */
170
+ private summarizeThenRelease;
171
+ /**
172
+ * Shared exit/error handling for both paths: ENOENT → install hint, non-zero
173
+ * exit → loud failure, code 0 → success (releases the worker). `settled`
174
+ * guards against a double-settle if error and exit both fire.
175
+ */
176
+ private wireChild;
75
177
  }
76
178
  /**
77
179
  * `--dry-run` runner: prints the exact headless command + handoff prompt it
@@ -80,8 +182,9 @@ export declare class AgentRunner implements TaskRunner {
80
182
  */
81
183
  export declare class DryRunRunner implements TaskRunner {
82
184
  private readonly agent;
185
+ private readonly model?;
83
186
  readonly agentName: SupportedAgent;
84
- constructor(agent: SupportedAgent);
187
+ constructor(agent: SupportedAgent, model?: string | undefined);
85
188
  run(task: SpecTaskRecord): Promise<void>;
86
189
  }
87
190
  /**
@@ -99,7 +202,18 @@ export declare function parseGitRemote(url: string): {
99
202
  interface DaemonDeps {
100
203
  runner?: TaskRunner;
101
204
  client?: ZenormClient;
205
+ ttyProbe?: TtyProbe;
102
206
  }
207
+ /**
208
+ * Resolve whether interactive mode is on. Opt-in is explicit by design (the
209
+ * spec keeps existing runs non-interactive unless enabled): the `--interactive`
210
+ * flag, or `interactive: true` in `.zenorm.json`. The flag wins; config only
211
+ * supplies the default when the flag is absent. A bad config value already
212
+ * threw in `validateConfig`, so this never silently coerces.
213
+ */
214
+ export declare function resolveInteractive(flagPassed: boolean, config: {
215
+ interactive?: boolean;
216
+ } | null): boolean;
103
217
  /**
104
218
  * The poll loop, isolated from arg/repo resolution so it can be unit-tested
105
219
  * with an injected client and runner. Returns the number of tasks processed.