@phnx-labs/agents-cli 1.20.17 → 1.20.18

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 (62) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -1
  3. package/dist/commands/budget.d.ts +14 -0
  4. package/dist/commands/budget.js +137 -0
  5. package/dist/commands/cost.d.ts +12 -0
  6. package/dist/commands/cost.js +139 -0
  7. package/dist/commands/exec.d.ts +20 -0
  8. package/dist/commands/exec.js +382 -5
  9. package/dist/commands/secrets.d.ts +15 -0
  10. package/dist/commands/secrets.js +250 -4
  11. package/dist/commands/sessions.js +4 -0
  12. package/dist/index.js +4 -0
  13. package/dist/lib/budget/config.d.ts +9 -0
  14. package/dist/lib/budget/config.js +115 -0
  15. package/dist/lib/budget/enforce.d.ts +94 -0
  16. package/dist/lib/budget/enforce.js +151 -0
  17. package/dist/lib/budget/ledger.d.ts +61 -0
  18. package/dist/lib/budget/ledger.js +107 -0
  19. package/dist/lib/budget/preflight.d.ts +110 -0
  20. package/dist/lib/budget/preflight.js +200 -0
  21. package/dist/lib/checkpoint.d.ts +54 -0
  22. package/dist/lib/checkpoint.js +56 -0
  23. package/dist/lib/cloud/rush.js +18 -0
  24. package/dist/lib/exec.d.ts +36 -0
  25. package/dist/lib/exec.js +192 -4
  26. package/dist/lib/git.d.ts +18 -0
  27. package/dist/lib/git.js +67 -4
  28. package/dist/lib/loop.d.ts +145 -0
  29. package/dist/lib/loop.js +330 -0
  30. package/dist/lib/mcp.d.ts +7 -0
  31. package/dist/lib/mcp.js +24 -0
  32. package/dist/lib/models.d.ts +11 -0
  33. package/dist/lib/models.js +21 -0
  34. package/dist/lib/plugins.js +5 -2
  35. package/dist/lib/pricing/cost.d.ts +46 -0
  36. package/dist/lib/pricing/cost.js +71 -0
  37. package/dist/lib/pricing/index.d.ts +8 -0
  38. package/dist/lib/pricing/index.js +8 -0
  39. package/dist/lib/pricing/prices.json +138 -0
  40. package/dist/lib/pricing/table.d.ts +17 -0
  41. package/dist/lib/pricing/table.js +73 -0
  42. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  43. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  44. package/dist/lib/secrets/agent.d.ts +134 -0
  45. package/dist/lib/secrets/agent.js +501 -0
  46. package/dist/lib/secrets/bundles.d.ts +21 -0
  47. package/dist/lib/secrets/bundles.js +43 -0
  48. package/dist/lib/session/db.d.ts +40 -0
  49. package/dist/lib/session/db.js +84 -2
  50. package/dist/lib/session/discover.d.ts +2 -0
  51. package/dist/lib/session/discover.js +126 -2
  52. package/dist/lib/session/render.d.ts +2 -0
  53. package/dist/lib/session/render.js +1 -1
  54. package/dist/lib/session/types.d.ts +4 -0
  55. package/dist/lib/teams/agents.d.ts +32 -0
  56. package/dist/lib/teams/agents.js +66 -3
  57. package/dist/lib/teams/api.js +20 -0
  58. package/dist/lib/teams/parsers.js +16 -4
  59. package/dist/lib/types.d.ts +48 -0
  60. package/dist/lib/workflows.d.ts +56 -0
  61. package/dist/lib/workflows.js +72 -5
  62. package/package.json +2 -1
@@ -135,6 +135,25 @@ function hasTransitiveDep(byName, startName, targetName, seen = new Set()) {
135
135
  }
136
136
  return false;
137
137
  }
138
+ /**
139
+ * Single-quote a string for safe interpolation into a POSIX `sh -c` command.
140
+ * Wraps in single quotes and escapes embedded single quotes via the standard
141
+ * `'\''` close-escape-reopen idiom, so arbitrary prompts/paths can't break out
142
+ * of quoting or inject shell syntax.
143
+ */
144
+ function shSingleQuote(value) {
145
+ return `'${value.replace(/'/g, `'\\''`)}'`;
146
+ }
147
+ /**
148
+ * Wrap a teammate argv in a POSIX shell command that runs it and then records
149
+ * the real exit code to `exitCodePath`. `echo $?` captures the status of the
150
+ * preceding command, so the sentinel reflects the underlying CLI's exit code,
151
+ * not the shell's. Single source of truth shared by launchProcess() and its
152
+ * test. See reapProcess() for how the sentinel is consumed.
153
+ */
154
+ export function buildSentinelCommand(cmd, exitCodePath) {
155
+ return `${cmd.map(shSingleQuote).join(' ')}; echo $? > ${shSingleQuote(exitCodePath)}`;
156
+ }
138
157
  /**
139
158
  * Capture a stable identifier for a process at the moment it was started.
140
159
  * Used to defeat PID reuse: a kill(pid, ...) is only safe when the process
@@ -456,6 +475,15 @@ export class AgentProcess {
456
475
  async getMetaPath() {
457
476
  return path.join(await this.getAgentDir(), 'meta.json');
458
477
  }
478
+ /**
479
+ * Path to the exit-code sentinel. The launcher wraps the teammate command in
480
+ * a shell that writes the underlying CLI's `$?` here once it exits. Detached
481
+ * teammates can't be wait()ed on by the parent, so this file is the only
482
+ * durable record of the real exit status — see reapProcess().
483
+ */
484
+ async getExitCodePath() {
485
+ return path.join(await this.getAgentDir(), 'exit_code');
486
+ }
459
487
  toDict() {
460
488
  return {
461
489
  agent_id: this.agentId,
@@ -748,14 +776,37 @@ export class AgentProcess {
748
776
  }
749
777
  await this.saveMeta();
750
778
  }
779
+ /**
780
+ * Recover the teammate's exit status after its process is gone.
781
+ *
782
+ * The teammate is spawned detached + unref()'d (see launchProcess), so the
783
+ * parent never gets the child's exit code from the OS. Instead the launcher
784
+ * wraps the command in a shell that records `$?` to the exit-code sentinel.
785
+ * This reads that file:
786
+ * - still alive -> null (no verdict yet)
787
+ * - sentinel present -> the real exit code (0 = success)
788
+ * - sentinel absent -> 1 (the shell was killed before it could write
789
+ * it, e.g. SIGKILL on timeout/stop — a real
790
+ * failure)
791
+ *
792
+ * Returning a real code (not a hardcoded 1) is what lets agents whose stream
793
+ * never emits a parsed terminal event — kimi, antigravity, droid — be marked
794
+ * completed on success instead of falsely failed.
795
+ */
751
796
  async reapProcess() {
752
797
  if (!this.pid)
753
798
  return null;
754
- try {
755
- process.kill(this.pid, 0);
799
+ // isProcessAlive() applies the start-time guard, so a recycled PID now
800
+ // owned by an unrelated process doesn't read as still-alive.
801
+ if (this.isProcessAlive())
756
802
  return null;
803
+ try {
804
+ const raw = (await fs.readFile(await this.getExitCodePath(), 'utf-8')).trim();
805
+ const code = Number.parseInt(raw, 10);
806
+ return Number.isNaN(code) ? 1 : code;
757
807
  }
758
808
  catch {
809
+ // No sentinel: the shell died before recording $? (killed mid-run).
759
810
  return 1;
760
811
  }
761
812
  }
@@ -998,7 +1049,19 @@ export class AgentManager {
998
1049
  const stdoutPath = await agent.getStdoutPath();
999
1050
  const stdoutFile = await fs.open(stdoutPath, 'w');
1000
1051
  const stdoutFd = stdoutFile.fd;
1001
- const childProcess = spawn(cmd[0], cmd.slice(1), {
1052
+ // Wrap the teammate command in a shell that records the underlying CLI's
1053
+ // exit code to a sentinel file. Detached + unref()'d children can't be
1054
+ // wait()ed on by this parent, so the sentinel is the only durable record
1055
+ // of the real exit status — reapProcess() reads it to decide
1056
+ // completed-vs-failed for agents whose stream emits no parsed terminal
1057
+ // event (kimi, antigravity, droid). Remove any stale sentinel from a
1058
+ // prior run of the same agent id first so a restart can't read it.
1059
+ const exitCodePath = await agent.getExitCodePath();
1060
+ await fs.rm(exitCodePath, { force: true }).catch(() => { });
1061
+ const wrappedCmd = buildSentinelCommand(cmd, exitCodePath);
1062
+ // detached:true makes the shell the process-group leader, so stop()'s
1063
+ // `kill(-pid)` still reaches the underlying CLI through the group.
1064
+ const childProcess = spawn('/bin/sh', ['-c', wrappedCmd], {
1002
1065
  stdio: ['ignore', stdoutFd, stdoutFd],
1003
1066
  cwd: agent.cwd || undefined,
1004
1067
  detached: true,
@@ -139,6 +139,26 @@ export async function handleSpawn(manager, taskName, agentType, prompt, cwd, mod
139
139
  const resolvedMode = resolveMode(mode, defaultMode);
140
140
  const resolvedEffort = effort ?? 'medium';
141
141
  debug(`[spawn] Spawning ${agentType} agent for task "${taskName}" [${resolvedMode}] effort=${resolvedEffort}${profileName ? ` profile=${profileName}` : ''}...`);
142
+ // Budget pre-flight gate (issue #346). Teammates inherit the project's caps:
143
+ // before launching one, project its estimated cost onto current spend and
144
+ // refuse when on_exceed:block would be breached. Cross-vendor by construction
145
+ // — a Claude teammate and a Codex teammate draw down the same per_project /
146
+ // per_day pool. Dormant (no-op) when no caps are configured.
147
+ {
148
+ const gateCwd = cwd || workspaceDir || worktreePath || process.cwd();
149
+ const { runPreflightGate } = await import('../budget/preflight.js');
150
+ const gate = runPreflightGate({
151
+ agent: agentType,
152
+ model: model ?? `${agentType}-default`,
153
+ mode: resolvedMode,
154
+ prompt,
155
+ project: gateCwd,
156
+ cwd: gateCwd,
157
+ });
158
+ if (!gate.dormant && !gate.decision.allow) {
159
+ throw new Error(`[budget] BLOCKED teammate "${taskName}" (${agentType}): ${gate.decision.reason}`);
160
+ }
161
+ }
142
162
  const agent = await manager.spawn(taskName, agentType, prompt, cwd, resolvedMode, resolvedEffort, parentSessionId, workspaceDir, version, name, after, model, envOverrides, taskType, cloudProvider, cloudSessionId, cloudRepo, cloudBranch, worktreeName, worktreePath, profileName);
143
163
  debug(`[spawn] Spawned ${agentType} agent ${agent.agentId} for task "${taskName}"`);
144
164
  return {
@@ -917,9 +917,16 @@ function normalizeGrok(raw) {
917
917
  // - {"role":"assistant","content":"..."} → final message
918
918
  // - {"role":"assistant","tool_calls":[{"function":{"name":"Bash","arguments":"<json>"}}]} → tool use
919
919
  // - {"role":"tool","tool_call_id":"...","content":"..."} → tool result
920
- // - {"role":"meta","type":"session.resume_hint","session_id":"..."} → init / session id
921
- // Tool arguments are JSON-stringified inside `function.arguments` and must be
922
- // parsed before extracting paths/commands. Verified against live `kimi` runs.
920
+ // - {"role":"meta","type":"session.resume_hint","session_id":"..."} → terminal/result
921
+ // Kimi emits NO dedicated result/turn-complete event and NO init event. The
922
+ // `session.resume_hint` meta is its terminal marker: emitted exactly once, as
923
+ // the LAST line, on clean completion (it carries the `kimi -r <id>` resume
924
+ // command). We map it to a success `result` so the team runner resolves status
925
+ // from the stream; the run's exit code remains the safety net for crashes that
926
+ // never reach the hint. Tool arguments are JSON-stringified inside
927
+ // `function.arguments` and must be parsed before extracting paths/commands.
928
+ // Verified against live `kimi` runs (no-tool and tool-using) — see
929
+ // __tests__/testdata/kimi-stream-*.jsonl.
923
930
  function normalizeKimi(raw) {
924
931
  const timestamp = new Date().toISOString();
925
932
  if (!raw || typeof raw !== 'object') {
@@ -1044,9 +1051,14 @@ function normalizeKimi(raw) {
1044
1051
  if (role === 'meta') {
1045
1052
  const metaType = typeof raw.type === 'string' ? raw.type : '';
1046
1053
  if (metaType === 'session.resume_hint') {
1054
+ // Kimi's terminal marker (see header). Emit a success `result` so the
1055
+ // team runner's terminal-event detection resolves the teammate to
1056
+ // COMPLETED from the stream. session_id is preserved for cross-
1057
+ // referencing — readNewEvents() captures it off any event.
1047
1058
  return [{
1048
- type: 'init',
1059
+ type: 'result',
1049
1060
  agent: 'kimi',
1061
+ status: 'success',
1050
1062
  session_id: typeof raw.session_id === 'string' ? raw.session_id : null,
1051
1063
  timestamp: timestamp,
1052
1064
  }];
@@ -22,6 +22,43 @@ export interface RunDefaults {
22
22
  export type RunConfig = Partial<Record<AgentId, AgentRunConfig>> & {
23
23
  defaults?: Record<string, RunDefaults>;
24
24
  };
25
+ /**
26
+ * What to do when a configured budget cap would be exceeded (issue #346).
27
+ * `block` refuses to launch (or kills a running child) and exits non-zero so
28
+ * CI/headless/teams/cloud all inherit the decision. `warn` prints the overrun
29
+ * but proceeds — useful for soft rollout / observability-only.
30
+ */
31
+ export type BudgetOnExceed = 'block' | 'warn';
32
+ /**
33
+ * `budget:` block in agents.yaml — cross-vendor spend guardrails (issue #346).
34
+ *
35
+ * Resolution is project > user (same precedence as `run:`); see
36
+ * `resolveBudgetConfig` in lib/budget/config.ts. Every cap is in USD. A cap is
37
+ * "unset" when undefined — only set caps are enforced. `per_agent` caps apply
38
+ * to one agent's spend; the top-level caps (`per_run`, `per_day`,
39
+ * `per_project`) aggregate ACROSS every vendor the CLI dispatches, which is the
40
+ * cross-vendor property no single-vendor control has.
41
+ */
42
+ export interface BudgetConfig {
43
+ /** Display currency. Only "USD" is priced today; carried for forward-compat. */
44
+ currency?: string;
45
+ /** Hard cap on the estimated/actual cost of a single run. */
46
+ per_run?: number;
47
+ /** Hard cap on total spend attributed to the current day (local date). */
48
+ per_day?: number;
49
+ /** Per-agent daily caps, keyed by agent id (e.g. { claude: 30, codex: 20 }). */
50
+ per_agent?: Partial<Record<AgentId, number>>;
51
+ /** Hard cap on cumulative spend attributed to the current project. */
52
+ per_project?: number;
53
+ /** block (refuse/kill) or warn (proceed). Defaults to block. */
54
+ on_exceed?: BudgetOnExceed;
55
+ /**
56
+ * Interactive confirm threshold (USD). When a run's pre-flight estimate is at
57
+ * or above this, prompt before launching (unless --yes). Does NOT gate a hard
58
+ * block — a cap breach always blocks regardless of this value.
59
+ */
60
+ require_confirm_over?: number;
61
+ }
25
62
  /** Preview features that users can opt into via `agents beta`. */
26
63
  export type BetaFeatureName = 'drive' | 'factory';
27
64
  /** Subset of chalk color names used for agent-specific terminal output. */
@@ -210,6 +247,8 @@ export interface InstalledHook {
210
247
  export interface Manifest {
211
248
  agents?: Partial<Record<AgentId, string>>;
212
249
  run?: RunConfig;
250
+ /** Spend guardrails (issue #346). Project-local block overrides user. */
251
+ budget?: BudgetConfig;
213
252
  beta?: {
214
253
  enabled?: BetaFeatureName[];
215
254
  };
@@ -516,6 +555,15 @@ export interface ExtraRepoConfig {
516
555
  export interface Meta {
517
556
  agents?: Partial<Record<AgentId, string>>;
518
557
  run?: RunConfig;
558
+ /** macOS secrets-agent config. `auto` makes the first real keychain read of a
559
+ * `session`-tier bundle populate the broker so concurrent runs read silently. */
560
+ secrets?: {
561
+ agent?: {
562
+ auto?: boolean;
563
+ };
564
+ };
565
+ /** Spend guardrails (issue #346). User-global caps; project agents.yaml overrides. */
566
+ budget?: BudgetConfig;
519
567
  beta?: {
520
568
  enabled?: BetaFeatureName[];
521
569
  };
@@ -6,6 +6,21 @@
6
6
  * are composed at runtime by `agents run <workflow>`.
7
7
  */
8
8
  import type { AgentId } from './types.js';
9
+ /**
10
+ * The `loop:` block as it appears in WORKFLOW.md frontmatter (YAML, snake_case).
11
+ * Parsed defensively and translated to the camelCase LoopConfig the driver
12
+ * consumes (src/lib/loop.ts). See docs/07-entrypoints-and-loops.md.
13
+ */
14
+ export interface LoopConfigRaw {
15
+ /** Stop condition. Only `signal` is supported today. */
16
+ until?: 'signal';
17
+ /** Hard cap on iterations. */
18
+ max_iterations?: number;
19
+ /** Token hard-cap, enforced outside the agent. */
20
+ budget?: number;
21
+ /** Delay between iterations ("0" back-to-back, "30m" paces). */
22
+ interval?: string;
23
+ }
9
24
  /** Parsed WORKFLOW.md frontmatter. */
10
25
  export interface WorkflowFrontmatter {
11
26
  name: string;
@@ -22,6 +37,12 @@ export interface WorkflowFrontmatter {
22
37
  * Pass `--no-auto-secrets` to skip this injection.
23
38
  */
24
39
  secrets?: string[];
40
+ /**
41
+ * Optional loop block: wraps the workflow in a bounded until-condition loop
42
+ * (issue #332). When present, `agents run <workflow>` honors it without a
43
+ * `--loop` flag. Validated/coerced in parseWorkflowFrontmatter.
44
+ */
45
+ loop?: LoopConfigRaw;
25
46
  }
26
47
  /** A workflow found during repo discovery. */
27
48
  export interface DiscoveredWorkflow {
@@ -39,6 +60,41 @@ export interface InstalledWorkflow {
39
60
  }
40
61
  /** Parse WORKFLOW.md frontmatter from a workflow directory. Returns null if invalid. */
41
62
  export declare function parseWorkflowFrontmatter(workflowDir: string): WorkflowFrontmatter | null;
63
+ /**
64
+ * Defensively coerce a frontmatter `loop:` value into a LoopConfigRaw.
65
+ *
66
+ * Mirrors the asStringArray discipline above: a malformed field is dropped to
67
+ * undefined rather than passed through, so the loop driver never sees a bad
68
+ * shape. Returns undefined when `loop:` is absent or not an object, or when no
69
+ * recognized field survives coercion (an all-garbage block is treated as
70
+ * "no loop", not "empty loop").
71
+ *
72
+ * Field rules:
73
+ * - until: only the literal `signal` is accepted; anything else dropped.
74
+ * - max_iterations: a finite positive integer; non-numbers/<=0 dropped.
75
+ * - budget: a finite positive number (tokens); non-numbers/<=0 dropped.
76
+ * - interval: a string (e.g. "0", "30m"); non-strings dropped.
77
+ */
78
+ export declare function parseLoopBlock(v: unknown): LoopConfigRaw | undefined;
79
+ /**
80
+ * Decide which subagent .md stems a workflow may use, given the discovered
81
+ * subagent files and the parsed `allowedAgents` frontmatter. This is the
82
+ * fail-closed security boundary for issue #324:
83
+ *
84
+ * - `allowedAgents === undefined` (field absent) -> NO restriction; allow all.
85
+ * - `allowedAgents === []` (present, empty) -> allow ZERO; copy none.
86
+ * - `allowedAgents = [a, b]` -> allow only those stems.
87
+ *
88
+ * An explicit empty array must NEVER widen to "allow all" — that would copy
89
+ * every subagent definition into the run, granting MORE access than declared.
90
+ *
91
+ * `available` are the .md filenames found in subagents/ (e.g. `security.md`).
92
+ * Returns the stems to copy and any allowedAgents entries with no matching file.
93
+ */
94
+ export declare function resolveAllowedSubagents(available: string[], allowedAgents: string[] | undefined): {
95
+ allowedStems: string[];
96
+ missing: string[];
97
+ };
42
98
  /** Count subagent .md files in a workflow's subagents/ directory. */
43
99
  export declare function countWorkflowSubagents(workflowDir: string): number;
44
100
  /**
@@ -28,21 +28,88 @@ export function parseWorkflowFrontmatter(workflowDir) {
28
28
  const parsed = yaml.parse(frontmatter);
29
29
  if (!parsed || typeof parsed !== 'object')
30
30
  return null;
31
+ // Capability-scoping fields are wired into the run (see src/commands/exec.ts);
32
+ // coerce to string arrays defensively so a malformed `tools: foo` (scalar) or
33
+ // `tools: [Read, 3]` (mixed) never reaches buildExecCommand as a bad shape.
34
+ const asStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === 'string') ? v : undefined;
31
35
  return {
32
36
  name: parsed.name || '',
33
37
  description: parsed.description || '',
34
38
  model: parsed.model,
35
- tools: parsed.tools,
36
- skills: parsed.skills,
37
- mcpServers: parsed.mcpServers,
38
- allowedAgents: parsed.allowedAgents,
39
- secrets: parsed.secrets,
39
+ tools: asStringArray(parsed.tools),
40
+ skills: asStringArray(parsed.skills),
41
+ mcpServers: asStringArray(parsed.mcpServers),
42
+ allowedAgents: asStringArray(parsed.allowedAgents),
43
+ secrets: asStringArray(parsed.secrets),
44
+ loop: parseLoopBlock(parsed.loop),
40
45
  };
41
46
  }
42
47
  catch {
43
48
  return null;
44
49
  }
45
50
  }
51
+ /**
52
+ * Defensively coerce a frontmatter `loop:` value into a LoopConfigRaw.
53
+ *
54
+ * Mirrors the asStringArray discipline above: a malformed field is dropped to
55
+ * undefined rather than passed through, so the loop driver never sees a bad
56
+ * shape. Returns undefined when `loop:` is absent or not an object, or when no
57
+ * recognized field survives coercion (an all-garbage block is treated as
58
+ * "no loop", not "empty loop").
59
+ *
60
+ * Field rules:
61
+ * - until: only the literal `signal` is accepted; anything else dropped.
62
+ * - max_iterations: a finite positive integer; non-numbers/<=0 dropped.
63
+ * - budget: a finite positive number (tokens); non-numbers/<=0 dropped.
64
+ * - interval: a string (e.g. "0", "30m"); non-strings dropped.
65
+ */
66
+ export function parseLoopBlock(v) {
67
+ if (!v || typeof v !== 'object' || Array.isArray(v))
68
+ return undefined;
69
+ const raw = v;
70
+ const out = {};
71
+ if (raw.until === 'signal')
72
+ out.until = 'signal';
73
+ if (typeof raw.max_iterations === 'number'
74
+ && Number.isFinite(raw.max_iterations)
75
+ && Number.isInteger(raw.max_iterations)
76
+ && raw.max_iterations > 0) {
77
+ out.max_iterations = raw.max_iterations;
78
+ }
79
+ if (typeof raw.budget === 'number' && Number.isFinite(raw.budget) && raw.budget > 0) {
80
+ out.budget = raw.budget;
81
+ }
82
+ if (typeof raw.interval === 'string')
83
+ out.interval = raw.interval;
84
+ return Object.keys(out).length > 0 ? out : undefined;
85
+ }
86
+ /**
87
+ * Decide which subagent .md stems a workflow may use, given the discovered
88
+ * subagent files and the parsed `allowedAgents` frontmatter. This is the
89
+ * fail-closed security boundary for issue #324:
90
+ *
91
+ * - `allowedAgents === undefined` (field absent) -> NO restriction; allow all.
92
+ * - `allowedAgents === []` (present, empty) -> allow ZERO; copy none.
93
+ * - `allowedAgents = [a, b]` -> allow only those stems.
94
+ *
95
+ * An explicit empty array must NEVER widen to "allow all" — that would copy
96
+ * every subagent definition into the run, granting MORE access than declared.
97
+ *
98
+ * `available` are the .md filenames found in subagents/ (e.g. `security.md`).
99
+ * Returns the stems to copy and any allowedAgents entries with no matching file.
100
+ */
101
+ export function resolveAllowedSubagents(available, allowedAgents) {
102
+ const stems = available.filter(f => f.endsWith('.md')).map(f => f.replace(/\.md$/, ''));
103
+ if (allowedAgents === undefined) {
104
+ return { allowedStems: stems, missing: [] };
105
+ }
106
+ const allow = new Set(allowedAgents);
107
+ const present = new Set(stems);
108
+ return {
109
+ allowedStems: stems.filter(s => allow.has(s)),
110
+ missing: allowedAgents.filter(a => !present.has(a)),
111
+ };
112
+ }
46
113
  /** Count subagent .md files in a workflow's subagents/ directory. */
47
114
  export function countWorkflowSubagents(workflowDir) {
48
115
  const subagentsDir = path.join(workflowDir, 'subagents');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.17",
3
+ "version": "1.20.18",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,6 +23,7 @@
23
23
  "files": [
24
24
  "dist/**/*.js",
25
25
  "dist/**/*.d.ts",
26
+ "dist/**/*.json",
26
27
  "dist/lib/secrets/Agents CLI.app/**",
27
28
  "scripts/postinstall.js",
28
29
  "scripts/install-helper.js",