@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.
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +16 -6
- package/dist/auth/device-flow.d.ts.map +1 -1
- package/dist/auth/device-flow.js +11 -0
- package/dist/auth/store.d.ts.map +1 -1
- package/dist/auth/store.js +7 -1
- package/dist/commands/interactive-agent.d.ts +92 -0
- package/dist/commands/interactive-agent.d.ts.map +1 -0
- package/dist/commands/interactive-agent.js +120 -0
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/pull.js +23 -2
- package/dist/commands/work-render.d.ts +22 -0
- package/dist/commands/work-render.d.ts.map +1 -1
- package/dist/commands/work-render.js +33 -0
- package/dist/commands/work.d.ts +117 -3
- package/dist/commands/work.d.ts.map +1 -1
- package/dist/commands/work.js +391 -41
- package/dist/config/environments.d.ts +31 -0
- package/dist/config/environments.d.ts.map +1 -0
- package/dist/config/environments.js +65 -0
- package/dist/config/schema.d.ts +10 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +6 -0
- package/dist/util/markdown.d.ts +16 -0
- package/dist/util/markdown.d.ts.map +1 -1
- package/dist/util/markdown.js +34 -0
- package/package.json +1 -1
package/dist/api/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/api/client.js
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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;
|
|
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"}
|
package/dist/auth/device-flow.js
CHANGED
|
@@ -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);
|
package/dist/auth/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/auth/store.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/auth/store.js
CHANGED
|
@@ -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
|
-
|
|
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":"
|
|
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"}
|
package/dist/commands/pull.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
package/dist/commands/work.d.ts
CHANGED
|
@@ -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.
|