@phnx-labs/agents-cli 1.22.74 → 1.22.76

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.
Files changed (148) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/README.md +21 -9
  3. package/dist/bootstrap.js +7 -7
  4. package/dist/cli/command-registry.js +5 -0
  5. package/dist/commands/artifacts-setup.js +1 -1
  6. package/dist/commands/artifacts.js +1 -1
  7. package/dist/commands/auth.js +7 -1
  8. package/dist/commands/browser.js +104 -10
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/computer.d.ts +1 -0
  11. package/dist/commands/computer.js +26 -7
  12. package/dist/commands/config.js +27 -4
  13. package/dist/commands/cost.js +6 -4
  14. package/dist/commands/doctor.d.ts +6 -5
  15. package/dist/commands/doctor.js +32 -274
  16. package/dist/commands/exec.d.ts +2 -0
  17. package/dist/commands/exec.js +9 -2
  18. package/dist/commands/harness.d.ts +1 -0
  19. package/dist/commands/harness.js +11 -3
  20. package/dist/commands/hooks.js +7 -6
  21. package/dist/commands/mcp.js +7 -6
  22. package/dist/commands/memory.js +7 -7
  23. package/dist/commands/monitors.js +3 -2
  24. package/dist/commands/open.d.ts +25 -12
  25. package/dist/commands/open.js +24 -10
  26. package/dist/commands/permissions.js +7 -6
  27. package/dist/commands/plugins.js +21 -17
  28. package/dist/commands/route.js +33 -16
  29. package/dist/commands/rules.js +7 -12
  30. package/dist/commands/sessions-share.js +1 -1
  31. package/dist/commands/setup-watchdog.js +2 -2
  32. package/dist/commands/setup.js +22 -1
  33. package/dist/commands/share.js +26 -10
  34. package/dist/commands/skills.js +7 -6
  35. package/dist/commands/subagents.js +7 -6
  36. package/dist/commands/sync.js +81 -10
  37. package/dist/commands/view.js +4 -1
  38. package/dist/commands/watchdog.d.ts +1 -1
  39. package/dist/commands/watchdog.js +10 -10
  40. package/dist/commands/webhook.d.ts +4 -0
  41. package/dist/commands/webhook.js +22 -4
  42. package/dist/commands/workflows.js +7 -6
  43. package/dist/lib/account-registry.js +27 -6
  44. package/dist/lib/accounting/rotate.d.ts +3 -1
  45. package/dist/lib/accounting/rotate.js +8 -4
  46. package/dist/lib/auth-health.d.ts +2 -0
  47. package/dist/lib/auth-health.js +2 -0
  48. package/dist/lib/browser/chrome.d.ts +21 -0
  49. package/dist/lib/browser/chrome.js +60 -3
  50. package/dist/lib/browser/drivers/local.d.ts +21 -0
  51. package/dist/lib/browser/drivers/local.js +102 -9
  52. package/dist/lib/browser/profiles.d.ts +29 -1
  53. package/dist/lib/browser/profiles.js +50 -1
  54. package/dist/lib/browser/types.d.ts +18 -0
  55. package/dist/lib/computer/computer-rpc.d.ts +6 -1
  56. package/dist/lib/computer/computer-rpc.js +23 -3
  57. package/dist/lib/computer/des.d.ts +1 -0
  58. package/dist/lib/computer/des.js +114 -0
  59. package/dist/lib/computer/rfb-client.d.ts +53 -0
  60. package/dist/lib/computer/rfb-client.js +562 -0
  61. package/dist/lib/config-keys.d.ts +7 -2
  62. package/dist/lib/config-keys.js +17 -2
  63. package/dist/lib/daemon/auth-sync-service.js +3 -0
  64. package/dist/lib/daemon/daemon.js +17 -10
  65. package/dist/lib/daemon/session-summarizer-service.d.ts +24 -0
  66. package/dist/lib/daemon/session-summarizer-service.js +39 -0
  67. package/dist/lib/daemon/usage-sync-service.js +3 -0
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/daemon-ticks.d.ts +2 -2
  71. package/dist/lib/daemon-ticks.js +2 -1
  72. package/dist/lib/daemon-webhooks.js +15 -2
  73. package/dist/lib/deeplink/register.js +10 -9
  74. package/dist/lib/deeplink/url.d.ts +4 -4
  75. package/dist/lib/deeplink/url.js +4 -4
  76. package/dist/lib/device-config.js +25 -0
  77. package/dist/lib/devices/doctor-findings.d.ts +4 -4
  78. package/dist/lib/devices/doctor-findings.js +14 -8
  79. package/dist/lib/devices/registry.js +2 -0
  80. package/dist/lib/devices/stats-cache.d.ts +4 -0
  81. package/dist/lib/devices/stats-cache.js +19 -0
  82. package/dist/lib/drift-sync.d.ts +3 -1
  83. package/dist/lib/drift-sync.js +16 -5
  84. package/dist/lib/exec.d.ts +2 -0
  85. package/dist/lib/exec.js +16 -1
  86. package/dist/lib/fleet-shared-repo-sync.d.ts +12 -0
  87. package/dist/lib/fleet-shared-repo-sync.js +101 -4
  88. package/dist/lib/fleet-shared-state.d.ts +8 -0
  89. package/dist/lib/heal.d.ts +4 -3
  90. package/dist/lib/heal.js +5 -4
  91. package/dist/lib/hosts/ready.d.ts +1 -1
  92. package/dist/lib/hosts/ready.js +16 -4
  93. package/dist/lib/hosts/reconnect.js +4 -2
  94. package/dist/lib/identity/client.d.ts +6 -0
  95. package/dist/lib/identity/index.d.ts +16 -0
  96. package/dist/lib/identity/index.js +25 -1
  97. package/dist/lib/profiles.d.ts +2 -0
  98. package/dist/lib/profiles.js +28 -9
  99. package/dist/lib/reconcile-and-repair.d.ts +109 -0
  100. package/dist/lib/reconcile-and-repair.js +267 -0
  101. package/dist/lib/routers.d.ts +12 -1
  102. package/dist/lib/routers.js +30 -1
  103. package/dist/lib/scheduling/routines.js +8 -2
  104. package/dist/lib/session/active.d.ts +13 -0
  105. package/dist/lib/session/db.d.ts +47 -7
  106. package/dist/lib/session/db.js +114 -12
  107. package/dist/lib/session/mirror.js +58 -0
  108. package/dist/lib/session/remote/remote-list.d.ts +2 -0
  109. package/dist/lib/session/remote/remote-list.js +4 -0
  110. package/dist/lib/session/remote/watch.js +22 -2
  111. package/dist/lib/session/session-cache.d.ts +19 -0
  112. package/dist/lib/session/session-cache.js +46 -0
  113. package/dist/lib/session/types.d.ts +34 -0
  114. package/dist/lib/share/backend.d.ts +6 -4
  115. package/dist/lib/share/backend.js +10 -8
  116. package/dist/lib/share/config.d.ts +4 -3
  117. package/dist/lib/share/config.js +10 -1
  118. package/dist/lib/share/delete.d.ts +1 -1
  119. package/dist/lib/share/delete.js +1 -1
  120. package/dist/lib/share/html.d.ts +1 -1
  121. package/dist/lib/share/html.js +1 -1
  122. package/dist/lib/share/provision.d.ts +1 -1
  123. package/dist/lib/share/provision.js +2 -2
  124. package/dist/lib/share/publish.d.ts +23 -7
  125. package/dist/lib/share/publish.js +58 -12
  126. package/dist/lib/share/worker-template.js +221 -60
  127. package/dist/lib/startup/command-registry.js +2 -2
  128. package/dist/lib/state.d.ts +15 -0
  129. package/dist/lib/state.js +29 -7
  130. package/dist/lib/summarizer/config.d.ts +46 -0
  131. package/dist/lib/summarizer/config.js +83 -0
  132. package/dist/lib/summarizer/pass.d.ts +45 -0
  133. package/dist/lib/summarizer/pass.js +112 -0
  134. package/dist/lib/summarizer/summarize.d.ts +68 -0
  135. package/dist/lib/summarizer/summarize.js +120 -0
  136. package/dist/lib/teams/agents.d.ts +4 -3
  137. package/dist/lib/teams/agents.js +12 -4
  138. package/dist/lib/teams/scheduler.d.ts +4 -2
  139. package/dist/lib/teams/scheduler.js +6 -6
  140. package/dist/lib/tmux/session.d.ts +2 -0
  141. package/dist/lib/tmux/session.js +7 -1
  142. package/dist/lib/types.d.ts +20 -0
  143. package/dist/lib/verbs.d.ts +23 -0
  144. package/dist/lib/verbs.js +24 -0
  145. package/dist/lib/view-types.d.ts +4 -0
  146. package/dist/lib/watchdog/rotate.d.ts +1 -1
  147. package/dist/lib/watchdog/rotate.js +1 -1
  148. package/package.json +1 -1
@@ -0,0 +1,267 @@
1
+ import { AGENTS, ALL_AGENT_IDS } from './agents.js';
2
+ import chalk from 'chalk';
3
+ import { heal, healChangedAnything } from './heal.js';
4
+ import { checkVersionHookWiring, registerHooksToSettings, repairManagedHookRuntimeArtifacts, } from './hooks/install.js';
5
+ import { getVersionHomePath, listInstalledVersions, isVersionIsolated, } from './installations/versions.js';
6
+ import { invalidateDoctorOverviewCache } from './devices/doctor-overview-cache.js';
7
+ import { remediateStaleAgentsCliInstalls, resolveRunningPackageRoot, } from './self-update.js';
8
+ import { getCliVersion } from './version.js';
9
+ import * as path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ const __moduleDirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
13
+ // Claude-family agents whose native hook wiring `checkVersionHookWiring` can
14
+ // verify (and `registerHooksToSettings` re-wire).
15
+ const HOOK_WIRING_FIX_AGENTS = ['claude', 'droid'];
16
+ // ─── hook re-wire (files reconciled but not referenced in settings.json) ─────
17
+ /**
18
+ * Re-wire hooks that reconcile as files but are absent from settings.json.
19
+ *
20
+ * heal() only re-syncs resources the diff flags missing/diff; a hook whose file
21
+ * is byte-identical to source but never referenced in settings.json is neither,
22
+ * so heal walks past it. registerHooksToSettings (the same call `agents sync`
23
+ * makes at versions.ts) regenerates the wiring, so run it for any Claude-family
24
+ * version this repair targets that has unwired hooks. Only claude/droid — the
25
+ * set checkVersionHookWiring can verify.
26
+ */
27
+ function rewireUnwiredHooks(agent, versions) {
28
+ const out = [];
29
+ const agents = agent
30
+ ? (HOOK_WIRING_FIX_AGENTS.includes(agent) ? [agent] : [])
31
+ : HOOK_WIRING_FIX_AGENTS;
32
+ for (const a of agents) {
33
+ // Explicit versions (the sync's reconcile targets) are honoured as-is; a
34
+ // sweep excludes isolated copies, mirroring heal().
35
+ const vers = agent && versions && versions.length > 0
36
+ ? versions
37
+ : listInstalledVersions(a).filter((v) => !isVersionIsolated(a, v));
38
+ for (const version of vers) {
39
+ const before = checkVersionHookWiring(a, version);
40
+ if (!before.supported)
41
+ continue;
42
+ const need = before.unwired.length + (before.settingsMissing ? (before.expected ?? 0) : 0);
43
+ if (need === 0)
44
+ continue;
45
+ try {
46
+ const registration = registerHooksToSettings(a, getVersionHomePath(a, version));
47
+ if (registration.errors.length > 0) {
48
+ out.push({ agent: a, version, rewired: 0, remaining: need, failure: 'register-failed' });
49
+ continue;
50
+ }
51
+ const after = checkVersionHookWiring(a, version);
52
+ const remaining = after.unwired.length + (after.settingsMissing ? (after.expected ?? 0) : 0);
53
+ out.push({ agent: a, version, rewired: Math.max(0, need - remaining), remaining });
54
+ }
55
+ catch {
56
+ // A shim write can fail before the native config writer returns its own
57
+ // errors. Record the same stable class and keep the repair moving to the
58
+ // one bounded runtime repair pass below.
59
+ out.push({ agent: a, version, rewired: 0, remaining: need, failure: 'register-failed' });
60
+ }
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+ function runtimeRepairFilter(agent, versions) {
66
+ if (!agent)
67
+ return undefined;
68
+ // A single concrete target narrows to that version. Broad selectors (@all,
69
+ // agent-only) still make one bounded repair pass for that harness.
70
+ return {
71
+ agent,
72
+ ...(versions && versions.length === 1 ? { version: versions[0] } : {}),
73
+ };
74
+ }
75
+ /**
76
+ * RUSH-2415: delete npx-cache / unsafe-legacy / pre-1.22.30 agents-cli copies
77
+ * when a fixed peer already exists. DESTRUCTIVE (`fs.rmSync`), so it is never
78
+ * automatic — it runs only via `agents sync --prune-clis`. `injection` scopes the
79
+ * scan+delete to sandbox paths in tests so it can never touch a real install.
80
+ */
81
+ function purgeStaleAgentsCliCopies(injection) {
82
+ let runningRoot = injection?.runningRoot;
83
+ if (!runningRoot) {
84
+ try {
85
+ // resolveRunningPackageRoot walks up until package.json names this package.
86
+ runningRoot = resolveRunningPackageRoot(__moduleDirname);
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ return remediateStaleAgentsCliInstalls({
93
+ runningRoot,
94
+ runningVersion: injection?.runningVersion ?? getCliVersion(),
95
+ ...(injection?.pathEnv !== undefined ? { pathEnv: injection.pathEnv } : {}),
96
+ ...(injection?.findOpts ? { findOpts: injection.findOpts } : {}),
97
+ ...(injection?.dryRun ? { dryRun: injection.dryRun } : {}),
98
+ });
99
+ }
100
+ // ─── public entrypoint ────────────────────────────────────────────────────────
101
+ /**
102
+ * Run the post-reconcile repair pass over the agents/versions a sync just
103
+ * touched: heal live-home gaps (mode 'full', matching the old `doctor --fix`),
104
+ * re-wire hooks the diff-driven heal leaves behind, run one bounded managed-hook
105
+ * runtime repair, and — ONLY when `pruneClis` is set on the umbrella (no-agent)
106
+ * path — purge stale/legacy agents-cli copies. The purge is never automatic.
107
+ * Returns a full account; never writes to stdout.
108
+ */
109
+ export async function repairAfterSync(opts) {
110
+ const healResult = await heal({
111
+ mode: 'full',
112
+ agent: opts.agent,
113
+ versions: opts.agent ? opts.versions : undefined,
114
+ ...(opts.cwd ? { cwd: opts.cwd } : {}),
115
+ });
116
+ // Re-wire hooks the diff-driven heal leaves behind (present file, not wired).
117
+ const hookRewire = rewireUnwiredHooks(opts.agent, opts.versions);
118
+ // One inspect→generate→verify pass after resource + native-wiring repair have
119
+ // settled. This routine never calls sync/register and never retries.
120
+ const hookRuntimeRepair = repairManagedHookRuntimeArtifacts({
121
+ filter: runtimeRepairFilter(opts.agent, opts.versions),
122
+ });
123
+ // The stale-CLI purge is DESTRUCTIVE and NEVER automatic: it runs only on the
124
+ // umbrella (no-agent) path AND only when the caller opted in via
125
+ // `agents sync --prune-clis`. Drift-fixers above always run; this does not.
126
+ const staleInstallPurge = (!opts.agent && opts.pruneClis === true)
127
+ ? purgeStaleAgentsCliCopies(opts.purgeInjection)
128
+ : null;
129
+ const report = {
130
+ heal: healResult,
131
+ hookRewire,
132
+ hookRuntimeRepair,
133
+ staleInstallPurge,
134
+ };
135
+ if (repairChangedAnything(report))
136
+ invalidateDoctorOverviewCache();
137
+ return report;
138
+ }
139
+ /** True when the repair pass changed (or attempted to change) anything. */
140
+ export function repairChangedAnything(report) {
141
+ return (healChangedAnything(report.heal) ||
142
+ report.hookRewire.some((r) => r.rewired > 0 || r.remaining > 0 || r.failure !== undefined) ||
143
+ report.hookRuntimeRepair.attempts.length > 0 ||
144
+ (report.staleInstallPurge !== null &&
145
+ (report.staleInstallPurge.removed.length > 0 || report.staleInstallPurge.failed.length > 0)));
146
+ }
147
+ /**
148
+ * True when the RECONCILE repair left a per-version problem a human must fix — an
149
+ * unresolvable managed hook runtime shim, or a hook that could not be re-wired.
150
+ * These gate a sync's `ok`/exit code (the yosemite-s1 class the old
151
+ * `doctor --fix` surfaced).
152
+ *
153
+ * The machine-wide stale-CLI purge is deliberately EXCLUDED: a purge that could
154
+ * not delete a system-wide install (typically `EACCES` on `/usr/local/...`, which
155
+ * needs sudo) is a hygiene issue unrelated to whether the reconcile succeeded, so
156
+ * it must not flip a fleet peer's `agents sync --json` to `ok: false`. Purge
157
+ * failures stay fully visible — in the JSON `repair.staleInstallPurge.failed` and
158
+ * the rendered `hold`/`manual` lines.
159
+ */
160
+ export function repairHadFailures(report) {
161
+ return (report.hookRewire.some((r) => r.failure !== undefined) ||
162
+ report.hookRuntimeRepair.needsAttention.length > 0);
163
+ }
164
+ /**
165
+ * Serialize the repair pass for a `--json` sync payload — the machine surface the
166
+ * deleted `doctor --fix --json` used to carry (heal detail + hook rewire + hook
167
+ * runtime repair + the umbrella stale-CLI purge). Attached under the `repair` key
168
+ * so fleet fan-out sees what the reconcile fixed and whether anything still needs
169
+ * a human (mirror `repairHadFailures`).
170
+ */
171
+ export function repairAfterSyncJson(report) {
172
+ return {
173
+ heal: report.heal,
174
+ hookRewire: report.hookRewire,
175
+ hookRuntimeRepair: report.hookRuntimeRepair,
176
+ staleInstallPurge: report.staleInstallPurge,
177
+ hadFailures: repairHadFailures(report),
178
+ };
179
+ }
180
+ // ─── rendering (human output; sync calls this only when !json && !quiet) ─────
181
+ function renderHealText(result, log) {
182
+ for (const r of result.repairedManifests) {
183
+ log(` ${chalk.green('repair')} plugin ${chalk.bold(r.plugin)} ${chalk.gray(`— dropped invalid ${r.droppedFields.join(', ')} field`)}`);
184
+ }
185
+ for (const r of result.refreshedPlugins) {
186
+ log(` ${chalk.green('refresh')} plugin ${chalk.bold(r.plugin)} ${chalk.gray(`${r.from} → ${r.to}`)}`);
187
+ }
188
+ for (const s of result.skippedPlugins) {
189
+ const why = s.reason === 'modified'
190
+ ? `locally modified — left as-is (run \`agents plugins update ${s.plugin}\` to force)`
191
+ : `no baseline recorded — left as-is (run \`agents plugins update ${s.plugin}\` to adopt)`;
192
+ log(` ${chalk.yellow('hold ')} plugin ${chalk.bold(s.plugin)} ${chalk.gray(`${s.from} → ${s.upstream} available; ${why}`)}`);
193
+ }
194
+ for (const v of result.versions) {
195
+ const label = `${AGENT_NAMES[v.agent] || v.agent}@${v.version}`;
196
+ if (v.healed.length === 0 && v.skipped.length === 0)
197
+ continue;
198
+ const byKind = new Map();
199
+ for (const h of v.healed)
200
+ byKind.set(h.kind, (byKind.get(h.kind) ?? 0) + 1);
201
+ const parts = Array.from(byKind, ([k, n]) => `${n} ${k}`);
202
+ if (v.healed.length > 0) {
203
+ log(` ${chalk.green('fixed ')} ${label} ${chalk.gray(parts.join(', '))}`);
204
+ }
205
+ const drift = v.skipped.filter((s) => s.reason === 'drift');
206
+ const unres = v.skipped.filter((s) => s.reason === 'unreconcilable');
207
+ if (drift.length > 0) {
208
+ log(` ${chalk.yellow('drift ')} ${label} ${chalk.gray(`${drift.length} hand-edited — left as-is (use \`--diff\` to inspect)`)}`);
209
+ }
210
+ if (unres.length > 0) {
211
+ const names = unres.map((s) => `${s.kind}/${s.name}`).join(', ');
212
+ log(` ${chalk.yellow('hold ')} ${label} ${chalk.gray(`${unres.length} couldn't reconcile (${names}) — source/home mismatch the writer can't satisfy`)}`);
213
+ }
214
+ }
215
+ }
216
+ function renderHookRewireText(rewired, log) {
217
+ for (const r of rewired) {
218
+ const label = `${AGENT_NAMES[r.agent] || r.agent}@${r.version}`;
219
+ if (r.failure) {
220
+ log(` ${chalk.red('hold ')} ${label} ${chalk.gray('native hook wiring could not be updated')}`);
221
+ }
222
+ else if (r.remaining === 0) {
223
+ log(` ${chalk.green('rewired')} ${label} ${chalk.gray(`${r.rewired} hook${r.rewired === 1 ? '' : 's'} wired into settings.json`)}`);
224
+ }
225
+ else {
226
+ log(` ${chalk.yellow('hold ')} ${label} ${chalk.gray(`${r.remaining} hook${r.remaining === 1 ? '' : 's'} still unwired — run \`agents sync ${r.agent}@${r.version} --yes\``)}`);
227
+ }
228
+ }
229
+ }
230
+ function renderHookRuntimeRepairText(repair, log) {
231
+ for (const fixed of repair.fixed) {
232
+ log(` ${chalk.green('fixed ')} ${chalk.gray(fixed)}`);
233
+ }
234
+ for (const unresolved of repair.needsAttention) {
235
+ log(` ${chalk.red('hold ')} ${chalk.gray(unresolved)}`);
236
+ }
237
+ }
238
+ function renderStaleInstallPurgeText(purge, log) {
239
+ if (purge.removed.length === 0 && purge.failed.length === 0 && purge.unresolved.length === 0)
240
+ return;
241
+ log(chalk.bold('\nStale agents-cli installs'));
242
+ for (const r of purge.removed) {
243
+ const why = r.reasons.join(', ');
244
+ log(` ${chalk.green('purged')} ${chalk.gray(`${r.packageRoot} ${r.version} (${why})`)}`);
245
+ }
246
+ for (const f of purge.failed) {
247
+ log(` ${chalk.red('hold ')} ${chalk.gray(`${f.packageRoot} ${f.version} — ${f.error}`)}`);
248
+ }
249
+ for (const u of purge.unresolved) {
250
+ log(` ${chalk.yellow('manual')} ${chalk.gray(`${u.packageRoot} ${u.version} — remove it with:`)}`);
251
+ log(` ${chalk.bold(u.manualRemoveCommand)}`);
252
+ }
253
+ }
254
+ /**
255
+ * Print the repair pass's human-readable detail. Sync calls this only on the
256
+ * interactive / non-json path; the header is emitted only when something actually
257
+ * changed, so a clean sync stays quiet.
258
+ */
259
+ export function renderRepairAfterSync(report, log = (s) => console.log(s)) {
260
+ if (!repairChangedAnything(report))
261
+ return;
262
+ renderHealText(report.heal, log);
263
+ renderHookRewireText(report.hookRewire, log);
264
+ renderHookRuntimeRepairText(report.hookRuntimeRepair, log);
265
+ if (report.staleInstallPurge)
266
+ renderStaleInstallPurgeText(report.staleInstallPurge, log);
267
+ }
@@ -52,6 +52,17 @@ export declare function readRouter(name: string, cwd?: string): Router;
52
52
  export declare function writeRouter(router: Router): void;
53
53
  /** Delete a router from the user layer. Returns false if it did not exist there. */
54
54
  export declare function deleteRouter(name: string): boolean;
55
+ /**
56
+ * Rename a router on disk, re-keying the user-layer file and preserving every
57
+ * field (harness allowlists, weights, linked accounts, hijack flag).
58
+ *
59
+ * Throws if `oldName` does not resolve, if it resolves from a non-user layer
60
+ * (renaming would write a shadowed user-layer copy -- see {@link routerSource}),
61
+ * or if `newName` already resolves in any layer. There is no `--force` /
62
+ * overwrite path -- a collision is a hard error directing the user to remove
63
+ * the target first.
64
+ */
65
+ export declare function renameRouter(oldName: string, newName: string): void;
55
66
  /**
56
67
  * Fail-loud token validation (E1 of the Agent Router spec): a router MUST NOT
57
68
  * persist a harness id or model/tier token this machine cannot vouch for.
@@ -76,6 +87,6 @@ export declare function validateRouter(router: Router): void;
76
87
  /**
77
88
  * List every router resolved project > user > system (deduplicated union,
78
89
  * project wins on a name collision -- same precedence as {@link resolveResource}).
79
- * Malformed files are silently skipped, surfaced via `agents route show <name>`.
90
+ * Malformed files are silently skipped, surfaced via `agents route view <name>`.
80
91
  */
81
92
  export declare function listRouters(cwd?: string): Router[];
@@ -89,6 +89,35 @@ export function deleteRouter(name) {
89
89
  fs.unlinkSync(file);
90
90
  return true;
91
91
  }
92
+ /**
93
+ * Rename a router on disk, re-keying the user-layer file and preserving every
94
+ * field (harness allowlists, weights, linked accounts, hijack flag).
95
+ *
96
+ * Throws if `oldName` does not resolve, if it resolves from a non-user layer
97
+ * (renaming would write a shadowed user-layer copy -- see {@link routerSource}),
98
+ * or if `newName` already resolves in any layer. There is no `--force` /
99
+ * overwrite path -- a collision is a hard error directing the user to remove
100
+ * the target first.
101
+ */
102
+ export function renameRouter(oldName, newName) {
103
+ validateRouterName(newName);
104
+ const source = routerSource(oldName);
105
+ if (source === null) {
106
+ throw new Error(`Router '${oldName}' not found.`);
107
+ }
108
+ if (source !== 'user') {
109
+ throw new Error(`Router '${oldName}' resolves from the '${source}' layer, not 'user' -- ` +
110
+ `agents route rename can only rename a user-layer router. Rename its file directly, ` +
111
+ `or create a user-layer router under a different name.`);
112
+ }
113
+ if (routerExists(newName)) {
114
+ throw new Error(`Router '${newName}' already exists; remove it first.`);
115
+ }
116
+ const router = readRouter(oldName);
117
+ router.name = newName;
118
+ writeRouter(router);
119
+ deleteRouter(oldName);
120
+ }
92
121
  /**
93
122
  * Fail-loud token validation (E1 of the Agent Router spec): a router MUST NOT
94
123
  * persist a harness id or model/tier token this machine cannot vouch for.
@@ -137,7 +166,7 @@ export function validateRouter(router) {
137
166
  /**
138
167
  * List every router resolved project > user > system (deduplicated union,
139
168
  * project wins on a name collision -- same precedence as {@link resolveResource}).
140
- * Malformed files are silently skipped, surfaced via `agents route show <name>`.
169
+ * Malformed files are silently skipped, surfaced via `agents route view <name>`.
141
170
  */
142
171
  export function listRouters(cwd) {
143
172
  const routers = [];
@@ -1025,10 +1025,16 @@ export function validateJob(config) {
1025
1025
  if (config.remoteCwd !== undefined && strategy !== 'host' && strategy !== 'fleet') {
1026
1026
  errors.push('remoteCwd only applies to host/fleet-placed routines — set hostStrategy: host|fleet, or drop it');
1027
1027
  }
1028
- if (config.project !== undefined && (typeof config.project !== 'string' || config.project.trim() === '')) {
1028
+ if (config.project === null) {
1029
+ errors.push('project is null — quote YAML values that look like null literals');
1030
+ }
1031
+ else if (config.project !== undefined && (typeof config.project !== 'string' || config.project.trim() === '')) {
1029
1032
  errors.push('project (the singular execution anchor) must be a non-empty project name');
1030
1033
  }
1031
- if (config.cwd !== undefined && (typeof config.cwd !== 'string' || config.cwd.trim() === '')) {
1034
+ if (config.cwd === null) {
1035
+ errors.push('cwd is null — a bare ~ is YAML null; quote it as "~" for the home directory');
1036
+ }
1037
+ else if (config.cwd !== undefined && (typeof config.cwd !== 'string' || config.cwd.trim() === '')) {
1032
1038
  errors.push('cwd (the portable execution directory) must be a non-empty path string');
1033
1039
  }
1034
1040
  // `remoteCwd` is the legacy host-placement path; `cwd` is its canonical
@@ -432,6 +432,19 @@ export interface ActiveSession {
432
432
  * session (PHNX-3688). Absent for non-tmux rows.
433
433
  */
434
434
  tmuxName?: string;
435
+ /**
436
+ * Daemon-computed 1–2 line goal from the session's first user turn (PHNX-3939).
437
+ * Produced off the request path by the background `SessionSummarizerService`
438
+ * and merged from the `session_summaries` cache in {@link applyImmutableMemo};
439
+ * rides the `sessions watch` stream free via the `...row` spread.
440
+ */
441
+ goal?: string;
442
+ /** Daemon-computed progress checkpoints, newest last (PHNX-3939). */
443
+ checkpoints?: import('./types.js').SessionCheckpoint[];
444
+ /** Daemon-computed detailed checklist for the session (PHNX-3939). */
445
+ summaryChecklist?: import('./types.js').SessionChecklistItem[];
446
+ /** Lifecycle of the daemon-computed summary; `skipped` when disabled (PHNX-3939). */
447
+ summaryState?: import('./types.js').SummaryState;
435
448
  }
436
449
  export declare function activeStatusFromCloudStatus(status: CloudTaskStatus): ActiveStatus;
437
450
  interface ActiveQueryOptions {
@@ -7,7 +7,7 @@
7
7
  * BM25 ranking, and label-first search for /rename'd sessions.
8
8
  */
9
9
  import Database from '../sqlite.js';
10
- import type { SessionAgentId, SessionEvent, SessionMeta } from './types.js';
10
+ import type { SessionAgentId, SessionCheckpoint, SessionChecklistItem, SessionEvent, SessionMeta, SummaryState } from './types.js';
11
11
  import { type IndexedToolCall } from './tool-calls.js';
12
12
  import { type ToolScanResumePoint } from './tool-store.js';
13
13
  /** Current schema version; bumped when migrations are added. Exported so tests
@@ -43,6 +43,8 @@ export declare const INSIGHTS_EXTRACTOR_VERSION = 7;
43
43
  export declare const SESSION_TOPIC_EXTRACTOR_VERSION = 2;
44
44
  /** Bump when classifyPhenotype's output changes so cached phenotypes recompute (PHNX-3327 v1). */
45
45
  export declare const SESSION_PHENOTYPE_EXTRACTOR_VERSION = 1;
46
+ /** Bump when the summarizer output shape changes so cached summaries recompute (PHNX-3939 v1). */
47
+ export declare const SESSION_SUMMARY_EXTRACTOR_VERSION = 1;
46
48
  /** File stat snapshot used to detect changes between scan runs. */
47
49
  export interface ScanStamp {
48
50
  fileMtimeMs: number;
@@ -337,8 +339,9 @@ export declare function countSessions(options?: QueryOptions): number;
337
339
  /** One grouped row in a cost/duration rollup. */
338
340
  interface UsageRollupRow {
339
341
  /**
340
- * Grouping key value: the agent id, project name, ISO date (YYYY-MM-DD), or
341
- * account identity (`claude:org=<uuid>` / `unattributed:<reason>`).
342
+ * Grouping key value: the agent id, project name, shortened model id,
343
+ * ISO date (YYYY-MM-DD), or account identity
344
+ * (`claude:org=<uuid>` / `unattributed:<reason>`).
342
345
  */
343
346
  key: string;
344
347
  /**
@@ -443,6 +446,38 @@ export declare function readSessionContent(id: string): string | undefined;
443
446
  * survives for this session.
444
447
  */
445
448
  export declare function readArchivedSessionPreview<T>(id: string): T | undefined;
449
+ /** The daemon-computed summary stored in `session_summaries` (PHNX-3939). */
450
+ export interface SessionSummaryEntry {
451
+ goal?: string;
452
+ checkpoints?: SessionCheckpoint[];
453
+ summaryChecklist?: SessionChecklistItem[];
454
+ summaryState: SummaryState;
455
+ }
456
+ /**
457
+ * Read one summary only when it matches the transcript bytes on disk (the
458
+ * reuse-or-recompute stamp the SessionSummarizerService gates on). Mirrors
459
+ * {@link readSessionPreviewCache}.
460
+ */
461
+ export declare function readSessionSummary(id: string, sourceStamp: {
462
+ fileMtimeMs: number | null;
463
+ fileSize: number | null;
464
+ }): SessionSummaryEntry | undefined;
465
+ /**
466
+ * Read the latest stored summary by session id, regardless of the transcript
467
+ * stamp — the low-cost merge read used by the display path (a live row through
468
+ * {@link applyImmutableMemo} or a history/mirror row) to surface the
469
+ * last-computed summary without a model call. A slightly-stale summary is the
470
+ * best available until the service recomputes; the SERVICE uses the stamped
471
+ * {@link readSessionSummary} to decide whether to recompute.
472
+ */
473
+ export declare function readSessionSummaryAny(id: string): SessionSummaryEntry | undefined;
474
+ /** Persist a computed summary against the exact transcript bytes it was derived from. */
475
+ export declare function writeSessionSummary(entry: {
476
+ id: string;
477
+ fileMtimeMs: number | null;
478
+ fileSize: number | null;
479
+ summary: SessionSummaryEntry;
480
+ }): void;
446
481
  /** A local-origin session, projected to the compact fields the fleet mirror publishes. */
447
482
  export interface LocalMirrorSource {
448
483
  id: string;
@@ -458,6 +493,8 @@ export interface LocalMirrorSource {
458
493
  timestamp: string;
459
494
  ticketId: string | null;
460
495
  prUrl: string | null;
496
+ /** Daemon-computed summary (PHNX-3939), so peers carry it without a transcript. */
497
+ summary: SessionSummaryEntry | null;
461
498
  }
462
499
  /**
463
500
  * The most-recently-active sessions whose transcript is genuinely local to this
@@ -482,6 +519,8 @@ export interface MirrorSessionUpsert {
482
519
  timestamp: string;
483
520
  ticketId?: string | null;
484
521
  prUrl?: string | null;
522
+ /** Daemon-computed summary carried from the publishing peer (PHNX-3939). */
523
+ summary?: SessionSummaryEntry | null;
485
524
  }
486
525
  /**
487
526
  * Write one peer session's digest into the local `sessions` index as a mirror
@@ -504,7 +543,7 @@ export declare function upsertMirrorSession(row: MirrorSessionUpsert, source: st
504
543
  export declare function pruneMirrorSessions(cutoffMs: number): number;
505
544
  /** Plugin provenance already indexed for resources used by one session. */
506
545
  export declare function getSessionPlugins(id: string): string[];
507
- export type UsageRollupGroup = 'agent' | 'project' | 'day' | 'account';
546
+ export type UsageRollupGroup = 'agent' | 'project' | 'day' | 'model' | 'account';
508
547
  /**
509
548
  * Smart-launch affinity priors: group sessions by origin machine, harness, or
510
549
  * joint (machine + agent). Ordered by launch count desc.
@@ -543,9 +582,10 @@ export declare function queryAffinityRollup(options: {
543
582
  }): AffinityRow[];
544
583
  /**
545
584
  * Aggregate cost / duration / tokens across sessions, grouped by agent,
546
- * project, or calendar day. Honors the same filter shape as querySessions
547
- * (agent, since/until, team-origin) so `agents insights cost --since 7d --by day`
548
- * lines up with what `agents sessions` would list. Ordered by cost desc.
585
+ * project, shortened model id, account, or calendar day. Honors the same
586
+ * filter shape as querySessions (agent, since/until, team-origin) so
587
+ * `agents insights cost --since 7d --by day` lines up with what
588
+ * `agents sessions` would list. Ordered by cost desc.
549
589
  */
550
590
  export declare function queryUsageRollup(options: QueryOptions & {
551
591
  groupBy: UsageRollupGroup;