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,156 @@
|
|
|
1
|
+
import { api } from './auth.js';
|
|
2
|
+
import { CliError } from './http.js';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { selectOne, terminalText } from './prompts.js';
|
|
5
|
+
import { requestInput } from './request-input.js';
|
|
6
|
+
import { identifier } from './world-contracts.js';
|
|
7
|
+
import { drillWorldPageMatches, runtimeRenewalPageMatches, worldControlInputMatches, worldControlMatches } from '../../contracts/campaign-world-contracts.js';
|
|
8
|
+
const eventKinds = { record: 'State change', operation: 'Service operation', ingress: 'Incoming event', message: 'Conversation' };
|
|
9
|
+
function elapsedTime(value) {
|
|
10
|
+
const milliseconds = BigInt(value), seconds = milliseconds / 1000n;
|
|
11
|
+
if (milliseconds < 1000n)
|
|
12
|
+
return `${milliseconds}ms`;
|
|
13
|
+
const days = seconds / 86400n;
|
|
14
|
+
return `${days ? `${days}d ` : ''}${seconds / 3600n % 24n}h ${seconds / 60n % 60n}m ${seconds % 60n}s`;
|
|
15
|
+
}
|
|
16
|
+
function eventOutcome(event) {
|
|
17
|
+
const value = event.result?.observation;
|
|
18
|
+
if (!value)
|
|
19
|
+
return event.state.replaceAll('_', ' ');
|
|
20
|
+
if (event.kind === 'operation')
|
|
21
|
+
return typeof value.http_status === 'number' ? `HTTP ${value.http_status}`
|
|
22
|
+
: value.mcp_is_error === true ? 'Tool returned an error'
|
|
23
|
+
: value.schema_status === 'mismatch' || value.schema_status === 'missing_structured' ? 'Response schema differs' : 'Tool response recorded';
|
|
24
|
+
if (event.kind === 'ingress')
|
|
25
|
+
return value.completion === 'failed' ? 'Task failed' : value.completion === 'rejected' ? 'Task rejected' : 'Task completed';
|
|
26
|
+
return 'Conversation turn recorded';
|
|
27
|
+
}
|
|
28
|
+
export async function campaignWorldReport(prefix, scope, options, output) {
|
|
29
|
+
if (options.page !== undefined && !/^[1-9][0-9]{0,8}$/.test(options.page)
|
|
30
|
+
|| (options.search?.length ?? 0) > 200 || options.status !== undefined
|
|
31
|
+
&& !['pending', 'applying', 'applied', 'unmet', 'uncertain', 'canceled'].includes(options.status))
|
|
32
|
+
throw new CliError('arguments_invalid', 'Use world-events with --page, --search and an event --status.', 2);
|
|
33
|
+
const value = await api(`${prefix}/world-events?page=${options.page ?? '1'}&q=${encodeURIComponent(options.search ?? '')}&state=${options.status ?? ''}`, { actor: scope.owner, workspace: scope.workspace, signal: options.signal });
|
|
34
|
+
if (!drillWorldPageMatches(value) || value.results.some(one => one.result?.download_path
|
|
35
|
+
&& one.result.download_path !== `${prefix}/evidence/${one.result.evidence_id}/bytes`))
|
|
36
|
+
throw new CliError('world_events_unavailable', 'The original world history could not be confirmed.');
|
|
37
|
+
const c = value.continuity;
|
|
38
|
+
const parts = prefix.split('/'), campaign = parts.at(-3), execution = parts.at(-1);
|
|
39
|
+
output(value, [c.mode ? `Continuous world: ${c.state}` : 'No continuous world schedule selected.',
|
|
40
|
+
...(c.started_at ? [`World started: ${c.started_at}`] : []),
|
|
41
|
+
...(c.interaction ? [`Last input activity: ${c.interaction.state === 'waiting_for_input' ? 'Waiting for next input' : c.interaction.state === 'active' ? 'Handling input' : 'Input channel closed'} at ${c.interaction.observed_at}`] : []),
|
|
42
|
+
...(c.elapsed_ms !== null ? [`Actual elapsed: ${elapsedTime(c.elapsed_ms)}`] : []),
|
|
43
|
+
...(c.event_state !== 'active' ? [`Future events: ${c.event_state === 'paused' ? 'Paused' : 'Canceled'}${c.state === 'running' ? '; original execution remains active' : ''}`] : []),
|
|
44
|
+
...(c.resource_deadline_at ? [`Execution deadline: ${c.resource_deadline_at}`] : []),
|
|
45
|
+
...(c.next_event ? [`Next event: ${c.next_event.key}${c.next_event.due_at ? ` at ${c.next_event.due_at}` : ''}`] : []),
|
|
46
|
+
...value.results.map(one => `${terminalText(one.key)} ${eventKinds[one.kind]} ${eventOutcome(one)} ${one.due_at ?? `after ${one.due_after_ms}ms`}`
|
|
47
|
+
+ (one.result?.download_path ? `\nEvidence: firedrill report ${campaign} drill ${execution} evidence ${one.result.evidence_id}` : '')),
|
|
48
|
+
`Page ${value.page}; ${value.count} events.`, ...(value.next ? ['More events are available with --page.'] : [])].join('\n'));
|
|
49
|
+
}
|
|
50
|
+
export async function campaignRuntimeReport(prefix, scope, options, output) {
|
|
51
|
+
if (options.page !== undefined && !/^[1-9][0-9]{0,8}$/.test(options.page))
|
|
52
|
+
throw new CliError('arguments_invalid', 'Choose a valid runtime-history page.', 2);
|
|
53
|
+
const value = await api(`${prefix}/runtime-renewals?page=${options.page ?? '1'}`, { actor: scope.owner, signal: options.signal });
|
|
54
|
+
if (!runtimeRenewalPageMatches(value))
|
|
55
|
+
throw new CliError('runtime_history_unavailable', 'The original runtime history could not be confirmed.');
|
|
56
|
+
output(value, value.count ? [...value.results.map(one => `${one.sequence} ${one.state} ${one.confirmed_expires_at ?? one.requested_expires_at}`),
|
|
57
|
+
`Page ${value.page}; ${value.count} renewals.`].join('\n') : 'No allocation renewals recorded.');
|
|
58
|
+
}
|
|
59
|
+
const actions = { pause: 'pause', resume: 'resume', 'cancel-future': 'cancel_future', 'revise-future': 'revise_future' };
|
|
60
|
+
const actionLabels = { pause: 'Pause future events', resume: 'Resume future events', cancel_future: 'Cancel future events', revise_future: 'Replace future events' };
|
|
61
|
+
const stateLabels = { queued: 'Requested', publishing: 'Sending', unknown: 'Awaiting confirmation', observed: 'Applied', rejected: 'Not applied', ended: 'Not applied' };
|
|
62
|
+
const commandName = (value) => `${actionLabels[value.action]}: ${stateLabels[value.state]}\nOriginal request: ${value.request_key}${value.problem ? '\n' + terminalText(value.problem) : ''}`;
|
|
63
|
+
export async function campaignWorldCommand(args, prefix, scope, options, output) {
|
|
64
|
+
const action = args[0] ?? 'list', interactive = !options.json && Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
65
|
+
const parts = prefix.split('/');
|
|
66
|
+
const campaign = parts[parts.length - 3], execution = parts[parts.length - 1];
|
|
67
|
+
identifier(campaign);
|
|
68
|
+
identifier(execution);
|
|
69
|
+
const selectedScope = { workspace: scope.workspace, campaign, execution };
|
|
70
|
+
if (args.length > (action === 'request' ? 2 : 1) || action === 'request' && !args[1]
|
|
71
|
+
|| !['list', 'controls', 'request', 'runtime', ...Object.keys(actions)].includes(action)
|
|
72
|
+
|| options.page !== undefined && !['list', 'controls', 'runtime'].includes(action)
|
|
73
|
+
|| options.search !== undefined && action !== 'list' || options.status !== undefined && action !== 'list'
|
|
74
|
+
|| options.file !== undefined && !actions[action] || options.change && !actions[action]
|
|
75
|
+
|| options.requestKey !== undefined && !actions[action])
|
|
76
|
+
throw new CliError('arguments_invalid', 'Use world-events [list|controls|runtime|request <original-key>|pause|resume|cancel-future|revise-future]. Mutations support --file, --request-key and --approve-change.', 2);
|
|
77
|
+
if (action === 'list')
|
|
78
|
+
return campaignWorldReport(prefix, scope, options, output);
|
|
79
|
+
if (action === 'runtime')
|
|
80
|
+
return campaignRuntimeReport(prefix, scope, options, output);
|
|
81
|
+
if (action === 'request') {
|
|
82
|
+
const key = identifier(args[1]);
|
|
83
|
+
const value = await api(`${prefix}/world-controls/requests/${key}`, { actor: scope.owner, signal: options.signal });
|
|
84
|
+
if (value.state === 'not_received') {
|
|
85
|
+
if (value.request_key !== key || Object.keys(value).sort().join(',') !== 'request_key,state')
|
|
86
|
+
throw new CliError('world_control_unavailable', 'The original request could not be confirmed.');
|
|
87
|
+
return output(value, 'The original request has not been received. Retry only its exact original body and key.');
|
|
88
|
+
}
|
|
89
|
+
if (!worldControlMatches(value, { ...selectedScope, actor: scope.owner }, { key }))
|
|
90
|
+
throw new CliError('world_control_unavailable', 'The original world control could not be confirmed.');
|
|
91
|
+
return output(value, commandName(value));
|
|
92
|
+
}
|
|
93
|
+
if (action === 'controls') {
|
|
94
|
+
if (options.page !== undefined && !/^[1-9][0-9]{0,8}$/.test(options.page))
|
|
95
|
+
throw new CliError('arguments_invalid', 'Choose a valid history page.', 2);
|
|
96
|
+
const value = await api(`${prefix}/world-controls?page=${options.page ?? '1'}`, { actor: scope.owner, signal: options.signal });
|
|
97
|
+
if (!Number.isSafeInteger(value.count) || value.count < 0 || !Array.isArray(value.results) || value.results.length > 100
|
|
98
|
+
|| !value.results.every(row => worldControlMatches(row, selectedScope)))
|
|
99
|
+
throw new CliError('world_control_unavailable', 'World control history could not be confirmed.');
|
|
100
|
+
return output(value, [...value.results.map(commandName), `Page ${value.page}; ${value.count} controls.`].join('\n'));
|
|
101
|
+
}
|
|
102
|
+
let body;
|
|
103
|
+
if (options.file) {
|
|
104
|
+
const value = JSON.parse(await requestInput(options.file, options.signal, 4096));
|
|
105
|
+
if (!worldControlInputMatches(value) || value.action !== actions[action])
|
|
106
|
+
throw new CliError('world_control_input_invalid', 'Provide this exact action, previous revision, expected watermark and optional next scenario revision.', 2);
|
|
107
|
+
body = value;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
if (!interactive)
|
|
111
|
+
throw new CliError('interaction_required', 'Automation needs the exact reviewed --file body, --request-key and --approve-change.', 2);
|
|
112
|
+
const current = await api(`${prefix}/world-events?page=1`, { actor: scope.owner, signal: options.signal });
|
|
113
|
+
if (!drillWorldPageMatches(current) || current.continuity.state !== 'running' || !current.continuity.revision)
|
|
114
|
+
throw new CliError('world_control_not_ready', 'This drill has no current continuous world.');
|
|
115
|
+
let candidate = null;
|
|
116
|
+
if (action === 'revise-future') {
|
|
117
|
+
let page = 1;
|
|
118
|
+
while (candidate === null) {
|
|
119
|
+
const rows = await api(`${prefix}/world-controls/candidates?page=${page}`, { actor: scope.owner, signal: options.signal });
|
|
120
|
+
if (!Number.isSafeInteger(rows.count) || !Array.isArray(rows.results) || rows.results.length > 100 || !rows.results.every(row => typeof row.id === 'string' && typeof row.name === 'string' && Number.isSafeInteger(row.revision) && row.revision > 0))
|
|
121
|
+
throw new CliError('world_revisions_unavailable', 'Available revisions could not be confirmed.');
|
|
122
|
+
if (!rows.count)
|
|
123
|
+
throw new CliError('world_revisions_unavailable', 'Save a reviewed scenario revision with new future events first.');
|
|
124
|
+
const choices = rows.results.map(row => ({ value: { id: identifier(row.id) },
|
|
125
|
+
label: row.name, detail: `Revision ${row.revision}; ${row.event_count} events` }));
|
|
126
|
+
if (page > 1)
|
|
127
|
+
choices.unshift({ value: { page: page - 1 }, label: 'Previous page', detail: '' });
|
|
128
|
+
if (rows.next)
|
|
129
|
+
choices.push({ value: { page: page + 1 }, label: 'Next page', detail: '' });
|
|
130
|
+
const selected = await selectOne('Choose the reviewed future timeline', choices, options.signal);
|
|
131
|
+
if ('id' in selected)
|
|
132
|
+
candidate = selected.id;
|
|
133
|
+
else
|
|
134
|
+
page = selected.page;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
body = { action: actions[action], previous_revision: current.continuity.revision, expected_watermark: current.continuity.applied_watermark,
|
|
138
|
+
expected_sequence: current.continuity.control_sequence, next_scenario_id: candidate };
|
|
139
|
+
}
|
|
140
|
+
const approved = options.change || interactive && await selectOne('Change future events only. The agent keeps running.', [
|
|
141
|
+
{ value: false, label: 'Cancel', detail: 'Leave the current timeline unchanged.' },
|
|
142
|
+
{ value: true, label: action === 'revise-future' ? 'Replace future events' : `${action} events`, detail: 'Applied history and original runtime permission stay unchanged.' },
|
|
143
|
+
], options.signal);
|
|
144
|
+
if (!approved)
|
|
145
|
+
throw new CliError('approval_required', 'Nothing was changed. Automation requires --approve-change.', 2);
|
|
146
|
+
if (!interactive && !options.requestKey)
|
|
147
|
+
throw new CliError('request_key_required', 'Keep a fixed --request-key for original-request recovery.', 2);
|
|
148
|
+
const key = options.requestKey ? identifier(options.requestKey) : randomUUID();
|
|
149
|
+
if (interactive)
|
|
150
|
+
process.stderr.write(`Original request: ${terminalText(key)}\nRecover with report ${campaign} drill ${execution} world-events request ${key}\n`);
|
|
151
|
+
const value = await api(`${prefix}/world-controls`, { actor: scope.owner, signal: options.signal, method: 'POST',
|
|
152
|
+
workspace: scope.workspace, idempotencyKey: key, body });
|
|
153
|
+
if (!worldControlMatches(value, { ...selectedScope, actor: scope.owner }, { key, body }))
|
|
154
|
+
throw new CliError('world_control_unavailable', `Inspect original request ${key} before another action.`);
|
|
155
|
+
output(value, commandName(value));
|
|
156
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { workspaceLink } from './browser-links.js';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { lstat, mkdir, open, opendir, realpath, rename, rmdir, unlink } from 'node:fs/promises';
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { api } from './auth.js';
|
|
9
|
+
import { privateDirectory } from './credentials.js';
|
|
10
|
+
import { CliError } from './http.js';
|
|
11
|
+
import { trustedPython } from './local-source.js';
|
|
12
|
+
import { selectOne } from './prompts.js';
|
|
13
|
+
import { bindSavedProject } from './source-connection.js';
|
|
14
|
+
import { processLifetimeLock } from './runner-process-lock.js';
|
|
15
|
+
import { identifier } from './world-contracts.js';
|
|
16
|
+
import { candidateSourceMatches, sourceChangeMatches, patchSourceFileMatches } from '../../contracts/agent-change-contracts.js';
|
|
17
|
+
import { nativeMetadataCanonical } from '../../contracts/local-value-source-contracts.js';
|
|
18
|
+
const hash = /^[a-f0-9]{64}$/;
|
|
19
|
+
const path = (scope, campaign, change) => `/api/workspaces/${identifier(scope.workspace)}/campaigns/${identifier(campaign)}/source-changes/${identifier(change)}`;
|
|
20
|
+
const auth = (scope, options) => ({ actor: scope.owner, workspace: scope.workspace, signal: options.signal });
|
|
21
|
+
async function folder(scope, key) {
|
|
22
|
+
const root = join(await privateDirectory(), `candidate-copy-${identifier(scope.owner)}-${identifier(scope.workspace)}-${identifier(key)}`);
|
|
23
|
+
await mkdir(root, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST')
|
|
24
|
+
throw error; });
|
|
25
|
+
const stat = await lstat(root);
|
|
26
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || process.getuid && stat.uid !== process.getuid()
|
|
27
|
+
|| process.platform !== 'win32' && !!(stat.mode & 0o077))
|
|
28
|
+
throw new CliError('candidate_copy_cache_unsafe', 'Use a private candidate-copy request directory owned by this OS user.');
|
|
29
|
+
return root;
|
|
30
|
+
}
|
|
31
|
+
async function owned(path, maximum) {
|
|
32
|
+
const before = await lstat(path);
|
|
33
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > maximum || process.getuid && before.uid !== process.getuid()
|
|
34
|
+
|| process.platform !== 'win32' && !!(before.mode & 0o077))
|
|
35
|
+
throw new CliError('candidate_copy_cache_unsafe', 'The private candidate-copy input cannot be confirmed.');
|
|
36
|
+
const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
37
|
+
const actual = await file.stat();
|
|
38
|
+
if (actual.dev !== before.dev || actual.ino !== before.ino || actual.size !== before.size || actual.mtimeMs !== before.mtimeMs) {
|
|
39
|
+
await file.close();
|
|
40
|
+
throw new CliError('candidate_copy_cache_changed', 'The private candidate input changed. No existing destination file was overwritten.');
|
|
41
|
+
}
|
|
42
|
+
return file;
|
|
43
|
+
}
|
|
44
|
+
async function savedIntent(root, scope, key) {
|
|
45
|
+
const file = await owned(join(root, 'intent.json'), 65536);
|
|
46
|
+
try {
|
|
47
|
+
const value = JSON.parse(await file.readFile('utf8'));
|
|
48
|
+
if (value.schema !== 'firedrill.native-candidate-copy.v1' || value.key !== key || value.owner !== scope.owner || value.workspace !== scope.workspace
|
|
49
|
+
|| identifier(value.campaign) !== value.campaign || identifier(value.change) !== value.change || !hash.test(value.scope_sha256)
|
|
50
|
+
|| !hash.test(value.draft_sha256) || value.download_source_consent !== true || typeof value.destination !== 'string'
|
|
51
|
+
|| !isAbsolute(value.destination) || value.destination !== resolve(value.destination))
|
|
52
|
+
throw new Error();
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new CliError('candidate_copy_request_invalid', 'The exact candidate-copy request cannot be confirmed. It was not submitted.');
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await file.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function identity(value, intent) {
|
|
63
|
+
if (!candidateSourceMatches(value, intent.workspace, intent.campaign, intent.change)
|
|
64
|
+
|| value.scope_sha256 !== intent.scope_sha256 || value.draft_sha256 !== intent.draft_sha256
|
|
65
|
+
|| ![value.files_sha256, value.snapshot?.sha256].every(one => typeof one === 'string' && hash.test(one))
|
|
66
|
+
|| !/^[1-9][0-9]{0,30}$/.test(value.snapshot.generation) || value.target_executed !== false || value.vault_access_inherited !== false
|
|
67
|
+
|| !Object.values(value.capacity ?? {}).every(one => Number.isSafeInteger(one) && one > 0)
|
|
68
|
+
|| ['files', 'file_bytes', 'expanded_bytes', 'archive_bytes', 'directory_bytes'].some(key => !(key in value.capacity))
|
|
69
|
+
|| !Number.isSafeInteger(value.file_count) || value.file_count < 1 || value.file_count > value.capacity.files
|
|
70
|
+
|| !Number.isSafeInteger(value.expanded_bytes) || value.expanded_bytes < 0 || value.expanded_bytes > value.capacity.expanded_bytes
|
|
71
|
+
|| !Number.isSafeInteger(value.snapshot.size) || value.snapshot.size < 1 || value.snapshot.size > value.capacity.archive_bytes) {
|
|
72
|
+
throw new CliError('candidate_copy_identity_invalid', 'The saved candidate source identity cannot be confirmed.');
|
|
73
|
+
}
|
|
74
|
+
for (const id of [value.source_id, value.world_id, value.agent_id, value.snapshot.id])
|
|
75
|
+
identifier(id);
|
|
76
|
+
}
|
|
77
|
+
async function writeMetadata(root, name, value) {
|
|
78
|
+
const temporary = join(root, name + '.' + randomUUID() + '.tmp');
|
|
79
|
+
const file = await open(temporary, 'wx', 0o600);
|
|
80
|
+
try {
|
|
81
|
+
await file.writeFile(JSON.stringify(value));
|
|
82
|
+
await file.sync();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await file.close();
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
await rename(temporary, join(root, name));
|
|
89
|
+
const directory = await open(root, constants.O_RDONLY | constants.O_DIRECTORY | (constants.O_NOFOLLOW ?? 0));
|
|
90
|
+
try {
|
|
91
|
+
await directory.sync();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await directory.close();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await unlink(temporary).catch(() => undefined);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function download(root, value, signed, signal) {
|
|
102
|
+
const existing = await owned(join(root, 'source.zip'), value.capacity.archive_bytes).catch(error => {
|
|
103
|
+
if (error.code === 'ENOENT')
|
|
104
|
+
return null;
|
|
105
|
+
throw error;
|
|
106
|
+
});
|
|
107
|
+
if (existing) {
|
|
108
|
+
try {
|
|
109
|
+
const hashed = createHash('sha256');
|
|
110
|
+
let bytes = 0;
|
|
111
|
+
for await (const chunk of existing.createReadStream({ autoClose: false })) {
|
|
112
|
+
signal.throwIfAborted();
|
|
113
|
+
bytes += chunk.length;
|
|
114
|
+
hashed.update(chunk);
|
|
115
|
+
}
|
|
116
|
+
if (bytes !== value.snapshot.size || hashed.digest('hex') !== value.snapshot.sha256)
|
|
117
|
+
throw new CliError('candidate_copy_cache_changed', 'The retained candidate archive changed. It was not overwritten.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await existing.close();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const url = new URL(signed);
|
|
125
|
+
if (url.protocol !== 'https:' || url.hostname !== 'storage.googleapis.com' || url.port || url.username || url.password || url.hash) {
|
|
126
|
+
throw new CliError('candidate_copy_destination_refused', 'The candidate download destination is not allowed.');
|
|
127
|
+
}
|
|
128
|
+
const temporary = join(root, 'source.' + randomUUID() + '.tmp');
|
|
129
|
+
const file = await open(temporary, 'wx', 0o600);
|
|
130
|
+
try {
|
|
131
|
+
const response = await fetch(url, { credentials: 'omit', redirect: 'error', referrerPolicy: 'no-referrer', signal: AbortSignal.any([signal, AbortSignal.timeout(300000)]) });
|
|
132
|
+
if (!response.ok || !response.body)
|
|
133
|
+
throw new CliError('candidate_copy_download_incomplete', 'The candidate download did not finish. Resume the original copy request.');
|
|
134
|
+
const hashed = createHash('sha256');
|
|
135
|
+
let bytes = 0;
|
|
136
|
+
const reader = response.body.getReader();
|
|
137
|
+
try {
|
|
138
|
+
while (true) {
|
|
139
|
+
const part = await reader.read();
|
|
140
|
+
if (part.done)
|
|
141
|
+
break;
|
|
142
|
+
bytes += part.value.length;
|
|
143
|
+
if (bytes > value.snapshot.size)
|
|
144
|
+
throw new CliError('candidate_copy_integrity_failed', 'The candidate download exceeds its exact archive size.');
|
|
145
|
+
hashed.update(part.value);
|
|
146
|
+
let at = 0;
|
|
147
|
+
while (at < part.value.length) {
|
|
148
|
+
const written = await file.write(part.value, at, part.value.length - at);
|
|
149
|
+
if (!written.bytesWritten)
|
|
150
|
+
throw new Error();
|
|
151
|
+
at += written.bytesWritten;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
await reader.cancel().catch(() => undefined);
|
|
157
|
+
reader.releaseLock();
|
|
158
|
+
}
|
|
159
|
+
if (bytes !== value.snapshot.size || hashed.digest('hex') !== value.snapshot.sha256)
|
|
160
|
+
throw new CliError('candidate_copy_integrity_failed', 'The candidate archive does not match its retained size and hash.');
|
|
161
|
+
await file.sync();
|
|
162
|
+
// The source is private request cache, not the user destination. Competing
|
|
163
|
+
// copies are serialized by the cache lock below.
|
|
164
|
+
await file.close();
|
|
165
|
+
await rename(temporary, join(root, 'source.zip'));
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await file.close().catch(() => undefined);
|
|
169
|
+
await unlink(temporary).catch(() => undefined);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function copyFiles(root, intent, options) {
|
|
173
|
+
if (process.platform === 'win32')
|
|
174
|
+
throw new CliError('candidate_copy_host_unsupported', 'Candidate copying currently requires a POSIX host with no-follow directory operations. No destination files were changed.', 2);
|
|
175
|
+
const runtime = await trustedPython(await realpath(process.cwd()), options.signal, !options.json);
|
|
176
|
+
const within = relative(intent.destination, runtime);
|
|
177
|
+
if (!isAbsolute(within) && within !== '..' && !within.startsWith('..' + sep))
|
|
178
|
+
throw new CliError('python_runtime_refused', 'Use Python installed outside the candidate folder.', 2);
|
|
179
|
+
const script = fileURLToPath(new URL('../runtime/candidate_copy.py', import.meta.url));
|
|
180
|
+
return new Promise((accept, refuse) => {
|
|
181
|
+
const chunks = [];
|
|
182
|
+
let bytes = 0;
|
|
183
|
+
const child = spawn(runtime, ['-I', '-B', script, '--cache', root], { shell: false, windowsHide: true,
|
|
184
|
+
env: Object.fromEntries(['PATH', 'SYSTEMROOT'].flatMap(key => process.env[key] === undefined ? [] : [[key, process.env[key]]])),
|
|
185
|
+
signal: options.signal, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
186
|
+
child.stdout.on('data', (chunk) => { bytes += chunk.length; if (bytes <= 65536)
|
|
187
|
+
chunks.push(chunk);
|
|
188
|
+
else
|
|
189
|
+
child.kill('SIGTERM'); });
|
|
190
|
+
child.stderr.resume();
|
|
191
|
+
child.once('error', () => refuse(new CliError(options.signal.aborted ? 'canceled' : 'candidate_copy_unavailable', 'Candidate copying stopped. Recover the original copy request; no destination file was overwritten.', options.signal.aborted ? 130 : 1)));
|
|
192
|
+
child.once('close', code => {
|
|
193
|
+
try {
|
|
194
|
+
const result = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
195
|
+
if (code !== 0 || bytes > 65536 || result.ok !== true || result.data?.schema !== 'firedrill.candidate-copy.v1'
|
|
196
|
+
|| result.data.destination !== intent.destination || result.data.target_executed !== false || result.data.original_files_changed !== false)
|
|
197
|
+
throw new Error();
|
|
198
|
+
accept(result.data);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
refuse(new CliError('candidate_copy_refused', 'The exact candidate copy could not be confirmed. Changed, extra or unknown destination files are never overwritten or removed. Inspect the retained copy request before resuming.'));
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async function copyOwnership(intent, root) {
|
|
207
|
+
const legacy = await lstat(join(root, 'copy.lock')).then(() => true, error => {
|
|
208
|
+
if (error.code === 'ENOENT')
|
|
209
|
+
return false;
|
|
210
|
+
throw error;
|
|
211
|
+
});
|
|
212
|
+
if (legacy)
|
|
213
|
+
throw new CliError('candidate_copy_pending', 'This original copy has a retained legacy lock. Preserve it for inspection. No source copy or removal was repeated.');
|
|
214
|
+
// Outside the erasable cache: forgetting recovery bytes must never unlink
|
|
215
|
+
// the shared kernel-lock inode or create two owners of a recreated directory.
|
|
216
|
+
return processLifetimeLock(join(await privateDirectory(), `candidate-copy-${identifier(intent.owner)}-${identifier(intent.workspace)}-${identifier(intent.key)}.lock`));
|
|
217
|
+
}
|
|
218
|
+
async function continueCopy(intent, root, scope, options, output) {
|
|
219
|
+
const lock = await copyOwnership(intent, root);
|
|
220
|
+
options = { ...options, signal: AbortSignal.any([options.signal, lock.signal]) };
|
|
221
|
+
try {
|
|
222
|
+
lock.assertHeld();
|
|
223
|
+
options.signal.throwIfAborted();
|
|
224
|
+
output('candidate_copy_request', { request_key: intent.key, campaign_id: intent.campaign, change_id: intent.change, destination: intent.destination }, `Copy request: ${intent.key}\nDestination: ${intent.destination}\nIf interrupted: firedrill changes copy-resume --workspace ${scope.workspace} --request-key ${intent.key}`);
|
|
225
|
+
const endpoint = path(scope, intent.campaign, intent.change) + '/export';
|
|
226
|
+
const response = await api(endpoint, { ...auth(scope, options), method: 'POST', idempotencyKey: intent.key,
|
|
227
|
+
body: { scope_sha256: intent.scope_sha256, draft_sha256: intent.draft_sha256, download_source_consent: true } });
|
|
228
|
+
const { download_url: transportOnly, request_key: key, ...value } = response;
|
|
229
|
+
if (key !== intent.key || typeof transportOnly !== 'string')
|
|
230
|
+
throw new CliError('candidate_copy_identity_invalid', 'The candidate download receipt does not match this request.');
|
|
231
|
+
identity(value, intent);
|
|
232
|
+
let retained = 0, pages = 0;
|
|
233
|
+
const manifestHash = createHash('sha256');
|
|
234
|
+
manifestHash.update('[');
|
|
235
|
+
const pageSet = randomUUID();
|
|
236
|
+
for (let page = 1;; page++) {
|
|
237
|
+
const result = await api(`${endpoint}?page=${page}&page_size=100`, auth(scope, options));
|
|
238
|
+
if (result.page !== page || result.page_size !== 100 || result.count !== value.file_count || nativeMetadataCanonical(result.identity) !== nativeMetadataCanonical(value)
|
|
239
|
+
|| !Array.isArray(result.results) || !result.results.length || result.results.length > result.page_size
|
|
240
|
+
|| !result.results.every(patchSourceFileMatches) || retained + result.results.length > value.file_count
|
|
241
|
+
|| Boolean(result.next) !== (retained + result.results.length < value.file_count)
|
|
242
|
+
|| result.next && result.results.length !== 100)
|
|
243
|
+
throw new CliError('candidate_copy_manifest_changed', 'The candidate source manifest changed. No destination files were changed.');
|
|
244
|
+
for (const item of result.results) {
|
|
245
|
+
if (retained++)
|
|
246
|
+
manifestHash.update(',');
|
|
247
|
+
manifestHash.update(nativeMetadataCanonical(item));
|
|
248
|
+
}
|
|
249
|
+
await writeMetadata(root, `manifest-${pageSet}-${page}.json`, { page, entries: result.results });
|
|
250
|
+
pages = page;
|
|
251
|
+
output('candidate_copy_progress', { stage: 'manifest', retained_files: retained, expected_files: value.file_count }, `Source manifest: ${retained} / ${value.file_count} files`);
|
|
252
|
+
if (!result.next)
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
manifestHash.update(']');
|
|
256
|
+
if (retained !== value.file_count || manifestHash.digest('hex') !== value.files_sha256)
|
|
257
|
+
throw new CliError('candidate_copy_manifest_incomplete', 'The candidate source manifest is incomplete or differs from its retained digest.');
|
|
258
|
+
// Commit the bounded page set only after its whole ordered digest matches.
|
|
259
|
+
// Python rechecks it while building a disk-backed index, never one giant list.
|
|
260
|
+
await writeMetadata(root, 'manifest.json', { schema: 'firedrill.candidate-manifest-pages.v1', identity: value, page_set: pageSet, pages });
|
|
261
|
+
// Remove only previous private manifest page sets after the new head is
|
|
262
|
+
// durable. Interrupted downloads can resume without unbounded cache growth.
|
|
263
|
+
for await (const entry of await opendir(root)) {
|
|
264
|
+
if (/^manifest-[a-f0-9-]{36}-[1-9][0-9]*\.json$/.test(entry.name) && !entry.name.startsWith(`manifest-${pageSet}-`)) {
|
|
265
|
+
const file = await owned(join(root, entry.name), 1024 * 1024);
|
|
266
|
+
await file.close();
|
|
267
|
+
await unlink(join(root, entry.name));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Fetch a current short-lived URL after paging, without dispatching a new
|
|
271
|
+
// export intent. This also rechecks authority before copying source bytes.
|
|
272
|
+
const fresh = await api(endpoint, { ...auth(scope, options), method: 'POST', idempotencyKey: intent.key,
|
|
273
|
+
body: { scope_sha256: intent.scope_sha256, draft_sha256: intent.draft_sha256, download_source_consent: true } });
|
|
274
|
+
const { download_url: freshTransport, request_key: freshKey, ...freshIdentity } = fresh;
|
|
275
|
+
identity(freshIdentity, intent);
|
|
276
|
+
if (freshKey !== intent.key || nativeMetadataCanonical(freshIdentity) !== nativeMetadataCanonical(value)
|
|
277
|
+
|| typeof freshTransport !== 'string')
|
|
278
|
+
throw new CliError('candidate_copy_identity_invalid', 'The current candidate no longer matches this copy request.');
|
|
279
|
+
await download(root, value, fresh.download_url, options.signal);
|
|
280
|
+
output('candidate_copy_progress', { stage: 'archive_verified', bytes: value.snapshot.size }, 'Candidate archive downloaded and hash-checked.');
|
|
281
|
+
const result = await copyFiles(root, intent, options);
|
|
282
|
+
if (result.source_id !== value.source_id || result.world_id !== value.world_id || result.agent_id !== value.agent_id
|
|
283
|
+
|| result.files_sha256 !== value.files_sha256 || result.file_count !== value.file_count)
|
|
284
|
+
throw new CliError('candidate_copy_identity_invalid', 'The local copy receipt does not match the accepted candidate. It was not bound to a world.');
|
|
285
|
+
lock.assertHeld();
|
|
286
|
+
options.signal.throwIfAborted();
|
|
287
|
+
await bindSavedProject(scope, result.destination, { world: value.world_id, source: value.source_id, agent: value.agent_id, content_digest: value.files_sha256 });
|
|
288
|
+
output('result', { ...result, request_key: intent.key }, `Candidate saved to ${result.destination}\n${result.file_count} files verified. Original working copy unchanged.\n${workspaceLink(scope.workspace, `/worlds/${identifier(value.world_id)}`)}\nNo dependencies installed, Vault values or .env files copied, agent run, commit or PR created. Review candidate configuration and execution separately.`);
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
await lock.close();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
export async function candidateCopy(scope, campaign, change, options, output) {
|
|
295
|
+
const key = options.request ? identifier(options.request) : randomUUID(), root = await folder(scope, key);
|
|
296
|
+
let intent = await savedIntent(root, scope, key).catch(error => { if (error.code === 'ENOENT')
|
|
297
|
+
return null; throw error; });
|
|
298
|
+
if (!campaign || !change) {
|
|
299
|
+
if (!options.request || options.into || !intent)
|
|
300
|
+
throw new CliError('candidate_copy_request_required', 'Use copy-resume with --request-key for the exact saved copy request.', 2);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
if (!options.into)
|
|
304
|
+
throw new CliError('candidate_copy_destination_required', 'Use --into <new-folder>. Candidate copying never overwrites the current project.', 2);
|
|
305
|
+
const destination = resolve(await realpath(dirname(resolve(options.into))), resolve(options.into).split(sep).at(-1));
|
|
306
|
+
const project = await realpath(process.cwd()), inside = relative(project, destination);
|
|
307
|
+
if (!isAbsolute(inside) && inside !== '..' && !inside.startsWith('..' + sep)) {
|
|
308
|
+
throw new CliError('candidate_copy_destination_required', 'Choose a new folder outside the current working copy. Existing project files and directories are not used as the copy destination.', 2);
|
|
309
|
+
}
|
|
310
|
+
if (intent && (intent.campaign !== campaign || intent.change !== change || intent.destination !== destination))
|
|
311
|
+
throw new CliError('candidate_copy_request_conflict', 'This key belongs to another destination or source. Resume its original request.', 2);
|
|
312
|
+
if (!intent) {
|
|
313
|
+
const row = await api(path(scope, campaign, change), auth(scope, options));
|
|
314
|
+
if (!sourceChangeMatches(row, scope.workspace, campaign, change) || row.state !== 'accepted' || !row.current || !hash.test(row.scope_sha256)
|
|
315
|
+
|| !row.draft_sha256 || !hash.test(row.draft_sha256))
|
|
316
|
+
throw new CliError('candidate_copy_unavailable', 'Save a current reviewed candidate before copying it.');
|
|
317
|
+
const consent = options.approveDownload || !options.json && process.stdin.isTTY && process.stderr.isTTY
|
|
318
|
+
&& await selectOne(`Download this private candidate source into ${destination}? Keep local recovery metadata and a private archive. This does not execute it.`, [{ value: false, label: 'Cancel' }, { value: true, label: 'Download separate candidate' }], options.signal);
|
|
319
|
+
if (!consent)
|
|
320
|
+
throw new CliError('candidate_copy_consent_required', 'No candidate source was downloaded. Use --approve-download only after reviewing source disclosure and destination.', 2);
|
|
321
|
+
intent = { schema: 'firedrill.native-candidate-copy.v1', ...scope, key, campaign, change, destination, scope_sha256: row.scope_sha256,
|
|
322
|
+
draft_sha256: row.draft_sha256, download_source_consent: true };
|
|
323
|
+
const file = await open(join(root, 'intent.json'), 'wx', 0o600);
|
|
324
|
+
try {
|
|
325
|
+
await file.writeFile(JSON.stringify(intent));
|
|
326
|
+
await file.sync();
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
await file.close();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
await continueCopy(intent, root, scope, options, output);
|
|
334
|
+
}
|
|
335
|
+
export async function forgetCandidateCopy(scope, options, output) {
|
|
336
|
+
if (!options.request)
|
|
337
|
+
throw new CliError('candidate_copy_request_required', 'Choose the exact private copy request with --request-key.', 2);
|
|
338
|
+
const key = identifier(options.request), root = await folder(scope, key);
|
|
339
|
+
const intent = await savedIntent(root, scope, key);
|
|
340
|
+
const consent = options.approveDownload || !options.json && process.stdin.isTTY && process.stderr.isTTY
|
|
341
|
+
&& await selectOne('Remove this private downloaded archive and copy-recovery files? Keep the candidate folder and all cloud records.', [{ value: false, label: 'Keep private recovery files' }, { value: true, label: 'Remove private recovery files' }], options.signal);
|
|
342
|
+
if (!consent)
|
|
343
|
+
throw new CliError('candidate_copy_consent_required', 'Private recovery files were kept. Use --approve-download only after reviewing this exact local removal.', 2);
|
|
344
|
+
const lock = await copyOwnership(intent, root);
|
|
345
|
+
options = { ...options, signal: AbortSignal.any([options.signal, lock.signal]) };
|
|
346
|
+
try {
|
|
347
|
+
lock.assertHeld();
|
|
348
|
+
options.signal.throwIfAborted();
|
|
349
|
+
for await (const entry of await opendir(root)) {
|
|
350
|
+
const name = entry.name;
|
|
351
|
+
if (!/^(?:intent\.json|manifest\.json|manifest-[a-f0-9-]{36}-[1-9][0-9]*\.json|source\.zip|copy-receipt\.json|copy\.lock|(?:source\.[a-f0-9-]+|manifest(?:-[a-f0-9-]{36}-[1-9][0-9]*)?\.json\.[a-f0-9-]+)\.tmp|receipt-[A-Za-z0-9_-]+)$/.test(name)) {
|
|
352
|
+
throw new CliError('candidate_copy_cache_unsafe', 'The private cache contains an unexpected item. No automatic recursive removal is permitted.');
|
|
353
|
+
}
|
|
354
|
+
const file = await owned(join(root, name), Number.MAX_SAFE_INTEGER);
|
|
355
|
+
await file.close();
|
|
356
|
+
}
|
|
357
|
+
// Intent is removed last, preserving scoped recovery through an interrupted
|
|
358
|
+
// removal. Only exact flat regular files in this owned cache are touched.
|
|
359
|
+
for await (const entry of await opendir(root)) {
|
|
360
|
+
const name = entry.name;
|
|
361
|
+
if (['intent.json', 'copy.lock'].includes(name))
|
|
362
|
+
continue;
|
|
363
|
+
lock.assertHeld();
|
|
364
|
+
options.signal.throwIfAborted();
|
|
365
|
+
await unlink(join(root, name));
|
|
366
|
+
}
|
|
367
|
+
lock.assertHeld();
|
|
368
|
+
options.signal.throwIfAborted();
|
|
369
|
+
await unlink(join(root, 'intent.json'));
|
|
370
|
+
await rmdir(root);
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
await lock.close();
|
|
374
|
+
}
|
|
375
|
+
output('result', { request_key: intent.key, local_cache_removed: true, candidate_folder_removed: false }, 'Private downloaded archive and recovery files removed. The candidate folder and cloud source/history are unchanged.');
|
|
376
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../contracts/catalog-contracts.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../contracts/ci-access-contracts.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../contracts/ci-configuration-contracts.js';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { api } from './auth.js';
|
|
2
|
+
import { workspaceLink } from './browser-links.js';
|
|
3
|
+
import { CliError } from './http.js';
|
|
4
|
+
import { selectOne, terminalText } from './prompts.js';
|
|
5
|
+
import { ciCoverageMatches, ciCoverageScenariosMatch, ciCoverageFilesMatch } from '../../contracts/ci-coverage-contracts.js';
|
|
6
|
+
import { ciCoverageReason, ciCoverageLinkLabel, ciCoverageFileLabel } from '../../contracts/ci-coverage-labels.js';
|
|
7
|
+
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
8
|
+
export async function ciCoverage(run, detail, scope, options, output) {
|
|
9
|
+
if (detail !== undefined && !['scenarios', 'files'].includes(detail)
|
|
10
|
+
|| !detail && [options.page, options.search, options.status].some(value => value !== undefined)
|
|
11
|
+
|| options.status !== undefined && (detail !== 'scenarios' || !['all', 'selected', 'skipped'].includes(options.status))
|
|
12
|
+
|| (options.search?.length ?? 0) > 200 || /[\u0000-\u001f\u007f]/.test(options.search ?? '')) {
|
|
13
|
+
throw new CliError('arguments_invalid', 'Use ci coverage <run> [scenarios|files]. Paging/search apply to details; --status all|selected|skipped applies to scenarios.', 2);
|
|
14
|
+
}
|
|
15
|
+
const path = `/api/workspaces/${scope.workspace}/ci/runs/${run}/coverage`, auth = { actor: scope.owner, workspace: scope.workspace, signal: options.signal };
|
|
16
|
+
const value = await api(path, auth);
|
|
17
|
+
if (!ciCoverageMatches(value, scope.workspace, run))
|
|
18
|
+
throw new CliError('ci_coverage_changed', 'The original scenario selection could not be confirmed. Reopen this CI run.');
|
|
19
|
+
const selection = value.selection;
|
|
20
|
+
if (!selection) {
|
|
21
|
+
output('ci_coverage', value, 'Scenario selection is unavailable. Reading does not prepare or execute a run.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const command = `firedrill ci coverage ${run}`;
|
|
25
|
+
if (!detail) {
|
|
26
|
+
output('ci_coverage', value, [`${selection.selected_count.toLocaleString()} of ${selection.baseline_count.toLocaleString()} baseline scenarios selected; ${selection.skipped_count.toLocaleString()} skipped.`,
|
|
27
|
+
`${selection.selected_drills.toLocaleString()} of ${selection.baseline_drills.toLocaleString()} baseline drills planned.`,
|
|
28
|
+
`Reviewed PR policy: ${selection.policy_mode === 'change_relevant' ? 'change-relevant scenarios' : 'all planned scenarios'}.`,
|
|
29
|
+
selection.reduced ? 'Reduced scope. Skipped scenarios are not part of this run.' : 'All baseline scenarios are included.',
|
|
30
|
+
`${ciCoverageReason(selection.reason)}.`, selection.changed_files === null ? 'Source comparison unavailable.' : `${selection.changed_files.toLocaleString()} changed paths in the supported source comparison.`,
|
|
31
|
+
`Inspect cases: ${command} scenarios --workspace ${scope.workspace}`, ...(selection.changed_files !== null ? [`Inspect source: ${command} files --workspace ${scope.workspace}`] : []),
|
|
32
|
+
].map(terminalText).join('\n'));
|
|
33
|
+
if (options.json || !process.stdin.isTTY || !process.stderr.isTTY)
|
|
34
|
+
return;
|
|
35
|
+
detail = await selectOne('Inspect original selection', [{ value: 'done', label: 'Done' }, { value: 'scenarios', label: 'Selected and skipped scenarios' },
|
|
36
|
+
...(selection.changed_files === null ? [] : [{ value: 'files', label: 'Changed source files' }])], options.signal);
|
|
37
|
+
if (detail === 'done')
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const page = Number(options.page ?? '1'), query = options.search ?? '';
|
|
41
|
+
const search = query ? ` --search ${quote(query)}` : '', json = options.json ? ' --json' : '';
|
|
42
|
+
if (detail === 'files') {
|
|
43
|
+
if (selection.changed_files === null)
|
|
44
|
+
throw new CliError('ci_comparison_unavailable', 'Source comparison is unavailable. Inspect scenario coverage for the selection reason.');
|
|
45
|
+
const rows = await api(`${path}/files?${new URLSearchParams({ page: String(page), q: query })}`, auth);
|
|
46
|
+
if (!ciCoverageFilesMatch(rows, scope.workspace, run, selection, query, page))
|
|
47
|
+
throw new CliError('ci_coverage_changed', 'This source page does not match the original retained selection.');
|
|
48
|
+
output('ci_coverage_files', rows, [...rows.results.map(row => `${row.path} · ${ciCoverageFileLabel(row.change)}`),
|
|
49
|
+
...(!rows.count ? [query ? 'No changed source paths match this search.' : 'No changes were recorded in the supported comparison.'] : []),
|
|
50
|
+
`Page ${rows.page}; ${rows.count.toLocaleString()} source paths. File changes do not establish behavioral impact.`,
|
|
51
|
+
...(rows.next ? [`Continue: ${command} files --workspace ${scope.workspace} --page ${rows.page + 1}${search}${json}`] : [])].map(terminalText).join('\n'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const selected = (options.status ?? 'all');
|
|
55
|
+
const rows = await api(`${path}/scenarios?${new URLSearchParams({ page: String(page), q: query, selected })}`, auth);
|
|
56
|
+
if (!ciCoverageScenariosMatch(rows, scope.workspace, run, selection, { page, query, selected }))
|
|
57
|
+
throw new CliError('ci_coverage_changed', 'This scenario page does not match the original retained selection.');
|
|
58
|
+
output('ci_coverage_scenarios', rows, [...rows.results.map(row => [row.available ? row.name : 'Scenario details unavailable',
|
|
59
|
+
`${row.selected ? 'Selected' : 'Skipped'} · ${ciCoverageReason(row.reason)} · Saved revision ${row.revision}`,
|
|
60
|
+
...row.links.map(link => ` ${link.path}: ${ciCoverageLinkLabel(link.status)}`),
|
|
61
|
+
...(row.detail_path ? [workspaceLink(scope.workspace, row.detail_path)] : [])].map(terminalText).join('\n')),
|
|
62
|
+
...(!rows.count ? ['No original scenarios match these filters.'] : []), `Page ${rows.page}; ${rows.count.toLocaleString()} baseline cases.`,
|
|
63
|
+
...(rows.next ? [`Continue: ${command} scenarios --workspace ${scope.workspace} --page ${rows.page + 1} --status ${selected}${search}${json}`] : [])].join('\n\n'));
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../contracts/ci-delivery-contracts.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../contracts/ci-result-contracts.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../contracts/ci-run-contracts.js';
|