@phnx-labs/agents-cli 1.22.6 → 1.22.7

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 (77) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +7 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.js +35 -4
  5. package/dist/commands/feed.d.ts +2 -1
  6. package/dist/commands/feed.js +47 -20
  7. package/dist/commands/focus.js +22 -1
  8. package/dist/commands/models.js +2 -2
  9. package/dist/commands/projects.js +122 -107
  10. package/dist/commands/secrets.js +1 -1
  11. package/dist/commands/sessions-backfill.d.ts +33 -0
  12. package/dist/commands/sessions-backfill.js +83 -1
  13. package/dist/commands/sessions-stats.d.ts +36 -0
  14. package/dist/commands/sessions-stats.js +263 -0
  15. package/dist/commands/sessions.d.ts +1 -1
  16. package/dist/commands/sessions.js +21 -1
  17. package/dist/commands/view.d.ts +2 -1
  18. package/dist/commands/view.js +62 -11
  19. package/dist/index.js +1 -2
  20. package/dist/lib/activity.js +2 -2
  21. package/dist/lib/agents.js +68 -11
  22. package/dist/lib/analytics/recipes.js +11 -5
  23. package/dist/lib/browser/profiles.d.ts +15 -7
  24. package/dist/lib/browser/profiles.js +53 -12
  25. package/dist/lib/byok-usage.d.ts +38 -0
  26. package/dist/lib/byok-usage.js +117 -0
  27. package/dist/lib/capabilities.js +1 -1
  28. package/dist/lib/exec.d.ts +20 -0
  29. package/dist/lib/exec.js +73 -8
  30. package/dist/lib/feed-outcome.d.ts +1 -0
  31. package/dist/lib/feed-outcome.js +2 -0
  32. package/dist/lib/feed-post.js +1 -1
  33. package/dist/lib/feed-ranking.d.ts +1 -0
  34. package/dist/lib/feed-ranking.js +4 -0
  35. package/dist/lib/feed.d.ts +4 -1
  36. package/dist/lib/feed.js +25 -0
  37. package/dist/lib/hosts/passthrough.js +0 -1
  38. package/dist/lib/mcp.js +6 -1
  39. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  40. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  41. package/dist/lib/model-tiers.js +4 -1
  42. package/dist/lib/models.js +63 -0
  43. package/dist/lib/profiles.d.ts +42 -1
  44. package/dist/lib/profiles.js +50 -2
  45. package/dist/lib/project-focus.d.ts +9 -0
  46. package/dist/lib/project-focus.js +23 -0
  47. package/dist/lib/project-key.d.ts +1 -1
  48. package/dist/lib/project-key.js +1 -1
  49. package/dist/lib/project-probe.d.ts +18 -0
  50. package/dist/lib/project-probe.js +46 -0
  51. package/dist/lib/project-status.d.ts +56 -0
  52. package/dist/lib/project-status.js +125 -18
  53. package/dist/lib/resources/mcp.js +3 -0
  54. package/dist/lib/resources/types.d.ts +1 -1
  55. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  56. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  57. package/dist/lib/secrets/audit.js +1 -1
  58. package/dist/lib/secrets/index.d.ts +1 -0
  59. package/dist/lib/secrets/index.js +29 -15
  60. package/dist/lib/secrets/remote.d.ts +1 -1
  61. package/dist/lib/secrets/remote.js +1 -1
  62. package/dist/lib/session/active.d.ts +2 -0
  63. package/dist/lib/session/bash-command.d.ts +2 -3
  64. package/dist/lib/session/db.d.ts +92 -1
  65. package/dist/lib/session/db.js +230 -1
  66. package/dist/lib/session/digest.d.ts +1 -1
  67. package/dist/lib/session/digest.js +1 -1
  68. package/dist/lib/share/publish.js +24 -0
  69. package/dist/lib/startup/command-registry.d.ts +0 -1
  70. package/dist/lib/startup/command-registry.js +0 -2
  71. package/dist/lib/subagents-registry.js +3 -0
  72. package/dist/lib/types.d.ts +11 -2
  73. package/dist/lib/usage.d.ts +5 -0
  74. package/dist/lib/usage.js +3 -3
  75. package/package.json +1 -1
  76. package/dist/commands/activity.d.ts +0 -87
  77. package/dist/commands/activity.js +0 -346
package/CHANGELOG.md CHANGED
@@ -1,9 +1,95 @@
1
1
  # Changelog
2
2
 
3
- ## 1.22.6
3
+ ## 1.22.7
4
+
5
+ - **`agents feed --project <name>` scopes the whole feed to one project.** Open
6
+ blocks, the updates view (`--filter updates`), and the trailing activity lane
7
+ are all filtered to the requested repo/project using the same worktree-aware
8
+ project key as `agents perf` (`lib/project-key.ts`). The masthead becomes
9
+ `<project> needs you` / `<project> updates`. Filtering is applied locally after
10
+ the fleet fan-out, so older peers that do not recognize `--project` still
11
+ contribute correctly. Source: `apps/cli/src/commands/feed.ts`,
12
+ `apps/cli/src/lib/feed-ranking.ts`.
13
+
14
+ - **Feed blocks are now stamped with their project.** The `feed-publish` hook
15
+ derives project from the session cwd, and `agents feed post --blocked` stamps it
16
+ on the declared block. Live-session enrichment backfills `project` onto older
17
+ blocks that lack it. Source: `apps/cli/src/lib/feed.ts`,
18
+ `apps/cli/src/lib/feed-outcome.ts`, `apps/cli/src/lib/session/active.ts`.
19
+
20
+ - **`agents activity` is removed.** The standalone milestone timeline is gone;
21
+ its stream is now read through `agents feed --filter all` (blocks + updates) or
22
+ `agents feed --filter updates` (updates only). `activity --project <name>` is
23
+ replaced by `feed --project <name>`. Source: `apps/cli/src/index.ts`,
24
+ `apps/cli/src/startup/command-registry.ts`, `apps/cli/src/commands/activity.ts`
25
+ (deleted), `apps/cli/docs/06-observability.md`,
26
+ `apps/cli/docs/11-projects.md`.
27
+
28
+ - **`agents browser start` no longer fails with "Custom binary not found" when the `default` profile came from another OS.** `~/.agents/agents.yaml` syncs across the fleet, so a `default` profile auto-created on macOS carried a `/Applications/Google Chrome.app/...` binary path that doesn't exist on a Linux box — a bare `browser start` there died with `Custom binary not found`, the top browser roadblock (one session burned six commands working around it). `ensureDefaultBrowserProfile` now validates that the resolved default can actually launch on THIS machine and, if its browser/binary is missing, regenerates the `default` from the installed-browser auto-detect instead of handing back the broken profile. A configured default (`profiles set-default`) that can't launch here warns and falls through to auto-detect; remote (`ssh://`) defaults skip the local binary check since their browser lives on the far host. Source: `apps/cli/src/lib/browser/profiles.ts`, `apps/cli/docs/browser.md`.
29
+
30
+ - **The stray "Agents CLI needs to authenticate to continue" Touch ID sheet now actually heals on an already-hashed machine (SEC-13/#1938 follow-up).** 1.22.5 added a one-time no-ACL re-store for a stale-ACL'd `agents-cli.hmackey` item — the internal HMAC key read *before every hashed keychain lookup*, whose damaged copy pops a generic, context-less Touch ID sheet on nearly any command that touches secrets. But it wired the heal only into `maybeAutoRekey`, which is bypassed for the hmackey and hashed-name lookups themselves (`prepareServiceName` returns early for `HMAC_KEY_ITEM` before `maybeAutoRekey` runs). So the exact hot paths that read the key — the `agents devices list` stats probe a SessionStart hook runs, and every background hashed read — never triggered the heal, and an already-migrated machine prompted forever. The documented `agents secrets rekey` remedy is also a no-op on such a machine: with no cleartext names left to re-key, it returns without re-storing the key. The heal now runs on the read path itself (`readHmacKeyRecord`): the first hashed lookup in the first process re-stores the record no-ACL exactly once (guarded by `healedNoAcl`, so it never churns the keychain afterward) — one last prompt on the read that heals it, then silent forever, on every path. Source: `apps/cli/src/lib/secrets/index.ts`.
31
+
32
+ - **Add Pi (Oh My Pi, `omp`) as a native harness.** agents-cli now installs, runs, and
33
+ syncs resources for [Oh My Pi](https://omp.sh) (`@oh-my-pi/pi-coding-agent`, binary
34
+ `omp`) under id `pi`. Pi is a Bun-based, terminal-first, multi-provider coding agent;
35
+ its cross-provider model catalog (OpenRouter, OpenAI, Anthropic, xAI, DeepSeek, …)
36
+ surfaces in `agents view` and `agents models pi` via `omp models --json`. It is
37
+ Claude-compatible: MCP (`.mcp.json`, stdio + http + headers), skills, file commands, and
38
+ Claude-shaped subagents all sync into `~/.omp/agent/`. Hooks, allowlist, and plugins are
39
+ intentionally off (omp's hook/approval/plugin models don't map to agents-cli's).
40
+ Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/exec.ts`, `apps/cli/src/lib/models.ts`.
41
+
42
+ - **`agents run <profile> --model <tier>` now resolves the cost tier against the profile's own harness, not its host's.** A custom-harness profile (e.g. a DeepSeek model routed through the `claude` host binary) resolved a tier token (`cheap|default|best|ultra`) by calling `resolveTier(options.agent, ...)` with `options.agent` already overwritten to the HOST agent's id — so `best` resolved against Claude's own catalog and could push a real Claude model id as the `--model` flag, clobbering the profile's own `ANTHROPIC_MODEL` env value. Profiles can now declare a `models:` block (per-tier model ids for the harness's own catalog); a requested tier resolves against it first — clamping an unset tier down to the next cheaper one that IS set — and the concrete id is substituted into both the env and the forwarded `--model` value before exec.ts's native (host-catalog) tier logic ever runs. A profile with no `models:` configured degrades gracefully to today's behavior — the harness's single pinned model, with an informational note instead of an error. Source: `apps/cli/src/lib/profiles.ts`, `apps/cli/src/commands/exec.ts`.
43
+
44
+ - **`agents projects status` card: host-grouped agents, focus units, and a warnings footer.** Live agents render under `@host` rows so the same harness on two machines is not collapsed into one cell. Focus counts are labeled `file-touches (Nd)` instead of bare integers. Repo drift, dirty trees, missing checkouts, slug mismatch, unmeasurable schedule, and crash piles land at the bottom with 🔴 critical / ⚠️ continue. Local workspace probe always feeds the footer (full fleet table still requires `--fleet`). Source: `apps/cli/src/lib/project-status.ts`, `project-focus.ts`, `project-probe.ts`, `commands/projects.ts`.
45
+
46
+ - **`agents projects status` and `view`/`show` share one body.** Named form is the full card (every milestone + definition); unnamed is the multi-project rollup. No second implementation to drift. Source: `apps/cli/src/commands/projects.ts`.
47
+
48
+ - **`agents sessions --help` and `05-sessions.md` now teach one session-lifecycle
49
+ matrix.** `focus` / `focus --attach-only` / `detach` / `attach` / `resume` are
50
+ listed as distinct intents (not synonyms), so operators stop guessing among
51
+ `go` / `focus` / `attach` / `resume`. Source: `apps/cli/src/commands/sessions.ts`,
52
+ `apps/cli/src/commands/focus.ts`, `apps/cli/docs/05-sessions.md`.
4
53
 
5
54
  - **Cost tiers are ignored (with a clear warning) for profile runs.** A profile's model comes from its endpoint (e.g. Kimi/DeepSeek/GLM via `agents run <profile>`), not the host harness's catalog — so passing `--model cheap|default|best|ultra` to a profile used to resolve against the *host* harness and forward an incompatible model id to the profile's endpoint. Now a tier on a profile run is discarded with a standout warning and the profile's configured model is used. Concrete `--model <id>` on a profile is unchanged. Source: `apps/cli/src/commands/exec.ts`.
6
55
 
56
+ - **`agents view` harness rows now lead with the version number.** Custom harness rows previously showed `via <host> <version>` — the host CLI name came first, which buried the version in the middle of the line. The format is now `<version> (forked from <host>)` for pinned harnesses and `<version> (forked from <host>, tracks default)` for unpinned ones that follow the host's global default. The `tracks default` label is shown in green so it stands out at a glance.
57
+
58
+ - **Chained fork lineage in harness headers.** When a custom harness is itself a fork of another custom harness (which in turn forks a native host), the block header now shows the full two-hop chain: `custom · forked from <intermediate> -> <native-host>`. Single-hop forks continue to show `custom · forked from <parent>`.
59
+
60
+ - **BYOK budget bar in `agents view`.** Custom harnesses backed by an OpenRouter key now show a live spend bar (amount used, remaining, and limit) inline on the model/auth row. Keys are deduplicated so multiple harnesses sharing the same keychain entry trigger exactly one API call. The bar is rendered only when a budget is available; harnesses without a BYOK key are unaffected. Source: `apps/cli/src/lib/byok-usage.ts`, `apps/cli/src/commands/view.ts`.
61
+
62
+ - **`agents run auto` with no prompt no longer silently attaches a dead pane or leaves an orphan session (RUSH-2185 / EXEC-23a).** Three latent bugs combined to produce this failure when `auto` picked a harness like `cursor-agent` that exits immediately without a prompt: (F1) the auto-picker had no gate for whether a harness can open a bare interactive REPL — `cursor-agent` was a valid candidate even though its CLI requires a prompt and exits on `argv = []`; (F2) `surfacePaneFailure` was guarded by `status !== 0`, so a clean exit-0 death produced only a bare `[detached]` line with no diagnostic; (F3) the "pane still alive → keep session" fall-through relied on `paneExitStatus` returning `{dead:false}`, which it also returns on any query error (a race right after the pane-died hook), leaving the session alive as an orphan. Fixed: (F1) a new `interactiveRepl` capability bit in `AgentConfig.capabilities` marks every harness; `auto` now filters to REPL-capable candidates before picking, and fails loud naming the installed harnesses when none qualify; (F2) `shouldRecapDeadPane(status, interactive)` surfaces the pane tail any time the run is interactive, regardless of exit code; (F3) `isPaneKnownAliveFromQueryResult(code, stdout)` is now required as positive proof before keeping a session — an ambiguous result tears the session down via `killSession` instead. Source: `apps/cli/src/lib/exec.ts`, `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/types.ts`, `apps/cli/src/lib/capabilities.ts`, `apps/cli/src/commands/exec.ts`, `apps/cli/docs/specifications.md` (EXEC-23a).
63
+
64
+ - **`agents run auto` can pick the Pi (`omp`) harness for prompt-less interactive runs again, and `main` builds green.** The `interactiveRepl` capability bit added by the RUSH-2185 / EXEC-23a fix landed at the same time as the new Pi harness, and neither change saw the other — so `pi` was the one agent in `AGENTS` that never declared the bit, and the completeness test that pins the registry to the capability list went red on `main` (`pi missing interactiveRepl`). Pi now declares `interactiveRepl: true`: bare `omp` runs the TUI, and `omp -p` is the one-shot form that answers a prompt and exits.
65
+
66
+ - **`projects status --fleet` no longer labels this box `@local`.** Local sessions from `getActiveSessions()` lacked `machine`; host-grouped agents stamped remotes only. Locals are now filled with `machineId()` before rollup so the agents roster and fleet lines agree. Source: `apps/cli/src/lib/project-status.ts`, `commands/projects.ts`.
67
+
68
+ - **`agents sessions stats` — which skills/commands you actually invoke, and which are dead weight.** A cheap, db-backed rollup of `session_resource_usage` (the skill/`Skill`-tool + slash-command tallies already recorded at index time), joined to `sessions` for attribution so `--agent`/`--project`/`--since`/`--machine` narrow the window and `--kind`/`--plugin` narrow the resources. A both-ends view: the most-invoked resources (`--bottom` flips to least-invoked, `--top <n>` caps), and the installed-but-never-invoked ones (cross-referenced against `listResources`/`discoverPlugins`) — the productized form of a manual transcript-scan audit. `--json` emits a versioned `sessions-stats` envelope. The signal captures EXPLICIT invocations only (slash commands + `Skill` tool calls) — an auto-triggered skill emits no event and reads as 0, and only Claude transcripts expose the signal today; both caveats are surfaced in help and output. A new `agents sessions backfill resources` folds historical sessions (indexed before the signal shipped) into the usage index, re-parsing each transcript from byte 0, gated by a new `resource_scan_ledger` (schema v31) so reruns skip completed transcripts — mirroring `agents sessions backfill tools`. Source: `apps/cli/src/commands/sessions-stats.ts`, `apps/cli/src/commands/sessions-backfill.ts`, `apps/cli/src/lib/session/db.ts`, `apps/cli/docs/05-sessions.md`, `apps/cli/docs/specifications.md` (SES-IF-4b).
69
+
70
+ - **`agents share` serves screenshots and recordings with a real content-type.**
71
+ Publishing a PNG/JPEG/GIF/WebP/AVIF image, an MP4/MOV/WebM video, or a PDF now
72
+ sets the matching `content-type` instead of `application/octet-stream`. GitHub's
73
+ image proxy (camo) only renders an inline `![](url)` when the asset is served as
74
+ a real image/video type, so this is what lets an agent drop a screenshot or a
75
+ screen recording straight into a PR body via `agents share <file>`. HTML, SVG,
76
+ CSS, JS, JSON, and text were already typed correctly. Source:
77
+ `apps/cli/src/lib/share/publish.ts`.
78
+
79
+ - **`agents trends tools-per-session` now counts every scanned session, not just `agents teams`
80
+ runs.** The recipe read `sessions.tool_call_count`, a column nothing populates except the
81
+ teams summarizer (`apps/cli/src/lib/teams/summarizer.ts`) — the general session indexer never
82
+ computes it. So every session that did not come from a team was scored 0 or excluded outright
83
+ by `WHERE tool_call_count IS NOT NULL`, pinning the fleet-wide p50 at 0 however many tools ran
84
+ and leaving only `claude` in the table. It now reads `tool_scan_ledger.call_count`, the
85
+ per-session count the tool indexer writes for every session it scans — the same index behind
86
+ `agents sessions --include tools`, so the two surfaces stop disagreeing. Sessions with
87
+ genuinely zero tool calls still count as 0 instead of vanishing. On a real 7-day window this
88
+ took the sample from 400 to 570 sessions and surfaced `grok`, `rush`, `codex`, `kimi`,
89
+ `droid` and `antigravity`, none of which had ever appeared. Run `agents sessions backfill
90
+ tools` once if historical sessions were never indexed. Source:
91
+ `apps/cli/src/lib/analytics/recipes.ts`.
92
+
7
93
  ## 1.22.5
8
94
 
9
95
  - **`agents events` can now filter by `--session <id>` and `--bundle <name>` — trace which agent/session triggered a secret access.** Every event already carries the provenance `sessionId`, and secrets events carry the `bundle` in their payload, but neither was queryable: you could see *that* the `share` bundle was read, not *which session* read it. `--session` (wired to the engine's existing `sessionId` filter) and `--bundle` (a new payload filter across both the operational log and the activity stream) close that gap. `agents events --module secrets --bundle share --session <id>` answers "which agent read the share bundle" — the attribution the Touch ID storm investigation needed, since the macOS biometric sheet itself emits no event. Source: `apps/cli/src/lib/event-stream.ts`, `apps/cli/src/commands/events.ts`, `apps/cli/docs/06-observability.md`.
package/README.md CHANGED
@@ -31,6 +31,8 @@
31
31
  <a href="https://x.ai" title="Grok Build (xAI)"><strong>Grok</strong></a>
32
32
  &nbsp;&nbsp;&nbsp;&nbsp;
33
33
  <a href="https://factory.ai" title="Factory AI Droid"><strong>Droid</strong></a>
34
+ &nbsp;&nbsp;&nbsp;&nbsp;
35
+ <a href="https://omp.sh" title="Oh My Pi"><strong>Pi</strong></a>
34
36
  </p>
35
37
 
36
38
  https://agents-cli.sh/demo.mp4
@@ -270,6 +272,11 @@ agents sessions --include tools --query 'program:git' --count --fleet --json
270
272
 
271
273
  # Populate historical tool rows once on each device
272
274
  agents sessions backfill tools --fleet
275
+
276
+ # Which skills/commands you actually invoke -- and which installed ones are dead weight
277
+ agents sessions stats
278
+ agents sessions stats --zero # only the never-invoked (dead weight)
279
+ agents sessions backfill resources # fold historical sessions into the usage index
273
280
  ```
274
281
 
275
282
  Interactive picker when you're in a terminal. Structured output (`--json`, `--markdown`, filtered by role or turn count) when piped.
package/dist/bin/agents CHANGED
Binary file
@@ -1568,7 +1568,7 @@ export function registerRunCommand(program) {
1568
1568
  await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
1569
1569
  process.exit(resumeExit);
1570
1570
  }
1571
- const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveHostSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES, collectHarnessCandidates, pickHarnessWeighted, classifyHarnessCandidates, formatHarnessPickBanner, formatNoHealthyHarnessError, formatNoHealthyAccountError }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports }, { shareRuntimeEnv },] = await Promise.all([
1571
+ const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveHostSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES, collectHarnessCandidates, pickHarnessWeighted, classifyHarnessCandidates, formatHarnessPickBanner, formatNoHealthyHarnessError, formatNoHealthyAccountError }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports, capableAgents }, { shareRuntimeEnv },] = await Promise.all([
1572
1572
  import('../lib/exec.js'),
1573
1573
  import('../lib/agents.js'),
1574
1574
  import('../lib/profiles.js'),
@@ -1629,9 +1629,24 @@ export function registerRunCommand(program) {
1629
1629
  // — launching a default "because it's there" is how a rotate loop
1630
1630
  // hammers an exhausted account.
1631
1631
  const byHarness = await collectHarnessCandidates();
1632
- const harnessPick = pickHarnessWeighted(byHarness);
1632
+ // F1 (RUSH-2185 / EXEC-23a): a prompt-less run is interactive; only
1633
+ // harnesses that can open a REPL with no argv are valid candidates.
1634
+ // cursor-agent and similar exit immediately without a prompt, which
1635
+ // leaves a silent [detached] pane and an orphan session.
1636
+ const interactive = prompt === undefined && options.headless !== true;
1637
+ const replCapable = interactive ? new Set(capableAgents('interactiveRepl')) : null;
1638
+ const candidateHarness = replCapable
1639
+ ? new Map([...byHarness].filter(([id]) => replCapable.has(id)))
1640
+ : byHarness;
1641
+ const harnessPick = pickHarnessWeighted(candidateHarness);
1633
1642
  if (!harnessPick) {
1634
- console.error(chalk.red(formatNoHealthyHarnessError(classifyHarnessCandidates(byHarness))));
1643
+ if (replCapable && byHarness.size > 0 && candidateHarness.size === 0) {
1644
+ const installed = [...byHarness.keys()].join(', ');
1645
+ console.error(chalk.red(`No installed harness supports a prompt-less interactive REPL (installed: ${installed}). Pass a prompt (-p) or install claude, codex, or another REPL-capable harness.`));
1646
+ }
1647
+ else {
1648
+ console.error(chalk.red(formatNoHealthyHarnessError(classifyHarnessCandidates(candidateHarness))));
1649
+ }
1635
1650
  process.exit(1);
1636
1651
  }
1637
1652
  agent = harnessPick.picked.agent;
@@ -1653,7 +1668,7 @@ export function registerRunCommand(program) {
1653
1668
  // so Chinese models (Kimi, DeepSeek, Qwen, GLM) can run inside
1654
1669
  // Claude Code without a local proxy.
1655
1670
  try {
1656
- const resolved = resolveProfileForRun(rawAgent);
1671
+ const resolved = resolveProfileForRun(rawAgent, options.model);
1657
1672
  agent = resolved.agent;
1658
1673
  if (!version)
1659
1674
  version = resolved.version;
@@ -1661,6 +1676,22 @@ export function registerRunCommand(program) {
1661
1676
  profileFallbackModel = resolved.fallbackModel;
1662
1677
  fromProfile = true;
1663
1678
  process.stderr.write(chalk.gray(`Resolved profile '${resolved.profileName}' -> ${agent}${version ? `@${version}` : ''}\n`));
1679
+ if (resolved.tierNote) {
1680
+ process.stderr.write(chalk.gray(`[agents] ${resolved.tierNote}\n`));
1681
+ }
1682
+ // A tier token (cheap/default/best/ultra) already resolved against
1683
+ // this PROFILE's own `models:` map above, when the profile opts in.
1684
+ // Replace the raw --model value here so the tier never reaches the
1685
+ // native, HOST-catalog tier block below. When the profile has no
1686
+ // `models:` opt-in at all, resolvedModel stays undefined and
1687
+ // options.model is left as the raw tier token on purpose — the
1688
+ // "cost tiers don't apply to profile ..." discard guard further
1689
+ // down this function is the canonical fallback for that case, and
1690
+ // this block must not race it with a second, differently-worded
1691
+ // message.
1692
+ if (resolved.resolvedModel !== undefined) {
1693
+ options.model = resolved.resolvedModel;
1694
+ }
1664
1695
  }
1665
1696
  catch (err) {
1666
1697
  console.error(chalk.red(err.message));
@@ -13,7 +13,7 @@
13
13
  * `agents feed post`: agent-callable free-text progress into the activity
14
14
  * stream (milestone `status.posted`). Session/agent/host/runtime/pid identity
15
15
  * is auto-stamped from env + the pid registry — no domain-specific flags.
16
- * Humans watch via the feed activity lane / `agents activity`.
16
+ * Humans watch via the feed activity lane.
17
17
  */
18
18
  import type { Command } from 'commander';
19
19
  import { type OpenBlock } from '../lib/feed.js';
@@ -47,6 +47,7 @@ export declare function formatOutcomeHeader(group: OutcomeGroup): string;
47
47
  export declare function sessionHintsFromActive(sessions: Array<{
48
48
  sessionId?: string;
49
49
  agentId?: string;
50
+ cwd?: string;
50
51
  ticket?: {
51
52
  id?: string;
52
53
  };
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified, buildDeclaredBlock, publishBlock, } from '../lib/feed.js';
3
3
  import { ensureActivityLogHook, readRecentActivity, formatActivityLine, formatProgressUpdate, mergeActivityEvents, parseActivityPayload, } from '../lib/activity.js';
4
+ import { projectKeyFromCwd } from '../lib/project-key.js';
4
5
  import { postFeedStatus } from '../lib/feed-post.js';
5
6
  import { parseFeedPostLevel, planFeedBroadcast, runFeedBroadcast, effectiveBroadcastConfig, blockBroadcastContext, blockDeliveryFailure, } from '../lib/feed-broadcast.js';
6
7
  import { getSessionById } from '../lib/session/db.js';
@@ -215,6 +216,7 @@ export function sessionHintsFromActive(sessions) {
215
216
  prNumber: s.pr?.number,
216
217
  prUrl: s.pr?.url,
217
218
  worktreeSlug: s.worktree?.slug,
219
+ project: s.cwd ? projectKeyFromCwd(s.cwd) : undefined,
218
220
  }));
219
221
  }
220
222
  export function registerFeedCommand(program) {
@@ -228,6 +230,7 @@ export function registerFeedCommand(program) {
228
230
  .option('--local', 'Only this machine -- skip the cross-machine SSH fan-out')
229
231
  .option('-H, --host <target...>', 'Scope to remote machine(s) over SSH; repeatable')
230
232
  .option('--device <target...>', 'Alias for --host; repeatable')
233
+ .option('--project <name>', 'Scope the feed to one project/repo (matches cwd basename, case-insensitive)')
231
234
  .option('--dispatch', 'Run stall suppression + default-on-no-answer policy and urgent notifications')
232
235
  .option('--pause <id>', 'Pause a runaway/needy local process (SIGSTOP) or cancel a cloud task')
233
236
  .option('--kill <id>', 'Kill a runaway/needy local process (SIGTERM) or cancel a cloud task');
@@ -317,7 +320,13 @@ docs/06-observability.md.
317
320
  // it the ask would scroll away like any other update.
318
321
  let outcomes;
319
322
  if (flags.blocked) {
320
- const block = buildDeclaredBlock(event, {
323
+ const block = buildDeclaredBlock({
324
+ sessionId: event.sessionId,
325
+ mailboxId: event.mailboxId,
326
+ host: event.host,
327
+ runtime: event.runtime,
328
+ cwd: event.cwd,
329
+ }, {
321
330
  // Prefer title as the front-loaded ask on the phone; body is detail.
322
331
  text: event.title
323
332
  ? (event.detail ? `${event.title}: ${event.detail}` : event.title)
@@ -399,12 +408,12 @@ docs/06-observability.md.
399
408
  if (filter === 'all') {
400
409
  console.log();
401
410
  renderUpdatesView(await gatherStatusPosts({
402
- limit: UPDATES_VIEW_LIMIT, hosts: opts.host, local: opts.local, includeLocal, self,
403
- }));
411
+ limit: UPDATES_VIEW_LIMIT, hosts: opts.host, local: opts.local, includeLocal, self, project: opts.project,
412
+ }), opts.project);
404
413
  return;
405
414
  }
406
415
  if (includeLocal)
407
- renderActivityLane();
416
+ renderActivityLane(opts.project);
408
417
  };
409
418
  // Updates view: deliberate progress posts only (blocks are decisions, not
410
419
  // announcements). Short-circuits the block pipeline — no dispatch policy —
@@ -425,7 +434,7 @@ docs/06-observability.md.
425
434
  console.log(JSON.stringify(updates, null, 2));
426
435
  return;
427
436
  }
428
- renderUpdatesView(updates);
437
+ renderUpdatesView(updates, opts.project);
429
438
  return;
430
439
  }
431
440
  // Active sessions feed both the GC sweep and outcome enrichment (ticket/PR).
@@ -487,7 +496,10 @@ docs/06-observability.md.
487
496
  blocks = mergeFeedBlocks(visibleLocalBlocks, remote.items);
488
497
  }
489
498
  }
490
- blocks = rankFeedBlocks(blocks, localSignals);
499
+ blocks = rankFeedBlocks(blocks, localSignals).filter((b) => blockMatchesProject(b, opts.project));
500
+ const dispatchBlocksProject = opts.project
501
+ ? dispatchBlocks.filter((b) => blockMatchesProject(b, opts.project))
502
+ : dispatchBlocks;
491
503
  const digest = suppressionDigest(preparedLocal.filter);
492
504
  if (digest && !opts.json) {
493
505
  console.log(chalk.dim(digest));
@@ -495,7 +507,7 @@ docs/06-observability.md.
495
507
  if (opts.dispatch) {
496
508
  const policy = loadPolicy();
497
509
  const now = new Date();
498
- for (const b of dispatchBlocks) {
510
+ for (const b of dispatchBlocksProject) {
499
511
  // Wrap per-block policy so one malformed block (e.g. a crafted
500
512
  // mailboxId that throws in mailboxDir) can't abort the whole loop and
501
513
  // strand every remaining block's dispatch.
@@ -540,7 +552,7 @@ docs/06-observability.md.
540
552
  }
541
553
  // Shared fleet-comms masthead (same family as `agents mailboxes`).
542
554
  console.log(masthead({
543
- title: 'they need you',
555
+ title: opts.project ? `${opts.project} needs you` : 'they need you',
544
556
  accent: 'amber',
545
557
  host: self,
546
558
  right: formatFeedMastheadRight(blocks),
@@ -630,6 +642,18 @@ export function resolveFeedFilter(raw) {
630
642
  return 'all';
631
643
  return 'needs';
632
644
  }
645
+ /** True when a block belongs to the requested project (case-insensitive). */
646
+ function blockMatchesProject(block, project) {
647
+ if (!project)
648
+ return true;
649
+ return (block.project ?? '').toLowerCase() === project.toLowerCase();
650
+ }
651
+ /** True when an activity event belongs to the requested project (case-insensitive). */
652
+ function eventMatchesProject(ev, project) {
653
+ if (!project)
654
+ return true;
655
+ return ((ev.project ?? projectKeyFromCwd(ev.cwd) ?? '')).toLowerCase() === project.toLowerCase();
656
+ }
633
657
  /**
634
658
  * Render one activity event in the lane: deliberate progress posts
635
659
  * (`status.posted`) use the rich multi-line {@link formatProgressUpdate}; hook
@@ -668,30 +692,33 @@ function readStatusPosts(limit) {
668
692
  * uses; `--local` (or the no-fanout env guard on a peer) keeps it to this box.
669
693
  */
670
694
  async function gatherStatusPosts(opts) {
671
- const local = opts.includeLocal ? readStatusPosts(opts.limit) : [];
695
+ const local = opts.includeLocal
696
+ ? readStatusPosts(opts.limit).filter((ev) => eventMatchesProject(ev, opts.project))
697
+ : [];
672
698
  const forceLocal = opts.local === true || process.env[FEED_NO_FANOUT_ENV] === '1';
673
699
  if (forceLocal)
674
- return local;
700
+ return local.slice(0, opts.limit);
675
701
  const remoteHosts = opts.hosts?.length ? remoteFeedHostsToDial(opts.hosts, opts.self) : undefined;
676
702
  if (opts.hosts?.length && (!remoteHosts || remoteHosts.length === 0))
677
- return local;
703
+ return local.slice(0, opts.limit);
678
704
  const remote = await gatherRemoteAgentsJson({
679
705
  args: ['feed', '--filter', 'updates', '--json'],
680
706
  noFanoutEnv: FEED_NO_FANOUT_ENV,
681
707
  hosts: remoteHosts,
682
708
  parse: parseActivityPayload,
683
709
  });
684
- return mergeActivityEvents(local, remote.items).slice(0, opts.limit);
710
+ const merged = mergeActivityEvents(local, remote.items).filter((ev) => eventMatchesProject(ev, opts.project));
711
+ return merged.slice(0, opts.limit);
685
712
  }
686
713
  /**
687
714
  * Render the **Updates** view: deliberate progress posts only (`status.posted`),
688
715
  * recency-ordered, with rich identity chips. Pure `file.edited` / git-hook noise
689
716
  * is excluded so operators see announcements, not tool churn.
690
717
  */
691
- function renderUpdatesView(updates) {
718
+ function renderUpdatesView(updates, project) {
692
719
  const hosts = new Set(updates.map((e) => e.host).filter(Boolean));
693
720
  console.log(masthead({
694
- title: 'updates',
721
+ title: project ? `${project} updates` : 'updates',
695
722
  accent: 'cyan',
696
723
  host: hosts.size > 1 ? `${hosts.size} machines` : (updates[0]?.host ?? machineId()),
697
724
  right: `${updates.length} post${updates.length === 1 ? '' : 's'}`,
@@ -712,16 +739,16 @@ function renderUpdatesView(updates) {
712
739
  * activity logs. Read-only tail of the logs -- no transcript re-parsing. Silent
713
740
  * when empty.
714
741
  */
715
- function renderActivityLane(limit = 6) {
742
+ function renderActivityLane(project, limit = 6) {
716
743
  const events = readRecentActivity({
717
744
  sinceMs: Date.now() - 24 * 60 * 60 * 1000,
718
- limit,
745
+ limit: limit * (project ? 4 : 1),
719
746
  tier: 'milestone',
720
- });
747
+ }).filter((ev) => eventMatchesProject(ev, project));
721
748
  if (events.length === 0)
722
749
  return;
723
- console.log(chalk.bold('\n recent activity'));
724
- for (const ev of events)
750
+ console.log(chalk.bold(project ? `\n recent activity · ${project}` : '\n recent activity'));
751
+ for (const ev of events.slice(0, limit))
725
752
  renderActivityEntry(ev);
726
- console.log(chalk.gray(' → agents activity for the full stream'));
753
+ console.log(chalk.gray(' → agents feed --project ' + (project ?? '<project>') + ' for the full stream'));
727
754
  }
@@ -20,8 +20,9 @@ import { runOnPeer } from '../lib/session/remote-list.js';
20
20
  import { discoverSessions } from '../lib/session/discover.js';
21
21
  import { openSurfaces, currentContext, availableBackends, detectCurrentBackend, } from '../lib/terminal/index.js';
22
22
  import { isInteractiveTerminal } from './utils.js';
23
+ import { setHelpSections } from '../lib/help.js';
23
24
  export function registerFocusCommand(program) {
24
- program
25
+ const cmd = program
25
26
  .command('focus')
26
27
  .argument('[id]', 'Short/full session id to focus; omit for an interactive picker')
27
28
  .option('--local', 'Only this machine (skip the cross-host sweep)')
@@ -30,6 +31,26 @@ export function registerFocusCommand(program) {
30
31
  .action(async (id, opts) => {
31
32
  await focusAction(id, opts);
32
33
  });
34
+ setHelpSections(cmd, {
35
+ examples: `
36
+ # Jump to a live session (attach pane/tab, or open a new tab and resume)
37
+ agents sessions focus a1b2c3d4
38
+
39
+ # Attach only — refuse if nothing is joinable (old sessions go)
40
+ agents sessions focus a1b2c3d4 --attach-only
41
+
42
+ # Pick from live sessions on this machine only
43
+ agents sessions focus --local
44
+ `,
45
+ notes: `
46
+ Lifecycle siblings (not synonyms):
47
+ focus live jump (default "take me there")
48
+ focus --attach-only attach only; never fork (replaces go)
49
+ detach / attach interactive ↔ headless presence
50
+ resume multi-select history → tabs
51
+ run --resume single scripted continue
52
+ `,
53
+ });
33
54
  }
34
55
  /**
35
56
  * Which fallback fires when a session has no attach rail. `--attach-only` (the old
@@ -16,13 +16,13 @@ import { setTierOverride, clearTierOverride, listTierOverrides } from '../lib/mo
16
16
  import { getModelPricing } from '../lib/pricing/index.js';
17
17
  import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
18
18
  import { wrapJoined } from './inspect.js';
19
- const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'cursor', 'openclaw', 'antigravity', 'kimi', 'grok', 'droid'];
19
+ const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'cursor', 'openclaw', 'antigravity', 'kimi', 'grok', 'droid', 'pi'];
20
20
  /**
21
21
  * Agents that don't necessarily install under ~/.agents/versions (cursor ships
22
22
  * via a curl script). For these, fall back to the PATH binary and synthesize
23
23
  * a version label from the install path so cache keys stay stable.
24
24
  */
25
- const PATH_ONLY_AGENTS = new Set(['cursor']);
25
+ const PATH_ONLY_AGENTS = new Set(['cursor', 'pi']);
26
26
  /** Derive a version label from the PATH-installed binary location for agents without managed versions. */
27
27
  function fallbackPathVersion(agent) {
28
28
  const src = locateModelSource(agent, 'unresolved');