@synap-core/cli 1.13.0 → 1.13.1-next.20261102.246dc73

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 (227) hide show
  1. package/README.md +13 -3
  2. package/dist/build-info.json +2 -3
  3. package/dist/commands/agents.js +12 -2
  4. package/dist/commands/agents.js.map +1 -1
  5. package/dist/commands/capability.d.ts +86 -0
  6. package/dist/commands/capability.js +117 -30
  7. package/dist/commands/capability.js.map +1 -1
  8. package/dist/commands/context.js +2 -1
  9. package/dist/commands/context.js.map +1 -1
  10. package/dist/commands/data-extra.d.ts +13 -0
  11. package/dist/commands/data-extra.js +23 -2
  12. package/dist/commands/data-extra.js.map +1 -1
  13. package/dist/commands/doctor.d.ts +18 -0
  14. package/dist/commands/doctor.js +28 -1
  15. package/dist/commands/doctor.js.map +1 -1
  16. package/dist/commands/init.js +4 -2
  17. package/dist/commands/init.js.map +1 -1
  18. package/dist/commands/keys-request.d.ts +48 -0
  19. package/dist/commands/keys-request.js +236 -0
  20. package/dist/commands/keys-request.js.map +1 -0
  21. package/dist/commands/keys-request.test.d.ts +1 -0
  22. package/dist/commands/keys-request.test.js +176 -0
  23. package/dist/commands/keys-request.test.js.map +1 -0
  24. package/dist/commands/knowledge.d.ts +42 -5
  25. package/dist/commands/knowledge.js +90 -27
  26. package/dist/commands/knowledge.js.map +1 -1
  27. package/dist/commands/market.d.ts +89 -31
  28. package/dist/commands/market.js +215 -222
  29. package/dist/commands/market.js.map +1 -1
  30. package/dist/commands/mcp.d.ts +1 -1
  31. package/dist/commands/mcp.js +14 -9
  32. package/dist/commands/mcp.js.map +1 -1
  33. package/dist/commands/pods.js +6 -2
  34. package/dist/commands/pods.js.map +1 -1
  35. package/dist/commands/proposals-actions.js +7 -1
  36. package/dist/commands/proposals-actions.js.map +1 -1
  37. package/dist/commands/providers.d.ts +22 -6
  38. package/dist/commands/providers.js +39 -32
  39. package/dist/commands/providers.js.map +1 -1
  40. package/dist/commands/raycast.d.ts +15 -0
  41. package/dist/commands/raycast.js +36 -10
  42. package/dist/commands/raycast.js.map +1 -1
  43. package/dist/commands/status.js +12 -6
  44. package/dist/commands/status.js.map +1 -1
  45. package/dist/commands/statusline.backoff.test.js +13 -3
  46. package/dist/commands/statusline.backoff.test.js.map +1 -1
  47. package/dist/commands/statusline.d.ts +104 -9
  48. package/dist/commands/statusline.js +392 -148
  49. package/dist/commands/statusline.js.map +1 -1
  50. package/dist/commands/statusline.line2.test.d.ts +15 -0
  51. package/dist/commands/statusline.line2.test.js +226 -0
  52. package/dist/commands/statusline.line2.test.js.map +1 -0
  53. package/dist/commands/statusline.needs-you.test.d.ts +1 -0
  54. package/dist/commands/statusline.needs-you.test.js +17 -0
  55. package/dist/commands/statusline.needs-you.test.js.map +1 -0
  56. package/dist/commands/statusline.now.test.d.ts +10 -0
  57. package/dist/commands/statusline.now.test.js +250 -0
  58. package/dist/commands/statusline.now.test.js.map +1 -0
  59. package/dist/commands/tracks.d.ts +1 -0
  60. package/dist/commands/tracks.js +4 -0
  61. package/dist/commands/tracks.js.map +1 -1
  62. package/dist/commands/upload.js +12 -0
  63. package/dist/commands/upload.js.map +1 -1
  64. package/dist/commands/wait.d.ts +124 -0
  65. package/dist/commands/wait.js +384 -0
  66. package/dist/commands/wait.js.map +1 -0
  67. package/dist/commands/wait.test.d.ts +8 -0
  68. package/dist/commands/wait.test.js +308 -0
  69. package/dist/commands/wait.test.js.map +1 -0
  70. package/dist/commands/worker-github-app.d.ts +63 -0
  71. package/dist/commands/worker-github-app.js +161 -0
  72. package/dist/commands/worker-github-app.js.map +1 -0
  73. package/dist/commands/worker-github-app.test.d.ts +1 -0
  74. package/dist/commands/worker-github-app.test.js +102 -0
  75. package/dist/commands/worker-github-app.test.js.map +1 -0
  76. package/dist/commands/worker-github-broker.d.ts +39 -0
  77. package/dist/commands/worker-github-broker.js +106 -0
  78. package/dist/commands/worker-github-broker.js.map +1 -0
  79. package/dist/commands/worker-github-broker.test.d.ts +1 -0
  80. package/dist/commands/worker-github-broker.test.js +90 -0
  81. package/dist/commands/worker-github-broker.test.js.map +1 -0
  82. package/dist/commands/worker-runs.test.d.ts +1 -0
  83. package/dist/commands/worker-runs.test.js +150 -0
  84. package/dist/commands/worker-runs.test.js.map +1 -0
  85. package/dist/commands/worker-worktrees.d.ts +34 -0
  86. package/dist/commands/worker-worktrees.js +87 -0
  87. package/dist/commands/worker-worktrees.js.map +1 -0
  88. package/dist/commands/worker.d.ts +192 -5
  89. package/dist/commands/worker.js +467 -45
  90. package/dist/commands/worker.js.map +1 -1
  91. package/dist/commands/worker.test.js +269 -3
  92. package/dist/commands/worker.test.js.map +1 -1
  93. package/dist/index.js +59 -4
  94. package/dist/index.js.map +1 -1
  95. package/dist/lib/agents-config.d.ts +9 -0
  96. package/dist/lib/agents-config.js +20 -0
  97. package/dist/lib/agents-config.js.map +1 -1
  98. package/dist/lib/atomic-file.d.ts +20 -1
  99. package/dist/lib/atomic-file.js +100 -2
  100. package/dist/lib/atomic-file.js.map +1 -1
  101. package/dist/lib/auth.d.ts +25 -0
  102. package/dist/lib/auth.js +49 -9
  103. package/dist/lib/auth.js.map +1 -1
  104. package/dist/lib/box/job-server.js +4 -0
  105. package/dist/lib/box/job-server.js.map +1 -1
  106. package/dist/lib/bridge-invocation.test.d.ts +1 -0
  107. package/dist/lib/bridge-invocation.test.js +75 -0
  108. package/dist/lib/bridge-invocation.test.js.map +1 -0
  109. package/dist/lib/bridge-status.d.ts +144 -0
  110. package/dist/lib/bridge-status.js +227 -0
  111. package/dist/lib/bridge-status.js.map +1 -0
  112. package/dist/lib/bridge-status.test.d.ts +6 -0
  113. package/dist/lib/bridge-status.test.js +197 -0
  114. package/dist/lib/bridge-status.test.js.map +1 -0
  115. package/dist/lib/client-header.test.js +19 -1
  116. package/dist/lib/client-header.test.js.map +1 -1
  117. package/dist/lib/conversation-headers.d.ts +67 -2
  118. package/dist/lib/conversation-headers.js +122 -6
  119. package/dist/lib/conversation-headers.js.map +1 -1
  120. package/dist/lib/conversation-headers.test.d.ts +1 -0
  121. package/dist/lib/conversation-headers.test.js +78 -0
  122. package/dist/lib/conversation-headers.test.js.map +1 -0
  123. package/dist/lib/credential-class.d.ts +12 -1
  124. package/dist/lib/credential-class.js +20 -0
  125. package/dist/lib/credential-class.js.map +1 -1
  126. package/dist/lib/device-connect.d.ts +45 -0
  127. package/dist/lib/device-connect.js +60 -0
  128. package/dist/lib/device-connect.js.map +1 -1
  129. package/dist/lib/goal-words.js +6 -3
  130. package/dist/lib/goal-words.js.map +1 -1
  131. package/dist/lib/hub-client.d.ts +59 -1
  132. package/dist/lib/hub-client.js +130 -36
  133. package/dist/lib/hub-client.js.map +1 -1
  134. package/dist/lib/key-locations.d.ts +3 -3
  135. package/dist/lib/key-locations.js +3 -39
  136. package/dist/lib/key-locations.js.map +1 -1
  137. package/dist/lib/machine-instance.d.ts +7 -5
  138. package/dist/lib/machine-instance.js +7 -5
  139. package/dist/lib/machine-instance.js.map +1 -1
  140. package/dist/lib/mcp-bridge.d.ts +43 -0
  141. package/dist/lib/mcp-bridge.js +199 -11
  142. package/dist/lib/mcp-bridge.js.map +1 -1
  143. package/dist/lib/mcp-bridge.test.js +194 -2
  144. package/dist/lib/mcp-bridge.test.js.map +1 -1
  145. package/dist/lib/mcp-call.d.ts +5 -1
  146. package/dist/lib/mcp-call.js +7 -6
  147. package/dist/lib/mcp-call.js.map +1 -1
  148. package/dist/lib/mcp-connector-url.test.d.ts +1 -0
  149. package/dist/lib/mcp-connector-url.test.js +13 -0
  150. package/dist/lib/mcp-connector-url.test.js.map +1 -0
  151. package/dist/lib/mcp-snippets.d.ts +16 -0
  152. package/dist/lib/mcp-snippets.js +20 -2
  153. package/dist/lib/mcp-snippets.js.map +1 -1
  154. package/dist/lib/now-line.d.ts +66 -0
  155. package/dist/lib/now-line.js +193 -0
  156. package/dist/lib/now-line.js.map +1 -0
  157. package/dist/lib/now-line.test.d.ts +5 -0
  158. package/dist/lib/now-line.test.js +177 -0
  159. package/dist/lib/now-line.test.js.map +1 -0
  160. package/dist/lib/package-updates.d.ts +66 -0
  161. package/dist/lib/package-updates.js +76 -0
  162. package/dist/lib/package-updates.js.map +1 -0
  163. package/dist/lib/pod.d.ts +0 -4
  164. package/dist/lib/pod.js +0 -7
  165. package/dist/lib/pod.js.map +1 -1
  166. package/dist/lib/register-claude-code-mcp.test.js +19 -0
  167. package/dist/lib/register-claude-code-mcp.test.js.map +1 -1
  168. package/dist/lib/safe-open.d.ts +5 -3
  169. package/dist/lib/safe-open.js +9 -8
  170. package/dist/lib/safe-open.js.map +1 -1
  171. package/dist/lib/self-host.d.ts +17 -0
  172. package/dist/lib/self-host.js +24 -0
  173. package/dist/lib/self-host.js.map +1 -0
  174. package/dist/lib/targets.d.ts +31 -0
  175. package/dist/lib/targets.js +100 -104
  176. package/dist/lib/targets.js.map +1 -1
  177. package/dist/lib/template-lifecycle.d.ts +0 -31
  178. package/dist/lib/template-lifecycle.js +0 -74
  179. package/dist/lib/template-lifecycle.js.map +1 -1
  180. package/dist/lib/term.d.ts +24 -0
  181. package/dist/lib/term.js +52 -0
  182. package/dist/lib/term.js.map +1 -0
  183. package/dist/lib/term.test.d.ts +1 -0
  184. package/dist/lib/term.test.js +29 -0
  185. package/dist/lib/term.test.js.map +1 -0
  186. package/dist/lib/transcript-index.d.ts +94 -0
  187. package/dist/lib/transcript-index.js +321 -0
  188. package/dist/lib/transcript-index.js.map +1 -0
  189. package/dist/lib/transcript-index.test.d.ts +10 -0
  190. package/dist/lib/transcript-index.test.js +200 -0
  191. package/dist/lib/transcript-index.test.js.map +1 -0
  192. package/dist/lib/wait-help.d.ts +11 -0
  193. package/dist/lib/wait-help.js +25 -0
  194. package/dist/lib/wait-help.js.map +1 -0
  195. package/dist/lib/wait-state.d.ts +57 -0
  196. package/dist/lib/wait-state.js +114 -0
  197. package/dist/lib/wait-state.js.map +1 -0
  198. package/package.json +2 -2
  199. package/skills/synap/SKILL.md +81 -44
  200. package/skills/synap/code-skills.md +15 -1
  201. package/skills/synap/concepts.md +36 -26
  202. package/skills/synap/document-embeds.md +15 -1
  203. package/skills/synap/focus-sessions.md +4 -4
  204. package/skills/synap/from-intent.md +2 -2
  205. package/skills/synap/governance-rules.md +2 -0
  206. package/skills/synap/hand-off.md +10 -6
  207. package/skills/synap/reflexes.md +4 -4
  208. package/skills/synap/work-model.md +1 -1
  209. package/skills/synap/writing-titles.md +7 -0
  210. package/skills/synap-app/SKILL.md +2 -2
  211. package/skills/synap-app/overview.md +2 -2
  212. package/skills/synap-market/SKILL.md +1 -1
  213. package/skills/synap-market/governance-and-catalog.md +1 -1
  214. package/skills/synap-schema/SKILL.md +4 -1
  215. package/skills/synap-schema/intent-vocabulary-ssot.md +4 -1
  216. package/dist/commands/template.d.ts +0 -42
  217. package/dist/commands/template.js +0 -191
  218. package/dist/commands/template.js.map +0 -1
  219. package/dist/lib/browser-auth.d.ts +0 -35
  220. package/dist/lib/browser-auth.js +0 -170
  221. package/dist/lib/browser-auth.js.map +0 -1
  222. package/dist/lib/openclaw.d.ts +0 -32
  223. package/dist/lib/openclaw.js +0 -165
  224. package/dist/lib/openclaw.js.map +0 -1
  225. package/dist/lib/seed.d.ts +0 -13
  226. package/dist/lib/seed.js +0 -135
  227. package/dist/lib/seed.js.map +0 -1
@@ -32,13 +32,15 @@ import { renderNextSteps, FLOW } from "../lib/next-steps.js";
32
32
  import { getStoredToken, isTokenLocallyExpired } from "../lib/auth.js";
33
33
  import prompts from "prompts";
34
34
  import { fetchPublicBrowseRows, fetchMyRows, fetchAvailableSlugs, fetchRetiredWorkspaceSlugs, fetchPackageDefinition, fetchPackageRecord, hydrateMineCategories, } from "../lib/cp-packages.js";
35
- import { fetchInstalledSlugs, fetchInstalledTemplates, fetchInstalledInventory, fetchInstalledInventoryStrict, installedRowsToTemplates, fetchWorkspaceAttachments, fetchWorkspaceAttachmentsStrict, verifyStampLanded, INSTALLED_KINDS } from "../lib/installed.js";
35
+ import { fetchInstalledSlugs, fetchInstalledTemplates, fetchInstalledInventory, installedRowsToTemplates, fetchWorkspaceAttachments, fetchWorkspaceAttachmentsStrict, verifyStampLanded, INSTALLED_KINDS } from "../lib/installed.js";
36
36
  import { isSuite } from "../lib/suite.js";
37
37
  import { TEMPLATE_STEP_LABELS, classifyTemplateStep, proposalsDoor, stepFromApplyState, templateStepGlyph, templateStepPhrase, } from "../lib/template-steps.js";
38
38
  import { capabilityAdd } from "./capability.js";
39
39
  import { bundledTemplatesVersion } from "../lib/bundle-version.js";
40
40
  import { HubNetworkError } from "../lib/hub-client.js";
41
- import { parseDataChanges, dataChangesFromRefusal, consentNeedsPerson, dataChangeRefusalMessage, printDataChanges, parseAcceptList, decideConsent, consentRerunCommand, consentRefusalLines, lifecycleNotice, overlayNotice, overlayNeedsAttention, } from "../lib/template-lifecycle.js";
41
+ import { checksFromStandings, fetchPackageStandings, StandingsUnavailableError, } from "../lib/package-updates.js";
42
+ import { podAdminOrigin } from "../lib/safe-open.js";
43
+ import { parseDataChanges, printDataChanges, parseAcceptList, decideConsent, consentRerunCommand, lifecycleNotice, overlayNotice, overlayNeedsAttention, } from "../lib/template-lifecycle.js";
42
44
  // Same longer budget launch uses — an apply writes profiles/views/entities.
43
45
  const APPLY_TIMEOUT_MS = 120_000;
44
46
  /** `--timeout <seconds>` → ms, falling back to `APPLY_TIMEOUT_MS` when unset/invalid. */
@@ -167,8 +169,8 @@ const KIND_HEADINGS = {
167
169
  // (`GET /installed`) calls the same thing `automation`. Both tokens reach
168
170
  // `kindHeading()` — `market --list` speaks CP, `market installed` speaks pod —
169
171
  // so both are mapped, to the ONE plural the registry says.
170
- workflow: "Rules",
171
- automation: "Rules",
172
+ workflow: "Automations",
173
+ automation: "Automations",
172
174
  view: "Views",
173
175
  cell: "Cards",
174
176
  workspace: "Spaces",
@@ -383,8 +385,8 @@ function printSeedOutcomes(dependencies, seedSummary) {
383
385
  log.hint(`${d.slug}: failed${d.seedOutcome.error ? ` (${d.seedOutcome.error})` : ""}`);
384
386
  }
385
387
  }
386
- // ── Apply-outcome honesty — shared by `marketInstall` + `applyOnePackage` ───
387
- // Both hit the same `/packages/apply` door and must render the SAME honest
388
+ // ── Apply-outcome honesty — `marketInstall`'s reading of `/packages/apply` ───
389
+ // Every install reads the same `/packages/apply` answer and must render the SAME honest
388
390
  // verdict from its response: a partial capability-seed failure, a version
389
391
  // stamp that silently didn't take, or a compose-overlay result are never a
390
392
  // plain "✓ success" — in the human text OR in `--json`.
@@ -437,8 +439,8 @@ export function podApplyState(res) {
437
439
  return null;
438
440
  }
439
441
  /**
440
- * The verdict of one `/packages/apply` response — the ONE reader both
441
- * `marketInstall` and `applyOnePackage` call. Layer 2's outcome comes from the
442
+ * The verdict of one `/packages/apply` response — the ONE reader
443
+ * `marketInstall` calls. Layer 2's outcome comes from the
442
444
  * pod's own state (`podApplyState`): an apply the pod says is unfinished is
443
445
  * never "updated", and never a stamp contradiction (the pod held the version
444
446
  * on purpose — redeploying fixes nothing).
@@ -457,12 +459,11 @@ export function classifyApplyResponse(entry, res, ctx) {
457
459
  return classifyApplyResult(entry, res.workspace, res.dependencies, res.seedSummary, ctx);
458
460
  }
459
461
  /**
460
- * Classify one `/packages/apply` response into the honest verdict both
461
- * `marketInstall` and `applyOnePackage` print (and both put in `--json`).
462
+ * Classify one `/packages/apply` response into the honest verdict
463
+ * `marketInstall` prints (and puts in `--json`).
462
464
  * `wasUnstamped` = the slug was already installed with no recorded version
463
465
  * BEFORE this call (only `marketInstall` can detect this today — see its
464
- * `fetchInstalledTemplates` call; `applyOnePackage` only ever re-applies
465
- * already-versioned slugs, so it always passes `false`).
466
+ * `fetchInstalledTemplates` call).
466
467
  */
467
468
  export function classifyApplyResult(entry, ws, dependencies, seedSummary, ctx) {
468
469
  // A post-workspace LAYER failure counts the same as a seed failure: the
@@ -517,7 +518,7 @@ export function deferredPlacementLines(res) {
517
518
  export function heldVersionLine(res) {
518
519
  return typeof res.templateApplyWarning === "string" ? res.templateApplyWarning : null;
519
520
  }
520
- /** Render a `classifyApplyResult` verdict — the ONE honest-text door for both `marketInstall` and `applyOnePackage`. */
521
+ /** Render a `classifyApplyResult` verdict — the ONE honest-text door of `marketInstall`. */
521
522
  export function printApplyVerdict(entry, verdict) {
522
523
  switch (verdict.status) {
523
524
  case "installed":
@@ -1260,17 +1261,50 @@ function stampVersion(pkg, slug, version) {
1260
1261
  const meta = pkg._meta ?? {};
1261
1262
  return { ...pkg, _meta: { ...meta, slug, ...(version ? { version } : {}) } };
1262
1263
  }
1263
- export function updateDoorFor(kind) {
1264
- switch (kind ?? "workspace") {
1265
- case "workspace":
1266
- return "packages-apply";
1267
- case "view":
1268
- case "skill":
1269
- case "automation":
1270
- return "configs-reconcile";
1271
- default:
1272
- return null;
1264
+ /**
1265
+ * The slugs with an update that `synap market update <slug>` can apply. Every
1266
+ * installed kind (space, capability, view, skill, automation) updates through
1267
+ * the pod's ONE update door (`POST /package-updates/{slug}/apply`), so every
1268
+ * slug with an update is offered; what only the person can do is said by the
1269
+ * pod at apply time (`NEEDS_PERSON`), never guessed here. Next-step
1270
+ * suggestions use it.
1271
+ */
1272
+ export function cliUpdatableSlugs(checks) {
1273
+ return checks.filter((c) => c.updateAvailable).map((c) => c.slug);
1274
+ }
1275
+ /** The status of an update left for the person to apply in pod admin (not a failure). */
1276
+ export const APPLY_IN_POD_ADMIN = "apply-in-pod-admin";
1277
+ /**
1278
+ * Where the person finishes ONE package's update: pod admin's review page for
1279
+ * it (`/marketplace/updates/<slug>`), when the pod-admin origin derives from
1280
+ * the pod's; else the pod's own link (`reviewUrl`, pod-relative
1281
+ * `/open/update/<slug>`, which the pod sends on to pod admin); else the
1282
+ * Updates tab's words.
1283
+ */
1284
+ export function packageReviewLink(podUrl, slug, reviewUrl) {
1285
+ const admin = podAdminOrigin(podUrl);
1286
+ if (admin)
1287
+ return `${admin}/marketplace/updates/${encodeURIComponent(slug)}`;
1288
+ if (typeof reviewUrl === "string" && reviewUrl.startsWith("/"))
1289
+ return `${podUrl.replace(/\/+$/, "")}${reviewUrl}`;
1290
+ if (typeof reviewUrl === "string" && /^https?:\/\//.test(reviewUrl))
1291
+ return reviewUrl;
1292
+ return undefined;
1293
+ }
1294
+ /** The one row for an update only the person can finish: on pod admin, not a failure. */
1295
+ function applyInPodAdminResult(c, podUrl, json, pod = {}) {
1296
+ const link = podUrl ? packageReviewLink(podUrl, c.slug, pod.reviewUrl) : undefined;
1297
+ if (!json) {
1298
+ log.warn(`${c.slug} is a ${c.kind ?? "workspace"} — to apply in pod admin${pod.why ? `: ${pod.why}` : "."} Nothing was written.`);
1299
+ log.hint(link ? `Apply it yourself: ${link}` : podUrl ? marketUpdatesDoor(podUrl) : "Apply it yourself from pod admin → Marketplace → Updates.");
1273
1300
  }
1301
+ return {
1302
+ slug: c.slug,
1303
+ outcome: APPLY_IN_POD_ADMIN,
1304
+ status: APPLY_IN_POD_ADMIN,
1305
+ warnings: [],
1306
+ ...(link ? { reviewUrl: link } : {}),
1307
+ };
1274
1308
  }
1275
1309
  /**
1276
1310
  * Per-installed-slug drift check. SERVER-FIRST: when the pod runs the
@@ -1335,113 +1369,137 @@ export function dedupeBySlug(checks) {
1335
1369
  const seen = new Set();
1336
1370
  return checks.filter((c) => (seen.has(c.slug) ? false : (seen.add(c.slug), true)));
1337
1371
  }
1372
+ /** The pod's typed refusals of an update (`services/package-updates/refusals.ts`). */
1373
+ const NEEDS_THE_PERSON = new Set(["NEEDS_PERSON", "CHOICES_NEEDED"]);
1338
1374
  /**
1339
- * Apply one package via the SAME door `marketInstall` uses
1340
- * (`POST /packages/apply`) — for an already-installed slug this re-runs the
1341
- * pod's idempotent-create path, which reconciles when the pod build supports
1342
- * it (see the `applyOnePackage` caveat below for what "created" means here).
1375
+ * Apply ONE installed package's update through the pod's ONE update door,
1376
+ * `POST /package-updates/{slug}/apply`, whatever its kind (space, capability,
1377
+ * view, skill, automation) and whoever holds the key. The pod applies the
1378
+ * rule (backend `services/package-updates/`): a person's apply lands; an
1379
+ * agent key's is filed for the person's approval (`proposed`) and lands when
1380
+ * they approve it; what only the person can do (agree to changes to existing
1381
+ * records, choose for a field both sides changed, change a view) is
1382
+ * `NEEDS_PERSON` / `CHOICES_NEEDED` with that package's review page. An
1383
+ * older pod that refuses an agent here (403, no `reasonCode`) keeps today's
1384
+ * line: to apply in pod admin.
1343
1385
  */
1344
- async function applyOnePackage(entry, cat, cfg, opts, applyTimeoutMs = APPLY_TIMEOUT_MS, extra = {}) {
1345
- if (cat.lockedSlugs.has(entry.slug)) {
1346
- const tier = cat.tierBySlug.get(entry.slug)?.requiredTier;
1386
+ export async function applyUpdateThroughDoor(c, cfg, opts, applyTimeoutMs = APPLY_TIMEOUT_MS, acceptDataChanges = []) {
1387
+ if (!c.latestVersion) {
1388
+ const error = "the pod named no catalog version to update to";
1347
1389
  if (!opts.json)
1348
- log.warn(`"${entry.name}" requires the ${tier ?? "a higher"} tier — skipping.`);
1349
- return { slug: entry.slug, outcome: "locked", status: "locked", warnings: [] };
1350
- }
1351
- const cpToken = getStoredToken()?.token;
1352
- // Same honest payload as `marketInstall` (see `resolveInstallPayload`).
1353
- const payload = extra.payload !== undefined ? extra.payload : await resolveInstallPayload(entry.slug, cat, cpToken);
1354
- if (!payload) {
1355
- if (!opts.json) {
1356
- log.error(`Couldn't fetch "${entry.name}"'s definition from the control plane.`);
1357
- if (!cat.loggedIn)
1358
- log.hint("It may be private — try: synap login");
1359
- }
1360
- return { slug: entry.slug, outcome: "fetch-failed", status: "fetch-failed", warnings: [] };
1390
+ log.error(`${c.slug} not updated — ${error}.`);
1391
+ return { slug: c.slug, outcome: "failed", status: "failed", warnings: [], error };
1361
1392
  }
1362
- const pkg = extra.acceptDataChanges && extra.acceptDataChanges.length > 0
1363
- ? { ...payload.pkg, acceptDataChanges: extra.acceptDataChanges }
1364
- : payload.pkg;
1365
- const s = opts.json ? null : ora(`Updating ${entry.name}…`).start();
1393
+ const body = {
1394
+ expectedVersion: c.latestVersion,
1395
+ choices: {},
1396
+ ...(acceptDataChanges.length > 0 ? { acceptDataChanges } : {}),
1397
+ };
1398
+ const s = opts.json ? null : ora(`Updating ${c.slug}…`).start();
1366
1399
  try {
1367
- const res = (await hubPost("/packages/apply", pkg, cfg, applyTimeoutMs));
1400
+ const res = (await hubPost(`/package-updates/${encodeURIComponent(c.slug)}/apply`, body, cfg, applyTimeoutMs));
1368
1401
  s?.stop();
1369
- if (writeGovernance(res) === "proposed") {
1370
- const proposalId = res.proposalId ? String(res.proposalId) : undefined;
1402
+ if (res?.status === "proposed") {
1403
+ const proposalIds = Array.isArray(res.proposalIds) ? res.proposalIds.map(String) : [];
1404
+ const links = proposalIds.map((id) => approvalLink(cfg.podUrl, id, undefined) ?? `${cfg.podUrl.replace(/\/+$/, "")}/open/${encodeURIComponent(id)}`);
1371
1405
  if (!opts.json) {
1372
- log.info(`${entry.name} — update proposed (under review, not live yet).`);
1373
- if (proposalId)
1374
- log.hint(`Review and approve: synap open proposal ${proposalId}`);
1406
+ log.info(`${c.slug} — update filed for your approval (not applied yet).`);
1407
+ for (const link of links)
1408
+ log.hint(`Approve it: ${link}`);
1375
1409
  }
1376
- return { slug: entry.slug, outcome: "proposed", status: "proposed", warnings: [] };
1410
+ return {
1411
+ slug: c.slug,
1412
+ outcome: "proposed",
1413
+ status: "proposed",
1414
+ warnings: [],
1415
+ ...(proposalIds[0] ? { proposalId: proposalIds[0], proposalIds } : {}),
1416
+ ...(links[0] ? { reviewUrl: links[0] } : {}),
1417
+ };
1377
1418
  }
1378
- const ws = res.workspace;
1379
- const dependencies = res.dependencies;
1380
- const seedSummary = res.seedSummary;
1381
- // `wasUnstamped: false` — `applyOnePackage` is only ever reached (from
1382
- // `marketUpdate`) for a slug that already had `updateAvailable: true`,
1383
- // which by construction means it already carried a recorded version. The
1384
- // stamp-contradiction check is therefore a no-op here by design; it's
1385
- // `marketInstall`'s job to catch it on a first-time/unstamped install.
1386
- const verdict = classifyApplyResponse(entry, res, {
1387
- remoteVersionSent: payload.catalogVersion,
1388
- wasUnstamped: false,
1389
- });
1390
- if (!opts.json) {
1391
- printApplyVerdict(entry, verdict);
1392
- printSeedOutcomes(dependencies, seedSummary);
1393
- printLayerOutcomes(ws?.layers);
1394
- for (const line of deferredPlacementLines(res))
1395
- log.info(line);
1419
+ if (res?.status === "applied") {
1420
+ const warning = typeof res.warning === "string" && res.warning ? res.warning : undefined;
1421
+ const receipt = res.receipt;
1422
+ // A field update whose every field stayed yours changed nothing.
1423
+ const unchanged = !!receipt && Array.isArray(receipt.changes) && receipt.changes.length === 0;
1424
+ const status = warning ? "updated-with-failures" : unchanged ? "unchanged" : "updated";
1425
+ if (!opts.json) {
1426
+ if (warning)
1427
+ log.warn(`${c.slug} — updated, not finished: ${warning}`);
1428
+ else
1429
+ log.success(`${c.slug} — ${status}`);
1430
+ }
1431
+ return { slug: c.slug, outcome: status, status, warnings: warning ? [warning] : [] };
1396
1432
  }
1397
- return {
1398
- slug: entry.slug,
1399
- outcome: ws?.outcome ?? ws?.status ?? "unknown",
1400
- status: verdict.status,
1401
- warnings: verdict.warnings,
1402
- dependencies,
1403
- seedSummary,
1404
- };
1433
+ const error = "the pod answered with a result this CLI can't read — not assuming it updated";
1434
+ if (!opts.json)
1435
+ log.error(`${c.slug} — ${error}`);
1436
+ return { slug: c.slug, outcome: "failed", status: "failed", warnings: [], error };
1405
1437
  }
1406
1438
  catch (e) {
1407
1439
  s?.stop();
1408
- // The pod refused: data changes are owed that the person has not agreed to.
1409
- // Nothing was written. Show the list and the exact command that agrees.
1410
- const owed = dataChangesFromRefusal(e);
1411
- if (owed) {
1440
+ const refusal = e instanceof HubError ? (e.body ?? {}) : {};
1441
+ const reasonCode = typeof refusal.reasonCode === "string" ? refusal.reasonCode : undefined;
1442
+ const said = e instanceof HubError ? (e.detail ?? e.message) : e instanceof Error ? e.message : String(e);
1443
+ if (reasonCode && NEEDS_THE_PERSON.has(reasonCode)) {
1444
+ return applyInPodAdminResult(c, cfg.podUrl, opts.json, { why: said, reviewUrl: refusal.reviewUrl });
1445
+ }
1446
+ // An older pod refuses an agent's space or capability update here: today's line.
1447
+ if (e instanceof HubError && e.status === 403 && !reasonCode && /\bagent\b/i.test(said)) {
1448
+ return applyInPodAdminResult(c, cfg.podUrl, opts.json);
1449
+ }
1450
+ if (reasonCode === "INVALID_CATALOG_DEFINITION") {
1451
+ const fieldErrors = (refusal.fieldErrors ?? undefined);
1412
1452
  if (!opts.json) {
1413
- const said = consentRefusalLines(entry.name, entry.slug, owed, consentNeedsPerson(e));
1414
- log.warn(said.warn);
1415
- printDataChanges(entry.slug, owed);
1416
- for (const hint of said.hints)
1417
- log.hint(hint);
1453
+ log.error(`${c.slug} not updated — ${said}`);
1454
+ for (const [field, messages] of Object.entries(fieldErrors ?? {}))
1455
+ log.dim(` ${field || "(the definition)"}: ${messages.join("; ")}`);
1418
1456
  }
1419
1457
  return {
1420
- slug: entry.slug,
1421
- outcome: "needs-consent",
1422
- status: "needs-consent",
1458
+ slug: c.slug,
1459
+ outcome: "failed",
1460
+ status: "failed",
1423
1461
  warnings: [],
1424
- pendingDataChanges: owed,
1462
+ error: said,
1463
+ ...(fieldErrors ? { fieldErrors } : {}),
1425
1464
  };
1426
1465
  }
1427
1466
  const timedOut = e instanceof HubNetworkError && e.code === "TIMEOUT";
1428
- const refusal = dataChangeRefusalMessage(e);
1429
1467
  if (!opts.json) {
1430
- log.error(`${entry.name} update failed`);
1431
- if (refusal)
1432
- log.warn(refusal);
1433
- else
1434
- renderHubError(e);
1468
+ log.error(`${c.slug} update failed`);
1469
+ renderHubError(e, { personDoor: marketUpdatesDoor(cfg.podUrl) });
1435
1470
  }
1436
1471
  return {
1437
- slug: entry.slug,
1472
+ slug: c.slug,
1438
1473
  outcome: timedOut ? "timed-out" : "failed",
1439
1474
  status: timedOut ? "timed-out" : "failed",
1440
1475
  warnings: [],
1441
- error: e instanceof Error ? e.message : String(e),
1476
+ error: said,
1442
1477
  };
1443
1478
  }
1444
1479
  }
1480
+ /**
1481
+ * Where the person approves a filed update: pod admin's proposal page, the
1482
+ * approval door (`/proposal/<id>`), when the pod-admin origin derives from the
1483
+ * pod's; else the link the pod sent (`reviewUrl`); else nothing.
1484
+ */
1485
+ export function approvalLink(podUrl, proposalId, reviewUrl) {
1486
+ const admin = podAdminOrigin(podUrl);
1487
+ if (admin && proposalId)
1488
+ return `${admin}/proposal/${encodeURIComponent(proposalId)}`;
1489
+ return typeof reviewUrl === "string" && reviewUrl.length > 0 ? reviewUrl : undefined;
1490
+ }
1491
+ /**
1492
+ * Where the person applies a package update a limited key may not apply
1493
+ * (a computer key's grant leaves out `workspace.update`, by design: applying a
1494
+ * workspace package is the person's act). Pod admin's Marketplace, Updates
1495
+ * tab, named by URL when the pod-admin origin derives from the pod's.
1496
+ */
1497
+ export function marketUpdatesDoor(podUrl) {
1498
+ const admin = podAdminOrigin(podUrl);
1499
+ return admin
1500
+ ? `Apply it yourself from pod admin → Marketplace → Updates: ${admin}/marketplace/updates`
1501
+ : "Apply it yourself from pod admin → Marketplace → Updates.";
1502
+ }
1445
1503
  /**
1446
1504
  * Ask the pod's write-free preview door (`POST /packages/preflight`, the same
1447
1505
  * body `/packages/apply` takes) which data changes an update would run.
@@ -1459,76 +1517,30 @@ async function previewDataChanges(entry, cat, cfg) {
1459
1517
  return { payload, error: `the pod's preview failed: ${status}` };
1460
1518
  }
1461
1519
  }
1462
- /** One slug's outcome out of the shared pod-wide config reconcile. */
1463
- async function reconcileOutcome(c, report, opts) {
1464
- let r;
1465
- try {
1466
- r = await report;
1467
- }
1468
- catch (e) {
1469
- if (!opts.json) {
1470
- log.error(`${c.slug} update failed`);
1471
- renderHubError(e);
1472
- }
1473
- return { slug: c.slug, outcome: "failed", status: "failed", warnings: [], error: e instanceof Error ? e.message : String(e) };
1474
- }
1475
- // A report this CLI cannot read is a FAILED read — never "up to date".
1476
- const lists = [r?.updated, r?.ownerOwnedSkipped, r?.skipped, r?.conflicts];
1477
- if (!lists.some(Array.isArray) || lists.some((l) => l !== undefined && !Array.isArray(l))) {
1478
- const error = "the pod's reconcile answered with a report this CLI can't read — not assuming it updated";
1479
- if (!opts.json)
1480
- log.error(`${c.slug} — ${error}`);
1481
- return { slug: c.slug, outcome: "failed", status: "failed", warnings: [], error };
1482
- }
1483
- const mine = (rows) => (rows ?? []).filter((e) => e.packageSlug === c.slug);
1484
- const conflicts = mine(r.conflicts);
1485
- const owned = mine(r.ownerOwnedSkipped);
1486
- const updated = mine(r.updated);
1487
- const warnings = owned.map((e) => `${e.name}: kept your edits — ${e.reason}`);
1488
- if (conflicts.length > 0) {
1489
- const error = conflicts.map((e) => `${e.name}: ${e.reason}`).join("; ");
1490
- if (!opts.json)
1491
- log.error(`${c.slug} update failed — ${error}`);
1492
- return { slug: c.slug, outcome: "failed", status: updated.length > 0 ? "updated-with-failures" : "failed", warnings, error };
1493
- }
1494
- if (!opts.json)
1495
- for (const w of warnings)
1496
- log.warn(`${c.slug} — ${w}`);
1497
- if (updated.length > 0 || owned.length > 0) {
1498
- // Every field it could advance did; a field you edited stays yours (warned above).
1499
- const status = updated.length > 0 ? "updated" : "unchanged";
1500
- if (!opts.json)
1501
- log.success(`${c.slug} — ${status}`);
1502
- return { slug: c.slug, outcome: status, status, warnings };
1503
- }
1504
- // Skipped (cache miss, unresolved definition…) or not reported at all: NOT updated — never "current".
1505
- const error = mine(r.skipped).map((e) => `${e.name}: ${e.reason}`).join("; ") || "the pod's reconcile did not report this package";
1506
- if (!opts.json)
1507
- log.warn(`${c.slug} was not updated — ${error}`);
1508
- return { slug: c.slug, outcome: "not-updated", status: "not-updated", warnings, error };
1509
- }
1510
1520
  export async function marketUpdate(slugsArg, opts) {
1511
1521
  const applyTimeoutMs = parseApplyTimeoutMs(opts.timeout);
1512
- // STRICT fetch: distinguish "pod unreachable" (transient — don't claim the
1513
- // pod is empty, that reads as data loss) from "genuinely nothing installed".
1514
1522
  const cat = await buildMarketCatalog();
1515
- let installedTemplates;
1516
- // Whether the pod answered from the ONE read door (`GET /installed`, all six
1517
- // kinds) or is old enough that only workspace packages were visible. An old
1518
- // pod is not an empty pod — say which, rather than letting a narrower list
1519
- // read as the whole truth.
1520
- let degraded = false;
1523
+ // THE POD'S ANSWER, never the CLI's: which installed package has an update
1524
+ // is derived once on the pod, for every kind, with one rule (Hub
1525
+ // `GET /package-updates` → `installed[]`), and pod admin's Updates tab reads
1526
+ // the same list. A pod that cannot answer is said out loud, never replaced
1527
+ // by a verdict computed here (that fork is how the CLI said "content
1528
+ // changed" while pod admin said nothing, 2 Nov 2026).
1529
+ let standings;
1521
1530
  try {
1522
- const inv = await fetchInstalledInventoryStrict();
1523
- installedTemplates = installedRowsToTemplates(inv.rows);
1524
- degraded = inv.degraded;
1531
+ standings = await fetchPackageStandings(await resolveHubConfig(opts));
1525
1532
  }
1526
1533
  catch (e) {
1527
1534
  // Unreachable ≠ success — a script checking `$?` (or `&&`-chaining) must see
1528
1535
  // failure, not exit 0, even though the JSON payload also carries the reason.
1529
1536
  process.exitCode = 1;
1537
+ const unavailable = e instanceof StandingsUnavailableError ? e.reason : null;
1530
1538
  if (opts.json) {
1531
- console.log(JSON.stringify({ checks: [], loggedIn: cat.loggedIn, error: "pod-unreachable" }, null, 2));
1539
+ console.log(JSON.stringify({ checks: [], loggedIn: cat.loggedIn, error: unavailable ?? "pod-unreachable" }, null, 2));
1540
+ return;
1541
+ }
1542
+ if (e instanceof StandingsUnavailableError) {
1543
+ log.error(e.message);
1532
1544
  return;
1533
1545
  }
1534
1546
  log.error("Couldn't reach your pod to read what's installed — not saying it's empty.");
@@ -1536,26 +1548,21 @@ export async function marketUpdate(slugsArg, opts) {
1536
1548
  log.hint("Check the pod is up / you're on the right --pod, then retry.");
1537
1549
  return;
1538
1550
  }
1539
- const degradedNote = degraded
1540
- ? "This pod does not serve GET /api/hub/installed — only workspace packages were checked. Capability / view / skill / automation / card installs were NOT (upgrade the pod to include them)."
1541
- : undefined;
1542
- if (installedTemplates.length === 0) {
1551
+ // Kept in the JSON for readers of the older shape: the pod's list covers
1552
+ // every kind, so it is never the workspace-only view of an old pod.
1553
+ const degraded = false;
1554
+ if (standings.length === 0) {
1543
1555
  if (opts.json) {
1544
- console.log(JSON.stringify({ checks: [], loggedIn: cat.loggedIn, degraded, ...(degradedNote ? { degradedNote } : {}) }, null, 2));
1556
+ console.log(JSON.stringify({ checks: [], loggedIn: cat.loggedIn, degraded }, null, 2));
1545
1557
  return;
1546
1558
  }
1547
1559
  log.warn("No installed packages found on this pod.");
1548
- if (degradedNote)
1549
- log.dim(degradedNote);
1550
1560
  log.hint("Install one: synap market install <slug>");
1551
1561
  return;
1552
1562
  }
1553
- // ONE ROW PER SLUG. `GET /installed` reports one row per installed OBJECT —
1554
- // a views pack that installed four views is four rows — and an update is
1555
- // applied per PACKAGE, not per object. Without this the preview listed
1556
- // `task-views-pack` four times and the "N up to date" tally counted it four
1557
- // times. `market installed` has always deduped the same way.
1558
- let checks = dedupeBySlug(computeUpdates(installedTemplates, cat));
1563
+ // One check per slug: the pod answers per package (slug + kind); a slug
1564
+ // installed as two kinds is updated per kind by its own door, but listed once.
1565
+ let checks = dedupeBySlug(checksFromStandings(standings));
1559
1566
  const entryBySlug = new Map(cat.entries.map((e) => [e.slug, e]));
1560
1567
  const explicitSlugs = !!slugsArg && slugsArg.length > 0;
1561
1568
  let unknownSlugs = [];
@@ -1622,8 +1629,9 @@ export async function marketUpdate(slugsArg, opts) {
1622
1629
  process.exit(1);
1623
1630
  }
1624
1631
  for (const c of withUpdates) {
1625
- // The data-change preview is the workspace door's (`/packages/preflight`).
1626
- if (updateDoorFor(c.kind) !== "packages-apply")
1632
+ // Only a space's update changes existing records: the preview of its
1633
+ // data changes is the template door's (`/packages/preflight`).
1634
+ if ((c.kind ?? "workspace") !== "workspace")
1627
1635
  continue;
1628
1636
  const entry = entryBySlug.get(c.slug);
1629
1637
  if (entry)
@@ -1663,6 +1671,8 @@ export async function marketUpdate(slugsArg, opts) {
1663
1671
  }
1664
1672
  log.heading(" Installed packages");
1665
1673
  log.blank();
1674
+ // The slug column fits the longest slug (capped), with at least 2 spaces after it.
1675
+ const slugW = Math.min(Math.max(28, ...checks.map((c) => c.slug.length + 2)), 46);
1666
1676
  for (const c of checks) {
1667
1677
  // ONE label door. This was a hand-rolled second copy that diverged from
1668
1678
  // `installedStatusLabel` in two ways that both LIED: it had no cache-cold
@@ -1673,14 +1683,12 @@ export async function marketUpdate(slugsArg, opts) {
1673
1683
  const label = retiredSlugs.has(c.slug)
1674
1684
  ? chalk.yellow("retired") + chalk.dim(` · synap templates retire ${c.slug}`)
1675
1685
  : installedStatusLabel(c, entryBySlug.get(c.slug)?.isPrivate ?? false, cat.loggedIn);
1676
- console.log(" " + chalk.cyan(c.slug.padEnd(28)) + label);
1686
+ console.log(" " + chalk.cyan(clip(c.slug, slugW - 2).padEnd(slugW)) + label);
1677
1687
  }
1678
1688
  log.blank();
1679
1689
  if (noVersionInfo.length > 0) {
1680
1690
  log.dim(`${noVersionInfo.length} installed without a version stamp — can't check ${noVersionInfo.length === 1 ? "it" : "them"} for updates.`);
1681
1691
  }
1682
- if (degradedNote)
1683
- log.dim(degradedNote);
1684
1692
  }
1685
1693
  // What the JSON carries for the new facts: per slug, the pod's data changes
1686
1694
  // (`null` = this pod sent none: an older pod) and the template lifecycle.
@@ -1722,7 +1730,9 @@ export async function marketUpdate(slugsArg, opts) {
1722
1730
  return;
1723
1731
  }
1724
1732
  if (!shouldApply) {
1725
- const steps = FLOW.afterMarketUpdateCheck(withUpdates.map((c) => c.slug));
1733
+ // Every update applies through the pod's one update door, so every one
1734
+ // is offered; what only the person can do is said when it is applied.
1735
+ const steps = FLOW.afterMarketUpdateCheck(cliUpdatableSlugs(withUpdates));
1726
1736
  if (opts.json) {
1727
1737
  console.log(JSON.stringify({
1728
1738
  checks,
@@ -1744,31 +1754,7 @@ export async function marketUpdate(slugsArg, opts) {
1744
1754
  }
1745
1755
  const acceptList = parseAcceptList(opts.acceptDataChanges);
1746
1756
  const results = [];
1747
- // One pod-wide reconcile serves every view/skill/automation update — run it
1748
- // at most once, lazily, and read each slug's outcome from its report.
1749
- let configsReport;
1750
1757
  for (const c of withUpdates) {
1751
- const door = updateDoorFor(c.kind);
1752
- if (door === null) {
1753
- if (!opts.json)
1754
- log.warn(`${c.slug} is a ${c.kind} — the pod has no per-package update for this kind, so it was not updated. Nothing was written.`);
1755
- results.push({ slug: c.slug, outcome: "no-updater", status: "no-updater", warnings: [] });
1756
- continue;
1757
- }
1758
- if (door === "configs-reconcile") {
1759
- configsReport ??= hubPost("/configs/reconcile", {}, cfg, applyTimeoutMs);
1760
- results.push(await reconcileOutcome(c, configsReport, { json: opts.json }));
1761
- continue;
1762
- }
1763
- const entry = entryBySlug.get(c.slug);
1764
- if (!entry) {
1765
- // Absent from the catalog THIS CLI read — not proof it left the catalog
1766
- // (that is the pod's `unlisted`, shown above, and only after a complete sync).
1767
- if (!opts.json)
1768
- log.warn(`Couldn't find "${c.slug}" in the catalog read just now — skipping.`);
1769
- results.push({ slug: c.slug, outcome: "not-in-catalog", status: "not-in-catalog", warnings: [] });
1770
- continue;
1771
- }
1772
1758
  const pv = previews.get(c.slug);
1773
1759
  const pending = pv?.changes ?? [];
1774
1760
  let accepted = [];
@@ -1793,17 +1779,14 @@ export async function marketUpdate(slugsArg, opts) {
1793
1779
  }
1794
1780
  if (missing.length > 0) {
1795
1781
  if (!opts.json) {
1796
- log.warn(`${entry.name} not updated: you have not agreed to its data changes. Nothing was written.`);
1782
+ log.warn(`${c.slug} not updated: you have not agreed to its data changes. Nothing was written.`);
1797
1783
  log.hint(`To agree and update: ${consentRerunCommand(c.slug, missing)}`);
1798
1784
  }
1799
1785
  results.push({ slug: c.slug, outcome: "needs-consent", status: "needs-consent", warnings: [], pendingDataChanges: missing });
1800
1786
  continue;
1801
1787
  }
1802
1788
  }
1803
- results.push(await applyOnePackage(entry, cat, cfg, { json: opts.json }, applyTimeoutMs, {
1804
- payload: pv?.payload,
1805
- acceptDataChanges: accepted,
1806
- }));
1789
+ results.push(await applyUpdateThroughDoor(c, cfg, { json: opts.json }, applyTimeoutMs, accepted));
1807
1790
  }
1808
1791
  if (results.some((r) => r.status === "needs-consent"))
1809
1792
  process.exitCode = 1;
@@ -1827,16 +1810,18 @@ export async function marketUpdate(slugsArg, opts) {
1827
1810
  renderNextSteps(steps);
1828
1811
  }
1829
1812
  /**
1830
- * One-line rollup over a batch of `applyOnePackage` outcomes — computed from
1831
- * the SAME per-item `status` (`classifyApplyResult`'s honest verdict) the
1813
+ * One-line rollup over a batch of update outcomes (`applyUpdateThroughDoor`) — computed from
1814
+ * the SAME per-item `status` the
1832
1815
  * loop already collects, not a separate tally. `"installed"`/`"updated"`/
1833
1816
  * `"composed"`/`"legacy-unknown"` count as a clean update, `"unchanged"` as
1834
1817
  * already-current, the `*-with-failures` and `"stamp-contradiction"` statuses
1835
1818
  * get their OWN bucket — a partial capability-seed failure or a stamp that
1836
1819
  * silently didn't take is never folded into a clean "updated" count.
1837
- * `"timed-out"` is broken out from the generic `"failed"` bucket; everything
1838
- * else (locked/proposed/fetch-failed/not-in-catalog/no-workspace/unknown)
1839
- * rolls into "failed".
1820
+ * `"timed-out"` is broken out from the generic `"failed"` bucket;
1821
+ * `"proposed"`/`"awaiting-approval"` count as waiting for the person's
1822
+ * approval (never failed); everything else
1823
+ * (locked/fetch-failed/not-in-catalog/no-workspace/unknown) rolls into
1824
+ * "failed".
1840
1825
  */
1841
1826
  export function summarizeApplyResults(results) {
1842
1827
  let updated = 0;
@@ -1846,6 +1831,7 @@ export function summarizeApplyResults(results) {
1846
1831
  let failed = 0;
1847
1832
  let proposed = 0;
1848
1833
  let needsConsent = 0;
1834
+ let toApplyInPodAdmin = 0;
1849
1835
  for (const r of results) {
1850
1836
  switch (r.status) {
1851
1837
  case "installed":
@@ -1880,17 +1866,23 @@ export function summarizeApplyResults(results) {
1880
1866
  case "needs-consent":
1881
1867
  needsConsent++;
1882
1868
  break;
1869
+ // The person's act (pod admin → Marketplace → Updates): nothing failed.
1870
+ case APPLY_IN_POD_ADMIN:
1871
+ toApplyInPodAdmin++;
1872
+ break;
1883
1873
  default:
1884
1874
  failed++;
1885
1875
  }
1886
1876
  }
1887
1877
  const parts = [`${updated} updated`, `${unchanged} already current`];
1888
1878
  if (proposed > 0)
1889
- parts.push(`${proposed} proposed (awaiting review)`);
1879
+ parts.push(`${proposed} waiting for your approval`);
1890
1880
  // The vocabulary's `unfinished` ("Not finished"): one state, one name on
1891
1881
  // every surface — never "completed with failures" (review2 UX §1 #2).
1892
1882
  if (withFailures > 0)
1893
1883
  parts.push(`${withFailures} ${templateStepPhrase("unfinished")}`);
1884
+ if (toApplyInPodAdmin > 0)
1885
+ parts.push(`${toApplyInPodAdmin} to apply in pod admin`);
1894
1886
  if (failed > 0)
1895
1887
  parts.push(`${failed} failed`);
1896
1888
  if (timedOut > 0)
@@ -1899,9 +1891,9 @@ export function summarizeApplyResults(results) {
1899
1891
  parts.push(`${needsConsent} waiting for your agreement to data changes`);
1900
1892
  const clean = withFailures === 0 && failed === 0 && timedOut === 0 && needsConsent === 0;
1901
1893
  // Proposed isn't a failure (never ⚠) but isn't live yet (not a bare ✓) —
1902
- // a distinct "→ awaiting review" marker keeps the summary honest.
1903
- const marker = !clean ? "⚠" : proposed > 0 ? "→" : "✓";
1904
- const color = !clean ? chalk.yellow : proposed > 0 ? chalk.cyan : chalk.green;
1894
+ // a distinct "→ waiting for your approval" marker keeps the summary honest.
1895
+ const marker = !clean ? "⚠" : proposed > 0 || toApplyInPodAdmin > 0 ? "→" : "✓";
1896
+ const color = !clean ? chalk.yellow : proposed > 0 || toApplyInPodAdmin > 0 ? chalk.cyan : chalk.green;
1905
1897
  return {
1906
1898
  updated,
1907
1899
  unchanged,
@@ -1910,6 +1902,7 @@ export function summarizeApplyResults(results) {
1910
1902
  timedOut,
1911
1903
  proposed,
1912
1904
  needsConsent,
1905
+ toApplyInPodAdmin,
1913
1906
  line: color(`${marker} ${parts.join(" · ")}`),
1914
1907
  };
1915
1908
  }
@@ -2389,7 +2382,7 @@ export async function marketInstalled(opts) {
2389
2382
  const entryBySlug = new Map(cat.entries.map((e) => [e.slug, e]));
2390
2383
  if (opts.layers) {
2391
2384
  const graph = await buildInstallationGraph(installedTemplates, cat, checks);
2392
- const steps = FLOW.afterMarketInstalledCheck(rows.filter((c) => c.updateAvailable).map((c) => c.slug));
2385
+ const steps = FLOW.afterMarketInstalledCheck(cliUpdatableSlugs(rows));
2393
2386
  if (opts.json) {
2394
2387
  console.log(JSON.stringify({
2395
2388
  nodes: [...graph.nodes.values()],
@@ -2416,7 +2409,7 @@ export async function marketInstalled(opts) {
2416
2409
  }
2417
2410
  if (opts.tree) {
2418
2411
  const { packages, leafRows } = await buildCompositionTrees(rows, cat, installedTemplates, workspaceCountBySlug);
2419
- const steps = FLOW.afterMarketInstalledCheck(rows.filter((c) => c.updateAvailable).map((c) => c.slug));
2412
+ const steps = FLOW.afterMarketInstalledCheck(cliUpdatableSlugs(rows));
2420
2413
  if (opts.json) {
2421
2414
  console.log(JSON.stringify({
2422
2415
  packages,
@@ -2476,7 +2469,7 @@ export async function marketInstalled(opts) {
2476
2469
  return;
2477
2470
  }
2478
2471
  if (opts.json) {
2479
- const steps = FLOW.afterMarketInstalledCheck(rows.filter((c) => c.updateAvailable).map((c) => c.slug));
2472
+ const steps = FLOW.afterMarketInstalledCheck(cliUpdatableSlugs(rows));
2480
2473
  console.log(JSON.stringify({
2481
2474
  rows: rows.map((c) => {
2482
2475
  const entry = entryBySlug.get(c.slug);
@@ -2594,7 +2587,7 @@ export async function marketInstalled(opts) {
2594
2587
  log.dim(degradedNote);
2595
2588
  if (!cat.loggedIn)
2596
2589
  log.dim("Not logged in — private template updates aren't visible. Run: synap login");
2597
- renderNextSteps(FLOW.afterMarketInstalledCheck(rows.filter((c) => c.updateAvailable).map((c) => c.slug)));
2590
+ renderNextSteps(FLOW.afterMarketInstalledCheck(cliUpdatableSlugs(rows)));
2598
2591
  }
2599
2592
  /** Classify one workspace's attachment, reusing `computeUpdates`' drift verdict for the stamped case. */
2600
2593
  function classifyAttachment(ws, check) {