@frontera-sdk/cli 1.52.39 → 1.52.41

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 (235) hide show
  1. package/README.md +9 -0
  2. package/package.json +6 -4
  3. package/src/adopt.ts +6 -2
  4. package/src/api/apps-api.ts +86 -4
  5. package/src/api/blueprint-authoring-api.ts +35 -11
  6. package/src/api/dataset-api.ts +29 -0
  7. package/src/api/governed-action-api.ts +90 -2
  8. package/src/api/platform-api.ts +87 -2
  9. package/src/api/workflow-api.ts +33 -2
  10. package/src/app-contract.ts +156 -0
  11. package/src/app-lock.ts +125 -0
  12. package/src/args.ts +14 -3
  13. package/src/auth-verify.ts +10 -0
  14. package/src/automation-template.ts +12 -1
  15. package/src/blueprint/action-rules.ts +516 -0
  16. package/src/blueprint/action-semantic-fit.ts +205 -0
  17. package/src/blueprint/compile.ts +51 -2
  18. package/src/blueprint/diff.ts +34 -2
  19. package/src/blueprint/metric-query.ts +215 -0
  20. package/src/blueprint/metric-rules.ts +122 -0
  21. package/src/blueprint/object-sets.ts +116 -0
  22. package/src/blueprint/ontology-edit-plan.ts +49 -5
  23. package/src/blueprint/projection.ts +24 -1
  24. package/src/blueprint/scaffold.ts +84 -42
  25. package/src/commands/action/available.ts +1 -1
  26. package/src/commands/action/criteria-refusal.ts +84 -0
  27. package/src/commands/action/decide.ts +5 -2
  28. package/src/commands/action/deploy.ts +13 -4
  29. package/src/commands/action/list.ts +134 -19
  30. package/src/commands/action/requests.ts +34 -6
  31. package/src/commands/action/submit.ts +28 -13
  32. package/src/commands/agent/draft-changes.ts +1 -1
  33. package/src/commands/agent/index-commands.ts +69 -7
  34. package/src/commands/agent/resolve.ts +2 -0
  35. package/src/commands/app/checks.ts +44 -0
  36. package/src/commands/app/deploy.ts +243 -60
  37. package/src/commands/app/deployments.ts +97 -0
  38. package/src/commands/app/dev.ts +103 -11
  39. package/src/commands/app/init.ts +36 -3
  40. package/src/commands/app/promote.ts +45 -19
  41. package/src/commands/app/pull.ts +3 -1
  42. package/src/commands/app/rollback.ts +46 -0
  43. package/src/commands/app/sdk.ts +54 -8
  44. package/src/commands/app/unpin.ts +27 -0
  45. package/src/commands/app/versions.ts +1 -1
  46. package/src/commands/automation/bindings.ts +4 -2
  47. package/src/commands/automation/build-entry.ts +4 -1
  48. package/src/commands/automation/index-commands.ts +3 -1
  49. package/src/commands/automation/pull.ts +6 -1
  50. package/src/commands/automation/run.ts +34 -9
  51. package/src/commands/blueprint/bind.ts +2 -1
  52. package/src/commands/blueprint/declarative.ts +332 -32
  53. package/src/commands/blueprint/editable.ts +3 -3
  54. package/src/commands/blueprint/get.ts +6 -2
  55. package/src/commands/blueprint/grants.ts +64 -10
  56. package/src/commands/blueprint/metric.ts +108 -0
  57. package/src/commands/blueprint/object-sets.ts +71 -0
  58. package/src/commands/blueprint/query.ts +155 -31
  59. package/src/commands/dataset/index-commands.ts +156 -7
  60. package/src/commands/knowledge/index-commands.ts +122 -20
  61. package/src/commands/local/check.ts +657 -44
  62. package/src/commands/local/data.ts +33 -12
  63. package/src/commands/local/dev.ts +876 -89
  64. package/src/commands/local/docs-mcp.ts +10 -18
  65. package/src/commands/local/index-commands.ts +300 -63
  66. package/src/commands/local/init.ts +18 -5
  67. package/src/commands/local/key.ts +0 -3
  68. package/src/commands/local/providers.ts +0 -7
  69. package/src/commands/local/remove.ts +116 -0
  70. package/src/commands/local/schema.ts +3 -4
  71. package/src/commands/local/sdk-refresh.ts +59 -0
  72. package/src/commands/local/snapshot.ts +100 -0
  73. package/src/commands/local/sync-request.ts +321 -14
  74. package/src/commands/local/sync.ts +103 -6
  75. package/src/commands/mcp/serve.ts +34 -0
  76. package/src/commands/registry.ts +54 -10
  77. package/src/commands/types.ts +29 -0
  78. package/src/commands/workflow/archive.ts +46 -0
  79. package/src/commands/workflow/get.ts +2 -1
  80. package/src/commands/workflow/index-commands.ts +5 -0
  81. package/src/commands/workflow/push.ts +49 -2
  82. package/src/commands/workflow/run.ts +65 -7
  83. package/src/commands/workflow/runs.ts +9 -6
  84. package/src/commands/workflow/webhook-test.ts +166 -0
  85. package/src/config.ts +43 -6
  86. package/src/dev-broker.ts +52 -0
  87. package/src/exit.ts +103 -4
  88. package/src/flag-help.ts +77 -17
  89. package/src/forge/claude-commands.ts +11 -6
  90. package/src/forge/claude-skills.ts +4 -4
  91. package/src/forge/touches.ts +15 -12
  92. package/src/function-outside-imports.ts +89 -0
  93. package/src/harness.ts +42 -24
  94. package/src/heal.ts +113 -2
  95. package/src/kit.ts +1 -1
  96. package/src/local/fix-words.ts +12 -0
  97. package/src/local/function-bundle-recovery.ts +775 -0
  98. package/src/local/local-actions.ts +103 -10
  99. package/src/local/local-agent-action-grants.ts +210 -0
  100. package/src/local/local-agent-checks.ts +232 -0
  101. package/src/local/local-agent-grants.ts +29 -7
  102. package/src/local/local-agent-lean-plugins.ts +68 -0
  103. package/src/local/local-agent-missing-tools.ts +68 -0
  104. package/src/local/local-agent-name.ts +27 -0
  105. package/src/local/local-app-code.ts +191 -0
  106. package/src/local/local-app-dev-files.ts +186 -0
  107. package/src/local/local-app-dev-session.ts +88 -0
  108. package/src/local/local-app-lock-install.ts +114 -0
  109. package/src/local/local-app-pulled.ts +167 -0
  110. package/src/local/local-app-restore.ts +111 -0
  111. package/src/local/local-app-runtime.ts +68 -0
  112. package/src/local/local-app-sdk-paths.ts +149 -0
  113. package/src/local/local-app-sdk.ts +509 -0
  114. package/src/local/local-applied.ts +212 -22
  115. package/src/local/local-apply-history.ts +84 -0
  116. package/src/local/local-apply-summary.ts +86 -0
  117. package/src/local/local-apply-watchdog.ts +95 -0
  118. package/src/local/local-apps.ts +181 -8
  119. package/src/local/local-blueprint-errors.ts +198 -0
  120. package/src/local/local-blueprint-outline.ts +31 -12
  121. package/src/local/local-blueprint-schemas.ts +582 -0
  122. package/src/local/local-build-inputs.ts +48 -0
  123. package/src/local/local-build-summary.ts +495 -28
  124. package/src/local/local-case-evidence.ts +120 -0
  125. package/src/local/local-case-not-run.ts +153 -0
  126. package/src/local/local-chat-apps.ts +93 -16
  127. package/src/local/local-check-inherited.ts +83 -0
  128. package/src/local/local-cities.ts +89 -0
  129. package/src/local/local-component-health.ts +119 -0
  130. package/src/local/local-control.ts +34 -7
  131. package/src/local/local-db-snapshot.ts +348 -0
  132. package/src/local/local-dev-references.ts +347 -36
  133. package/src/local/local-dev-sync.ts +141 -12
  134. package/src/local/local-docker.ts +35 -1
  135. package/src/local/local-docs-search.ts +1 -1
  136. package/src/local/local-docs.ts +281 -2
  137. package/src/local/local-empty-workflows.ts +92 -0
  138. package/src/local/local-env.ts +16 -1
  139. package/src/local/local-file-checks.ts +104 -0
  140. package/src/local/local-function-ids.ts +141 -0
  141. package/src/local/local-function-recovery.ts +141 -0
  142. package/src/local/local-function-state.ts +87 -0
  143. package/src/local/local-keys.ts +7 -5
  144. package/src/local/local-knowledge-documents.ts +1 -1
  145. package/src/local/local-knowledge-ingestion.ts +225 -0
  146. package/src/local/local-knowledge-uploads.ts +217 -0
  147. package/src/local/local-lessons.ts +1 -1
  148. package/src/local/local-library.ts +1 -1
  149. package/src/local/local-media-placeholders.ts +182 -0
  150. package/src/local/local-media-sets.ts +90 -10
  151. package/src/local/local-model-parameters.ts +89 -0
  152. package/src/local/local-object-sets.ts +18 -6
  153. package/src/local/local-operating.ts +35 -1
  154. package/src/local/local-org-settings.ts +206 -8
  155. package/src/local/local-orphans.ts +321 -0
  156. package/src/local/local-personas.ts +296 -0
  157. package/src/local/local-portability.ts +143 -18
  158. package/src/local/local-ports.ts +99 -7
  159. package/src/local/local-project-notices.ts +234 -0
  160. package/src/local/local-project-secrets.ts +133 -0
  161. package/src/local/local-project.ts +12 -0
  162. package/src/local/local-provider-refusal.ts +66 -0
  163. package/src/local/local-request-evidence.ts +112 -0
  164. package/src/local/local-runtime-health.ts +123 -0
  165. package/src/local/local-runtime-history.ts +50 -0
  166. package/src/local/local-runtime-target.ts +42 -0
  167. package/src/local/local-sample-data.ts +141 -0
  168. package/src/local/local-sample-remake.ts +138 -0
  169. package/src/local/local-scaffold.ts +336 -53
  170. package/src/local/local-schema-check.ts +148 -0
  171. package/src/local/local-schemas.ts +742 -25
  172. package/src/local/local-secret-carry.ts +107 -0
  173. package/src/local/local-secrets.ts +41 -11
  174. package/src/local/local-seed.ts +102 -26
  175. package/src/local/local-stand-ins.ts +3 -3
  176. package/src/local/local-state.ts +43 -2
  177. package/src/local/local-studio-server.ts +170 -7
  178. package/src/local/local-supervisor.ts +44 -6
  179. package/src/local/local-sync-merge.ts +1 -1
  180. package/src/local/local-sync-names.ts +2 -0
  181. package/src/local/local-sync-plan.ts +19 -8
  182. package/src/local/local-sync-push.ts +15 -12
  183. package/src/local/local-sync-refs.ts +49 -3
  184. package/src/local/local-sync-remote.ts +349 -38
  185. package/src/local/local-sync-snapshot.ts +18 -13
  186. package/src/local/local-synthetic-data.ts +2116 -99
  187. package/src/local/local-test-baseline.ts +113 -0
  188. package/src/local/local-test-cases.ts +76 -1
  189. package/src/local/local-test-runner.ts +582 -91
  190. package/src/local/local-test-runs.ts +13 -5
  191. package/src/local/local-vendored-sdk.ts +183 -0
  192. package/src/local/local-workflow-state.ts +16 -3
  193. package/src/local/local-workflow-tests.ts +482 -0
  194. package/src/local/local-workspace-grants.ts +81 -0
  195. package/src/main.ts +50 -9
  196. package/src/mcp/a11y-scan.ts +55 -0
  197. package/src/mcp/browser/ax-snapshot.ts +181 -0
  198. package/src/mcp/browser/credentials.ts +78 -0
  199. package/src/mcp/browser/frames.ts +336 -0
  200. package/src/mcp/browser/headless-session.ts +413 -0
  201. package/src/mcp/browser/headless-tab.ts +575 -0
  202. package/src/mcp/browser/keys.ts +70 -0
  203. package/src/mcp/browser/page-scripts.ts +129 -0
  204. package/src/mcp/browser/wait.ts +73 -0
  205. package/src/mcp/forge-link.ts +126 -0
  206. package/src/mcp/headless.ts +199 -0
  207. package/src/mcp/mcp-apps.ts +435 -0
  208. package/src/mcp/mcp-chat.ts +226 -0
  209. package/src/mcp/mcp-cli.ts +53 -0
  210. package/src/mcp/mcp-protocol.ts +122 -0
  211. package/src/mcp/mcp-runtime.ts +113 -0
  212. package/src/mcp/mcp-server.ts +108 -0
  213. package/src/mcp/mcp-traces.ts +409 -0
  214. package/src/mcp/page-observation.ts +193 -0
  215. package/src/mcp/tools/agent-tools.ts +700 -0
  216. package/src/mcp/tools/app-tools.ts +241 -0
  217. package/src/mcp/tools/browser-tools.ts +310 -0
  218. package/src/mcp/tools/docs-tools.ts +32 -0
  219. package/src/mcp/tools/project-tools.ts +205 -0
  220. package/src/mcp/tools/test-tools.ts +339 -0
  221. package/src/mcp/tools/workflow-tools.ts +611 -0
  222. package/src/output.ts +1 -1
  223. package/src/pack.ts +8 -3
  224. package/src/packaging.ts +1 -1
  225. package/src/project-bootstrap.ts +107 -4
  226. package/src/project-context.ts +18 -1
  227. package/src/project.ts +27 -2
  228. package/src/provenance.ts +25 -0
  229. package/src/render-evidence.ts +65 -7
  230. package/src/scopes.ts +1 -1
  231. package/src/template.ts +14 -8
  232. package/src/templates/next-app-files.ts +35 -6
  233. package/src/templates/next-skills.ts +61 -27
  234. package/src/vendor/kit-assets.json +6 -6
  235. package/src/vendor/sdk-sources.json +17 -14
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto'
3
3
  import { GovernedActionApi } from '../../api/governed-action-api'
4
4
  import { CliError, UsageError } from '../../errors'
5
5
  import { deriveOntologyEditPlan, PlanDerivationError } from '../../blueprint/ontology-edit-plan'
6
+ import { validationFindings } from '../../local/local-actions'
6
7
  import { flagBool, flagString, type Command, type CommandContext } from '../types'
7
8
 
8
9
  /**
@@ -92,10 +93,18 @@ export const actionDeploy: Command = {
92
93
  const validation = await client.validateBindingRevision(binding.id, 1)
93
94
 
94
95
  if (validation.status !== 'valid') {
95
- throw new CliError(`Deployment validation returned "${validation.status}".`, {
96
- code: 'FAILURE',
97
- hint: JSON.stringify(validation.findings ?? validation),
98
- })
96
+ // Each failed check, by its code and the platform's message, one per line; the full report in `--json` details.
97
+ const failed = validationFindings(validation)
98
+ throw new CliError(
99
+ `the write path for "${apiName}" did not validate${failed.length ? `: ${failed.length} check${failed.length === 1 ? '' : 's'} failed` : ` (status "${validation.status}")`}`,
100
+ {
101
+ code: 'ACTION_DEPLOY_INVALID',
102
+ hint: failed.length
103
+ ? `${failed.map((f) => `✕ ${f}`).join('\n ')}\n fix the Action (blueprint/actions/${apiName}.yaml) or its object type's editable fields, publish, and deploy again`
104
+ : 'the service gave no findings for this report; read it with `--json`',
105
+ details: { action: apiName, bindingRevisionId: binding.id, validationReportId: validation.id, status: validation.status, findings: validation.findings ?? null, failed },
106
+ },
107
+ )
99
108
  }
100
109
 
101
110
  return {
@@ -1,49 +1,164 @@
1
- import { GovernedActionApi } from '../../api/governed-action-api'
1
+ import { GovernedActionApi, type DeployableActionEntry } from '../../api/governed-action-api'
2
+ import type { BuildEntry } from '../../local/local-applied'
3
+ import { blueprintBuild } from '../../local/local-blueprint-outline'
4
+ import { localRuntimeAt } from '../../local/local-runtime-target'
2
5
  import { table } from '../../table'
3
6
  import type { Command } from '../types'
4
7
 
5
8
  /**
6
- * Published Actions and whether anything can run them.
9
+ * Every Action and whether anything can run it.
7
10
  *
8
11
  * `deploymentStatus` is the question this answers: an Action can be published,
9
- * correct and completely inert, and nothing else in the CLI says so. `unbound`
10
- * here is the same state `blueprint publish` refuses to create and that an
11
- * agent silently cannot see.
12
+ * correct and completely inert, and nothing else in the CLI says so. The plain
13
+ * published list holds only Actions that can be invoked, so it never showed
14
+ * the ones that matter here: this reads the deployable view, which keeps
15
+ * every published Action with its readiness (one deployable read per Action
16
+ * on a service without the list), and the draft, for Actions authored but not
17
+ * published yet. WHY NOT is said for each one that cannot run, with the
18
+ * command that moves it.
12
19
  */
20
+
21
+ export interface ActionRow {
22
+ apiName: string
23
+ published: boolean
24
+ /**
25
+ * `active`, `disabled`, `unbound`, `suspended`, `revoked`; `—` when not published;
26
+ * `left-off` on a local runtime that `local dev` did not give the Action to.
27
+ */
28
+ deployment: string
29
+ available: boolean
30
+ /** The service's readiness reason when it cannot run (`ACTION_UNBOUND`), or null. */
31
+ reason: string | null
32
+ /** In words, with the next command; empty when it runs. */
33
+ whyNot: string
34
+ /**
35
+ * What it runs with that its owner should fix (the service's deployment
36
+ * warnings): a value it offers that the semantic type it writes refuses.
37
+ * It still runs; a request with that value is refused when submitted.
38
+ */
39
+ warnings?: string[]
40
+ }
41
+
42
+ /** WHY NOT, in words, from the readiness the service reports. */
43
+ export function whyNot(apiName: string, entry: { published: boolean; deploymentStatus?: string; reason?: string | null; available?: boolean }): string {
44
+ if (!entry.published) return `not published yet — \`frontera blueprint publish\``
45
+ if (entry.available && (!entry.reason || entry.reason === 'ACTION_AVAILABLE' || entry.reason === 'ACTION_READY')) return ''
46
+ const code = entry.reason && entry.reason !== 'ACTION_AVAILABLE' ? ` (${entry.reason})` : ''
47
+ switch (entry.deploymentStatus) {
48
+ case 'unbound':
49
+ return `no write path${code} — \`frontera action deploy ${apiName}\`, then \`action review\``
50
+ case 'disabled':
51
+ return `ships switched off${code} — \`frontera action deploy ${apiName}\`, then \`action review\``
52
+ case 'suspended':
53
+ return `its deployment is suspended${code} — review it again with \`frontera action review\``
54
+ case 'revoked':
55
+ return `its deployment was revoked${code} — \`frontera action deploy ${apiName}\` again`
56
+ case 'active':
57
+ return entry.reason === 'ACTION_FORBIDDEN' ? `deployed, but no role may run it${code} — \`frontera action grant\`` : `deployed, but not available${code}`
58
+ default:
59
+ return `not deployed${code} — \`frontera action deploy ${apiName}\``
60
+ }
61
+ }
62
+
63
+ function draftName(entry: Record<string, unknown>): string | null {
64
+ const nested = (entry.definition ?? entry.action) as { apiName?: unknown } | undefined
65
+ const name = nested?.apiName ?? entry.apiName
66
+ return typeof name === 'string' && name ? name : null
67
+ }
68
+
69
+ /** The rows: every published Action with its readiness, then each Action only the draft has. */
70
+ export async function actionRows(client: Pick<GovernedActionApi, 'listDeployableActions' | 'listDraftActions' | 'listPublishedActions' | 'publishedAction'>): Promise<ActionRow[]> {
71
+ const drafts = await client.listDraftActions().then((list) => (Array.isArray(list) ? list.flatMap((e) => draftName(e) ?? []) : [])).catch(() => [] as string[])
72
+ let deployable = await client.listDeployableActions()
73
+ if (!deployable) {
74
+ // A service without the list view: each Action the draft or the published list names, read one at a time.
75
+ const published = await client.listPublishedActions().catch(() => [])
76
+ const names = [...new Set([...published.map((p) => p.definition.apiName), ...drafts])]
77
+ deployable = []
78
+ for (const name of names) {
79
+ const one = await client.publishedAction(name).catch(() => null)
80
+ if (one) deployable.push({ definition: one.definition as DeployableActionEntry['definition'], deploymentStatus: one.deploymentStatus, availability: one.availability, reason: one.reason })
81
+ }
82
+ }
83
+ const rows: ActionRow[] = deployable.map((e) => {
84
+ const available = e.availability === 'available' && e.executable !== false
85
+ const row = { published: true, deploymentStatus: e.deploymentStatus, reason: e.reason ?? null, available }
86
+ return {
87
+ apiName: e.definition.apiName,
88
+ published: true,
89
+ deployment: e.deploymentStatus ?? '—',
90
+ available,
91
+ reason: e.reason && e.reason !== 'ACTION_AVAILABLE' ? e.reason : null,
92
+ whyNot: whyNot(e.definition.apiName, row),
93
+ ...(e.warnings?.length ? { warnings: e.warnings.map((w) => w.message) } : {}),
94
+ }
95
+ })
96
+ const listed = new Set(rows.map((r) => r.apiName))
97
+ for (const name of drafts) {
98
+ if (listed.has(name)) continue
99
+ rows.push({ apiName: name, published: false, deployment: '—', available: false, reason: null, whyNot: whyNot(name, { published: false }) })
100
+ }
101
+ return rows.sort((a, b) => a.apiName.localeCompare(b.apiName))
102
+ }
103
+
104
+ /**
105
+ * On a project's own local runtime, each Action `local dev` left off it (the
106
+ * runtime refused its file, or it acts on a type left off here): the runtime
107
+ * never holds those, so the service's list cannot name them. From the last
108
+ * build's record, for the Blueprint files as they are now.
109
+ */
110
+ export function leftOffActionRows(actions: BuildEntry['actions'] | null, listed: ReadonlySet<string>): ActionRow[] {
111
+ return Object.entries(actions ?? {}).flatMap(([apiName, a]) => {
112
+ if (a.status !== 'left-off' || listed.has(apiName)) return []
113
+ const why = (a.reason ?? 'local dev left it off').replace(/^the local runtime refused it: /, '')
114
+ return [{
115
+ apiName,
116
+ published: false,
117
+ deployment: 'left-off',
118
+ available: false,
119
+ reason: a.refused ? 'LOCAL_RUNTIME_REFUSED' : 'LOCAL_LEFT_OFF',
120
+ whyNot: a.refused
121
+ ? `the local runtime refused its file: ${why} — fix blueprint/actions/, then \`frontera local dev --once\``
122
+ : `left off this runtime: ${why}`,
123
+ }]
124
+ })
125
+ }
126
+
13
127
  export const actionList: Command = {
14
128
  meta: {
15
129
  noun: 'action',
16
130
  verb: 'list',
17
131
  args: [],
18
132
  flags: {},
19
- summary: 'List published Actions and their deployment state',
20
- examples: ['frontera action list'],
133
+ summary: 'List every Action, its deployment state and why it cannot run',
134
+ examples: ['frontera action list', 'frontera action list --json'],
21
135
  },
22
136
 
23
137
  async run(ctx) {
24
138
  const client = new GovernedActionApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
25
- const actions = await client.listPublishedActions()
139
+ const actions = await actionRows(client)
140
+ // The project's own local runtime: the Actions `local dev` left off it, which the runtime cannot list.
141
+ const local = localRuntimeAt(ctx.cwd, ctx.apiUrl)
142
+ if (local) {
143
+ actions.push(...leftOffActionRows(blueprintBuild(local.project.root).actions, new Set(actions.map((a) => a.apiName))))
144
+ actions.sort((a, b) => a.apiName.localeCompare(b.apiName))
145
+ }
26
146
 
27
147
  if (actions.length === 0) {
28
148
  return {
29
149
  data: { actions: [] },
30
- text: 'No published Actions.\n Author one, then `frontera blueprint publish`.',
150
+ text: 'No Actions.\n Author one in blueprint/actions/, then `frontera blueprint apply` and `frontera blueprint publish`.',
31
151
  }
32
152
  }
33
153
 
34
- const rows = actions.map((entry) => ({
35
- apiName: entry.definition.apiName,
36
- deployment: entry.deploymentStatus ?? '—',
37
- // Present only when it is NOT invocable, which is when it matters.
38
- reason: entry.reason && entry.reason !== 'ACTION_AVAILABLE' ? entry.reason : '',
39
- }))
40
-
154
+ // Each Action that runs with something its owner should fix, said under the table.
155
+ const warned = actions.flatMap((r) => (r.warnings ?? []).map((w) => ` warning: ${r.apiName}: ${w} It runs; its owner fixes the Action's contract.`))
41
156
  return {
42
157
  data: { actions },
43
- text: table(
158
+ text: [table(
44
159
  ['ACTION', 'DEPLOYMENT', 'WHY NOT'],
45
- rows.map((r) => [r.apiName, r.deployment, r.reason]),
46
- ),
160
+ actions.map((r) => [r.apiName, r.deployment, r.whyNot]),
161
+ ), ...warned].join('\n'),
47
162
  }
48
163
  },
49
164
  }
@@ -1,5 +1,6 @@
1
1
  import { GovernedActionApi } from '../../api/governed-action-api'
2
2
  import { UsageError } from '../../errors'
3
+ import { localRuntimeAt } from '../../local/local-runtime-target'
3
4
  import { table } from '../../table'
4
5
  import { flagBool, flagString, type Command } from '../types'
5
6
 
@@ -51,18 +52,19 @@ export const actionRequests: Command = {
51
52
  const request = await client.getRequest(requestId)
52
53
  return {
53
54
  data: request,
55
+ // Why it failed, first: the reason, the refusal it came from and its code, when the service says.
56
+ text: [failureLines((request as { failure?: unknown }).failure), table(
54
57
  // Flattened rather than curated: the surface records provenance,
55
58
  // attempts and failure detail under names that differ by adapter, and
56
59
  // naming a subset here would hide exactly the field that explains the
57
60
  // failure being investigated.
58
- text: table(
59
61
  ['field', 'value'],
60
62
  Object.entries(request).map(([k, v]) => [
61
63
  k,
62
64
  v === null || v === undefined ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v),
63
65
  ]),
64
66
  [undefined, 80],
65
- ),
67
+ )].filter(Boolean).join('\n\n'),
66
68
  }
67
69
  }
68
70
 
@@ -100,6 +102,8 @@ export const actionRequests: Command = {
100
102
 
101
103
  if (page.requests.length === 0) {
102
104
  const filtered = flagString(ctx, 'lifecycle')
105
+ // On the project's own local runtime a person is a local persona (`--as`), never a session token to fetch.
106
+ const local = localRuntimeAt(ctx.cwd, ctx.apiUrl) !== null
103
107
  if (awaitingMe) {
104
108
  return {
105
109
  data: page,
@@ -108,8 +112,12 @@ export const actionRequests: Command = {
108
112
  : 'Nothing is waiting for your decision.\n')
109
113
  + ' This lists the requests you could approve or reject right now: not ones\n'
110
114
  + ' you raised, already decided, or that wait on a step for an App role.\n'
111
- + ' It is a person\'s list: on a key it is always empty. Set FRONTERA_TOKEN\n'
112
- + ' to your session token to read your own.'
115
+ + (local
116
+ ? ' It is a person\'s list: on a key it is always empty. On this local runtime,\n'
117
+ + ' `frontera action requests --lifecycle awaiting_approval` lists what waits,\n'
118
+ + ' and `frontera action approve <requestId> --as <approver>` decides it.'
119
+ : ' It is a person\'s list: on a key it is always empty. Set FRONTERA_TOKEN\n'
120
+ + ' to your session token to read your own.')
113
121
  + incomplete,
114
122
  }
115
123
  }
@@ -119,8 +127,11 @@ export const actionRequests: Command = {
119
127
  ? `No requests with lifecycle "${filtered}".`
120
128
  : 'No Action requests recorded.\n'
121
129
  + ' Nothing has invoked a deployed Action yet. Raise one with\n'
122
- + ' `frontera action submit`, which acts as a person: set FRONTERA_TOKEN\n'
123
- + ' to your session token and FRONTERA_API_URL.',
130
+ + (local
131
+ ? ' `frontera action submit <apiName> --as submitter`: on this local runtime a\n'
132
+ + ' local persona acts as the person (`frontera local status` lists them).'
133
+ : ' `frontera action submit`, which acts as a person: set FRONTERA_TOKEN\n'
134
+ + ' to your session token and FRONTERA_API_URL.'),
124
135
  }
125
136
  }
126
137
 
@@ -151,3 +162,20 @@ export const actionRequests: Command = {
151
162
  }
152
163
  },
153
164
  }
165
+
166
+ /**
167
+ * A failed request's `failure` in words: its reason, the refusal it came from
168
+ * and its code, each on a line. Empty when the request has no failure.
169
+ */
170
+ export function failureLines(failure: unknown): string {
171
+ if (!failure || typeof failure !== 'object') return ''
172
+ const f = failure as { reason?: unknown; refusal?: unknown; reasonCode?: unknown; businessOutcomeCode?: unknown }
173
+ const text = (v: unknown) => (typeof v === 'string' && v ? v : typeof v === 'object' && v ? JSON.stringify(v) : '')
174
+ const lines = [
175
+ text(f.reason) ? `Failed: ${text(f.reason)}` : 'Failed',
176
+ text(f.refusal) ? ` refusal: ${text(f.refusal)}` : '',
177
+ text(f.reasonCode) ? ` code: ${text(f.reasonCode)}` : '',
178
+ text(f.businessOutcomeCode) ? ` outcome: ${text(f.businessOutcomeCode)}` : '',
179
+ ].filter(Boolean)
180
+ return lines.length > 1 || text(f.reason) ? lines.join('\n') : ''
181
+ }
@@ -3,6 +3,7 @@ import { CliError, UsageError } from '../../errors'
3
3
  import { toEnvelope } from '../../exit'
4
4
  import { parseInputFlag } from '../automation/run'
5
5
  import { flagString, type Command } from '../types'
6
+ import { criteriaRefusal } from './criteria-refusal'
6
7
  import { requestSummary } from './request-summary'
7
8
 
8
9
  /**
@@ -55,11 +56,16 @@ export const actionSubmit: Command = {
55
56
  'subject-version': 'string',
56
57
  reason: 'string',
57
58
  'idempotency-key': 'string',
59
+ as: 'string',
58
60
  },
59
61
  summary: 'Submit a request to a deployed Action',
62
+ details: [
63
+ 'On the local runtime, `--as <persona>` submits as a local persona (`--as submitter`), so a later step can be decided by another one. Refused anywhere else.',
64
+ ],
60
65
  examples: [
61
66
  'frontera action submit createTicket title=Broken priority=2',
62
67
  'frontera action submit escalateTicket --subject TKT-1 --subject-version 0',
68
+ 'frontera action submit escalateTicket id=TKT-1 expectedVersion=0',
63
69
  'frontera action submit createTicket --input @params.json',
64
70
  'frontera action submit createTicket --idempotency-key retry-2026-09-26-01',
65
71
  ],
@@ -72,11 +78,8 @@ export const actionSubmit: Command = {
72
78
  }
73
79
  const subject = flagString(ctx, 'subject')
74
80
  const version = flagString(ctx, 'subject-version')
75
- if ((subject === undefined) !== (version === undefined)) {
76
- throw new UsageError(
77
- '--subject and --subject-version go together',
78
- `pass both: frontera action submit ${apiName} --subject <objectId> --subject-version <version>`,
79
- )
81
+ if (version !== undefined && subject === undefined) {
82
+ throw new UsageError('--subject-version names the version of a --subject', `pass the record too: frontera action submit ${apiName} --subject <objectId> --subject-version <version>`)
80
83
  }
81
84
 
82
85
  const client = new GovernedActionApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
@@ -94,7 +97,13 @@ export const actionSubmit: Command = {
94
97
  ...(await parseInputFlag(ctx)),
95
98
  ...Object.fromEntries(pairs.map((raw) => pair(raw, stringInputs(action)))),
96
99
  }
97
- const missing = (action.inputSchema?.properties?.input?.required ?? []).filter((name) => !(name in input))
100
+ const existing = action.subject?.mode === 'existing' && action.subject.objectTypeId
101
+ // An edit names its record either way: `--subject` (and `--subject-version`), or the key
102
+ // and `expectedVersion` inputs. The platform fills whichever half is left out from the
103
+ // other, so with `--subject` the key and version inputs are not missing; the service
104
+ // names any input that still is.
105
+ const filled = (name: string) => existing && subject !== undefined && (name === 'expectedVersion' || /^(id|.+(Id|Key|Pk))$/.test(name))
106
+ const missing = (action.inputSchema?.properties?.input?.required ?? []).filter((name) => !(name in input) && !filled(name))
98
107
  if (missing.length > 0) {
99
108
  throw new UsageError(
100
109
  `"${apiName}" needs ${missing.join(', ')}`,
@@ -103,11 +112,16 @@ export const actionSubmit: Command = {
103
112
  }
104
113
 
105
114
  const invocation: Record<string, unknown> = { input }
106
- const existing = action.subject?.mode === 'existing' && action.subject.objectTypeId
107
- if (existing && subject === undefined) {
115
+ if (existing && subject !== undefined && version === undefined && !('expectedVersion' in input)) {
116
+ throw new UsageError(
117
+ `"${apiName}" edits a record, so it says the version the change was decided on`,
118
+ `frontera action submit ${apiName} --subject ${subject} --subject-version <version> (\`blueprint instance\` shows it as _meta.recordVersion)`,
119
+ )
120
+ }
121
+ if (existing && subject === undefined && !('expectedVersion' in input)) {
108
122
  throw new UsageError(
109
123
  `"${apiName}" acts on an existing object`,
110
- `name it: frontera action submit ${apiName} --subject <objectId> --subject-version <version>`,
124
+ `name it and its version: frontera action submit ${apiName} --subject <objectId> --subject-version <version>, or pass its key input and expectedVersion=<version>`,
111
125
  )
112
126
  }
113
127
  if (!existing && subject !== undefined) {
@@ -116,10 +130,10 @@ export const actionSubmit: Command = {
116
130
  `drop --subject and --subject-version: frontera action submit ${apiName} key=value…`,
117
131
  )
118
132
  }
119
- if (existing) {
133
+ if (existing && subject !== undefined) {
120
134
  // The object type is the Action's, not the caller's to choose.
121
135
  invocation.subjectRef = { objectTypeId: action.subject!.objectTypeId, objectId: subject }
122
- invocation.expectedSubjectVersion = version
136
+ if (version !== undefined) invocation.expectedSubjectVersion = version
123
137
  }
124
138
  const reason = flagString(ctx, 'reason')
125
139
  if (reason !== undefined) invocation.reason = reason
@@ -146,8 +160,9 @@ export const actionSubmit: Command = {
146
160
  } catch (err) {
147
161
  // Only where the outcome is unknown. A refusal the service answered
148
162
  // (400, 403, 404, 409) is replayed identically under the same key, so
149
- // naming the retry there would invite a loop; it is rethrown as is.
150
- if (!transient(err)) throw err
163
+ // naming the retry there would invite a loop; it is rethrown as is —
164
+ // a submission-criteria refusal with each criterion's message, not only its code.
165
+ if (!transient(err)) throw criteriaRefusal(err, apiName, ctx.cwd) ?? err
151
166
  const envelope = toEnvelope(err)
152
167
  throw new CliError(envelope.message, {
153
168
  code: err instanceof CliError ? envelope.code : 'FAILURE',
@@ -79,7 +79,7 @@ export async function agentDraftChanges(api: PlatformApi, id: string): Promise<A
79
79
  (k) => stableStringify(draftSections[k]) !== stableStringify(liveSnapshot[k]),
80
80
  )
81
81
  : []),
82
- ].sort()
82
+ ].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
83
83
 
84
84
  return { hasDraft: true, published, revision: draft.revision, changed }
85
85
  }
@@ -152,9 +152,9 @@ const create: Command = {
152
152
  data: agent,
153
153
  text: [
154
154
  `created ${slug}${agent.id ? ` ${agent.id}` : ''}`,
155
- // Said plainly because the next thing a caller does is run `agent list`,
156
- // see nothing, and conclude the create failed.
157
- ` it is in \`configuring\` and will NOT appear in \`frontera agent list\` until published`,
155
+ // Said plainly: a caller who runs `agent list` next sees it as a draft, not
156
+ // among the published ones, and should not conclude the create failed.
157
+ ` it is in \`configuring\` until published: \`frontera agent list\` shows it as \`draft only\``,
158
158
  ` next: frontera agent apply ${slug} -f <file> then frontera agent publish ${slug}`,
159
159
  ].join('\n'),
160
160
  }
@@ -171,7 +171,20 @@ const list: Command = {
171
171
  examples: ['frontera agent list', 'frontera agent list --json'],
172
172
  },
173
173
  async run(ctx) {
174
- const rows = (await api(ctx).agents()) as AgentRow[]
174
+ const client = api(ctx)
175
+ const published = (await client.agents()) as AgentRow[]
176
+ // An Agent never published (one created and only applied, as every Agent on a local runtime is)
177
+ // is not in that list; the workspace's full listing adds it, marked as a draft. A platform
178
+ // without that listing lists the published ones, as before.
179
+ const all = ((await client.allAgents().catch(() => [])) ?? []) as AgentRow[]
180
+ const known = new Set(published.map((a) => a.id))
181
+ const drafts = all.filter((a) => a.lifecycleStatus === 'configuring' && !known.has(a.id) && !String(a.agentId ?? '').startsWith('preview_'))
182
+ const rows = [...published, ...drafts]
183
+ // An archived Agent is in the published listing: said as archived, not as live. One with no published
184
+ // version is a draft, not published, even when it reads active: restore now brings a draft-only Agent
185
+ // back `configuring`, but one restored before that fix still reads active.
186
+ const state = (a: AgentRow) => (a.lifecycleStatus === 'archived' ? 'archived' : a.lifecycleStatus === 'configuring' || (a.currentVersionId === null && a.lifecycleStatus === 'active') ? 'draft only' : 'published')
187
+ const withState = drafts.length > 0 || rows.some((a) => state(a) !== 'published')
175
188
  return {
176
189
  data: rows,
177
190
  text:
@@ -179,8 +192,8 @@ const list: Command = {
179
192
  ? 'No agents in this workspace.'
180
193
  : // Either ID or SLUG works wherever an agent is named.
181
194
  table(
182
- ['id', 'slug', 'name'],
183
- rows.map((a) => [a.id ?? '?', a.agentId ?? '', a.name ?? '']),
195
+ withState ? ['id', 'slug', 'name', 'state'] : ['id', 'slug', 'name'],
196
+ rows.map((a) => [a.id ?? '?', a.agentId ?? '', a.name ?? '', ...(withState ? [state(a)] : [])]),
184
197
  ),
185
198
  }
186
199
  },
@@ -230,7 +243,16 @@ const get: Command = {
230
243
  const identity = (row ?? {}) as {
231
244
  id?: string
232
245
  agentId?: string
233
- currentVersion?: { versionNumber?: number }
246
+ currentVersionId?: string | null
247
+ currentVersion?: { versionNumber?: number } | null
248
+ draft?: unknown
249
+ }
250
+ // Never published: the composition is the empty published side, which reads as an Agent with nothing.
251
+ if (row && !identity.currentVersionId && !identity.currentVersion) {
252
+ throw new CliError(`${identity.agentId ?? t.ref} has no published version yet: only its draft exists`, {
253
+ code: 'NOT_FOUND',
254
+ hint: `read the draft: \`frontera agent get ${identity.agentId ?? t.ref} --draft\`${identity.draft ? '' : ' (`frontera agent list` shows its state)'}`,
255
+ })
234
256
  }
235
257
  return {
236
258
  data: snapshot,
@@ -388,6 +410,44 @@ const discard: Command = {
388
410
  },
389
411
  }
390
412
 
413
+ const archive: Command = {
414
+ meta: {
415
+ noun: 'agent',
416
+ verb: 'archive',
417
+ args: [{ name: 'agent', required: true, description: 'agent id or slug' }],
418
+ flags: {},
419
+ summary: 'Archive an agent: it leaves service with its versions and conversations kept (`agent restore` puts it back)',
420
+ examples: ['frontera agent archive assistant'],
421
+ },
422
+ async run(ctx) {
423
+ const t = await target(ctx, 'archive')
424
+ const row = (await t.api.agent(t.id).catch(() => null)) as AgentRow | null
425
+ if (row?.lifecycleStatus === 'archived') return { data: { archived: true, agent: t.ref, already: true }, text: `${t.ref} is already archived.` }
426
+ await t.api.archiveAgent(t.id)
427
+ return {
428
+ data: { archived: true, agent: t.ref },
429
+ text: [`Archived ${t.ref}: no chat, channel or workflow reaches it now; its versions and conversations are kept.`, ` its slug stays taken; \`frontera agent restore ${t.ref}\` puts it back`].join('\n'),
430
+ }
431
+ },
432
+ }
433
+
434
+ const restore: Command = {
435
+ meta: {
436
+ noun: 'agent',
437
+ verb: 'restore',
438
+ args: [{ name: 'agent', required: true, description: 'agent id or slug' }],
439
+ flags: {},
440
+ summary: 'Put an archived agent back into service, as it was',
441
+ examples: ['frontera agent restore assistant'],
442
+ },
443
+ async run(ctx) {
444
+ const t = await target(ctx, 'restore')
445
+ const result = (await t.api.restoreAgent(t.id)) as { restored?: boolean; lifecycleStatus?: string } | null
446
+ if (result && result.restored === false) return { data: { restored: false, agent: t.ref }, text: `${t.ref} isn't archived${result.lifecycleStatus ? ` (it is ${result.lifecycleStatus === 'configuring' ? 'a draft' : result.lifecycleStatus})` : ''}: nothing to restore.` }
447
+ return { data: { restored: true, agent: t.ref }, text: `Restored ${t.ref}.` }
448
+ },
449
+ }
450
+
391
451
  const versions: Command = {
392
452
  meta: {
393
453
  noun: 'agent',
@@ -555,6 +615,8 @@ export const agentCommands: Command[] = [
555
615
  publish,
556
616
  diff,
557
617
  discard,
618
+ archive,
619
+ restore,
558
620
  versions,
559
621
  revert,
560
622
  skillAttach,
@@ -8,6 +8,8 @@ export interface AgentRow {
8
8
  name?: string
9
9
  kind?: string
10
10
  lifecycleStatus?: string
11
+ /** The published version; null for an Agent never published. */
12
+ currentVersionId?: string | null
11
13
  }
12
14
 
13
15
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
@@ -0,0 +1,44 @@
1
+ import { AppsApi, type DeploymentCheck } from '../../api/apps-api'
2
+ import { UsageError } from '../../errors'
3
+ import { table } from '../../table'
4
+ import { projectApp, requireProjectFrom } from './shared'
5
+ import type { Command } from '../types'
6
+
7
+ /** The checks a deployment passes in the App's workspace, with which ones the workspace requires. */
8
+ export function renderChecks(deployment: string, checks: DeploymentCheck[]): string {
9
+ if (!checks.length) return `${deployment}: no checks recorded`
10
+ return table(
11
+ ['CHECK', 'STATUS', 'REQUIRED', 'DETAIL'],
12
+ checks.map((c) => [c.label, c.status, c.required ? 'yes' : '-', c.detail]),
13
+ [undefined, undefined, undefined, 72],
14
+ )
15
+ }
16
+
17
+ export const appChecks: Command = {
18
+ meta: {
19
+ noun: 'app',
20
+ verb: 'checks',
21
+ args: [
22
+ {
23
+ name: 'deployment',
24
+ required: true,
25
+ description: 'the deployment id or release label, from `frontera app deployments`',
26
+ producer: 'frontera app deployments — then `frontera app checks <deployment>`',
27
+ },
28
+ ],
29
+ flags: {},
30
+ summary: 'Show the build, render, contract and accessibility checks for a deployment',
31
+ examples: ['frontera app checks dpl_01JABCDEF', 'frontera app checks 1.4.0 --json'],
32
+ needsProject: true,
33
+ },
34
+
35
+ async run(ctx) {
36
+ const deployment = ctx.positional[0]
37
+ if (!deployment) throw new UsageError('say which deployment', 'frontera app checks <deployment id or label>')
38
+ const project = requireProjectFrom(ctx)
39
+ const client = new AppsApi(ctx.apiUrl, ctx.token)
40
+ const app = await projectApp(client, project)
41
+ const res = await client.checks(app.id, deployment)
42
+ return { data: res, text: renderChecks(res.deployment, res.checks) }
43
+ },
44
+ }