agent-tempo 1.7.0-beta.9 → 1.7.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/CLAUDE.md +17 -1
- package/README.md +2 -1
- package/dashboard/package.json +1 -1
- package/dist/activities/outbox.js +10 -0
- package/dist/activities/resolve.d.ts +83 -18
- package/dist/activities/resolve.js +139 -26
- package/dist/cli/command-center-command.js +25 -2
- package/dist/cli/commands.d.ts +28 -0
- package/dist/cli/commands.js +88 -17
- package/dist/cli/mcp.d.ts +26 -2
- package/dist/cli/mcp.js +33 -4
- package/dist/cli/startup.js +8 -2
- package/dist/cli.js +17 -5
- package/dist/client/subscribe.d.ts +10 -0
- package/dist/client/subscribe.js +2 -0
- package/dist/daemon.d.ts +44 -7
- package/dist/daemon.js +54 -8
- package/dist/http/deliverability.d.ts +68 -0
- package/dist/http/deliverability.js +78 -0
- package/dist/http/qa.js +8 -1
- package/dist/http/writes.js +24 -2
- package/dist/pi/install.d.ts +15 -0
- package/dist/pi/install.js +35 -0
- package/dist/pi/mission-control/actions.d.ts +30 -2
- package/dist/pi/mission-control/actions.js +92 -3
- package/dist/pi/mission-control/board.d.ts +84 -0
- package/dist/pi/mission-control/board.js +62 -1
- package/dist/pi/mission-control/extension.d.ts +122 -1
- package/dist/pi/mission-control/extension.js +332 -13
- package/dist/pi/mission-control/render.d.ts +25 -0
- package/dist/pi/mission-control/render.js +131 -12
- package/dist/spawn.d.ts +43 -26
- package/dist/spawn.js +64 -37
- package/dist/tools/cue.js +12 -0
- package/dist/tools/ensemble.js +33 -2
- package/dist/tools/recruit.js +8 -26
- package/dist/utils/sdk-probe.d.ts +12 -0
- package/dist/utils/sdk-probe.js +28 -0
- package/package.json +8 -4
package/dist/cli/commands.js
CHANGED
|
@@ -35,7 +35,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.status = status;
|
|
37
37
|
exports.init = init;
|
|
38
|
+
exports.initProject = initProject;
|
|
38
39
|
exports.server = server;
|
|
40
|
+
exports.formatEnsembleReadyLines = formatEnsembleReadyLines;
|
|
39
41
|
exports.up = up;
|
|
40
42
|
exports.formatScheduleRecurrence = formatScheduleRecurrence;
|
|
41
43
|
exports.lineupScheduleToEntry = lineupScheduleToEntry;
|
|
@@ -63,6 +65,7 @@ const croner_1 = require("croner");
|
|
|
63
65
|
const client_1 = require("@temporalio/client");
|
|
64
66
|
const spawn_1 = require("../spawn");
|
|
65
67
|
const probe_1 = require("../pi/probe");
|
|
68
|
+
const install_1 = require("../pi/install");
|
|
66
69
|
const config_1 = require("../config");
|
|
67
70
|
const git_info_1 = require("../git-info");
|
|
68
71
|
const connection_1 = require("../connection");
|
|
@@ -709,7 +712,10 @@ async function start(opts) {
|
|
|
709
712
|
out.log(`\nCheck status: ${out.dim('agent-tempo status ' + opts.ensemble)}`);
|
|
710
713
|
if (startLineup && startInitialStartup) {
|
|
711
714
|
console.log();
|
|
712
|
-
|
|
715
|
+
// #832 — name the RESOLVED ensemble (opts.ensemble), not the lineup's `name:`.
|
|
716
|
+
const [readyLine, connectLine] = formatEnsembleReadyLines(opts.ensemble, startLineup.name, startLineup.players.length);
|
|
717
|
+
out.success(readyLine);
|
|
718
|
+
out.log(` ${out.dim(connectLine)}`);
|
|
713
719
|
}
|
|
714
720
|
}
|
|
715
721
|
async function status(opts) {
|
|
@@ -857,7 +863,7 @@ async function status(opts) {
|
|
|
857
863
|
async function init(opts) {
|
|
858
864
|
if (opts.project) {
|
|
859
865
|
// Per-project .mcp.json mode
|
|
860
|
-
return initProject(opts.dir);
|
|
866
|
+
return initProject(opts.dir, opts.suppressNextSteps);
|
|
861
867
|
}
|
|
862
868
|
// Default: global install via `claude mcp add`
|
|
863
869
|
if ((0, mcp_1.isGlobalMcpRegistered)() || (0, mcp_1.isMcpConfigured)(opts.dir)) {
|
|
@@ -868,22 +874,35 @@ async function init(opts) {
|
|
|
868
874
|
const claudePath = (0, spawn_1.resolveClaudePath)();
|
|
869
875
|
if (claudePath === 'claude') {
|
|
870
876
|
out.warn('claude binary not found — falling back to project-level .mcp.json');
|
|
871
|
-
return initProject(opts.dir);
|
|
877
|
+
return initProject(opts.dir, opts.suppressNextSteps);
|
|
872
878
|
}
|
|
873
|
-
|
|
879
|
+
const reg = (0, mcp_1.addGlobalMcp)();
|
|
880
|
+
if (reg.ok) {
|
|
874
881
|
out.success('Registered agent-tempo globally (user scope)');
|
|
875
882
|
out.log(` ${out.dim('Available in all Claude Code sessions')}`);
|
|
876
883
|
}
|
|
877
884
|
else {
|
|
878
885
|
out.warn('Failed to register globally — falling back to project-level .mcp.json');
|
|
879
|
-
|
|
886
|
+
// #818 — surface WHY (the captured `claude mcp add` stderr) so a fresh-install
|
|
887
|
+
// failure is diagnosable instead of silent. Show the first line; `DEBUG=1` logs full.
|
|
888
|
+
if (reg.error)
|
|
889
|
+
out.log(` ${out.dim(reg.error.split('\n')[0])}`);
|
|
890
|
+
return initProject(opts.dir, opts.suppressNextSteps);
|
|
891
|
+
}
|
|
892
|
+
if (!opts.suppressNextSteps) {
|
|
893
|
+
out.log(`\nNext steps:`);
|
|
894
|
+
out.log(` 1. Start Temporal: ${out.dim('temporal server start-dev')}`);
|
|
895
|
+
out.log(` 2. Start conductor: ${out.dim('agent-tempo conduct')}`);
|
|
880
896
|
}
|
|
881
|
-
out.log(`\nNext steps:`);
|
|
882
|
-
out.log(` 1. Start Temporal: ${out.dim('temporal server start-dev')}`);
|
|
883
|
-
out.log(` 2. Start conductor: ${out.dim('agent-tempo conduct')}`);
|
|
884
897
|
}
|
|
885
|
-
/**
|
|
886
|
-
|
|
898
|
+
/**
|
|
899
|
+
* Per-project .mcp.json install (legacy, used with --project flag).
|
|
900
|
+
*
|
|
901
|
+
* Exported for #818 regression coverage (the `suppressNextSteps` gate) — it only
|
|
902
|
+
* touches the filesystem + stdout, so it is unit-testable without shelling out to
|
|
903
|
+
* `claude`. Not part of the public CLI surface.
|
|
904
|
+
*/
|
|
905
|
+
function initProject(dir, suppressNextSteps) {
|
|
887
906
|
const mcpPath = (0, path_1.join)(dir, '.mcp.json');
|
|
888
907
|
const entry = {
|
|
889
908
|
command: 'agent-tempo-server',
|
|
@@ -919,9 +938,13 @@ function initProject(dir) {
|
|
|
919
938
|
out.success('Created .mcp.json with agent-tempo config');
|
|
920
939
|
}
|
|
921
940
|
out.log(` ${out.dim(mcpPath)}`);
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
941
|
+
// #818 — `up` suppresses this stale manual-setup block (it already started
|
|
942
|
+
// Temporal + the conductor); the manual `agent-tempo init` verb still shows it.
|
|
943
|
+
if (!suppressNextSteps) {
|
|
944
|
+
out.log(`\nNext steps:`);
|
|
945
|
+
out.log(` 1. Start Temporal: ${out.dim('temporal server start-dev')}`);
|
|
946
|
+
out.log(` 2. Start conductor: ${out.dim('agent-tempo conduct')}`);
|
|
947
|
+
}
|
|
925
948
|
}
|
|
926
949
|
// --- Temporal server management ---
|
|
927
950
|
// #700 P1 — DEFAULT_DB_PATH, isTemporalReachable, and registerSearchAttributes
|
|
@@ -1018,6 +1041,25 @@ async function server(opts) {
|
|
|
1018
1041
|
out.success('Temporal ready');
|
|
1019
1042
|
}
|
|
1020
1043
|
}
|
|
1044
|
+
/**
|
|
1045
|
+
* #832 — the operator-facing "ensemble ready" lines for `up` / `conduct`.
|
|
1046
|
+
*
|
|
1047
|
+
* Keyed on the RESOLVED ensemble name (what `command-center` / `status` take —
|
|
1048
|
+
* `--ensemble` flag > positional > env > `default`), NOT the lineup's `name:`
|
|
1049
|
+
* field. The two diverge whenever any of those overrides the lineup name, so the
|
|
1050
|
+
* old `Ensemble <lineup-name> is ready` line named a thing the operator can't
|
|
1051
|
+
* actually `connect` to. Surfaces the connect command so the next step is one
|
|
1052
|
+
* copy-paste away. Pure + exported for unit testing (the surrounding `up` does
|
|
1053
|
+
* I/O and isn't unit-testable). The shared markdown {@link ensembleReadyBanner}
|
|
1054
|
+
* stays lineup-keyed for the conductor's seeded directive (a different surface).
|
|
1055
|
+
*/
|
|
1056
|
+
function formatEnsembleReadyLines(ensemble, lineupName, playerCount) {
|
|
1057
|
+
const plural = playerCount === 1 ? '' : 's';
|
|
1058
|
+
return [
|
|
1059
|
+
`Ensemble "${ensemble}" is ready (from lineup ${lineupName}). ${playerCount} player${plural} on standby.`,
|
|
1060
|
+
`Connect: agent-tempo command-center ${ensemble}`,
|
|
1061
|
+
];
|
|
1062
|
+
}
|
|
1021
1063
|
async function up(opts) {
|
|
1022
1064
|
const config = (0, config_1.getConfig)(opts);
|
|
1023
1065
|
// #689 — best-effort sweep of stale 0600 secret env files (residual from a shell
|
|
@@ -1074,7 +1116,10 @@ async function up(opts) {
|
|
|
1074
1116
|
out.check('MCP configured', true);
|
|
1075
1117
|
}
|
|
1076
1118
|
else {
|
|
1077
|
-
|
|
1119
|
+
// #818 — suppress init's manual "Next steps" block: `up` has already started
|
|
1120
|
+
// Temporal + the conductor above, so printing "1. Start Temporal / 2. Start
|
|
1121
|
+
// conductor" here is stale and misleading.
|
|
1122
|
+
await init({ dir: process.cwd(), suppressNextSteps: true });
|
|
1078
1123
|
out.check('MCP configured', true);
|
|
1079
1124
|
}
|
|
1080
1125
|
// Always forward all resolved Temporal settings to child processes.
|
|
@@ -1274,10 +1319,33 @@ async function up(opts) {
|
|
|
1274
1319
|
if (!process.env.ANTHROPIC_API_KEY) {
|
|
1275
1320
|
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.');
|
|
1276
1321
|
}
|
|
1322
|
+
// #825 — extension-registration guard (mirrors command-center's #820 Bug-2
|
|
1323
|
+
// guard). `up --agent pi` no longer passes an inline `-e` (that risked a
|
|
1324
|
+
// divergent-copy double-load, #825); it now relies on the player extension
|
|
1325
|
+
// being registered in Pi's settings.json. On a box that never ran `install-pi`,
|
|
1326
|
+
// a plain `pi` would launch with NO extension — no claim/heartbeat, a silent
|
|
1327
|
+
// non-conductor (the #820 Bug-2 failure, transplanted to the conductor). So
|
|
1328
|
+
// auto-install idempotently before spawning; fail loud with the manual command
|
|
1329
|
+
// if the write fails. (Checks the GLOBAL settings.json, like command-center; a
|
|
1330
|
+
// user who ran `install-pi --project` still works — `pi` loads the project
|
|
1331
|
+
// path and the same realpath dedupes, so the redundant global install is a
|
|
1332
|
+
// harmless idempotent write, never a second load.)
|
|
1333
|
+
if (!(0, install_1.arePiExtensionsRegistered)()) {
|
|
1334
|
+
try {
|
|
1335
|
+
const result = (0, install_1.installPiExtensions)();
|
|
1336
|
+
out.log(out.dim(` Registered the Pi extensions in ${result.settingsPath} (first-run install-pi).`));
|
|
1337
|
+
}
|
|
1338
|
+
catch (err) {
|
|
1339
|
+
out.error('Cannot start Pi conductor — the Pi extensions are not registered and auto-install failed: ' +
|
|
1340
|
+
`${err instanceof Error ? err.message : String(err)}. Run \`agent-tempo install-pi\` manually, then retry.`);
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1277
1344
|
let piSpawn;
|
|
1278
1345
|
try {
|
|
1279
|
-
// resolvePiInteractiveBinary
|
|
1280
|
-
//
|
|
1346
|
+
// resolvePiInteractiveBinary throws fail-clean (Pi CLI missing) — caught
|
|
1347
|
+
// here, no terminal launched. #825: no more `-e`/extension resolution — the
|
|
1348
|
+
// player extension loads from settings.json, registered + guarded just above.
|
|
1281
1349
|
piSpawn = (0, spawn_1.buildPiConductorSpawn)({
|
|
1282
1350
|
ensemble: opts.ensemble,
|
|
1283
1351
|
sessionName,
|
|
@@ -1362,7 +1430,10 @@ async function up(opts) {
|
|
|
1362
1430
|
// nothing is deferred — we only surface the banner on initial-startup paths.
|
|
1363
1431
|
if (lineup && initialStartup) {
|
|
1364
1432
|
console.log();
|
|
1365
|
-
|
|
1433
|
+
// #832 — name the RESOLVED ensemble (opts.ensemble), not the lineup's `name:`.
|
|
1434
|
+
const [readyLine, connectLine] = formatEnsembleReadyLines(opts.ensemble, lineup.name, lineup.players.length);
|
|
1435
|
+
out.success(readyLine);
|
|
1436
|
+
out.log(` ${out.dim(connectLine)}`);
|
|
1366
1437
|
}
|
|
1367
1438
|
console.log();
|
|
1368
1439
|
}
|
package/dist/cli/mcp.d.ts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
/** Check if agent-tempo is registered in `claude mcp list` (global user scope). */
|
|
2
2
|
export declare function isGlobalMcpRegistered(): boolean;
|
|
3
|
-
/**
|
|
4
|
-
export
|
|
3
|
+
/** Result of {@link addGlobalMcp} — `ok` plus the captured failure reason. */
|
|
4
|
+
export interface AddGlobalMcpResult {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* #818 — when `ok` is false, the captured failure detail (the `claude mcp add`
|
|
8
|
+
* stderr, or the error message). Lets the caller surface WHY global registration
|
|
9
|
+
* failed instead of a bare "fell back to project .mcp.json" with no diagnosis.
|
|
10
|
+
*/
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Register agent-tempo globally via `claude mcp add -s user`.
|
|
15
|
+
*
|
|
16
|
+
* #818 — global registration was observed FAILING on a fresh published-package
|
|
17
|
+
* install (Windows), and the old `catch { return false }` swallowed the reason,
|
|
18
|
+
* leaving the silent project-`.mcp.json` fallback undiagnosable. We now capture the
|
|
19
|
+
* `claude mcp add` stderr and return it so the caller can surface it; a DEBUG-gated
|
|
20
|
+
* breadcrumb (`[agent-tempo:mcp]`) records the full detail for `DEBUG=1` runs.
|
|
21
|
+
*
|
|
22
|
+
* Windows hypothesis (worth confirming when reproducing): `claude mcp add -s user`
|
|
23
|
+
* may fail to resolve/write the user-scope config — e.g. the `claude` shim on PATH
|
|
24
|
+
* resolves to a `.cmd`/`.ps1` wrapper that execFileSync can't invoke directly, or
|
|
25
|
+
* the user-scope config path under `%APPDATA%`/`%USERPROFILE%` isn't writable in
|
|
26
|
+
* that shell. The captured stderr is the first concrete signal to disambiguate.
|
|
27
|
+
*/
|
|
28
|
+
export declare function addGlobalMcp(): AddGlobalMcpResult;
|
|
5
29
|
/** Remove agent-tempo from global MCP config via `claude mcp remove`. */
|
|
6
30
|
export declare function removeGlobalMcp(): boolean;
|
|
7
31
|
/** Check if agent-tempo MCP is configured (global or project-level). */
|
package/dist/cli/mcp.js
CHANGED
|
@@ -22,17 +22,46 @@ function isGlobalMcpRegistered() {
|
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
25
|
+
/** Pull the most useful failure detail off a thrown execFileSync error. */
|
|
26
|
+
function execFailureDetail(err) {
|
|
27
|
+
const e = err;
|
|
28
|
+
const stderr = e?.stderr;
|
|
29
|
+
if (stderr) {
|
|
30
|
+
const s = (Buffer.isBuffer(stderr) ? stderr.toString('utf8') : String(stderr)).trim();
|
|
31
|
+
if (s)
|
|
32
|
+
return s;
|
|
33
|
+
}
|
|
34
|
+
return e?.message ? String(e.message).trim() : String(err);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Register agent-tempo globally via `claude mcp add -s user`.
|
|
38
|
+
*
|
|
39
|
+
* #818 — global registration was observed FAILING on a fresh published-package
|
|
40
|
+
* install (Windows), and the old `catch { return false }` swallowed the reason,
|
|
41
|
+
* leaving the silent project-`.mcp.json` fallback undiagnosable. We now capture the
|
|
42
|
+
* `claude mcp add` stderr and return it so the caller can surface it; a DEBUG-gated
|
|
43
|
+
* breadcrumb (`[agent-tempo:mcp]`) records the full detail for `DEBUG=1` runs.
|
|
44
|
+
*
|
|
45
|
+
* Windows hypothesis (worth confirming when reproducing): `claude mcp add -s user`
|
|
46
|
+
* may fail to resolve/write the user-scope config — e.g. the `claude` shim on PATH
|
|
47
|
+
* resolves to a `.cmd`/`.ps1` wrapper that execFileSync can't invoke directly, or
|
|
48
|
+
* the user-scope config path under `%APPDATA%`/`%USERPROFILE%` isn't writable in
|
|
49
|
+
* that shell. The captured stderr is the first concrete signal to disambiguate.
|
|
50
|
+
*/
|
|
26
51
|
function addGlobalMcp() {
|
|
27
52
|
try {
|
|
28
53
|
(0, child_process_1.execFileSync)('claude', [
|
|
29
54
|
'mcp', 'add', 'agent-tempo', '-s', 'user',
|
|
30
55
|
'--', 'agent-tempo-server',
|
|
31
56
|
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
32
|
-
return true;
|
|
57
|
+
return { ok: true };
|
|
33
58
|
}
|
|
34
|
-
catch {
|
|
35
|
-
|
|
59
|
+
catch (err) {
|
|
60
|
+
const error = execFailureDetail(err);
|
|
61
|
+
if (process.env.DEBUG) {
|
|
62
|
+
console.error('[agent-tempo:mcp] `claude mcp add -s user` failed:', error);
|
|
63
|
+
}
|
|
64
|
+
return { ok: false, error };
|
|
36
65
|
}
|
|
37
66
|
}
|
|
38
67
|
/** Remove agent-tempo from global MCP config via `claude mcp remove`. */
|
package/dist/cli/startup.js
CHANGED
|
@@ -321,8 +321,14 @@ async function stepMcpConfig(cache, cwd, now) {
|
|
|
321
321
|
return { status: 'ok', durationMs: 0 };
|
|
322
322
|
}
|
|
323
323
|
// Install into global user scope (matches current `init` behavior).
|
|
324
|
-
if (!(0, mcp_1.isGlobalMcpRegistered)()
|
|
325
|
-
|
|
324
|
+
if (!(0, mcp_1.isGlobalMcpRegistered)()) {
|
|
325
|
+
const reg = (0, mcp_1.addGlobalMcp)();
|
|
326
|
+
if (reg.ok) {
|
|
327
|
+
return { status: 'action-taken', durationMs: 0, detail: 'registered agent-tempo in user MCP scope' };
|
|
328
|
+
}
|
|
329
|
+
// #818 — include the captured failure reason so the bootstrap step is diagnosable.
|
|
330
|
+
const why = reg.error ? ` (${reg.error.split('\n')[0]})` : '';
|
|
331
|
+
return { status: 'failed', durationMs: 0, detail: `Could not register agent-tempo MCP${why}. Run \`agent-tempo init\` manually.` };
|
|
326
332
|
}
|
|
327
333
|
return { status: 'failed', durationMs: 0, detail: 'Could not register agent-tempo MCP. Run `agent-tempo init` manually.' };
|
|
328
334
|
});
|
package/dist/cli.js
CHANGED
|
@@ -443,7 +443,11 @@ async function main() {
|
|
|
443
443
|
// import — an operator can open the board even when the Temporal SDK is broken.
|
|
444
444
|
const { commandCenterCommand } = await Promise.resolve().then(() => __importStar(require('./cli/command-center-command')));
|
|
445
445
|
await commandCenterCommand({
|
|
446
|
-
|
|
446
|
+
// #820 (Bug 3) — route through the canonical resolver (flag > positional >
|
|
447
|
+
// env > 'default'), mirroring `up` post-#685. Previously passed the bare
|
|
448
|
+
// `positional[1]`, which command-center-command then dropped in favor of
|
|
449
|
+
// `config.ensemble` (env/default) → `command-center <ensemble>` watched 'default'.
|
|
450
|
+
ensemble: (0, resolve_ensemble_1.resolveEnsemble)(args),
|
|
447
451
|
...overrides,
|
|
448
452
|
});
|
|
449
453
|
return;
|
|
@@ -540,10 +544,18 @@ async function main() {
|
|
|
540
544
|
});
|
|
541
545
|
break;
|
|
542
546
|
case 'destroy': {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
+
// #835 — use the SHARED resolver (flag > positional > env > default) so
|
|
548
|
+
// `destroy` matches every other verb. It previously hand-rolled INVERTED
|
|
549
|
+
// precedence (`positional || ensemble || env`), so `destroy foo --ensemble
|
|
550
|
+
// bar` destroyed `foo` — the lone outlier. `destroy` is gated by a typed
|
|
551
|
+
// confirmation (or `--yes`), so the resolver's `default` fallback is safe.
|
|
552
|
+
const target = (0, resolve_ensemble_1.resolveEnsemble)(args);
|
|
553
|
+
// #835 — surface a flag-over-positional override as a non-fatal note so the
|
|
554
|
+
// flag silently winning over a DIFFERENT positional isn't a silent mask
|
|
555
|
+
// (flag wins by design — this only warns, never errors).
|
|
556
|
+
const positional = args.positional[1];
|
|
557
|
+
if (args.ensemble && positional && args.ensemble !== positional) {
|
|
558
|
+
out.warn(`note: --ensemble "${args.ensemble}" overrides positional "${positional}"`);
|
|
547
559
|
}
|
|
548
560
|
await destroy({
|
|
549
561
|
ensemble: target,
|
|
@@ -81,6 +81,16 @@ export interface SubscribeDeps {
|
|
|
81
81
|
* present (Node 20), the wrapper falls back to fetch.
|
|
82
82
|
*/
|
|
83
83
|
EventSourceImpl?: typeof EventSource;
|
|
84
|
+
/**
|
|
85
|
+
* #826 — force the fetch transport even when a native `EventSource` is
|
|
86
|
+
* available and no token is set. The fetch path is the only one that
|
|
87
|
+
* surfaces a permanent **401/404** as a thrown {@link SubscribeHttpError};
|
|
88
|
+
* native `EventSource` swallows those into its own silent reconnect cycle.
|
|
89
|
+
* The mission-control board needs that hard-error visibility (404 → `gone`,
|
|
90
|
+
* 401 → auth hint), so it sets this. TUI / dashboard leave it unset and keep
|
|
91
|
+
* the auto-selection (native `EventSource` on a tokenless loopback board).
|
|
92
|
+
*/
|
|
93
|
+
forceFetch?: boolean;
|
|
84
94
|
/**
|
|
85
95
|
* Override sleep — used by tests to fast-forward backoff. Accepts an
|
|
86
96
|
* `AbortSignal` so the wrapper can wake early on abort.
|
package/dist/client/subscribe.js
CHANGED
|
@@ -216,6 +216,8 @@ function makeIterator(args) {
|
|
|
216
216
|
* for `Authorization: Bearer …` and is the only option in Node 20.
|
|
217
217
|
*/
|
|
218
218
|
function canUseEventSource(deps) {
|
|
219
|
+
if (deps.forceFetch)
|
|
220
|
+
return false; // #826 — caller needs throw-on-permanent
|
|
219
221
|
if (deps.token)
|
|
220
222
|
return false;
|
|
221
223
|
return resolveEventSource(deps) !== undefined;
|
package/dist/daemon.d.ts
CHANGED
|
@@ -74,14 +74,13 @@ export declare function ensureDevNamespace(connection: Pick<Connection, 'workflo
|
|
|
74
74
|
* Test-only dependency-injection seam for {@link computeHostProfile}.
|
|
75
75
|
*
|
|
76
76
|
* Production callers omit `deps` entirely; the defaults resolve to the
|
|
77
|
-
* real probes
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* but only includes synchronous probes — `computeHostProfile` is sync
|
|
82
|
-
* by contract.
|
|
77
|
+
* real probes. Tests inject stubs to exercise installed-vs-not-installed
|
|
78
|
+
* scenarios deterministically without touching the host filesystem or
|
|
79
|
+
* spawning subprocesses. All probes are synchronous — `computeHostProfile`
|
|
80
|
+
* is sync by contract.
|
|
83
81
|
*
|
|
84
|
-
* Added in #532 PR-2 for the copilot
|
|
82
|
+
* Added in #532 PR-2 for the copilot probe; extended in #819 for pi,
|
|
83
|
+
* opencode, and claude-api probes.
|
|
85
84
|
*/
|
|
86
85
|
export interface ComputeHostProfileDeps {
|
|
87
86
|
/**
|
|
@@ -91,6 +90,44 @@ export interface ComputeHostProfileDeps {
|
|
|
91
90
|
* `undefined` when missing or unresolvable.
|
|
92
91
|
*/
|
|
93
92
|
resolveCopilotSdkVersionSync?: () => string | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* #819 — Synchronous Pi availability probe. Default: checks Node ≥ 22.19
|
|
95
|
+
* floor via {@link checkPiNodeFloor} AND `@earendil-works/pi-coding-agent`
|
|
96
|
+
* install via {@link probeSdkInstall}. Returns `{ available: true }` when
|
|
97
|
+
* both pass, `{ available: false }` otherwise. Tests inject a stub to
|
|
98
|
+
* avoid filesystem walks and Node-version coupling.
|
|
99
|
+
*/
|
|
100
|
+
probePiSync?: () => {
|
|
101
|
+
available: boolean;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* #819 — Synchronous opencode availability probe. Default: checks
|
|
105
|
+
* `@opencode-ai/sdk` install via {@link probeSdkInstall} AND
|
|
106
|
+
* `opencode` binary on PATH via {@link hasOpencodeCliOnPath}. Returns
|
|
107
|
+
* `true` when both pass. Tests inject a stub.
|
|
108
|
+
*/
|
|
109
|
+
probeOpencodeSync?: () => boolean;
|
|
110
|
+
/**
|
|
111
|
+
* #819 — Synchronous claude-api availability probe. Default: checks
|
|
112
|
+
* `@anthropic-ai/sdk` install via {@link probeSdkInstall} AND
|
|
113
|
+
* `ANTHROPIC_API_KEY` in the daemon's process env. Returns `true` when
|
|
114
|
+
* both pass. Tests inject a stub.
|
|
115
|
+
*
|
|
116
|
+
* Why the key is required (not SDK-only like copilot):
|
|
117
|
+
* The claude-api adapter is spawned BY the daemon and inherits the
|
|
118
|
+
* DAEMON's env (`spawn.ts` passes no explicit env; the adapter process
|
|
119
|
+
* reads its own `process.env.ANTHROPIC_API_KEY` at execution time —
|
|
120
|
+
* `src/adapters/claude-api/adapter.ts:270`). Recruit does NOT plumb the
|
|
121
|
+
* key from the recruiting session. Therefore: the daemon having the key
|
|
122
|
+
* IS the deliverability predicate — advertising when the daemon env has
|
|
123
|
+
* the key makes this probe agree exactly with what recruit can deliver.
|
|
124
|
+
* This is not a conservative heuristic; it is the precise gate.
|
|
125
|
+
*
|
|
126
|
+
* The daemon inherits its env at spawn time (`...process.env` in
|
|
127
|
+
* `src/cli/daemon.ts`). A user adding the key after the daemon is
|
|
128
|
+
* already running must restart the daemon for it to be advertised.
|
|
129
|
+
*/
|
|
130
|
+
probeClaudeApiSync?: () => boolean;
|
|
94
131
|
}
|
|
95
132
|
/**
|
|
96
133
|
* Build the daemon's capability profile from its config + runtime env.
|
package/dist/daemon.js
CHANGED
|
@@ -82,6 +82,8 @@ const orphans_1 = require("./reconcile/orphans");
|
|
|
82
82
|
const agent_types_1 = require("./ensemble/agent-types");
|
|
83
83
|
const pre_flight_1 = require("./adapters/claude-code-headless/pre-flight");
|
|
84
84
|
const daemon_adapter_versions_1 = require("./daemon-adapter-versions");
|
|
85
|
+
const sdk_probe_1 = require("./utils/sdk-probe");
|
|
86
|
+
const probe_1 = require("./pi/probe");
|
|
85
87
|
const log = (...args) => console.error(`[agent-tempo:daemon ${new Date().toISOString()}]`, ...args);
|
|
86
88
|
/**
|
|
87
89
|
* Daemon process start time, captured at module load. Issue #399 Q5.3b
|
|
@@ -304,6 +306,16 @@ function daemonVersion() {
|
|
|
304
306
|
*/
|
|
305
307
|
function computeHostProfile(config, deps = {}) {
|
|
306
308
|
const resolveCopilotSync = deps.resolveCopilotSdkVersionSync ?? daemon_adapter_versions_1.resolveCopilotSdkVersionSync;
|
|
309
|
+
// #819 — production defaults for new optional-adapter probes. Each default
|
|
310
|
+
// is a one-liner wrapping the real probe; tests inject stubs.
|
|
311
|
+
const probePiSyncFn = deps.probePiSync ??
|
|
312
|
+
(() => (0, probe_1.checkPiNodeFloor)().ok && (0, sdk_probe_1.probeSdkInstall)(probe_1.PI_PACKAGE)
|
|
313
|
+
? { available: true }
|
|
314
|
+
: { available: false });
|
|
315
|
+
const probeOpencodeSyncFn = deps.probeOpencodeSync ??
|
|
316
|
+
(() => (0, sdk_probe_1.probeSdkInstall)('@opencode-ai/sdk') && (0, sdk_probe_1.hasOpencodeCliOnPath)());
|
|
317
|
+
const probeClaudeApiSyncFn = deps.probeClaudeApiSync ??
|
|
318
|
+
(() => (0, sdk_probe_1.probeSdkInstall)('@anthropic-ai/sdk') && !!process.env.ANTHROPIC_API_KEY);
|
|
307
319
|
const agentTypes = (() => {
|
|
308
320
|
try {
|
|
309
321
|
return (0, agent_types_1.listAgentTypes)().map((a) => a.name);
|
|
@@ -353,18 +365,52 @@ function computeHostProfile(config, deps = {}) {
|
|
|
353
365
|
// require failures, but the boot path must not crash on any
|
|
354
366
|
// surprise here.
|
|
355
367
|
}
|
|
368
|
+
// #819 — pi probe. Delegates to `probePiSyncFn` (injected for tests;
|
|
369
|
+
// production default checks Node ≥ 22.19 floor + SDK install). Mirrors
|
|
370
|
+
// the recruit pre-flight: both conditions must pass before advertising.
|
|
371
|
+
try {
|
|
372
|
+
if (probePiSyncFn().available && !availableAgentTypes.includes('pi')) {
|
|
373
|
+
availableAgentTypes.push('pi');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// Boot path must not crash on any probe failure.
|
|
378
|
+
}
|
|
379
|
+
// #819 — opencode probe. Delegates to `probeOpencodeSyncFn` (injected for
|
|
380
|
+
// tests; production default checks SDK install + `opencode` binary on PATH).
|
|
381
|
+
try {
|
|
382
|
+
if (probeOpencodeSyncFn() && !availableAgentTypes.includes('opencode')) {
|
|
383
|
+
availableAgentTypes.push('opencode');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Boot path must not crash on any probe failure.
|
|
388
|
+
}
|
|
389
|
+
// #819 — claude-api probe. Delegates to `probeClaudeApiSyncFn` (injected
|
|
390
|
+
// for tests; production default checks SDK install + ANTHROPIC_API_KEY in
|
|
391
|
+
// daemon env). The key check is not a conservative heuristic — the daemon
|
|
392
|
+
// env IS the deliverability predicate (adapter inherits daemon env; recruit
|
|
393
|
+
// does not plumb a key from the caller). See ComputeHostProfileDeps for the
|
|
394
|
+
// full rationale.
|
|
395
|
+
try {
|
|
396
|
+
if (probeClaudeApiSyncFn() && !availableAgentTypes.includes('claude-api')) {
|
|
397
|
+
availableAgentTypes.push('claude-api');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// Boot path must not crash on any probe failure.
|
|
402
|
+
}
|
|
356
403
|
return {
|
|
357
404
|
hostname: os.hostname(),
|
|
358
405
|
version: daemonVersion(),
|
|
359
406
|
defaultAgent: config.defaultAgent,
|
|
360
|
-
// #520 + #532 PR-2 — was: `[config.defaultAgent]`. Now grows when
|
|
361
|
-
// the optional probes pass: `claude-code-headless` (when `claude`
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
// `
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
// keeps the wire shape forward-compatible.
|
|
407
|
+
// #520 + #532 PR-2 + #819 — was: `[config.defaultAgent]`. Now grows when
|
|
408
|
+
// the optional probes pass: `claude-code-headless` (when `claude` is on
|
|
409
|
+
// PATH AND logged in), `copilot` (when `@github/copilot-sdk` is installed),
|
|
410
|
+
// `pi` (when `@earendil-works/pi-coding-agent` installed + Node ≥ 22.19),
|
|
411
|
+
// `opencode` (when `@opencode-ai/sdk` installed + `opencode` on PATH),
|
|
412
|
+
// `claude-api` (when `@anthropic-ai/sdk` installed + ANTHROPIC_API_KEY in
|
|
413
|
+
// daemon env). Recording as an array keeps the wire shape forward-compatible.
|
|
368
414
|
availableAgentTypes,
|
|
369
415
|
availablePlayerTypes: agentTypes,
|
|
370
416
|
claudeBin: config.claudeBin,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon-side cue-deliverability preflight (#822).
|
|
3
|
+
*
|
|
4
|
+
* The maestro-outbox write endpoints (`POST .../cue`, `.../ask`, `.../reset`,
|
|
5
|
+
* and the board `/handoff` which routes through `.../cue`) historically enqueued
|
|
6
|
+
* to ANY target phase and returned a bare `202 { ok: true }` — so the
|
|
7
|
+
* command-center board rendered a confident `✓` even when the target had no live
|
|
8
|
+
* adapter (phase `detached`/`gone`) and the message merely queued on the
|
|
9
|
+
* workflow inbox, undelivered until re-attach. The MCP `cue` tool already guards
|
|
10
|
+
* this (`UNDELIVERABLE_PHASES` in `src/tools/cue.ts`) but it runs in-process
|
|
11
|
+
* before the Temporal write; the HTTP surface skipped the preflight entirely.
|
|
12
|
+
*
|
|
13
|
+
* This helper mirrors that phase check for the HTTP write surface, with one
|
|
14
|
+
* deliberate product divergence (design note Part 2, Ruling A): the operator
|
|
15
|
+
* board is **warn-but-queue**, NOT hard-fail like the MCP tool. The MCP tool
|
|
16
|
+
* serves an autonomous LLM that should re-route; the board serves a human
|
|
17
|
+
* operator who wants the message to durably land and just needs to KNOW it's
|
|
18
|
+
* queued, not delivered. So the endpoint keeps the enqueue and returns the
|
|
19
|
+
* target's deliverability so the board can render `⚠ queued (detached — delivers
|
|
20
|
+
* on re-attach)` instead of `✓`.
|
|
21
|
+
*
|
|
22
|
+
* Soft-failing: the preflight is bounded (1s) and never blocks the enqueue. On
|
|
23
|
+
* timeout / query error / unresolvable target it reports `'live'` (best-effort —
|
|
24
|
+
* the same posture as the MCP tool's preflight catch).
|
|
25
|
+
*/
|
|
26
|
+
import type { TempoClient } from '../client/interface';
|
|
27
|
+
import type { AttachmentPhase } from '../types';
|
|
28
|
+
/**
|
|
29
|
+
* Phases where the target has no live adapter, so a cue/ask/reset queues on the
|
|
30
|
+
* workflow inbox undelivered (auto-redelivers on re-attach). Mirrors
|
|
31
|
+
* `UNDELIVERABLE_PHASES` in `src/tools/cue.ts` — `draining` is deliberately
|
|
32
|
+
* EXCLUDED (a brief mid-shutdown state whose adapter is still consuming pending
|
|
33
|
+
* messages; flagging it would false-positive on normal teardown).
|
|
34
|
+
*/
|
|
35
|
+
export declare const UNDELIVERABLE_PHASES: ReadonlySet<AttachmentPhase>;
|
|
36
|
+
/** Bounded preflight window — matches the MCP cue tool's 1s operator-interactive cap. */
|
|
37
|
+
export declare const DELIVERABILITY_PREFLIGHT_TIMEOUT_MS = 1000;
|
|
38
|
+
/** `'queued'` = enqueued but no live adapter to receive now; `'live'` = a live
|
|
39
|
+
* adapter is (or is best-effort assumed) present. */
|
|
40
|
+
export type Delivery = 'queued' | 'live';
|
|
41
|
+
export interface DeliverabilityResult {
|
|
42
|
+
delivery: Delivery;
|
|
43
|
+
/** The observed phase, when the preflight query succeeded. */
|
|
44
|
+
phase?: AttachmentPhase;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Bounded, soft-failing phase preflight. Returns `{ delivery: 'queued', phase }`
|
|
48
|
+
* ONLY when an undeliverable phase is positively observed; on timeout / query
|
|
49
|
+
* error / unresolvable target it returns `{ delivery: 'live' }` (best-effort —
|
|
50
|
+
* never block the enqueue). Pure of HTTP concerns so `writes.ts` and `qa.ts`
|
|
51
|
+
* share one implementation.
|
|
52
|
+
*/
|
|
53
|
+
export declare function checkDeliverability(client: TempoClient, ensemble: string, playerId: string): Promise<DeliverabilityResult>;
|
|
54
|
+
/**
|
|
55
|
+
* Build the additive deliverability fields folded into the `202` JSON body of an
|
|
56
|
+
* outbox-enqueue endpoint. Superset shape satisfying both the design note
|
|
57
|
+
* (`delivery`/`phase`) and the QA regression bar (`queued`/`warning`):
|
|
58
|
+
*
|
|
59
|
+
* - undeliverable → `{ delivery:'queued', phase, queued:true, warning:'queued-undeliverable (<phase>)' }`
|
|
60
|
+
* - live → `{ delivery:'live', queued:false }` (NO `warning` field — a healthy
|
|
61
|
+
* target must read as a clean success)
|
|
62
|
+
*/
|
|
63
|
+
export declare function deliverabilityResponseFields(r: DeliverabilityResult): {
|
|
64
|
+
delivery: Delivery;
|
|
65
|
+
queued: boolean;
|
|
66
|
+
phase?: AttachmentPhase;
|
|
67
|
+
warning?: string;
|
|
68
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DELIVERABILITY_PREFLIGHT_TIMEOUT_MS = exports.UNDELIVERABLE_PHASES = void 0;
|
|
4
|
+
exports.checkDeliverability = checkDeliverability;
|
|
5
|
+
exports.deliverabilityResponseFields = deliverabilityResponseFields;
|
|
6
|
+
/**
|
|
7
|
+
* Phases where the target has no live adapter, so a cue/ask/reset queues on the
|
|
8
|
+
* workflow inbox undelivered (auto-redelivers on re-attach). Mirrors
|
|
9
|
+
* `UNDELIVERABLE_PHASES` in `src/tools/cue.ts` — `draining` is deliberately
|
|
10
|
+
* EXCLUDED (a brief mid-shutdown state whose adapter is still consuming pending
|
|
11
|
+
* messages; flagging it would false-positive on normal teardown).
|
|
12
|
+
*/
|
|
13
|
+
exports.UNDELIVERABLE_PHASES = new Set(['detached', 'gone']);
|
|
14
|
+
/** Bounded preflight window — matches the MCP cue tool's 1s operator-interactive cap. */
|
|
15
|
+
exports.DELIVERABILITY_PREFLIGHT_TIMEOUT_MS = 1000;
|
|
16
|
+
/** Resolve `p`, or reject with a timeout after `ms`. */
|
|
17
|
+
function withTimeout(p, ms) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const timer = setTimeout(() => reject(new Error('deliverability preflight timed out')), ms);
|
|
20
|
+
if (typeof timer.unref === 'function') {
|
|
21
|
+
timer.unref();
|
|
22
|
+
}
|
|
23
|
+
p.then((v) => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
resolve(v);
|
|
26
|
+
}, (err) => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
reject(err);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Bounded, soft-failing phase preflight. Returns `{ delivery: 'queued', phase }`
|
|
34
|
+
* ONLY when an undeliverable phase is positively observed; on timeout / query
|
|
35
|
+
* error / unresolvable target it returns `{ delivery: 'live' }` (best-effort —
|
|
36
|
+
* never block the enqueue). Pure of HTTP concerns so `writes.ts` and `qa.ts`
|
|
37
|
+
* share one implementation.
|
|
38
|
+
*/
|
|
39
|
+
async function checkDeliverability(client, ensemble, playerId) {
|
|
40
|
+
try {
|
|
41
|
+
const info = await withTimeout(client.attachmentInfo(ensemble, playerId), exports.DELIVERABILITY_PREFLIGHT_TIMEOUT_MS);
|
|
42
|
+
const phase = info?.phase;
|
|
43
|
+
if (phase !== undefined && exports.UNDELIVERABLE_PHASES.has(phase)) {
|
|
44
|
+
return { delivery: 'queued', phase };
|
|
45
|
+
}
|
|
46
|
+
return { delivery: 'live', ...(phase !== undefined ? { phase } : {}) };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Timed out / query threw / no session — proceed best-effort (the message
|
|
50
|
+
// still enqueues; auto-redelivery on re-attach applies). Matches the MCP
|
|
51
|
+
// cue tool's soft-fail-to-best-effort posture.
|
|
52
|
+
return { delivery: 'live' };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the additive deliverability fields folded into the `202` JSON body of an
|
|
57
|
+
* outbox-enqueue endpoint. Superset shape satisfying both the design note
|
|
58
|
+
* (`delivery`/`phase`) and the QA regression bar (`queued`/`warning`):
|
|
59
|
+
*
|
|
60
|
+
* - undeliverable → `{ delivery:'queued', phase, queued:true, warning:'queued-undeliverable (<phase>)' }`
|
|
61
|
+
* - live → `{ delivery:'live', queued:false }` (NO `warning` field — a healthy
|
|
62
|
+
* target must read as a clean success)
|
|
63
|
+
*/
|
|
64
|
+
function deliverabilityResponseFields(r) {
|
|
65
|
+
if (r.delivery === 'queued') {
|
|
66
|
+
return {
|
|
67
|
+
delivery: 'queued',
|
|
68
|
+
queued: true,
|
|
69
|
+
...(r.phase !== undefined ? { phase: r.phase } : {}),
|
|
70
|
+
warning: `queued-undeliverable${r.phase !== undefined ? ` (${r.phase})` : ''}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
delivery: 'live',
|
|
75
|
+
queued: false,
|
|
76
|
+
...(r.phase !== undefined ? { phase: r.phase } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|