@phnx-labs/agents-cli 1.22.72 → 1.22.74
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/CHANGELOG.md +73 -0
- package/README.md +2 -0
- package/dist/cli/command-registry.d.ts +1 -1
- package/dist/cli/command-registry.js +2 -1
- package/dist/commands/packages-materialize.d.ts +17 -0
- package/dist/commands/packages-materialize.js +93 -0
- package/dist/commands/packages.d.ts +6 -4
- package/dist/commands/packages.js +8 -4
- package/dist/commands/repo.js +8 -8
- package/dist/commands/sessions-picker.js +38 -7
- package/dist/commands/sessions.d.ts +50 -1
- package/dist/commands/sessions.js +85 -10
- package/dist/lib/actor.d.ts +36 -4
- package/dist/lib/actor.js +73 -9
- package/dist/lib/agent-spec/index.d.ts +4 -0
- package/dist/lib/agent-spec/index.js +5 -0
- package/dist/lib/agent-spec/materialize.d.ts +13 -0
- package/dist/lib/agent-spec/materialize.js +414 -0
- package/dist/lib/agent-spec/package-resolve.d.ts +12 -0
- package/dist/lib/agent-spec/package-resolve.js +274 -0
- package/dist/lib/agent-spec/package-schema.d.ts +5 -0
- package/dist/lib/agent-spec/package-schema.js +147 -0
- package/dist/lib/agent-spec/package-types.d.ts +121 -0
- package/dist/lib/agent-spec/package-types.js +11 -0
- package/dist/lib/cloud/rush.d.ts +1 -1
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/daemon/usage-sync-service.d.ts +12 -5
- package/dist/lib/daemon/usage-sync-service.js +27 -5
- package/dist/lib/exec.js +3 -0
- package/dist/lib/fleet-shared-state.d.ts +30 -0
- package/dist/lib/fleet-shared-state.js +5 -0
- package/dist/lib/hooks/install.d.ts +31 -1
- package/dist/lib/hooks/install.js +44 -2
- package/dist/lib/mcp.d.ts +14 -2
- package/dist/lib/mcp.js +12 -2
- package/dist/lib/packages/output-home.d.ts +39 -0
- package/dist/lib/packages/output-home.js +203 -0
- package/dist/lib/paths.d.ts +9 -0
- package/dist/lib/paths.js +26 -0
- package/dist/lib/project-resources.d.ts +6 -2
- package/dist/lib/project-resources.js +133 -44
- package/dist/lib/rush-session.d.ts +10 -2
- package/dist/lib/rush-session.js +12 -3
- package/dist/lib/secrets/drivers/rush.js +1 -1
- package/dist/lib/session/active.d.ts +12 -0
- package/dist/lib/session/active.js +5 -1
- package/dist/lib/session/actor-sidecar.d.ts +7 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/cloud.js +1 -1
- package/dist/lib/session/db.d.ts +60 -1
- package/dist/lib/session/db.js +191 -6
- package/dist/lib/session/live-metadata.d.ts +24 -0
- package/dist/lib/session/live-metadata.js +55 -0
- package/dist/lib/session/mirror.d.ts +67 -0
- package/dist/lib/session/mirror.js +158 -0
- package/dist/lib/session/types.d.ts +18 -0
- package/dist/lib/spinner.d.ts +39 -0
- package/dist/lib/spinner.js +41 -0
- package/dist/lib/startup/command-registry.js +1 -1
- package/dist/lib/types.d.ts +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.74
|
|
4
|
+
|
|
5
|
+
- **Launching an agent no longer dirties your tracked `.gitignore` (PHNX-3718).**
|
|
6
|
+
The per-harness resource sync self-manages ignore rules for the generated
|
|
7
|
+
`.claude/`, `.cursor/`, `.codex/`, … dirs it writes on every launch. Those
|
|
8
|
+
rules used to be written into the tracked `<project>/.gitignore`, but they are
|
|
9
|
+
never committed upstream — so every launch left the repo with a permanent
|
|
10
|
+
`M .gitignore` (one managed block per harness that had run there) that in turn
|
|
11
|
+
blocked `git pull` with *"local changes to .gitignore would be overwritten by
|
|
12
|
+
merge"*. The managed block now lives in `.git/info/exclude` (git's per-clone,
|
|
13
|
+
uncommitted ignore file, resolved via `git rev-parse --git-path info/exclude`
|
|
14
|
+
so it works from a subdir, linked worktree, or submodule), keeping the
|
|
15
|
+
generated dirs hidden while leaving the working tree clean. On the next launch,
|
|
16
|
+
a repo already dirtied by the old behavior self-heals: the leftover managed
|
|
17
|
+
block is stripped from the tracked `.gitignore` (hand-written rules untouched;
|
|
18
|
+
an untracked file that held only our block is removed). Source:
|
|
19
|
+
`cli/src/lib/project-resources.ts`.
|
|
20
|
+
|
|
21
|
+
- **`sessions preview` of a remote session no longer dead-ends on the dispatcher
|
|
22
|
+
(PHNX-3890).** A box that launched a session running on another device kept a
|
|
23
|
+
transcript-less live row for it whose machine defaulted to itself, so
|
|
24
|
+
`agents sessions preview <full-uuid>` — and the interactive browser's preview
|
|
25
|
+
pane — rendered `Live session — full transcript not indexed here.` instead of
|
|
26
|
+
fetching the owning peer's digest. A passive peer rendered the same session
|
|
27
|
+
fine. The launcher shim is now reconciled against the fleet's own view of which
|
|
28
|
+
device runs the session, and a full-UUID hit only skips the fleet fan-out when
|
|
29
|
+
this box can actually answer for it (a transcript on disk, or a row that already
|
|
30
|
+
names another device). Remote previews fill in asynchronously over SSH; a
|
|
31
|
+
locally readable transcript or synced mirror still renders with no hop, and an
|
|
32
|
+
unreachable owner still fails loud. Source: `cli/src/commands/sessions.ts`,
|
|
33
|
+
`cli/src/lib/session/live-metadata.ts`.
|
|
34
|
+
|
|
35
|
+
## 1.22.73
|
|
36
|
+
|
|
37
|
+
- **Ctrl-C works again while `agents sessions` is reaching other machines (PHNX-3791).** The static session list, `Searching the fleet…`, and the cloud session/fetch paths wrap their long SSH/network waits in `ora(...)`. ora defaults to `discardStdin: true`, which on a TTY flips stdin into raw mode for the spinner's lifetime (via `stdin-discarder`) — and raw mode disables the terminal's own ISIG, so a Ctrl-C keystroke no longer raises SIGINT. ora's compensating "re-emit SIGINT when I read a 0x03 byte" only fires when stdin is in *flowing* mode, but a fresh `process.stdin` has `flowing === null`, `isPaused()` returns false, the discarder skips its `resume()`, and `prependListener('data')` doesn't auto-resume — so the byte is never read and SIGINT is never raised. The global handler in `index.ts` (`process.exit(130)`) was correct; it simply never fired. The user-visible result: a sweep that stalls on offline/unreachable boxes (each burning a ~12s SSH `ConnectTimeout`) could not be aborted — measured, `agents sessions --flat` had to be SIGKILLed. New `lib/spinner.ts#interruptibleSpinner` is a drop-in for `ora(text)` built with `discardStdin: false`: the terminal stays in cooked mode, Ctrl-C raises SIGINT normally (and the terminal SIGINTs the whole foreground group, reaping the outstanding `ssh` children), and the process exits promptly — only cosmetic stray-keystroke discarding during the spin is lost. The five cross-machine/network spinners in `sessions.ts` route through it; short local spinners are unchanged. Source: `cli/src/lib/spinner.ts`, `cli/src/lib/spinner.test.ts`, `cli/src/commands/sessions.ts`.
|
|
38
|
+
|
|
39
|
+
- **Remote-host session previews render inline, no per-row SSH (PHNX-3792).** On
|
|
40
|
+
the interactive device, a session that originated on another box used to show
|
|
41
|
+
as a bare `[host/<peer>]` row with no topic, and its preview pane fetched the
|
|
42
|
+
peer's digest live over SSH — slow, and blank when the peer was asleep. Each
|
|
43
|
+
box now mirrors its own sessions' lightweight preview/metadata (topic, first
|
|
44
|
+
user message, last activity, agent+version, cwd, ticket, PR) into its
|
|
45
|
+
conflict-free `devices/<device>/daemon-state.json`, which the daemon's existing
|
|
46
|
+
bounded Git transport already delivers fleet-wide. The picker, `agents sessions`
|
|
47
|
+
list, and `focus` read the local mirror, so a peer session shows its real
|
|
48
|
+
topic/preview inline — even while the peer is offline (last-synced). The live
|
|
49
|
+
SSH digest fetch remains the fallback for a never-synced session, and `space`
|
|
50
|
+
still reads the full transcript live. No transcript leaves the worker; the
|
|
51
|
+
mirror is bounded and pruned by age. Source: `src/lib/session/mirror.ts`,
|
|
52
|
+
`src/lib/fleet-shared-state.ts`, `src/lib/session/db.ts`,
|
|
53
|
+
`src/lib/daemon/usage-sync-service.ts`, `src/commands/sessions-picker.ts`.
|
|
54
|
+
|
|
55
|
+
- **Ctrl-C works during `agents repo` git operations too (PHNX-3793).** Follow-up to PHNX-3791. `cli/src/commands/repo.ts` wrapped its seven git network ops — clone, git-backing, pull, push, adopt, sync — in bare `ora(...)` spinners, whose default `discardStdin: true` raw-modes the TTY and swallows Ctrl-C for the whole operation (same trap PHNX-3791 fixed for the session sweep). A `git clone`/`pull` stalled on a slow or dead remote could hang longer than a fleet sweep with no way to abort. All seven now use `lib/spinner.ts#interruptibleSpinner` (`discardStdin: false`), so Ctrl-C raises SIGINT and the process exits promptly. Source: `cli/src/commands/repo.ts`.
|
|
56
|
+
|
|
57
|
+
- **`agents sessions --json` now carries the initiator's Phoenix ID (PHNX-3798).**
|
|
58
|
+
The actor resolver already mapped a run's tailnet identity to a `phoenixId`
|
|
59
|
+
(the stable internal work identity) and rode it through the exec env, but it was
|
|
60
|
+
never persisted per session, so a durable listing couldn't attribute a session
|
|
61
|
+
to a person's work identity. The `phoenixId` now flows the same path the
|
|
62
|
+
`actor`/`initiatedBy` fields already take — stamped into the durable actor
|
|
63
|
+
sidecar at launch, joined into a new write-once `phoenix_id` column via a v47
|
|
64
|
+
schema migration, and emitted by the sessions feed. Best-effort throughout: a
|
|
65
|
+
session with no mapped Phoenix ID (the common case) simply omits the field
|
|
66
|
+
rather than emitting a null/empty sentinel. This is the persistence half of the
|
|
67
|
+
ownership work whose resolver landed in PHNX-3798's first PR. Source:
|
|
68
|
+
`src/lib/session/actor-sidecar.ts`, `src/lib/session/db.ts`,
|
|
69
|
+
`src/lib/session/types.ts`, `src/lib/exec.ts`.
|
|
70
|
+
|
|
71
|
+
- **`agents packages materialize` materializes a schema-v3 `agent.yaml` package into an ephemeral Claude, Codex, or OpenCode home (PHNX-3838).** The noun-first surface takes `--harness`, `--harness-version` (root `--version` is the CLI version flag), and `--output-home`. It is a thin front door over the canonical native-home materializer (`agent-spec/materialize.ts`): it resolves the package once and projects every declared resource (instructions, skills, subagents, mcp, hooks) into the output home, then prints the materialization receipt (agent ref + digest, harness, per-resource targets, warnings); `--json` is the Factory/Prix Cloud handoff and is byte-identical to the `materialization-receipt.json` written into the home. It writes only under `--output-home`: it never mutates the live user home, never copies secrets, and never execs a harness. A non-portable harness, a non-exact `--harness-version`, a missing/invalid package, and an output-path escape (including the live `~/.claude`/`~/.codex`/`~/.opencode` homes) all fail loud. Source: `cli/src/commands/packages-materialize.ts`, `cli/src/lib/packages/output-home.ts`, `cli/src/lib/agent-spec/materialize.ts`.
|
|
72
|
+
- **`agents packages materialize` is hardened against four path-traversal / symlink escapes (PHNX-3838).** (1) `materializeAgentPackage` now realpath-verifies every per-resource write/delete target against the canonical output home before touching disk, and `--output-home` additionally refuses the live home ROOT and canonicalizes symlinked ancestors. So neither `--output-home "$HOME"` (the materializer appends `.claude`/`.codex`/`.opencode`), a symlink aliasing `$HOME`, nor a symlink planted at the harness-config-dir join point inside the output home can write into the live harness home — and a direct (Factory / Prix Cloud) caller that skips the CLI front door is protected too. (2) The stale-prune step strictly validates the unsigned `materialization-receipt.json` schema and realpath-checks every `resources[].target` as a contained path at deletion time, so a planted receipt can no longer make it `rm -rf` a `../victim`, an absolute path, or a path under a symlinked ancestor outside the output home. (3) A package `hook.name` must be a safe single path segment, re-checked for containment before the script is copied and `chmod +x`'d. (4) Package resources reject symlinked file/directory/hook sources and realpath-check every source under the package's real root, so `instructions.md -> /etc/passwd` or a skill dir symlinked outside the package can no longer be read or copied. Source: `cli/src/lib/packages/output-home.ts`, `cli/src/lib/agent-spec/materialize.ts`, `cli/src/lib/agent-spec/package-resolve.ts`, `cli/src/lib/paths.ts`.
|
|
73
|
+
- **`agents packages materialize` closes two more output-home boundary gaps (PHNX-3838).** (1) When any protected `~/.claude`/`~/.codex`/`~/.opencode` home is a DANGLING symlink or dangling chain (following relative links and chains), `resolveOutputHome` now fails closed and refuses EVERY output home until the link is repaired — because the materializer's first `mkdir -p` would otherwise follow the link and re-create the operator's live harness home at the materialized tree, and the absent target it points at has no canonical spelling to compare a specific candidate against. (2) Materializing a package that carries hooks no longer runs the normal orphan-shim sweep against the ONE process-global shim dir (`~/.agents/.cache/shims/hooks/`): that sweep deletes every shim absent from the manifest it is handed, so with a package's hooks-only manifest it would wipe the operator's unrelated hook shims. `registerHooksToSettings` gained an isolated `skipGlobalShimSweep` option that the materializer sets; normal install/sync keeps the sweep. Source: `cli/src/lib/packages/output-home.ts`, `cli/src/lib/hooks/install.ts`, `cli/src/lib/agent-spec/materialize.ts`.
|
|
74
|
+
- **`agents packages materialize` fails closed on a dangling protected home instead of approximating filesystem collation (PHNX-3838).** An earlier iteration compared the live-home identity case- and Unicode-normalization-insensitively (NFC + `toLowerCase`) on macOS/Windows to catch a spelling-equivalent alias of a DANGLING `~/.claude` (e.g. an absent `.../Real-Claude` vs `--output-home .../real-claude`). An independent macOS review proved that approximation wrong: APFS treats e.g. U+017F ſ and ASCII `s` as identical while JS `toLowerCase` does not, and an OS-derived case-sensitivity flag is wrong for per-volume semantics. The guard no longer tries to reproduce a volume's collation from the path text — when a protected home dangles it refuses every output home (fail closed, above), and an EXISTING protected home is compared with exact `realpath` identity, which needs no approximation. The `pathIdentityKey`/`pathIsWithin`/`FS_CASE_INSENSITIVE` helpers added for the approximation are removed. Source: `cli/src/lib/packages/output-home.ts`, `cli/src/lib/paths.ts`.
|
|
75
|
+
|
|
3
76
|
## 1.22.72
|
|
4
77
|
|
|
5
78
|
- **The self-managed `.gitignore` block now ignores the `.agents-managed.json` manifest too, so a synced harness dir is actually clean in `git status` (PHNX-3717).** The project-resource sync's gitignore reconciliation (shipped in 1.22.71) listed the synced commands/skills but not the manifest marker file the sync also writes into each harness dir — so `.factory/` (etc.) still showed as untracked on the strength of that one unignored file, defeating the point. `reconcileProjectGitignore` now feeds `.agents-managed.json` through the same anchoring + escape guard, and the block persists (shrunk to just the manifest) while that file exists rather than fully pruning. Covered by a new test that asserts a real `git status --porcelain` no longer reports the generated dir. Source: `cli/src/lib/project-resources.ts`, `cli/src/lib/project-resources.test.ts`.
|
package/README.md
CHANGED
|
@@ -154,6 +154,8 @@ agents mcp list
|
|
|
154
154
|
|
|
155
155
|
Skills, slash commands, rules, hooks, and permissions work the same way -- install once in `~/.agents/`, synced to every agent's native format automatically.
|
|
156
156
|
|
|
157
|
+
A schema-v3 `agent.yaml` package materializes into an ephemeral Claude, Codex, or OpenCode home for Factory / Prix Cloud workers (`agents packages materialize ./reviewer --harness codex --harness-version 0.42.0 --output-home "$OUTPUT_HOME" --json`) -- it never writes the live user home.
|
|
158
|
+
|
|
157
159
|
```bash
|
|
158
160
|
agents skills add gh:yourteam/python-expert # Knowledge pack -> all agents
|
|
159
161
|
agents commands add gh:yourteam/commands # Slash commands -> all agents
|
|
@@ -105,7 +105,7 @@ export declare const LAZY_COMMAND_NAMES: ReadonlySet<string>;
|
|
|
105
105
|
*
|
|
106
106
|
* Most names map to a single loader. The exceptions encode real coupling on main:
|
|
107
107
|
* - `add`/`use`/`list`/`remove`/`rm`/`purge` all come from the versions module.
|
|
108
|
-
* - `registry`/`search`/`install` all come from the packages module.
|
|
108
|
+
* - `registry`/`search`/`install`/`packages` all come from the packages module.
|
|
109
109
|
* - `trash` and `restore` are separate registrars in the trash module.
|
|
110
110
|
* - `prune` needs versions FIRST (it creates `prune <specs...>`) then prune.js
|
|
111
111
|
* (which finds that command and attaches the `cleanup` subcommand).
|
|
@@ -115,7 +115,7 @@ export const LAZY_COMMAND_NAMES = new Set([
|
|
|
115
115
|
*
|
|
116
116
|
* Most names map to a single loader. The exceptions encode real coupling on main:
|
|
117
117
|
* - `add`/`use`/`list`/`remove`/`rm`/`purge` all come from the versions module.
|
|
118
|
-
* - `registry`/`search`/`install` all come from the packages module.
|
|
118
|
+
* - `registry`/`search`/`install`/`packages` all come from the packages module.
|
|
119
119
|
* - `trash` and `restore` are separate registrars in the trash module.
|
|
120
120
|
* - `prune` needs versions FIRST (it creates `prune <specs...>`) then prune.js
|
|
121
121
|
* (which finds that command and attaches the `cleanup` subcommand).
|
|
@@ -152,6 +152,7 @@ export const COMMAND_LOADERS = {
|
|
|
152
152
|
registry: [loadPackages],
|
|
153
153
|
search: [loadPackages],
|
|
154
154
|
install: [loadPackages],
|
|
155
|
+
packages: [loadPackages],
|
|
155
156
|
routines: [loadRoutines],
|
|
156
157
|
monitors: [loadMonitors],
|
|
157
158
|
projects: [loadProjects],
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents packages materialize` — user-facing front door for portable-agent
|
|
3
|
+
* materialization (PHNX-3838). ONE execution path: this command resolves the
|
|
4
|
+
* schema-v3 package once with {@link resolveAgentPackage} and projects it into
|
|
5
|
+
* an ephemeral native home with the canonical {@link materializeAgentPackage}
|
|
6
|
+
* (agent-spec/materialize.ts). The front door owns only what a materializer must
|
|
7
|
+
* not: the portable-harness allowlist, an exact harness version, and the
|
|
8
|
+
* output-home refusal that keeps a run off the live `~/.claude` / `~/.codex` /
|
|
9
|
+
* `~/.opencode` homes.
|
|
10
|
+
*/
|
|
11
|
+
import type { Command } from 'commander';
|
|
12
|
+
import { type MaterializationReceipt } from '../lib/agent-spec/index.js';
|
|
13
|
+
import { PORTABLE_HARNESSES } from '../lib/packages/output-home.js';
|
|
14
|
+
export { PORTABLE_HARNESSES };
|
|
15
|
+
export type { MaterializationReceipt };
|
|
16
|
+
/** Register `agents packages materialize`. */
|
|
17
|
+
export declare function registerPortablePackageCommands(program: Command): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { die, isJsonMode } from '../lib/format.js';
|
|
3
|
+
import { setHelpSections } from '../lib/help.js';
|
|
4
|
+
import { resolveAgentPackage, materializeAgentPackage, AgentPackageError, } from '../lib/agent-spec/index.js';
|
|
5
|
+
import { MaterializeGuardError, PORTABLE_HARNESSES, assertExactHarnessVersion, assertPortableHarness, resolveOutputHome, } from '../lib/packages/output-home.js';
|
|
6
|
+
export { PORTABLE_HARNESSES };
|
|
7
|
+
function printReceipt(receipt, outputHome) {
|
|
8
|
+
console.log(chalk.bold(`Materialized ${receipt.agent.ref} → ${receipt.harness.id}@${receipt.harness.version}`));
|
|
9
|
+
console.log(` digest ${receipt.agent.digest}`);
|
|
10
|
+
console.log(` output home ${outputHome}`);
|
|
11
|
+
for (const entry of receipt.resources) {
|
|
12
|
+
console.log(` ${entry.kind.padEnd(12)} ${entry.name} → ${entry.target}`);
|
|
13
|
+
}
|
|
14
|
+
if (receipt.warnings.length > 0) {
|
|
15
|
+
console.log(chalk.yellow(' warnings'));
|
|
16
|
+
for (const warning of receipt.warnings)
|
|
17
|
+
console.log(chalk.yellow(` ${warning}`));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function fail(err, json) {
|
|
21
|
+
if (err instanceof MaterializeGuardError || err instanceof AgentPackageError) {
|
|
22
|
+
die(err.message, 1, { json });
|
|
23
|
+
}
|
|
24
|
+
die(err instanceof Error ? err.message : String(err), 1, { json });
|
|
25
|
+
}
|
|
26
|
+
/** Register `agents packages materialize`. */
|
|
27
|
+
export function registerPortablePackageCommands(program) {
|
|
28
|
+
const packagesCmd = program
|
|
29
|
+
.command('packages')
|
|
30
|
+
.description('Portable agent packages — materialize schema-v3 agent.yaml into an ephemeral harness home');
|
|
31
|
+
setHelpSections(packagesCmd, {
|
|
32
|
+
examples: `
|
|
33
|
+
# Materialize a package into an ephemeral Claude home
|
|
34
|
+
agents packages materialize ./reviewer --harness claude --harness-version 2.1.0 --output-home /tmp/reviewer-claude
|
|
35
|
+
|
|
36
|
+
# Machine-readable receipt for Factory / Prix Cloud
|
|
37
|
+
agents packages materialize ./reviewer --harness codex --harness-version 0.42.0 --output-home "$OUTPUT_HOME" --json
|
|
38
|
+
|
|
39
|
+
# OpenCode worker on a Factory box
|
|
40
|
+
agents packages materialize ./reviewer --harness opencode --harness-version 1.0.0 --output-home "$OUTPUT_HOME" --json
|
|
41
|
+
`,
|
|
42
|
+
notes: `
|
|
43
|
+
Materialize writes only under --output-home. It never mutates ~/.claude,
|
|
44
|
+
~/.codex, or ~/.opencode, never copies secrets, and never execs a harness.
|
|
45
|
+
|
|
46
|
+
Factory usage: point --output-home at the worker's ephemeral home and pass
|
|
47
|
+
--json so the orchestrator can read the receipt (agent ref + digest,
|
|
48
|
+
harness, per-resource targets, warnings).
|
|
49
|
+
`,
|
|
50
|
+
});
|
|
51
|
+
const materializeCmd = packagesCmd
|
|
52
|
+
.command('materialize <package>')
|
|
53
|
+
.description('Materialize a schema-v3 agent.yaml package into an ephemeral Claude, Codex, or OpenCode home')
|
|
54
|
+
.requiredOption('--harness <id>', `Target harness: ${PORTABLE_HARNESSES.join(', ')}`)
|
|
55
|
+
.requiredOption('--harness-version <version>', 'Exact harness version to gate capabilities and stamp on the receipt (not --version: that is the CLI version flag)')
|
|
56
|
+
.requiredOption('--output-home <dir>', 'Ephemeral home to write into (must not escape or target the live user home)')
|
|
57
|
+
.option('--json', 'Emit the materialization receipt as JSON')
|
|
58
|
+
.action((pkg, opts) => {
|
|
59
|
+
const json = isJsonMode(opts);
|
|
60
|
+
try {
|
|
61
|
+
const harness = assertPortableHarness(opts.harness);
|
|
62
|
+
const harnessVersion = assertExactHarnessVersion(opts.harnessVersion);
|
|
63
|
+
const outputHome = resolveOutputHome(opts.outputHome);
|
|
64
|
+
const resolved = resolveAgentPackage(pkg);
|
|
65
|
+
const receipt = materializeAgentPackage(resolved, { harness, harnessVersion, outputHome });
|
|
66
|
+
if (json) {
|
|
67
|
+
// Verbatim canonical receipt — byte-identical to the
|
|
68
|
+
// materialization-receipt.json the materializer wrote into the home.
|
|
69
|
+
console.log(JSON.stringify(receipt, null, 2));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
printReceipt(receipt, outputHome);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
fail(err, json);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
setHelpSections(materializeCmd, {
|
|
79
|
+
examples: `
|
|
80
|
+
agents packages materialize ./reviewer --harness claude --harness-version 2.1.0 --output-home /tmp/reviewer-claude
|
|
81
|
+
agents packages materialize ./reviewer --harness codex --harness-version 0.42.0 --output-home "$OUTPUT_HOME" --json
|
|
82
|
+
agents packages materialize ./reviewer --harness opencode --harness-version 1.0.0 --output-home "$OUTPUT_HOME" --json
|
|
83
|
+
`,
|
|
84
|
+
notes: `
|
|
85
|
+
Factory / Prix Cloud: materialize into the worker's ephemeral home, then
|
|
86
|
+
exec the harness with HOME=$OUTPUT_HOME. The --json receipt is the handoff.
|
|
87
|
+
|
|
88
|
+
Supported harnesses: claude, codex, opencode. Any other id fails as an
|
|
89
|
+
unsupported capability. A package must be a directory containing agent.yaml
|
|
90
|
+
with schema_version: 3 and an execution block declaring the harness.
|
|
91
|
+
`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package registry and installation commands.
|
|
3
3
|
*
|
|
4
|
-
* Registers `agents registry`, `agents search`,
|
|
5
|
-
* for discovering and installing MCP servers,
|
|
6
|
-
* hooks from configured registries or GitHub sources
|
|
4
|
+
* Registers `agents registry`, `agents search`, `agents install`, and
|
|
5
|
+
* `agents packages materialize` for discovering and installing MCP servers,
|
|
6
|
+
* skills, commands, and hooks from configured registries or GitHub sources,
|
|
7
|
+
* and for materializing a schema-v3 agent.yaml package into an ephemeral
|
|
8
|
+
* harness home (PHNX-3838).
|
|
7
9
|
*/
|
|
8
10
|
import type { Command } from 'commander';
|
|
9
11
|
import type { McpPackage } from '../lib/types.js';
|
|
10
12
|
import { type McpCommandSpec } from '../lib/mcp.js';
|
|
11
13
|
export declare function buildMcpPackageCommand(pkg: McpPackage): McpCommandSpec;
|
|
12
|
-
/** Register the `agents registry`, `agents search`, and `agents
|
|
14
|
+
/** Register the `agents registry`, `agents search`, `agents install`, and `agents packages` commands. */
|
|
13
15
|
export declare function registerPackagesCommands(program: Command): void;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package registry and installation commands.
|
|
3
3
|
*
|
|
4
|
-
* Registers `agents registry`, `agents search`,
|
|
5
|
-
* for discovering and installing MCP servers,
|
|
6
|
-
* hooks from configured registries or GitHub sources
|
|
4
|
+
* Registers `agents registry`, `agents search`, `agents install`, and
|
|
5
|
+
* `agents packages materialize` for discovering and installing MCP servers,
|
|
6
|
+
* skills, commands, and hooks from configured registries or GitHub sources,
|
|
7
|
+
* and for materializing a schema-v3 agent.yaml package into an ephemeral
|
|
8
|
+
* harness home (PHNX-3838).
|
|
7
9
|
*/
|
|
8
10
|
import * as fs from 'fs';
|
|
9
11
|
import chalk from 'chalk';
|
|
@@ -23,6 +25,7 @@ import { listInstalledVersions, resolveConfiguredAgentTargets, syncResourcesToVe
|
|
|
23
25
|
import { formatPath, isInteractiveTerminal, isPromptCancelled, parseCommaSeparatedList, requireDestructiveArg, requireInteractiveSelection, resolveInstalledAgentTargetsAutoInstalling, } from './utils.js';
|
|
24
26
|
import { itemPicker } from '../lib/picker.js';
|
|
25
27
|
import { registerMcpCommandToTargets, discoverMcpConfigsFromRepo, installMcpConfigCentrally, } from '../lib/mcp.js';
|
|
28
|
+
import { registerPortablePackageCommands } from './packages-materialize.js';
|
|
26
29
|
export function buildMcpPackageCommand(pkg) {
|
|
27
30
|
const packageName = pkg.name || pkg.registry_name;
|
|
28
31
|
if (pkg.runtime === 'node') {
|
|
@@ -80,8 +83,9 @@ async function pickRegistryName(type, verb, pred) {
|
|
|
80
83
|
throw err;
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
|
-
/** Register the `agents registry`, `agents search`, and `agents
|
|
86
|
+
/** Register the `agents registry`, `agents search`, `agents install`, and `agents packages` commands. */
|
|
84
87
|
export function registerPackagesCommands(program) {
|
|
88
|
+
registerPortablePackageCommands(program);
|
|
85
89
|
// ==========================================================================
|
|
86
90
|
// REGISTRY COMMANDS
|
|
87
91
|
// ==========================================================================
|
package/dist/commands/repo.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { visibleWidth, padVisible } from '../lib/format.js';
|
|
3
3
|
import { stripAnsi } from '../lib/session/width.js';
|
|
4
|
-
import
|
|
4
|
+
import { interruptibleSpinner } from '../lib/spinner.js';
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import * as os from 'os';
|
|
@@ -785,7 +785,7 @@ export function registerRepoCommands(program) {
|
|
|
785
785
|
return;
|
|
786
786
|
}
|
|
787
787
|
const parsed = parseSource(options.from);
|
|
788
|
-
const spinner =
|
|
788
|
+
const spinner = interruptibleSpinner(`Cloning ${options.from} into ${targetDir}...`).start();
|
|
789
789
|
try {
|
|
790
790
|
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
791
791
|
await simpleGit().clone(parsed.url, targetDir);
|
|
@@ -885,7 +885,7 @@ export function registerRepoCommands(program) {
|
|
|
885
885
|
process.exitCode = 1;
|
|
886
886
|
return;
|
|
887
887
|
}
|
|
888
|
-
const spinner =
|
|
888
|
+
const spinner = interruptibleSpinner(`Cloning ${source}...`).start();
|
|
889
889
|
try {
|
|
890
890
|
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
891
891
|
await simpleGit().clone(parsed.url, targetDir);
|
|
@@ -1023,7 +1023,7 @@ export function registerRepoCommands(program) {
|
|
|
1023
1023
|
// user. Only the USER repo gets this — system is cloned by setup, extras
|
|
1024
1024
|
// by `repo add`.
|
|
1025
1025
|
if (t.alias === 'user' && url) {
|
|
1026
|
-
const spinner =
|
|
1026
|
+
const spinner = interruptibleSpinner(`Git-backing ${t.dir} from ${url}...`).start();
|
|
1027
1027
|
const res = await adoptRepo(url, t.dir);
|
|
1028
1028
|
if (!res.success) {
|
|
1029
1029
|
spinner.fail(`user: could not git-back — ${res.error}`);
|
|
@@ -1049,7 +1049,7 @@ export function registerRepoCommands(program) {
|
|
|
1049
1049
|
// Skip system repo unless explicitly requested
|
|
1050
1050
|
continue;
|
|
1051
1051
|
}
|
|
1052
|
-
const spinner =
|
|
1052
|
+
const spinner = interruptibleSpinner(`Pulling ${formatRepoTarget(t.alias, t.dir)}...`).start();
|
|
1053
1053
|
const result = await pullRepo(t.dir);
|
|
1054
1054
|
if (result.success) {
|
|
1055
1055
|
spinner.succeed(`${formatRepoTarget(t.alias, t.dir, result.branch)}: ${result.commit}`);
|
|
@@ -1127,7 +1127,7 @@ export function registerRepoCommands(program) {
|
|
|
1127
1127
|
// the previous tick's state and delays propagation by one repo cycle.
|
|
1128
1128
|
if (t.alias === 'user')
|
|
1129
1129
|
await publishUserRepoAccountState(true);
|
|
1130
|
-
const spinner =
|
|
1130
|
+
const spinner = interruptibleSpinner(`Pushing ${formatRepoTarget(t.alias, t.dir)}...`).start();
|
|
1131
1131
|
const result = await commitAndPush(t.dir, options.message);
|
|
1132
1132
|
if (result.success) {
|
|
1133
1133
|
spinner.succeed(`${formatRepoTarget(t.alias, t.dir, result.branch)}: ${result.detail ?? 'pushed'}`);
|
|
@@ -1171,7 +1171,7 @@ Examples:
|
|
|
1171
1171
|
// state, then fall through to the normal sync (PHNX-3301). System is
|
|
1172
1172
|
// cloned by setup and extras by `repo add`, so those still skip.
|
|
1173
1173
|
if (t.alias === 'user' && fs.existsSync(t.dir)) {
|
|
1174
|
-
const spinner =
|
|
1174
|
+
const spinner = interruptibleSpinner(`Adopting ${formatRepoTarget(t.alias, t.dir)} in place...`).start();
|
|
1175
1175
|
const adopted = await adoptUserRepoIfNeeded(t.dir);
|
|
1176
1176
|
if (!adopted || !adopted.success) {
|
|
1177
1177
|
spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${adopted?.error ?? 'adopt failed'}`);
|
|
@@ -1199,7 +1199,7 @@ Examples:
|
|
|
1199
1199
|
// so the same transaction carries it; consume peers after the pull.
|
|
1200
1200
|
if (t.alias === 'user')
|
|
1201
1201
|
await publishUserRepoAccountState(false);
|
|
1202
|
-
const spinner =
|
|
1202
|
+
const spinner = interruptibleSpinner(`Syncing ${formatRepoTarget(t.alias, t.dir)}...`).start();
|
|
1203
1203
|
const result = t.alias === 'user'
|
|
1204
1204
|
? await (async () => {
|
|
1205
1205
|
const { syncFleetSharedStateRepo } = await import('../lib/fleet-shared-repo-sync.js');
|
|
@@ -306,6 +306,7 @@ function previewCacheKey(session, remote) {
|
|
|
306
306
|
session.topic, session.ticketId, session.prUrl, session.messageCount,
|
|
307
307
|
session.tokenCount, session.model, session.todos, session.plan,
|
|
308
308
|
session.recentDirectoriesTouched, session.skillsUsed,
|
|
309
|
+
session.firstUserMessage, session.mirrorSyncedAt,
|
|
309
310
|
]);
|
|
310
311
|
}
|
|
311
312
|
export function clearPreviewMemoryCacheForTest() {
|
|
@@ -363,12 +364,38 @@ export function buildPreview(session) {
|
|
|
363
364
|
return cached;
|
|
364
365
|
const safe = sanitizeMeta(session);
|
|
365
366
|
// Remote session: the transcript is on the peer's disk, so there is nothing to
|
|
366
|
-
// parse here.
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
367
|
+
// parse here. The common case is now a fleet-synced MIRROR row (PHNX-3792): its
|
|
368
|
+
// topic + first-user snippet + metadata are already local, so the compact card
|
|
369
|
+
// renders INLINE with no per-row SSH. The live SSH digest fetch stays as the
|
|
370
|
+
// fallback for a never-synced row (or a peer whose digest was already fetched
|
|
371
|
+
// this session), and `space` still reads the full transcript live over SSH.
|
|
371
372
|
if (remote) {
|
|
373
|
+
// A fleet-synced MIRROR row is the explicit signal that this box already
|
|
374
|
+
// holds the peer session's topic + first-user snippet locally (PHNX-3792):
|
|
375
|
+
// render inline with NO per-row SSH. An ordinary remote row (a live fan-out
|
|
376
|
+
// row, or a never-synced host-dispatch stub) is NOT treated as synced — it
|
|
377
|
+
// keeps the live digest fetch below, so this change is scoped to mirror rows.
|
|
378
|
+
const fetched = remoteDigestCache.get(remoteDigestKey(session.id, remote));
|
|
379
|
+
if (fetched?.state === 'ready') {
|
|
380
|
+
// A richer live digest already landed this session (changed files, tool
|
|
381
|
+
// mix) — prefer it over the lightweight mirror card.
|
|
382
|
+
const note = ' ' + chalk.gray(`on `) + chalk.bold.white(remote)
|
|
383
|
+
+ chalk.gray(` — enter to resume there`);
|
|
384
|
+
const body = formatCompactPreview(fetched.digest, safe);
|
|
385
|
+
const output = [formatHeader(safe, []), '', note, body].filter(Boolean).join('\n');
|
|
386
|
+
previewCache.set(cacheKey, output);
|
|
387
|
+
return output;
|
|
388
|
+
}
|
|
389
|
+
if (safe.mirrorSyncedAt !== undefined) {
|
|
390
|
+
const note = ' ' + chalk.gray(`on `) + chalk.bold.white(remote)
|
|
391
|
+
+ chalk.gray(` — synced from the fleet; enter to resume there, or space to read it live over SSH`);
|
|
392
|
+
const metaBody = formatMetaOnlyBody(safe);
|
|
393
|
+
const output = [formatHeader(safe, []), '', note, metaBody].filter(Boolean).join('\n');
|
|
394
|
+
previewCache.set(cacheKey, output);
|
|
395
|
+
return output;
|
|
396
|
+
}
|
|
397
|
+
// Not a mirror row: fall back to the live SSH digest fetch (kicked off on
|
|
398
|
+
// first render; the pane repaints when it lands).
|
|
372
399
|
const note = ' ' + chalk.gray(`on `) + chalk.bold.white(remote)
|
|
373
400
|
+ chalk.gray(` — enter to resume there, or space then enter to read it over SSH`);
|
|
374
401
|
const entry = remoteDigestForPreview(session, remote);
|
|
@@ -554,8 +581,12 @@ function formatMetaOnlyBody(session) {
|
|
|
554
581
|
const termWidth = process.stdout.columns || 80;
|
|
555
582
|
const valueWidth = termWidth - VERB_GUTTER - 5;
|
|
556
583
|
// Same verb-led rows as the digest body (RUSH-2757), from SessionMeta alone.
|
|
557
|
-
|
|
558
|
-
|
|
584
|
+
// Prefer the fuller first genuine user turn over the one-line topic when the
|
|
585
|
+
// row carries it (a fleet-synced mirror row does — PHNX-3792) so the inline
|
|
586
|
+
// card reads like the originating prompt, not just its title.
|
|
587
|
+
const asked = session.firstUserMessage?.trim() || session.topic?.trim();
|
|
588
|
+
if (asked) {
|
|
589
|
+
lines.push(verbLabel('Asked') + chalk.white(`"${truncate(asked, valueWidth)}"`));
|
|
559
590
|
}
|
|
560
591
|
const compact = formatTodoCompact(session.todos);
|
|
561
592
|
const teamLine = formatTeamLineage(session);
|
|
@@ -896,9 +896,58 @@ export declare function metadataResolveOutcome(localMatches: SessionMeta[], remo
|
|
|
896
896
|
sessions: SessionMeta[];
|
|
897
897
|
unreachable: string[];
|
|
898
898
|
}, selector: string): MetadataResolveOutcome;
|
|
899
|
-
/**
|
|
899
|
+
/**
|
|
900
|
+
* Whether a local candidate answers a full-UUID selector **definitively** — i.e.
|
|
901
|
+
* whether finding it on this box is the whole answer, so the fleet fan-out can be
|
|
902
|
+
* skipped (PHNX-3890).
|
|
903
|
+
*
|
|
904
|
+
* Two shapes qualify, and both are claims this box can actually back:
|
|
905
|
+
*
|
|
906
|
+
* - **A real transcript on this disk** (`filePath`). It renders locally, whoever
|
|
907
|
+
* owns it — a local session or a fleet-synced mirror — so no SSH hop is needed.
|
|
908
|
+
* - **A genuine peer attribution** (`machine` names another box). The row already
|
|
909
|
+
* routes the read to its owner through `transcriptOnPeerOf`, so the peer that
|
|
910
|
+
* would answer the fan-out is the peer the render already dials.
|
|
911
|
+
*
|
|
912
|
+
* What does NOT qualify is the launcher-shim shape: a transcript-less live row
|
|
913
|
+
* whose `machine` is this box only because it DEFAULTED there
|
|
914
|
+
* (`activeSessionToSessionMeta`'s `active.machine ?? self`,
|
|
915
|
+
* `computeLocalMetadataMatches`'s `machine || localMachine`). A dispatcher holds
|
|
916
|
+
* exactly that row for a session whose agent and transcript live on a peer: the
|
|
917
|
+
* launch process is here, the conversation is not. Treating it as definitive is
|
|
918
|
+
* what dead-ended `agents sessions preview <full-uuid>` on the local
|
|
919
|
+
* "Live session — full transcript not indexed here." stub while a passive peer —
|
|
920
|
+
* which has no local row at all, so it fans out — rendered the real digest.
|
|
921
|
+
* Process locality is not transcript locality.
|
|
922
|
+
*/
|
|
923
|
+
export declare function isLocallyDefinitiveMatch(session: SessionMeta, self: string): boolean;
|
|
924
|
+
/**
|
|
925
|
+
* Drop the launcher-shim local rows that a peer has since answered for
|
|
926
|
+
* (PHNX-3890). `fleetCandidatesByQuery` groups a logical session's copies per
|
|
927
|
+
* machine and every consumer reads `hits[0]`, with local rows passed first — so
|
|
928
|
+
* simply fanning out is not enough: the self-defaulted shim would still win the
|
|
929
|
+
* attribution and route the read back to a box with no transcript. When the peer
|
|
930
|
+
* that actually owns the session has answered the sweep, its row is the strictly
|
|
931
|
+
* better one, and the shim carries no information the candidate loses (same
|
|
932
|
+
* logical id, so the candidate — and any ambiguity it is part of — survives
|
|
933
|
+
* through the remote hit).
|
|
934
|
+
*
|
|
935
|
+
* A row that is {@link isLocallyDefinitiveMatch} is never dropped, so a locally
|
|
936
|
+
* readable transcript or a synced mirror still renders here with no SSH hop. When
|
|
937
|
+
* no peer answered for that id, the shim is kept: with no fleet evidence, a
|
|
938
|
+
* transcript-less self-attributed row is indistinguishable from a session THIS box
|
|
939
|
+
* just started, and dropping it would re-break the cold-index lookup RUSH-2682
|
|
940
|
+
* fixed.
|
|
941
|
+
*/
|
|
942
|
+
export declare function preferOwnerAttribution(localMatches: SessionMeta[], remoteSessions: SessionMeta[], self: string): SessionMeta[];
|
|
943
|
+
/** Injectable dependencies for the live-registry cold-miss fallback (RUSH-2682)
|
|
944
|
+
* and the fleet-attribution reconciliation (PHNX-3890). */
|
|
900
945
|
export type LiveMetadataDeps = {
|
|
901
946
|
loadActive?: typeof loadLocalActiveSessions;
|
|
947
|
+
/** The fleet-active snapshot rows used to attribute a self-defaulted launcher
|
|
948
|
+
* shim to its true execution host. Defaults to the cached fleet snapshot; the
|
|
949
|
+
* cache is warmed by any fleet-wide `agents sessions --active`. */
|
|
950
|
+
loadFleetActive?: () => ActiveSession[];
|
|
902
951
|
};
|
|
903
952
|
/**
|
|
904
953
|
* Local metadata candidates for a selector: the indexed SQLite rows, plus — when
|