firedrill-run 2.2.5 → 3.0.1
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.
- package/README.md +176 -22
- package/bin/firedrill.mjs +29 -5
- package/dist/assembly.json +13 -0
- package/dist/cli/runtime/candidate_copy.py +395 -0
- package/dist/cli/runtime/source_snapshot.py +391 -0
- package/dist/cli/src/administration.js +295 -0
- package/dist/cli/src/agent-changes.js +496 -0
- package/dist/cli/src/assessment-requests.js +67 -0
- package/dist/cli/src/auth.js +506 -0
- package/dist/cli/src/billing-commerce-contracts.js +1 -0
- package/dist/cli/src/billing-usage.js +227 -0
- package/dist/cli/src/billing-wallet-contracts.js +1 -0
- package/dist/cli/src/billing.js +170 -0
- package/dist/cli/src/bootstrap-process-lock.js +136 -0
- package/dist/cli/src/browser-links.js +14 -0
- package/dist/cli/src/campaign-controls.js +122 -0
- package/dist/cli/src/campaign-diagnoses.js +118 -0
- package/dist/cli/src/campaign-reports.js +1215 -0
- package/dist/cli/src/campaign-world.js +156 -0
- package/dist/cli/src/candidate-copy.js +376 -0
- package/dist/cli/src/catalog-contracts.js +1 -0
- package/dist/cli/src/ci-access-contracts.js +1 -0
- package/dist/cli/src/ci-configuration-contracts.js +1 -0
- package/dist/cli/src/ci-coverage.js +64 -0
- package/dist/cli/src/ci-delivery-contracts.js +1 -0
- package/dist/cli/src/ci-result-contracts.js +1 -0
- package/dist/cli/src/ci-run-contracts.js +1 -0
- package/dist/cli/src/ci-setup-proposals.js +52 -0
- package/dist/cli/src/company-policies.js +229 -0
- package/dist/cli/src/completion.js +79 -0
- package/dist/cli/src/configuration-intents.js +277 -0
- package/dist/cli/src/configuration.js +369 -0
- package/dist/cli/src/continuous-simulation.js +277 -0
- package/dist/cli/src/coordinated-checkpoints.js +133 -0
- package/dist/cli/src/credential-mode.js +139 -0
- package/dist/cli/src/credentials.js +331 -0
- package/dist/cli/src/credit-format.js +8 -0
- package/dist/cli/src/dataset-contracts.js +1 -0
- package/dist/cli/src/dataset-export.js +154 -0
- package/dist/cli/src/dataset-reads.js +37 -0
- package/dist/cli/src/dependency-preparation.js +239 -0
- package/dist/cli/src/director-tool-setup.js +104 -0
- package/dist/cli/src/driver-review.js +225 -0
- package/dist/cli/src/evaluation-authoring.js +315 -0
- package/dist/cli/src/evidence-bundles.js +367 -0
- package/dist/cli/src/execution-proposals.js +36 -0
- package/dist/cli/src/http.js +302 -0
- package/dist/cli/src/local-container-boundary.js +38 -0
- package/dist/cli/src/local-container-policy.js +201 -0
- package/dist/cli/src/local-execution-setup.js +81 -0
- package/dist/cli/src/local-source-basis.js +73 -0
- package/dist/cli/src/local-source.js +66 -0
- package/dist/cli/src/local-value-store.js +91 -0
- package/dist/cli/src/local-values.js +163 -0
- package/dist/cli/src/login-organizations.js +70 -0
- package/dist/cli/src/main.js +1627 -0
- package/dist/cli/src/mcp-connection.js +109 -0
- package/dist/cli/src/native/bindings.js +137 -0
- package/dist/cli/src/native/canonical.js +370 -0
- package/dist/cli/src/native/cloud-resources.js +942 -0
- package/dist/cli/src/native/companion-channel.js +421 -0
- package/dist/cli/src/native/companion-ipc.js +473 -0
- package/dist/cli/src/native/contracts.js +218 -0
- package/dist/cli/src/native/control-plane.js +1354 -0
- package/dist/cli/src/native/credit-approval.js +15 -0
- package/dist/cli/src/native/dependency-proxy.js +1006 -0
- package/dist/cli/src/native/drill-cleanup.js +115 -0
- package/dist/cli/src/native/drill-runner.js +1562 -0
- package/dist/cli/src/native/drivers/action-trace.js +210 -0
- package/dist/cli/src/native/drivers/function-wrapper.js +152 -0
- package/dist/cli/src/native/drivers/http.js +442 -0
- package/dist/cli/src/native/drivers/index.js +47 -0
- package/dist/cli/src/native/drivers/jsonl-process.js +538 -0
- package/dist/cli/src/native/drivers/playwright.js +600 -0
- package/dist/cli/src/native/drivers/signed-event.js +244 -0
- package/dist/cli/src/native/drivers/types.js +578 -0
- package/dist/cli/src/native/evidence-filter.js +255 -0
- package/dist/cli/src/native/isolated-access.js +241 -0
- package/dist/cli/src/native/library-sharing.js +220 -0
- package/dist/cli/src/native/local-run.js +667 -0
- package/dist/cli/src/native/local-secrets.js +19 -0
- package/dist/cli/src/native/mcp-config.js +123 -0
- package/dist/cli/src/native/metadata-requests.js +120 -0
- package/dist/cli/src/native/observations.js +1159 -0
- package/dist/cli/src/native/owned-groups.js +46 -0
- package/dist/cli/src/native/plan.js +594 -0
- package/dist/cli/src/native/png.js +159 -0
- package/dist/cli/src/native/ports.js +43 -0
- package/dist/cli/src/native/readiness.js +139 -0
- package/dist/cli/src/native/reassessments.js +206 -0
- package/dist/cli/src/native/redaction.js +235 -0
- package/dist/cli/src/native/registration.js +155 -0
- package/dist/cli/src/native/roots.js +110 -0
- package/dist/cli/src/native/run-cleanup.js +199 -0
- package/dist/cli/src/native/scratch.js +117 -0
- package/dist/cli/src/native/screenshots.js +60 -0
- package/dist/cli/src/native/session.js +556 -0
- package/dist/cli/src/native/standalone-proxy.js +580 -0
- package/dist/cli/src/native/standalone-renewals.js +183 -0
- package/dist/cli/src/native/standalone-state.js +193 -0
- package/dist/cli/src/native/standalone-tools.js +187 -0
- package/dist/cli/src/native/supervisor.js +831 -0
- package/dist/cli/src/native/target-auth.js +161 -0
- package/dist/cli/src/native/target-state.js +330 -0
- package/dist/cli/src/native/tool-reuse.js +364 -0
- package/dist/cli/src/native/tool-setups.js +336 -0
- package/dist/cli/src/native/world-reuse.js +288 -0
- package/dist/cli/src/onboarding.js +23 -0
- package/dist/cli/src/priority-requests.js +70 -0
- package/dist/cli/src/priority.js +223 -0
- package/dist/cli/src/prompts.js +184 -0
- package/dist/cli/src/real-operations.js +221 -0
- package/dist/cli/src/recipe-intents.js +58 -0
- package/dist/cli/src/recorded-context-intents.js +59 -0
- package/dist/cli/src/recorded-context.js +248 -0
- package/dist/cli/src/recordings.js +69 -0
- package/dist/cli/src/registration-guide.js +39 -0
- package/dist/cli/src/report-exports.js +213 -0
- package/dist/cli/src/request-input.js +47 -0
- package/dist/cli/src/resource-lifecycle.js +141 -0
- package/dist/cli/src/revision-requests.js +55 -0
- package/dist/cli/src/runner-abandon.js +53 -0
- package/dist/cli/src/runner-campaign-controls.js +262 -0
- package/dist/cli/src/runner-claim-reads.js +36 -0
- package/dist/cli/src/runner-completion.js +65 -0
- package/dist/cli/src/runner-configuration.js +546 -0
- package/dist/cli/src/runner-container.js +794 -0
- package/dist/cli/src/runner-evidence-erasure.js +351 -0
- package/dist/cli/src/runner-evidence-privacy.js +37 -0
- package/dist/cli/src/runner-evidence-retention.js +161 -0
- package/dist/cli/src/runner-evidence.js +326 -0
- package/dist/cli/src/runner-execute.js +92 -0
- package/dist/cli/src/runner-execution-deadline.js +25 -0
- package/dist/cli/src/runner-grants.js +307 -0
- package/dist/cli/src/runner-identity.js +615 -0
- package/dist/cli/src/runner-inputs.js +308 -0
- package/dist/cli/src/runner-jobs.js +49 -0
- package/dist/cli/src/runner-model-transport.js +392 -0
- package/dist/cli/src/runner-ownership.js +332 -0
- package/dist/cli/src/runner-process-lock.js +175 -0
- package/dist/cli/src/runner-runtime.js +396 -0
- package/dist/cli/src/runner-session-channel.js +377 -0
- package/dist/cli/src/runner-trial-completion.js +34 -0
- package/dist/cli/src/runner-trial-journal.js +358 -0
- package/dist/cli/src/runner-trial-live.js +393 -0
- package/dist/cli/src/runner-trial-protocol.js +289 -0
- package/dist/cli/src/runner-trial-sync.js +110 -0
- package/dist/cli/src/runner-trial-transfer.js +107 -0
- package/dist/cli/src/runner-voice.js +40 -0
- package/dist/cli/src/runner-world-claim.js +21 -0
- package/dist/cli/src/runner-world-collector.js +159 -0
- package/dist/cli/src/runner-world-command-evidence.js +487 -0
- package/dist/cli/src/runner-world-commands.js +268 -0
- package/dist/cli/src/runner-world-completion.js +76 -0
- package/dist/cli/src/runner-world-erasure.js +95 -0
- package/dist/cli/src/runner-world-evidence-sync.js +91 -0
- package/dist/cli/src/runner-world-journal.js +510 -0
- package/dist/cli/src/runner-world-native-filters.js +134 -0
- package/dist/cli/src/runner-world-observations.js +50 -0
- package/dist/cli/src/runner-world-session-protocol.js +134 -0
- package/dist/cli/src/saved-records.js +40 -0
- package/dist/cli/src/scenario-authoring.js +212 -0
- package/dist/cli/src/scenario-discovery.js +216 -0
- package/dist/cli/src/scenario-paths.js +68 -0
- package/dist/cli/src/scenario-requests.js +4 -0
- package/dist/cli/src/service-receipts.js +105 -0
- package/dist/cli/src/simple/authoring.js +270 -0
- package/dist/cli/src/simple/credit-approval.js +16 -0
- package/dist/cli/src/simple/help.js +98 -0
- package/dist/cli/src/simple/init.js +96 -0
- package/dist/cli/src/simple/library-sharing.js +414 -0
- package/dist/cli/src/simple/metadata-requests.js +46 -0
- package/dist/cli/src/simple/native-report.js +841 -0
- package/dist/cli/src/simple/native-tools.js +780 -0
- package/dist/cli/src/simple/plan-apply.js +506 -0
- package/dist/cli/src/simple/reassessment.js +323 -0
- package/dist/cli/src/simple/register.js +123 -0
- package/dist/cli/src/simple/run.js +122 -0
- package/dist/cli/src/simple/status.js +49 -0
- package/dist/cli/src/simple/tool-renewal.js +308 -0
- package/dist/cli/src/simple/tool-reuse.js +526 -0
- package/dist/cli/src/simple/tool-start-catalog.js +100 -0
- package/dist/cli/src/simple/tool-state.js +426 -0
- package/dist/cli/src/simple/tools-author.js +222 -0
- package/dist/cli/src/simple/watch.js +28 -0
- package/dist/cli/src/simple/world-reuse.js +586 -0
- package/dist/cli/src/simulation-intents.js +406 -0
- package/dist/cli/src/simulation-run.js +1058 -0
- package/dist/cli/src/source-batch.js +322 -0
- package/dist/cli/src/source-connection.js +451 -0
- package/dist/cli/src/source-runtime.js +346 -0
- package/dist/cli/src/source-selection-intents.js +75 -0
- package/dist/cli/src/source-sets.js +214 -0
- package/dist/cli/src/target-models.js +109 -0
- package/dist/cli/src/tool-catalog.js +224 -0
- package/dist/cli/src/tool-command-contracts.js +1 -0
- package/dist/cli/src/tool-command-export.js +245 -0
- package/dist/cli/src/tool-connection-contracts.js +1 -0
- package/dist/cli/src/tool-connection-reads.js +23 -0
- package/dist/cli/src/tool-contributions.js +58 -0
- package/dist/cli/src/tool-data.js +116 -0
- package/dist/cli/src/tool-datasets.js +176 -0
- package/dist/cli/src/tool-group-contracts.js +1 -0
- package/dist/cli/src/tool-group-reads.js +26 -0
- package/dist/cli/src/tool-group-state.js +147 -0
- package/dist/cli/src/tool-groups.js +311 -0
- package/dist/cli/src/tool-instances.js +186 -0
- package/dist/cli/src/tool-lifecycle-contracts.js +1 -0
- package/dist/cli/src/tool-lifecycle-reads.js +30 -0
- package/dist/cli/src/tool-lifecycle.js +323 -0
- package/dist/cli/src/tool-package-contracts.js +1 -0
- package/dist/cli/src/tool-package-reads.js +41 -0
- package/dist/cli/src/tool-records.js +53 -0
- package/dist/cli/src/tool-runtime.js +96 -0
- package/dist/cli/src/tool-state-contracts.js +1 -0
- package/dist/cli/src/tool-state-reads.js +31 -0
- package/dist/cli/src/tools.js +306 -0
- package/dist/cli/src/trial-actions.js +93 -0
- package/dist/cli/src/trial-checkpoints.js +133 -0
- package/dist/cli/src/trial-checks.js +70 -0
- package/dist/cli/src/trial-state.js +197 -0
- package/dist/cli/src/vault.js +266 -0
- package/dist/cli/src/workspace-registration.js +383 -0
- package/dist/cli/src/workspace-team-contracts.js +1 -0
- package/dist/cli/src/world-access.js +231 -0
- package/dist/cli/src/world-agent.js +4 -0
- package/dist/cli/src/world-checkpoint-scopes.js +15 -0
- package/dist/cli/src/world-contracts.js +6 -0
- package/dist/cli/src/world-creation.js +201 -0
- package/dist/cli/src/world-datasets.js +207 -0
- package/dist/cli/src/world-inspection.js +257 -0
- package/dist/cli/src/world-recipes.js +103 -0
- package/dist/cli/src/world-requests.js +241 -0
- package/dist/cli/src/world-session-checkpoints.js +196 -0
- package/dist/cli/src/world-session-lifecycle.js +207 -0
- package/dist/cli/src/world-session-requests.js +215 -0
- package/dist/cli/src/world-session-state.js +145 -0
- package/dist/cli/src/world-session-voice.js +66 -0
- package/dist/cli/src/world-sessions.js +288 -0
- package/dist/cli/src/world-stream.js +99 -0
- package/dist/cli/src/world-time-report.js +58 -0
- package/dist/contracts/agent-change-contracts.js +75 -0
- package/dist/contracts/agent-execution-contracts.js +38 -0
- package/dist/contracts/application-state-contracts.js +26 -0
- package/dist/contracts/assessment-aggregate-contracts.js +15 -0
- package/dist/contracts/assessment-contracts.js +74 -0
- package/dist/contracts/billing-commerce-contracts.js +107 -0
- package/dist/contracts/billing-wallet-contracts.js +43 -0
- package/dist/contracts/campaign-lifecycle.js +1 -0
- package/dist/contracts/campaign-world-contracts.js +80 -0
- package/dist/contracts/catalog-contracts.js +73 -0
- package/dist/contracts/ci-access-contracts.js +68 -0
- package/dist/contracts/ci-configuration-contracts.js +88 -0
- package/dist/contracts/ci-coverage-contracts.js +65 -0
- package/dist/contracts/ci-coverage-labels.js +25 -0
- package/dist/contracts/ci-delivery-contracts.js +61 -0
- package/dist/contracts/ci-result-contracts.js +103 -0
- package/dist/contracts/ci-run-contracts.js +51 -0
- package/dist/contracts/ci-setup-contracts.js +88 -0
- package/dist/contracts/clip-contracts.js +32 -0
- package/dist/contracts/comparison-contracts.js +53 -0
- package/dist/contracts/composite-contracts.js +34 -0
- package/dist/contracts/context-contracts.js +76 -0
- package/dist/contracts/continuation-report-contracts.js +1 -0
- package/dist/contracts/continuity-requirements.js +8 -0
- package/dist/contracts/coordinated-checkpoint-contracts.js +34 -0
- package/dist/contracts/dataset-contracts.js +88 -0
- package/dist/contracts/dependency-decision-contracts.js +36 -0
- package/dist/contracts/dependency-protocol-contracts.js +43 -0
- package/dist/contracts/director-execution.js +51 -0
- package/dist/contracts/driver-review-contracts.js +99 -0
- package/dist/contracts/evidence-contracts.js +65 -0
- package/dist/contracts/evidence-coverage-contracts.js +10 -0
- package/dist/contracts/export-contracts.js +44 -0
- package/dist/contracts/github-directory-contracts.js +96 -0
- package/dist/contracts/insight-contracts.js +11 -0
- package/dist/contracts/inspection-contracts.js +1 -0
- package/dist/contracts/local-claim-contracts.js +39 -0
- package/dist/contracts/local-configuration-contracts.js +7 -0
- package/dist/contracts/local-evidence-contracts.js +32 -0
- package/dist/contracts/local-grant-contracts.js +83 -0
- package/dist/contracts/local-input-contracts.js +19 -0
- package/dist/contracts/local-runner-contracts.js +42 -0
- package/dist/contracts/local-trial-live-contracts.js +16 -0
- package/dist/contracts/local-value-source-contracts.js +92 -0
- package/dist/contracts/local-world-native-contracts.js +8 -0
- package/dist/contracts/mcp-contracts.js +42 -0
- package/dist/contracts/media-contracts.js +28 -0
- package/dist/contracts/native-check-contracts.js +31 -0
- package/dist/contracts/native-identity-contracts.js +52 -0
- package/dist/contracts/onboarding-contracts.js +32 -0
- package/dist/contracts/performance-contracts.js +85 -0
- package/dist/contracts/planning-contracts.js +81 -0
- package/dist/contracts/real-operation-contracts.js +30 -0
- package/dist/contracts/recommendation-contracts.js +54 -0
- package/dist/contracts/report-contracts.js +1 -0
- package/dist/contracts/resource-lifecycle-contracts.js +57 -0
- package/dist/contracts/revision-contracts.js +12 -0
- package/dist/contracts/revision-request-contracts.js +39 -0
- package/dist/contracts/saved-record-contracts.js +76 -0
- package/dist/contracts/scenario-draft-comparison.js +91 -0
- package/dist/contracts/scenario-world-contracts.js +14 -0
- package/dist/contracts/service-contracts.js +1 -0
- package/dist/contracts/session-command-contracts.js +56 -0
- package/dist/contracts/session-contracts.js +65 -0
- package/dist/contracts/session-state-contracts.js +67 -0
- package/dist/contracts/session-voice-contracts.js +45 -0
- package/dist/contracts/source-set-contracts.js +26 -0
- package/dist/contracts/source-upload-contracts.js +22 -0
- package/dist/contracts/tool-command-contracts.js +57 -0
- package/dist/contracts/tool-connection-contracts.js +87 -0
- package/dist/contracts/tool-contribution-contracts.js +35 -0
- package/dist/contracts/tool-group-contracts.js +53 -0
- package/dist/contracts/tool-group-state-contracts.js +63 -0
- package/dist/contracts/tool-lifecycle-contracts.js +94 -0
- package/dist/contracts/tool-package-contracts.js +91 -0
- package/dist/contracts/tool-protocol-contracts.js +71 -0
- package/dist/contracts/tool-runtime-billing-contracts.js +28 -0
- package/dist/contracts/tool-setup-proposal.js +59 -0
- package/dist/contracts/tool-state-contracts.js +79 -0
- package/dist/contracts/trial-state-contracts.js +30 -0
- package/dist/contracts/typescript/agent-change.js +1 -0
- package/dist/contracts/typescript/assessment-aggregate.js +1 -0
- package/dist/contracts/typescript/assessment.js +1 -0
- package/dist/contracts/typescript/billing-commerce.js +1 -0
- package/dist/contracts/typescript/campaigns.js +1 -0
- package/dist/contracts/typescript/catalog.js +1 -0
- package/dist/contracts/typescript/ci-access.js +1 -0
- package/dist/contracts/typescript/ci-configuration.js +1 -0
- package/dist/contracts/typescript/ci-delivery.js +1 -0
- package/dist/contracts/typescript/ci-result.js +1 -0
- package/dist/contracts/typescript/ci-runs.js +1 -0
- package/dist/contracts/typescript/clips.js +1 -0
- package/dist/contracts/typescript/comparison.js +1 -0
- package/dist/contracts/typescript/composite.js +1 -0
- package/dist/contracts/typescript/connections.js +1 -0
- package/dist/contracts/typescript/context.js +1 -0
- package/dist/contracts/typescript/datasets.js +1 -0
- package/dist/contracts/typescript/evidence-coverage.js +1 -0
- package/dist/contracts/typescript/export.js +1 -0
- package/dist/contracts/typescript/github-directory.js +1 -0
- package/dist/contracts/typescript/insight.js +1 -0
- package/dist/contracts/typescript/inspection.js +1 -0
- package/dist/contracts/typescript/intake.js +1 -0
- package/dist/contracts/typescript/local-claim.js +1 -0
- package/dist/contracts/typescript/local-configuration.js +1 -0
- package/dist/contracts/typescript/local-evidence.js +1 -0
- package/dist/contracts/typescript/local-grant.js +1 -0
- package/dist/contracts/typescript/local-input.js +1 -0
- package/dist/contracts/typescript/local-runner.js +1 -0
- package/dist/contracts/typescript/local-trial-live.js +1 -0
- package/dist/contracts/typescript/local-value-source.js +1 -0
- package/dist/contracts/typescript/local-world-native.js +1 -0
- package/dist/contracts/typescript/mcp.js +1 -0
- package/dist/contracts/typescript/media.js +1 -0
- package/dist/contracts/typescript/native-check.js +1 -0
- package/dist/contracts/typescript/native-identity.js +1 -0
- package/dist/contracts/typescript/onboarding.js +1 -0
- package/dist/contracts/typescript/performance.js +1 -0
- package/dist/contracts/typescript/planning.js +1 -0
- package/dist/contracts/typescript/preparation.js +1 -0
- package/dist/contracts/typescript/real-operation.js +1 -0
- package/dist/contracts/typescript/recommendation.js +1 -0
- package/dist/contracts/typescript/reports.js +1 -0
- package/dist/contracts/typescript/resource-lifecycle.js +1 -0
- package/dist/contracts/typescript/saved-record.js +1 -0
- package/dist/contracts/typescript/scenarios.js +1 -0
- package/dist/contracts/typescript/services.js +1 -0
- package/dist/contracts/typescript/session-command.js +1 -0
- package/dist/contracts/typescript/session-state.js +1 -0
- package/dist/contracts/typescript/session-voice.js +1 -0
- package/dist/contracts/typescript/session.js +1 -0
- package/dist/contracts/typescript/tool-command.js +1 -0
- package/dist/contracts/typescript/tool-connection.js +1 -0
- package/dist/contracts/typescript/tool-contribution.js +1 -0
- package/dist/contracts/typescript/tool-groups.js +1 -0
- package/dist/contracts/typescript/tool-lifecycle.js +1 -0
- package/dist/contracts/typescript/tool-package.js +1 -0
- package/dist/contracts/typescript/tool-state.js +1 -0
- package/dist/contracts/typescript/trial-state.js +1 -0
- package/dist/contracts/typescript/vault.js +1 -0
- package/dist/contracts/typescript/workspace-team.js +1 -0
- package/dist/contracts/typescript/world-stream.js +1 -0
- package/dist/contracts/workspace-team-contracts.js +55 -0
- package/dist/contracts/world-command.js +65 -0
- package/dist/contracts/world-lifetime-contracts.js +74 -0
- package/dist/contracts/world-recipe-contracts.js +27 -0
- package/dist/contracts/world-stream-contracts.js +27 -0
- package/dist/package.json +1 -0
- package/package.json +16 -16
- package/LICENSE.md +0 -23
- package/PACKAGING.json +0 -50
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// `firedrill report <campaign> drill <drill> reassess ...`: a separately quoted,
|
|
2
|
+
// human-approved new evaluation of one finished drill's retained evidence.
|
|
3
|
+
// The agent is never run again and no local compute is charged. The drill's
|
|
4
|
+
// original result stays exactly as it was; each reassessment is its own attempt
|
|
5
|
+
// with its own criterion results, never merged into the original result or the
|
|
6
|
+
// campaign's quality figures.
|
|
7
|
+
//
|
|
8
|
+
// Root reviews it under a request key printed before anything is sent and
|
|
9
|
+
// freezes an estimate at once; nothing is reserved or evaluated until the human
|
|
10
|
+
// approves that exact estimate. A rerun with the printed key continues the
|
|
11
|
+
// original review, never a second one, and a lost approval is read back, never
|
|
12
|
+
// charged twice. A reviewed reassessment can be cancelled unused; an approved one
|
|
13
|
+
// cannot. Only the member who ran the drill can review one (root reads only that
|
|
14
|
+
// member's retained basis), only the member who reviewed one can approve or
|
|
15
|
+
// cancel it, and anyone in the workspace can read them.
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import { approveCredits } from './credit-approval.js';
|
|
18
|
+
import { CliError } from '../http.js';
|
|
19
|
+
import { selectOne, terminalText } from '../prompts.js';
|
|
20
|
+
import { getNativeEvaluationReport } from '../native/cloud-resources.js';
|
|
21
|
+
import { errorCode, isTransient, retryTransient } from '../native/control-plane.js';
|
|
22
|
+
import { admitReassessment, cancelReassessment, getReassessment, listReassessmentCriteria, listReassessments, reassessmentId, recoverReassessment, reviewReassessment, } from '../native/reassessments.js';
|
|
23
|
+
import { criterionLine, drillSlot, nativeCampaign, scopeOf } from './native-report.js';
|
|
24
|
+
import { amount } from './plan-apply.js';
|
|
25
|
+
/** The verbs this module owns under `report <campaign> drill <drill>`. */
|
|
26
|
+
export const REASSESSMENT_VERBS = new Set(['reassess', 'reassessments', 'reassessment', 'reassessment-request', 'cancel-reassessment']);
|
|
27
|
+
/** An estimate this close to expiry is not approved: approval would race it. */
|
|
28
|
+
const QUOTE_MARGIN_MS = 60_000;
|
|
29
|
+
const RECOVER_MS = 15_000;
|
|
30
|
+
/** Original evaluation states with a frozen basis and nothing left to evaluate. */
|
|
31
|
+
const FINISHED = new Set(['complete', 'needs_attention']);
|
|
32
|
+
const POLICY_REFUSALS = new Set(['native_evaluation_policy_required', 'assessment_configuration_required',
|
|
33
|
+
'native_funding_policy_unavailable', 'billing_credit_policy_unavailable']);
|
|
34
|
+
const policyRefusal = (error) => error instanceof CliError && error.httpStatus === 503 && POLICY_REFUSALS.has(error.code);
|
|
35
|
+
const uncertainResponse = (error) => !policyRefusal(error) && (isTransient(error)
|
|
36
|
+
|| ['request_failed', 'connection_uncertain', 'response_unavailable', 'native_response_unconfirmed'].includes(errorCode(error) ?? ''));
|
|
37
|
+
const SEPARATE_NOTE = 'A reassessment is a new evaluation of the drill\'s retained evidence: the agent is not run again and no local compute is '
|
|
38
|
+
+ 'charged. Its results are its own, kept apart from the drill\'s original result and the campaign\'s quality figures, and never combined with them. '
|
|
39
|
+
+ 'Each scored criterion is assessed 0-100 against its own threshold. A criterion without a score is not assessed; inspect its status for the reason. It is not a zero or an agent failure.';
|
|
40
|
+
/** Whether these `report` arguments name a reassessment command (checked before any request). */
|
|
41
|
+
export function reassessmentShape(args) {
|
|
42
|
+
return args[1] === 'drill' && args[2] !== undefined && REASSESSMENT_VERBS.has(args[3] ?? '');
|
|
43
|
+
}
|
|
44
|
+
function stateText(row) {
|
|
45
|
+
switch (row.state) {
|
|
46
|
+
case 'reviewed': return 'reviewed, not approved (nothing reserved or evaluated)';
|
|
47
|
+
case 'evaluating': return 'approved, evaluating';
|
|
48
|
+
case 'completed': return 'completed';
|
|
49
|
+
case 'cancelled': return row.basis_id === null ? 'cancelled before approval (nothing reserved or evaluated)'
|
|
50
|
+
: 'stopped by Firedrill after approval, before every criterion was evaluated (no results are shown for it)';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function evaluatorText(row) {
|
|
54
|
+
switch (row.profile_comparison) {
|
|
55
|
+
case 'same': return 'the same evaluator as the original result';
|
|
56
|
+
case 'different': return 'a different evaluator from the one behind the original result, so scores may differ for that reason too';
|
|
57
|
+
case 'unknown': return 'not comparable with the original result (it made no scoring request whose evaluator could be compared)';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const estimate = (row) => amount(row.quote.maximum_units, row.quote.credit_scale, row.quote.denomination);
|
|
61
|
+
function reassessmentText(drill, row) {
|
|
62
|
+
return [
|
|
63
|
+
`Reassessment ${row.id} of drill ${drill}: ${stateText(row)}`,
|
|
64
|
+
` ${row.criteria_count} criteria from the original evaluation ${row.source_basis_id}; evaluator: ${evaluatorText(row)}`,
|
|
65
|
+
` Request ${row.request_key}, reviewed ${terminalText(row.created_at)}${row.completed_at ? `, ended ${terminalText(row.completed_at)}` : ''}`,
|
|
66
|
+
` Estimate: up to ${estimate(row)} (estimate ${row.quote.id})`,
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
function quoteText(drill, row) {
|
|
70
|
+
return [
|
|
71
|
+
`Reassess drill ${drill}: a new evaluation of its ${row.criteria_count} criteria over its retained evidence.`,
|
|
72
|
+
'The agent is not run again and no local compute is charged. The original result stays as it is; this one is kept separately.',
|
|
73
|
+
`Evaluator: ${evaluatorText(row)}.`,
|
|
74
|
+
`Evaluation estimate (maximum, reserved only once approved): ${estimate(row)}`,
|
|
75
|
+
`Estimate expires: ${terminalText(row.quote.expires_at)}`,
|
|
76
|
+
].join('\n');
|
|
77
|
+
}
|
|
78
|
+
function pageNumber(value) {
|
|
79
|
+
if (value === undefined)
|
|
80
|
+
return undefined;
|
|
81
|
+
if (!/^[1-9][0-9]{0,6}$/.test(value))
|
|
82
|
+
throw new CliError('arguments_invalid', 'Use --page with a positive page number.', 2);
|
|
83
|
+
return Number(value);
|
|
84
|
+
}
|
|
85
|
+
const tty = (options) => !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
86
|
+
/** Root admits and cancels only the reviewer's own reassessment; after a successful read, its refusal says so. */
|
|
87
|
+
const reviewerOnly = (error) => {
|
|
88
|
+
if (errorCode(error) === 'native_reassessment_unavailable') {
|
|
89
|
+
throw new CliError('native_reassessment_owner_only', 'Only the member who reviewed this reassessment can approve or cancel it, and only while '
|
|
90
|
+
+ 'its original evaluation is retained. Nothing was reserved or changed.', 2, error instanceof CliError ? error.httpStatus : null);
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
};
|
|
94
|
+
/** Each verb's arguments and flags, checked before anything is read or sent. */
|
|
95
|
+
export function checkReassessmentArguments(args, options) {
|
|
96
|
+
const [, , , verb, value, sub] = args;
|
|
97
|
+
const takes = {
|
|
98
|
+
reassess: ['request', 'quote', 'approve', 'approveCredits'], reassessments: ['page'], reassessment: sub === 'results' ? ['page'] : [],
|
|
99
|
+
'reassessment-request': [], 'cancel-reassessment': ['approve'],
|
|
100
|
+
};
|
|
101
|
+
const flags = { request: '--request-key', quote: '--usage-quote',
|
|
102
|
+
approve: '--approve-assessment', approveCredits: '--approve-credits', page: '--page' };
|
|
103
|
+
const extra = Object.keys(flags)
|
|
104
|
+
.filter((flag) => options[flag] !== undefined && options[flag] !== false && !takes[verb]?.includes(flag));
|
|
105
|
+
if (extra.length)
|
|
106
|
+
throw new CliError('arguments_invalid', `report <campaign> drill <drill> ${verb} does not take ${extra.map((flag) => flags[flag]).join(', ')}.`, 2);
|
|
107
|
+
const valid = verb === 'reassess' || verb === 'reassessments' ? args.length === 4
|
|
108
|
+
: verb === 'reassessment' ? args.length === 5 || (args.length === 6 && sub === 'results')
|
|
109
|
+
: args.length === 5;
|
|
110
|
+
if (!valid) {
|
|
111
|
+
throw new CliError('arguments_invalid', 'Use report <campaign> drill <drill> reassess, reassessments, reassessment <reassessment-id> [results], '
|
|
112
|
+
+ 'reassessment-request <key> or cancel-reassessment <reassessment-id>.', 2);
|
|
113
|
+
}
|
|
114
|
+
if (verb === 'reassess' && options.quote !== undefined && options.request === undefined) {
|
|
115
|
+
throw new CliError('arguments_invalid', 'Use --usage-quote with the --request-key that was printed with it. Nothing was sent.', 2);
|
|
116
|
+
}
|
|
117
|
+
// Every id and page is checked here, before the workspace or campaign is read.
|
|
118
|
+
if (options.request !== undefined)
|
|
119
|
+
reassessmentId(options.request, 'request key');
|
|
120
|
+
if (options.quote !== undefined)
|
|
121
|
+
reassessmentId(options.quote, 'quote id');
|
|
122
|
+
if (value !== undefined)
|
|
123
|
+
reassessmentId(value, verb === 'reassessment-request' ? 'request key' : 'reassessment id');
|
|
124
|
+
pageNumber(options.page);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* `reassess [--request-key <key>] [--usage-quote <quote-id>] [--approve-assessment]`.
|
|
128
|
+
* The review is continued by its key; approval names the exact estimate the human saw.
|
|
129
|
+
*/
|
|
130
|
+
async function reassess(scope, drill, assignmentId, command, options, emit) {
|
|
131
|
+
const signal = options.signal;
|
|
132
|
+
const quoteGiven = options.quote === undefined ? undefined : reassessmentId(options.quote, 'quote id');
|
|
133
|
+
const requestKey = options.request === undefined ? randomUUID() : reassessmentId(options.request, 'request key');
|
|
134
|
+
const recovery = `${command} reassess --request-key ${requestKey}`;
|
|
135
|
+
const read = `${command} reassessment-request ${requestKey}`;
|
|
136
|
+
// A reassessment already reviewed under this key is continued, never reviewed again.
|
|
137
|
+
let row = options.request === undefined ? null : await recoverReassessment(scope, { assignmentId, requestKey, signal });
|
|
138
|
+
if (!row) {
|
|
139
|
+
const original = await getNativeEvaluationReport(scope, { assignmentId, signal });
|
|
140
|
+
if (original.basis_id === null || !FINISHED.has(original.status) || !original.criteria_total || original.pending !== 0) {
|
|
141
|
+
throw new CliError('native_reassessment_not_ready', 'Only a drill whose original evaluation has finished can be reassessed; this one is '
|
|
142
|
+
+ `${terminalText(original.status.replace(/_/g, ' '))}${original.pending ? ' with criteria still pending' : original.criteria_total ? '' : ' with no criteria evaluated'}. `
|
|
143
|
+
+ `Check it with ${command} results. Nothing was sent.`, 2);
|
|
144
|
+
}
|
|
145
|
+
emit('native_reassessment_request', { drill_id: drill, assignment_id: assignmentId, basis_id: original.basis_id, request_key: requestKey }, `Reassessment request: ${requestKey}\nIf interrupted, continue with ${recovery}. Do not start again under a new key.`);
|
|
146
|
+
try {
|
|
147
|
+
row = await reviewReassessment(scope, { assignmentId, basisId: original.basis_id, requestKey, signal });
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
// Root reviews only from the drill runner's own retained basis; the report above was readable, so this is who, or retention.
|
|
151
|
+
if (errorCode(error) === 'native_evaluation_basis_unavailable') {
|
|
152
|
+
throw new CliError('native_reassessment_runner_only', 'Only the member who ran this drill can reassess it, and only while its original '
|
|
153
|
+
+ 'evaluation evidence is retained. Nothing was reserved or evaluated.', 2, error instanceof CliError ? error.httpStatus : null);
|
|
154
|
+
}
|
|
155
|
+
// Only an unanswered (or cancelled) review may have landed; a refusal is reported as it is. A review reserves nothing.
|
|
156
|
+
if (policyRefusal(error) || (!signal.aborted && !uncertainResponse(error)))
|
|
157
|
+
throw error;
|
|
158
|
+
row = await recoverReassessment(scope, { assignmentId, requestKey, signal: AbortSignal.timeout(RECOVER_MS) }).catch(() => null);
|
|
159
|
+
if (!row) {
|
|
160
|
+
throw new CliError('native_reassessment_unconfirmed', `The reassessment request was not confirmed. Nothing is reserved by a review. `
|
|
161
|
+
+ `Read it with ${read}, or continue it with ${recovery}.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const reviewed = row;
|
|
166
|
+
if (reviewed.state !== 'reviewed') {
|
|
167
|
+
// Already approved or closed: shown as it is, never approved again.
|
|
168
|
+
emit('native_reassessment', reviewed, reassessmentText(drill, reviewed) + (reviewed.state === 'cancelled'
|
|
169
|
+
? '\nThis reassessment is closed. To reassess the drill, start a new one without --request-key.'
|
|
170
|
+
: `\nResults: ${command} reassessment ${reviewed.id} results`));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const cancel = `${command} cancel-reassessment ${reviewed.id}`;
|
|
174
|
+
if (Date.parse(reviewed.quote.expires_at) - Date.now() <= QUOTE_MARGIN_MS) {
|
|
175
|
+
throw new CliError('native_reassessment_quote_expired', 'This estimate has expired or expires within a minute. Nothing was reserved or evaluated. '
|
|
176
|
+
+ `Close this review with ${cancel} --approve-assessment, then start a new reassessment without --request-key.`, 2);
|
|
177
|
+
}
|
|
178
|
+
if (quoteGiven !== undefined && quoteGiven !== reviewed.quote.id) {
|
|
179
|
+
throw new CliError('idempotency_conflict', `That estimate is not this reassessment's (its estimate is ${reviewed.quote.id}). Nothing was reserved or evaluated.`, 2);
|
|
180
|
+
}
|
|
181
|
+
let approved = options.approve === true && (!reviewed.quote.credit_approval_required || quoteGiven === reviewed.quote.id);
|
|
182
|
+
emit('native_reassessment_quote', { ...reviewed, drill_id: drill }, quoteText(drill, reviewed));
|
|
183
|
+
if (!approved && tty(options)) {
|
|
184
|
+
approved = await selectOne('Reassess this drill?', [
|
|
185
|
+
{ value: false, label: 'Not now', detail: 'Nothing is reserved or evaluated. The review stays open until it is closed or its estimate expires.' },
|
|
186
|
+
{ value: true, label: 'Reassess it', detail: 'Reserves up to the estimate above and evaluates the retained evidence again. The agent is not run again.' },
|
|
187
|
+
], signal);
|
|
188
|
+
}
|
|
189
|
+
if (!approved) {
|
|
190
|
+
throw new CliError('assessment_consent_required', 'Nothing was reserved or evaluated. Show the human this estimate; after they approve it: '
|
|
191
|
+
+ `${recovery} --usage-quote ${reviewed.quote.id} --approve-assessment\nIf they decline, close the review: ${cancel} --approve-assessment`, 2);
|
|
192
|
+
}
|
|
193
|
+
if (!Number.isFinite(Date.parse(reviewed.quote.expires_at)) || Date.parse(reviewed.quote.expires_at) - Date.now() <= QUOTE_MARGIN_MS) {
|
|
194
|
+
throw new CliError('native_reassessment_quote_expired', 'This estimate expired or is too close to expiry after approval. Nothing was submitted. '
|
|
195
|
+
+ `Close this review with ${cancel} --approve-assessment, then review a new estimate.`, 2);
|
|
196
|
+
}
|
|
197
|
+
const creditApproval = await approveCredits([reviewed.quote], options, tty(options), `${recovery} --usage-quote ${reviewed.quote.id} --approve-assessment`);
|
|
198
|
+
let admitted;
|
|
199
|
+
try {
|
|
200
|
+
admitted = await retryTransient(() => admitReassessment(scope, { reassessment: reviewed, signal, creditApproval }), {
|
|
201
|
+
signal, attempts: 3, onTransient: (error) => { if (policyRefusal(error))
|
|
202
|
+
throw error; },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (signal.aborted)
|
|
207
|
+
throw error;
|
|
208
|
+
if (policyRefusal(error))
|
|
209
|
+
throw error;
|
|
210
|
+
// An unanswered approval may have landed: the reassessment says, and nothing is admitted or charged twice.
|
|
211
|
+
const current = await getReassessment(scope, { assignmentId, reassessmentId: reviewed.id, signal: AbortSignal.timeout(RECOVER_MS) }).catch(() => null);
|
|
212
|
+
if (!current || current.state === 'reviewed') {
|
|
213
|
+
if (error instanceof CliError && error.httpStatus !== null && !uncertainResponse(error))
|
|
214
|
+
throw error;
|
|
215
|
+
throw new CliError('native_reassessment_unconfirmed', `The approval was not confirmed. Check ${command} reassessment ${reviewed.id}; `
|
|
216
|
+
+ `if it is still reviewed, run ${recovery} --usage-quote ${reviewed.quote.id} --approve-assessment. It is never charged twice.`);
|
|
217
|
+
}
|
|
218
|
+
admitted = current;
|
|
219
|
+
}
|
|
220
|
+
emit('native_reassessment', admitted, reassessmentText(drill, admitted) + (admitted.state === 'cancelled'
|
|
221
|
+
? admitted.basis_id === null ? '\nIt was closed before approval, so nothing was evaluated.'
|
|
222
|
+
: '\nIt stopped after approval. Any confirmed evaluation usage remains recorded separately.'
|
|
223
|
+
: `\nThe new evaluation runs over the retained evidence only. Results: ${command} reassessment ${admitted.id} results`));
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* `report <campaign> drill <drill> reassess [--request-key <key>] [--usage-quote <quote-id>] [--approve-assessment]
|
|
227
|
+
* | reassessments [--page <n>] | reassessment <id> [results [--page <n>]] | reassessment-request <key>
|
|
228
|
+
* | cancel-reassessment <id> [--approve-assessment]`.
|
|
229
|
+
*/
|
|
230
|
+
export async function reassessmentCommand(args, options, emit) {
|
|
231
|
+
checkReassessmentArguments(args, options);
|
|
232
|
+
const [campaignId, , drillId, verb, value, sub] = args;
|
|
233
|
+
const signal = options.signal;
|
|
234
|
+
const scope = await scopeOf(options);
|
|
235
|
+
const campaign = await nativeCampaign(scope, campaignId, signal);
|
|
236
|
+
if (!campaign) {
|
|
237
|
+
throw new CliError('native_campaign_not_found', 'That is not a simulation in this workspace that can be reassessed. Find simulations with firedrill report.', 2);
|
|
238
|
+
}
|
|
239
|
+
const slot = await drillSlot(scope, campaign.campaign_id, drillId, signal);
|
|
240
|
+
const assignmentId = slot.assignment_id;
|
|
241
|
+
const drill = slot.drill_id ?? drillId;
|
|
242
|
+
const command = `firedrill report ${campaign.campaign_id} drill ${drill}`;
|
|
243
|
+
if (verb === 'reassess')
|
|
244
|
+
return reassess(scope, drill, assignmentId, command, options, emit);
|
|
245
|
+
if (verb === 'reassessments') {
|
|
246
|
+
const page = await listReassessments(scope, { assignmentId, page: pageNumber(options.page), signal });
|
|
247
|
+
emit('native_reassessments', { drill_id: drill, assignment_id: assignmentId, ...page }, [
|
|
248
|
+
`Reassessments of drill ${drill}, page ${page.page} (${page.count} in all):`,
|
|
249
|
+
...page.results.map((one) => ` ${one.id} ${stateText(one)} ${one.criteria_count} criteria ${terminalText(one.created_at)}`),
|
|
250
|
+
page.results.length ? null : ' none',
|
|
251
|
+
page.next ? `More: --page ${page.page + 1}` : null,
|
|
252
|
+
page.results.length ? `Read one: ${command} reassessment <reassessment-id> [results]` : null,
|
|
253
|
+
`The original result: ${command} results`,
|
|
254
|
+
`Reassess the drill: ${command} reassess`,
|
|
255
|
+
].filter((line) => line !== null).join('\n'));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (verb === 'reassessment-request') {
|
|
259
|
+
const requestKey = reassessmentId(value, 'request key');
|
|
260
|
+
const row = await recoverReassessment(scope, { assignmentId, requestKey, signal });
|
|
261
|
+
if (!row) {
|
|
262
|
+
emit('native_reassessment_request', { drill_id: drill, assignment_id: assignmentId, request_key: requestKey, state: 'not_found' }, `None of your reassessments of this drill is recorded under request ${requestKey}. An unanswered request may still arrive; `
|
|
263
|
+
+ `a review reserves nothing. Continue it with ${command} reassess --request-key ${requestKey}.`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
emit('native_reassessment', row, reassessmentText(drill, row));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const id = reassessmentId(value, 'reassessment id');
|
|
270
|
+
const row = await getReassessment(scope, { assignmentId, reassessmentId: id, signal }).catch((error) => {
|
|
271
|
+
if (error instanceof CliError && error.httpStatus === 404) {
|
|
272
|
+
throw new CliError('native_reassessment_not_found', `That is not a reassessment of this drill. List them with ${command} reassessments.`, 2);
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
});
|
|
276
|
+
if (verb === 'reassessment' && sub === 'results') {
|
|
277
|
+
const page = pageNumber(options.page);
|
|
278
|
+
const criteria = await listReassessmentCriteria(scope, { assignmentId, reassessmentId: row.id, page, signal });
|
|
279
|
+
const shown = criteria.page;
|
|
280
|
+
emit('native_reassessment_results', { reassessment: row, criteria }, [
|
|
281
|
+
reassessmentText(drill, row),
|
|
282
|
+
...criteria.results.length ? [
|
|
283
|
+
`Criteria of this reassessment, page ${shown} (${criteria.count} in all):`,
|
|
284
|
+
...criteria.results.map((one) => criterionLine(one, true)),
|
|
285
|
+
criteria.next ? `More: --page ${shown + 1}` : null,
|
|
286
|
+
] : [row.state === 'reviewed' ? `No results: it has not been approved. Approve it with ${command} reassess --request-key ${row.request_key}`
|
|
287
|
+
: row.state === 'cancelled' ? 'No results are shown for a cancelled reassessment.' : 'No criterion results yet.'],
|
|
288
|
+
`The original result: ${command} results`,
|
|
289
|
+
SEPARATE_NOTE,
|
|
290
|
+
].filter((line) => line !== null).join('\n'));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (verb === 'reassessment' || row.state !== 'reviewed') {
|
|
294
|
+
emit('native_reassessment', row, reassessmentText(drill, row) + (row.state === 'reviewed'
|
|
295
|
+
? `\nApprove it: ${command} reassess --request-key ${row.request_key}\nClose it unused: ${command} cancel-reassessment ${row.id}`
|
|
296
|
+
: verb === 'cancel-reassessment' ? `\nThis reassessment is ${stateText(row)}, so there is nothing to cancel; nothing was sent.`
|
|
297
|
+
: row.state === 'cancelled' ? '' : `\nResults: ${command} reassessment ${row.id} results`));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
let approved = options.approve === true;
|
|
301
|
+
if (!approved && tty(options)) {
|
|
302
|
+
approved = await selectOne('Cancel this unused reassessment review?', [
|
|
303
|
+
{ value: false, label: 'Not now', detail: 'Nothing is sent.' },
|
|
304
|
+
{ value: true, label: 'Cancel it', detail: 'Its estimate can no longer be approved. Nothing was reserved or evaluated for it.' },
|
|
305
|
+
], signal);
|
|
306
|
+
}
|
|
307
|
+
if (!approved)
|
|
308
|
+
throw new CliError('assessment_consent_required', `Nothing was sent. After the human approves: ${command} cancel-reassessment ${row.id} --approve-assessment`, 2);
|
|
309
|
+
// Cancelling is idempotent; an approved reassessment is refused by root and read back, never undone.
|
|
310
|
+
let closed;
|
|
311
|
+
try {
|
|
312
|
+
closed = await retryTransient(() => cancelReassessment(scope, { assignmentId, reassessmentId: row.id, signal }), { signal, attempts: 3 });
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
if (errorCode(error) !== 'native_reassessment_already_admitted')
|
|
316
|
+
reviewerOnly(error);
|
|
317
|
+
closed = await getReassessment(scope, { assignmentId, reassessmentId: row.id, signal });
|
|
318
|
+
}
|
|
319
|
+
emit('native_reassessment', closed, reassessmentText(drill, closed) + (closed.state === 'cancelled'
|
|
320
|
+
? closed.basis_id === null ? '\nCancelled before approval. Nothing was reserved or evaluated.'
|
|
321
|
+
: '\nStopped after approval. Any confirmed evaluation usage remains recorded separately.'
|
|
322
|
+
: `\nIt had already been approved, so nothing was cancelled or undone. Results: ${command} reassessment ${closed.id} results`));
|
|
323
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { CliError } from '../http.js';
|
|
2
|
+
import { terminalText } from '../prompts.js';
|
|
3
|
+
import { selectedWorkspace } from '../world-access.js';
|
|
4
|
+
import { declaredAgentName, declaredNameRecorded, getNativeDescriptor, listNativeDescriptors, registerNativeAgent, registrationKey, reviseNativeDescriptor } from '../native/registration.js';
|
|
5
|
+
import { normalizeDescriptor } from '../native/canonical.js';
|
|
6
|
+
import { answerLost, recoverLostWrite } from '../native/metadata-requests.js';
|
|
7
|
+
import { metadataRequestCommand } from './metadata-requests.js';
|
|
8
|
+
import { readJsonInput } from './plan-apply.js';
|
|
9
|
+
const unconfirmed = () => new CliError('native_registration_unconfirmed', 'The recovered registration does not match this descriptor. Nothing else was sent.', 2);
|
|
10
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
function registeredText(receipt) {
|
|
12
|
+
return `Agent ${receipt.agent_id}.\n${revisionText(receipt.descriptor)}`;
|
|
13
|
+
}
|
|
14
|
+
const scopeOf = (options) => selectedWorkspace({ workspace: options.workspace, signal: options.signal,
|
|
15
|
+
interactive: !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY, openBrowser: options.openBrowser && !options.json });
|
|
16
|
+
function revisionText(row) {
|
|
17
|
+
return [`Agent: ${row.agent_id}`, `Descriptor revision ${row.revision}: ${row.id}`, `Digest: ${row.descriptor_sha256}`,
|
|
18
|
+
`Serial execution required: ${row.requires_serial_execution ? 'yes' : 'no'}`, `Recorded: ${terminalText(row.created_at)}`,
|
|
19
|
+
...(row.caller_attribution ? [`Recorded from: ${row.caller_attribution.channel === 'management_mcp' ? 'remote MCP' : row.caller_attribution.channel}`
|
|
20
|
+
+ (row.caller_attribution.declared_agent_name ? `, by a coding agent that called itself ${terminalText(row.caller_attribution.declared_agent_name)} (self-reported, not verified)` : '')] : []),
|
|
21
|
+
...(row.startup_order ? [`Startup order: ${row.startup_order.map(terminalText).join(', ')}`] : [])].join('\n');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Registers declared target metadata (no source upload, no paths, no secrets,
|
|
25
|
+
* nothing executes). Without --agent it creates a local agent; with --agent it
|
|
26
|
+
* records the next immutable descriptor revision.
|
|
27
|
+
*/
|
|
28
|
+
export async function registerCommand(args, options, emit) {
|
|
29
|
+
if (args[0] === 'request' || args[0] === 'withdraw-request') {
|
|
30
|
+
// The original first registration of an agent, by its request key. A descriptor revision's request is under `descriptors`.
|
|
31
|
+
if (args.length !== 2 || options.descriptor !== undefined || options.name !== undefined || options.agent !== undefined
|
|
32
|
+
|| options.expectedRevision !== undefined || options.request !== undefined)
|
|
33
|
+
throw new CliError('arguments_invalid', 'Use firedrill register request|withdraw-request <request-key>. For a descriptor revision use firedrill descriptors request|withdraw-request <request-key> --agent <id>.', 2);
|
|
34
|
+
if (options.declaredAgentName !== undefined)
|
|
35
|
+
throw new CliError('arguments_invalid', '--declared-agent-name applies to a registration or revision, not to reading or withdrawing one.', 2);
|
|
36
|
+
const scope = await scopeOf(options);
|
|
37
|
+
await metadataRequestCommand(args[0], { kind: 'agent', requestKey: args[1] }, scope, options, emit, registeredText, `firedrill register withdraw-request ${args[1].toLowerCase()}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (options.approve !== undefined)
|
|
41
|
+
throw new CliError('arguments_invalid', '--approve-change applies to register withdraw-request only. Nothing was submitted.', 2);
|
|
42
|
+
const revising = options.agent !== undefined;
|
|
43
|
+
if (args.length || !options.descriptor || (revising ? options.name !== undefined || options.expectedRevision === undefined
|
|
44
|
+
: options.name === undefined || options.expectedRevision !== undefined))
|
|
45
|
+
throw new CliError('arguments_invalid', 'Use firedrill register --descriptor <path|-> --name <name>, or --descriptor <path|-> --agent <id> --expected-revision <n> for a new revision. Nothing was submitted.', 2);
|
|
46
|
+
if (revising && !/^(0|[1-9][0-9]{0,8})$/.test(options.expectedRevision ?? ''))
|
|
47
|
+
throw new CliError('native_revision_invalid', 'Use the latest descriptor revision number. Nothing was submitted.', 2);
|
|
48
|
+
// Checked before anything is read or sent; a replay with --request must repeat the same name (or none).
|
|
49
|
+
const declared = declaredAgentName(options.declaredAgentName);
|
|
50
|
+
const descriptor = JSON.parse(await readJsonInput(options.descriptor, 'the local descriptor', options.signal));
|
|
51
|
+
const requestKey = registrationKey(options.request);
|
|
52
|
+
const scope = await scopeOf(options);
|
|
53
|
+
// Announce the key before any request so an interrupted attempt can be replayed exactly.
|
|
54
|
+
if (options.json)
|
|
55
|
+
emit('request_prepared', { request_key: requestKey, declared_agent_name: declared ?? null }, '');
|
|
56
|
+
else
|
|
57
|
+
process.stderr.write(`Request key: ${requestKey} (rerun with --request ${requestKey}${declared === undefined ? '' : ' and the same --declared-agent-name'} to replay this exact registration)\n`);
|
|
58
|
+
// A lost answer is read back by its key: never sent again here, never under a new key.
|
|
59
|
+
const digest = () => normalizeDescriptor(descriptor).descriptor_sha256;
|
|
60
|
+
if (revising) {
|
|
61
|
+
let row;
|
|
62
|
+
try {
|
|
63
|
+
row = await reviseNativeDescriptor(scope, { agentId: options.agent, expectedRevision: Number(options.expectedRevision),
|
|
64
|
+
descriptor, requestKey, declaredAgentName: declared, signal: options.signal });
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!answerLost(error) || options.signal.aborted || !UUID.test(options.agent))
|
|
68
|
+
throw error;
|
|
69
|
+
const recovered = await recoverLostWrite(scope, { kind: 'descriptor', requestKey, agentId: options.agent }, { replay: `the same command with --request ${requestKey}`, withdraw: `firedrill descriptors withdraw-request ${requestKey} --agent ${options.agent.toLowerCase()}` });
|
|
70
|
+
if (recovered.revision !== Number(options.expectedRevision) + 1 || recovered.descriptor_sha256 !== digest()
|
|
71
|
+
|| !declaredNameRecorded(recovered, declared))
|
|
72
|
+
throw unconfirmed();
|
|
73
|
+
row = { ...recovered, request_key: requestKey };
|
|
74
|
+
}
|
|
75
|
+
emit('native_descriptor', row, `${revisionText(row)}\nRequest key: ${row.request_key}\nMetadata only. Nothing was uploaded or started.`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
let receipt;
|
|
79
|
+
try {
|
|
80
|
+
receipt = await registerNativeAgent(scope, { name: options.name, descriptor, requestKey, declaredAgentName: declared, signal: options.signal });
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (!answerLost(error) || options.signal.aborted)
|
|
84
|
+
throw error;
|
|
85
|
+
const recovered = await recoverLostWrite(scope, { kind: 'agent', requestKey }, { replay: `the same command with --request ${requestKey}`, withdraw: `firedrill register withdraw-request ${requestKey}` });
|
|
86
|
+
if (recovered.descriptor.descriptor_sha256 !== digest() || !declaredNameRecorded(recovered.descriptor, declared))
|
|
87
|
+
throw unconfirmed();
|
|
88
|
+
// The receipt contains the descriptor, not the original registration name.
|
|
89
|
+
// A lost conflict for a changed name must not be reported as a new success.
|
|
90
|
+
emit('native_request', { kind: 'agent', request_key: requestKey, state: 'committed', receipt: recovered,
|
|
91
|
+
submitted_change_confirmed: false }, `Recovered original agent ${recovered.agent_id}.\n${revisionText(recovered.descriptor)}\nRequest key: ${requestKey}\nThe submitted registration name is not confirmed. Inspect the original agent before continuing.`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
emit('native_agent', receipt, `Registered agent ${receipt.agent_id}.\n${revisionText(receipt.descriptor)}\nRequest key: ${receipt.request_key}`
|
|
95
|
+
+ '\nMetadata only. Nothing was uploaded or started. Prepare and approve a cloud campaign, then use firedrill run --campaign <campaign-id> --plan <file> --descriptor <file>.');
|
|
96
|
+
}
|
|
97
|
+
export async function descriptorsCommand(args, options, emit) {
|
|
98
|
+
if (args[0] === 'request' || args[0] === 'withdraw-request') {
|
|
99
|
+
// One descriptor revision's original request, by its key, for the agent it was recorded on.
|
|
100
|
+
if (args.length !== 2 || !options.agent || options.descriptor !== undefined || options.page !== undefined)
|
|
101
|
+
throw new CliError('arguments_invalid', 'Use firedrill descriptors request|withdraw-request <request-key> --agent <id>.', 2);
|
|
102
|
+
const scope = await scopeOf(options);
|
|
103
|
+
await metadataRequestCommand(args[0], { kind: 'descriptor', requestKey: args[1], agentId: options.agent }, scope, options, emit, revisionText, `firedrill descriptors withdraw-request ${args[1].toLowerCase()} --agent ${options.agent.toLowerCase()}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (options.approve !== undefined)
|
|
107
|
+
throw new CliError('arguments_invalid', '--approve-change applies to descriptors withdraw-request only.', 2);
|
|
108
|
+
if (args.length || !options.agent || options.descriptor !== undefined && options.page !== undefined)
|
|
109
|
+
throw new CliError('arguments_invalid', 'Use firedrill descriptors --agent <id> [--page <n>], or --agent <id> --descriptor <id>.', 2);
|
|
110
|
+
if (options.page !== undefined && !/^[1-9][0-9]{0,8}$/.test(options.page))
|
|
111
|
+
throw new CliError('native_page_invalid', 'Use a page number of 1 or more.', 2);
|
|
112
|
+
const scope = await scopeOf(options);
|
|
113
|
+
if (options.descriptor !== undefined) {
|
|
114
|
+
const row = await getNativeDescriptor(scope, { agentId: options.agent, descriptorId: options.descriptor, signal: options.signal });
|
|
115
|
+
emit('native_descriptor', row, revisionText(row));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const page = Number(options.page ?? '1');
|
|
119
|
+
const result = await listNativeDescriptors(scope, { agentId: options.agent, page, signal: options.signal });
|
|
120
|
+
emit('native_descriptors', result, [result.results.map(row => `Revision ${row.revision} ${row.id} ${row.descriptor_sha256} ${terminalText(row.created_at)}`).join('\n')
|
|
121
|
+
|| 'No descriptor revisions for this agent.', `Page ${page}, ${result.count} revisions.`,
|
|
122
|
+
...(result.next ? [`Next: firedrill descriptors --agent ${options.agent} --page ${page + 1}`] : [])].join('\n'));
|
|
123
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { CliError } from '../http.js';
|
|
2
|
+
import { login } from '../auth.js';
|
|
3
|
+
import { credentialMode, isCredentialCompanion, loadCredentials } from '../credentials.js';
|
|
4
|
+
import { terminalText } from '../prompts.js';
|
|
5
|
+
import { selectedWorkspace } from '../world-access.js';
|
|
6
|
+
import { approvePlan, parsePlan, planDigest } from '../native/plan.js';
|
|
7
|
+
import { localSecretBindings } from '../native/local-secrets.js';
|
|
8
|
+
import { normalizeDescriptor } from '../native/canonical.js';
|
|
9
|
+
import { httpControlPlane } from '../native/control-plane.js';
|
|
10
|
+
import { detectCapabilities } from '../native/drivers/index.js';
|
|
11
|
+
import { runCompanion } from '../native/session.js';
|
|
12
|
+
import { LOCAL_RUN_ERRORS, runOwnedAliases } from '../native/local-run.js';
|
|
13
|
+
import { readJsonInput } from './plan-apply.js';
|
|
14
|
+
import { enrollLocalRunner, nativeExecutorKey, withNativeExecutor } from '../runner-identity.js';
|
|
15
|
+
// Release wiring supplies the packaged version; there is no version constant in source yet.
|
|
16
|
+
const CLIENT_VERSION = 'development';
|
|
17
|
+
/**
|
|
18
|
+
* Approve the local plan for this session and run the companion until the
|
|
19
|
+
* signal stops it. Commands come only from the approved plan; the cloud learns
|
|
20
|
+
* only the plan digest. SIGINT/SIGTERM abort the signal, and the companion
|
|
21
|
+
* stops the processes it owns before returning.
|
|
22
|
+
*/
|
|
23
|
+
export async function runCommand(args, options, emit) {
|
|
24
|
+
if (args.length || !options.plan || !options.descriptor)
|
|
25
|
+
throw new CliError('arguments_invalid', 'Use firedrill run --plan <path|-> --descriptor <path|-> --campaign <id>, with --approve for automation.', 2);
|
|
26
|
+
// A run follows one campaign over the runner device's single channel with the service.
|
|
27
|
+
if (options.campaign === undefined)
|
|
28
|
+
throw new CliError('native_campaign_required', 'Use --campaign with the campaign id Firedrill printed (campaign_created): a run follows one campaign\'s drills. Nothing was started.', 2);
|
|
29
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(options.campaign))
|
|
30
|
+
throw new CliError('arguments_invalid', 'Use --campaign with the campaign id Firedrill printed. Nothing was started.', 2);
|
|
31
|
+
if (options.enrollDevice && (credentialMode() !== 'session' || options.runner !== undefined))
|
|
32
|
+
throw new CliError('arguments_invalid', 'Use --enroll-device with run only in session-only mode and without --runner; otherwise enroll a runner for this terminal sign-in with firedrill runner enroll. Nothing was started.', 2);
|
|
33
|
+
if (options.plan === '-' && options.descriptor === '-')
|
|
34
|
+
throw new CliError('arguments_invalid', 'Read only one of --plan or --descriptor from standard input. Nothing was started.', 2);
|
|
35
|
+
const stdinUsed = options.plan === '-' || options.descriptor === '-';
|
|
36
|
+
const interactive = !options.json && !stdinUsed && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
37
|
+
const plan = parsePlan(await readJsonInput(options.plan, 'the local plan', options.signal));
|
|
38
|
+
let descriptorValue;
|
|
39
|
+
try {
|
|
40
|
+
descriptorValue = JSON.parse(await readJsonInput(options.descriptor, 'the local descriptor', options.signal));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof CliError)
|
|
44
|
+
throw error;
|
|
45
|
+
throw new CliError('request_file_invalid', 'The descriptor is not valid JSON. Nothing was started.', 2);
|
|
46
|
+
}
|
|
47
|
+
const normalized = normalizeDescriptor(descriptorValue);
|
|
48
|
+
if (plan.descriptor_sha256 !== normalized.descriptor_sha256)
|
|
49
|
+
throw new CliError('native_descriptor_mismatch', 'The local plan was written for a different descriptor. Nothing was started.', 2);
|
|
50
|
+
const digest = planDigest(plan);
|
|
51
|
+
const approval = await approvePlan(plan, normalized.descriptor, { interactive, approve: options.approve === true, signal: options.signal,
|
|
52
|
+
output: text => { if (!options.json)
|
|
53
|
+
process.stderr.write(text); } });
|
|
54
|
+
for (const service of plan.services)
|
|
55
|
+
localSecretBindings(service);
|
|
56
|
+
emit('plan_approved', { plan_sha256: digest, descriptor_sha256: normalized.descriptor_sha256 }, `Local plan approved for this session.\nPlan: ${digest}`);
|
|
57
|
+
// A run that became the companion signs in itself when no credential is held
|
|
58
|
+
// (always true in session-only mode; also on first use in persistent mode).
|
|
59
|
+
const held = isCredentialCompanion() ? await loadCredentials().catch((error) => {
|
|
60
|
+
// login() reports the platform remediation for an unavailable OS store.
|
|
61
|
+
if (error instanceof CliError && error.code === 'credential_store_unavailable')
|
|
62
|
+
return null;
|
|
63
|
+
throw error;
|
|
64
|
+
}) : true;
|
|
65
|
+
if (!held) {
|
|
66
|
+
const session = await login({ json: options.json, open: options.openBrowser, explicitOpen: options.explicitOpen === true, signal: options.signal });
|
|
67
|
+
const mode = credentialMode();
|
|
68
|
+
emit('signed_in', { mode }, mode === 'session'
|
|
69
|
+
? `Signed in for this session only as ${terminalText(session.user.email)}.`
|
|
70
|
+
: `Signed in as ${terminalText(session.user.email)}; the credential is kept in the OS store.`);
|
|
71
|
+
}
|
|
72
|
+
const scope = await selectedWorkspace({ workspace: options.workspace, interactive, signal: options.signal,
|
|
73
|
+
openBrowser: options.openBrowser && !options.json });
|
|
74
|
+
const capabilities = { client_version: CLIENT_VERSION, platform: process.platform, arch: process.arch,
|
|
75
|
+
drivers: await detectCapabilities(plan), max_parallel_drills: plan.max_parallel_drills, isolation: 'native-trusted' };
|
|
76
|
+
emit('companion_capabilities', capabilities, ['Drivers:', ...capabilities.drivers.map(one => ` ${one.profile}: ${one.available ? 'available' : `unavailable${one.reason ? ` (${terminalText(one.reason)})` : ''}`}`),
|
|
77
|
+
'Local targets run as trusted native processes, not in a sandbox.'].join('\n'));
|
|
78
|
+
// The enrolled runner device executes and signs for this companion; enrollment stays its own consented step.
|
|
79
|
+
let runner;
|
|
80
|
+
try {
|
|
81
|
+
runner = await nativeExecutorKey(scope, options.runner, options.signal);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
// Runners are bound to one native session, and session-only sign-in keeps nothing in the OS store (the headless
|
|
85
|
+
// case it exists for). So with no runner for this session, a session-only run enrolls one whose private key stays
|
|
86
|
+
// in this process's memory. It needs the same consent as runner enroll: a prompt, or --enroll-device.
|
|
87
|
+
if (!(error instanceof CliError) || error.code !== 'runner_required' || options.runner !== undefined || credentialMode() !== 'session')
|
|
88
|
+
throw error;
|
|
89
|
+
// The campaign's shared services can be closed only by the runner that registered them, and this one's key
|
|
90
|
+
// would end with this process: refused before any runner is enrolled or anything starts.
|
|
91
|
+
const shared = runOwnedAliases(plan);
|
|
92
|
+
if (shared.length) {
|
|
93
|
+
throw new CliError(LOCAL_RUN_ERRORS.persistent_runner_required, `This plan runs shared services for the whole campaign `
|
|
94
|
+
+ `(${shared.map(terminalText).join(', ')}). Only the runner device that started them can report how they ended and close them, `
|
|
95
|
+
+ 'and a session-only runner\'s key exists only in this run\'s memory, so they could never be closed after it ends. '
|
|
96
|
+
+ 'Ask the human to enroll a runner whose key this machine keeps (firedrill runner enroll; it needs the OS credential store), '
|
|
97
|
+
+ 'then rerun, or make these services per drill (per_drill: true in the plan). Nothing was enrolled and no drill was started.', 2);
|
|
98
|
+
}
|
|
99
|
+
const receipt = await enrollLocalRunner(scope, { json: options.json, openBrowser: options.openBrowser, signal: options.signal,
|
|
100
|
+
enroll: options.enrollDevice === true || undefined, name: 'Session-only runner', ephemeral: true }, emit).catch((enrollError) => {
|
|
101
|
+
// Without a prompt, the flag stands in for the person's consent, so the message says to ask them first.
|
|
102
|
+
if (!(enrollError instanceof CliError) || enrollError.code !== 'runner_consent_required')
|
|
103
|
+
throw enrollError;
|
|
104
|
+
throw new CliError('runner_consent_required', 'No runner is enrolled for this session-only sign-in. Ask the person first; only after they agree, '
|
|
105
|
+
+ 'rerun with --enroll-device to enroll one whose key stays in this run\'s memory. Nothing was enrolled and no drill was started.', 2);
|
|
106
|
+
});
|
|
107
|
+
if (['not_retained', 'revoked', 'expired', 'access_removed'].includes(receipt.state)) {
|
|
108
|
+
throw new CliError('runner_required', `The session-only runner ${receipt.device_key} is not usable (${receipt.state.replaceAll('_', ' ')}). `
|
|
109
|
+
+ 'Its key existed only in this process; run again to enroll a new one. No drill was started.', 2);
|
|
110
|
+
}
|
|
111
|
+
runner = receipt.device_key;
|
|
112
|
+
}
|
|
113
|
+
emit('runner_selected', { device_key: runner, campaign_id: options.campaign.toLowerCase() }, `Runner device ${runner} runs the drills of campaign ${options.campaign.toLowerCase()}: those offered to it and the campaign's waiting ones.`);
|
|
114
|
+
// runCompanion reports its own native_companion_stopped result and stops the processes it owns.
|
|
115
|
+
await withNativeExecutor(scope, runner, options.signal, executor => runCompanion({
|
|
116
|
+
controlPlane: httpControlPlane({ workspace: scope.workspace, actor: scope.owner, executor,
|
|
117
|
+
campaign: options.campaign ? { id: options.campaign.toLowerCase(), descriptor_sha256: normalized.descriptor_sha256 } : null }),
|
|
118
|
+
plan, descriptor: normalized.descriptor, campaignId: options.campaign?.toLowerCase() ?? null,
|
|
119
|
+
capabilities, signal: options.signal, output: emit, approval
|
|
120
|
+
}));
|
|
121
|
+
options.signal.throwIfAborted();
|
|
122
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { api, credentialOverview } from '../auth.js';
|
|
2
|
+
import { CliError } from '../http.js';
|
|
3
|
+
import { terminalText } from '../prompts.js';
|
|
4
|
+
import { detectCapabilities } from '../native/drivers/index.js';
|
|
5
|
+
import { parsePlan, processControlLimitations } from '../native/plan.js';
|
|
6
|
+
import { readJsonInput } from './plan-apply.js';
|
|
7
|
+
export async function machineReport(signal, planPath) {
|
|
8
|
+
const credentials = await credentialOverview(signal);
|
|
9
|
+
// Without a plan, drivers report machine-level availability only; a plan adds its own checks.
|
|
10
|
+
const plan = planPath ? parsePlan(await readJsonInput(planPath, 'the local plan', signal)) : null;
|
|
11
|
+
const drivers = await detectCapabilities(plan);
|
|
12
|
+
return { platform: process.platform, arch: process.arch, node: process.version, isolation: 'native-trusted', credentials, drivers,
|
|
13
|
+
drivers_checked_against_plan: plan !== null, limitations: processControlLimitations() };
|
|
14
|
+
}
|
|
15
|
+
export function machineText(value) {
|
|
16
|
+
return [
|
|
17
|
+
`Machine: ${value.platform} ${value.arch}, Node ${value.node}. Local targets run as trusted native processes, not in a sandbox.`,
|
|
18
|
+
...value.limitations,
|
|
19
|
+
`Credentials: ${value.credentials.mode === 'session' ? 'session-only (memory)' : 'persistent (OS credential store)'}, ${value.credentials.signed_in ? 'signed in' : 'not signed in'}`,
|
|
20
|
+
...(value.credentials.store && !value.credentials.store.available
|
|
21
|
+
? [`Credential store: unavailable (${value.credentials.store.backend})${value.credentials.store.remediation ? `\n${value.credentials.store.remediation}` : ''}`]
|
|
22
|
+
: value.credentials.store ? [`Credential store: ${value.credentials.store.backend}`] : []),
|
|
23
|
+
`Companion: ${value.credentials.companion_process ? 'this process' : value.credentials.companion
|
|
24
|
+
? `running (pid ${value.credentials.companion.pid}, ${value.credentials.companion.mode === 'session' ? 'session-only' : 'persistent'})` : 'not running'}`,
|
|
25
|
+
`Drivers${value.drivers_checked_against_plan ? ' (checked against the plan)' : ' (machine only; add --plan <path|-> for plan checks)'}:\n`
|
|
26
|
+
+ value.drivers.map(one => ` ${one.profile}: ${one.available ? 'available' : `unavailable${one.reason ? ` (${one.reason})` : ''}`}`).join('\n'),
|
|
27
|
+
].map(line => line.split('\n').map(terminalText).join('\n'));
|
|
28
|
+
}
|
|
29
|
+
export async function statusCommand(args, options, emit) {
|
|
30
|
+
if (args.length)
|
|
31
|
+
throw new CliError('arguments_invalid', 'Use firedrill status with optional --plan <path|-> and --json.', 2);
|
|
32
|
+
const machine = await machineReport(options.signal, options.plan);
|
|
33
|
+
let session = null;
|
|
34
|
+
if (machine.credentials.signed_in) {
|
|
35
|
+
try {
|
|
36
|
+
session = await api('/api/auth/native/session', { signal: options.signal });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (!(error instanceof CliError) || error.code !== 'authentication_required')
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
emit('status', { signed_in: session !== null, account: session ? { id: session.user.id, email: session.user.email } : null,
|
|
44
|
+
workspace: session?.workspace ? { id: session.workspace.id, name: session.workspace.name } : null, machine }, [
|
|
45
|
+
session ? `Account: ${terminalText(session.user.email)}` : 'Account: not signed in. Run firedrill init.',
|
|
46
|
+
`Workspace: ${session?.workspace ? terminalText(session.workspace.name) : 'Not selected'}`,
|
|
47
|
+
...machineText(machine),
|
|
48
|
+
].join('\n'));
|
|
49
|
+
}
|