firedrill-run 2.2.5 → 3.0.0

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 (392) hide show
  1. package/README.md +178 -22
  2. package/bin/firedrill.mjs +29 -5
  3. package/dist/assembly.json +13 -0
  4. package/dist/cli/runtime/candidate_copy.py +395 -0
  5. package/dist/cli/runtime/source_snapshot.py +391 -0
  6. package/dist/cli/src/administration.js +295 -0
  7. package/dist/cli/src/agent-changes.js +496 -0
  8. package/dist/cli/src/assessment-requests.js +67 -0
  9. package/dist/cli/src/auth.js +465 -0
  10. package/dist/cli/src/billing-commerce-contracts.js +1 -0
  11. package/dist/cli/src/billing-usage.js +227 -0
  12. package/dist/cli/src/billing-wallet-contracts.js +1 -0
  13. package/dist/cli/src/billing.js +170 -0
  14. package/dist/cli/src/bootstrap-process-lock.js +136 -0
  15. package/dist/cli/src/browser-links.js +14 -0
  16. package/dist/cli/src/campaign-controls.js +122 -0
  17. package/dist/cli/src/campaign-diagnoses.js +118 -0
  18. package/dist/cli/src/campaign-reports.js +1215 -0
  19. package/dist/cli/src/campaign-world.js +156 -0
  20. package/dist/cli/src/candidate-copy.js +376 -0
  21. package/dist/cli/src/catalog-contracts.js +1 -0
  22. package/dist/cli/src/ci-access-contracts.js +1 -0
  23. package/dist/cli/src/ci-configuration-contracts.js +1 -0
  24. package/dist/cli/src/ci-coverage.js +64 -0
  25. package/dist/cli/src/ci-delivery-contracts.js +1 -0
  26. package/dist/cli/src/ci-result-contracts.js +1 -0
  27. package/dist/cli/src/ci-run-contracts.js +1 -0
  28. package/dist/cli/src/ci-setup-proposals.js +52 -0
  29. package/dist/cli/src/company-policies.js +229 -0
  30. package/dist/cli/src/completion.js +79 -0
  31. package/dist/cli/src/configuration-intents.js +277 -0
  32. package/dist/cli/src/configuration.js +369 -0
  33. package/dist/cli/src/continuous-simulation.js +277 -0
  34. package/dist/cli/src/coordinated-checkpoints.js +133 -0
  35. package/dist/cli/src/credential-mode.js +139 -0
  36. package/dist/cli/src/credentials.js +331 -0
  37. package/dist/cli/src/credit-format.js +8 -0
  38. package/dist/cli/src/dataset-contracts.js +1 -0
  39. package/dist/cli/src/dataset-export.js +154 -0
  40. package/dist/cli/src/dataset-reads.js +37 -0
  41. package/dist/cli/src/dependency-preparation.js +239 -0
  42. package/dist/cli/src/director-tool-setup.js +104 -0
  43. package/dist/cli/src/driver-review.js +225 -0
  44. package/dist/cli/src/evaluation-authoring.js +315 -0
  45. package/dist/cli/src/evidence-bundles.js +367 -0
  46. package/dist/cli/src/execution-proposals.js +36 -0
  47. package/dist/cli/src/http.js +302 -0
  48. package/dist/cli/src/local-container-boundary.js +38 -0
  49. package/dist/cli/src/local-container-policy.js +201 -0
  50. package/dist/cli/src/local-execution-setup.js +81 -0
  51. package/dist/cli/src/local-source-basis.js +73 -0
  52. package/dist/cli/src/local-source.js +66 -0
  53. package/dist/cli/src/local-value-store.js +91 -0
  54. package/dist/cli/src/local-values.js +163 -0
  55. package/dist/cli/src/login-organizations.js +70 -0
  56. package/dist/cli/src/main.js +1622 -0
  57. package/dist/cli/src/mcp-connection.js +109 -0
  58. package/dist/cli/src/native/bindings.js +137 -0
  59. package/dist/cli/src/native/canonical.js +370 -0
  60. package/dist/cli/src/native/cloud-resources.js +942 -0
  61. package/dist/cli/src/native/companion-channel.js +421 -0
  62. package/dist/cli/src/native/companion-ipc.js +473 -0
  63. package/dist/cli/src/native/contracts.js +218 -0
  64. package/dist/cli/src/native/control-plane.js +1354 -0
  65. package/dist/cli/src/native/credit-approval.js +15 -0
  66. package/dist/cli/src/native/dependency-proxy.js +1006 -0
  67. package/dist/cli/src/native/drill-cleanup.js +115 -0
  68. package/dist/cli/src/native/drill-runner.js +1562 -0
  69. package/dist/cli/src/native/drivers/action-trace.js +210 -0
  70. package/dist/cli/src/native/drivers/function-wrapper.js +152 -0
  71. package/dist/cli/src/native/drivers/http.js +442 -0
  72. package/dist/cli/src/native/drivers/index.js +47 -0
  73. package/dist/cli/src/native/drivers/jsonl-process.js +538 -0
  74. package/dist/cli/src/native/drivers/playwright.js +600 -0
  75. package/dist/cli/src/native/drivers/signed-event.js +244 -0
  76. package/dist/cli/src/native/drivers/types.js +578 -0
  77. package/dist/cli/src/native/evidence-filter.js +255 -0
  78. package/dist/cli/src/native/isolated-access.js +241 -0
  79. package/dist/cli/src/native/library-sharing.js +220 -0
  80. package/dist/cli/src/native/local-run.js +667 -0
  81. package/dist/cli/src/native/local-secrets.js +19 -0
  82. package/dist/cli/src/native/mcp-config.js +123 -0
  83. package/dist/cli/src/native/metadata-requests.js +120 -0
  84. package/dist/cli/src/native/observations.js +1159 -0
  85. package/dist/cli/src/native/owned-groups.js +46 -0
  86. package/dist/cli/src/native/plan.js +594 -0
  87. package/dist/cli/src/native/png.js +159 -0
  88. package/dist/cli/src/native/ports.js +43 -0
  89. package/dist/cli/src/native/readiness.js +139 -0
  90. package/dist/cli/src/native/reassessments.js +206 -0
  91. package/dist/cli/src/native/redaction.js +235 -0
  92. package/dist/cli/src/native/registration.js +155 -0
  93. package/dist/cli/src/native/roots.js +110 -0
  94. package/dist/cli/src/native/run-cleanup.js +199 -0
  95. package/dist/cli/src/native/scratch.js +117 -0
  96. package/dist/cli/src/native/screenshots.js +60 -0
  97. package/dist/cli/src/native/session.js +556 -0
  98. package/dist/cli/src/native/standalone-proxy.js +580 -0
  99. package/dist/cli/src/native/standalone-renewals.js +183 -0
  100. package/dist/cli/src/native/standalone-state.js +193 -0
  101. package/dist/cli/src/native/standalone-tools.js +187 -0
  102. package/dist/cli/src/native/supervisor.js +831 -0
  103. package/dist/cli/src/native/target-auth.js +161 -0
  104. package/dist/cli/src/native/target-state.js +330 -0
  105. package/dist/cli/src/native/tool-reuse.js +364 -0
  106. package/dist/cli/src/native/tool-setups.js +336 -0
  107. package/dist/cli/src/native/world-reuse.js +288 -0
  108. package/dist/cli/src/onboarding.js +23 -0
  109. package/dist/cli/src/priority-requests.js +70 -0
  110. package/dist/cli/src/priority.js +223 -0
  111. package/dist/cli/src/prompts.js +184 -0
  112. package/dist/cli/src/real-operations.js +221 -0
  113. package/dist/cli/src/recipe-intents.js +58 -0
  114. package/dist/cli/src/recorded-context-intents.js +59 -0
  115. package/dist/cli/src/recorded-context.js +248 -0
  116. package/dist/cli/src/recordings.js +69 -0
  117. package/dist/cli/src/registration-guide.js +39 -0
  118. package/dist/cli/src/report-exports.js +213 -0
  119. package/dist/cli/src/request-input.js +47 -0
  120. package/dist/cli/src/resource-lifecycle.js +141 -0
  121. package/dist/cli/src/revision-requests.js +55 -0
  122. package/dist/cli/src/runner-abandon.js +53 -0
  123. package/dist/cli/src/runner-campaign-controls.js +262 -0
  124. package/dist/cli/src/runner-claim-reads.js +36 -0
  125. package/dist/cli/src/runner-completion.js +65 -0
  126. package/dist/cli/src/runner-configuration.js +546 -0
  127. package/dist/cli/src/runner-container.js +794 -0
  128. package/dist/cli/src/runner-evidence-erasure.js +351 -0
  129. package/dist/cli/src/runner-evidence-privacy.js +37 -0
  130. package/dist/cli/src/runner-evidence-retention.js +161 -0
  131. package/dist/cli/src/runner-evidence.js +326 -0
  132. package/dist/cli/src/runner-execute.js +92 -0
  133. package/dist/cli/src/runner-execution-deadline.js +25 -0
  134. package/dist/cli/src/runner-grants.js +307 -0
  135. package/dist/cli/src/runner-identity.js +615 -0
  136. package/dist/cli/src/runner-inputs.js +308 -0
  137. package/dist/cli/src/runner-jobs.js +49 -0
  138. package/dist/cli/src/runner-model-transport.js +392 -0
  139. package/dist/cli/src/runner-ownership.js +332 -0
  140. package/dist/cli/src/runner-process-lock.js +175 -0
  141. package/dist/cli/src/runner-runtime.js +396 -0
  142. package/dist/cli/src/runner-session-channel.js +377 -0
  143. package/dist/cli/src/runner-trial-completion.js +34 -0
  144. package/dist/cli/src/runner-trial-journal.js +358 -0
  145. package/dist/cli/src/runner-trial-live.js +393 -0
  146. package/dist/cli/src/runner-trial-protocol.js +289 -0
  147. package/dist/cli/src/runner-trial-sync.js +110 -0
  148. package/dist/cli/src/runner-trial-transfer.js +107 -0
  149. package/dist/cli/src/runner-voice.js +40 -0
  150. package/dist/cli/src/runner-world-claim.js +21 -0
  151. package/dist/cli/src/runner-world-collector.js +159 -0
  152. package/dist/cli/src/runner-world-command-evidence.js +487 -0
  153. package/dist/cli/src/runner-world-commands.js +268 -0
  154. package/dist/cli/src/runner-world-completion.js +76 -0
  155. package/dist/cli/src/runner-world-erasure.js +95 -0
  156. package/dist/cli/src/runner-world-evidence-sync.js +91 -0
  157. package/dist/cli/src/runner-world-journal.js +510 -0
  158. package/dist/cli/src/runner-world-native-filters.js +134 -0
  159. package/dist/cli/src/runner-world-observations.js +50 -0
  160. package/dist/cli/src/runner-world-session-protocol.js +134 -0
  161. package/dist/cli/src/saved-records.js +40 -0
  162. package/dist/cli/src/scenario-authoring.js +212 -0
  163. package/dist/cli/src/scenario-discovery.js +216 -0
  164. package/dist/cli/src/scenario-paths.js +68 -0
  165. package/dist/cli/src/scenario-requests.js +4 -0
  166. package/dist/cli/src/service-receipts.js +105 -0
  167. package/dist/cli/src/simple/authoring.js +270 -0
  168. package/dist/cli/src/simple/credit-approval.js +16 -0
  169. package/dist/cli/src/simple/help.js +98 -0
  170. package/dist/cli/src/simple/init.js +61 -0
  171. package/dist/cli/src/simple/library-sharing.js +414 -0
  172. package/dist/cli/src/simple/metadata-requests.js +46 -0
  173. package/dist/cli/src/simple/native-report.js +841 -0
  174. package/dist/cli/src/simple/native-tools.js +780 -0
  175. package/dist/cli/src/simple/plan-apply.js +506 -0
  176. package/dist/cli/src/simple/reassessment.js +323 -0
  177. package/dist/cli/src/simple/register.js +123 -0
  178. package/dist/cli/src/simple/run.js +122 -0
  179. package/dist/cli/src/simple/status.js +49 -0
  180. package/dist/cli/src/simple/tool-renewal.js +308 -0
  181. package/dist/cli/src/simple/tool-reuse.js +526 -0
  182. package/dist/cli/src/simple/tool-start-catalog.js +100 -0
  183. package/dist/cli/src/simple/tool-state.js +426 -0
  184. package/dist/cli/src/simple/tools-author.js +222 -0
  185. package/dist/cli/src/simple/watch.js +28 -0
  186. package/dist/cli/src/simple/world-reuse.js +586 -0
  187. package/dist/cli/src/simulation-intents.js +406 -0
  188. package/dist/cli/src/simulation-run.js +1058 -0
  189. package/dist/cli/src/source-batch.js +322 -0
  190. package/dist/cli/src/source-connection.js +451 -0
  191. package/dist/cli/src/source-runtime.js +346 -0
  192. package/dist/cli/src/source-selection-intents.js +75 -0
  193. package/dist/cli/src/source-sets.js +214 -0
  194. package/dist/cli/src/target-models.js +109 -0
  195. package/dist/cli/src/tool-catalog.js +224 -0
  196. package/dist/cli/src/tool-command-contracts.js +1 -0
  197. package/dist/cli/src/tool-command-export.js +245 -0
  198. package/dist/cli/src/tool-connection-contracts.js +1 -0
  199. package/dist/cli/src/tool-connection-reads.js +23 -0
  200. package/dist/cli/src/tool-contributions.js +58 -0
  201. package/dist/cli/src/tool-data.js +116 -0
  202. package/dist/cli/src/tool-datasets.js +176 -0
  203. package/dist/cli/src/tool-group-contracts.js +1 -0
  204. package/dist/cli/src/tool-group-reads.js +26 -0
  205. package/dist/cli/src/tool-group-state.js +147 -0
  206. package/dist/cli/src/tool-groups.js +311 -0
  207. package/dist/cli/src/tool-instances.js +186 -0
  208. package/dist/cli/src/tool-lifecycle-contracts.js +1 -0
  209. package/dist/cli/src/tool-lifecycle-reads.js +30 -0
  210. package/dist/cli/src/tool-lifecycle.js +323 -0
  211. package/dist/cli/src/tool-package-contracts.js +1 -0
  212. package/dist/cli/src/tool-package-reads.js +41 -0
  213. package/dist/cli/src/tool-records.js +53 -0
  214. package/dist/cli/src/tool-runtime.js +96 -0
  215. package/dist/cli/src/tool-state-contracts.js +1 -0
  216. package/dist/cli/src/tool-state-reads.js +31 -0
  217. package/dist/cli/src/tools.js +306 -0
  218. package/dist/cli/src/trial-actions.js +93 -0
  219. package/dist/cli/src/trial-checkpoints.js +133 -0
  220. package/dist/cli/src/trial-checks.js +70 -0
  221. package/dist/cli/src/trial-state.js +197 -0
  222. package/dist/cli/src/vault.js +266 -0
  223. package/dist/cli/src/workspace-registration.js +383 -0
  224. package/dist/cli/src/workspace-team-contracts.js +1 -0
  225. package/dist/cli/src/world-access.js +231 -0
  226. package/dist/cli/src/world-agent.js +4 -0
  227. package/dist/cli/src/world-checkpoint-scopes.js +15 -0
  228. package/dist/cli/src/world-contracts.js +6 -0
  229. package/dist/cli/src/world-creation.js +201 -0
  230. package/dist/cli/src/world-datasets.js +207 -0
  231. package/dist/cli/src/world-inspection.js +257 -0
  232. package/dist/cli/src/world-recipes.js +103 -0
  233. package/dist/cli/src/world-requests.js +241 -0
  234. package/dist/cli/src/world-session-checkpoints.js +196 -0
  235. package/dist/cli/src/world-session-lifecycle.js +207 -0
  236. package/dist/cli/src/world-session-requests.js +215 -0
  237. package/dist/cli/src/world-session-state.js +145 -0
  238. package/dist/cli/src/world-session-voice.js +66 -0
  239. package/dist/cli/src/world-sessions.js +288 -0
  240. package/dist/cli/src/world-stream.js +99 -0
  241. package/dist/cli/src/world-time-report.js +58 -0
  242. package/dist/contracts/agent-change-contracts.js +75 -0
  243. package/dist/contracts/agent-execution-contracts.js +38 -0
  244. package/dist/contracts/application-state-contracts.js +26 -0
  245. package/dist/contracts/assessment-aggregate-contracts.js +15 -0
  246. package/dist/contracts/assessment-contracts.js +74 -0
  247. package/dist/contracts/billing-commerce-contracts.js +107 -0
  248. package/dist/contracts/billing-wallet-contracts.js +43 -0
  249. package/dist/contracts/campaign-lifecycle.js +1 -0
  250. package/dist/contracts/campaign-world-contracts.js +80 -0
  251. package/dist/contracts/catalog-contracts.js +73 -0
  252. package/dist/contracts/ci-access-contracts.js +68 -0
  253. package/dist/contracts/ci-configuration-contracts.js +88 -0
  254. package/dist/contracts/ci-coverage-contracts.js +65 -0
  255. package/dist/contracts/ci-coverage-labels.js +25 -0
  256. package/dist/contracts/ci-delivery-contracts.js +61 -0
  257. package/dist/contracts/ci-result-contracts.js +103 -0
  258. package/dist/contracts/ci-run-contracts.js +51 -0
  259. package/dist/contracts/ci-setup-contracts.js +88 -0
  260. package/dist/contracts/clip-contracts.js +32 -0
  261. package/dist/contracts/comparison-contracts.js +53 -0
  262. package/dist/contracts/composite-contracts.js +34 -0
  263. package/dist/contracts/context-contracts.js +76 -0
  264. package/dist/contracts/continuation-report-contracts.js +1 -0
  265. package/dist/contracts/continuity-requirements.js +8 -0
  266. package/dist/contracts/coordinated-checkpoint-contracts.js +34 -0
  267. package/dist/contracts/dataset-contracts.js +88 -0
  268. package/dist/contracts/dependency-decision-contracts.js +36 -0
  269. package/dist/contracts/dependency-protocol-contracts.js +43 -0
  270. package/dist/contracts/director-execution.js +51 -0
  271. package/dist/contracts/driver-review-contracts.js +99 -0
  272. package/dist/contracts/evidence-contracts.js +65 -0
  273. package/dist/contracts/evidence-coverage-contracts.js +10 -0
  274. package/dist/contracts/export-contracts.js +44 -0
  275. package/dist/contracts/github-directory-contracts.js +96 -0
  276. package/dist/contracts/insight-contracts.js +11 -0
  277. package/dist/contracts/inspection-contracts.js +1 -0
  278. package/dist/contracts/local-claim-contracts.js +39 -0
  279. package/dist/contracts/local-configuration-contracts.js +7 -0
  280. package/dist/contracts/local-evidence-contracts.js +32 -0
  281. package/dist/contracts/local-grant-contracts.js +83 -0
  282. package/dist/contracts/local-input-contracts.js +19 -0
  283. package/dist/contracts/local-runner-contracts.js +42 -0
  284. package/dist/contracts/local-trial-live-contracts.js +16 -0
  285. package/dist/contracts/local-value-source-contracts.js +92 -0
  286. package/dist/contracts/local-world-native-contracts.js +8 -0
  287. package/dist/contracts/mcp-contracts.js +42 -0
  288. package/dist/contracts/media-contracts.js +28 -0
  289. package/dist/contracts/native-check-contracts.js +31 -0
  290. package/dist/contracts/native-identity-contracts.js +52 -0
  291. package/dist/contracts/onboarding-contracts.js +32 -0
  292. package/dist/contracts/performance-contracts.js +85 -0
  293. package/dist/contracts/planning-contracts.js +81 -0
  294. package/dist/contracts/real-operation-contracts.js +30 -0
  295. package/dist/contracts/recommendation-contracts.js +54 -0
  296. package/dist/contracts/report-contracts.js +1 -0
  297. package/dist/contracts/resource-lifecycle-contracts.js +57 -0
  298. package/dist/contracts/revision-contracts.js +12 -0
  299. package/dist/contracts/revision-request-contracts.js +39 -0
  300. package/dist/contracts/saved-record-contracts.js +76 -0
  301. package/dist/contracts/scenario-draft-comparison.js +91 -0
  302. package/dist/contracts/scenario-world-contracts.js +14 -0
  303. package/dist/contracts/service-contracts.js +1 -0
  304. package/dist/contracts/session-command-contracts.js +56 -0
  305. package/dist/contracts/session-contracts.js +65 -0
  306. package/dist/contracts/session-state-contracts.js +67 -0
  307. package/dist/contracts/session-voice-contracts.js +45 -0
  308. package/dist/contracts/source-set-contracts.js +26 -0
  309. package/dist/contracts/source-upload-contracts.js +22 -0
  310. package/dist/contracts/tool-command-contracts.js +57 -0
  311. package/dist/contracts/tool-connection-contracts.js +87 -0
  312. package/dist/contracts/tool-contribution-contracts.js +35 -0
  313. package/dist/contracts/tool-group-contracts.js +53 -0
  314. package/dist/contracts/tool-group-state-contracts.js +63 -0
  315. package/dist/contracts/tool-lifecycle-contracts.js +94 -0
  316. package/dist/contracts/tool-package-contracts.js +91 -0
  317. package/dist/contracts/tool-protocol-contracts.js +71 -0
  318. package/dist/contracts/tool-runtime-billing-contracts.js +28 -0
  319. package/dist/contracts/tool-setup-proposal.js +59 -0
  320. package/dist/contracts/tool-state-contracts.js +79 -0
  321. package/dist/contracts/trial-state-contracts.js +30 -0
  322. package/dist/contracts/typescript/agent-change.js +1 -0
  323. package/dist/contracts/typescript/assessment-aggregate.js +1 -0
  324. package/dist/contracts/typescript/assessment.js +1 -0
  325. package/dist/contracts/typescript/billing-commerce.js +1 -0
  326. package/dist/contracts/typescript/campaigns.js +1 -0
  327. package/dist/contracts/typescript/catalog.js +1 -0
  328. package/dist/contracts/typescript/ci-access.js +1 -0
  329. package/dist/contracts/typescript/ci-configuration.js +1 -0
  330. package/dist/contracts/typescript/ci-delivery.js +1 -0
  331. package/dist/contracts/typescript/ci-result.js +1 -0
  332. package/dist/contracts/typescript/ci-runs.js +1 -0
  333. package/dist/contracts/typescript/clips.js +1 -0
  334. package/dist/contracts/typescript/comparison.js +1 -0
  335. package/dist/contracts/typescript/composite.js +1 -0
  336. package/dist/contracts/typescript/connections.js +1 -0
  337. package/dist/contracts/typescript/context.js +1 -0
  338. package/dist/contracts/typescript/datasets.js +1 -0
  339. package/dist/contracts/typescript/evidence-coverage.js +1 -0
  340. package/dist/contracts/typescript/export.js +1 -0
  341. package/dist/contracts/typescript/github-directory.js +1 -0
  342. package/dist/contracts/typescript/insight.js +1 -0
  343. package/dist/contracts/typescript/inspection.js +1 -0
  344. package/dist/contracts/typescript/intake.js +1 -0
  345. package/dist/contracts/typescript/local-claim.js +1 -0
  346. package/dist/contracts/typescript/local-configuration.js +1 -0
  347. package/dist/contracts/typescript/local-evidence.js +1 -0
  348. package/dist/contracts/typescript/local-grant.js +1 -0
  349. package/dist/contracts/typescript/local-input.js +1 -0
  350. package/dist/contracts/typescript/local-runner.js +1 -0
  351. package/dist/contracts/typescript/local-trial-live.js +1 -0
  352. package/dist/contracts/typescript/local-value-source.js +1 -0
  353. package/dist/contracts/typescript/local-world-native.js +1 -0
  354. package/dist/contracts/typescript/mcp.js +1 -0
  355. package/dist/contracts/typescript/media.js +1 -0
  356. package/dist/contracts/typescript/native-check.js +1 -0
  357. package/dist/contracts/typescript/native-identity.js +1 -0
  358. package/dist/contracts/typescript/onboarding.js +1 -0
  359. package/dist/contracts/typescript/performance.js +1 -0
  360. package/dist/contracts/typescript/planning.js +1 -0
  361. package/dist/contracts/typescript/preparation.js +1 -0
  362. package/dist/contracts/typescript/real-operation.js +1 -0
  363. package/dist/contracts/typescript/recommendation.js +1 -0
  364. package/dist/contracts/typescript/reports.js +1 -0
  365. package/dist/contracts/typescript/resource-lifecycle.js +1 -0
  366. package/dist/contracts/typescript/saved-record.js +1 -0
  367. package/dist/contracts/typescript/scenarios.js +1 -0
  368. package/dist/contracts/typescript/services.js +1 -0
  369. package/dist/contracts/typescript/session-command.js +1 -0
  370. package/dist/contracts/typescript/session-state.js +1 -0
  371. package/dist/contracts/typescript/session-voice.js +1 -0
  372. package/dist/contracts/typescript/session.js +1 -0
  373. package/dist/contracts/typescript/tool-command.js +1 -0
  374. package/dist/contracts/typescript/tool-connection.js +1 -0
  375. package/dist/contracts/typescript/tool-contribution.js +1 -0
  376. package/dist/contracts/typescript/tool-groups.js +1 -0
  377. package/dist/contracts/typescript/tool-lifecycle.js +1 -0
  378. package/dist/contracts/typescript/tool-package.js +1 -0
  379. package/dist/contracts/typescript/tool-state.js +1 -0
  380. package/dist/contracts/typescript/trial-state.js +1 -0
  381. package/dist/contracts/typescript/vault.js +1 -0
  382. package/dist/contracts/typescript/workspace-team.js +1 -0
  383. package/dist/contracts/typescript/world-stream.js +1 -0
  384. package/dist/contracts/workspace-team-contracts.js +55 -0
  385. package/dist/contracts/world-command.js +65 -0
  386. package/dist/contracts/world-lifetime-contracts.js +74 -0
  387. package/dist/contracts/world-recipe-contracts.js +27 -0
  388. package/dist/contracts/world-stream-contracts.js +27 -0
  389. package/dist/package.json +1 -0
  390. package/package.json +16 -16
  391. package/LICENSE.md +0 -23
  392. package/PACKAGING.json +0 -50
@@ -0,0 +1,1058 @@
1
+ import { workspaceLink } from './browser-links.js';
2
+ import { createHash } from 'node:crypto';
3
+ import { createInterface } from 'node:readline/promises';
4
+ import { setTimeout as sleep } from 'node:timers/promises';
5
+ import { api } from './auth.js';
6
+ import { CliError } from './http.js';
7
+ import { selectMany, selectOne, terminalText } from './prompts.js';
8
+ import { executionPermission, localPermissionPage, savedLocalPermission } from './runner-grants.js';
9
+ import { withRunnerPresence } from './runner-identity.js';
10
+ import { retainLocalValidation } from './runner-evidence.js';
11
+ import { retainLocalTrial } from './runner-trial-journal.js';
12
+ import { continueIntent, createIntent, inspectIntent, intentPage, optionalIntent, withdrawIntent } from './simulation-intents.js';
13
+ import { preparation } from './world-agent.js';
14
+ import { selectedWorld } from './world-access.js';
15
+ import { identifier } from './world-contracts.js';
16
+ import { isNativeSetupReceipt, planMatchesRequest, planningDriverMatches, planningDefaultsMatch } from '../../contracts/planning-contracts.js';
17
+ import { executionScheduleText } from '../../contracts/agent-execution-contracts.js';
18
+ import { datasetReads } from './dataset-reads.js';
19
+ import { localAssignmentsPage, readLocalClaim } from './runner-claim-reads.js';
20
+ import { retainedLocalClaim } from './runner-ownership.js';
21
+ import { executionProposal, executionProposalText, executableProposal, proposalBasis } from './execution-proposals.js';
22
+ import { executionPlanMatches } from '../../contracts/director-execution.js';
23
+ import { continuityRequirementsMatch } from '../../contracts/continuity-requirements.js';
24
+ import { applicationCheckpointMatches } from '../../contracts/application-state-contracts.js';
25
+ import { resourcePageMatches } from '../../contracts/world-lifetime-contracts.js';
26
+ import { coordinatedCheckpointMatches, coordinatedCheckpointDetailMatches } from '../../contracts/coordinated-checkpoint-contracts.js';
27
+ import { creditAmount } from './credit-format.js';
28
+ import { nativeMetadataCanonical } from '../../contracts/local-value-source-contracts.js';
29
+ import { localSetupRequired, setupLocalExecution } from './local-execution-setup.js';
30
+ import { reviewDriverInput } from './driver-review.js';
31
+ const terminal = (state) => ['completed', 'failed', 'canceled'].includes(state);
32
+ const interactive = (options) => !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
33
+ const auth = (context, signal) => ({ actor: context.owner, workspace: context.workspace, signal });
34
+ async function readSetup(context, signal) {
35
+ const value = await api(`${context.prefix}/simulation-plan`, auth(context, signal));
36
+ if (!planningDriverMatches(value))
37
+ throw new CliError('agent_execution_identity_changed', 'The prepared input adapter could not be confirmed. Read simulation setup again.');
38
+ if (!planningDefaultsMatch(value))
39
+ throw new CliError('simulation_plan_unconfirmed', 'Simulation settings could not be confirmed. Read simulation setup again.');
40
+ return value;
41
+ }
42
+ const reportLink = (context, id) => workspaceLink(context.workspace, `/campaigns/${identifier(id)}`);
43
+ async function contextFor(options) {
44
+ return selectedWorld({ workspace: options.workspace, world: options.world, expectedOwner: options.expectedOwner, interactive: interactive(options),
45
+ openBrowser: options.openBrowser, signal: options.signal });
46
+ }
47
+ async function confirmation(options, label, detail) {
48
+ if (options.run)
49
+ return;
50
+ if (!interactive(options))
51
+ throw new CliError('simulation_confirmation_required', 'Use --run to approve the reviewed operation in a non-interactive command.', 2);
52
+ if (!await selectOne(label, [{ value: true, label: 'Continue', detail }, { value: false, label: 'Cancel' }], options.signal)) {
53
+ throw new CliError('canceled', 'No new execution was admitted.', 130);
54
+ }
55
+ }
56
+ export async function startingData(context, setup, options) {
57
+ if (setup.agent_comparison) {
58
+ const retained = setup.agent_comparison.starting_data;
59
+ if (options.dataset !== undefined && identifier(options.dataset) !== retained?.dataset_id) {
60
+ throw new CliError('agent_comparison_starting_data_changed', 'Keep the original starting data for both sides of this comparison.', 2);
61
+ }
62
+ if (retained && setup.real_connections_count !== 0)
63
+ throw new CliError('agent_comparison_starting_data_changed', 'This comparison retains synthetic starting data. Review simulated connections instead of using real accounts.', 2);
64
+ return retained ? { id: identifier(retained.dataset_id), edit_count: retained.edit_count } : null;
65
+ }
66
+ if (setup.real_connections_count !== 0) {
67
+ if (options.dataset !== undefined)
68
+ throw new CliError('world_dataset_unavailable', 'Saved starting data requires simulated connections and no frozen comparison.', 2);
69
+ return null;
70
+ }
71
+ if (options.dataset !== undefined) {
72
+ const id = identifier(options.dataset);
73
+ const row = await datasetReads(context.prefix, context, options.signal).detail(id);
74
+ if (row.kind !== 'world' || !row.can_start_session)
75
+ throw new CliError('world_dataset_unavailable', 'Choose an available saved version for this prepared world.', 2);
76
+ return row;
77
+ }
78
+ if (!interactive(options))
79
+ return null;
80
+ let page = 1;
81
+ while (true) {
82
+ const result = await datasetReads(context.prefix, context, options.signal).page({ startable: true, page });
83
+ if (page === 1 && result.count === 0)
84
+ return null;
85
+ const choices = [{ value: 'prepared', label: 'Prepared data', detail: 'Use the world’s original starting data.' },
86
+ ...result.results.filter(row => row.kind === 'world' && row.can_start_session).map(row => ({ value: identifier(row.id), label: row.name,
87
+ detail: `Version ${row.version} · ${row.edit_count} saved edits` }))];
88
+ if (page > 1)
89
+ choices.push({ value: 'previous', label: 'Previous page', detail: '' });
90
+ if (result.next)
91
+ choices.push({ value: 'next', label: 'Next page', detail: '' });
92
+ const answer = await selectOne('Starting data', choices, options.signal);
93
+ if (answer === 'prepared')
94
+ return null;
95
+ if (answer === 'previous') {
96
+ page--;
97
+ continue;
98
+ }
99
+ if (answer === 'next') {
100
+ page++;
101
+ continue;
102
+ }
103
+ const row = result.results.find(one => one.id === answer && one.kind === 'world' && one.can_start_session);
104
+ if (!row)
105
+ throw new CliError('world_dataset_selection_changed', 'The starting-data selection changed. No simulation was started.');
106
+ return row;
107
+ }
108
+ }
109
+ async function numberOption(value, fallback, maximum, label, options) {
110
+ if (!Number.isSafeInteger(maximum) || maximum < 1 || !Number.isSafeInteger(fallback) || fallback < 1)
111
+ throw new CliError('simulation_capacity_unavailable', 'Simulation planning capacity is unavailable.');
112
+ let answer = value;
113
+ if (answer === undefined && interactive(options)) {
114
+ const prompt = createInterface({ input: process.stdin, output: process.stderr, terminal: false });
115
+ try {
116
+ answer = (await prompt.question(`${label} [${fallback}]: `, { signal: options.signal })).trim();
117
+ }
118
+ finally {
119
+ prompt.close();
120
+ }
121
+ }
122
+ if (answer === undefined || answer === '')
123
+ answer = String(fallback);
124
+ if (!/^[1-9][0-9]*$/.test(answer) || !Number.isSafeInteger(Number(answer)) || Number(answer) > maximum) {
125
+ throw new CliError('simulation_capacity_required', `Use ${label.toLowerCase()} between 1 and ${maximum}.`, 2);
126
+ }
127
+ return Number(answer);
128
+ }
129
+ async function applicationStartingState(context, setup, options, dataset, selected, proposal) {
130
+ const requested = options.applicationCheckpoint !== undefined || options.coordinatedCheckpoint !== undefined;
131
+ const hasApprovals = options.acknowledgeFreshWorldState || options.shareApplicationCheckpointWithDevice
132
+ || options.acknowledgeSupportedCheckpointScope || options.shareCoordinatedCheckpointWithDevice;
133
+ if (setup.real_connections_count !== 0 || setup.agent_comparison || dataset || proposal) {
134
+ if (requested || hasApprovals)
135
+ throw new CliError('application_continuation_combination_unavailable', 'Choose application continuation in an ordinary simulation plan with simulated connections and no separate dataset or comparison.', 2);
136
+ return null;
137
+ }
138
+ let kind = options.coordinatedCheckpoint !== undefined ? 'coordinated' : 'application';
139
+ let checkpoint = null;
140
+ if (!requested && interactive(options)) {
141
+ const choice = await selectOne('Starting state', [
142
+ { value: 'fresh', label: 'Prepared world and new agent process' },
143
+ { value: 'coordinated', label: 'Saved world and agent state', detail: 'Only supported stores and source-defined application state.' },
144
+ { value: 'application', label: 'Saved application state, fresh synthetic services' }
145
+ ], options.signal);
146
+ if (choice === 'fresh') {
147
+ if (hasApprovals)
148
+ throw new CliError('application_checkpoint_required', 'Saved-state approvals require a selected checkpoint.', 2);
149
+ return null;
150
+ }
151
+ kind = choice;
152
+ }
153
+ const route = kind === 'coordinated' ? 'coordinated-checkpoints' : 'application-checkpoints';
154
+ if (requested) {
155
+ const id = identifier((options.coordinatedCheckpoint ?? options.applicationCheckpoint));
156
+ checkpoint = await api(`${context.prefix}/${route}/${id}`, auth(context, options.signal));
157
+ if (!(kind === 'coordinated' ? coordinatedCheckpointDetailMatches(checkpoint, context.world, { id })
158
+ : applicationCheckpointMatches(checkpoint, context.world, { id })) || !checkpoint.available)
159
+ throw new CliError('application_checkpoint_unavailable', 'Choose a retained application checkpoint from this world.', 2);
160
+ }
161
+ else if (interactive(options)) {
162
+ let page = 1;
163
+ while (true) {
164
+ const value = await api(`${context.prefix}/${route}?state=retained&page=${page}`, auth(context, options.signal));
165
+ if (!resourcePageMatches(value, page, row => (kind === 'coordinated' ? coordinatedCheckpointMatches(row, context.world)
166
+ : applicationCheckpointMatches(row, context.world)) && row.state === 'retained'))
167
+ throw new CliError('application_checkpoint_unconfirmed', 'The saved application state list could not be confirmed.');
168
+ if (!value.count && page === 1)
169
+ break;
170
+ const choices = [{ value: 'fresh', label: 'No application checkpoint', detail: 'Start from the prepared source and world.' },
171
+ ...value.results.filter(row => row.available).map(row => ({ value: row.id, label: row.name, detail: 'components' in row
172
+ ? row.components.map(one => one.name).join(', ') : `${row.application_component_count} application components and supported synthetic service data` }))];
173
+ if (page > 1)
174
+ choices.push({ value: 'previous', label: 'Previous page', detail: '' });
175
+ if (value.next)
176
+ choices.push({ value: 'next', label: 'Next page', detail: '' });
177
+ const answer = await selectOne('Saved application state (optional)', choices, options.signal);
178
+ if (answer === 'fresh')
179
+ break;
180
+ if (answer === 'previous') {
181
+ page--;
182
+ continue;
183
+ }
184
+ if (answer === 'next') {
185
+ page++;
186
+ continue;
187
+ }
188
+ checkpoint = value.results.find(row => row.id === answer && row.available) ?? null;
189
+ if (!checkpoint)
190
+ throw new CliError('application_checkpoint_unconfirmed', 'Choose the saved application state again.');
191
+ break;
192
+ }
193
+ }
194
+ if (!checkpoint) {
195
+ if (hasApprovals)
196
+ throw new CliError('application_checkpoint_required', 'Application-state approvals require a selected checkpoint.', 2);
197
+ return null;
198
+ }
199
+ if (kind === 'coordinated' ? options.acknowledgeFreshWorldState || options.shareApplicationCheckpointWithDevice
200
+ : options.acknowledgeSupportedCheckpointScope || options.shareCoordinatedCheckpointWithDevice)
201
+ throw new CliError('application_checkpoint_consent_invalid', 'Use only the selected checkpoint type’s scope and device-download approvals.', 2);
202
+ let acknowledged = (kind === 'coordinated' ? options.acknowledgeSupportedCheckpointScope : options.acknowledgeFreshWorldState) === true;
203
+ if (!acknowledged && interactive(options))
204
+ acknowledged = await selectOne(kind === 'coordinated' ? 'Supported saved world and agent state' : 'Application state with fresh synthetic services', [
205
+ { value: false, label: 'Cancel' }, { value: true, label: 'Continue in a new process',
206
+ detail: kind === 'coordinated' ? 'Each drill starts from supported captured application and synthetic service data. External accounts, live processes and elapsed wall time are not restored.'
207
+ : 'Each drill starts with this source-supported application state. Synthetic services start from prepared data; the previous world and process memory are not restored.' }
208
+ ], options.signal);
209
+ if (!acknowledged)
210
+ throw new CliError('application_continuation_acknowledgement_required', `Review saved-state coverage or use ${kind === 'coordinated' ? '--acknowledge-supported-checkpoint-scope' : '--acknowledge-fresh-world-state'} in automation.`, 2);
211
+ let share = (kind === 'coordinated' ? options.shareCoordinatedCheckpointWithDevice : options.shareApplicationCheckpointWithDevice) === true;
212
+ if (selected.location === 'local') {
213
+ if (!share && interactive(options))
214
+ share = await selectOne('Download saved application state to this device?', [
215
+ { value: false, label: 'Cancel' }, { value: true, label: 'Download for these drills', detail: 'Private runner cache is outside your project. Device copies and backups cannot be erased remotely.' }
216
+ ], options.signal);
217
+ if (!share)
218
+ throw new CliError('local_application_checkpoint_consent_required', `Approve the download or use ${kind === 'coordinated' ? '--share-coordinated-checkpoint-with-device' : '--share-application-checkpoint-with-device'}.`, 2);
219
+ }
220
+ else if (share)
221
+ throw new CliError('local_application_checkpoint_consent_invalid', 'A device download is only available with local execution.', 2);
222
+ return { kind, checkpoint, acknowledged, share };
223
+ }
224
+ const LEGACY_LAUNCH_RETIRED = 'Starting work from the older world commands is retired in this release, managed or on this device; retained history, reports, cancellation, cleanup and original-request recovery stay available. Use firedrill plan, apply and run for new simulations. Nothing was sent.';
225
+ /**
226
+ * Where an original launch from the older world commands would run. This
227
+ * release starts none of them, managed or on this device (FIR-504: root's
228
+ * `managed_target_launch` and `legacy_local_execution` gates are off), so every
229
+ * caller is refused here, before any permission, grant, container or cloud
230
+ * request. `simulation resume` watches without calling this.
231
+ */
232
+ export async function execution(context, purpose, options, output) {
233
+ throw new CliError('legacy_launch_retired', LEGACY_LAUNCH_RETIRED, 2);
234
+ }
235
+ async function scenarios(context, setup, options) {
236
+ if (options.all && options.scenarios?.length)
237
+ throw new CliError('scenario_selection_invalid', 'Choose --all-scenarios or explicit --scenario IDs.', 2);
238
+ if (options.scenarios?.length)
239
+ return { mode: 'ids', query: '', ids: [...new Set(options.scenarios.map(identifier))].sort(), excluded: [] };
240
+ let all = options.all;
241
+ if (!all) {
242
+ if (!interactive(options))
243
+ throw new CliError('scenario_selection_required', 'Use --all-scenarios or one or more --scenario <id> options.', 2);
244
+ all = await selectOne('Scenarios', [{ value: true, label: 'All current scenarios' }, { value: false, label: 'Choose scenarios' }], options.signal);
245
+ }
246
+ if (all)
247
+ return { mode: 'all_matching', query: '', ids: [], excluded: [] };
248
+ const selected = new Set();
249
+ let page = 1;
250
+ while (true) {
251
+ const result = await api(`${context.prefix}/scenarios?page=${page}&selection_revision=${encodeURIComponent(setup.selection_revision)}`, auth(context, options.signal));
252
+ if (result.selection_revision !== setup.selection_revision)
253
+ throw new CliError('scenario_selection_changed', 'The scenario list changed. Review the plan again.');
254
+ const checked = await selectMany('Scenarios', result.results.map(row => ({ value: identifier(row.id), label: row.name })), options.signal, [...selected]);
255
+ for (const row of result.results)
256
+ selected.delete(row.id);
257
+ for (const id of checked)
258
+ selected.add(id);
259
+ const action = await selectOne(`${selected.size} selected`, [{ value: 'done', label: 'Continue' },
260
+ ...(result.next ? [{ value: 'next', label: 'Next page' }] : []), ...(page > 1 ? [{ value: 'previous', label: 'Previous page' }] : [])], options.signal);
261
+ if (action === 'next') {
262
+ page++;
263
+ continue;
264
+ }
265
+ if (action === 'previous') {
266
+ page--;
267
+ continue;
268
+ }
269
+ if (!selected.size)
270
+ throw new CliError('scenario_selection_required', 'Select at least one scenario.', 2);
271
+ return { mode: 'ids', query: '', ids: [...selected].sort(), excluded: [] };
272
+ }
273
+ }
274
+ async function validateWorld(context, selected, options, output, proposal) {
275
+ const world = await preparation(context, options.signal);
276
+ if (proposal)
277
+ proposalBasis(await executionProposal(context, proposal.id, options.signal), world);
278
+ if (!Number.isSafeInteger(world.real_connections_count) || world.real_connections_count < 0) {
279
+ throw new CliError('startup_connections_unavailable', 'Reopen this world to review its current connection choices. No validation was queued.');
280
+ }
281
+ const real = world.real_connections_count > 0;
282
+ if (real) {
283
+ let effects = options.acknowledgeExternalEffects === true, startup = options.approveRealStartup === true;
284
+ if (interactive(options)) {
285
+ if (!effects)
286
+ effects = await selectOne('Real-account access', [
287
+ { value: false, label: 'Cancel' }, { value: true, label: 'Allow the reviewed real-account operations',
288
+ detail: 'External reads and changes may occur. Stopping or resetting the world cannot undo them.' }
289
+ ], options.signal);
290
+ if (effects && !startup)
291
+ startup = await selectOne('Calls during startup', [
292
+ { value: false, label: 'Cancel' }, { value: true, label: 'Allow calls while the original agent starts',
293
+ detail: 'Start one instance. Do not run scenario setup, operation probes or reset checks.' }
294
+ ], options.signal);
295
+ }
296
+ if (!effects || !startup)
297
+ throw new CliError('real_startup_consent_required', 'Real-account startup needs --acknowledge-external-effects and --approve-real-startup, or explicit interactive approval. No validation was queued.', 2);
298
+ }
299
+ else if (options.approveRealStartup || options.acknowledgeExternalEffects) {
300
+ throw new CliError('real_startup_consent_invalid', 'This world has no real connection selected. Omit real-account startup approvals.', 2);
301
+ }
302
+ await confirmation(options, 'Validate startup?', 'Start the original agent and check this world’s supported connections. Target-provider model use is billed separately.');
303
+ if (selected.location === 'local')
304
+ await executionPermission(context, selected.request, selected.device, 'validation', options.signal);
305
+ const body = { configuration_revision: world.basis.config_revision,
306
+ ...(real ? { validation_scope: 'target_startup_only', acknowledge_external_effects: true, acknowledge_startup_calls: true,
307
+ world_revision: world.basis.world_revision, source_id: world.basis.source_id, source_revision: world.agent.revision, startup_actor_id: context.owner } : {}),
308
+ execution_location: selected.location, ...(selected.location === 'local' ? { local_grant_id: selected.grant.id } : {}) };
309
+ const original = proposal ? await optionalIntent(context, proposal.validation_request_key) : null;
310
+ if (original && (original.kind !== 'validation' || original.body.configuration_revision !== body.configuration_revision
311
+ || original.body.execution_location !== body.execution_location || original.body.local_grant_id !== body.local_grant_id
312
+ || original.body.validation_scope !== body.validation_scope))
313
+ throw new CliError('execution_proposal_changed', 'The original validation request has different inputs. It was not replaced.');
314
+ if (proposal)
315
+ proposalBasis(await executionProposal(context, proposal.id, options.signal), await preparation(context, options.signal));
316
+ const saved = await createIntent(context, 'validation', original?.body ?? body, options.signal, output, proposal?.validation_request_key);
317
+ if (!isNativeSetupReceipt(saved.receipt) || saved.receipt.state !== 'accepted' || !saved.receipt.resource_available || !saved.receipt.resource_id) {
318
+ throw new CliError('validation_acceptance_unconfirmed', `Inspect setup request ${saved.intent.key}. No replacement validation was created.`);
319
+ }
320
+ return observeValidation(context, saved.receipt.resource_id, selected, options, output, proposal?.id);
321
+ }
322
+ async function observeValidation(context, jobId, selected, options, output, proposalId) {
323
+ const read = async () => {
324
+ const original = proposalId ? (await executionProposal(context, proposalId, options.signal)).validation : null;
325
+ if (proposalId && (!original || original.id !== jobId))
326
+ throw new CliError('validation_pointer_changed', 'The original proposed validation could not be confirmed. No replacement was started.');
327
+ const current = (await api(`${context.prefix}/validation`, auth(context, options.signal))).validation;
328
+ if (original && current?.id !== original.id) {
329
+ output('validation_original', original, `Original validation ${original.id}: ${original.state}. Another validation is now selected.`);
330
+ throw new CliError('validation_pointer_changed', 'The original status is retained, but current-world diagnostics belong to another job. No validation was repeated.');
331
+ }
332
+ return current;
333
+ };
334
+ const initial = await read();
335
+ if (!initial || initial.id !== jobId || initial.execution_location !== selected.location)
336
+ throw new CliError('validation_pointer_changed', 'The original validation could not be confirmed. No replacement was started.');
337
+ if (selected.location === 'local' && (initial.local_grant_id !== selected.grant.id || initial.local_device_key !== selected.device)) {
338
+ throw new CliError('validation_permission_changed', 'Use the original validation’s device and local permission. No replacement was selected.');
339
+ }
340
+ if (selected.location === 'local') {
341
+ const original = await retainedLocalClaim(context, selected.device, jobId);
342
+ if (original && !terminal(initial.state) && (original.stage === 'work_dispatched' || original.stage === 'not_dispatched'
343
+ || initial.state !== 'queued')) {
344
+ if (original.stage === 'work_dispatched' && !original.lease)
345
+ throw new CliError('local_owner_identity_changed', 'The original dispatched operation identity is unavailable. Preserve its ownership journal; no replacement callback or target was started.');
346
+ const scope = `--workspace ${context.workspace}`;
347
+ const commands = original.stage === 'not_dispatched'
348
+ ? [`firedrill runner abandon ${selected.device} ${jobId} --world ${context.world} ${scope}`]
349
+ : original.stage === 'work_dispatched' && original.lease
350
+ ? [`firedrill runner evidence sync ${original.lease} ${scope}`, `firedrill runner completion sync ${original.lease} ${scope}`]
351
+ : [`firedrill runner abandon ${selected.device} ${jobId} --world ${context.world} ${scope}`];
352
+ output('local_validation_recovery', { job_id: jobId, operation_id: original.lease, ownership_stage: original.stage,
353
+ execution_repeated: false, recovery_commands: commands }, `Original local validation ${jobId}: ${initial.state}. No execution callback was repeated.\n`
354
+ + (original.stage === 'work_dispatched'
355
+ ? 'Keep the original runner active. If it was interrupted, the existing commands below recover retained evidence and closing facts only. They do not restart its target.'
356
+ : original.stage === 'not_dispatched' ? 'This original claim was abandoned before launch. Reconcile its original closing receipt.'
357
+ : 'This job already has an original claim. Keep its original runner active if it is still running; otherwise close the unstarted claim with the abandon command below. Starting it again is retired in this release.')
358
+ + '\n' + commands.join('\n')
359
+ + (original.stage === 'work_dispatched' && original.lease
360
+ ? `\nIf the original runner was interrupted and cleanup is still required, review firedrill runner cleanup ${original.lease} ${scope} before completion sync. Do not clean up an active runner.` : ''));
361
+ throw new CliError('local_execution_recovery_required', 'Use the original local recovery handoff above, then reopen this proposal to observe its retained result. No target was restarted.', 2);
362
+ }
363
+ }
364
+ if (selected.location === 'local' && initial.state === 'queued') {
365
+ output('local_validation_assigned', { job_id: jobId, device_key: selected.device }, 'Running the saved validation on this device.');
366
+ await withRunnerPresence(context, selected.device, options.signal, output, active => retainLocalValidation(context, selected.device, jobId, selected.request, active));
367
+ }
368
+ let previous = '';
369
+ while (true) {
370
+ const value = await read();
371
+ // Another tab may replace the current pointer only after terminal work.
372
+ // Never attribute that other job's readiness to this exact setup request.
373
+ if (!value || value.id !== jobId)
374
+ throw new CliError('validation_pointer_changed', 'Inspect the original setup request. Another validation is now selected.');
375
+ const summary = `${value.state}:${value.phase}`;
376
+ if (summary !== previous) {
377
+ output('validation_progress', value, `Validation: ${value.state.replaceAll('_', ' ')} (${value.phase.replaceAll('_', ' ')}).`);
378
+ previous = summary;
379
+ }
380
+ if (terminal(value.state)) {
381
+ if (value.state === 'completed' && value.current && value.scope === 'target_startup_only' && value.target_startup_observed) {
382
+ output('startup_observed', value, 'Original agent startup observed. Scenario checks were not run.'
383
+ + (value.execution_ready ? '' : '\n' + (value.readiness_issues.map(row => row.message).join(' ') || 'Simulation execution is not ready.')));
384
+ return value;
385
+ }
386
+ if (!value.current || !value.execution_ready) {
387
+ const detail = value.current ? value.failure?.diagnostics : null;
388
+ const message = [value.failure?.message ?? (value.readiness_issues.map(row => row.message).join(' ') || 'Review this world’s execution requirements before running simulations.'),
389
+ ...(detail ? [`During: ${detail.phase}`, ...(detail.process ? [`Process: ${detail.process}`] : []),
390
+ ...(detail.signals.length ? [`Output reported: ${detail.signals.join(', ')}`] : []), detail.next_step] : [])];
391
+ throw new CliError('world_execution_not_ready', message.map(line => terminalText(line)).join('\n'));
392
+ }
393
+ output('validation_finished', value, 'Startup validation completed. This is execution readiness, not a behavior assessment.');
394
+ return value;
395
+ }
396
+ await sleep(1500, undefined, { signal: options.signal });
397
+ }
398
+ }
399
+ async function campaign(context, id, signal) {
400
+ const value = await api(`/api/workspaces/${context.workspace}/campaigns/${identifier(id)}`, auth(context, signal));
401
+ if (value.id !== id || value.world_id !== context.world || !['local', 'managed'].includes(value.execution_location))
402
+ throw new CliError('campaign_identity_changed', 'The saved simulation does not belong to this world.');
403
+ return value;
404
+ }
405
+ async function quoteFor(context, plan, selected, signal) {
406
+ const value = await api(`${context.prefix}/simulation-plans/${identifier(plan.id)}/quote`, auth(context, signal));
407
+ if (value.plan_id !== plan.id || value.execution_location !== selected.location || typeof value.funded !== 'boolean' || typeof value.funding_held !== 'boolean'
408
+ || !['planned_trials', 'available_units', 'reserved_units', 'admitted_concurrency_ceiling'].every(key => Number.isSafeInteger(value[key]) && Number(value[key]) >= 0)
409
+ || value.planned_trials < 1 || !value.readiness || typeof value.readiness.ready !== 'boolean'
410
+ || !value.credits || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(value.credits.quote_id)
411
+ || !['credit_scale', 'estimated_units', 'maximum_units', 'available_units', 'reserved_units'].every(key => Number.isSafeInteger(value.credits[key]) && Number(value.credits[key]) >= 0)
412
+ || value.credits.credit_scale < 1 || !/^10*$/.test(String(value.credits.credit_scale)) || value.credits.maximum_units < value.credits.estimated_units
413
+ || !Number.isFinite(Date.parse(value.credits.expires_at)) || Date.parse(value.credits.expires_at) <= Date.now()) {
414
+ throw new CliError('simulation_quote_unavailable', 'The exact simulation quote could not be confirmed. No campaign was admitted.');
415
+ }
416
+ return value;
417
+ }
418
+ async function localCampaignRecovery(context, campaignId, selected, active, page, signal, output) {
419
+ let rows;
420
+ try {
421
+ rows = await localAssignmentsPage(context, selected.device, { available: false, world: context.world, campaign: campaignId, page }, signal);
422
+ }
423
+ catch (error) {
424
+ // A page can disappear after retention changes. Start a new bounded sweep;
425
+ // an absent page is not permission to replay a claim or start a replacement.
426
+ if (page > 1 && error instanceof CliError && error.httpStatus === 404)
427
+ return 1;
428
+ throw error;
429
+ }
430
+ for (const row of rows.results) {
431
+ if (row.world_id !== context.world || row.device_key !== selected.device || row.grant_id !== selected.grant
432
+ || row.purpose !== 'trial' || selected.nativeSession && row.native_session_id !== selected.nativeSession)
433
+ throw new CliError('local_assignment_changed', 'An original local assignment does not match this simulation permission. No recovery action was sent.');
434
+ if (!row.claim || active.has(row.job_id) || terminal(row.job_state) && row.claim.cleanup_recorded)
435
+ continue;
436
+ const original = await retainedLocalClaim(context, selected.device, row.job_id);
437
+ const saved = await readLocalClaim(context, selected.device, row.job_id, row.claim.request_key, signal);
438
+ const claim = saved.claim;
439
+ if (!claim || claim.id !== row.claim.id || saved.payload_digest !== row.payload_digest
440
+ || saved.grant_id !== selected.grant || saved.world_id !== context.world || saved.purpose !== 'trial'
441
+ || saved.native_session_id !== row.native_session_id)
442
+ throw new CliError('local_owner_identity_changed', 'The original local ownership changed during inspection. Read its saved claim; no execution was repeated.');
443
+ if (terminal(saved.job_state) && claim.cleanup_recorded)
444
+ continue;
445
+ if (original && (original.request !== claim.request_key || original.grant !== saved.grant_id
446
+ || original.proof.payload.native_session_id !== saved.native_session_id
447
+ || original.proof.payload.payload_digest !== saved.payload_digest
448
+ || createHash('sha256').update(nativeMetadataCanonical(original.proof.payload)).digest('hex') !== claim.request_digest
449
+ || selected.request && original.grantRequest !== selected.request
450
+ || original.lease !== null && original.lease !== claim.id
451
+ || original.attempt !== null && original.attempt !== claim.attempt_id))
452
+ throw new CliError('local_owner_identity_changed', 'The private ownership journal differs from this original claim. Preserve it for inspection; no execution was repeated.');
453
+ const scope = `--workspace ${context.workspace}`;
454
+ const status = `firedrill runner claim status ${selected.device} ${row.job_id} ${claim.request_key} ${scope}`;
455
+ const commands = [status];
456
+ let cleanupCommand = null;
457
+ let detail = 'Keep the original runner active if it is still running. This terminal has no original ownership journal, so recover from the original terminal. Do not create a replacement claim.';
458
+ if (original?.stage === 'work_dispatched') {
459
+ if (!original.lease || !original.attempt)
460
+ throw new CliError('local_owner_identity_changed', 'The dispatched operation identity is unavailable. Preserve the original journals; no target was restarted.');
461
+ commands.push(`firedrill runner evidence sync ${original.lease} ${scope}`, `firedrill runner completion sync ${original.lease} ${scope}`);
462
+ cleanupCommand = `firedrill runner cleanup ${original.lease} ${scope}`;
463
+ detail = 'This job already handed off to its original container. If that runner is active, leave it running. If it was interrupted, recover its saved evidence and closing facts; do not rerun the target.';
464
+ }
465
+ else if (original && claim.attempt_id) {
466
+ const unstarted = original.stage !== 'not_dispatched' && claim.state === 'claimed'
467
+ && !terminal(saved.job_state) && Date.parse(claim.expires_at) > Date.parse(saved.server_time);
468
+ // Resuming pre-launch ownership is retired with legacy local execution (FIR-504); only closing it remains.
469
+ commands.push(`firedrill runner abandon ${selected.device} ${row.job_id} --world ${context.world} ${scope}`);
470
+ detail = unstarted ? 'This original claim has not handed off to a container in its retained ownership journal. Keep its original runner active if it is still running; otherwise close it with the abandon command below. Starting it again is retired in this release.'
471
+ : 'This original claim cannot launch new work. Use its existing abandon command to reconcile unstarted ownership. That command refuses a retained container; it does not delete an unknown resource.';
472
+ }
473
+ output('local_simulation_recovery', { campaign_id: campaignId, job_id: row.job_id, claim_request_key: claim.request_key,
474
+ operation_id: original?.lease ?? null, ownership_stage: original?.stage ?? null, claim_state: claim.state,
475
+ execution_repeated: false, recovery_commands: commands, cleanup_after_runner_stopped: cleanupCommand,
476
+ report_url: reportLink(context, campaignId) }, `Original local drill ${row.job_id}: ${saved.job_state}. No execution callback was repeated.\n${detail}\n`
477
+ + commands.join('\n') + (cleanupCommand ? `\nOnly after the original runner has stopped, if cleanup is still needed, review ${cleanupCommand} before completion sync. Do not clean up an active runner.` : '')
478
+ + `\nSimulation report: ${reportLink(context, campaignId)}`);
479
+ throw new CliError('local_execution_recovery_required', 'Use the original local recovery handoff above, then continue this saved simulation. No claim, target or cleanup was repeated.', 2);
480
+ }
481
+ return rows.next ? page + 1 : 1;
482
+ }
483
+ async function watch(context, id, selected, parallel, options, output) {
484
+ const attempted = new Set(), active = new Map();
485
+ let failure, previous = '', recoveryPage = 1;
486
+ const work = async (signal) => {
487
+ let completed;
488
+ try {
489
+ while (true) {
490
+ const value = await campaign(context, id, signal);
491
+ if (value.execution_location !== selected.location)
492
+ throw new CliError('campaign_location_changed', 'The retained simulation uses a different execution location.');
493
+ const summary = JSON.stringify({ state: value.state, trials: value.trial_states, invocations: value.observed_invocations, active: active.size });
494
+ if (summary !== previous) {
495
+ output('campaign_progress', { ...value, local_assignments_active: active.size }, `${value.state.replaceAll('_', ' ')}: ${Object.entries(value.trial_states).map(([state, count]) => `${count} ${state.replaceAll('_', ' ')}`).join(', ') || 'allocating drills'}.\n`
496
+ + `${value.observed_invocations} retained target-invocation observations${selected.location === 'local' ? `; ${active.size} local assignments active` : ''}.`);
497
+ previous = summary;
498
+ }
499
+ if (failure)
500
+ throw failure;
501
+ if (selected.location === 'local')
502
+ recoveryPage = await localCampaignRecovery(context, id, { device: selected.device, grant: selected.grant.id, request: selected.request,
503
+ nativeSession: selected.grant.document.native_session_id }, active, recoveryPage, signal, output);
504
+ if (terminal(value.state)) {
505
+ completed = value;
506
+ break;
507
+ }
508
+ if (!value.cancel_requested && selected.location === 'local' && active.size < parallel) {
509
+ const rows = await localAssignmentsPage(context, selected.device, { available: true, world: context.world, campaign: id }, signal);
510
+ for (const row of rows.results) {
511
+ if (active.size >= parallel)
512
+ break;
513
+ if (attempted.has(row.job_id))
514
+ continue;
515
+ identifier(row.job_id);
516
+ if (row.world_id !== context.world || row.device_key !== selected.device || row.grant_id !== selected.grant.id || row.purpose !== 'trial')
517
+ throw new CliError('local_assignment_changed', 'A local assignment does not match this simulation permission.');
518
+ attempted.add(row.job_id);
519
+ const running = retainLocalTrial(context, selected.device, row.job_id, selected.request, signal, output)
520
+ .then(result => { output('local_trial_retained', { job_id: row.job_id, result }, `Saved drill ${row.job_id}: evidence handoff returned.`); })
521
+ .catch(error => { failure ??= error; })
522
+ .finally(() => active.delete(row.job_id));
523
+ active.set(row.job_id, running);
524
+ }
525
+ }
526
+ await sleep(1500, undefined, { signal });
527
+ }
528
+ }
529
+ finally {
530
+ // Every already-started callback owns its cleanup/closing. Never abandon
531
+ // those promises or replay one after an uncertain claim or connection.
532
+ await Promise.all(active.values());
533
+ }
534
+ // The cloud campaign can finish while the last local callback still owns
535
+ // evidence sync or cleanup. Do not hide that callback's later failure behind
536
+ // an earlier return from the terminal-state observation.
537
+ if (failure)
538
+ throw failure;
539
+ if (!completed)
540
+ throw new CliError('campaign_observation_unavailable', 'The final simulation status was not observed. Inspect its saved report; no drill was restarted.');
541
+ return completed;
542
+ };
543
+ return selected.location === 'local' ? withRunnerPresence(context, selected.device, options.signal, output, work) : work(options.signal);
544
+ }
545
+ export async function simulationCommand(action, options, output) {
546
+ const context = await contextFor(options);
547
+ const proposal = options.proposal ? await executionProposal(context, options.proposal, options.signal) : undefined;
548
+ if (proposal && (options.applicationCheckpoint !== undefined || options.coordinatedCheckpoint !== undefined || options.acknowledgeFreshWorldState
549
+ || options.shareApplicationCheckpointWithDevice || options.acknowledgeSupportedCheckpointScope || options.shareCoordinatedCheckpointWithDevice))
550
+ throw new CliError('execution_proposal_selection_changed', 'Keep the proposal starting state unchanged. Use ordinary simulation setup to choose an application checkpoint.', 2);
551
+ if (proposal) {
552
+ if (proposal.action !== (action === 'run' ? 'simulate' : 'validate'))
553
+ throw new CliError('execution_proposal_action_changed', 'Use the command matching the proposed action. No work was started.', 2);
554
+ output('execution_proposal', proposal, executionProposalText(proposal));
555
+ // Structured automation first gets a read-only review. --run never supplies
556
+ // location, real-effect, capture, evidence or funding permission by itself.
557
+ if (!interactive(options) && !options.run)
558
+ return;
559
+ if (proposal.campaign) {
560
+ await resumeSimulation(proposal.campaign.id, { ...options, proposal: undefined }, output);
561
+ return;
562
+ }
563
+ if (action === 'validate' && proposal.validation && terminal(proposal.validation.state)) {
564
+ const value = proposal.validation;
565
+ output('validation_finished', value, `Original startup validation ${value.state}. ${value.current ? 'Current inputs.' : 'Inputs have since changed.'} `
566
+ + 'This retained job status is not a readiness or behavior assessment.');
567
+ const status = (await api(`${context.prefix}/validation`, auth(context, options.signal))).validation;
568
+ if (status?.id === value.id)
569
+ output('validation_result', status, `${status.execution_ready ? 'Execution readiness recorded.' : 'Execution readiness not confirmed.'}`
570
+ + (status.failure ? `\n${status.failure.message}` : ''));
571
+ if (value.state !== 'completed')
572
+ throw new CliError('world_execution_not_ready', status?.id === value.id ? status.failure?.message ?? 'The original validation did not complete.' : 'The original validation did not complete. No replacement was started.');
573
+ return;
574
+ }
575
+ if (!proposal.validation)
576
+ executableProposal(proposal);
577
+ const location = proposal.plan?.execution.location ?? proposal.validation?.execution_location ?? proposal.location;
578
+ if (location !== 'choose' && (location === 'managed' && (options.local || options.grant || options.device) || location === 'local' && options.managed)) {
579
+ throw new CliError('execution_proposal_location_changed', 'Keep this proposal’s reviewed execution location. A different location needs a new proposal.', 2);
580
+ }
581
+ if (proposal.plan) {
582
+ if (options.repetitions !== undefined && Number(options.repetitions) !== proposal.repetitions
583
+ || options.parallel !== undefined && Number(options.parallel) !== proposal.parallelism
584
+ || options.seconds !== undefined && Number(options.seconds) !== proposal.trial_seconds
585
+ || options.scenarios?.length || options.all)
586
+ throw new CliError('execution_proposal_selection_changed', 'The proposal keeps its exact saved selection and limits. Omit selection overrides.', 2);
587
+ }
588
+ else if (action === 'run') {
589
+ const sameIds = (values) => [...new Set(values.map(identifier))].sort().join(',');
590
+ if (options.repetitions !== undefined && Number(options.repetitions) !== proposal.repetitions
591
+ || options.parallel !== undefined && Number(options.parallel) !== proposal.parallelism
592
+ || options.seconds !== undefined && Number(options.seconds) !== proposal.trial_seconds
593
+ || options.all && proposal.selector.mode !== 'all_matching'
594
+ || options.scenarios?.length && (proposal.selector.mode !== 'ids' || sameIds(options.scenarios) !== sameIds(proposal.selector.ids))) {
595
+ throw new CliError('execution_proposal_selection_changed', 'Keep the proposed scenarios and limits. Different choices need a new proposal.', 2);
596
+ }
597
+ options = { ...options, all: proposal.selector.mode === 'all_matching', scenarios: proposal.selector.mode === 'ids' ? proposal.selector.ids : undefined,
598
+ repetitions: String(proposal.repetitions), parallel: String(proposal.parallelism), seconds: String(proposal.trial_seconds) };
599
+ }
600
+ // A model's suggested location is not the user's execution choice. Only
601
+ // already-saved work supplies an immutable location during recovery.
602
+ if (proposal.plan || proposal.validation)
603
+ options = { ...options,
604
+ ...(location === 'managed' ? { managed: true } : location === 'local' ? { local: true } : {}) };
605
+ }
606
+ const selected = await execution(context, action === 'validate' ? 'validation' : 'trial', options, output);
607
+ if (proposal && proposal.location !== 'choose' && selected.location !== proposal.location) {
608
+ throw new CliError('execution_proposal_location_changed', 'The chosen location differs from the proposal. No work was started.', 2);
609
+ }
610
+ if (proposal?.validation && action === 'validate') {
611
+ await confirmation(options, 'Observe original startup validation?', 'Accepted work is not restarted. Only a still-unclaimed original local assignment can start.');
612
+ await observeValidation(context, proposal.validation.id, selected, options, output, proposal.id);
613
+ return;
614
+ }
615
+ if (action === 'validate') {
616
+ await validateWorld(context, selected, options, output, proposal);
617
+ return;
618
+ }
619
+ let setup = await readSetup(context, options.signal);
620
+ if (proposal) {
621
+ proposalBasis(proposal, await preparation(context, options.signal), setup);
622
+ if (proposal.plan) {
623
+ await admitPlan(context, proposal.plan, selected, options, output, proposal);
624
+ return;
625
+ }
626
+ const original = await optionalIntent(context, proposal.plan_request_key);
627
+ if (original) {
628
+ const body = original.body;
629
+ if (original.kind !== 'plan' || body.world_revision !== proposal.world_revision || body.configuration_revision !== proposal.configuration_revision
630
+ || body.selection_revision !== proposal.selection_revision || body.evaluation_revision !== proposal.evaluation_revision
631
+ || body.previous_plan_id !== proposal.previous_plan_id || body.repetitions !== proposal.repetitions
632
+ || body.parallelism !== proposal.parallelism || body.trial_seconds !== proposal.trial_seconds
633
+ || body.selector.mode !== proposal.selector.mode || body.selector.query !== '' || (body.selector.excluded?.length ?? 0) !== 0
634
+ || [...new Set(body.selector.ids ?? [])].sort().join(',') !== [...new Set(proposal.selector.ids)].sort().join(',')
635
+ || body.execution?.location !== selected.location || selected.location === 'local'
636
+ && (body.execution?.location !== 'local' || body.execution.grant_id !== selected.grant.id)) {
637
+ throw new CliError('execution_proposal_changed', 'The original saved plan request has different inputs. It was not replaced.');
638
+ }
639
+ output('execution_plan_request', { request_key: original.key, body }, 'Recovering the original reviewed plan, with its exact saved evidence and connection choices.');
640
+ await confirmation(options, 'Continue the original plan request?', 'The saved request body and approvals stay unchanged. No new plan key is created.');
641
+ const saved = await createIntent(context, 'plan', original.body, options.signal, output, proposal.plan_request_key);
642
+ if (!isNativeSetupReceipt(saved.receipt) || saved.receipt.state !== 'accepted' || !saved.receipt.resource_available || !saved.receipt.plan
643
+ || !planMatchesRequest(saved.receipt.plan, body) || !executionPlanMatches(proposal, saved.receipt.plan)) {
644
+ throw new CliError('simulation_plan_unconfirmed', `Inspect original setup request ${original.key}. No campaign was admitted.`);
645
+ }
646
+ await admitPlan(context, saved.receipt.plan, selected, options, output, proposal);
647
+ return;
648
+ }
649
+ }
650
+ if (setup.agent_comparison && (!setup.agent_comparison.current || options.scenarios?.length)) {
651
+ throw new CliError('agent_comparison_selection_changed', 'Review this comparison and keep its complete frozen scenario selection. No startup check or plan was created.', 2);
652
+ }
653
+ if (setup.agent_comparison?.starting_data) {
654
+ if (setup.real_connections_count !== 0 || options.dataset !== undefined && identifier(options.dataset) !== setup.agent_comparison.starting_data.dataset_id) {
655
+ throw new CliError('agent_comparison_starting_data_changed', 'Keep the original starting data and simulated connections for both comparison sides. No startup check or plan was created.', 2);
656
+ }
657
+ const initial = setup;
658
+ const startup = await api(`${context.prefix}/validation${selected.location === 'local' ? '?trial_grant_id=' + selected.grant.id : ''}`, auth(context, options.signal));
659
+ const value = startup.validation;
660
+ if (value?.active)
661
+ throw new CliError('validation_in_progress', 'Startup checks are already active for this comparison world. Inspect that original job; no duplicate was queued.', 2);
662
+ if (!value?.current || value.state !== 'completed' || value.scope !== 'native_runtime_and_scenario_preconditions'
663
+ || !value.execution_ready || value.execution_location !== selected.location
664
+ || selected.location === 'local' && startup.selected_execution_compatible !== true) {
665
+ let approve = options.validate === true;
666
+ if (!approve && interactive(options))
667
+ approve = await selectOne('Validate this comparison world first?', [
668
+ { value: true, label: 'Review and run startup checks', detail: 'Starting-data edits are applied and recorded separately in each drill.' },
669
+ { value: false, label: 'Exit without saving a plan' }
670
+ ], options.signal);
671
+ if (!approve)
672
+ throw new CliError('comparison_validation_required', 'Validate this world in the selected execution location first, or use --validate. No plan was saved.', 2);
673
+ await validateWorld(context, selected, options, output, proposal);
674
+ setup = await readSetup(context, options.signal);
675
+ if (setup.agent_comparison?.id !== initial.agent_comparison.id || setup.plan?.id !== initial.plan?.id
676
+ || setup.world_revision !== initial.world_revision || setup.configuration_revision !== initial.configuration_revision
677
+ || setup.selection_revision !== initial.selection_revision || setup.evaluation_revision !== initial.evaluation_revision) {
678
+ throw new CliError('agent_comparison_selection_changed', 'Comparison inputs changed during startup checks. Review the current setup; no plan was saved.', 2);
679
+ }
680
+ }
681
+ }
682
+ const frozen = setup.agent_comparison;
683
+ if (frozen && (!frozen.current || options.scenarios?.length))
684
+ throw new CliError('agent_comparison_selection_changed', 'This comparison requires its complete frozen selection. Review the original comparison if it is no longer current.', 2);
685
+ const selector = frozen ? { mode: 'all_matching', query: '', ids: [], excluded: [] } : await scenarios(context, setup, options);
686
+ const parallelCapacity = Math.min(setup.capacity.max_parallelism, selected.location === 'local' ? selected.grant.document.limits.parallelism : setup.capacity.max_parallelism);
687
+ if (frozen && (frozen.repetitions > setup.capacity.max_repetitions || frozen.parallelism > parallelCapacity || frozen.trial_seconds > setup.capacity.max_trial_seconds
688
+ || options.repetitions !== undefined && Number(options.repetitions) !== frozen.repetitions
689
+ || options.parallel !== undefined && Number(options.parallel) !== frozen.parallelism || options.seconds !== undefined && Number(options.seconds) !== frozen.trial_seconds)) {
690
+ throw new CliError('agent_comparison_settings_changed', 'Keep the original comparison settings. Choose compatible execution permission or review a new comparison instead of silently lowering the settings.', 2);
691
+ }
692
+ const repetitions = frozen?.repetitions ?? await numberOption(options.repetitions, setup.defaults.repetitions, setup.capacity.max_repetitions, 'Runs per scenario', options);
693
+ const parallelism = frozen?.parallelism ?? await numberOption(options.parallel, setup.defaults.parallelism, parallelCapacity, 'Parallel drills', options);
694
+ const continuity = await api(context.prefix + '/continuous-requirements', {
695
+ ...auth(context, options.signal), method: 'POST', body: { selector, world_revision: setup.world_revision, selection_revision: setup.selection_revision }
696
+ });
697
+ if (!continuityRequirementsMatch(continuity, context.world, setup.world_revision, setup.selection_revision))
698
+ throw new CliError('continuous_plan_changed', 'The selected scenario duration could not be confirmed. Review setup again.');
699
+ if (continuity.minimum_trial_seconds > setup.capacity.max_trial_seconds)
700
+ throw new CliError('continuous_capacity_required', 'This selection needs more time than the current drill capacity. Choose a shorter scenario or contact support.', 2);
701
+ const seconds = frozen?.trial_seconds ?? await numberOption(options.seconds, setup.plan?.current ? setup.defaults.trial_seconds : Math.max(setup.defaults.trial_seconds, continuity.minimum_trial_seconds), setup.capacity.max_trial_seconds, 'Seconds per drill', options);
702
+ if (seconds < continuity.minimum_trial_seconds)
703
+ throw new CliError('continuous_duration_required', `This selection needs at least ${continuity.minimum_trial_seconds} seconds per drill. Review its actual-time schedule.`, 2);
704
+ if (frozen)
705
+ output('comparison_scope', frozen, `${frozen.side === 'control' ? 'Control' : 'Candidate'} comparison: ${frozen.scenario_count} frozen scenarios, ${repetitions} repetitions. Criteria and world definitions are retained from the original report.`);
706
+ let assess = options.assess === true;
707
+ if (!assess && setup.automatic_criteria_count && interactive(options))
708
+ assess = await selectOne('Saved evaluation criteria', [
709
+ { value: true, label: 'Assess this simulation', detail: 'Use selected evidence and policy passages for AI scoring and cited explanations after each drill and at declared memory checkpoints.' },
710
+ { value: false, label: 'Run without automatic assessment' }
711
+ ], options.signal);
712
+ if (frozen?.run_saved_criteria && setup.automatic_criteria_count && !assess)
713
+ throw new CliError('assessment_consent_required', 'This comparison retains its assessment criteria. Approve evidence sharing with --assess or cancel; it will not run with weaker criteria.', 2);
714
+ let driverInput = options.approveAgentInput === true;
715
+ if (setup.execution_driver?.requires_input) {
716
+ if (setup.real_connections_count > 0)
717
+ throw new CliError('agent_driver_action_denied', 'This agent input adapter requires simulated connections.', 2);
718
+ await reviewDriverInput(context, setup, options, output);
719
+ const timing = executionScheduleText(setup.execution_driver);
720
+ if (timing)
721
+ output('execution_timing', { kind: setup.execution_driver.kind,
722
+ scheduled_after_ms: setup.execution_driver.scheduled_after_ms }, timing);
723
+ if (!driverInput && interactive(options))
724
+ driverInput = await selectOne('Agent interaction', [
725
+ { value: false, label: 'Cancel' }, { value: true, label: 'Use the prepared input adapter',
726
+ detail: `Reviewed ${setup.execution_driver.kind.replaceAll('_', ' ')} recipe ${setup.execution_driver.revision}. Simulated users can interact only with the isolated agent and its approved dependencies.${timing ? ` ${timing}` : ''}` }
727
+ ], options.signal);
728
+ if (!driverInput)
729
+ throw new CliError('agent_driver_input_required', `Review input with firedrill driver review --world ${context.world} --workspace ${context.workspace}. Automation requires explicit --approve-agent-input.`, 2);
730
+ }
731
+ else if (driverInput)
732
+ throw new CliError('agent_execution_identity_changed', 'No non-chat input adapter is selected. Remove --approve-agent-input.', 2);
733
+ const desktopInputRequired = setup.execution_driver?.kind === 'desktop';
734
+ if (desktopInputRequired && !setup.desktop_capture_available)
735
+ throw new CliError('desktop_capture_unavailable', 'The prepared desktop input adapter needs private screen capture. Review this world’s desktop setup before planning.', 2);
736
+ let recordBrowser = options.recordBrowser === true;
737
+ if (recordBrowser && desktopInputRequired)
738
+ throw new CliError('simulation_capture_policy_invalid', 'This input adapter requires desktop capture. Omit --record-browser and review --record-desktop with --acknowledge-desktop-screen-content.', 2);
739
+ if (typeof setup.mask_browser_form_values !== 'boolean')
740
+ throw new CliError('simulation_capture_policy_changed', 'The browser capture preference could not be confirmed. Refresh simulation setup before saving.', 2);
741
+ if (recordBrowser && !setup.browser_capture_available)
742
+ throw new CliError('browser_capture_unavailable', 'Browser captures need a prepared browser entry point with simulated connections.', 2);
743
+ if (!desktopInputRequired && !recordBrowser && setup.browser_capture_available && interactive(options))
744
+ recordBrowser = await selectOne('Browser evidence', [
745
+ { value: false, label: 'Do not save browser captures' },
746
+ { value: true, label: 'Save private browser captures', detail: `${setup.mask_browser_form_values ? 'Form values will be masked.' : 'Form values may be visible.'} Passwords and marked-private content stay masked. Other visible text may be retained. Captures are not sent to the evaluator.` }
747
+ ], options.signal);
748
+ let recordVoice = options.recordVoice === true;
749
+ if (recordVoice && !setup.voice_capture_available)
750
+ throw new CliError('voice_capture_unavailable', 'Audio recording needs a prepared voice session with simulated connections.', 2);
751
+ if (!recordVoice && setup.voice_capture_available && interactive(options))
752
+ recordVoice = await selectOne('Audio evidence', [
753
+ { value: false, label: 'Do not save audio recordings' },
754
+ { value: true, label: 'Save private conversation audio', detail: 'Audio is not automatically redacted. Recordings follow workspace retention and are not sent to the evaluator.' }
755
+ ], options.signal);
756
+ let recordDesktop = options.recordDesktop === true;
757
+ let screenAcknowledged = options.acknowledgeDesktopScreenContent === true;
758
+ if (recordDesktop && !setup.desktop_capture_available)
759
+ throw new CliError('desktop_capture_unavailable', 'Desktop captures need a prepared private desktop with simulated connections.', 2);
760
+ if (recordBrowser && recordDesktop)
761
+ throw new CliError('simulation_capture_policy_invalid', 'Choose --record-browser or --record-desktop, not both.', 2);
762
+ if (!recordDesktop && !recordBrowser && setup.desktop_capture_available && interactive(options))
763
+ recordDesktop = await selectOne('Desktop evidence', [
764
+ { value: false, label: desktopInputRequired ? 'Cancel desktop simulation' : 'Do not save desktop captures' },
765
+ { value: true, label: 'Save private desktop captures', detail: `${desktopInputRequired ? 'Required to see and operate this desktop. ' : ''}Declared pixel regions are masked. Text screening can miss sensitive content and does not hide form values automatically.` }
766
+ ], options.signal);
767
+ if (recordDesktop && !screenAcknowledged && interactive(options)) {
768
+ screenAcknowledged = await selectOne('Screen content', [
769
+ { value: false, label: desktopInputRequired ? 'Cancel desktop simulation' : 'Continue without desktop recording' },
770
+ { value: true, label: 'I understand visible screen content may be retained', detail: 'Captures follow workspace retention and are not sent to the evaluator.' }
771
+ ], options.signal);
772
+ if (!screenAcknowledged)
773
+ recordDesktop = false;
774
+ }
775
+ if (desktopInputRequired && (!recordDesktop || !screenAcknowledged)) {
776
+ output('desktop_capture_consent_required', { world_id: context.world, driver_contract_sha256: setup.execution_driver.contract_sha256,
777
+ required_input: [...(!recordDesktop ? ['record_desktop'] : []), ...(!screenAcknowledged ? ['acknowledge_desktop_screen_content'] : [])],
778
+ required_flags: [...(!recordDesktop ? ['--record-desktop'] : []), ...(!screenAcknowledged ? ['--acknowledge-desktop-screen-content'] : [])] }, 'This desktop input adapter needs explicitly approved private screen capture and acknowledgement of visible screen content.');
779
+ throw new CliError('desktop_capture_consent_required', 'Review --record-desktop and --acknowledge-desktop-screen-content before creating this desktop plan. No screen-recording consent was added automatically.', 2);
780
+ }
781
+ if (recordDesktop && !screenAcknowledged)
782
+ throw new CliError('desktop_capture_consent_required', 'Desktop recording requires --acknowledge-desktop-screen-content, or omit --record-desktop.', 2);
783
+ if (!recordDesktop && screenAcknowledged)
784
+ throw new CliError('desktop_capture_consent_invalid', 'Use --acknowledge-desktop-screen-content only with --record-desktop.', 2);
785
+ if (!Number.isSafeInteger(setup.real_connections_count) || setup.real_connections_count < 0)
786
+ throw new CliError('simulation_setup_changed', 'The connection selection could not be confirmed. Reopen simulation setup.');
787
+ let externalEffects;
788
+ if (setup.real_connections_count > 0) {
789
+ let effects = options.approveRealSimulation === true, shared = options.acknowledgeSharedState === true;
790
+ if (!effects && interactive(options))
791
+ effects = await selectOne('Real-account simulation', [
792
+ { value: false, label: 'Cancel' }, { value: true, label: 'Approve the selected real-account operations',
793
+ detail: `${setup.real_connections_count} real connections. Agent startup, readiness and scenario execution may affect actual accounts. ${repetitions} repetitions, up to ${parallelism} concurrent, ${seconds}s per drill.` }
794
+ ], options.signal);
795
+ if (!shared && effects && interactive(options))
796
+ shared = await selectOne('External account state', [
797
+ { value: false, label: 'Cancel' }, { value: true, label: 'Continue with shared account state',
798
+ detail: 'Actual accounts are shared across drills and are not reset. Simulations may influence one another. An uncertain real write requires review, not automatic replay.' }
799
+ ], options.signal);
800
+ if (!effects || !shared)
801
+ throw new CliError('real_simulation_consent_required', 'Review real-account effects and shared state. Automation requires --approve-real-simulation --acknowledge-shared-state; startup approval and --run do not grant this permission.', 2);
802
+ externalEffects = { actor_id: context.owner, acknowledge_effects: true, acknowledge_shared_state: true };
803
+ }
804
+ else if (options.approveRealSimulation || options.acknowledgeSharedState) {
805
+ throw new CliError('real_simulation_selection_changed', 'This configuration has no real-account connections. Remove the real-simulation approval options and review the selection.', 2);
806
+ }
807
+ if (options.applicationCheckpoint !== undefined && options.coordinatedCheckpoint !== undefined)
808
+ throw new CliError('application_continuation_combination_unavailable', 'Choose one saved application or coordinated checkpoint, not both.', 2);
809
+ if ((options.applicationCheckpoint !== undefined || options.coordinatedCheckpoint !== undefined) && options.dataset !== undefined)
810
+ throw new CliError('application_continuation_combination_unavailable', 'Choose an application checkpoint or a separate starting dataset, not both.', 2);
811
+ const dataset = options.applicationCheckpoint !== undefined || options.coordinatedCheckpoint !== undefined ? null : await startingData(context, setup, options);
812
+ const application = await applicationStartingState(context, setup, options, dataset, selected, proposal);
813
+ let shareDatasetWithDevice = options.shareDatasetWithDevice === true;
814
+ if (dataset && selected.location === 'local') {
815
+ if (!shareDatasetWithDevice && interactive(options))
816
+ shareDatasetWithDevice = await selectOne('Download starting data to this device?', [
817
+ { value: false, label: 'Cancel' }, { value: true, label: 'Download the selected data for these drills',
818
+ detail: 'Use a private runner cache outside your project. Firedrill cannot remotely erase device copies or backups.' }
819
+ ], options.signal);
820
+ if (!shareDatasetWithDevice)
821
+ throw new CliError('local_dataset_consent_required', 'Approve the selected starting-data download interactively or use --share-dataset-with-device. No simulation was admitted.', 2);
822
+ }
823
+ else if (shareDatasetWithDevice) {
824
+ throw new CliError('local_dataset_consent_invalid', 'Use --share-dataset-with-device only with saved starting data and local execution.', 2);
825
+ }
826
+ if (dataset)
827
+ output('simulation_starting_data', { id: dataset.id, edit_count: dataset.edit_count }, `${dataset.edit_count} saved edits selected for each independent drill. Application will be recorded separately from its score.`);
828
+ const body = { previous_plan_id: setup.plan?.id ?? null, world_revision: setup.world_revision,
829
+ configuration_revision: setup.configuration_revision, selection_revision: setup.selection_revision, evaluation_revision: setup.evaluation_revision,
830
+ share_evaluation_evidence: assess, share_evaluation_explanation: assess, run_saved_criteria: frozen?.run_saved_criteria ?? assess, selector, repetitions, parallelism, trial_seconds: seconds, record_browser: recordBrowser, record_voice: recordVoice,
831
+ record_desktop: recordDesktop, acknowledge_desktop_screen_content: screenAcknowledged,
832
+ driver_input_authorized: driverInput,
833
+ driver_contract_sha256: driverInput ? setup.execution_driver.contract_sha256 : null,
834
+ mask_browser_form_values: setup.mask_browser_form_values,
835
+ execution: selected.location === 'local' ? { location: 'local', grant_id: selected.grant.id } : { location: 'managed' }, dataset_id: dataset?.id ?? null,
836
+ share_dataset_with_device: shareDatasetWithDevice,
837
+ application_checkpoint_id: application?.kind === 'application' ? application.checkpoint.id : null,
838
+ acknowledge_fresh_world_state: application?.kind === 'application' && application.acknowledged,
839
+ share_application_checkpoint_with_device: application?.kind === 'application' && application.share,
840
+ coordinated_checkpoint_id: application?.kind === 'coordinated' ? application.checkpoint.id : null,
841
+ acknowledge_supported_checkpoint_scope: application?.kind === 'coordinated' && application.acknowledged,
842
+ share_coordinated_checkpoint_with_device: application?.kind === 'coordinated' && application.share,
843
+ ...(externalEffects ? { external_effects: externalEffects } : {}) };
844
+ if (proposal)
845
+ proposalBasis(await executionProposal(context, proposal.id, options.signal), await preparation(context, options.signal), await readSetup(context, options.signal));
846
+ const saved = await createIntent(context, 'plan', body, options.signal, output, proposal?.plan_request_key);
847
+ if (!isNativeSetupReceipt(saved.receipt))
848
+ throw new CliError('simulation_plan_unconfirmed', `Inspect setup request ${saved.intent.key}. No campaign was started.`);
849
+ const plan = saved.receipt.plan;
850
+ if (saved.receipt.state !== 'accepted' || !saved.receipt.resource_available || !plan?.current || !plan.id
851
+ || !planMatchesRequest(plan, body)
852
+ || plan.dataset_id !== (dataset?.id ?? null) || shareDatasetWithDevice && !plan.dataset_download_approved_for_actor
853
+ || !/^[a-f0-9]{64}$/.test(plan.recipe_sha256)) {
854
+ throw new CliError('simulation_plan_unconfirmed', `Inspect setup request ${saved.intent.key}. No campaign was started.`);
855
+ }
856
+ await admitPlan(context, plan, selected, options, output, proposal);
857
+ }
858
+ async function admitPlan(context, plan, selected, options, output, proposal) {
859
+ if (!plan.current || plan.execution.location !== selected.location
860
+ || selected.location === 'local' && (plan.execution.location !== 'local' || plan.execution.grant_id !== selected.grant.id)
861
+ || plan.mode === 'external_shared' && !plan.real_access_approved_for_actor
862
+ || plan.dataset_id && selected.location === 'local' && !plan.dataset_download_approved_for_actor
863
+ || plan.application_checkpoint_id && (!plan.acknowledge_fresh_world_state || !plan.application_continuation
864
+ || plan.dataset_id || plan.mode === 'external_shared'
865
+ || selected.location === 'local' && !plan.application_checkpoint_download_approved_for_actor)
866
+ || plan.coordinated_checkpoint_id && (!plan.acknowledge_supported_checkpoint_scope || !plan.application_continuation
867
+ || plan.application_continuation.dependencies !== 'coordinated_native_checkpoint' || plan.dataset_id || plan.mode === 'external_shared'
868
+ || selected.location === 'local' && !plan.coordinated_checkpoint_download_approved_for_actor)
869
+ || plan.record_desktop !== plan.acknowledge_desktop_screen_content
870
+ || options.dataset !== undefined && options.dataset !== plan.dataset_id
871
+ || options.applicationCheckpoint !== undefined && identifier(options.applicationCheckpoint) !== plan.application_checkpoint_id
872
+ || options.coordinatedCheckpoint !== undefined && identifier(options.coordinatedCheckpoint) !== plan.coordinated_checkpoint_id
873
+ || options.acknowledgeFreshWorldState && !plan.application_checkpoint_id
874
+ || options.shareApplicationCheckpointWithDevice && (!plan.application_checkpoint_id || selected.location !== 'local')
875
+ || options.acknowledgeSupportedCheckpointScope && !plan.coordinated_checkpoint_id
876
+ || options.shareCoordinatedCheckpointWithDevice && (!plan.coordinated_checkpoint_id || selected.location !== 'local')
877
+ || options.assess === true && !plan.run_saved_criteria || options.recordBrowser === true && !plan.record_browser
878
+ || options.recordVoice === true && !plan.record_voice || options.recordDesktop === true && !plan.record_desktop
879
+ || options.acknowledgeDesktopScreenContent === true && !plan.record_desktop
880
+ || (options.approveRealSimulation || options.acknowledgeSharedState) && plan.mode !== 'external_shared'
881
+ || options.shareDatasetWithDevice === true && (!plan.dataset_id || selected.location !== 'local')) {
882
+ throw new CliError('simulation_plan_changed', 'The original plan or its location, capture, evidence, dataset or real-account approval could not be confirmed. It was not replaced.', 2);
883
+ }
884
+ output('simulation_plan_review', plan, `${plan.planned_trials} planned drills. ${plan.execution.location} execution. `
885
+ + `${plan.run_saved_criteria ? 'Saved criteria enabled' : 'No automatic saved-criteria assessment'}. `
886
+ + `Browser captures: ${plan.record_browser ? 'yes' : 'no'}; audio: ${plan.record_voice ? 'yes' : 'no'}; desktop: ${plan.record_desktop ? 'yes' : 'no'}.`
887
+ + (plan.mode === 'external_shared' ? '\nReviewed real-account effects use shared state and cannot be reset.' : '')
888
+ + (plan.dataset_id ? `\nStarting data: ${plan.dataset_id}${selected.location === 'local' ? ', download approved for this actor' : ''}.` : '')
889
+ + (plan.application_continuation ? `\nApplication continuation: ${plan.application_continuation.components.map(one => terminalText(one.name)).join(', ')}. `
890
+ + (plan.application_continuation.dependencies === 'coordinated_native_checkpoint' ? 'Captured supported native state is restored in a new process. Wall time continues.'
891
+ : 'A new process and fresh prepared synthetic services are used for each drill.') : ''));
892
+ let quote = await quoteFor(context, plan, selected, options.signal);
893
+ if (!quote.readiness.ready) {
894
+ if (plan.dataset_id || plan.application_checkpoint_id || plan.coordinated_checkpoint_id)
895
+ throw new CliError('starting_data_plan_needs_review', `Plan saved, but startup evidence or execution permission needs review. ${quote.readiness.message ?? ''} Review starting data and save a current plan before running. No new validation or campaign was queued.`, 2);
896
+ let validate = options.validate === true;
897
+ if (!validate && interactive(options))
898
+ validate = await selectOne('Startup validation is required', [
899
+ { value: true, label: 'Validate before running', detail: quote.readiness.message ?? undefined },
900
+ { value: false, label: 'Keep the saved plan and exit' }
901
+ ], options.signal);
902
+ if (!validate)
903
+ throw new CliError('world_execution_not_ready', 'Plan saved. Run firedrill validate or approve --validate before starting this simulation.', 2);
904
+ const current = proposal ? await executionProposal(context, proposal.id, options.signal) : undefined;
905
+ if (current?.validation)
906
+ await observeValidation(context, current.validation.id, selected, options, output, current.id);
907
+ else
908
+ await validateWorld(context, selected, options, output, current);
909
+ quote = await quoteFor(context, plan, selected, options.signal);
910
+ }
911
+ if (quote.plan_id !== plan.id || quote.execution_location !== selected.location || !quote.readiness.ready)
912
+ throw new CliError('simulation_not_ready', quote.readiness.message ?? 'Review the saved plan before running.');
913
+ const credits = (units) => creditAmount(units, quote.credits.credit_scale);
914
+ output('simulation_quote', quote, `${quote.planned_trials} drills; up to ${quote.admitted_concurrency_ceiling} concurrent.\n`
915
+ + `Estimated ${credits(quote.credits.estimated_units)} credits; maximum ${credits(quote.credits.maximum_units)}. Target model use is separate.`);
916
+ if (quote.funding_held)
917
+ throw new CliError('billing_adjustment_pending', 'A payment adjustment needs review before new simulations can start. Existing reports remain available. No campaign was admitted.');
918
+ if (!quote.funded)
919
+ throw new CliError('simulation_balance_insufficient', 'Open Billing to add credits or apply an event code. No campaign was admitted.');
920
+ if (quote.admitted_concurrency_ceiling < 1)
921
+ throw new CliError('simulation_capacity_pending', 'Execution capacity is not available for this plan yet. No campaign was admitted.');
922
+ await confirmation(options, 'Run this simulation?', `Reserve ${credits(quote.credits.estimated_units)} estimated credits now, with a maximum authorized budget of ${credits(quote.credits.maximum_units)} credits for these drills.`);
923
+ if (proposal) {
924
+ const current = await executionProposal(context, proposal.id, options.signal);
925
+ if (current.campaign) {
926
+ await resumeSimulation(current.campaign.id, { ...options, proposal: undefined }, output);
927
+ return;
928
+ }
929
+ proposalBasis(current, await preparation(context, options.signal), await readSetup(context, options.signal));
930
+ if (!current.plan || current.plan.id !== plan.id || current.plan.recipe_sha256 !== plan.recipe_sha256 || !executionPlanMatches(current, plan)) {
931
+ throw new CliError('simulation_plan_changed', 'The original proposal’s saved plan changed. No campaign was admitted.');
932
+ }
933
+ }
934
+ if (Date.parse(quote.credits.expires_at) <= Date.now())
935
+ throw new CliError('credit_quote_expired', 'Review the current simulation budget again. No simulation started.');
936
+ const admitted = await createIntent(context, 'admission', { plan_id: plan.id, recipe_sha256: plan.recipe_sha256, usage_quote_id: quote.credits.quote_id }, options.signal, output, proposal?.admission_request_key);
937
+ if (isNativeSetupReceipt(admitted.receipt) || admitted.receipt.state !== 'admitted' || !admitted.receipt.campaign_id)
938
+ throw new CliError('campaign_acceptance_unconfirmed', `Inspect setup request ${admitted.intent.key}. No replacement campaign was started.`);
939
+ const id = identifier(admitted.receipt.campaign_id);
940
+ output('campaign_admitted', { campaign_id: id, report_url: reportLink(context, id) }, `Simulation saved. Report: ${reportLink(context, id)}\nCtrl+C stops this terminal, not the saved campaign.`);
941
+ const result = await watch(context, id, selected, Math.min(plan.parallelism, quote.admitted_concurrency_ceiling), options, output);
942
+ output('campaign_finished', { ...result, report_url: reportLink(context, id) }, `Simulation ${result.state}. Report: ${reportLink(context, id)}`);
943
+ }
944
+ export async function simulationRequest(action, key, options, output) {
945
+ const context = await contextFor(options);
946
+ if (action === 'list') {
947
+ let after = options.after;
948
+ while (true) {
949
+ const page = await intentPage(context, after, options.signal);
950
+ output('simulation_requests', { ...page, results: page.results.map(row => ({ request_key: row.intent.key,
951
+ kind: row.intent.kind, created_at: row.intent.created_at, receipt: row.receipt })) }, (page.results.map(row => `${row.intent.key} ${row.intent.kind} ${row.receipt.state}`).join('\n') || 'No saved setup requests on this page.')
952
+ + (page.next_after ? `\nMore requests: firedrill simulation requests --world ${context.world} --workspace ${context.workspace} --after ${page.next_after}` : ''));
953
+ if (!page.next_after || !interactive(options) || !await selectOne('Saved setup requests', [
954
+ { value: true, label: 'Next page' }, { value: false, label: 'Done' },
955
+ ], options.signal))
956
+ break;
957
+ after = page.next_after;
958
+ }
959
+ return;
960
+ }
961
+ if (!key)
962
+ throw new CliError('setup_request_required', 'Choose the original setup request ID.', 2);
963
+ let result;
964
+ if (action === 'continue') {
965
+ result = await inspectIntent(context, identifier(key), options.signal);
966
+ if (['not_received', 'pending'].includes(result.receipt.state)) {
967
+ // Resending would start legacy validation, planning or admission, which this release retires (FIR-504;
968
+ // root's managed_target_launch gate is off). Reading and withdrawing the original stay available.
969
+ const scope = `--world ${context.world} --workspace ${context.workspace}`;
970
+ throw new CliError('legacy_launch_retired', `Original setup ${result.intent.key}: ${result.receipt.state}. Sending it again is retired in this release.\n`
971
+ + `Read it: firedrill simulation request status ${result.intent.key} ${scope}\nWithdraw it: firedrill simulation request withdraw ${result.intent.key} ${scope}\n`
972
+ + 'New simulations use firedrill plan, apply and run. Nothing was sent.', 2);
973
+ }
974
+ }
975
+ else
976
+ result = action === 'withdraw' ? await withdrawIntent(context, identifier(key), options.signal) : await inspectIntent(context, identifier(key), options.signal);
977
+ const setup = isNativeSetupReceipt(result.receipt) ? result.receipt : null;
978
+ const admission = !isNativeSetupReceipt(result.receipt) ? result.receipt : null;
979
+ output('simulation_request_receipt', { request_key: result.intent.key, kind: result.intent.kind, ...result.receipt }, `Setup ${result.intent.key}: ${result.receipt.state}.`
980
+ + (['accepted', 'admitted'].includes(result.receipt.state) ? ' Accepted work was not canceled or restarted.' : '')
981
+ + (result.receipt.state === 'not_received' ? ' This is not proof that the original request cannot still arrive.' : '')
982
+ + (setup?.resource_id ? `\n${result.intent.kind === 'validation' ? 'Validation job' : 'Plan'}: ${setup.resource_id}` : '')
983
+ + (admission?.campaign_id ? `\nReport: ${reportLink(context, admission.campaign_id)}` : ''));
984
+ }
985
+ /**
986
+ * Watch an original simulation without starting anything (FIR-504). Root's
987
+ * launch gates are off in this release, so never-claimed legacy drills stay
988
+ * unclaimed: no permission is selected, no assignment is claimed and no runner
989
+ * presence is opened. Original local claims are handed to their existing
990
+ * recovery commands, exactly as before; `simulation cancel` stops the rest.
991
+ */
992
+ export async function resumeSimulation(id, options, output) {
993
+ id = identifier(id);
994
+ const context = await contextFor(options);
995
+ let current = await campaign(context, id, options.signal);
996
+ if (terminal(current.state)) {
997
+ output('campaign_finished', { ...current, report_url: reportLink(context, id) }, `Simulation ${current.state}. Report: ${reportLink(context, id)}`);
998
+ return;
999
+ }
1000
+ if (current.execution_location === 'local' && options.managed || current.execution_location === 'managed' && (options.local || options.device || options.grant)) {
1001
+ throw new CliError('campaign_location_changed', 'Continue in this simulation’s original execution location. No work was relocated.');
1002
+ }
1003
+ const location = current.execution_location;
1004
+ const original = location === 'local' ? current.local_execution : null;
1005
+ if (location === 'local') {
1006
+ if (!original)
1007
+ throw new CliError('campaign_permission_changed', 'The original local execution permission is unavailable. No replacement was selected.');
1008
+ identifier(original.grant_id);
1009
+ identifier(original.device_key);
1010
+ if (options.device && identifier(options.device) !== original.device_key)
1011
+ throw new CliError('campaign_permission_changed', 'Use the original device saved with this simulation. No replacement was selected.');
1012
+ }
1013
+ const scope = `--world ${context.world} --workspace ${context.workspace}`;
1014
+ const cancel = `firedrill simulation cancel ${id} ${scope}`;
1015
+ output('campaign_watch_only', { campaign_id: id, execution_location: location, new_work_started: false, execution_repeated: false,
1016
+ cancel_command: cancel, report_url: reportLink(context, id) }, 'Watching the original simulation only. Starting work from the older world commands is retired in this release, so never-claimed drills are not started and nothing is replayed.\n'
1017
+ + `Stop it: ${cancel}\nNew simulations use firedrill plan, apply and run.`);
1018
+ let previous = '', recoveryPage = 1;
1019
+ while (true) {
1020
+ const summary = JSON.stringify({ state: current.state, trials: current.trial_states, invocations: current.observed_invocations });
1021
+ if (summary !== previous) {
1022
+ output('campaign_progress', current, `${current.state.replaceAll('_', ' ')}: ${Object.entries(current.trial_states).map(([state, count]) => `${count} ${state.replaceAll('_', ' ')}`).join(', ') || 'allocating drills'}.\n`
1023
+ + `${current.observed_invocations} retained target-invocation observations.`);
1024
+ previous = summary;
1025
+ }
1026
+ if (terminal(current.state))
1027
+ break;
1028
+ if (original) {
1029
+ // Scoped GETs only: an original claim is handed to its existing recovery commands; nothing is claimed here.
1030
+ // Expired permission is not a reason to hide original ownership or select a replacement.
1031
+ try {
1032
+ recoveryPage = await localCampaignRecovery(context, id, { device: original.device_key, grant: original.grant_id }, new Map(), recoveryPage, options.signal, output);
1033
+ if (recoveryPage === 1 && !current.cancel_requested) {
1034
+ const unclaimed = await localAssignmentsPage(context, original.device_key, { available: true, world: context.world, campaign: id }, options.signal);
1035
+ if (unclaimed.results.length) {
1036
+ output('campaign_unstarted_retained', { campaign_id: id, execution_repeated: false, new_work_started: false,
1037
+ cancel_command: cancel, report_url: reportLink(context, id) }, `This simulation still has drills that never started. Starting them is retired in this release, so it will not finish on its own.\nStop it: ${cancel}\nSimulation report: ${reportLink(context, id)}`);
1038
+ throw new CliError('legacy_launch_retired', `Cancel the original simulation with ${cancel}, or leave it as retained history. New simulations use firedrill plan, apply and run. Nothing was started.`, 2);
1039
+ }
1040
+ }
1041
+ }
1042
+ catch (error) {
1043
+ if (!(error instanceof CliError) || ![403, 404].includes(error.httpStatus ?? 0))
1044
+ throw error;
1045
+ const command = `firedrill runner jobs ${original.device_key} --workspace ${context.workspace}`;
1046
+ output('local_simulation_permission_recovery', { campaign_id: id, device_key: original.device_key,
1047
+ grant_id: original.grant_id, execution_repeated: false, recovery_commands: [command], report_url: reportLink(context, id) }, 'The original execution permission is unavailable. No replacement permission or target was selected. Inspect the original jobs from their authorized terminal and recover retained evidence or cleanup there.\n'
1048
+ + command + `\nSimulation report: ${reportLink(context, id)}`);
1049
+ throw new CliError('campaign_permission_changed', 'The original local permission is unavailable. Use the original-job recovery handoff; existing work was not moved or restarted.');
1050
+ }
1051
+ }
1052
+ await sleep(1500, undefined, { signal: options.signal });
1053
+ current = await campaign(context, id, options.signal);
1054
+ if (current.execution_location !== location)
1055
+ throw new CliError('campaign_location_changed', 'The retained simulation uses a different execution location.');
1056
+ }
1057
+ output('campaign_finished', { ...current, report_url: reportLink(context, id) }, `Simulation ${current.state}. Report: ${reportLink(context, id)}`);
1058
+ }