@phnx-labs/agents-cli 1.22.69 → 1.22.71

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 (130) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +34 -9
  3. package/dist/bootstrap.js +4 -4
  4. package/dist/commands/accounts.js +23 -7
  5. package/dist/commands/exec.js +2 -2
  6. package/dist/commands/import.js +2 -2
  7. package/dist/commands/models.js +2 -2
  8. package/dist/commands/permissions.js +2 -2
  9. package/dist/commands/repo.js +2 -2
  10. package/dist/commands/rules.js +1 -1
  11. package/dist/commands/send.js +9 -3
  12. package/dist/commands/sessions-export.d.ts +5 -1
  13. package/dist/commands/sessions-export.js +100 -24
  14. package/dist/commands/sessions-import.d.ts +2 -1
  15. package/dist/commands/sessions-import.js +85 -21
  16. package/dist/commands/traces.js +1 -1
  17. package/dist/lib/account-capabilities.js +0 -2
  18. package/dist/lib/account-registry.d.ts +3 -3
  19. package/dist/lib/account-registry.js +25 -7
  20. package/dist/lib/accounting/usage-sync.d.ts +1 -1
  21. package/dist/lib/accounting/usage-sync.js +3 -3
  22. package/dist/lib/acp/client.d.ts +1 -1
  23. package/dist/lib/acp/client.js +12 -1
  24. package/dist/lib/acp/harnesses.js +1 -1
  25. package/dist/lib/add-dir.js +0 -2
  26. package/dist/lib/agent-cli-commands.js +0 -2
  27. package/dist/lib/agent-spec/agents.d.ts +1 -1
  28. package/dist/lib/agent-spec/agents.js +2 -83
  29. package/dist/lib/browser/ipc.d.ts +34 -0
  30. package/dist/lib/browser/ipc.js +149 -20
  31. package/dist/lib/browser/remote-control.d.ts +6 -3
  32. package/dist/lib/browser/remote-control.js +6 -3
  33. package/dist/lib/browser/service.d.ts +4 -1
  34. package/dist/lib/browser/service.js +7 -1
  35. package/dist/lib/browser/types.d.ts +3 -1
  36. package/dist/lib/channels/registry.d.ts +6 -0
  37. package/dist/lib/channels/send.d.ts +11 -2
  38. package/dist/lib/channels/send.js +11 -2
  39. package/dist/lib/cloud/rush.d.ts +10 -2
  40. package/dist/lib/cloud/rush.js +15 -9
  41. package/dist/lib/daemon/auth-sync-service.js +1 -1
  42. package/dist/lib/daemon/browser-task-reap-service.js +1 -1
  43. package/dist/lib/daemon/daemon.js +41 -11
  44. package/dist/lib/daemon/heartbeat-service.js +3 -3
  45. package/dist/lib/daemon/keychain-reap-service.js +1 -1
  46. package/dist/lib/daemon/runner.d.ts +18 -1
  47. package/dist/lib/daemon/runner.js +237 -80
  48. package/dist/lib/daemon/self-heal-service.js +13 -3
  49. package/dist/lib/daemon/self-update-service.d.ts +174 -0
  50. package/dist/lib/daemon/self-update-service.js +353 -0
  51. package/dist/lib/daemon/state-dir-check-service.js +3 -3
  52. package/dist/lib/daemon/usage-sync-service.js +1 -1
  53. package/dist/lib/daemon/watchdog-service.js +4 -4
  54. package/dist/lib/daemon-services.d.ts +1 -1
  55. package/dist/lib/daemon-services.js +5 -0
  56. package/dist/lib/device-config.d.ts +12 -1
  57. package/dist/lib/device-config.js +63 -13
  58. package/dist/lib/exec-bounded.d.ts +52 -0
  59. package/dist/lib/exec-bounded.js +113 -0
  60. package/dist/lib/exec.d.ts +2 -2
  61. package/dist/lib/exec.js +3 -34
  62. package/dist/lib/feed/events.d.ts +22 -14
  63. package/dist/lib/feed/events.js +84 -44
  64. package/dist/lib/feed-broadcast.d.ts +12 -29
  65. package/dist/lib/feed-broadcast.js +28 -27
  66. package/dist/lib/fleet-shared-state.d.ts +12 -5
  67. package/dist/lib/fleet-shared-state.js +50 -20
  68. package/dist/lib/fs-atomic.d.ts +11 -0
  69. package/dist/lib/fs-atomic.js +60 -0
  70. package/dist/lib/hooks/install.js +0 -87
  71. package/dist/lib/hosts/reconcile.d.ts +11 -4
  72. package/dist/lib/hosts/reconcile.js +31 -5
  73. package/dist/lib/installations/strategies.js +1 -1
  74. package/dist/lib/mcp-registry.js +0 -13
  75. package/dist/lib/mcp.js +2 -2
  76. package/dist/lib/model-tiers.js +1 -1
  77. package/dist/lib/models.js +0 -63
  78. package/dist/lib/notify.d.ts +11 -0
  79. package/dist/lib/notify.js +17 -4
  80. package/dist/lib/owner-message.d.ts +46 -3
  81. package/dist/lib/owner-message.js +26 -6
  82. package/dist/lib/permissions-registry.d.ts +0 -2
  83. package/dist/lib/permissions-registry.js +3 -50
  84. package/dist/lib/permissions.d.ts +3 -17
  85. package/dist/lib/permissions.js +4 -73
  86. package/dist/lib/project-resources.d.ts +12 -0
  87. package/dist/lib/project-resources.js +129 -0
  88. package/dist/lib/routine-process-cleanup.d.ts +2 -2
  89. package/dist/lib/routine-process-cleanup.js +45 -34
  90. package/dist/lib/rush-session.d.ts +19 -0
  91. package/dist/lib/rush-session.js +24 -0
  92. package/dist/lib/secrets/drivers/rush.js +2 -1
  93. package/dist/lib/secrets/reaper.d.ts +2 -2
  94. package/dist/lib/secrets/reaper.js +13 -10
  95. package/dist/lib/secrets/reserved-sync.d.ts +1 -1
  96. package/dist/lib/secrets/reserved-sync.js +4 -4
  97. package/dist/lib/self-update.d.ts +21 -8
  98. package/dist/lib/self-update.js +54 -31
  99. package/dist/lib/session/cloud.js +2 -1
  100. package/dist/lib/session/sync/backend.d.ts +61 -0
  101. package/dist/lib/session/sync/backend.js +89 -0
  102. package/dist/lib/session/sync/managed-config.d.ts +29 -0
  103. package/dist/lib/session/sync/managed-config.js +23 -0
  104. package/dist/lib/session/sync/managed-key.d.ts +45 -0
  105. package/dist/lib/session/sync/managed-key.js +128 -0
  106. package/dist/lib/session/sync/net-client.d.ts +65 -0
  107. package/dist/lib/session/sync/net-client.js +117 -0
  108. package/dist/lib/session/sync/provision.d.ts +19 -0
  109. package/dist/lib/session/sync/provision.js +38 -0
  110. package/dist/lib/session/sync/r2.d.ts +5 -2
  111. package/dist/lib/session/sync/r2.js +5 -2
  112. package/dist/lib/session/sync/worker-template.d.ts +6 -0
  113. package/dist/lib/session/sync/worker-template.js +847 -0
  114. package/dist/lib/sink-format.d.ts +34 -0
  115. package/dist/lib/sink-format.js +17 -0
  116. package/dist/lib/staleness/detectors/permissions.js +0 -20
  117. package/dist/lib/staleness/writers/commands.js +1 -1
  118. package/dist/lib/staleness/writers/hooks.js +2 -2
  119. package/dist/lib/subagents-registry.js +2 -12
  120. package/dist/lib/subagents.d.ts +0 -10
  121. package/dist/lib/subagents.js +0 -37
  122. package/dist/lib/tmux/orphan-reap.js +6 -4
  123. package/dist/lib/tmux/session.js +4 -1
  124. package/dist/lib/traces/classify.d.ts +8 -1
  125. package/dist/lib/traces/insights.d.ts +13 -1
  126. package/dist/lib/traces/insights.js +78 -3
  127. package/dist/lib/traces/sync.js +8 -3
  128. package/dist/lib/traces/worker-template.js +9 -5
  129. package/dist/lib/types.d.ts +1 -1
  130. package/package.json +1 -1
@@ -191,18 +191,6 @@ export function locateModelSource(agent, version) {
191
191
  return { path: pathBin, kind: 'cli' };
192
192
  return null;
193
193
  }
194
- if (agent === 'pi') {
195
- // omp (Oh My Pi) installs via `bun install -g`; a version-managed install
196
- // exposes it under node_modules/.bin/omp, otherwise it lives on PATH. We let
197
- // the CLI produce its own catalog via `omp models --json` (extractPiCatalog).
198
- const cli = path.join(versionDir, 'node_modules', '.bin', 'omp');
199
- if (fs.existsSync(cli))
200
- return { path: cli, kind: 'cli' };
201
- const pathBin = findOnPath('omp');
202
- if (pathBin)
203
- return { path: pathBin, kind: 'cli' };
204
- return null;
205
- }
206
194
  if (agent === 'muse') {
207
195
  // Muse Code is a self-updating native binary on PATH. It has no catalog
208
196
  // CLI; the published model IDs are fixed by Meta Model API docs. Point at
@@ -841,55 +829,6 @@ function extractKimiCatalog(binaryPath) {
841
829
  }
842
830
  return { models, aliases: {} };
843
831
  }
844
- /**
845
- * Extract Oh My Pi's catalog via `omp models --json`. omp is a cross-provider
846
- * aggregator: its catalog is the union of every provider it has a key for, so
847
- * ids are provider-qualified selectors (`anthropic/claude-opus-4-8`,
848
- * `openai/gpt-5.2`, `xai/grok-4`, `deepseek/deepseek-chat`, …) — exactly the
849
- * `provider/model` convention `ModelInfo.id` already uses. Output shape:
850
- * {"models":[{"provider":"anthropic","id":"claude-opus-4-8",
851
- * "selector":"anthropic/claude-opus-4-8","name":"Claude Opus 4.8",
852
- * "cost":{...}}, ...]}
853
- * The catalog is gated per-provider by key presence (no key -> that provider's
854
- * models are absent, and an empty env yields `{"models":[]}`). That is truthful:
855
- * the extractor surfaces exactly the providers the user has authenticated.
856
- * Pricing is attached uniformly by getModelCatalog via getModelPricing(id),
857
- * which strips the `provider/` prefix — so no per-catalog price shape here.
858
- */
859
- function extractPiCatalog(binaryPath) {
860
- let stdout;
861
- try {
862
- stdout = execFileSync(binaryPath, ['models', '--json'], {
863
- encoding: 'utf-8',
864
- stdio: ['ignore', 'pipe', 'ignore'],
865
- timeout: 15_000,
866
- maxBuffer: 64 * 1024 * 1024,
867
- });
868
- }
869
- catch {
870
- return { models: [], aliases: {} };
871
- }
872
- let parsed;
873
- try {
874
- parsed = JSON.parse(stdout.replace(/\x1b\[[0-9;]*[A-Za-z]/g, ''));
875
- }
876
- catch {
877
- return { models: [], aliases: {} };
878
- }
879
- if (!parsed || !Array.isArray(parsed.models))
880
- return { models: [], aliases: {} };
881
- const models = [];
882
- const seen = new Set();
883
- for (const m of parsed.models) {
884
- // Prefer the provider-qualified selector; fall back to provider/id.
885
- const id = m.selector || (m.provider && m.id ? `${m.provider}/${m.id}` : m.id);
886
- if (!id || seen.has(id))
887
- continue;
888
- seen.add(id);
889
- models.push({ id, displayName: typeof m.name === 'string' ? m.name : undefined });
890
- }
891
- return { models, aliases: {} };
892
- }
893
832
  /**
894
833
  * Build (or load from cache) the model catalog for a specific (agent, version).
895
834
  * Cache is keyed on source-file mtime (binary or js module), so re-extracts
@@ -967,8 +906,6 @@ export function getModelCatalog(agent, version) {
967
906
  ({ models, aliases } = extractKimiCatalog(src.path));
968
907
  else if (agent === 'grok')
969
908
  ({ models, aliases } = extractGrokCatalog(src.path));
970
- else if (agent === 'pi')
971
- ({ models, aliases } = extractPiCatalog(src.path));
972
909
  else if (agent === 'muse')
973
910
  ({ models, aliases } = extractMuseCatalog());
974
911
  }
@@ -16,6 +16,7 @@
16
16
  import type { OpenBlock } from './feed/feed.js';
17
17
  import type { Meta } from './types.js';
18
18
  import type { SendResult } from './channels/registry.js';
19
+ import { type SinkMessageFormat } from './sink-format.js';
19
20
  export interface OwnerNotifyOptions {
20
21
  /** Config source (defaults to `readMeta()`); lets callers/tests inject it. */
21
22
  meta?: Meta;
@@ -28,6 +29,16 @@ export interface OwnerNotifyOptions {
28
29
  thread?: string;
29
30
  attachments?: string[];
30
31
  from?: string;
32
+ /**
33
+ * Per-destination body composer (PHNX-3698). The owner policy fans one alert
34
+ * out to several channels (imessage + slack, …), and only Slack can render a
35
+ * labeled link — so when set, each destination gets the body shaped for its
36
+ * OWN resolved provider (Slack → `mrkdwn` `<url|label>` links, everything else
37
+ * → `plain` the human sentence, no URLs) instead of the single `text` going to
38
+ * every channel. Absent for callers whose body is already final (urgent
39
+ * blocks, monitor summaries): `text` is delivered verbatim to every channel.
40
+ */
41
+ composeForFormat?: (format: SinkMessageFormat) => string;
31
42
  }
32
43
  export interface NotifyResult {
33
44
  ok: boolean;
@@ -3,6 +3,7 @@ import { getOwnerNotifyDestinationsFromHumans } from './humans.js';
3
3
  import { registerBuiltinProviders } from './channels/providers/index.js';
4
4
  import { lookupTransport } from './channels/resolve.js';
5
5
  import { forwardOwnerNotifyToPeer } from './channels/owner-forward.js';
6
+ import { sinkMessageFormat } from './sink-format.js';
6
7
  export function formatUrgentBlockMessage(block) {
7
8
  const q = block.questions[0];
8
9
  const header = q?.header ? `[${q.header}] ` : '';
@@ -72,11 +73,20 @@ export async function sendToOwner(text, options = {}) {
72
73
  registerBuiltinProviders();
73
74
  const deliveries = [];
74
75
  for (const { channel, to: target } of addressable) {
75
- const { provider, error } = lookupTransport(channel, meta);
76
+ const { provider, providerName, error } = lookupTransport(channel, meta);
77
+ // Each destination gets the body shaped for the provider it ACTUALLY
78
+ // delivers through (the same `notify.transports` remap `lookupTransport`
79
+ // applied), so a Slack destination turns its ticket keys + session crumb
80
+ // into `<url|label>` links while a sibling iMessage copy stays plain
81
+ // (PHNX-3698). Without a composer the caller's already-final `text` is used
82
+ // for every channel.
83
+ const body = options.composeForFormat
84
+ ? options.composeForFormat(sinkMessageFormat(providerName))
85
+ : text;
76
86
  let result;
77
87
  try {
78
88
  result = provider
79
- ? await provider.send(text, {
89
+ ? await provider.send(body, {
80
90
  target,
81
91
  ownerScoped: options.target === undefined,
82
92
  dryRun: options.dryRun,
@@ -99,12 +109,15 @@ export async function sendToOwner(text, options = {}) {
99
109
  };
100
110
  }
101
111
  else {
102
- result = await forwardOwnerNotifyToPeer(text, channel, target, meta, {
112
+ result = await forwardOwnerNotifyToPeer(body, channel, target, meta, {
103
113
  envelope: { thread: options.thread, from: options.from },
104
114
  }) ?? result;
105
115
  }
106
116
  }
107
- deliveries.push(result);
117
+ // Echo the exact per-destination body delivered, so a caller (and
118
+ // `agents notify --dry-run --json`) can see Slack got the labeled-link
119
+ // variant and iMessage the plain one — the observable proof of PHNX-3698.
120
+ deliveries.push({ ...result, body });
108
121
  }
109
122
  if (deliveries.length === 1)
110
123
  return deliveries[0];
@@ -1,12 +1,55 @@
1
+ /**
2
+ * Compose an owner-bound phone ping through the SAME shaper `agents feed post`
3
+ * uses, so `agents notify` / `agents send --to owner` stop shipping a raw body
4
+ * dump (PHNX-3698).
5
+ *
6
+ * Before this, an owner send delivered the body verbatim: a long wall of prose,
7
+ * a `TEAM-N` key iMessage renders as dead text, and no way back to the session.
8
+ * Routing the body through {@link composeBroadcastMessage} makes an owner ping
9
+ * identical to an important feed post of the same event — short-shaped body,
10
+ * every ticket key linkified to its Linear URL, and the current session's
11
+ * `…/console/sessions/<id>` page as a tappable crumb.
12
+ *
13
+ * The session/agent/host are resolved the same way a feed post resolves them
14
+ * ({@link resolvePostIdentity} — the pid-registry / env walk), so a `notify`
15
+ * run inside an agent session inherits that session with no flag. Outside a
16
+ * session (a human at a shell) identity is undefined: the ping still gets
17
+ * short-shaping and ticket linkification, just no session crumb.
18
+ */
19
+ import { type FeedBroadcastContext } from './feed-broadcast.js';
20
+ import type { SinkMessageFormat } from './sink-format.js';
1
21
  export interface OwnerMessageOptions {
2
22
  /** Scannable subject line, when the caller has one (feed post does; notify does not). */
3
23
  title?: string;
4
24
  /** Explicit session id (`--session`); otherwise resolved from the run environment. */
5
25
  sessionId?: string;
26
+ /**
27
+ * Rendering vocabulary for this body (PHNX-3698). Slack `mrkdwn` linkifies the
28
+ * ticket keys and the session crumb as `<url|label>`; `plain` (the default)
29
+ * keeps the human sentence with no URLs, for iMessage / owner-scoped rush.
30
+ * The owner fan-out passes one format per destination.
31
+ */
32
+ format?: SinkMessageFormat;
6
33
  }
7
34
  /**
8
- * Shape a raw owner-send body into the composed broadcast message. Pure except
9
- * for the identity/index reads that {@link resolvePostIdentity} /
10
- * {@link getSessionById} already perform for feed posts.
35
+ * Resolve the run identity into the broadcast context an owner ping is built
36
+ * from — the shared shape a `feed post` uses. Pure except for the identity/index
37
+ * reads that {@link resolvePostIdentity} / {@link getSessionById} already perform
38
+ * for feed posts. Built ONCE so the owner fan-out can render it in more than one
39
+ * format (Slack vs iMessage) without re-walking the pid registry per destination.
40
+ */
41
+ export declare function ownerMessageContext(rawText: string, opts?: OwnerMessageOptions): FeedBroadcastContext;
42
+ /**
43
+ * Shape a raw owner-send body into the composed broadcast message. `format`
44
+ * decides how links surface — `plain` (default) for iMessage / owner-scoped rush,
45
+ * `mrkdwn` for a Slack owner destination (PHNX-3698).
11
46
  */
12
47
  export declare function composeOwnerMessage(rawText: string, opts?: OwnerMessageOptions): string;
48
+ /**
49
+ * A per-format composer bound to ONE resolved context — the shape the owner
50
+ * fan-out wants (PHNX-3698). `agents notify` / `agents send --to owner` build
51
+ * this once, use `compose('plain')` for the envelope's display body, and hand
52
+ * `compose` to `sendToOwner` so each policy destination re-renders in its own
53
+ * format without re-walking the pid registry.
54
+ */
55
+ export declare function ownerMessageComposer(rawText: string, opts?: OwnerMessageOptions): (format: SinkMessageFormat) => string;
@@ -21,17 +21,19 @@ import { resolvePostIdentity } from './feed-post.js';
21
21
  import { getSessionById, resolveFullSessionId } from './session/db.js';
22
22
  import { linearIssueUrl } from './session/linear.js';
23
23
  /**
24
- * Shape a raw owner-send body into the composed broadcast message. Pure except
25
- * for the identity/index reads that {@link resolvePostIdentity} /
26
- * {@link getSessionById} already perform for feed posts.
24
+ * Resolve the run identity into the broadcast context an owner ping is built
25
+ * from — the shared shape a `feed post` uses. Pure except for the identity/index
26
+ * reads that {@link resolvePostIdentity} / {@link getSessionById} already perform
27
+ * for feed posts. Built ONCE so the owner fan-out can render it in more than one
28
+ * format (Slack vs iMessage) without re-walking the pid registry per destination.
27
29
  */
28
- export function composeOwnerMessage(rawText, opts = {}) {
30
+ export function ownerMessageContext(rawText, opts = {}) {
29
31
  const identity = resolvePostIdentity({ sessionId: opts.sessionId });
30
32
  // A footer crumb that would 404 (an 8-char short id) is upgraded to the full
31
33
  // indexed id so the console URL resolves; a full/native id passes through.
32
34
  const session = resolveFullSessionId(identity?.sessionId);
33
35
  const ticket = session ? getSessionById(session)?.ticketId : undefined;
34
- const ctx = {
36
+ return {
35
37
  ...(opts.title?.trim() ? { title: opts.title.trim() } : {}),
36
38
  text: rawText,
37
39
  level: 'important',
@@ -40,5 +42,23 @@ export function composeOwnerMessage(rawText, opts = {}) {
40
42
  ...(identity?.host ? { host: identity.host } : {}),
41
43
  ...(session ? { session } : {}),
42
44
  };
43
- return composeBroadcastMessage(ctx);
45
+ }
46
+ /**
47
+ * Shape a raw owner-send body into the composed broadcast message. `format`
48
+ * decides how links surface — `plain` (default) for iMessage / owner-scoped rush,
49
+ * `mrkdwn` for a Slack owner destination (PHNX-3698).
50
+ */
51
+ export function composeOwnerMessage(rawText, opts = {}) {
52
+ return composeBroadcastMessage(ownerMessageContext(rawText, opts), opts.format ?? 'plain');
53
+ }
54
+ /**
55
+ * A per-format composer bound to ONE resolved context — the shape the owner
56
+ * fan-out wants (PHNX-3698). `agents notify` / `agents send --to owner` build
57
+ * this once, use `compose('plain')` for the envelope's display body, and hand
58
+ * `compose` to `sendToOwner` so each policy destination re-renders in its own
59
+ * format without re-walking the pid registry.
60
+ */
61
+ export function ownerMessageComposer(rawText, opts = {}) {
62
+ const ctx = ownerMessageContext(rawText, opts);
63
+ return (format) => composeBroadcastMessage(ctx, format);
44
64
  }
@@ -1,8 +1,6 @@
1
1
  import type { AgentId, PermissionSet } from './types.js';
2
2
  /** Canonical tool (lowercased) -> Grok's lowercase tool vocabulary. */
3
3
  export declare const GROK_TOOL_BY_CANONICAL: Record<string, string | undefined>;
4
- /** Canonical tool -> Kiro CLI v3 capability id. */
5
- export declare const KIRO_CAPABILITY_BY_TOOL: Record<string, string | undefined>;
6
4
  /** Canonical tool -> OpenClaw tool id (tool-level granularity only). */
7
5
  export declare const CANONICAL_TO_OPENCLAW_TOOL: Record<string, string>;
8
6
  /** Canonical tool -> Antigravity's action namespace. */
@@ -7,7 +7,7 @@
7
7
  * behind `agents permissions add <path>` used to dispatch through hand-written
8
8
  * 3-arm switches while
9
9
  * `applyPermissionsToVersion` wrote 13 harnesses, so permissions were written
10
- * for cursor, antigravity, grok, kimi, droid, copilot, kiro, openclaw and
10
+ * for cursor, antigravity, grok, kimi, droid, copilot, openclaw and
11
11
  * hermes and then reported as absent for all ten (RUSH-2676).
12
12
  *
13
13
  * The key set is pinned to `capableAgents('allowlist')` by
@@ -24,7 +24,7 @@
24
24
  * ## Every reverse projection is lossy, and says how
25
25
  *
26
26
  * The forward direction discards information — several canonical tools collapse
27
- * onto one native id (`Read`/`Grep`/`Glob` all become Kiro's `fs_read`), and
27
+ * onto one native id (`Read`/`Grep`/`Glob` all become a coarse capability), and
28
28
  * pattern grammars differ (`git:*` becomes `git *`). Reading back therefore
29
29
  * recovers a permission set that GRANTS THE SAME ACCESS, not the byte-identical
30
30
  * strings that were written. Each target documents its own collapse. Callers that
@@ -45,21 +45,6 @@ export const GROK_TOOL_BY_CANONICAL = {
45
45
  grep: 'grep',
46
46
  webfetch: 'webfetch',
47
47
  };
48
- /** Canonical tool -> Kiro CLI v3 capability id. */
49
- export const KIRO_CAPABILITY_BY_TOOL = {
50
- bash: 'shell',
51
- read: 'fs_read',
52
- grep: 'fs_read',
53
- glob: 'fs_read',
54
- write: 'fs_write',
55
- edit: 'fs_write',
56
- notebookedit: 'fs_write',
57
- webfetch: 'web_fetch',
58
- websearch: 'web_search',
59
- mcp: 'mcp',
60
- subagent: 'subagent',
61
- skill: 'skill',
62
- };
63
48
  /** Canonical tool -> OpenClaw tool id (tool-level granularity only). */
64
49
  export const CANONICAL_TO_OPENCLAW_TOOL = {
65
50
  bash: 'exec',
@@ -81,7 +66,7 @@ export const ANTIGRAVITY_ACTION_BY_TOOL = {
81
66
  * native id. Several canonical tools collapse onto one native id, so the
82
67
  * inverse must pick a representative; declaration order in the forward table is
83
68
  * that choice, which is why `read` (declared before `grep`/`glob`) represents
84
- * Kiro's `fs_read`.
69
+ * a coarser native capability.
85
70
  */
86
71
  function invertFirstWins(forward) {
87
72
  const out = {};
@@ -94,7 +79,6 @@ function invertFirstWins(forward) {
94
79
  return out;
95
80
  }
96
81
  const CANONICAL_BY_GROK_TOOL = invertFirstWins(GROK_TOOL_BY_CANONICAL);
97
- const CANONICAL_BY_KIRO_CAPABILITY = invertFirstWins(KIRO_CAPABILITY_BY_TOOL);
98
82
  const CANONICAL_BY_OPENCLAW_TOOL = invertFirstWins(CANONICAL_TO_OPENCLAW_TOOL);
99
83
  const CANONICAL_BY_ANTIGRAVITY_ACTION = invertFirstWins(ANTIGRAVITY_ACTION_BY_TOOL);
100
84
  /** Filename Codex stores generated deny rules under `.codex/rules/`. */
@@ -585,37 +569,6 @@ export const PERMISSION_TARGETS = {
585
569
  return permissionSet(allow, []);
586
570
  },
587
571
  },
588
- kiro: {
589
- home: (h) => path.join(h, '.kiro', 'settings', 'permissions.yaml'),
590
- lossyBecause: 'Read/Grep/Glob share fs_read and Write/Edit/NotebookEdit share fs_write',
591
- toCanonical(configPath) {
592
- const config = readYaml(configPath);
593
- const rules = config?.rules;
594
- if (!Array.isArray(rules))
595
- return null;
596
- const allow = [];
597
- const deny = [];
598
- for (const rule of rules) {
599
- if (!rule || typeof rule !== 'object' || Array.isArray(rule))
600
- continue;
601
- const r = rule;
602
- if (typeof r.capability !== 'string')
603
- continue;
604
- const lowerTool = CANONICAL_BY_KIRO_CAPABILITY[r.capability];
605
- if (!lowerTool)
606
- continue;
607
- const matches = stringList(r.match);
608
- const canonicals = matches.length
609
- ? matches.map((m) => canonicalRule(lowerTool, lowerTool === 'bash' ? denormalizeBashPattern(m) : m))
610
- : [canonicalRule(lowerTool, null)];
611
- if (r.effect === 'deny')
612
- deny.push(...canonicals);
613
- else if (r.effect === 'allow')
614
- allow.push(...canonicals);
615
- }
616
- return permissionSet(allow, deny);
617
- },
618
- },
619
572
  openclaw: {
620
573
  home: (h) => path.join(h, '.openclaw', 'openclaw.json'),
621
574
  lossyBecause: 'OpenClaw gates whole tools, so only blanket rules were written and all read back as blanket',
@@ -190,19 +190,6 @@ export type KimiRule = {
190
190
  decision: 'allow' | 'deny';
191
191
  pattern: string;
192
192
  };
193
- export type KiroRule = {
194
- capability: string;
195
- effect: 'allow' | 'deny';
196
- match?: string[];
197
- exclude?: string[];
198
- };
199
- /**
200
- * Convert canonical permissions to Kiro CLI v3 capability rules.
201
- * Kiro stores these rules in ~/.kiro/settings/permissions.yaml.
202
- */
203
- export declare function convertToKiroFormat(set: PermissionSet): {
204
- rules: KiroRule[];
205
- };
206
193
  /**
207
194
  * Convert a canonical permission set to Kimi Code's `[permission].rules` format.
208
195
  * Kimi (`~/.kimi-code/config.toml`) reads rules of the form
@@ -264,7 +251,7 @@ export declare function convertToCodexFormat(set: PermissionSet, cwd?: string):
264
251
  * Every other allowlist-capable harness returns the canonical `PermissionSet`
265
252
  * that `PERMISSION_TARGETS` reads back. Before RUSH-2676 they returned `null`,
266
253
  * so permissions written for cursor, antigravity, grok, kimi, droid,
267
- * copilot, kiro, openclaw and hermes were reported as absent.
254
+ * copilot, openclaw and hermes were reported as absent.
268
255
  */
269
256
  export declare function readAgentPermissions(agentId: AgentId, scope?: 'user' | 'project', cwd?: string, options?: {
270
257
  home?: string;
@@ -315,8 +302,7 @@ export declare function exportPermissionsFromPath(filePath: string): PermissionS
315
302
  /**
316
303
  * Which harness owns `filePath`, by comparing against the trailing path segments
317
304
  * each registry target declares. Longer (more specific) suffixes win, so
318
- * `.kiro/settings/permissions.yaml` is never mistaken for a bare `permissions.yaml`.
319
- */
305
+ */
320
306
  export declare function detectPermissionAgentFromPath(filePath: string): AgentId | null;
321
307
  /**
322
308
  * Get the default permission set from central storage.
@@ -356,7 +342,7 @@ export declare function saveDefaultPermissionSet(set: PermissionSet): {
356
342
  * (`lossyBecause` in `permissions-registry.ts`), so a canonical subset check
357
343
  * would false-diff a correctly-synced home.
358
344
  *
359
- * Every other allowlist harness (codex/grok/kimi/kiro/antigravity/hermes/copilot)
345
+ * Every other allowlist harness (codex/grok/kimi/antigravity/hermes/copilot)
360
346
  * stores a lossy projection — a sandbox flag, whole-tool gate, per-directory
361
347
  * approval, or a split/merged pattern — with no faithful per-group provenance, so
362
348
  * doctor stays presence-only there and says so (`detail: 'format cannot verify
@@ -20,7 +20,7 @@ import { updateGeminiSettings } from './gemini-settings.js';
20
20
  // The canonical<->native tool vocabularies live in the registry, which also owns
21
21
  // the reverse projections — so the serializers below and the readers there can
22
22
  // never disagree about what `fs_read` or `developer__shell` means.
23
- import { ANTIGRAVITY_ACTION_BY_TOOL, CANONICAL_TO_OPENCLAW_TOOL, CODEX_RULES_FILENAME, stripJsonComments, GROK_TOOL_BY_CANONICAL, KIRO_CAPABILITY_BY_TOOL, PERMISSION_TARGETS, readCanonicalPermissions, } from './permissions-registry.js';
23
+ import { ANTIGRAVITY_ACTION_BY_TOOL, CANONICAL_TO_OPENCLAW_TOOL, CODEX_RULES_FILENAME, stripJsonComments, GROK_TOOL_BY_CANONICAL, PERMISSION_TARGETS, readCanonicalPermissions, } from './permissions-registry.js';
24
24
  export { CODEX_RULES_FILENAME };
25
25
  const HOME = os.homedir();
26
26
  export const COMPUTER_PERMISSION_RULE_PREFIX = 'Computer';
@@ -799,43 +799,6 @@ function canonicalToGrokRule(perm, action) {
799
799
  }
800
800
  return { action, tool, pattern };
801
801
  }
802
- /**
803
- * Convert canonical permissions to Kiro CLI v3 capability rules.
804
- * Kiro stores these rules in ~/.kiro/settings/permissions.yaml.
805
- */
806
- export function convertToKiroFormat(set) {
807
- const rules = [];
808
- for (const perm of set.allow) {
809
- const rule = canonicalToKiroRule(perm, 'allow');
810
- if (rule)
811
- rules.push(rule);
812
- }
813
- for (const perm of set.deny ?? []) {
814
- const rule = canonicalToKiroRule(perm, 'deny');
815
- if (rule)
816
- rules.push(rule);
817
- }
818
- return { rules };
819
- }
820
- function canonicalToKiroRule(perm, effect) {
821
- if (BLANKET_BASH_FORMS.has(perm)) {
822
- return { capability: 'shell', effect };
823
- }
824
- const parsed = parseCanonicalPreserveCase(perm);
825
- const capability = KIRO_CAPABILITY_BY_TOOL[parsed.tool.toLowerCase()];
826
- if (!capability)
827
- return null;
828
- if (parsed.pattern === null || parsed.pattern === '*' || parsed.pattern === '**') {
829
- return { capability, effect };
830
- }
831
- const lowerTool = parsed.tool.toLowerCase();
832
- const pattern = lowerTool === 'bash'
833
- ? normalizeBashPattern(parsed.pattern)
834
- : (lowerTool === 'webfetch' || lowerTool === 'websearch') && parsed.pattern.startsWith('domain:')
835
- ? parsed.pattern.slice('domain:'.length)
836
- : parsed.pattern;
837
- return { capability, effect, match: [pattern] };
838
- }
839
802
  /**
840
803
  * Parse a canonical permission string preserving the tool's original casing.
841
804
  * `parseCanonicalPattern` lowercases the tool name, which is fine for Grok
@@ -1147,7 +1110,7 @@ function readCodexPermissions(scope = 'user', cwd, options) {
1147
1110
  * Every other allowlist-capable harness returns the canonical `PermissionSet`
1148
1111
  * that `PERMISSION_TARGETS` reads back. Before RUSH-2676 they returned `null`,
1149
1112
  * so permissions written for cursor, antigravity, grok, kimi, droid,
1150
- * copilot, kiro, openclaw and hermes were reported as absent.
1113
+ * copilot, openclaw and hermes were reported as absent.
1151
1114
  */
1152
1115
  export function readAgentPermissions(agentId, scope = 'user', cwd, options) {
1153
1116
  switch (agentId) {
@@ -1598,37 +1561,6 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
1598
1561
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
1599
1562
  return { success: true };
1600
1563
  }
1601
- if (agentId === 'kiro') {
1602
- const permissionsPath = path.join(versionHome, '.kiro', 'settings', 'permissions.yaml');
1603
- let config = {};
1604
- if (fs.existsSync(permissionsPath)) {
1605
- const parsed = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
1606
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1607
- config = parsed;
1608
- }
1609
- }
1610
- const newRules = convertToKiroFormat(set).rules;
1611
- if (merge) {
1612
- const existingRules = Array.isArray(config.rules) ? config.rules : [];
1613
- const seen = new Set();
1614
- config.rules = [...existingRules, ...newRules].filter((rule) => {
1615
- if (!rule || typeof rule !== 'object' || Array.isArray(rule))
1616
- return false;
1617
- const record = rule;
1618
- const key = `${String(record.effect)}|${String(record.capability)}|${JSON.stringify(record.match ?? [])}|${JSON.stringify(record.exclude ?? [])}`;
1619
- if (seen.has(key))
1620
- return false;
1621
- seen.add(key);
1622
- return true;
1623
- });
1624
- }
1625
- else {
1626
- config.rules = newRules;
1627
- }
1628
- fs.mkdirSync(path.dirname(permissionsPath), { recursive: true });
1629
- fs.writeFileSync(permissionsPath, yaml.stringify(config), 'utf-8');
1630
- return { success: true };
1631
- }
1632
1564
  if (agentId === 'openclaw') {
1633
1565
  // OpenClaw's allowlist lives in ~/.openclaw/openclaw.json under `tools`.
1634
1566
  // Only blanket tool-level rules map (see convertToOpenClawFormat). We
@@ -1788,8 +1720,7 @@ export function exportPermissionsFromPath(filePath) {
1788
1720
  /**
1789
1721
  * Which harness owns `filePath`, by comparing against the trailing path segments
1790
1722
  * each registry target declares. Longer (more specific) suffixes win, so
1791
- * `.kiro/settings/permissions.yaml` is never mistaken for a bare `permissions.yaml`.
1792
- */
1723
+ */
1793
1724
  export function detectPermissionAgentFromPath(filePath) {
1794
1725
  const normalized = path.resolve(filePath).split(path.sep).join('/');
1795
1726
  let best = null;
@@ -1906,7 +1837,7 @@ export function saveDefaultPermissionSet(set) {
1906
1837
  * (`lossyBecause` in `permissions-registry.ts`), so a canonical subset check
1907
1838
  * would false-diff a correctly-synced home.
1908
1839
  *
1909
- * Every other allowlist harness (codex/grok/kimi/kiro/antigravity/hermes/copilot)
1840
+ * Every other allowlist harness (codex/grok/kimi/antigravity/hermes/copilot)
1910
1841
  * stores a lossy projection — a sandbox flag, whole-tool gate, per-directory
1911
1842
  * approval, or a split/merged pattern — with no faithful per-group provenance, so
1912
1843
  * doctor stays presence-only there and says so (`detail: 'format cannot verify
@@ -5,6 +5,18 @@ export interface ProjectResourceSyncResult {
5
5
  }
6
6
  export declare function projectAgentRoot(projectRoot: string, agent: AgentId): string;
7
7
  export declare function syncProjectResourcesToAgent(agent: AgentId, version: string, projectAgentsDir: string): ProjectResourceSyncResult;
8
+ /**
9
+ * Turn the manifest's managed paths (relative to agentRoot) into anchored,
10
+ * POSIX, projectRoot-relative `.gitignore` entries. Two guards keep it honest:
11
+ * - drop any path that escapes the harness config dir (e.g. grok writes
12
+ * commands back into the tracked `.agents/` tree via a `../` subdir —
13
+ * ignoring that would hide tracked source; separate bug, PHNX-3718);
14
+ * - the manifest only ever holds paths the sync itself generated (pre-existing
15
+ * user/committed files are skipped and never recorded), so ignoring exactly
16
+ * these never masks a hand-authored or committed file (e.g. a repo that
17
+ * commits its own `.claude/CLAUDE.md` keeps it — it is not in the manifest).
18
+ */
19
+ export declare function managedGitignoreEntries(agentRoot: string, projectRoot: string, managed: string[]): string[];
8
20
  /**
9
21
  * One human line for the files a project sync left alone because you already
10
22
  * wrote them. This is the normal steady state — every sync of a project whose