agent-tempo 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -72,7 +72,10 @@ async function main() {
72
72
  await idleServer.connect(transport);
73
73
  return;
74
74
  }
75
- const config = (0, config_1.getConfig)();
75
+ // #676 FIX-1 — resolve config WITH sources so recruit can tell an operator-SET
76
+ // defaultAgent from the built-in 'claude' default. `.config` is the same Config
77
+ // getConfig() returns (behavior-preserving); `.sources.defaultAgent` is the origin.
78
+ const { config, sources } = (0, config_1.getConfigWithSources)();
76
79
  const isConductor = process.env[config_1.ENV.CONDUCTOR] === 'true';
77
80
  const requestedName = process.env[config_1.ENV.PLAYER_NAME] || '';
78
81
  // Conductors use their requested name or fall back to 'conductor'.
@@ -289,7 +292,7 @@ async function main() {
289
292
  // the same call. Adding a new tool? Add it once in `server-tools.ts`.
290
293
  (0, server_tools_1.registerAllTempoTools)(mcpServer, {
291
294
  client, config, getPlayerId, setPlayerId, handle, workflowId,
292
- ownAgentType, isConductor,
295
+ ownAgentType, defaultAgentSource: sources.defaultAgent, isConductor,
293
296
  });
294
297
  const MAESTRO_ACK = '\n\n[IMPORTANT: This message is from a human (Maestro). Immediately cue the sender back with a brief acknowledgment and your planned next step before doing the work.]';
295
298
  // Start message poller — push messages into Claude Code via channel notifications.
package/dist/spawn.d.ts CHANGED
@@ -150,6 +150,16 @@ export interface CopilotBridgeOpts {
150
150
  attachmentId?: string;
151
151
  attachmentRunId?: string;
152
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;
153
163
  }
154
164
  export interface CopilotBridgeResult {
155
165
  pid: number | undefined;
package/dist/spawn.js CHANGED
@@ -518,6 +518,10 @@ function buildPiConductorSpawn(opts) {
518
518
  [config_1.ENV.TASK_QUEUE]: opts.taskQueue,
519
519
  [config_1.ENV.ENSEMBLE]: opts.ensemble,
520
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',
521
525
  [config_1.ENV.PLAYER_NAME]: opts.sessionName,
522
526
  ...(opts.devMode ? { [config_1.ENV.DEV_MODE]: '1' } : {}),
523
527
  ...(opts.anthropicApiKey ? { ANTHROPIC_API_KEY: opts.anthropicApiKey } : {}),
@@ -565,6 +569,9 @@ function spawnCopilotBridge(opts) {
565
569
  [config_1.ENV.BRIDGE_MODE]: '', // Clear parent's bridge mode
566
570
  [config_1.ENV.TEMPORAL_ADDRESS]: opts.temporalAddress,
567
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' } : {}),
568
575
  // Forward Temporal connection settings so child processes can connect
569
576
  ...(opts.temporalNamespace ? { [config_1.ENV.TEMPORAL_NAMESPACE]: opts.temporalNamespace } : {}),
570
577
  ...(opts.temporalApiKey ? { [config_1.ENV.TEMPORAL_API_KEY]: opts.temporalApiKey } : {}),
@@ -1,5 +1,5 @@
1
1
  import { Client, WorkflowHandle } from '@temporalio/client';
2
- import { Config } from '../config';
2
+ import { Config, ConfigSource } from '../config';
3
3
  import { AgentType } from '../types';
4
4
  import type { HostInfo } from '../types';
5
5
  import { type TempoToolDescriptor } from './descriptor';
@@ -13,7 +13,24 @@ import { type TempoToolDescriptor } from './descriptor';
13
13
  export interface RegisterRecruitToolDeps {
14
14
  listHostsFn?: (client: Client) => Promise<HostInfo[]>;
15
15
  }
16
- export declare function buildRecruitTool(client: Client, config: Config, getPlayerId: () => string, handle: WorkflowHandle, ownAgentType?: AgentType, deps?: RegisterRecruitToolDeps): TempoToolDescriptor;
16
+ /**
17
+ * #676 FIX-1 — recruit agent precedence: explicit `argAgent` > operator-SET
18
+ * `configDefault` > `ownAgentType` (this player's mirror-fallback). The
19
+ * `defaultAgentSource` distinguishes an operator-set default (origin
20
+ * flag/env/config/temporal-cli) from the built-in 'claude' default (source
21
+ * 'default') / truly-unset ('none') — only an operator-set default wins over the
22
+ * mirror, so a copilot/pi conductor recruits its own kind by default. Pure +
23
+ * exported for unit testing.
24
+ */
25
+ export declare function resolveRecruitAgent(argAgent: AgentType | undefined, configDefault: AgentType, defaultAgentSource: ConfigSource | undefined, ownAgentType: AgentType): AgentType;
26
+ export declare function buildRecruitTool(client: Client, config: Config, getPlayerId: () => string, handle: WorkflowHandle, ownAgentType?: AgentType,
27
+ /**
28
+ * #676 FIX-1 — the SOURCE of `config.defaultAgent` (from getConfigWithSources),
29
+ * used to distinguish an operator-SET default from the built-in 'claude'
30
+ * default (source 'default'). Undefined → treated as not-operator-set →
31
+ * recruit falls back to `ownAgentType` (preserves the pre-FIX-1 mirror).
32
+ */
33
+ defaultAgentSource?: ConfigSource, deps?: RegisterRecruitToolDeps): TempoToolDescriptor;
17
34
  /**
18
35
  * Given a host liveness+profile snapshot, validate that `targetHost` is
19
36
  * (a) known, (b) recruit-ready, and (c) advertises support for
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.resolveRecruitAgent = resolveRecruitAgent;
36
37
  exports.buildRecruitTool = buildRecruitTool;
37
38
  exports.checkHostPreflight = checkHostPreflight;
38
39
  exports.nearestHostname = nearestHostname;
@@ -78,7 +79,30 @@ function hasOpencodeOnPath() {
78
79
  return false;
79
80
  }
80
81
  }
81
- function buildRecruitTool(client, config, getPlayerId, handle, ownAgentType = 'claude', deps = {}) {
82
+ /**
83
+ * #676 FIX-1 — recruit agent precedence: explicit `argAgent` > operator-SET
84
+ * `configDefault` > `ownAgentType` (this player's mirror-fallback). The
85
+ * `defaultAgentSource` distinguishes an operator-set default (origin
86
+ * flag/env/config/temporal-cli) from the built-in 'claude' default (source
87
+ * 'default') / truly-unset ('none') — only an operator-set default wins over the
88
+ * mirror, so a copilot/pi conductor recruits its own kind by default. Pure +
89
+ * exported for unit testing.
90
+ */
91
+ function resolveRecruitAgent(argAgent, configDefault, defaultAgentSource, ownAgentType) {
92
+ if (argAgent)
93
+ return argAgent;
94
+ const operatorSet = !!defaultAgentSource
95
+ && ['flag', 'env', 'config', 'temporal-cli'].includes(defaultAgentSource);
96
+ return operatorSet ? configDefault : ownAgentType;
97
+ }
98
+ function buildRecruitTool(client, config, getPlayerId, handle, ownAgentType = 'claude',
99
+ /**
100
+ * #676 FIX-1 — the SOURCE of `config.defaultAgent` (from getConfigWithSources),
101
+ * used to distinguish an operator-SET default from the built-in 'claude'
102
+ * default (source 'default'). Undefined → treated as not-operator-set →
103
+ * recruit falls back to `ownAgentType` (preserves the pre-FIX-1 mirror).
104
+ */
105
+ defaultAgentSource, deps = {}) {
82
106
  // Lazy default — only imports utils/hosts when actually called, so the
83
107
  // MCP server's module load graph doesn't drag the whole join layer
84
108
  // into every consumer at import time.
@@ -133,7 +157,7 @@ function buildRecruitTool(client, config, getPlayerId, handle, ownAgentType = 'c
133
157
  handler: async (args) => {
134
158
  const { workDir, name, initialMessage } = args;
135
159
  const isConductor = args.conductor === true;
136
- const agent = args.agent || ownAgentType;
160
+ const agent = resolveRecruitAgent(args.agent, config.defaultAgent, defaultAgentSource, ownAgentType);
137
161
  const model = args.model;
138
162
  const agentTypeName = args.type;
139
163
  const systemPrompt = args.systemPrompt;
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.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Many agents, one tempo. Durable coordination for multi-agent work via Temporal.",
5
5
  "keywords": [
6
6
  "mcp",