agent-tempo 1.4.2 → 1.5.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/CLAUDE.md +1 -1
- package/README.md +21 -1
- package/dashboard/package.json +1 -1
- package/dist/cli/commands.js +50 -2
- package/dist/cli/config-command.d.ts +15 -0
- package/dist/cli/config-command.js +22 -7
- package/dist/client/core.js +15 -0
- package/dist/client/interface.d.ts +9 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +9 -0
- package/dist/http/server.js +12 -1
- package/dist/pi/extension.d.ts +54 -0
- package/dist/pi/extension.js +39 -0
- package/dist/pi/pi-types.d.ts +24 -5
- package/dist/pi/render-tools.js +2 -0
- package/dist/spawn.d.ts +86 -2
- package/dist/spawn.js +126 -8
- package/dist/tui/index.js +1 -0
- package/dist/utils/parent-death-watchdog.d.ts +12 -0
- package/dist/utils/parent-death-watchdog.js +25 -0
- package/package.json +3 -2
package/CLAUDE.md
CHANGED
|
@@ -210,7 +210,7 @@ daemon worker notes, `npx ts-node` dev runner).
|
|
|
210
210
|
- **Claude API adapter** (`agent: 'claude-api'`, #131): Headless adapter that drives sessions via the Anthropic Messages API (`@anthropic-ai/sdk`) — no terminal, no Claude Code CLI. Requires `ANTHROPIC_API_KEY` env var and the `@anthropic-ai/sdk` optional dependency. Default model `claude-opus-4-7` (overridable via `model` recruit arg or `CLAUDE_TEMPO_API_MODEL` env). Claude-API players have access to agent-tempo MCP tools (cue, report, recall, ensemble, …) but not file-edit/shell/web tools. See `src/adapters/claude-api/`.
|
|
211
211
|
- **OpenCode adapter** (`agent: 'opencode'`, #449): Headless multi-provider adapter that drives sessions via [SST OpenCode](https://opencode.ai) as a managed subprocess — supports Anthropic, OpenAI, Bedrock, Vertex, Ollama, and ~70 other providers via OpenCode's `provider/model` selector. Requires OpenCode CLI (`npm install -g opencode-ai`) and the `@opencode-ai/sdk` optional dependency. Recruit with `model: 'provider/name'` (e.g. `'anthropic/claude-opus-4-7'`). Tool bridging is MCP-native — OpenCode spawns `dist/server.js` as its own stdio MCP child. Session state is persisted server-side by OpenCode; the adapter stashes the session id on workflow metadata for reconnect across `opencode serve` restarts. See `src/adapters/opencode/`.
|
|
212
212
|
- **Claude Code headless adapter** (`agent: 'claude-code-headless'`, #520): Headless adapter that drives sessions via the official `claude` CLI as a per-turn `claude -p --output-format stream-json` subprocess. The whole point: turns bill against the host's existing Claude Code subscription extra-usage credits (Pro / Max plans) rather than a Console workspace API key — the only ToS-clean way for a third-party tool to tap that pool. Requires the `claude` binary on PATH AND a logged-in Claude Code session (`claude auth login`); recruit pre-flight rejects with an actionable error otherwise. Tool surface is the union of full Claude Code built-ins (Bash / Read / Write / Edit / Glob / Grep / WebSearch / WebFetch) and the agent-tempo MCP surface — registered via inline `--mcp-config` so `claude` spawns `dist/server.js` as its own MCP child (no in-process bridge). Recruit knobs: `permissionMode` (default `'acceptEdits'`) or `dangerouslySkipPermissions: true` (mutually exclusive). Sessions resume across restart via the existing `sessionId` metadata field — the same UUID is shared with the interactive `claude-code` adapter (per-cwd JSONL is per-cwd, not per-adapter). See `src/adapters/claude-code-headless/` and `examples/ensembles/tempo-headless-jam.yaml`.
|
|
213
|
-
- **Pi adapter** (`agent: 'pi'`, #632
|
|
213
|
+
- **Pi adapter** (`agent: 'pi'`, #632 / #666): Two modes. **(1) Interactive conductor** (#666): `agent-tempo up --agent pi --ensemble <name>` launches `pi` in a real terminal with the agent-tempo extension auto-loaded (`pi -e dist/pi/extension.js`); the Pi session self-bootstraps its Temporal workflow and attaches as a conductor/player — no separate recruiter step. From the TUI, `/recruit-conductor` relaunches the active ensemble's conductor — set `conductor.agent: pi` in that ensemble's lineup to make it a Pi conductor. Requires `@earendil-works/pi-coding-agent` on Node ≥ 22.19. Recommended: `ANTHROPIC_API_KEY` (without it the session falls back to Pi's own auth/default model). The `AGENT_TEMPO_*` env is auto-wired by `up`; power users can invoke the extension directly with `pi -e dist/pi/extension.js`. `--model provider/model` selector (e.g. `'github-copilot/gpt-4o'`) is a fast-follow. **(2) Headless player** (Phase 3a): `recruit` with `agent: 'pi'` — no terminal, no BaseAttachment; runs `createAgentSession` with an in-memory `SessionManager`; the module-scope singleton owns claim/heartbeat/tools/cue pump (MD-D). MD-C tool-access policy: `toolAccess: 'restricted'` (default — Bash/shell/exec HARD-BLOCKED) | `'standard'` (scoped Bash) | `'full'` (unsandboxed; requires `force: true`). `noExtensions: true` closes the S2 exec-tool-bypass gap. See `src/adapters/pi/` and `src/pi/headless.ts`.
|
|
214
214
|
- **Mission-control Pi extension** (3f): An observer-only Pi extension that turns one interactive Pi TUI into a live ensemble board + operator controller. HTTP-drives the daemon — coarse ensemble view via `/v1/events/:ensemble` SSE + fine per-player tail via `/inner` (T3); operator controls (cue/pause/play/restart/destroy + gate arm/disarm/decide) POST to the daemon write surface using `AGENT_TEMPO_HTTP_ADMIN_TOKEN`. **Never claims attachment or registers as a player** — invisible to the ensemble. ~200ms render throttle from an in-memory `BoardModel`. Requires an interactive Pi session with `ctx.hasUI`. See `src/pi/mission-control/`.
|
|
215
215
|
- **Mock adapter** (`agent: 'mock'`, dev mode only): Four modes: `echo` (echoes input), `scripted` (replays YAML scenario rules), `silent` (drains messages without replying — heartbeat-stale validation), `chaos` (probabilistic fail/crash injection via seeded PRNG). Only registered when `isDevMode()` is true; stripped from the npm tarball by `prepack`. See `src/adapters/mock/`.
|
|
216
216
|
- **Saveable state** (#334, ADR 0011): Per-player curated state slots — the player itself decides what context survives a restart. Three MCP tools: `save_state` (owner-only write, max 4 slots × 32 KiB), `fetch_state` (read self or peer; audit identity recorded on each entry's `savedBy`), `clear_state` (owner-only). `restart` accepts `loadFromState: true | 'someKey'` to seed the new session from a saved-state slot instead of (or, with `transcript: 'replay'`, alongside) transcript replay. Saved-state delivery uses `from: 'self-restart'` as a stable system identity. Empty-slot fallback: graceful — falls through to transcript replay with a log line. See [docs/design/334-player-saveable-state.md](docs/design/334-player-saveable-state.md).
|
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Each session registers as a **player** in Temporal. Players discover each other
|
|
|
37
37
|
| 🖥️ **Terminal UI** | Chat-focused TUI with slash commands, overlays, and interactive wizards |
|
|
38
38
|
| 🌐 **Cross-machine** | Any session that can reach your Temporal server can join the ensemble |
|
|
39
39
|
| ⏸️ **Hold / Pause / Resume** | Pre-warm a full team before delivering tasks; pause and resume mid-session |
|
|
40
|
-
| 🤖 **Headless adapters** | Copilot bridge, Claude API, OpenCode, Claude Code headless (`claude -p` — bills against your Claude Code subscription), and Pi AI — mix providers and headless agents in the same ensemble |
|
|
40
|
+
| 🤖 **Headless adapters** | Copilot bridge, Claude API, OpenCode, Claude Code headless (`claude -p` — bills against your Claude Code subscription), and Pi AI (headless player or interactive conductor) — mix providers and headless agents in the same ensemble |
|
|
41
41
|
|
|
42
42
|
## Installation
|
|
43
43
|
|
|
@@ -261,6 +261,26 @@ GitHub Copilot CLI sessions can join an ensemble using `--agent copilot`. Recrui
|
|
|
261
261
|
|
|
262
262
|
📖 [Copilot bridge setup and limitations → docs/copilot.md](docs/copilot.md)
|
|
263
263
|
|
|
264
|
+
## Pi AI Integration
|
|
265
|
+
|
|
266
|
+
Pi AI sessions can join an ensemble in two modes:
|
|
267
|
+
|
|
268
|
+
**Interactive conductor** — launch Pi in a real terminal with the agent-tempo extension auto-loaded:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
agent-tempo up --agent pi --ensemble <name>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The Pi session self-bootstraps its Temporal workflow and attaches as a conductor or player. The `AGENT_TEMPO_*` environment is wired automatically. For power users, the underlying extension path is `dist/pi/extension.js` — invoke directly with `pi -e dist/pi/extension.js`.
|
|
275
|
+
|
|
276
|
+
From the TUI, `/recruit-conductor` relaunches the active ensemble's conductor — set `conductor.agent: pi` in that ensemble's lineup to make it a Pi conductor.
|
|
277
|
+
|
|
278
|
+
**Prerequisites:** `@earendil-works/pi-coding-agent` on Node ≥ 22.19. Recommended: `ANTHROPIC_API_KEY` (without it the session falls back to Pi's own auth/default model).
|
|
279
|
+
|
|
280
|
+
**Headless Pi players** — recruit as a background agent slot using `agent: 'pi'` (see [docs/design/pi-hardening-h1-h2-h3.md](docs/design/pi-hardening-h1-h2-h3.md)).
|
|
281
|
+
|
|
282
|
+
📖 [Pi integration reference → docs/design/pi-hardening-h1-h2-h3.md](docs/design/pi-hardening-h1-h2-h3.md)
|
|
283
|
+
|
|
264
284
|
## Worker Daemon
|
|
265
285
|
|
|
266
286
|
The daemon runs Temporal workers as a background process — it starts automatically on first use. Manage it explicitly with `agent-tempo daemon start|stop|status|logs`.
|
package/dashboard/package.json
CHANGED
package/dist/cli/commands.js
CHANGED
|
@@ -62,6 +62,7 @@ const crypto_1 = require("crypto");
|
|
|
62
62
|
const croner_1 = require("croner");
|
|
63
63
|
const client_1 = require("@temporalio/client");
|
|
64
64
|
const spawn_1 = require("../spawn");
|
|
65
|
+
const probe_1 = require("../pi/probe");
|
|
65
66
|
const config_1 = require("../config");
|
|
66
67
|
const git_info_1 = require("../git-info");
|
|
67
68
|
const connection_1 = require("../connection");
|
|
@@ -344,6 +345,10 @@ async function applyLineupPlayersAndSchedules(args) {
|
|
|
344
345
|
temporalTlsKeyPath: config.temporalTlsKeyPath,
|
|
345
346
|
isConductor: false,
|
|
346
347
|
workDir: playerWorkDir,
|
|
348
|
+
// #672 — a `up --lineup` copilot PLAYER is also spawned directly (no
|
|
349
|
+
// terminal) by the transient CLI → same self-kill bug as the conductor.
|
|
350
|
+
// Skip the ppid-poll; daemon-recruit copilot (outbox.ts) keeps it.
|
|
351
|
+
transientSpawner: true,
|
|
347
352
|
});
|
|
348
353
|
}
|
|
349
354
|
else {
|
|
@@ -588,6 +593,9 @@ async function start(opts) {
|
|
|
588
593
|
temporalTlsKeyPath: config.temporalTlsKeyPath,
|
|
589
594
|
isConductor: opts.conductor,
|
|
590
595
|
workDir,
|
|
596
|
+
// #672 — CLI-direct copilot spawn (start path): transient `up`/`conduct`
|
|
597
|
+
// spawner → skip the ppid-poll. Daemon-recruit copilot (outbox.ts) omits it.
|
|
598
|
+
transientSpawner: true,
|
|
591
599
|
});
|
|
592
600
|
out.success(`Launched copilot bridge "${sessionName}" (pid ${pid ?? 'unknown'})`);
|
|
593
601
|
}
|
|
@@ -1163,8 +1171,9 @@ async function up(opts) {
|
|
|
1163
1171
|
// outside dev mode so a mis-configured lineup doesn't spawn a real session
|
|
1164
1172
|
// unexpectedly (mirrors the player-level guard at ~line 209).
|
|
1165
1173
|
const conductorAgent = lineup?.conductor?.agent === 'copilot' ? 'copilot' :
|
|
1166
|
-
lineup?.conductor?.agent === '
|
|
1167
|
-
|
|
1174
|
+
lineup?.conductor?.agent === 'pi' ? 'pi' :
|
|
1175
|
+
lineup?.conductor?.agent === 'mock' && (0, config_1.isDevMode)() ? 'mock' :
|
|
1176
|
+
opts.agent;
|
|
1168
1177
|
// Step 5: Connect to Temporal and check for existing conductor
|
|
1169
1178
|
console.log();
|
|
1170
1179
|
const connection = await (0, connection_1.createTemporalConnection)(config);
|
|
@@ -1302,8 +1311,47 @@ async function up(opts) {
|
|
|
1302
1311
|
temporalTlsKeyPath: config.temporalTlsKeyPath,
|
|
1303
1312
|
isConductor: true,
|
|
1304
1313
|
workDir: process.cwd(),
|
|
1314
|
+
// #672 — the `up` CLI is a TRANSIENT spawner; the detached bridge must NOT
|
|
1315
|
+
// ppid-poll it (would self-kill seconds after launch → lease never renews →
|
|
1316
|
+
// all players detach). The daemon-recruit path (outbox.ts) omits this.
|
|
1317
|
+
transientSpawner: true,
|
|
1305
1318
|
}));
|
|
1306
1319
|
}
|
|
1320
|
+
else if (conductorAgent === 'pi') {
|
|
1321
|
+
// Interactive Pi conductor (#666). MUST launch `pi` in a REAL TERMINAL —
|
|
1322
|
+
// Pi only fires session_start / attaches in a TTY (headless/print-mode does
|
|
1323
|
+
// NOT). So this uses launchInTerminal, NOT spawnPiHeadless (that's recruited
|
|
1324
|
+
// players). One branch serves `up --agent pi` AND TUI /recruit-conductor.
|
|
1325
|
+
//
|
|
1326
|
+
// PREFLIGHT — fail clean BEFORE launching a terminal that would die:
|
|
1327
|
+
const nodeFloor = (0, probe_1.checkPiNodeFloor)(); // best-effort proxy on the daemon's Node
|
|
1328
|
+
if (!nodeFloor.ok) {
|
|
1329
|
+
out.error(`Cannot start Pi conductor — ${nodeFloor.reason}`);
|
|
1330
|
+
process.exit(1);
|
|
1331
|
+
}
|
|
1332
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
1333
|
+
out.warn('ANTHROPIC_API_KEY is not set — the Pi conductor will fall back to Pi\'s own auth/default model. Set it if Pi needs an Anthropic key.');
|
|
1334
|
+
}
|
|
1335
|
+
let piSpawn;
|
|
1336
|
+
try {
|
|
1337
|
+
// resolvePiInteractiveBinary / resolvePiExtensionPath throw fail-clean
|
|
1338
|
+
// (Pi CLI missing / extension unbuilt) — caught here, no terminal launched.
|
|
1339
|
+
piSpawn = (0, spawn_1.buildPiConductorSpawn)({
|
|
1340
|
+
ensemble: opts.ensemble,
|
|
1341
|
+
sessionName,
|
|
1342
|
+
temporalEnvVars,
|
|
1343
|
+
taskQueue: config.taskQueue,
|
|
1344
|
+
devMode: (0, config_1.isDevMode)(),
|
|
1345
|
+
conductorTypeName: resolvedConductorType?.name || conductorTypeName,
|
|
1346
|
+
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
catch (err) {
|
|
1350
|
+
out.error(`Cannot start Pi conductor — ${err instanceof Error ? err.message : String(err)}`);
|
|
1351
|
+
process.exit(1);
|
|
1352
|
+
}
|
|
1353
|
+
({ pid } = (0, spawn_1.launchInTerminal)(piSpawn.cmd, piSpawn.args, process.cwd(), piSpawn.env));
|
|
1354
|
+
}
|
|
1307
1355
|
else {
|
|
1308
1356
|
const claudeArgs = [
|
|
1309
1357
|
'--dangerously-skip-permissions',
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
import type { AgentType } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Agents valid as a persistent `defaultAgent` — the conductor-capable PRODUCTION
|
|
4
|
+
* agents. `defaultAgent` drives the conductor that `up` / `start` / `conduct`
|
|
5
|
+
* spawn when no `--agent` is given (`cli.ts` `resolvedAgent`), and the
|
|
6
|
+
* conductor-spawn branch only realises `copilot` / `pi` / else→`claude`. So:
|
|
7
|
+
* - `mock` is DEV-ONLY (recruit pre-flight rejects it outside dev mode) — never
|
|
8
|
+
* a persistent default.
|
|
9
|
+
* - the headless adapters (`claude-api` / `opencode` / `claude-code-headless`)
|
|
10
|
+
* can't be a conductor — they'd silently fall through to `claude` — so they
|
|
11
|
+
* are not offered here.
|
|
12
|
+
* Single source of truth for the interactive selector + `config set` validation
|
|
13
|
+
* (#666 — adds `pi` so the new interactive Pi conductor can be the default).
|
|
14
|
+
*/
|
|
15
|
+
export declare const VALID_DEFAULT_AGENTS: readonly AgentType[];
|
|
1
16
|
/** Interactive config setup: `agent-tempo config` */
|
|
2
17
|
export declare function configInteractive(): Promise<void>;
|
|
3
18
|
/** Non-interactive: `agent-tempo config set <key> <value>` */
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.VALID_DEFAULT_AGENTS = void 0;
|
|
36
37
|
exports.configInteractive = configInteractive;
|
|
37
38
|
exports.configSet = configSet;
|
|
38
39
|
exports.configShow = configShow;
|
|
@@ -41,6 +42,20 @@ const readline = __importStar(require("readline"));
|
|
|
41
42
|
const config_1 = require("../config");
|
|
42
43
|
const config_2 = require("../config");
|
|
43
44
|
const out = __importStar(require("./output"));
|
|
45
|
+
/**
|
|
46
|
+
* Agents valid as a persistent `defaultAgent` — the conductor-capable PRODUCTION
|
|
47
|
+
* agents. `defaultAgent` drives the conductor that `up` / `start` / `conduct`
|
|
48
|
+
* spawn when no `--agent` is given (`cli.ts` `resolvedAgent`), and the
|
|
49
|
+
* conductor-spawn branch only realises `copilot` / `pi` / else→`claude`. So:
|
|
50
|
+
* - `mock` is DEV-ONLY (recruit pre-flight rejects it outside dev mode) — never
|
|
51
|
+
* a persistent default.
|
|
52
|
+
* - the headless adapters (`claude-api` / `opencode` / `claude-code-headless`)
|
|
53
|
+
* can't be a conductor — they'd silently fall through to `claude` — so they
|
|
54
|
+
* are not offered here.
|
|
55
|
+
* Single source of truth for the interactive selector + `config set` validation
|
|
56
|
+
* (#666 — adds `pi` so the new interactive Pi conductor can be the default).
|
|
57
|
+
*/
|
|
58
|
+
exports.VALID_DEFAULT_AGENTS = ['claude', 'copilot', 'pi'];
|
|
44
59
|
// NOTE: `createTemporalConnection` is dynamic-imported inside `configInteractive`'s
|
|
45
60
|
// connection-test step (issue #157 PR C). Top-level static import would pull in
|
|
46
61
|
// `@temporalio/client`, defeating the crash-proof property of `config show` /
|
|
@@ -131,11 +146,11 @@ async function configInteractive() {
|
|
|
131
146
|
config.temporalTlsKeyPath = await ask('TLS key path', existing.temporalTlsKeyPath);
|
|
132
147
|
}
|
|
133
148
|
// Default agent type
|
|
134
|
-
const agentChoice = await choose('Default agent', [
|
|
135
|
-
if (agentChoice
|
|
136
|
-
config.defaultAgent =
|
|
149
|
+
const agentChoice = await choose('Default agent', [...exports.VALID_DEFAULT_AGENTS]);
|
|
150
|
+
if (agentChoice !== 'claude') {
|
|
151
|
+
config.defaultAgent = agentChoice;
|
|
137
152
|
}
|
|
138
|
-
// Don't set defaultAgent if claude — it's the default, keeps config clean
|
|
153
|
+
// Don't set defaultAgent if claude — it's the implicit default, keeps config clean
|
|
139
154
|
(0, config_1.saveConfigFile)(config);
|
|
140
155
|
out.success(`Saved to ${config_1.CONFIG_FILE_PATH}`);
|
|
141
156
|
// Test connection
|
|
@@ -190,9 +205,9 @@ function configSet(key, value) {
|
|
|
190
205
|
out.log(` Valid keys: ${Object.keys(keyMap).join(', ')}`);
|
|
191
206
|
process.exit(1);
|
|
192
207
|
}
|
|
193
|
-
// Validate agent type
|
|
194
|
-
if (configKey === 'defaultAgent' && value
|
|
195
|
-
out.error(`Invalid agent type: "${value}". Must be
|
|
208
|
+
// Validate agent type — restrict to the conductor-capable production agents.
|
|
209
|
+
if (configKey === 'defaultAgent' && !exports.VALID_DEFAULT_AGENTS.includes(value)) {
|
|
210
|
+
out.error(`Invalid agent type: "${value}". Must be one of: ${exports.VALID_DEFAULT_AGENTS.join(', ')}.`);
|
|
196
211
|
process.exit(1);
|
|
197
212
|
}
|
|
198
213
|
config[configKey] = value;
|
package/dist/client/core.js
CHANGED
|
@@ -1167,6 +1167,21 @@ function createTempoClientCore(client, opts = {}) {
|
|
|
1167
1167
|
return false;
|
|
1168
1168
|
}
|
|
1169
1169
|
},
|
|
1170
|
+
async ensembleExists(ensemble) {
|
|
1171
|
+
// #673 — STRONGLY-CONSISTENT existence check. `describe()` the per-ensemble
|
|
1172
|
+
// maestro HUB (started at `up`/creation via `ensureMaestroWorkflow`) — it
|
|
1173
|
+
// reflects a just-started workflow IMMEDIATELY, unlike `listEnsembles`
|
|
1174
|
+
// (Temporal visibility, eventually consistent on Cloud). Only RUNNING
|
|
1175
|
+
// counts as "exists": a TERMINATED/COMPLETED hub (destroyed ensemble) → false,
|
|
1176
|
+
// and a never-created hub throws WorkflowNotFoundError → false.
|
|
1177
|
+
try {
|
|
1178
|
+
const desc = await handle((0, config_1.maestroWorkflowId)(ensemble)).describe();
|
|
1179
|
+
return desc.status.name === 'RUNNING';
|
|
1180
|
+
}
|
|
1181
|
+
catch {
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1170
1185
|
// ── Maestro session (TUI-owned workflow for two-way messaging) ──
|
|
1171
1186
|
async ensureMaestroSession(ensemble) {
|
|
1172
1187
|
const workflowId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
|
|
@@ -428,6 +428,15 @@ export interface TempoClientCore {
|
|
|
428
428
|
isConnected(): Promise<boolean>;
|
|
429
429
|
/** Check if the Global Maestro workflow is running. */
|
|
430
430
|
hasGlobalMaestro(): Promise<boolean>;
|
|
431
|
+
/**
|
|
432
|
+
* #673 — STRONGLY-CONSISTENT existence check for an ensemble: `describe()` the
|
|
433
|
+
* per-ensemble maestro hub workflow (started at `up`/creation) and report
|
|
434
|
+
* whether it's RUNNING. Unlike {@link listEnsembles} (Temporal VISIBILITY,
|
|
435
|
+
* eventually consistent on Cloud), `describe` reflects a just-started workflow
|
|
436
|
+
* immediately — the SSE existence gate uses it as a fallback so a fresh
|
|
437
|
+
* ensemble isn't 404'd before visibility catches up.
|
|
438
|
+
*/
|
|
439
|
+
ensembleExists(ensemble: string): Promise<boolean>;
|
|
431
440
|
/**
|
|
432
441
|
* Subscribe to the per-ensemble SSE event stream exposed by the daemon
|
|
433
442
|
* at `/v1/events/:ensemble`. Returns an `AsyncIterable<TempoEvent>` —
|
package/dist/config.d.ts
CHANGED
|
@@ -121,6 +121,15 @@ export declare const ENV: {
|
|
|
121
121
|
* module loads (see `src/cli/dev-mode-bootstrap.ts`).
|
|
122
122
|
*/
|
|
123
123
|
readonly DEV_MODE: "AGENT_TEMPO_DEV_MODE";
|
|
124
|
+
/**
|
|
125
|
+
* #672 — set to `'1'` by a TRANSIENT-CLI spawner (e.g. the short-lived `up`
|
|
126
|
+
* conductor) on a process it intentionally DETACHES to outlive that spawner.
|
|
127
|
+
* Tells the parent-death watchdog to skip ONLY the ppid-poll signal (which
|
|
128
|
+
* would otherwise self-kill the detached process when the transient spawner
|
|
129
|
+
* exits); the universally-correct stdin-EOF signal stays. Daemon-recruit
|
|
130
|
+
* spawns do NOT set it, so recruited adapters keep the #604 anti-leak ppid-poll.
|
|
131
|
+
*/
|
|
132
|
+
readonly NO_PPID_WATCHDOG: "AGENT_TEMPO_NO_PPID_WATCHDOG";
|
|
124
133
|
/**
|
|
125
134
|
* Escape hatch for triple-isolated environments (ADR 0014 §5.3). When
|
|
126
135
|
* set, `resolveTempoHome()` returns this path verbatim — bypassing both
|
package/dist/config.js
CHANGED
|
@@ -152,6 +152,15 @@ exports.ENV = {
|
|
|
152
152
|
* module loads (see `src/cli/dev-mode-bootstrap.ts`).
|
|
153
153
|
*/
|
|
154
154
|
DEV_MODE: 'AGENT_TEMPO_DEV_MODE',
|
|
155
|
+
/**
|
|
156
|
+
* #672 — set to `'1'` by a TRANSIENT-CLI spawner (e.g. the short-lived `up`
|
|
157
|
+
* conductor) on a process it intentionally DETACHES to outlive that spawner.
|
|
158
|
+
* Tells the parent-death watchdog to skip ONLY the ppid-poll signal (which
|
|
159
|
+
* would otherwise self-kill the detached process when the transient spawner
|
|
160
|
+
* exits); the universally-correct stdin-EOF signal stays. Daemon-recruit
|
|
161
|
+
* spawns do NOT set it, so recruited adapters keep the #604 anti-leak ppid-poll.
|
|
162
|
+
*/
|
|
163
|
+
NO_PPID_WATCHDOG: 'AGENT_TEMPO_NO_PPID_WATCHDOG',
|
|
155
164
|
/**
|
|
156
165
|
* Escape hatch for triple-isolated environments (ADR 0014 §5.3). When
|
|
157
166
|
* set, `resolveTempoHome()` returns this path verbatim — bypassing both
|
package/dist/http/server.js
CHANGED
|
@@ -546,9 +546,20 @@ async function handle(req, res, ctx) {
|
|
|
546
546
|
}
|
|
547
547
|
// Validate existence before opening the SSE stream — clean 404 when
|
|
548
548
|
// the ensemble was never live, instead of an empty stream.
|
|
549
|
+
//
|
|
550
|
+
// #673 — `listEnsembles` is a Temporal VISIBILITY query (eventually
|
|
551
|
+
// consistent; ~seconds behind on Temporal Cloud), so immediately after
|
|
552
|
+
// `up`/creation the just-started maestro hub isn't indexed yet and this
|
|
553
|
+
// gate would 404 — which the subscribe client classes as PERMANENT, leaving
|
|
554
|
+
// the TUI stuck on "Loading messages…". Before 404'ing, fall back to a
|
|
555
|
+
// STRONGLY-CONSISTENT describe of the maestro hub (`ensembleExists`): a
|
|
556
|
+
// RUNNING hub means the ensemble is live even if visibility hasn't caught up.
|
|
549
557
|
const list = await ctx.client.listEnsembles().catch(() => []);
|
|
550
558
|
if (!list.find((e) => e.name === ensemble)) {
|
|
551
|
-
|
|
559
|
+
const existsStrong = await ctx.client.ensembleExists(ensemble).catch(() => false);
|
|
560
|
+
if (!existsStrong) {
|
|
561
|
+
return (0, responses_1.errorResponse)(res, 404, { error: 'ensemble-not-found', ensemble });
|
|
562
|
+
}
|
|
552
563
|
}
|
|
553
564
|
const bus = ctx.aggregate.getOrCreateEnsembleBus(ensemble);
|
|
554
565
|
return (0, sse_handler_1.handleSseRequest)(req, res, {
|
package/dist/pi/extension.d.ts
CHANGED
|
@@ -1,15 +1,57 @@
|
|
|
1
1
|
import type { Client } from '@temporalio/client';
|
|
2
2
|
import { type Config } from '../config';
|
|
3
3
|
import type { ExtensionAPI, PiAgentSession } from './pi-types';
|
|
4
|
+
import { PhaseDriver } from './phase-driver';
|
|
5
|
+
import { PiWorkflowClient } from './workflow-client';
|
|
6
|
+
import { CuePump } from './cue-pump';
|
|
7
|
+
import { ResetPump } from './reset-pump';
|
|
8
|
+
import { InnerLoopPublisher } from './inner-loop-publisher';
|
|
4
9
|
/** Runtime mode. Headless = recruited unsupervised player (MD-C gate active). */
|
|
5
10
|
export type PiExtensionMode = 'interactive' | 'headless';
|
|
6
11
|
export type PiToolAccess = 'restricted' | 'standard' | 'full';
|
|
12
|
+
/**
|
|
13
|
+
* B1 runtime guard (#645 H4) — the type gate's blind spot.
|
|
14
|
+
*
|
|
15
|
+
* `PiEventPayload.session` is UNDECLARED in Pi 0.78's `.d.ts` — it's an
|
|
16
|
+
* interactive-only RUNTIME field, so the pi-drift type gate can't assert it. In
|
|
17
|
+
* INTERACTIVE mode the `session_start` payload MUST carry `session` (the cue +
|
|
18
|
+
* reset pumps inject into it); a null session there means injection is silently
|
|
19
|
+
* inert — a likely Pi API drift. (Headless legitimately omits it — it wires
|
|
20
|
+
* `rt.session` via `setRuntimeSession` — so the guard is interactive-only.)
|
|
21
|
+
*
|
|
22
|
+
* Pure + injected `warn` so it unit-tests without the workflow harness.
|
|
23
|
+
*/
|
|
24
|
+
export declare function warnIfInteractiveSessionMissing(mode: PiExtensionMode, payload: {
|
|
25
|
+
session?: unknown;
|
|
26
|
+
}, warn: (msg: string) => void): void;
|
|
7
27
|
export interface PiExtensionOptions {
|
|
8
28
|
/** Default `'interactive'`. Headless installs the MD-C tool_call gate. */
|
|
9
29
|
mode?: PiExtensionMode;
|
|
10
30
|
/** MD-C tool-class policy (headless only). Default `'restricted'`. */
|
|
11
31
|
toolAccess?: PiToolAccess;
|
|
12
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Per-PLAYER runtime — lives in the module-scope `runtimes` map and SURVIVES Pi
|
|
35
|
+
* extension-instance rebuilds. Holds the durable attachment (handle + lease +
|
|
36
|
+
* heartbeat, inside `wf`), the phase driver, the cue pump, and the session ptr.
|
|
37
|
+
*/
|
|
38
|
+
interface PiPlayerRuntime {
|
|
39
|
+
readonly workflowId: string;
|
|
40
|
+
readonly wf: PiWorkflowClient;
|
|
41
|
+
readonly driver: PhaseDriver;
|
|
42
|
+
readonly pump: CuePump;
|
|
43
|
+
/**
|
|
44
|
+
* 3c — inner-loop publisher: observes Pi events to maintain Tier-1 coarse state
|
|
45
|
+
* (sampled by the heartbeat via `wf.setCoarseProvider`) and forward Tier-2 fine
|
|
46
|
+
* frames to the daemon (presence-gated, off-wire). Started on first attach,
|
|
47
|
+
* stopped on teardown.
|
|
48
|
+
*/
|
|
49
|
+
readonly pub: InnerLoopPublisher;
|
|
50
|
+
/** 3d D14 — polls the workflow's pending reset → clean-wipe (newSession) + ack. */
|
|
51
|
+
readonly reset: ResetPump;
|
|
52
|
+
session: PiAgentSession | null;
|
|
53
|
+
lastSessionId?: string;
|
|
54
|
+
}
|
|
13
55
|
/**
|
|
14
56
|
* Build the Pi extension factory. `mode='headless'` installs the MD-C tool_call
|
|
15
57
|
* gate; `mode='interactive'` (default) does not (the human owns their machine).
|
|
@@ -40,6 +82,18 @@ export declare function setRuntimeSession(workflowId: string, session: PiAgentSe
|
|
|
40
82
|
export declare function __setPiClientFactoryForTests(factory: (config: Config) => Promise<Client>): void;
|
|
41
83
|
/** Stop timers, clear the per-player runtime map + shared-client singletons + factory. */
|
|
42
84
|
export declare function __resetPiRuntimesForTests(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Seed a fake runtime into the module-scope `runtimes` map. TEST ESCAPE HATCH —
|
|
87
|
+
* do NOT call from production code. The map is otherwise unreachable from a test;
|
|
88
|
+
* this is the seam for covering lifecycle paths like {@link detachAllPiRuntimesForExit}.
|
|
89
|
+
*/
|
|
90
|
+
export declare function __seedRuntimeForTests(workflowId: string, rt: PiPlayerRuntime): void;
|
|
91
|
+
/**
|
|
92
|
+
* Clear the module-scope `runtimes` map WITHOUT timer/heartbeat teardown (for
|
|
93
|
+
* afterEach isolation in runtime-seeding tests; use {@link __resetPiRuntimesForTests}
|
|
94
|
+
* for the full singleton reset). TEST ESCAPE HATCH — do NOT call from production code.
|
|
95
|
+
*/
|
|
96
|
+
export declare function __clearRuntimesForTests(): void;
|
|
43
97
|
/** Default export — interactive-mode extension (the human `pi` CLI entry). */
|
|
44
98
|
declare const piExtension: (pi: ExtensionAPI) => void;
|
|
45
99
|
export default piExtension;
|
package/dist/pi/extension.js
CHANGED
|
@@ -33,11 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.warnIfInteractiveSessionMissing = warnIfInteractiveSessionMissing;
|
|
36
37
|
exports.createPiExtension = createPiExtension;
|
|
37
38
|
exports.detachAllPiRuntimesForExit = detachAllPiRuntimesForExit;
|
|
38
39
|
exports.setRuntimeSession = setRuntimeSession;
|
|
39
40
|
exports.__setPiClientFactoryForTests = __setPiClientFactoryForTests;
|
|
40
41
|
exports.__resetPiRuntimesForTests = __resetPiRuntimesForTests;
|
|
42
|
+
exports.__seedRuntimeForTests = __seedRuntimeForTests;
|
|
43
|
+
exports.__clearRuntimesForTests = __clearRuntimesForTests;
|
|
41
44
|
/**
|
|
42
45
|
* agent-tempo Pi extension — interactive (Phase 2) + headless (Phase 3a) runtime.
|
|
43
46
|
*
|
|
@@ -91,6 +94,24 @@ const log = (...args) => {
|
|
|
91
94
|
};
|
|
92
95
|
const nowIso = () => new Date().toISOString();
|
|
93
96
|
const PI_AGENT_TYPE = 'claude'; // Pi is not yet a first-class AgentType.
|
|
97
|
+
/**
|
|
98
|
+
* B1 runtime guard (#645 H4) — the type gate's blind spot.
|
|
99
|
+
*
|
|
100
|
+
* `PiEventPayload.session` is UNDECLARED in Pi 0.78's `.d.ts` — it's an
|
|
101
|
+
* interactive-only RUNTIME field, so the pi-drift type gate can't assert it. In
|
|
102
|
+
* INTERACTIVE mode the `session_start` payload MUST carry `session` (the cue +
|
|
103
|
+
* reset pumps inject into it); a null session there means injection is silently
|
|
104
|
+
* inert — a likely Pi API drift. (Headless legitimately omits it — it wires
|
|
105
|
+
* `rt.session` via `setRuntimeSession` — so the guard is interactive-only.)
|
|
106
|
+
*
|
|
107
|
+
* Pure + injected `warn` so it unit-tests without the workflow harness.
|
|
108
|
+
*/
|
|
109
|
+
function warnIfInteractiveSessionMissing(mode, payload, warn) {
|
|
110
|
+
if (mode === 'interactive' && payload.session == null) {
|
|
111
|
+
warn('WARNING: interactive session_start carried no session — cue/reset injection inert; ' +
|
|
112
|
+
'possible Pi API drift (#645)');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
94
115
|
// MD-C shell/exec tool-class membership is owned by `tool-capability.ts`
|
|
95
116
|
// (`classify(name) === 'exec'`, content signed off by tempo-security). F1
|
|
96
117
|
// import-refactor (3d): this REPLACES the former local `SHELL_TOOL_NAMES` set —
|
|
@@ -282,6 +303,8 @@ function createPiExtension(options = {}) {
|
|
|
282
303
|
}
|
|
283
304
|
// ── Lifecycle: session_start → first attach OR re-bind ──
|
|
284
305
|
pi.on('session_start', async (payload) => {
|
|
306
|
+
// B1 (#645 H4): warn loudly if interactive session_start lost its session.
|
|
307
|
+
warnIfInteractiveSessionMissing(mode, payload, log);
|
|
285
308
|
try {
|
|
286
309
|
const rt = await attachOrRebind(payload);
|
|
287
310
|
await refreshSessionId(rt, rt.session?.id);
|
|
@@ -402,6 +425,22 @@ function __resetPiRuntimesForTests() {
|
|
|
402
425
|
connectedClient = null;
|
|
403
426
|
clientFactory = getSharedClient;
|
|
404
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Seed a fake runtime into the module-scope `runtimes` map. TEST ESCAPE HATCH —
|
|
430
|
+
* do NOT call from production code. The map is otherwise unreachable from a test;
|
|
431
|
+
* this is the seam for covering lifecycle paths like {@link detachAllPiRuntimesForExit}.
|
|
432
|
+
*/
|
|
433
|
+
function __seedRuntimeForTests(workflowId, rt) {
|
|
434
|
+
runtimes.set(workflowId, rt);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Clear the module-scope `runtimes` map WITHOUT timer/heartbeat teardown (for
|
|
438
|
+
* afterEach isolation in runtime-seeding tests; use {@link __resetPiRuntimesForTests}
|
|
439
|
+
* for the full singleton reset). TEST ESCAPE HATCH — do NOT call from production code.
|
|
440
|
+
*/
|
|
441
|
+
function __clearRuntimesForTests() {
|
|
442
|
+
runtimes.clear();
|
|
443
|
+
}
|
|
405
444
|
/** Default export — interactive-mode extension (the human `pi` CLI entry). */
|
|
406
445
|
const piExtension = createPiExtension();
|
|
407
446
|
exports.default = piExtension;
|
package/dist/pi/pi-types.d.ts
CHANGED
|
@@ -37,13 +37,19 @@ export interface PiCustomMessageOptions {
|
|
|
37
37
|
*/
|
|
38
38
|
deliverAs?: 'steer' | 'followUp';
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* A message injected into a live Pi session. Real `sendCustomMessage` msg param =
|
|
42
|
+
* `Pick<CustomMessage, "customType" | "content" | "display" | "details">` — so
|
|
43
|
+
* `customType` + `display` are REQUIRED (C1, #645 H4). `content` is kept
|
|
44
|
+
* `string` (narrower than the real `string | (TextContent | ImageContent)[]`,
|
|
45
|
+
* still assignable — we only ever inject text).
|
|
46
|
+
*/
|
|
41
47
|
export interface PiOutboundMessage {
|
|
42
48
|
/** Free-form tag Pi surfaces to the agent (we use `'cue'`). */
|
|
43
|
-
customType
|
|
49
|
+
customType: string;
|
|
44
50
|
content: string;
|
|
45
51
|
/** Render the injected content in the human-visible transcript. */
|
|
46
|
-
display
|
|
52
|
+
display: boolean;
|
|
47
53
|
}
|
|
48
54
|
/**
|
|
49
55
|
* The live, human-attached agent session. `sendCustomMessage` is bound in the
|
|
@@ -73,7 +79,13 @@ export interface PiAgentSession {
|
|
|
73
79
|
* RE-ACQUIRE the session from each payload (never cache across switches — D11).
|
|
74
80
|
*/
|
|
75
81
|
export interface PiEventPayload {
|
|
76
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* UNDECLARED in Pi 0.78 `.d.ts` — an interactive-only RUNTIME field; headless
|
|
84
|
+
* populates `rt.session` via `setRuntimeSession` instead. NOT type-assertable,
|
|
85
|
+
* so the pi-drift gate EXCLUDES it (see test/pi-drift/assert.ts) and the
|
|
86
|
+
* runtime guard B1 (extension.ts) covers it: a null session on interactive
|
|
87
|
+
* `session_start` warns of possible Pi API drift.
|
|
88
|
+
*/
|
|
77
89
|
session?: PiAgentSession;
|
|
78
90
|
/** A per-message/per-turn identifier when the event carries one. */
|
|
79
91
|
messageId?: string;
|
|
@@ -90,7 +102,8 @@ export interface PiEventPayload {
|
|
|
90
102
|
/**
|
|
91
103
|
* Pi context-window usage — the PULL-only token signal (3c). There is NO token
|
|
92
104
|
* event in Pi 0.78; usage is queried on demand via `ExtensionContext.getContextUsage()`.
|
|
93
|
-
* Mirrors
|
|
105
|
+
* Mirrors `ContextUsage`, exported from `@earendil-works/pi-coding-agent`
|
|
106
|
+
* (extensions surface), NOT pi-ai (verified against the installed 0.78 `.d.ts`).
|
|
94
107
|
*/
|
|
95
108
|
export interface PiContextUsage {
|
|
96
109
|
/** Estimated context tokens in use, or `null` when unknown. */
|
|
@@ -205,6 +218,12 @@ export interface PiToolResult {
|
|
|
205
218
|
*/
|
|
206
219
|
export interface PiToolDefinition {
|
|
207
220
|
name: string;
|
|
221
|
+
/**
|
|
222
|
+
* Human-facing label. The real `ToolDefinition` REQUIRES `label` (C4, #645 H4);
|
|
223
|
+
* render-tools sets it to the tool name. Omitting it would RED the gate's
|
|
224
|
+
* ToolDefinition row.
|
|
225
|
+
*/
|
|
226
|
+
label: string;
|
|
208
227
|
description?: string;
|
|
209
228
|
/** TypeBox schema object. Typed `unknown` to avoid coupling pi-types to typebox. */
|
|
210
229
|
parameters: unknown;
|
package/dist/pi/render-tools.js
CHANGED
|
@@ -43,6 +43,8 @@ function renderToPi(pi, descriptors) {
|
|
|
43
43
|
for (const d of descriptors) {
|
|
44
44
|
pi.registerTool({
|
|
45
45
|
name: d.name,
|
|
46
|
+
// Real ToolDefinition REQUIRES `label` (C4, #645 H4) — use the tool name.
|
|
47
|
+
label: d.name,
|
|
46
48
|
description: d.description,
|
|
47
49
|
parameters: (0, zod_to_typebox_1.zodShapeToTypeBox)(d.params, d.name),
|
|
48
50
|
// Pi calls execute POSITIONALLY: (toolCallId, params, signal, onUpdate, ctx).
|
package/dist/spawn.d.ts
CHANGED
|
@@ -38,9 +38,12 @@ export declare function findLinuxTerminal(): string | null;
|
|
|
38
38
|
* Build a shell command string that sets env vars and runs claude.
|
|
39
39
|
* Uses inline `KEY=val` syntax which works in bash, zsh, AND fish.
|
|
40
40
|
*/
|
|
41
|
-
export declare function
|
|
41
|
+
export declare function buildTerminalCommand(bin: string, binArgs: string[], envVars: Record<string, string>): string;
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
43
|
+
* Launch ANY binary in a visible terminal window (the cross-platform core
|
|
44
|
+
* extracted from `spawnInTerminal`, #666 C1). Generic over `bin`/`args` so it
|
|
45
|
+
* drives both Claude (via the {@link spawnInTerminal} wrapper) and the
|
|
46
|
+
* interactive Pi conductor (`pi -e <ext>`).
|
|
44
47
|
*
|
|
45
48
|
* Strategy per terminal:
|
|
46
49
|
* - Ghostty: `initial input` into a normal window (preserves full shell env)
|
|
@@ -49,11 +52,82 @@ export declare function buildClaudeCommand(claudeBin: string, claudeArgs: string
|
|
|
49
52
|
* - Windows: shell:true with env vars
|
|
50
53
|
* - Linux: terminal emulator with -e flag
|
|
51
54
|
*/
|
|
55
|
+
export declare function launchInTerminal(bin: string, args: string[], workDir: string, envVars: Record<string, string>): {
|
|
56
|
+
pid: number | undefined;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Spawn a Claude Code session in a visible terminal window — a thin, unchanged
|
|
60
|
+
* wrapper over {@link launchInTerminal} (resolves the claude binary, forwards
|
|
61
|
+
* the rest). Signature preserved for the existing callers (commands.ts conductor
|
|
62
|
+
* + outbox.ts recruit-spawn) + the spawn-route regression tests (#666 C1).
|
|
63
|
+
*/
|
|
52
64
|
export declare function spawnInTerminal(claudeArgs: string[], workDir: string, envVars: Record<string, string>, options?: {
|
|
53
65
|
claudeBin?: string;
|
|
54
66
|
}): {
|
|
55
67
|
pid: number | undefined;
|
|
56
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the INTERACTIVE Pi CLI binary (#666) — the human-TTY `pi`, DISTINCT
|
|
71
|
+
* from {@link resolvePiPath} (the headless adapter entry). Interactive TTY mode
|
|
72
|
+
* is REQUIRED for a conductor: Pi only fires `session_start` / attaches in a real
|
|
73
|
+
* terminal (non-TTY / `--print` → print-mode → tools register but NO attach).
|
|
74
|
+
*
|
|
75
|
+
* `pi` on PATH wins; else fall back to the installed package CLI via `node`.
|
|
76
|
+
* THROWS fail-clean if neither resolves, so the caller never launches a terminal
|
|
77
|
+
* that would immediately die. Collaborators injectable for unit tests.
|
|
78
|
+
*/
|
|
79
|
+
export declare function resolvePiInteractiveBinary(deps?: {
|
|
80
|
+
onPath?: (bin: string) => boolean;
|
|
81
|
+
exists?: (p: string) => boolean;
|
|
82
|
+
}): {
|
|
83
|
+
cmd: string;
|
|
84
|
+
args: string[];
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the absolute path to the BUNDLED `dist/pi/extension.js` for `pi -e <abs>`
|
|
88
|
+
* (#666). Pi loads the BUILT CommonJS extension even in dev. Mirrors
|
|
89
|
+
* {@link resolvePiPath}'s dev/prod `__dirname` split: prod `__dirname` = `dist/`
|
|
90
|
+
* (→ `dist/pi/extension.js`); dev `__dirname` = `src/` (→ sibling `dist/pi/…`).
|
|
91
|
+
* Existence-checked + fail-clean ("run npm run build"). Injectable for tests.
|
|
92
|
+
*/
|
|
93
|
+
export declare function resolvePiExtensionPath(deps?: {
|
|
94
|
+
exists?: (p: string) => boolean;
|
|
95
|
+
isDev?: boolean;
|
|
96
|
+
baseDir?: string;
|
|
97
|
+
}): string;
|
|
98
|
+
/** Inputs for {@link buildPiConductorSpawn} (pure — unit-tested without spawning). */
|
|
99
|
+
export interface PiConductorSpawnOpts {
|
|
100
|
+
ensemble: string;
|
|
101
|
+
sessionName: string;
|
|
102
|
+
/** Temporal env (address/namespace/api-key/tls) built by the caller. */
|
|
103
|
+
temporalEnvVars: Record<string, string>;
|
|
104
|
+
/** Temporal task queue — the Pi extension's PiWorkflowClient needs it (confirm #1). */
|
|
105
|
+
taskQueue: string;
|
|
106
|
+
devMode: boolean;
|
|
107
|
+
/** Conductor agent-type name → AGENT_TEMPO_PLAYER_TYPE, when typed. */
|
|
108
|
+
conductorTypeName?: string;
|
|
109
|
+
/** Forwarded if set (warn-not-fail upstream when unset). */
|
|
110
|
+
anthropicApiKey?: string;
|
|
111
|
+
/** Injectable resolvers (default to the real ones, which fail-clean on miss). */
|
|
112
|
+
resolveBinary?: () => {
|
|
113
|
+
cmd: string;
|
|
114
|
+
args: string[];
|
|
115
|
+
};
|
|
116
|
+
resolveExtension?: () => string;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build the interactive Pi conductor spawn spec — `{ cmd, args, env }` for
|
|
120
|
+
* {@link launchInTerminal} (#666 C3). PURE + injectable so the env/args mapping is
|
|
121
|
+
* unit-tested. The default resolvers THROW fail-clean (binary missing / extension
|
|
122
|
+
* unbuilt) BEFORE a terminal is launched. `args` = `[...binArgs, '-e', <ext>]`;
|
|
123
|
+
* conductor INSTRUCTIONS arrive via the lineup-baked workflow messages → cue pump
|
|
124
|
+
* (no `--system-prompt` for the MVP).
|
|
125
|
+
*/
|
|
126
|
+
export declare function buildPiConductorSpawn(opts: PiConductorSpawnOpts): {
|
|
127
|
+
cmd: string;
|
|
128
|
+
args: string[];
|
|
129
|
+
env: Record<string, string>;
|
|
130
|
+
};
|
|
57
131
|
export interface CopilotBridgeOpts {
|
|
58
132
|
name: string;
|
|
59
133
|
ensemble: string;
|
|
@@ -76,6 +150,16 @@ export interface CopilotBridgeOpts {
|
|
|
76
150
|
attachmentId?: string;
|
|
77
151
|
attachmentRunId?: string;
|
|
78
152
|
adapterId?: string;
|
|
153
|
+
/**
|
|
154
|
+
* #672 — set true by a TRANSIENT-CLI spawner that launches this bridge DETACHED
|
|
155
|
+
* to outlive it: BOTH the `up` conductor (commands.ts) AND the `up --lineup`
|
|
156
|
+
* copilot PLAYER loop (commands.ts applyLineupPlayersAndSchedules) — both spawn
|
|
157
|
+
* the bridge directly (no terminal), so its ppid is the short-lived CLI. When
|
|
158
|
+
* set, the bridge skips the ppid-poll that would self-kill it on the CLI's exit
|
|
159
|
+
* (stdin-EOF stays). The DAEMON-recruit path (outbox.ts) OMITS it → the bridge
|
|
160
|
+
* keeps the ppid-poll (#604 anti-leak on daemon death; ppid = persistent daemon).
|
|
161
|
+
*/
|
|
162
|
+
transientSpawner?: boolean;
|
|
79
163
|
}
|
|
80
164
|
export interface CopilotBridgeResult {
|
|
81
165
|
pid: number | undefined;
|
package/dist/spawn.js
CHANGED
|
@@ -6,8 +6,12 @@ exports.shellQuote = shellQuote;
|
|
|
6
6
|
exports.resolveClaudePath = resolveClaudePath;
|
|
7
7
|
exports.detectMacTerminal = detectMacTerminal;
|
|
8
8
|
exports.findLinuxTerminal = findLinuxTerminal;
|
|
9
|
-
exports.
|
|
9
|
+
exports.buildTerminalCommand = buildTerminalCommand;
|
|
10
|
+
exports.launchInTerminal = launchInTerminal;
|
|
10
11
|
exports.spawnInTerminal = spawnInTerminal;
|
|
12
|
+
exports.resolvePiInteractiveBinary = resolvePiInteractiveBinary;
|
|
13
|
+
exports.resolvePiExtensionPath = resolvePiExtensionPath;
|
|
14
|
+
exports.buildPiConductorSpawn = buildPiConductorSpawn;
|
|
11
15
|
exports.spawnCopilotBridge = spawnCopilotBridge;
|
|
12
16
|
exports.spawnMockAdapter = spawnMockAdapter;
|
|
13
17
|
exports.spawnClaudeApiAdapter = spawnClaudeApiAdapter;
|
|
@@ -234,17 +238,20 @@ function findLinuxTerminal() {
|
|
|
234
238
|
* Build a shell command string that sets env vars and runs claude.
|
|
235
239
|
* Uses inline `KEY=val` syntax which works in bash, zsh, AND fish.
|
|
236
240
|
*/
|
|
237
|
-
function
|
|
241
|
+
function buildTerminalCommand(bin, binArgs, envVars) {
|
|
238
242
|
const envInline = Object.entries(envVars)
|
|
239
243
|
.map(([k, v]) => `${k}=${shellQuote(v)}`)
|
|
240
244
|
.join(' ');
|
|
241
245
|
// Quote the binary path if it contains spaces (e.g., "C:\Program Files\...")
|
|
242
|
-
const quotedBin =
|
|
243
|
-
const args =
|
|
246
|
+
const quotedBin = bin.includes(' ') ? shellQuote(bin) : bin;
|
|
247
|
+
const args = binArgs.map(a => shellQuote(a)).join(' ');
|
|
244
248
|
return envInline ? `${envInline} ${quotedBin} ${args}` : `${quotedBin} ${args}`;
|
|
245
249
|
}
|
|
246
250
|
/**
|
|
247
|
-
*
|
|
251
|
+
* Launch ANY binary in a visible terminal window (the cross-platform core
|
|
252
|
+
* extracted from `spawnInTerminal`, #666 C1). Generic over `bin`/`args` so it
|
|
253
|
+
* drives both Claude (via the {@link spawnInTerminal} wrapper) and the
|
|
254
|
+
* interactive Pi conductor (`pi -e <ext>`).
|
|
248
255
|
*
|
|
249
256
|
* Strategy per terminal:
|
|
250
257
|
* - Ghostty: `initial input` into a normal window (preserves full shell env)
|
|
@@ -253,9 +260,14 @@ function buildClaudeCommand(claudeBin, claudeArgs, envVars) {
|
|
|
253
260
|
* - Windows: shell:true with env vars
|
|
254
261
|
* - Linux: terminal emulator with -e flag
|
|
255
262
|
*/
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
|
|
263
|
+
function launchInTerminal(bin, args, workDir, envVars) {
|
|
264
|
+
// Internal aliases keep the platform body below byte-identical to the original
|
|
265
|
+
// spawnInTerminal (behavior-preserving extraction, #666 C1 — minimal blast
|
|
266
|
+
// radius; the existing terminal-spawn tests are the proof). The terminal logic
|
|
267
|
+
// is bin/args-agnostic; the `claude*` names are historical.
|
|
268
|
+
const claudeBin = bin;
|
|
269
|
+
const claudeArgs = args;
|
|
270
|
+
const claudeInvocation = buildTerminalCommand(claudeBin, claudeArgs, envVars);
|
|
259
271
|
if (process.platform === 'darwin') {
|
|
260
272
|
const detected = detectMacTerminal();
|
|
261
273
|
log(`Terminal detection: TERM_PROGRAM=${JSON.stringify(process.env.TERM_PROGRAM)}, detected=${detected}`);
|
|
@@ -414,6 +426,109 @@ function spawnInTerminal(claudeArgs, workDir, envVars, options) {
|
|
|
414
426
|
child.unref();
|
|
415
427
|
return { pid: child.pid };
|
|
416
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* Spawn a Claude Code session in a visible terminal window — a thin, unchanged
|
|
431
|
+
* wrapper over {@link launchInTerminal} (resolves the claude binary, forwards
|
|
432
|
+
* the rest). Signature preserved for the existing callers (commands.ts conductor
|
|
433
|
+
* + outbox.ts recruit-spawn) + the spawn-route regression tests (#666 C1).
|
|
434
|
+
*/
|
|
435
|
+
function spawnInTerminal(claudeArgs, workDir, envVars, options) {
|
|
436
|
+
return launchInTerminal(resolveClaudePath(options?.claudeBin), claudeArgs, workDir, envVars);
|
|
437
|
+
}
|
|
438
|
+
// --- Interactive Pi conductor (#666) ---
|
|
439
|
+
/** Is `bin` resolvable on PATH? (where/which, mirrors resolveClaudePath.) */
|
|
440
|
+
function binaryOnPath(bin) {
|
|
441
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
442
|
+
try {
|
|
443
|
+
(0, child_process_1.execFileSync)(cmd, [bin], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/** Walk up from `__dirname` for the installed Pi package's CLI entry. */
|
|
451
|
+
function findPiPackageCli(exists) {
|
|
452
|
+
let dir = __dirname;
|
|
453
|
+
for (let i = 0; i < 8; i += 1) {
|
|
454
|
+
const candidate = (0, path_1.join)(dir, 'node_modules', '@earendil-works', 'pi-coding-agent', 'dist', 'cli.js');
|
|
455
|
+
if (exists(candidate))
|
|
456
|
+
return candidate;
|
|
457
|
+
const parent = (0, path_1.dirname)(dir);
|
|
458
|
+
if (parent === dir)
|
|
459
|
+
break;
|
|
460
|
+
dir = parent;
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Resolve the INTERACTIVE Pi CLI binary (#666) — the human-TTY `pi`, DISTINCT
|
|
466
|
+
* from {@link resolvePiPath} (the headless adapter entry). Interactive TTY mode
|
|
467
|
+
* is REQUIRED for a conductor: Pi only fires `session_start` / attaches in a real
|
|
468
|
+
* terminal (non-TTY / `--print` → print-mode → tools register but NO attach).
|
|
469
|
+
*
|
|
470
|
+
* `pi` on PATH wins; else fall back to the installed package CLI via `node`.
|
|
471
|
+
* THROWS fail-clean if neither resolves, so the caller never launches a terminal
|
|
472
|
+
* that would immediately die. Collaborators injectable for unit tests.
|
|
473
|
+
*/
|
|
474
|
+
function resolvePiInteractiveBinary(deps = {}) {
|
|
475
|
+
const onPath = deps.onPath ?? binaryOnPath;
|
|
476
|
+
const exists = deps.exists ?? fs_1.existsSync;
|
|
477
|
+
if (onPath('pi'))
|
|
478
|
+
return { cmd: 'pi', args: [] };
|
|
479
|
+
const cli = findPiPackageCli(exists);
|
|
480
|
+
if (cli)
|
|
481
|
+
return { cmd: 'node', args: [cli] };
|
|
482
|
+
throw new Error('Pi CLI not found. Install it with `npm install -g pi-ai` and ensure `pi` is on PATH ' +
|
|
483
|
+
'(or add the @earendil-works/pi-coding-agent package). The conductor needs the interactive Pi CLI.');
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Resolve the absolute path to the BUNDLED `dist/pi/extension.js` for `pi -e <abs>`
|
|
487
|
+
* (#666). Pi loads the BUILT CommonJS extension even in dev. Mirrors
|
|
488
|
+
* {@link resolvePiPath}'s dev/prod `__dirname` split: prod `__dirname` = `dist/`
|
|
489
|
+
* (→ `dist/pi/extension.js`); dev `__dirname` = `src/` (→ sibling `dist/pi/…`).
|
|
490
|
+
* Existence-checked + fail-clean ("run npm run build"). Injectable for tests.
|
|
491
|
+
*/
|
|
492
|
+
function resolvePiExtensionPath(deps = {}) {
|
|
493
|
+
const exists = deps.exists ?? fs_1.existsSync;
|
|
494
|
+
const isDev = deps.isDev ?? __filename.endsWith('.ts');
|
|
495
|
+
const base = deps.baseDir ?? __dirname;
|
|
496
|
+
const extPath = isDev
|
|
497
|
+
? (0, path_1.resolve)(base, '..', 'dist', 'pi', 'extension.js') // dev: src/ → repo/dist/pi/extension.js
|
|
498
|
+
: (0, path_1.resolve)(base, 'pi', 'extension.js'); // prod: dist/ → dist/pi/extension.js
|
|
499
|
+
if (!exists(extPath)) {
|
|
500
|
+
throw new Error(`Pi conductor extension not found at ${extPath}. Run \`npm run build\` first.`);
|
|
501
|
+
}
|
|
502
|
+
return extPath;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Build the interactive Pi conductor spawn spec — `{ cmd, args, env }` for
|
|
506
|
+
* {@link launchInTerminal} (#666 C3). PURE + injectable so the env/args mapping is
|
|
507
|
+
* unit-tested. The default resolvers THROW fail-clean (binary missing / extension
|
|
508
|
+
* unbuilt) BEFORE a terminal is launched. `args` = `[...binArgs, '-e', <ext>]`;
|
|
509
|
+
* conductor INSTRUCTIONS arrive via the lineup-baked workflow messages → cue pump
|
|
510
|
+
* (no `--system-prompt` for the MVP).
|
|
511
|
+
*/
|
|
512
|
+
function buildPiConductorSpawn(opts) {
|
|
513
|
+
const { cmd, args: binArgs } = (opts.resolveBinary ?? resolvePiInteractiveBinary)();
|
|
514
|
+
const extPath = (opts.resolveExtension ?? resolvePiExtensionPath)();
|
|
515
|
+
const args = [...binArgs, '-e', extPath];
|
|
516
|
+
const env = {
|
|
517
|
+
...opts.temporalEnvVars,
|
|
518
|
+
[config_1.ENV.TASK_QUEUE]: opts.taskQueue,
|
|
519
|
+
[config_1.ENV.ENSEMBLE]: opts.ensemble,
|
|
520
|
+
[config_1.ENV.CONDUCTOR]: 'true', // codebase-consistent; the Pi extension accepts '1'|'true'
|
|
521
|
+
// #672 — the Pi conductor is launched detached by the transient `up` CLI:
|
|
522
|
+
// skip the ppid-poll (no current pi process installs the watchdog, but this is
|
|
523
|
+
// propagation-safe + principled if a pi subprocess ever does; stdin-EOF stays).
|
|
524
|
+
[config_1.ENV.NO_PPID_WATCHDOG]: '1',
|
|
525
|
+
[config_1.ENV.PLAYER_NAME]: opts.sessionName,
|
|
526
|
+
...(opts.devMode ? { [config_1.ENV.DEV_MODE]: '1' } : {}),
|
|
527
|
+
...(opts.anthropicApiKey ? { ANTHROPIC_API_KEY: opts.anthropicApiKey } : {}),
|
|
528
|
+
...(opts.conductorTypeName ? { [config_1.ENV.PLAYER_TYPE]: opts.conductorTypeName } : {}),
|
|
529
|
+
};
|
|
530
|
+
return { cmd, args, env };
|
|
531
|
+
}
|
|
417
532
|
/**
|
|
418
533
|
* Resolve the path to the compiled copilot bridge adapter entry point.
|
|
419
534
|
* In dev (ts-node), returns a ts-node command; in production, returns the dist path.
|
|
@@ -454,6 +569,9 @@ function spawnCopilotBridge(opts) {
|
|
|
454
569
|
[config_1.ENV.BRIDGE_MODE]: '', // Clear parent's bridge mode
|
|
455
570
|
[config_1.ENV.TEMPORAL_ADDRESS]: opts.temporalAddress,
|
|
456
571
|
[config_1.ENV.CONDUCTOR]: opts.isConductor ? 'true' : '',
|
|
572
|
+
// #672 — transient-CLI spawner: the detached bridge skips the ppid-poll
|
|
573
|
+
// (would self-kill on the short-lived `up` exit). Daemon recruit omits it.
|
|
574
|
+
...(opts.transientSpawner ? { [config_1.ENV.NO_PPID_WATCHDOG]: '1' } : {}),
|
|
457
575
|
// Forward Temporal connection settings so child processes can connect
|
|
458
576
|
...(opts.temporalNamespace ? { [config_1.ENV.TEMPORAL_NAMESPACE]: opts.temporalNamespace } : {}),
|
|
459
577
|
...(opts.temporalApiKey ? { [config_1.ENV.TEMPORAL_API_KEY]: opts.temporalApiKey } : {}),
|
package/dist/tui/index.js
CHANGED
|
@@ -134,6 +134,7 @@ function createDummyClient() {
|
|
|
134
134
|
restore: fail,
|
|
135
135
|
isConnected: async () => false,
|
|
136
136
|
hasGlobalMaestro: async () => false,
|
|
137
|
+
ensembleExists: async () => false,
|
|
137
138
|
getSchedules: async () => [],
|
|
138
139
|
cancelSchedule: fail,
|
|
139
140
|
getEnsembleChat: async () => ({ messages: [], total: 0, hasMore: false, hasConductor: false }),
|
|
@@ -1 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Should the ppid-poll signal be installed? FALSE only when a TRANSIENT-CLI
|
|
3
|
+
* spawner set {@link ENV.NO_PPID_WATCHDOG} on a process it intentionally detached
|
|
4
|
+
* to OUTLIVE it (#672 — e.g. the short-lived `up` conductor: polling its dead pid
|
|
5
|
+
* would self-kill the conductor seconds after launch). Pure + injectable.
|
|
6
|
+
*
|
|
7
|
+
* Skipping ppid-poll is propagation-SAFE: the flag inherits down the spawn tree,
|
|
8
|
+
* but stdin-EOF (always installed) protects any child — its stdin IS this
|
|
9
|
+
* process's pipe, so it fires the instant THIS process dies. Only the ppid-poll
|
|
10
|
+
* (which keys on the SPAWNER, not the immediate parent) is the harmful signal.
|
|
11
|
+
*/
|
|
12
|
+
export declare function shouldInstallPpidPoll(env?: NodeJS.ProcessEnv): boolean;
|
|
1
13
|
export declare function installParentDeathWatchdog(): void;
|
|
@@ -23,15 +23,40 @@
|
|
|
23
23
|
// we falsely conclude the parent is alive. The stdin EOF path catches
|
|
24
24
|
// that case immediately, so this is purely a fallback.
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.shouldInstallPpidPoll = shouldInstallPpidPoll;
|
|
26
27
|
exports.installParentDeathWatchdog = installParentDeathWatchdog;
|
|
28
|
+
const config_1 = require("../config");
|
|
27
29
|
const log = (...args) => console.error('[agent-tempo:watchdog]', ...args);
|
|
30
|
+
/**
|
|
31
|
+
* Should the ppid-poll signal be installed? FALSE only when a TRANSIENT-CLI
|
|
32
|
+
* spawner set {@link ENV.NO_PPID_WATCHDOG} on a process it intentionally detached
|
|
33
|
+
* to OUTLIVE it (#672 — e.g. the short-lived `up` conductor: polling its dead pid
|
|
34
|
+
* would self-kill the conductor seconds after launch). Pure + injectable.
|
|
35
|
+
*
|
|
36
|
+
* Skipping ppid-poll is propagation-SAFE: the flag inherits down the spawn tree,
|
|
37
|
+
* but stdin-EOF (always installed) protects any child — its stdin IS this
|
|
38
|
+
* process's pipe, so it fires the instant THIS process dies. Only the ppid-poll
|
|
39
|
+
* (which keys on the SPAWNER, not the immediate parent) is the harmful signal.
|
|
40
|
+
*/
|
|
41
|
+
function shouldInstallPpidPoll(env = process.env) {
|
|
42
|
+
return env[config_1.ENV.NO_PPID_WATCHDOG] !== '1';
|
|
43
|
+
}
|
|
28
44
|
function installParentDeathWatchdog() {
|
|
29
45
|
const exit = (reason) => {
|
|
30
46
|
log('parent gone (', reason, ') — exiting');
|
|
31
47
|
process.exit(0);
|
|
32
48
|
};
|
|
49
|
+
// stdin-EOF — UNIVERSALLY correct + ALWAYS installed: a closed stdin pipe means
|
|
50
|
+
// the IMMEDIATE parent is gone. This is what reaps a detached process's OWN
|
|
51
|
+
// children even when ppid-poll is skipped (the child's stdin is our pipe).
|
|
33
52
|
process.stdin.on('end', () => exit('stdin end'));
|
|
34
53
|
process.stdin.on('close', () => exit('stdin close'));
|
|
54
|
+
// ppid-poll — keys on the SPAWNER's death. Correct for a long-lived daemon
|
|
55
|
+
// spawner (#604 anti-leak), HARMFUL for a transient CLI that detached us to
|
|
56
|
+
// outlive it (#672). Skipped when the spawner marked itself transient; the
|
|
57
|
+
// Temporal lease TTL reaps a genuinely-orphaned detached process instead.
|
|
58
|
+
if (!shouldInstallPpidPoll())
|
|
59
|
+
return;
|
|
35
60
|
const parentPid = process.ppid;
|
|
36
61
|
if (parentPid && parentPid > 1) {
|
|
37
62
|
const timer = setInterval(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-tempo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Many agents, one tempo. Durable coordination for multi-agent work via Temporal.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -81,7 +81,8 @@
|
|
|
81
81
|
"lint:lockstep-version": "node -e \"const r=require('./package.json').version,d=require('./dashboard/package.json').version;if(r!==d){console.error('Version drift: root='+r+' dashboard='+d+'. Bump dashboard/package.json#version to match root.');process.exit(1);}console.log('Lockstep OK: '+r);\"",
|
|
82
82
|
"lint:lockfile-canonical": "bash scripts/check-lockfile-canonical.sh",
|
|
83
83
|
"lint:dashboard-css-sync": "npm run build:scripts && node dist/scripts/check-components-css-sync.js",
|
|
84
|
-
"
|
|
84
|
+
"lint:pi-drift": "node scripts/check-pi-drift.js",
|
|
85
|
+
"check:all": "npm run lint:test-ensemble-literals && npm run lint:skip-reasons && npm run lint:lockstep-version && npm run lint:lockfile-canonical && npm run lint:surface-drift && npm run lint:no-stale-scaffold && npm run build && npm run lint:pi-drift && npm run lint:dashboard-css-sync && npm test && npm --prefix dashboard run lint && npm --prefix dashboard run test && npm run size-limit && npm run verify-tarball"
|
|
85
86
|
},
|
|
86
87
|
"optionalDependencies": {
|
|
87
88
|
"@anthropic-ai/sdk": "~0.91.1",
|