agent-tempo 1.3.1 → 1.4.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 +39 -5
- package/README.md +6 -2
- package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
- package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
- package/dashboard/dist/index.html +1 -1
- package/dashboard/package.json +1 -1
- package/dist/activities/outbox.d.ts +30 -1
- package/dist/activities/outbox.js +96 -3
- package/dist/adapters/base.js +5 -0
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +7 -0
- package/dist/adapters/pi/adapter.d.ts +2 -0
- package/dist/adapters/pi/adapter.js +43 -0
- package/dist/adapters/pi/index.d.ts +16 -0
- package/dist/adapters/pi/index.js +10 -0
- package/dist/client/core.js +9 -2
- package/dist/client/interface.d.ts +6 -0
- package/dist/config.d.ts +79 -0
- package/dist/config.js +74 -0
- package/dist/daemon.js +32 -1
- package/dist/http/aggregate.d.ts +22 -1
- package/dist/http/aggregate.js +41 -0
- package/dist/http/auth.d.ts +94 -8
- package/dist/http/auth.js +93 -9
- package/dist/http/body.d.ts +4 -1
- package/dist/http/body.js +6 -3
- package/dist/http/event-bus.js +1 -0
- package/dist/http/event-types.d.ts +34 -2
- package/dist/http/event-types.js +1 -0
- package/dist/http/gate-audit.d.ts +12 -0
- package/dist/http/gate-audit.js +95 -0
- package/dist/http/gate-registry.d.ts +167 -0
- package/dist/http/gate-registry.js +163 -0
- package/dist/http/gate-routes.d.ts +48 -0
- package/dist/http/gate-routes.js +102 -0
- package/dist/http/ingest-registry.d.ts +30 -0
- package/dist/http/ingest-registry.js +108 -0
- package/dist/http/inner-loop-routes.d.ts +66 -0
- package/dist/http/inner-loop-routes.js +182 -0
- package/dist/http/inner-loop.d.ts +92 -0
- package/dist/http/inner-loop.js +155 -0
- package/dist/http/server.d.ts +38 -3
- package/dist/http/server.js +211 -6
- package/dist/http/snapshot.d.ts +6 -0
- package/dist/http/snapshot.js +6 -0
- package/dist/pi/cue-pump.d.ts +61 -0
- package/dist/pi/cue-pump.js +95 -0
- package/dist/pi/extension.d.ts +45 -0
- package/dist/pi/extension.js +407 -0
- package/dist/pi/gate-client.d.ts +54 -0
- package/dist/pi/gate-client.js +136 -0
- package/dist/pi/headless.d.ts +85 -0
- package/dist/pi/headless.js +250 -0
- package/dist/pi/index.d.ts +28 -0
- package/dist/pi/index.js +43 -0
- package/dist/pi/inner-loop-client.d.ts +67 -0
- package/dist/pi/inner-loop-client.js +164 -0
- package/dist/pi/inner-loop-publisher.d.ts +187 -0
- package/dist/pi/inner-loop-publisher.js +236 -0
- package/dist/pi/lazy-proxy.d.ts +37 -0
- package/dist/pi/lazy-proxy.js +55 -0
- package/dist/pi/mission-control/actions.d.ts +48 -0
- package/dist/pi/mission-control/actions.js +98 -0
- package/dist/pi/mission-control/board.d.ts +88 -0
- package/dist/pi/mission-control/board.js +141 -0
- package/dist/pi/mission-control/extension.d.ts +51 -0
- package/dist/pi/mission-control/extension.js +330 -0
- package/dist/pi/mission-control/index.d.ts +15 -0
- package/dist/pi/mission-control/index.js +32 -0
- package/dist/pi/mission-control/inner-tail.d.ts +48 -0
- package/dist/pi/mission-control/inner-tail.js +76 -0
- package/dist/pi/mission-control/pi-ui.d.ts +43 -0
- package/dist/pi/mission-control/pi-ui.js +10 -0
- package/dist/pi/mission-control/render.d.ts +6 -0
- package/dist/pi/mission-control/render.js +98 -0
- package/dist/pi/phase-driver.d.ts +74 -0
- package/dist/pi/phase-driver.js +122 -0
- package/dist/pi/pi-types.d.ts +222 -0
- package/dist/pi/pi-types.js +21 -0
- package/dist/pi/probe.d.ts +99 -0
- package/dist/pi/probe.js +179 -0
- package/dist/pi/render-tools.d.ts +17 -0
- package/dist/pi/render-tools.js +56 -0
- package/dist/pi/reset-pump.d.ts +47 -0
- package/dist/pi/reset-pump.js +85 -0
- package/dist/pi/session-seed.d.ts +74 -0
- package/dist/pi/session-seed.js +103 -0
- package/dist/pi/tool-capability.d.ts +60 -0
- package/dist/pi/tool-capability.js +156 -0
- package/dist/pi/workflow-client.d.ts +158 -0
- package/dist/pi/workflow-client.js +289 -0
- package/dist/pi/zod-to-typebox.d.ts +74 -0
- package/dist/pi/zod-to-typebox.js +191 -0
- package/dist/server-tools.d.ts +2 -0
- package/dist/server-tools.js +50 -46
- package/dist/spawn.d.ts +55 -0
- package/dist/spawn.js +72 -0
- package/dist/tools/agent-types.d.ts +2 -2
- package/dist/tools/agent-types.js +22 -17
- package/dist/tools/attachment-info.d.ts +2 -2
- package/dist/tools/attachment-info.js +38 -33
- package/dist/tools/broadcast.d.ts +2 -2
- package/dist/tools/broadcast.js +69 -64
- package/dist/tools/cancel-stage.d.ts +2 -2
- package/dist/tools/cancel-stage.js +20 -15
- package/dist/tools/clear-state.d.ts +2 -2
- package/dist/tools/clear-state.js +25 -20
- package/dist/tools/coat-check-evict.d.ts +2 -2
- package/dist/tools/coat-check-evict.js +29 -24
- package/dist/tools/coat-check-get.d.ts +2 -2
- package/dist/tools/coat-check-get.js +38 -33
- package/dist/tools/coat-check-list.d.ts +2 -2
- package/dist/tools/coat-check-list.js +48 -43
- package/dist/tools/coat-check-put.d.ts +2 -2
- package/dist/tools/coat-check-put.js +38 -33
- package/dist/tools/cue.d.ts +2 -2
- package/dist/tools/cue.js +57 -52
- package/dist/tools/descriptor.d.ts +72 -0
- package/dist/tools/descriptor.js +39 -0
- package/dist/tools/destroy.d.ts +2 -2
- package/dist/tools/destroy.js +153 -148
- package/dist/tools/ensemble.d.ts +2 -2
- package/dist/tools/ensemble.js +71 -66
- package/dist/tools/evaluate-gate.d.ts +2 -2
- package/dist/tools/evaluate-gate.js +33 -27
- package/dist/tools/fetch-state.d.ts +2 -2
- package/dist/tools/fetch-state.js +42 -37
- package/dist/tools/gates.d.ts +2 -2
- package/dist/tools/gates.js +39 -34
- package/dist/tools/hosts.d.ts +2 -2
- package/dist/tools/hosts.js +25 -20
- package/dist/tools/listen.d.ts +2 -2
- package/dist/tools/listen.js +23 -18
- package/dist/tools/load-lineup.d.ts +2 -2
- package/dist/tools/load-lineup.js +324 -319
- package/dist/tools/migrate.d.ts +2 -2
- package/dist/tools/migrate.js +45 -40
- package/dist/tools/pause.d.ts +2 -2
- package/dist/tools/pause.js +34 -29
- package/dist/tools/play.d.ts +2 -2
- package/dist/tools/play.js +53 -48
- package/dist/tools/quality-gate.d.ts +2 -2
- package/dist/tools/quality-gate.js +26 -21
- package/dist/tools/recall.d.ts +2 -2
- package/dist/tools/recall.js +32 -27
- package/dist/tools/recruit.d.ts +2 -2
- package/dist/tools/recruit.js +340 -256
- package/dist/tools/release.d.ts +2 -2
- package/dist/tools/release.js +85 -80
- package/dist/tools/report.d.ts +2 -2
- package/dist/tools/report.js +28 -23
- package/dist/tools/reset.d.ts +3 -0
- package/dist/tools/reset.js +51 -0
- package/dist/tools/restart.d.ts +2 -2
- package/dist/tools/restart.js +51 -46
- package/dist/tools/restore.d.ts +2 -2
- package/dist/tools/restore.js +76 -71
- package/dist/tools/save-lineup.d.ts +2 -2
- package/dist/tools/save-lineup.js +32 -27
- package/dist/tools/save-state.d.ts +2 -2
- package/dist/tools/save-state.js +31 -26
- package/dist/tools/schedule.d.ts +2 -2
- package/dist/tools/schedule.js +133 -128
- package/dist/tools/schedules.d.ts +2 -2
- package/dist/tools/schedules.js +41 -36
- package/dist/tools/set-ensemble-description.d.ts +2 -2
- package/dist/tools/set-ensemble-description.js +26 -21
- package/dist/tools/set-name.d.ts +2 -2
- package/dist/tools/set-name.js +38 -33
- package/dist/tools/set-part.d.ts +2 -2
- package/dist/tools/set-part.js +20 -15
- package/dist/tools/shutdown.d.ts +2 -2
- package/dist/tools/shutdown.js +39 -34
- package/dist/tools/stage.d.ts +2 -2
- package/dist/tools/stage.js +28 -23
- package/dist/tools/stages.d.ts +2 -2
- package/dist/tools/stages.js +36 -31
- package/dist/tools/unschedule.d.ts +2 -2
- package/dist/tools/unschedule.js +30 -25
- package/dist/tools/who-am-i.d.ts +2 -2
- package/dist/tools/who-am-i.js +36 -31
- package/dist/tools/worktree.d.ts +2 -2
- package/dist/tools/worktree.js +134 -129
- package/dist/tui/index.js +6 -6
- package/dist/types.d.ts +47 -2
- package/dist/types.js +1 -1
- package/dist/utils/default-part.js +1 -0
- package/dist/utils/sdk-probe.d.ts +23 -0
- package/dist/utils/sdk-probe.js +46 -7
- package/dist/worker.d.ts +3 -1
- package/dist/worker.js +6 -2
- package/dist/workflows/session.js +70 -2
- package/dist/workflows/signals.d.ts +32 -2
- package/dist/workflows/signals.js +25 -2
- package/package.json +4 -1
- package/workflow-bundle.js +97 -6
- package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
- package/dist/tools/helpers.d.ts +0 -21
- package/dist/tools/helpers.js +0 -25
package/dist/tools/recruit.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.buildRecruitTool = buildRecruitTool;
|
|
37
37
|
exports.checkHostPreflight = checkHostPreflight;
|
|
38
38
|
exports.nearestHostname = nearestHostname;
|
|
39
39
|
const zod_1 = require("zod");
|
|
@@ -42,13 +42,20 @@ const config_1 = require("../config");
|
|
|
42
42
|
const types_1 = require("../types");
|
|
43
43
|
const resolve_1 = require("./resolve");
|
|
44
44
|
const signals_1 = require("../workflows/signals");
|
|
45
|
-
const
|
|
45
|
+
const descriptor_1 = require("./descriptor");
|
|
46
46
|
const agent_types_1 = require("../ensemble/agent-types");
|
|
47
47
|
const validation_1 = require("../utils/validation");
|
|
48
48
|
const sdk_probe_1 = require("../utils/sdk-probe");
|
|
49
49
|
const pre_flight_1 = require("../adapters/claude-code-headless/pre-flight");
|
|
50
50
|
const types_2 = require("../adapters/claude-code-headless/types");
|
|
51
|
+
const probe_1 = require("../pi/probe");
|
|
51
52
|
const toolLog = (...args) => console.error('[agent-tempo:recruit]', ...args);
|
|
53
|
+
/**
|
|
54
|
+
* True dynamic ESM import (survives tsc's commonjs downlevel) — used to load
|
|
55
|
+
* pi-ai's sessionless `getModel` for the Copilot model-index pre-flight gate.
|
|
56
|
+
*/
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
58
|
+
const esmImport = new Function('s', 'return import(s)');
|
|
52
59
|
/**
|
|
53
60
|
* #449 Phase C — check whether the `opencode` binary is on PATH. Used by
|
|
54
61
|
* the recruit pre-flight to fail fast with an actionable error before the
|
|
@@ -71,7 +78,7 @@ function hasOpencodeOnPath() {
|
|
|
71
78
|
return false;
|
|
72
79
|
}
|
|
73
80
|
}
|
|
74
|
-
function
|
|
81
|
+
function buildRecruitTool(client, config, getPlayerId, handle, ownAgentType = 'claude', deps = {}) {
|
|
75
82
|
// Lazy default — only imports utils/hosts when actually called, so the
|
|
76
83
|
// MCP server's module load graph doesn't drag the whole join layer
|
|
77
84
|
// into every consumer at import time.
|
|
@@ -83,282 +90,359 @@ function registerRecruitTool(server, client, config, getPlayerId, handle, ownAge
|
|
|
83
90
|
taskQueue: config.taskQueue,
|
|
84
91
|
});
|
|
85
92
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.describe('
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.
|
|
99
|
-
|
|
100
|
-
.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
// #131 / #449 Phase C — model knob is meaningful for claude-api AND
|
|
153
|
-
// opencode (different shapes — bare vs `provider/model` — both flow
|
|
154
|
-
// through the same recruit field). Reject silently-ignored params for
|
|
155
|
-
// the other adapters so users learn the right shape.
|
|
156
|
-
// Local-spawn pre-flight checks (env vars + SDK install + binaries)
|
|
157
|
-
// run only when `host` is unset; cross-host recruits delegate to the
|
|
158
|
-
// target daemon's `availableAgentTypes` advertisement (the existing
|
|
159
|
-
// `checkHostPreflight` path), which already gates on whether the
|
|
160
|
-
// remote daemon resolved the SDK at boot.
|
|
161
|
-
if (model != null && agent !== 'claude-api' && agent !== 'opencode') {
|
|
162
|
-
return (0, helpers_1.fail)(`model is only valid when agent: "claude-api" or agent: "opencode" (got agent: "${agent}").`);
|
|
163
|
-
}
|
|
164
|
-
// #520 — claude-code-headless permission knobs are mutually exclusive
|
|
165
|
-
// and only meaningful for that adapter. Reject silently-ignored
|
|
166
|
-
// params so users learn the right shape.
|
|
167
|
-
if (permissionMode != null && agent !== 'claude-code-headless') {
|
|
168
|
-
return (0, helpers_1.fail)(`permissionMode is only valid when agent: "claude-code-headless" (got agent: "${agent}").`);
|
|
169
|
-
}
|
|
170
|
-
if (dangerouslySkipPermissions && agent !== 'claude-code-headless') {
|
|
171
|
-
return (0, helpers_1.fail)(`dangerouslySkipPermissions is only valid when agent: "claude-code-headless" (got agent: "${agent}").`);
|
|
172
|
-
}
|
|
173
|
-
if (permissionMode != null && dangerouslySkipPermissions) {
|
|
174
|
-
return (0, helpers_1.fail)(`permissionMode and dangerouslySkipPermissions are mutually exclusive — pass at most one.`);
|
|
175
|
-
}
|
|
176
|
-
if (agent === 'claude-api' && !host && !force) {
|
|
177
|
-
if (!process.env.ANTHROPIC_API_KEY) {
|
|
178
|
-
return (0, helpers_1.fail)(`agent: "claude-api" requires the ANTHROPIC_API_KEY environment variable on the spawn host. Set it before recruiting (export ANTHROPIC_API_KEY=sk-...) or use \`force: true\` to bypass this check.`);
|
|
93
|
+
return {
|
|
94
|
+
name: 'recruit',
|
|
95
|
+
description: `Start a new named session in a directory. Rejects if the name is already active. Supports Claude Code or Copilot CLI agents. Defaults to "${ownAgentType}" (same as this session).`,
|
|
96
|
+
params: {
|
|
97
|
+
workDir: zod_1.z.string().max(validation_1.PATH_MAX).describe('The working directory for the new session'),
|
|
98
|
+
name: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).describe('Name for the new session'),
|
|
99
|
+
conductor: zod_1.z.boolean().optional()
|
|
100
|
+
.describe('Whether this session is a conductor (default: false)'),
|
|
101
|
+
initialMessage: zod_1.z.string().max(validation_1.MESSAGE_MAX).optional()
|
|
102
|
+
.describe('Optional task or message for the new session (sent after it sets its name)'),
|
|
103
|
+
agent: zod_1.z.enum(types_1.AGENT_TYPES).optional()
|
|
104
|
+
.describe(`Which agent to use (default: "${ownAgentType}", same as this session). "mock" requires dev mode (--dev). "claude-api" runs headless via the Anthropic Messages API — requires ANTHROPIC_API_KEY env var and the @anthropic-ai/sdk optional dependency installed; has access to agent-tempo MCP tools (cue, report, recall, ensemble, …) but NOT file-edit or shell tools (use "claude" for those). "opencode" runs headless via a local opencode serve subprocess; multi-provider (Anthropic, OpenAI, Bedrock, Ollama, …) — requires the @opencode-ai/sdk optional dep and an opencode binary on PATH. opencode players ARE file-op-capable (file edits / shell / web search via OpenCode's built-in tools). "claude-code-headless" runs the official Claude Code CLI as a headless per-turn \`claude -p\` subprocess — requires the \`claude\` binary on PATH AND a logged-in Claude Code session (\`claude auth login\`); turns bill against the host's existing subscription extra-usage credits, NOT a Console API key. claude-code-headless players have full Claude Code tool access (Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch).`),
|
|
105
|
+
model: zod_1.z.string().regex(/^[a-z0-9][a-z0-9-/.:_]*$/).optional()
|
|
106
|
+
.describe('Model id. For "claude-api": bare Anthropic id (e.g. "claude-opus-4-7"). For "opencode": combined "provider/model" (e.g. "anthropic/claude-opus-4-7", "openai/gpt-4o", "ollama/llama3"). Falls back to AGENT_TEMPO_API_MODEL (claude-api) or AGENT_TEMPO_OPENCODE_MODEL (opencode), then a constants-pinned default. Ignored for claude / copilot / mock adapters.'),
|
|
107
|
+
type: zod_1.z.string().optional()
|
|
108
|
+
.describe('Agent type name — references a Claude Code agent definition (e.g., "tempo-soloist")'),
|
|
109
|
+
systemPrompt: zod_1.z.string().optional()
|
|
110
|
+
.describe('Path to a .md file to use as custom agent system prompt (--system-prompt)'),
|
|
111
|
+
host: zod_1.z.string().optional()
|
|
112
|
+
.describe('Target hostname for cross-machine recruiting. Omit for local spawn.'),
|
|
113
|
+
force: zod_1.z.boolean().optional()
|
|
114
|
+
.describe('Force-terminate any existing session with this name before recruiting. Use when a previous session is orphaned or stuck.'),
|
|
115
|
+
mockMode: zod_1.z.enum(types_1.MOCK_MODES).optional()
|
|
116
|
+
.describe('Dev-mode only (agent: "mock"). Mock adapter mode. Default: "echo". "silent" never replies (heartbeat-stale validation); "chaos" probabilistic fail/crash injection (env-tuned).'),
|
|
117
|
+
mockScenario: zod_1.z.string().optional()
|
|
118
|
+
.describe('Dev-mode only (agent: "mock", mockMode: "scripted"). Bare scenario name (resolved against shipped scenarios/) or absolute path to a scenario YAML.'),
|
|
119
|
+
// #520 — claude-code-headless adapter knobs. Both ignored for other adapters.
|
|
120
|
+
permissionMode: zod_1.z.enum(types_2.CLAUDE_CODE_PERMISSION_MODES).optional()
|
|
121
|
+
.describe('claude-code-headless only. Permission mode forwarded to `claude -p --permission-mode`. Default "acceptEdits" auto-approves writes + common fs commands. "bypassPermissions" / "dangerouslySkipPermissions" trades safety for speed in trusted contexts. "plan" plans without executing — not useful for headless players. Mutually exclusive with `dangerouslySkipPermissions`.'),
|
|
122
|
+
dangerouslySkipPermissions: zod_1.z.boolean().optional()
|
|
123
|
+
.describe('claude-code-headless only. When true, passes `--dangerously-skip-permissions` to `claude -p` instead of `--permission-mode`. Use only in sandboxed/trusted contexts. Mutually exclusive with `permissionMode`.'),
|
|
124
|
+
// Phase 3a / MD-C — headless Pi tool-access policy. Ignored for other agents.
|
|
125
|
+
// NOTE: stays `.optional()` (NOT `.default('restricted')`): this tool's
|
|
126
|
+
// params are rendered to the Pi front-end via renderToPi → the zod→TypeBox
|
|
127
|
+
// converter, which is fail-loud on `.default()` (D1). A schema default would
|
|
128
|
+
// throw at Pi tool registration ("Pi missing tool: recruit"). The concrete
|
|
129
|
+
// 'restricted' default is applied once at the read site below instead.
|
|
130
|
+
toolAccess: zod_1.z.enum(['restricted', 'standard', 'full']).optional()
|
|
131
|
+
.describe('pi only. Headless Pi tool-class policy. "restricted" (default): agent-tempo tools + Read/Edit/Write; Bash/shell/exec HARD-BLOCKED. "standard": Bash enabled; no tool-level scope restriction (operator/container responsible for scoping). "full": unsandboxed — requires force: true (admin confirmation). Ignored for other agents.'),
|
|
132
|
+
},
|
|
133
|
+
handler: async (args) => {
|
|
134
|
+
const { workDir, name, initialMessage } = args;
|
|
135
|
+
const isConductor = args.conductor === true;
|
|
136
|
+
const agent = args.agent || ownAgentType;
|
|
137
|
+
const model = args.model;
|
|
138
|
+
const agentTypeName = args.type;
|
|
139
|
+
const systemPrompt = args.systemPrompt;
|
|
140
|
+
const host = args.host;
|
|
141
|
+
const force = args.force === true;
|
|
142
|
+
const mockMode = args.mockMode;
|
|
143
|
+
const mockScenario = args.mockScenario;
|
|
144
|
+
const permissionMode = args.permissionMode;
|
|
145
|
+
const dangerouslySkipPermissions = args.dangerouslySkipPermissions === true;
|
|
146
|
+
// N1 (adapted): normalize to a concrete value ONCE here so the guard below
|
|
147
|
+
// and the outbox entry both see a real value with no scattered `?? 'restricted'`.
|
|
148
|
+
// (A schema `.default()` would be cleaner but the Pi converter rejects it —
|
|
149
|
+
// see the toolAccess param note above.) Omitted → 'restricted'.
|
|
150
|
+
const toolAccess = (args.toolAccess ?? 'restricted');
|
|
151
|
+
// ADR 0014 §7 gate 3 — recruit-time rejection of `agent: 'mock'`
|
|
152
|
+
// outside dev mode. Defense-in-depth: even if a hand-edited install
|
|
153
|
+
// had `dist/adapters/mock/` present (gate 1 bypassed) AND somehow
|
|
154
|
+
// got the registry to register the descriptor (gate 2 bypassed),
|
|
155
|
+
// this rejects the request with a clear, actionable error.
|
|
156
|
+
if (agent === 'mock' && !(0, config_1.isDevMode)()) {
|
|
157
|
+
return (0, descriptor_1.fail)(`agent: "mock" is only available in dev mode. Restart agent-tempo with --dev (or set AGENT_TEMPO_DEV_MODE=1) to enable.`);
|
|
179
158
|
}
|
|
180
|
-
|
|
181
|
-
|
|
159
|
+
// mockMode / mockScenario are only meaningful with the mock adapter —
|
|
160
|
+
// reject silently-ignored params so users learn the right flag shape.
|
|
161
|
+
if (mockMode != null && agent !== 'mock') {
|
|
162
|
+
return (0, descriptor_1.fail)(`mockMode is only valid when agent: "mock" (got agent: "${agent}").`);
|
|
182
163
|
}
|
|
183
|
-
|
|
184
|
-
return (0,
|
|
164
|
+
if (mockScenario != null && agent !== 'mock') {
|
|
165
|
+
return (0, descriptor_1.fail)(`mockScenario is only valid when agent: "mock" (got agent: "${agent}").`);
|
|
185
166
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// (signal that opencode integration is intended on this host) AND
|
|
189
|
-
// the `opencode` binary on PATH (the adapter spawns `opencode serve`
|
|
190
|
-
// as a subprocess). Cross-host recruits skip both — the target
|
|
191
|
-
// daemon's `availableAgentTypes` is the gate there.
|
|
192
|
-
if (agent === 'opencode' && !host && !force) {
|
|
193
|
-
if (!(0, sdk_probe_1.probeSdkInstall)('@opencode-ai/sdk')) {
|
|
194
|
-
return (0, helpers_1.fail)(`agent: "opencode" requires the @opencode-ai/sdk optional dependency. Install with \`npm install @opencode-ai/sdk\` and retry, or use \`force: true\` to bypass this check.`);
|
|
167
|
+
if (agent === 'mock' && mockMode === 'scripted' && !mockScenario) {
|
|
168
|
+
return (0, descriptor_1.fail)(`mockMode: "scripted" requires mockScenario (a bare scenario name or path to a YAML file).`);
|
|
195
169
|
}
|
|
196
|
-
|
|
197
|
-
|
|
170
|
+
// PR-3: silent + chaos modes don't consult the scenario file. Reject
|
|
171
|
+
// explicitly rather than silently ignore so users learn the right
|
|
172
|
+
// shape and don't sit wondering why their scenario wasn't applied.
|
|
173
|
+
if (mockScenario && (mockMode === 'silent' || mockMode === 'chaos')) {
|
|
174
|
+
return (0, descriptor_1.fail)(`mockMode: "${mockMode}" does not use a scenario. Drop mockScenario or switch to mockMode: "scripted".`);
|
|
198
175
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
return (0,
|
|
176
|
+
// #131 / #449 Phase C — model knob is meaningful for claude-api AND
|
|
177
|
+
// opencode (different shapes — bare vs `provider/model` — both flow
|
|
178
|
+
// through the same recruit field). Reject silently-ignored params for
|
|
179
|
+
// the other adapters so users learn the right shape.
|
|
180
|
+
// Local-spawn pre-flight checks (env vars + SDK install + binaries)
|
|
181
|
+
// run only when `host` is unset; cross-host recruits delegate to the
|
|
182
|
+
// target daemon's `availableAgentTypes` advertisement (the existing
|
|
183
|
+
// `checkHostPreflight` path), which already gates on whether the
|
|
184
|
+
// remote daemon resolved the SDK at boot.
|
|
185
|
+
if (model != null && agent !== 'claude-api' && agent !== 'opencode') {
|
|
186
|
+
return (0, descriptor_1.fail)(`model is only valid when agent: "claude-api" or agent: "opencode" (got agent: "${agent}").`);
|
|
210
187
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
188
|
+
// #520 — claude-code-headless permission knobs are mutually exclusive
|
|
189
|
+
// and only meaningful for that adapter. Reject silently-ignored
|
|
190
|
+
// params so users learn the right shape.
|
|
191
|
+
if (permissionMode != null && agent !== 'claude-code-headless') {
|
|
192
|
+
return (0, descriptor_1.fail)(`permissionMode is only valid when agent: "claude-code-headless" (got agent: "${agent}").`);
|
|
214
193
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// pattern; copilot has no subprocess CLI on PATH). The bridge
|
|
218
|
-
// subprocess hard-requires `@github/copilot-sdk` at module load
|
|
219
|
-
// (`src/adapters/copilot/adapter.ts:71`), so without this gate the
|
|
220
|
-
// user only learns of the missing dep AFTER bridge spawn —
|
|
221
|
-
// adapter crashes with `process.exit(1)` and the player sits in
|
|
222
|
-
// `booting` until lease timeout. We use `probeSdkInstall` (FS walk)
|
|
223
|
-
// rather than `require.resolve` because pnpm layouts without a
|
|
224
|
-
// top-level hoisted link otherwise false-negative; see issue #532
|
|
225
|
-
// investigation footnote. GITHUB_TOKEN / Copilot CLI login are
|
|
226
|
-
// intentionally NOT checked: the SDK falls through to the
|
|
227
|
-
// logged-in user (`adapter.ts:31, :263`), so token presence is not
|
|
228
|
-
// a hard requirement. Cross-host recruits skip — the target
|
|
229
|
-
// daemon's `availableAgentTypes` is the gate there.
|
|
230
|
-
if (agent === 'copilot' && !host && !force) {
|
|
231
|
-
if (!(0, sdk_probe_1.probeSdkInstall)('@github/copilot-sdk')) {
|
|
232
|
-
return (0, helpers_1.fail)(`agent: "copilot" requires the @github/copilot-sdk optional dependency. Install with \`npm install @github/copilot-sdk\` and retry, or use \`force: true\` to bypass this check.`);
|
|
194
|
+
if (dangerouslySkipPermissions && agent !== 'claude-code-headless') {
|
|
195
|
+
return (0, descriptor_1.fail)(`dangerouslySkipPermissions is only valid when agent: "claude-code-headless" (got agent: "${agent}").`);
|
|
233
196
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
let allowedTools;
|
|
241
|
-
if (agentTypeName) {
|
|
242
|
-
const info = (0, agent_types_1.resolveAgentType)(agentTypeName);
|
|
243
|
-
if (!info) {
|
|
244
|
-
const available = (0, agent_types_1.listAgentTypes)().map(t => t.name);
|
|
245
|
-
return (0, helpers_1.fail)(`Unknown agent type "${agentTypeName}". Available types: ${available.length ? available.join(', ') : '(none)'}`);
|
|
197
|
+
// Reject an EXPLICIT non-default toolAccess on a non-pi agent. With Zod
|
|
198
|
+
// `.default('restricted')` the omitted case is indistinguishable from an
|
|
199
|
+
// explicit `'restricted'`, so we only flag a non-default value — that is
|
|
200
|
+
// unambiguously a user mistake (toolAccess is ignored for non-pi agents).
|
|
201
|
+
if (toolAccess !== 'restricted' && agent !== 'pi') {
|
|
202
|
+
return (0, descriptor_1.fail)(`toolAccess is only valid when agent: "pi" (got agent: "${agent}").`);
|
|
246
203
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
agentDefinitionDescription = info.description;
|
|
250
|
-
nativeResolvable = info.nativeResolvable;
|
|
251
|
-
allowedTools = info.allowedTools;
|
|
252
|
-
}
|
|
253
|
-
// Validate name
|
|
254
|
-
const nameError = (0, validation_1.validatePlayerName)(name);
|
|
255
|
-
if (nameError) {
|
|
256
|
-
return (0, helpers_1.fail)(nameError);
|
|
257
|
-
}
|
|
258
|
-
if (name === 'conductor' && !isConductor) {
|
|
259
|
-
return (0, helpers_1.fail)(`The name "conductor" is reserved for conductor sessions. Use a different name, or set conductor: true.`);
|
|
260
|
-
}
|
|
261
|
-
// ── #274 AC12 — cross-host recruit pre-flight ──
|
|
262
|
-
//
|
|
263
|
-
// When `host` is specified, verify that the target is live + has
|
|
264
|
-
// the requested agent runtime available BEFORE submitting to the
|
|
265
|
-
// outbox. Otherwise `recruit --host foo` could queue forever on a
|
|
266
|
-
// per-host task queue nobody's listening on (the exact failure
|
|
267
|
-
// mode #274 set out to close).
|
|
268
|
-
//
|
|
269
|
-
// `force: true` bypasses pre-flight entirely (AC12d) — covers the
|
|
270
|
-
// scripted-recruit-on-about-to-boot-daemon case plus any other
|
|
271
|
-
// "I know what I'm doing" override. RPC failure during pre-flight
|
|
272
|
-
// falls through with a warning (AC12e) so today's recruit
|
|
273
|
-
// availability characteristics are preserved.
|
|
274
|
-
if (host && !force) {
|
|
275
|
-
try {
|
|
276
|
-
const hosts = await listHostsFn(client);
|
|
277
|
-
const preflightError = checkHostPreflight(hosts, host, agent);
|
|
278
|
-
if (preflightError)
|
|
279
|
-
return (0, helpers_1.fail)(preflightError);
|
|
204
|
+
if (toolAccess === 'full' && !force) {
|
|
205
|
+
return (0, descriptor_1.fail)(`toolAccess: "full" (unsandboxed Pi) requires force: true (admin confirmation). "restricted" (default) and "standard" do not.`);
|
|
280
206
|
}
|
|
281
|
-
|
|
282
|
-
|
|
207
|
+
if (permissionMode != null && dangerouslySkipPermissions) {
|
|
208
|
+
return (0, descriptor_1.fail)(`permissionMode and dangerouslySkipPermissions are mutually exclusive — pass at most one.`);
|
|
283
209
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
210
|
+
if (agent === 'claude-api' && !host && !force) {
|
|
211
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
212
|
+
return (0, descriptor_1.fail)(`agent: "claude-api" requires the ANTHROPIC_API_KEY environment variable on the spawn host. Set it before recruiting (export ANTHROPIC_API_KEY=sk-...) or use \`force: true\` to bypass this check.`);
|
|
213
|
+
}
|
|
288
214
|
try {
|
|
289
|
-
|
|
290
|
-
const conductorHandle = client.workflow.getHandle(conductorWfId);
|
|
291
|
-
const desc = await conductorHandle.describe();
|
|
292
|
-
if (desc.status.name === 'RUNNING') {
|
|
293
|
-
if (force) {
|
|
294
|
-
await conductorHandle.terminate(`Force-terminated for re-recruit by ${getPlayerId()}`);
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
return (0, helpers_1.fail)(`A conductor is already running in ensemble "${config.ensemble}". Use \`agent-tempo conduct --replace\` from the CLI to replace it, \`stop\` it first, or use \`force: true\` to replace it.`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
215
|
+
require.resolve('@anthropic-ai/sdk');
|
|
300
216
|
}
|
|
301
217
|
catch {
|
|
302
|
-
|
|
218
|
+
return (0, descriptor_1.fail)(`agent: "claude-api" requires the @anthropic-ai/sdk optional dependency. Install with \`npm install @anthropic-ai/sdk\` and retry, or use \`force: true\` to bypass this check.`);
|
|
303
219
|
}
|
|
304
220
|
}
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
221
|
+
// #449 Phase C — opencode pre-flight. Two checks: the optional SDK
|
|
222
|
+
// (signal that opencode integration is intended on this host) AND
|
|
223
|
+
// the `opencode` binary on PATH (the adapter spawns `opencode serve`
|
|
224
|
+
// as a subprocess). Cross-host recruits skip both — the target
|
|
225
|
+
// daemon's `availableAgentTypes` is the gate there.
|
|
226
|
+
if (agent === 'opencode' && !host && !force) {
|
|
227
|
+
if (!(0, sdk_probe_1.probeSdkInstall)('@opencode-ai/sdk')) {
|
|
228
|
+
return (0, descriptor_1.fail)(`agent: "opencode" requires the @opencode-ai/sdk optional dependency. Install with \`npm install @opencode-ai/sdk\` and retry, or use \`force: true\` to bypass this check.`);
|
|
229
|
+
}
|
|
230
|
+
if (!hasOpencodeOnPath()) {
|
|
231
|
+
return (0, descriptor_1.fail)(`agent: "opencode" requires the \`opencode\` binary on PATH. Install with \`npm install -g opencode-ai\` and retry, or use \`force: true\` to bypass this check.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// #520 — claude-code-headless pre-flight. Two checks, both bounded by
|
|
235
|
+
// short timeouts (3s + 5s). The auth probe uses the official `claude
|
|
236
|
+
// auth status` subcommand — no billed API call. Cross-host recruits
|
|
237
|
+
// skip both — the target daemon's `availableAgentTypes` is the gate
|
|
238
|
+
// there (PR-2 wires the daemon-side probe).
|
|
239
|
+
if (agent === 'claude-code-headless' && !host && !force) {
|
|
240
|
+
const claudeBin = config.claudeBin ?? 'claude';
|
|
241
|
+
const binProbe = (0, pre_flight_1.probeClaudeBinary)(claudeBin);
|
|
242
|
+
if (!binProbe.ok) {
|
|
243
|
+
return (0, descriptor_1.fail)(`agent: "claude-code-headless" pre-flight failed: ${binProbe.error} Use \`force: true\` to bypass.`);
|
|
244
|
+
}
|
|
245
|
+
const authProbe = (0, pre_flight_1.probeClaudeAuth)(claudeBin);
|
|
246
|
+
if (!authProbe.loggedIn) {
|
|
247
|
+
return (0, descriptor_1.fail)(`agent: "claude-code-headless" pre-flight failed: ${authProbe.error} Use \`force: true\` to bypass.`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// #532 — copilot pre-flight. SDK-only probe (mirrors claude-api's
|
|
251
|
+
// pattern; copilot has no subprocess CLI on PATH). The bridge
|
|
252
|
+
// subprocess hard-requires `@github/copilot-sdk` at module load
|
|
253
|
+
// (`src/adapters/copilot/adapter.ts:71`), so without this gate the
|
|
254
|
+
// user only learns of the missing dep AFTER bridge spawn —
|
|
255
|
+
// adapter crashes with `process.exit(1)` and the player sits in
|
|
256
|
+
// `booting` until lease timeout. We use `probeSdkInstall` (FS walk)
|
|
257
|
+
// rather than `require.resolve` because pnpm layouts without a
|
|
258
|
+
// top-level hoisted link otherwise false-negative; see issue #532
|
|
259
|
+
// investigation footnote. GITHUB_TOKEN / Copilot CLI login are
|
|
260
|
+
// intentionally NOT checked: the SDK falls through to the
|
|
261
|
+
// logged-in user (`adapter.ts:31, :263`), so token presence is not
|
|
262
|
+
// a hard requirement. Cross-host recruits skip — the target
|
|
263
|
+
// daemon's `availableAgentTypes` is the gate there.
|
|
264
|
+
if (agent === 'copilot' && !host && !force) {
|
|
265
|
+
if (!(0, sdk_probe_1.probeSdkInstall)('@github/copilot-sdk')) {
|
|
266
|
+
return (0, descriptor_1.fail)(`agent: "copilot" requires the @github/copilot-sdk optional dependency. Install with \`npm install @github/copilot-sdk\` and retry, or use \`force: true\` to bypass this check.`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Node-floor (Decision B, #645) — AUTHORITATIVE, NON-BYPASSABLE gate, so it
|
|
270
|
+
// sits ABOVE the `&& !force` block below. Unlike sdk-probe (a filesystem
|
|
271
|
+
// heuristic with possible false-negatives, where `force` stays legitimate),
|
|
272
|
+
// the Node floor has NO false-negative — `process.versions.node` is
|
|
273
|
+
// authoritative and Pi literally cannot import on sub-22.19 — so there is no
|
|
274
|
+
// legitimate override. Checked even under `force: true`, matching the
|
|
275
|
+
// unconditional runHeadlessPi backstop, so recruit fails clean BEFORE spawn
|
|
276
|
+
// in ALL local cases. Local recruit only — cross-host (`host` set) defers to
|
|
277
|
+
// the target daemon's availableAgentTypes. Gates BOTH the Copilot and
|
|
278
|
+
// Anthropic/Pi-default paths below.
|
|
279
|
+
if (agent === 'pi' && !host) {
|
|
280
|
+
const nodeFloor = (0, probe_1.checkPiNodeFloor)();
|
|
281
|
+
if (!nodeFloor.ok)
|
|
282
|
+
return (0, descriptor_1.fail)(`agent: "pi" — ${nodeFloor.reason}`);
|
|
283
|
+
}
|
|
284
|
+
// Phase 3a — headless Pi pre-flight. The Pi SDK is an optional Node-22.19+
|
|
285
|
+
// dep required at the headless entry; gate at recruit (cross-host skips —
|
|
286
|
+
// the target daemon's availableAgentTypes is the gate there).
|
|
287
|
+
if (agent === 'pi' && !host && !force) {
|
|
288
|
+
// Validate the model selector format up front (provider/model).
|
|
289
|
+
let parsedModel;
|
|
290
|
+
if (model) {
|
|
291
|
+
const r = (0, config_1.parsePiProviderModel)(model);
|
|
292
|
+
if ('error' in r)
|
|
293
|
+
return (0, descriptor_1.fail)(`agent: "pi" — ${r.error} Or use \`force: true\` to bypass.`);
|
|
294
|
+
parsedModel = r;
|
|
295
|
+
}
|
|
296
|
+
if (parsedModel?.provider === 'github-copilot') {
|
|
297
|
+
// Copilot-via-Pi: full pre-flight (deps + version floor + Copilot auth +
|
|
298
|
+
// model-index Gate 4 via pi-ai's sessionless getModel, injected here).
|
|
299
|
+
let resolveModel;
|
|
300
|
+
try {
|
|
301
|
+
const piAi = await esmImport(probe_1.PI_AI_PACKAGE);
|
|
302
|
+
resolveModel = piAi.getModel;
|
|
303
|
+
}
|
|
304
|
+
catch { /* dep-present gate inside the preflight fails first if pi-ai is unimportable */ }
|
|
305
|
+
const pf = (0, probe_1.probeCopilotPiPreflight)({ requestedModel: parsedModel, resolveModel });
|
|
306
|
+
if (!pf.available)
|
|
307
|
+
return (0, descriptor_1.fail)(`agent: "pi" (Copilot) pre-flight failed: ${pf.reason}`);
|
|
308
|
+
}
|
|
309
|
+
else if (!(0, sdk_probe_1.probeSdkInstall)(probe_1.PI_PACKAGE)) {
|
|
310
|
+
// Anthropic / Pi-default path: just require the Pi SDK installed.
|
|
311
|
+
return (0, descriptor_1.fail)(`agent: "pi" requires the ${probe_1.PI_PACKAGE} optional dependency (Node >= 22.19). Install with \`npm install -g ${probe_1.PI_PACKAGE}\` and retry, or use \`force: true\` to bypass.`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Resolve agent type if provided
|
|
315
|
+
let agentDefinition;
|
|
316
|
+
let agentDefinitionPath;
|
|
317
|
+
let agentDefinitionDescription;
|
|
318
|
+
let nativeResolvable;
|
|
319
|
+
let allowedTools;
|
|
320
|
+
if (agentTypeName) {
|
|
321
|
+
const info = (0, agent_types_1.resolveAgentType)(agentTypeName);
|
|
322
|
+
if (!info) {
|
|
323
|
+
const available = (0, agent_types_1.listAgentTypes)().map(t => t.name);
|
|
324
|
+
return (0, descriptor_1.fail)(`Unknown agent type "${agentTypeName}". Available types: ${available.length ? available.join(', ') : '(none)'}`);
|
|
325
|
+
}
|
|
326
|
+
agentDefinition = info.name;
|
|
327
|
+
agentDefinitionPath = info.path;
|
|
328
|
+
agentDefinitionDescription = info.description;
|
|
329
|
+
nativeResolvable = info.nativeResolvable;
|
|
330
|
+
allowedTools = info.allowedTools;
|
|
331
|
+
}
|
|
332
|
+
// Validate name
|
|
333
|
+
const nameError = (0, validation_1.validatePlayerName)(name);
|
|
334
|
+
if (nameError) {
|
|
335
|
+
return (0, descriptor_1.fail)(nameError);
|
|
336
|
+
}
|
|
337
|
+
if (name === 'conductor' && !isConductor) {
|
|
338
|
+
return (0, descriptor_1.fail)(`The name "conductor" is reserved for conductor sessions. Use a different name, or set conductor: true.`);
|
|
339
|
+
}
|
|
340
|
+
// ── #274 AC12 — cross-host recruit pre-flight ──
|
|
341
|
+
//
|
|
342
|
+
// When `host` is specified, verify that the target is live + has
|
|
343
|
+
// the requested agent runtime available BEFORE submitting to the
|
|
344
|
+
// outbox. Otherwise `recruit --host foo` could queue forever on a
|
|
345
|
+
// per-host task queue nobody's listening on (the exact failure
|
|
346
|
+
// mode #274 set out to close).
|
|
347
|
+
//
|
|
348
|
+
// `force: true` bypasses pre-flight entirely (AC12d) — covers the
|
|
349
|
+
// scripted-recruit-on-about-to-boot-daemon case plus any other
|
|
350
|
+
// "I know what I'm doing" override. RPC failure during pre-flight
|
|
351
|
+
// falls through with a warning (AC12e) so today's recruit
|
|
352
|
+
// availability characteristics are preserved.
|
|
353
|
+
if (host && !force) {
|
|
354
|
+
try {
|
|
355
|
+
const hosts = await listHostsFn(client);
|
|
356
|
+
const preflightError = checkHostPreflight(hosts, host, agent);
|
|
357
|
+
if (preflightError)
|
|
358
|
+
return (0, descriptor_1.fail)(preflightError);
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
toolLog(`Host pre-flight RPC failed for "${host}"; proceeding without validation (use --force to silence this warning): ${err instanceof Error ? err.message : err}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
// Check if a conductor already exists when recruiting a conductor
|
|
366
|
+
if (isConductor) {
|
|
312
367
|
try {
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
await
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
368
|
+
const conductorWfId = (0, config_1.conductorWorkflowId)(config.ensemble);
|
|
369
|
+
const conductorHandle = client.workflow.getHandle(conductorWfId);
|
|
370
|
+
const desc = await conductorHandle.describe();
|
|
371
|
+
if (desc.status.name === 'RUNNING') {
|
|
372
|
+
if (force) {
|
|
373
|
+
await conductorHandle.terminate(`Force-terminated for re-recruit by ${getPlayerId()}`);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
return (0, descriptor_1.fail)(`A conductor is already running in ensemble "${config.ensemble}". Use \`agent-tempo conduct --replace\` from the CLI to replace it, \`stop\` it first, or use \`force: true\` to replace it.`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
320
379
|
}
|
|
321
380
|
catch {
|
|
322
|
-
//
|
|
381
|
+
// No existing conductor — proceed
|
|
323
382
|
}
|
|
324
383
|
}
|
|
325
|
-
|
|
326
|
-
|
|
384
|
+
// Check if a session with this name is already active
|
|
385
|
+
const existing = await (0, resolve_1.resolveSession)(client, config.ensemble, name);
|
|
386
|
+
if (existing) {
|
|
387
|
+
if (force) {
|
|
388
|
+
// Force-terminate the existing session before recruiting
|
|
389
|
+
await existing.terminate(`Force-terminated for re-recruit by ${getPlayerId()}`);
|
|
390
|
+
// Best-effort notify conductor
|
|
391
|
+
try {
|
|
392
|
+
const condId = (0, config_1.conductorWorkflowId)(config.ensemble);
|
|
393
|
+
const condHandle = client.workflow.getHandle(condId);
|
|
394
|
+
await condHandle.signal('receiveMessage', {
|
|
395
|
+
from: 'system',
|
|
396
|
+
text: `Session "${name}" was force-terminated for re-recruit by ${getPlayerId()}.`,
|
|
397
|
+
responseRequested: false,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// Conductor may not exist — that's fine
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
return (0, descriptor_1.fail)(`Session **${name}** is already active. Use \`cue\` to send it a message, \`stop\` it first, or use \`force: true\` to replace it.`);
|
|
406
|
+
}
|
|
327
407
|
}
|
|
408
|
+
const entry = {
|
|
409
|
+
type: 'recruit',
|
|
410
|
+
targetName: name,
|
|
411
|
+
workDir,
|
|
412
|
+
isConductor,
|
|
413
|
+
initialMessage,
|
|
414
|
+
agent,
|
|
415
|
+
systemPrompt: agentDefinition ? undefined : systemPrompt,
|
|
416
|
+
targetHostname: host,
|
|
417
|
+
agentDefinition,
|
|
418
|
+
agentDefinitionPath,
|
|
419
|
+
agentDefinitionDescription,
|
|
420
|
+
nativeResolvable,
|
|
421
|
+
allowedTools,
|
|
422
|
+
claudeBin: config.claudeBin,
|
|
423
|
+
...(agent === 'mock' ? { mockMode: mockMode ?? 'echo' } : {}),
|
|
424
|
+
...(agent === 'mock' && mockScenario ? { mockScenario } : {}),
|
|
425
|
+
...(agent === 'claude-api' && model ? { model } : {}),
|
|
426
|
+
// #520 — claude-code-headless permission knobs flow through the
|
|
427
|
+
// outbox so PR-2's spawn helper picks them up via env. Fields
|
|
428
|
+
// are only present on the entry when actually set; the
|
|
429
|
+
// OutboxEntryInput shape will gain typed `permissionMode` /
|
|
430
|
+
// `dangerouslySkipPermissions` fields in PR-2.
|
|
431
|
+
...(agent === 'claude-code-headless' && permissionMode ? { permissionMode } : {}),
|
|
432
|
+
...(agent === 'claude-code-headless' && dangerouslySkipPermissions ? { dangerouslySkipPermissions: true } : {}),
|
|
433
|
+
// Phase 3a / MD-C — explicit toolAccess on the entry. Normalized at the
|
|
434
|
+
// read site (above) to a concrete value, so the spawned gate + audit
|
|
435
|
+
// always see one without a fallback here.
|
|
436
|
+
...(agent === 'pi' ? { toolAccess } : {}),
|
|
437
|
+
};
|
|
438
|
+
const entryId = await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
|
|
439
|
+
return (0, descriptor_1.ok)(`Recruit request submitted for **${name}** in ${workDir}. The session will be spawned shortly. (outbox: ${entryId})`);
|
|
328
440
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
initialMessage,
|
|
335
|
-
agent,
|
|
336
|
-
systemPrompt: agentDefinition ? undefined : systemPrompt,
|
|
337
|
-
targetHostname: host,
|
|
338
|
-
agentDefinition,
|
|
339
|
-
agentDefinitionPath,
|
|
340
|
-
agentDefinitionDescription,
|
|
341
|
-
nativeResolvable,
|
|
342
|
-
allowedTools,
|
|
343
|
-
claudeBin: config.claudeBin,
|
|
344
|
-
...(agent === 'mock' ? { mockMode: mockMode ?? 'echo' } : {}),
|
|
345
|
-
...(agent === 'mock' && mockScenario ? { mockScenario } : {}),
|
|
346
|
-
...(agent === 'claude-api' && model ? { model } : {}),
|
|
347
|
-
// #520 — claude-code-headless permission knobs flow through the
|
|
348
|
-
// outbox so PR-2's spawn helper picks them up via env. Fields
|
|
349
|
-
// are only present on the entry when actually set; the
|
|
350
|
-
// OutboxEntryInput shape will gain typed `permissionMode` /
|
|
351
|
-
// `dangerouslySkipPermissions` fields in PR-2.
|
|
352
|
-
...(agent === 'claude-code-headless' && permissionMode ? { permissionMode } : {}),
|
|
353
|
-
...(agent === 'claude-code-headless' && dangerouslySkipPermissions ? { dangerouslySkipPermissions: true } : {}),
|
|
354
|
-
};
|
|
355
|
-
const entryId = await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
|
|
356
|
-
return (0, helpers_1.ok)(`Recruit request submitted for **${name}** in ${workDir}. The session will be spawned shortly. (outbox: ${entryId})`);
|
|
357
|
-
}
|
|
358
|
-
catch (err) {
|
|
359
|
-
return (0, helpers_1.fail)(`Failed to recruit: ${(0, helpers_1.formatError)(err)}`);
|
|
360
|
-
}
|
|
361
|
-
});
|
|
441
|
+
catch (err) {
|
|
442
|
+
return (0, descriptor_1.fail)(`Failed to recruit: ${(0, descriptor_1.formatError)(err)}`);
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
};
|
|
362
446
|
}
|
|
363
447
|
// ────────────────────────────────────────────────────────────────────────
|
|
364
448
|
// #274 AC12 — host pre-flight validation (exported for unit tests)
|