diff --git a/.github/workflows/cookiecutter-bootstrap.yml b/.github/workflows/cookiecutter-bootstrap.yml index 9b76b493e..d52f92175 100644 --- a/.github/workflows/cookiecutter-bootstrap.yml +++ b/.github/workflows/cookiecutter-bootstrap.yml @@ -232,13 +232,13 @@ jobs: run: npm ci - name: Download Linux bootstrap artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: cookiecutter-bootstrap-linux path: tests/results/_agent - name: Download Windows bootstrap artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: cookiecutter-bootstrap-windows path: tests/results/_agent @@ -282,7 +282,7 @@ jobs: - name: Upload template verification report if: ${{ always() }} - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: template-agent-verification-${{ github.run_id }} path: tests/results/_agent/promotion/template-agent-verification-report.json diff --git a/.github/workflows/downstream-promotion.yml b/.github/workflows/downstream-promotion.yml index 314bd08d4..0c0a3520b 100644 --- a/.github/workflows/downstream-promotion.yml +++ b/.github/workflows/downstream-promotion.yml @@ -324,7 +324,7 @@ jobs: - name: Upload downstream promotion artifacts if: ${{ always() && hashFiles('tests/results/_agent/onboarding/*.json', 'tests/results/_agent/promotion/*.json') != '' }} - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: downstream-promotion-${{ github.run_id }} path: | diff --git a/.github/workflows/publish-tools-image.yml b/.github/workflows/publish-tools-image.yml index 96442227a..5ff116854 100644 --- a/.github/workflows/publish-tools-image.yml +++ b/.github/workflows/publish-tools-image.yml @@ -69,7 +69,7 @@ jobs: --labels-file "$RUNNER_TEMP/tools-image-labels.txt" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v4 diff --git a/.github/workflows/release-conductor.yml b/.github/workflows/release-conductor.yml index 7da0e9088..b8587422e 100644 --- a/.github/workflows/release-conductor.yml +++ b/.github/workflows/release-conductor.yml @@ -8,6 +8,11 @@ on: required: false default: false type: boolean + repair_existing_tag: + description: 'Repair an existing authoritative tag as a signed annotated tag' + required: false + default: false + type: boolean channel: description: 'Release channel' required: false @@ -66,12 +71,82 @@ jobs: run: | pwsh -NoLogo -NoProfile -File tools/priority/Resolve-PolicyToken.ps1 -TokenFileName release-conductor-gh-token.txt + - name: Configure release tag signing material + shell: bash + env: + RELEASE_TAG_SIGNING_PRIVATE_KEY: ${{ secrets.RELEASE_TAG_SIGNING_PRIVATE_KEY }} + RELEASE_TAG_SIGNING_PUBLIC_KEY: ${{ secrets.RELEASE_TAG_SIGNING_PUBLIC_KEY }} + RELEASE_TAG_SIGNING_IDENTITY_NAME: ${{ vars.RELEASE_TAG_SIGNING_IDENTITY_NAME || '' }} + RELEASE_TAG_SIGNING_IDENTITY_EMAIL: ${{ vars.RELEASE_TAG_SIGNING_IDENTITY_EMAIL || '' }} + run: | + set -euo pipefail + if [[ -z "${RELEASE_TAG_SIGNING_PRIVATE_KEY:-}" ]]; then + echo "No release tag signing key configured; skipping workflow-owned signing setup." + exit 0 + fi + if [[ -z "${GH_TOKEN:-}" ]]; then + echo "::error::GH_TOKEN is unavailable after Resolve-PolicyToken; cannot derive workflow signing identity." + exit 1 + fi + signing_dir="$RUNNER_TEMP/release-tag-signing" + mkdir -p "$signing_dir" + private_key_path="$signing_dir/id_release_tag_signing" + public_key_path="${private_key_path}.pub" + signing_login="$(gh api user --jq '.login')" + signing_id="$(gh api user --jq '.id')" + signing_name="${RELEASE_TAG_SIGNING_IDENTITY_NAME:-}" + signing_email="${RELEASE_TAG_SIGNING_IDENTITY_EMAIL:-}" + + if [[ -z "$signing_name" ]]; then + signing_name="$(gh api user --jq '.name // .login')" + fi + if [[ -z "$signing_email" ]]; then + signing_email="$(gh api user --jq '.email // ""')" + fi + if [[ -z "$signing_email" ]]; then + signing_email="${signing_id}+${signing_login}@users.noreply.github.com" + fi + + identity_source="policy-token-user" + if [[ -n "${RELEASE_TAG_SIGNING_IDENTITY_NAME:-}" || -n "${RELEASE_TAG_SIGNING_IDENTITY_EMAIL:-}" ]]; then + identity_source="repo-variable-override" + fi + + printf '%s\n' "$RELEASE_TAG_SIGNING_PRIVATE_KEY" > "$private_key_path" + chmod 600 "$private_key_path" + + if [[ -n "${RELEASE_TAG_SIGNING_PUBLIC_KEY:-}" ]]; then + printf '%s\n' "$RELEASE_TAG_SIGNING_PUBLIC_KEY" > "$public_key_path" + else + ssh-keygen -y -f "$private_key_path" > "$public_key_path" + fi + chmod 644 "$public_key_path" + + git config gpg.format ssh + git config user.signingkey "$public_key_path" + git config user.name "$signing_name" + git config user.email "$signing_email" + git config tag.gpgSign true + + { + echo "RELEASE_TAG_SIGNING_BACKEND=ssh" + echo "RELEASE_TAG_SIGNING_SOURCE=workflow-secret" + echo "RELEASE_TAG_SIGNING_IDENTITY_NAME=$signing_name" + echo "RELEASE_TAG_SIGNING_IDENTITY_EMAIL=$signing_email" + echo "RELEASE_TAG_SIGNING_IDENTITY_LOGIN=$signing_login" + echo "RELEASE_TAG_SIGNING_IDENTITY_ID=$signing_id" + echo "RELEASE_TAG_SIGNING_IDENTITY_SOURCE=$identity_source" + } >> "$GITHUB_ENV" + - name: Run release conductor shell: pwsh env: RELEASE_CONDUCTOR_ENABLED: ${{ vars.RELEASE_CONDUCTOR_ENABLED || '0' }} + RELEASE_TAG_SIGNING_BACKEND: ${{ env.RELEASE_TAG_SIGNING_BACKEND || '' }} + RELEASE_TAG_SIGNING_SOURCE: ${{ env.RELEASE_TAG_SIGNING_SOURCE || '' }} run: | npm ci --ignore-scripts + node tools/npm/run-script.mjs priority:queue:supervisor -- --dry-run --report tests/results/_agent/queue/queue-supervisor-report.json node tools/npm/run-script.mjs priority:policy:snapshot -- --output tests/results/_agent/policy/policy-state-snapshot.json $reportPath = 'tests/results/_agent/release/release-conductor-report.json' @@ -105,6 +180,10 @@ jobs: $args += '--dry-run' } + if ('${{ inputs.repair_existing_tag }}' -eq 'true') { + $args += '--repair-existing-tag' + } + $channelInput = '${{ inputs.channel }}' if (-not [string]::IsNullOrWhiteSpace($channelInput)) { $args += @('--channel', $channelInput.Trim().ToLowerInvariant()) @@ -134,5 +213,6 @@ jobs: name: release-conductor-${{ github.run_id }} path: | tests/results/_agent/release/release-conductor-report.json + tests/results/_agent/queue/queue-supervisor-report.json tests/results/_agent/policy/policy-state-snapshot.json if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df311d6a0..cfd5a95bf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,12 @@ on: push: tags: - 'v*' + workflow_dispatch: + inputs: + release_tag: + description: 'Authoritative release tag to publish or replay.' + required: true + type: string jobs: certification-matrix: @@ -13,17 +19,40 @@ jobs: actions: read contents: read outputs: + target_tag: ${{ steps.release_target.outputs.tag }} channel: ${{ steps.channel.outputs.channel }} steps: + - name: Resolve release target tag + id: release_target + shell: bash + run: | + set -euo pipefail + tag="${GITHUB_REF_NAME}" + if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + tag='${{ inputs.release_tag }}' + fi + if [[ -z "$tag" ]]; then + echo "Release target tag could not be resolved." >&2 + exit 1 + fi + if [[ "$tag" != v* ]]; then + echo "Release target tag must start with v: $tag" >&2 + exit 1 + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@v5 + with: + ref: ${{ steps.release_target.outputs.tag }} - name: Resolve release channel id: channel shell: bash run: | set -euo pipefail + resolved_tag='${{ steps.release_target.outputs.tag }}' channel="stable" - if [[ "${GITHUB_REF_NAME}" == *"-rc."* ]]; then + if [[ "$resolved_tag" == *-rc.* ]]; then channel="rc" fi echo "channel=${channel}" >> "$GITHUB_OUTPUT" @@ -68,8 +97,31 @@ jobs: id-token: write outputs: cli_version: ${{ steps.meta.outputs.cli_version }} + target_tag: ${{ needs.certification-matrix.outputs.target_tag }} + env: + RELEASE_TAG: ${{ needs.certification-matrix.outputs.target_tag }} steps: - uses: actions/checkout@v5 + with: + ref: ${{ env.RELEASE_TAG }} + + - name: Checkout replay automation surfaces + if: ${{ github.event_name == 'workflow_dispatch' }} + uses: actions/checkout@v5 + with: + ref: develop + path: .release-automation + + - name: Resolve release automation root + id: automation_root + shell: bash + run: | + set -euo pipefail + automation_root='.' + if [[ -f '.release-automation/tools/priority/release-trust-remediation.mjs' ]]; then + automation_root='.release-automation' + fi + echo "path=${automation_root}" >> "$GITHUB_OUTPUT" - name: Setup Node 20 uses: actions/setup-node@v6 @@ -85,7 +137,7 @@ jobs: shell: bash run: | set -euo pipefail - TAG="${GITHUB_REF_NAME}" + TAG="${RELEASE_TAG}" awk -v tag="$TAG" ' $0 ~ "^##[[:space:]]*\\[?" tag "\\]?$" {print; in=1; next} in && $0 ~ "^##[[:space:]]*\\[" {exit} @@ -212,7 +264,7 @@ jobs: if-no-files-found: error - name: Attest release assets provenance - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@v4 with: subject-path: | artifacts/cli/*.zip @@ -233,7 +285,7 @@ jobs: --checksums artifacts/cli/SHA256SUMS.txt \ --sbom artifacts/cli/sbom.spdx.json \ --provenance artifacts/cli/provenance.json \ - --tag-ref "${{ github.ref_name }}" \ + --tag-ref "${RELEASE_TAG}" \ --signer-workflow "${{ github.repository }}/.github/workflows/release.yml" \ --report tests/results/_agent/supply-chain/release-trust-gate.json @@ -245,12 +297,25 @@ jobs: -JsonPath tests/results/_agent/supply-chain/release-trust-gate.json ` -SchemaPath docs/schemas/supply-chain-trust-gate-v1.schema.json + - name: Append release trust remediation guidance + if: always() + shell: bash + run: | + set -euo pipefail + node "${{ steps.automation_root.outputs.path }}/tools/priority/release-trust-remediation.mjs" \ + --trust-report tests/results/_agent/supply-chain/release-trust-gate.json \ + --tag-ref "${RELEASE_TAG}" \ + --output tests/results/_agent/release/release-trust-remediation.md \ + --summary "$GITHUB_STEP_SUMMARY" + - name: Upload supply-chain trust artifact if: always() uses: actions/upload-artifact@v7 with: name: release-supply-chain-trust-${{ github.run_id }} - path: tests/results/_agent/supply-chain/release-trust-gate.json + path: | + tests/results/_agent/supply-chain/release-trust-gate.json + tests/results/_agent/release/release-trust-remediation.md if-no-files-found: error - name: Enforce rollback drill health gate @@ -259,9 +324,9 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - node tools/priority/rollback-drill-health.mjs \ + node "${{ steps.automation_root.outputs.path }}/tools/priority/rollback-drill-health.mjs" \ --repo "${{ github.repository }}" \ - --policy tools/policy/release-rollback-policy.json \ + --policy "${{ steps.automation_root.outputs.path }}/tools/policy/release-rollback-policy.json" \ --report tests/results/_agent/release/rollback-drill-health.json - name: Validate rollback drill health schema @@ -270,7 +335,7 @@ jobs: run: | pwsh -NoLogo -NoProfile -File tools/Invoke-JsonSchemaLite.ps1 ` -JsonPath tests/results/_agent/release/rollback-drill-health.json ` - -SchemaPath docs/schemas/release-rollback-drill-health-v1.schema.json + -SchemaPath '${{ steps.automation_root.outputs.path }}/docs/schemas/release-rollback-drill-health-v1.schema.json' - name: Upload rollback drill health artifact if: always() @@ -283,7 +348,6 @@ jobs: - name: Append release surface map shell: pwsh env: - RELEASE_TAG: ${{ github.ref_name }} CLI_VERSION: ${{ steps.meta.outputs.cli_version }} MODULE_VERSION: ${{ steps.comparevi_tools.outputs.comparevi_tools_release_version }} MODULE_METADATA: tests/results/_agent/release/comparevi-tools-artifact.json @@ -304,6 +368,7 @@ jobs: if: steps.notes.outputs.fallback == 'false' uses: softprops/action-gh-release@v2 with: + tag_name: ${{ env.RELEASE_TAG }} body_path: RELEASE_NOTES.md files: | artifacts/cli/*.zip @@ -316,6 +381,7 @@ jobs: if: steps.notes.outputs.fallback == 'true' uses: softprops/action-gh-release@v2 with: + tag_name: ${{ env.RELEASE_TAG }} generate_release_notes: true files: | artifacts/cli/*.zip @@ -334,7 +400,7 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, workflow_id: 'publish-tools-image.yml', - ref: '${{ github.ref_name }}', + ref: 'develop', inputs: { version: releaseVersion, channel: releaseChannel @@ -345,6 +411,8 @@ jobs: validate-cli-artifacts: runs-on: ${{ matrix.os }} needs: release + env: + RELEASE_TAG: ${{ needs.release.outputs.target_tag }} strategy: matrix: os: [windows-latest, ubuntu-latest, macos-latest] @@ -518,7 +586,7 @@ jobs: } pwsh -NoLogo -NoProfile -File tools/release-review/Write-ReleaseReviewScenarioSummary.ps1 ` -Os '${{ matrix.os }}' ` - -Tag '${{ github.ref_name }}' ` + -Tag '${{ env.RELEASE_TAG }}' ` -ChecksumOutcome $checksumOutcome ` -SmokeOutcome $smokeOutcome ` -OutputPath 'tests/results/release-review/scenario-summary-${{ matrix.os }}.json' @@ -616,6 +684,8 @@ jobs: actions: read contents: read issues: write + env: + RELEASE_TAG: ${{ needs.release.outputs.target_tag }} steps: - uses: actions/checkout@v5 @@ -655,7 +725,8 @@ jobs: shell: bash run: | set -euo pipefail - source_sha="$(git rev-parse "${{ github.ref_name }}^{commit}")" + git fetch --force --tags origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}" + source_sha="$(git rev-parse "${RELEASE_TAG}^{commit}")" echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT" - name: Resolve downstream proving artifact selection @@ -743,7 +814,7 @@ jobs: -ScenarioRoot tests/results/release-contract/scenarios ` -ProfilePath tools/release-review/scenario-profiles.json ` -PolicyPath tools/policy/release-review-gates.json ` - -Tag '${{ github.ref_name }}' ` + -Tag '${{ env.RELEASE_TAG }}' ` -OutputIndexPath tests/results/release-contract/review-index.json ` -OutputCommentPath tests/results/release-contract/review-comment.md if (Test-Path -LiteralPath 'tests/results/release-contract/review-comment.md' -PathType Leaf) { @@ -800,7 +871,7 @@ jobs: 'cancelled' { 'blocked' } default { 'fail' } } - $channel = if ('${{ github.ref_name }}' -match '-rc\.') { 'rc' } else { 'stable' } + $channel = if ('${{ env.RELEASE_TAG }}' -match '-rc\.') { 'rc' } else { 'stable' } pwsh -NoLogo -NoProfile -File tools/Write-PromotionEvidenceLedger.ps1 -OutputPath tests/results/promotion-contract/release-ledger.json -WorkflowName '${{ github.workflow }}' -Stream 'comparevi-cli' -Channel $channel -Version '${{ needs.release.outputs.cli_version }}' -GateStatus $gateStatus -GateReason 'release-contract job result' -SummaryPath 'tests/results/release-contract/review-index.json' -StepSummaryPath $env:GITHUB_STEP_SUMMARY - name: Emit SLO metrics @@ -822,11 +893,11 @@ jobs: run: | set -euo pipefail channel="stable" - if printf '%s' "${{ github.ref_name }}" | grep -q -- '-rc\.'; then + if printf '%s' "${RELEASE_TAG}" | grep -q -- '-rc\.'; then channel="rc" fi signed_tag_args=() - if ! printf '%s' "${{ github.ref_name }}" | grep -Eq -- '-tools\.[0-9]+$'; then + if ! printf '%s' "${RELEASE_TAG}" | grep -Eq -- '-tools\.[0-9]+$'; then signed_tag_args+=(--require-signed-tag) fi node tools/priority/release-scorecard.mjs \ @@ -834,7 +905,7 @@ jobs: --stream comparevi-cli \ --channel "${channel}" \ --version "${{ needs.release.outputs.cli_version }}" \ - --tag-ref "${{ github.ref_name }}" \ + --tag-ref "${RELEASE_TAG}" \ --ledger tests/results/promotion-contract/release-ledger.json \ --slo tests/results/_agent/slo/release-slo-metrics.json \ --rollback tests/results/_agent/release/rollback-drill-health.json \ diff --git a/AGENTS.md b/AGENTS.md index dcc6e110d..9558d03df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,6 +105,8 @@ Keep it short, stable, and helper-oriented. Deep runbooks belong in checked-in d `tests/results/_agent/handoff/autonomous-governor-summary.json`. - `node tools/npm/run-script.mjs priority:governor:portfolio` refreshes the cross-repo operating receipt at `tests/results/_agent/handoff/autonomous-governor-portfolio-summary.json`. +- `node tools/npm/run-script.mjs priority:context:concentrate` refreshes the compact durable memory receipt at + `tests/results/_agent/handoff/sagan-context-concentrator.json`. - `node tools/npm/run-script.mjs priority:continuity` refreshes the continuity receipts at `tests/results/_agent/runtime/continuity-telemetry.json` and `tests/results/_agent/handoff/continuity-summary.json`. @@ -116,6 +118,7 @@ Keep it short, stable, and helper-oriented. Deep runbooks belong in checked-in d - `tests/results/_agent/issue/no-standing-priority.json` - `tests/results/_agent/handoff/autonomous-governor-summary.json` - `tests/results/_agent/handoff/autonomous-governor-portfolio-summary.json` + - `tests/results/_agent/handoff/sagan-context-concentrator.json` - `tests/results/_agent/handoff/continuity-summary.json` - `tests/results/_agent/handoff/entrypoint-status.json` - `tests/results/_agent/runtime/` diff --git a/AGENT_HANDOFF.txt b/AGENT_HANDOFF.txt index 39cf21ec9..eca6def34 100644 --- a/AGENT_HANDOFF.txt +++ b/AGENT_HANDOFF.txt @@ -9,8 +9,7 @@ Live repository state belongs in the machine-generated artifacts under ## First Actions 1. Run `pwsh -NoLogo -NoProfile -File tools/priority/bootstrap.ps1`. -2. Check `.agent_priority_cache.json` and - `tests/results/_agent/issue/router.json`. +2. Check `.agent_priority_cache.json` and `tests/results/_agent/issue/router.json`. 3. Run `pwsh -NoLogo -NoProfile -File tools/Print-AgentHandoff.ps1 -ApplyToggles -AutoTrim` when you need the current watcher/handoff snapshot. 4. When human disposition surfaces are in play, run @@ -25,7 +24,9 @@ Live repository state belongs in the machine-generated artifacts under `tests/results/_agent/handoff/autonomous-governor-summary.json` for the current repo owner decision and `tests/results/_agent/handoff/autonomous-governor-portfolio-summary.json` - for the cross-repo owner decision. + for the cross-repo owner decision, and + `tests/results/_agent/handoff/sagan-context-concentrator.json` for the compact + hot/warm durable memory view of subagent work and blockers. ## Live State Surfaces @@ -43,6 +44,8 @@ Live repository state belongs in the machine-generated artifacts under `node tools/npm/run-script.mjs priority:governor:summary` - Governor portfolio: `node tools/npm/run-script.mjs priority:governor:portfolio` +- Context concentrator: + `node tools/npm/run-script.mjs priority:context:concentrate` ## Current-State Artifacts @@ -54,6 +57,7 @@ Live repository state belongs in the machine-generated artifacts under - `tests/results/_agent/handoff/entrypoint-status.json` - `tests/results/_agent/handoff/monitoring-mode.json` - `tests/results/_agent/handoff/autonomous-governor-summary.json` +- `tests/results/_agent/handoff/sagan-context-concentrator.json` - `tests/results/_agent/handoff/autonomous-governor-portfolio-summary.json` - `tests/results/_agent/handoff/human-go-no-go-latest.json` - `tests/results/_agent/handoff/*.json` @@ -65,8 +69,7 @@ Live repository state belongs in the machine-generated artifacts under - The project board is visibility only. - Treat `queue-empty` as a valid idle state; do not invent a null issue context. - Treat `queue-empty + safe-idle + pivot-ready` as an explicit monitoring state. -- Prefer sanitized repo helpers (`node tools/npm/run-script.mjs ...`) over raw - `npm`. +- Prefer sanitized repo helpers (`node tools/npm/run-script.mjs ...`) over raw `npm`. ## When Handoff Looks Wrong diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ab972772..e89b8cf97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,24 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve expectations between backend releases in this repo and facade releases in `comparevi-history`. +## [v0.6.4-rc.2] - 2026-03-24 + +### Changed + +- The published `CompareVI.Tools` bundle contract now targets the next RC + identity so release publication can carry the merged producer-owned Docker + contract instead of replaying the earlier `v0.6.4-rc.1` bundle metadata. +- Release branch materials now treat `v0.6.4-rc.2` as the active RC cut for the + template Docker-profile rail, keeping the changelog, PR notes, checklist, and + archived release notes aligned to the same release identity. + +### Added + +- The `CompareVI.Tools` release manifest now includes the producer-owned + `consumerContract.capabilities.dockerProfile` capability and + `consumerContract.dockerImageContract` source needed by + `LabviewGitHubCiTemplate#20` once the next authoritative bundle is published. + ## [v0.6.4-rc.1] - 2026-03-22 ### Changed diff --git a/Directory.Build.props b/Directory.Build.props index 3876e87cc..c32b8466b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ false true true - 0.6.4-rc.1 + 0.6.4-rc.2 0.6.4.0 0.6.4.0 $(Version)+local diff --git a/dist/tools/schemas/definitions.js b/dist/tools/schemas/definitions.js index 2e93876b1..0edf0b512 100644 --- a/dist/tools/schemas/definitions.js +++ b/dist/tools/schemas/definitions.js @@ -279,35 +279,136 @@ const warmupModeSchema = z.enum(['detect', 'spawn', 'skip']); const warmupEventsSchema = z.union([z.string().min(1), z.null()]); const compareCliSchema = cliInfoSchema; const comparePolicySchema = z.enum(['lv-first', 'cli-first', 'cli-only', 'lv-only']); +const testStandCompareOutcomeSchema = z + .object({ + exitCode: z.number(), + seconds: z.number().optional(), + command: z.string().optional(), + diff: z.boolean().optional(), +}) + .nullable(); +const testStandCompareNodeSchema = z.object({ + events: z.string().min(1), + capture: z.union([z.string().min(1), z.null()]), + report: z.boolean(), + command: z.string().min(1).optional(), + cliPath: z.string().min(1).optional(), + cli: compareCliSchema.optional(), + staging: z + .object({ + enabled: z.boolean(), + root: z.union([z.string().min(1), z.null()]), + }) + .optional(), + allowSameLeaf: z.boolean().optional(), + policy: comparePolicySchema.optional(), + mode: z.string().min(1).optional(), + autoCli: z.boolean().optional(), + sameName: z.boolean().optional(), + timeoutSeconds: z.number().min(0).optional(), +}); +const testStandExecutionCellSchema = z.object({ + cellId: z.string().min(1).nullable().optional(), + leaseId: z.string().min(1).nullable().optional(), + leasePath: z.string().min(1).nullable().optional(), + agentId: z.string().min(1).nullable().optional(), + agentClass: z.enum(['sagan', 'subagent', 'other']).nullable().optional(), + cellClass: z.enum(['worker', 'coordinator', 'kernel-coordinator']).nullable().optional(), + suiteClass: z.enum(['single-compare', 'dual-plane-parity']).nullable().optional(), + planeBinding: z.string().min(1).nullable().optional(), + runtimeSurface: z.literal('windows-native-teststand').nullable().optional(), + premiumSaganMode: z.boolean().optional(), + operatorAuthorizationRef: z.string().min(1).nullable().optional(), + workingRoot: z.string().min(1).nullable().optional(), + artifactRoot: z.string().min(1).nullable().optional(), + isolatedLaneGroupId: z.string().min(1).nullable().optional(), + hostOsFingerprintSha256: hexSha256.nullable().optional(), +}); +const testStandProcessModelSchema = z.object({ + runtimeSurface: z.literal('windows-native-teststand'), + processModelClass: z.enum(['sequential-process-model', 'parallel-process-model']), + windowsOnly: z.literal(true), + rootHarnessInstanceId: z.string().min(1), + planeCount: z.number().int().min(1), +}); +const testStandHarnessInstanceSchema = z.object({ + harnessKind: z.string().min(1), + instanceId: z.string().min(1), + role: z.enum(['single-plane', 'coordinator', 'plane-child']), + processModelClass: z.enum(['sequential-process-model', 'parallel-process-model']), + planeBinding: z.string().min(1).nullable().optional(), + parentInstanceId: z.string().min(1).nullable().optional(), +}); +const testStandPlaneSessionSchema = z.object({ + plane: z.string().min(1), + architecture: z.enum(['32-bit', '64-bit']), + labviewExePath: z.union([z.string().min(1), z.null()]).optional(), + outputRoot: z.string().min(1), + warmup: z.object({ + mode: warmupModeSchema, + events: warmupEventsSchema, + }), + compare: testStandCompareNodeSchema, + outcome: testStandCompareOutcomeSchema, + error: z.union([z.string().min(1), z.null()]).optional(), + exitCode: z.number(), + executionCell: testStandExecutionCellSchema.nullable().optional(), + harnessInstance: testStandHarnessInstanceSchema.nullable().optional(), + processModel: testStandProcessModelSchema.optional(), +}); +const testStandParitySummarySchema = z.object({ + status: z.enum(['match', 'mismatch', 'incomplete']), + comparedFields: z.array(z.string().min(1)), + exitCodeParity: z.boolean().nullable().optional(), + diffParity: z.boolean().nullable().optional(), + mismatchCount: z.number().int().min(0), + mismatches: z.array(z.object({ + field: z.string().min(1), + x64: z.union([z.string().min(1), z.number(), z.boolean(), z.null()]).optional(), + x32: z.union([z.string().min(1), z.number(), z.boolean(), z.null()]).optional(), + })), +}); const testStandCompareSessionSchema = z.object({ - schema: z.literal('teststand-compare-session/v1'), + schema: z.enum(['teststand-compare-session/v1', 'teststand-compare-session/v2']), at: isoString, warmup: z.object({ mode: warmupModeSchema, events: warmupEventsSchema, }), - compare: z.object({ - events: z.string().min(1), - capture: z.union([z.string().min(1), z.null()]), - report: z.boolean(), - command: z.string().min(1).optional(), - cliPath: z.string().min(1).optional(), - cli: compareCliSchema.optional(), - policy: comparePolicySchema.optional(), - mode: z.string().min(1).optional(), - autoCli: z.boolean().optional(), - sameName: z.boolean().optional(), - timeoutSeconds: z.number().min(0).optional(), - }), - outcome: z + compare: testStandCompareNodeSchema, + outcome: testStandCompareOutcomeSchema, + error: z.union([z.string().min(1), z.null()]).optional(), + executionCell: testStandExecutionCellSchema.nullable().optional(), + harnessInstance: testStandHarnessInstanceSchema.nullable().optional(), + processModel: testStandProcessModelSchema.optional(), + suiteClass: z.enum(['single-compare', 'dual-plane-parity']).optional(), + primaryPlane: z.string().min(1).optional(), + requestedSimultaneous: z.boolean().optional(), + planes: z .object({ - exitCode: z.number(), - seconds: z.number().optional(), - command: z.string().optional(), - diff: z.boolean().optional(), + x64: testStandPlaneSessionSchema, + x32: testStandPlaneSessionSchema, }) - .nullable(), - error: z.union([z.string().min(1), z.null()]).optional(), + .optional(), + parity: testStandParitySummarySchema.optional(), +}).superRefine((value, ctx) => { + if (value.schema === 'teststand-compare-session/v2') { + if (!value.suiteClass) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'suiteClass is required for v2 sessions', path: ['suiteClass'] }); + } + if (!value.primaryPlane) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'primaryPlane is required for v2 sessions', path: ['primaryPlane'] }); + } + if (typeof value.requestedSimultaneous !== 'boolean') { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'requestedSimultaneous is required for v2 sessions', path: ['requestedSimultaneous'] }); + } + if (!value.planes) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'planes is required for v2 sessions', path: ['planes'] }); + } + if (!value.parity) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'parity is required for v2 sessions', path: ['parity'] }); + } + } }); const invokerEventSchema = z.object({ timestamp: isoString, diff --git a/docs/DELIVERY_CONTROL_PLANE_PR_LANES.md b/docs/DELIVERY_CONTROL_PLANE_PR_LANES.md index 81f67596a..3299f3e71 100644 --- a/docs/DELIVERY_CONTROL_PLANE_PR_LANES.md +++ b/docs/DELIVERY_CONTROL_PLANE_PR_LANES.md @@ -17,7 +17,7 @@ This scaffold tracks the six planned mergeable slices so agents can resume deter - Keep each lane mergeable and production-safe in isolation. - Rebase each next lane on merged upstream `develop` before opening its PR. -- Preserve signed-tag policy; release conductor must remain proposal-only when signing material is unavailable. +- Preserve signed-tag policy; release conductor must fail closed before tag creation when signing material is unavailable. ## Resume commands diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 3f08c9325..534892d47 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -177,10 +177,14 @@ Quick reference for building, testing, and releasing the LVCompare composite act `gh issue create/edit`. Omit `--output` to print to STDOUT. - `pwsh -NoLogo -NoProfile -File tools/Post-IssueComment.ps1 -Issue -BodyFile issue-comment.md` Posts GitHub issue comments through `--body-file` by default so multiline Markdown survives PowerShell and mixed - Windows/WSL shells without backtick-escape drift. + Windows/WSL shells without backtick-escape drift. The wrapper also appends the + checked-in durable budget hook by default; pass `-SkipBudgetHook` only when a + test or break-glass path must suppress the attestation. - `pwsh -NoLogo -NoProfile -File tools/Post-PullRequestComment.ps1 -PullRequest -Repo -BodyFile pr-comment.md` Posts GitHub pull-request comments through `--body-file` by default so multiline Markdown survives PowerShell and - mixed Windows/WSL shells without backtick-escape drift. + mixed Windows/WSL shells without backtick-escape drift. The wrapper also + appends the checked-in durable budget hook by default; pass `-SkipBudgetHook` + only when a test or break-glass path must suppress the attestation. - `node tools/priority/github-helper.mjs snippet --issue 531 --prefix Fixes` Emits an auto-link snippet (defaults to `Fixes #531`) you can drop into PR descriptions so GitHub auto-closes the issue. - `node tools/npm/run-script.mjs priority:project:portfolio:apply -- --url --use-config` @@ -476,8 +480,8 @@ For Docker/Desktop VI history validation, run fast-loop lanes explicitly: Auto mode activates on release windows, open `release/*` PRs, or `release-burst` labels, and backs off for 30 minutes whenever the throughput controller enters `stabilize`. For queue-aware release proposals, run `node tools/npm/run-script.mjs priority:release:conductor -- --dry-run`. - Apply mode requires `RELEASE_CONDUCTOR_ENABLED=1`; if signing material is unavailable, the conductor remains - proposal-only and emits evidence without mutating tags. + Apply mode requires `RELEASE_CONDUCTOR_ENABLED=1`; if signing material is unavailable, the conductor now fails closed + before tag creation and emits readiness evidence without mutating tags. Hosted `schedule` and `workflow_run` conductor lanes stay proposal-only when apply mode is disabled, and dry-runs record advisory-only queue-evidence / no-recent-success diagnostics instead of failing for missing queue artifacts or idle dwell windows. @@ -657,6 +661,9 @@ pwsh -File tools/Print-AgentHandoff.ps1 -ApplyToggles -AutoTrim `node tools/npm/run-script.mjs priority:handoff`, which now prints the entrypoint index alongside the standing-priority snapshot and other handoff summaries. +- Refresh the compact hot/warm durable memory view directly with + `node tools/npm/run-script.mjs priority:context:concentrate`, which writes + `tests/results/_agent/handoff/sagan-context-concentrator.json`. - The overall future-agent handoff contract is summarized in [`docs/knowledgebase/Agent-Handoff-Surfaces.md`](./knowledgebase/Agent-Handoff-Surfaces.md). - See [`WATCHER_TELEMETRY_DX.md`](./WATCHER_TELEMETRY_DX.md) for automation response expectations. diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 2485919db..d51c27a59 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -12,6 +12,56 @@ All values are strings; use `1` / `0` for boolean-style flags. | `LVCOMPARE_PATH` | Optional override for LVCompare.exe (must resolve to canonical path) | | `WORKING_DIRECTORY` | Process CWD when invoking LVCompare | +## Canonical host OS fingerprint + +The authoritative OS/build receipt for the canonical Windows host lane group is +written by: + +```powershell +node tools/npm/run-script.mjs env:labview:2026:host-planes +``` + +Primary artifact: + +- `tests/results/_agent/host-planes/labview-2026-host-plane-report.json` + +Key fields: + +- `host.osFingerprint.fingerprintSha256` +- `host.osFingerprint.isolatedLaneGroupId` +- `host.osFingerprint.canonical.version` +- `host.osFingerprint.canonical.buildNumber` +- `host.osFingerprint.canonical.ubr` +- `host.osFingerprint.canonical.displayVersion` +- `host.osFingerprint.canonical.editionId` + +Treat those fields as the upgrade-attribution baseline for isolated lane +groups. If the fingerprint changes after a host upgrade, classify that as host +OS drift first and only then assess Docker, LabVIEW, or workflow regressions. +`computerName`, branding labels, and boot/install timestamps remain advisory. + +## Docker lane handshake + +Use the lease helper when a background agent needs an isolated Docker lane: + +```powershell +node tools/npm/run-script.mjs priority:lane:docker:handshake -- --action request --lane-id docker-agent-epicurus-linux-01 --agent-id epicurus --agent-class subagent --capability docker-lane +``` + +Primary artifact: + +- `tests/results/_agent/runtime/docker-lane-handshake.json` + +Notes: + +- The helper depends on the canonical host-plane report and the operator cost profile. +- Ordinary subagent Docker leases use the configured operator labor rate as-is. +- Premium simultaneous `docker-lane` plus `native-labview-2026-32` is Sagan-only, requires `operatorAuthorizationRef`, + and bills at `1.5x` the configured operator labor rate. +- The handshake projects `host.osFingerprint.isolatedLaneGroupId` and + `host.osFingerprint.fingerprintSha256` into the lease so Docker-lane usage stays attributable to the canonical host OS + baseline. + ## Dispatcher guards (leak detection / cleanup) | Variable | Notes | @@ -64,6 +114,9 @@ Notes: the wrapper and the TestStand harness invoke the LabVIEW CLI directly to generate an HTML report and enrich `lvcompare-capture.json` with an `environment.cli` metadata block (path, version, reportType, reportPath, status, message). +- `tools/TestStand-CompareHarness.ps1` remains a native-plane consumer surface. It is useful when you need deterministic + warmup plus compare session receipts, but it should still be attributed to the selected native plane rather than to a + separate execution plane. - When a CLI report is produced, embedded artefacts (for example, diff images) are decoded into `tests/results//compare/cli-images/`, and `environment.cli.artifacts` records the report size, image count, and exported file paths so downstream tooling can rehydrate attachments. diff --git a/docs/RELEASE_OPERATIONS_RUNBOOK.md b/docs/RELEASE_OPERATIONS_RUNBOOK.md index 965bbbbf0..7a3fca1d1 100644 --- a/docs/RELEASE_OPERATIONS_RUNBOOK.md +++ b/docs/RELEASE_OPERATIONS_RUNBOOK.md @@ -53,11 +53,20 @@ Configuration path: `Settings -> Environments -> -> Required revie backend stable release as fully complete 5. Finalize release (draft tag + metadata): - `node tools/npm/run-script.mjs release:finalize -- ` -6. Verify rollback drill health: +6. Verify workflow signing readiness before authoritative tag publication: + - `node tools/npm/run-script.mjs priority:release:signing:readiness` + - confirm `tests/results/_agent/release/release-signing-readiness.json` reports: + - `codePathState = ready` + - `signingCapabilityState = configured` + - `signingAuthorityState = ready` + - `releaseConductorApplyState = enabled` + - if `externalBlocker` reports any signing secret, signing authority, or apply-gating blocker, + treat that as an explicit external blocker and stop before rerunning release publication flows +7. Verify rollback drill health: - `node tools/npm/run-script.mjs priority:rollback:drill:health -- --repo ` - confirm `tests/results/_agent/release/rollback-drill-health.json` reports `status=pass` -7. Obtain environment approvals for protected deployments from GitHub UI/mobile. -8. Record evidence links in the governing issue/PR before closure. +8. Obtain environment approvals for protected deployments from GitHub UI/mobile. +9. Record evidence links in the governing issue/PR before closure. ## One-command rollback @@ -138,6 +147,35 @@ For unattended cadence, use `.github/workflows/downstream-onboarding-feedback.ym ## Supply-chain trust remediation classes +Before relying on a local workstation tag, prefer the release conductor +automation path: + +- run `.github/workflows/release-conductor.yml` in apply mode +- if the authoritative release tag already exists but the trust gate reports + `tag-not-annotated` or `tag-signature-unverified`, rerun + `.github/workflows/release-conductor.yml` with: + - the target `version` + - `apply = true` + - `repair_existing_tag = true` +- provision `RELEASE_TAG_SIGNING_PRIVATE_KEY` and optional + `RELEASE_TAG_SIGNING_PUBLIC_KEY` for workflow-owned signing +- optionally set `RELEASE_TAG_SIGNING_IDENTITY_NAME` and + `RELEASE_TAG_SIGNING_IDENTITY_EMAIL` when the signing authority should use an + explicit Git identity override; otherwise the workflow derives the signer + identity from the resolved policy token account +- inspect `tests/results/_agent/release/release-conductor-report.json` + first +- require both: + - `release.tagCreated = true` + - `release.tagPushed = true` + - when repair mode is used: + - `release.repair.status = repaired` + - `release.repair.remoteTargetCommitOid` matches the authoritative commit + - `release.publicationReplay.status = dispatched` + - `release.publicationReplay.ref = develop` + - `release.publicationReplay.tagInputValue` matches the authoritative tag + - the replayed `Release on tag` run succeeds for the repaired tag + When the release trust gate fails, inspect `tests/results/_agent/supply-chain/release-trust-gate.json` and follow the matching remediation path: @@ -148,7 +186,23 @@ matching remediation path: - `tag-signature-cli-unavailable` - Restore GitHub CLI availability on runner and retry release. - `tag-not-annotated`, `tag-signature-unverified` - - Recreate release tag as a signed annotated tag and rerun release. + - Use `node tools/npm/run-script.mjs priority:release:signing:readiness` + first. + - If signing readiness is `ready`, run the release conductor in repair mode + for the target version so the authoritative tag is recreated as a signed + annotated tag without changing the intended release commit. + - Rerun release only after the repair report shows + `release.repair.status = repaired` and + `release.publicationReplay.status = dispatched`. + - Repaired-tag replay now dispatches `release.yml` from `develop` with + `workflow_dispatch.inputs.release_tag=`; do not rely on the + repaired tag itself carrying the newer workflow definition. +- `workflow-signing-secret-missing`, `workflow-signing-secret-unverifiable` +- `workflow-signing-admin-scope-missing`, `workflow-signing-key-missing`, `workflow-signing-authority-unverifiable` +- `release-conductor-apply-disabled`, `release-conductor-apply-unverifiable` + - Use `node tools/npm/run-script.mjs priority:release:signing:readiness` to confirm the blocker, provision or repair + the workflow signing secrets, signing authority, or release-conductor enablement, and only then rerun authoritative + release publication. - `checksum-invalid-line`, `checksum-empty`, `checksum-entry-missing-file`, `checksum-missing-artifact`, `checksum-mismatch` - Regenerate `SHA256SUMS.txt` from fresh artifacts and ensure no post-pack mutation occurred. - `sbom-parse-failed`, `sbom-invalid` @@ -178,6 +232,7 @@ matching remediation path: - `tests/results/_agent/release/release--branch.json` - `tests/results/_agent/release/release--finalize.json` +- `tests/results/_agent/release/release-signing-readiness.json` - `tests/results/_agent/policy/policy-drift-report.json` - `tests/results/_agent/health-snapshot/health-snapshot.json` - `tests/results/_agent/supply-chain/release-trust-gate.json` diff --git a/docs/RELEASE_PROMOTION_CONTRACT.md b/docs/RELEASE_PROMOTION_CONTRACT.md index c585dbbb0..a41dcb69a 100644 --- a/docs/RELEASE_PROMOTION_CONTRACT.md +++ b/docs/RELEASE_PROMOTION_CONTRACT.md @@ -16,6 +16,9 @@ alignment, and evidence ledger expectations. - Certification runbook: `docs/CERTIFICATION_MATRIX.md` - Supply-chain trust gate script: `tools/priority/supply-chain-trust-gate.mjs` - Supply-chain trust gate schema: `docs/schemas/supply-chain-trust-gate-v1.schema.json` +- Release signing readiness script: `tools/priority/release-signing-readiness.mjs` +- Release signing readiness schema: + `docs/schemas/release-signing-readiness-report-v1.schema.json` - Rollback policy: `tools/policy/release-rollback-policy.json` - Rollback command: `tools/priority/rollback-release.mjs` - Rollback drill health gate: `tools/priority/rollback-drill-health.mjs` @@ -137,6 +140,107 @@ Release tags must pass the supply-chain trust gate before GitHub Release publica If the trust gate fails, release publication is blocked (fail-closed) and the report artifact must be used for remediation. +Release publication also emits trust remediation guidance when the trust gate +finds repair-eligible tag failures: + +- Markdown artifact: + `tests/results/_agent/release/release-trust-remediation.md` +- Required behavior: + - preserve the existing release tag identity + - route `tag-not-annotated` and `tag-signature-unverified` to release + conductor repair mode + - instruct repair using: + - `version = ` + - `apply = true` + - `repair_existing_tag = true` + +Authoritative signed tag publication now belongs to the release conductor control +plane: + +- `.github/workflows/release-conductor.yml` may load + `RELEASE_TAG_SIGNING_PRIVATE_KEY` and optional + `RELEASE_TAG_SIGNING_PUBLIC_KEY` +- the workflow may also honor optional repo variables: + - `RELEASE_TAG_SIGNING_IDENTITY_NAME` + - `RELEASE_TAG_SIGNING_IDENTITY_EMAIL` + - when unset, the workflow derives signer identity from the resolved policy + token account before recreating or publishing the signed tag +- when signing material is present, release conductor must: + - configure workflow-owned tag signing + - configure workflow-owned signer identity + - create the signed annotated tag + - push the tag to the authoritative remote for the target repository + - when repairing an existing tag, dispatch `.github/workflows/release.yml` + from `develop` with `workflow_dispatch.inputs.release_tag=` so + publication replays deterministically against the repaired authoritative tag +- `tests/results/_agent/release/release-conductor-report.json` must record: + - signing backend/source + - signer identity used for tag creation/repair + - whether the tag was created + - whether the tag was pushed authoritatively + - whether repair mode was requested/performed for an existing tag + - the authoritative remote tag object/commit used for repair + - which workflow ref carried the replay dispatch + - which explicit tag input was used for repaired-tag publication replay + - whether repaired-tag publication replay was dispatched + - any push failure blocker + +## Workflow signing readiness + +Authoritative release-tag publication must also expose workflow signing readiness +before the release lane is rerun: + +- Report script: `node tools/npm/run-script.mjs priority:release:signing:readiness` +- Report artifact: + `tests/results/_agent/release/release-signing-readiness.json` + +That report distinguishes: + +- `codePathState` + - whether the checked-in release conductor exposes the workflow-owned signing + contract +- `signingCapabilityState` + - whether repository Actions secrets actually provide the signing material +- `publicationState` + - whether authoritative signed tag publication has already succeeded + +If the report emits an external blocker such as: + +- `workflow-signing-secret-missing` +- `workflow-signing-secret-unverifiable` +- `workflow-signing-admin-scope-missing` +- `workflow-signing-key-missing` +- `workflow-signing-authority-unverifiable` +- `release-conductor-apply-disabled` +- `release-conductor-apply-unverifiable` + +promotion remains blocked by external signing readiness. Repair the specific +secret, authority, or apply-gating surface first, then refresh readiness +instead of rerunning release publication just to rediscover the same blocker. + +## Published CompareVI.Tools bundle observer + +Once a release exists, compare can also observe the actually published +`CompareVI.Tools` asset and check whether it is already producer-native for the +template's `vi-history` distribution contract: + +- Observer script: + `node tools/npm/run-script.mjs priority:release:published:bundle` +- Report artifact: + `tests/results/_agent/release/release-published-bundle-observer.json` + +This observer downloads the published `CompareVI.Tools-v*.zip` asset, extracts +`comparevi-tools-release.json`, and proves whether the published bundle already +exposes: + +- `consumerContract.capabilities.viHistory` +- `upstream-producer` / `release-bundle` +- `versionContract.authoritativeConsumerPin` +- the declared bundle import path and referenced consumer contract paths + +That surface is the compare-side bridge between signed release publication and +template issue `LabviewGitHubCiTemplate#18`. + ## Rollback drill health gate Release tags must pass rollback drill health before GitHub Release publication: diff --git a/docs/SELFHOSTED_CI_SETUP.md b/docs/SELFHOSTED_CI_SETUP.md index 795a8b0f7..e70a0e352 100644 --- a/docs/SELFHOSTED_CI_SETUP.md +++ b/docs/SELFHOSTED_CI_SETUP.md @@ -13,6 +13,9 @@ Minimal steps to provision a Windows runner suitable for LVCompare workflows. ## Runner configuration - Labels: `self-hosted`, `Windows`, `X64`. +- Prefer a small number of coarse GitHub runner labels over one runner registration per background agent. +- Isolated Docker lanes should be leased locally through the Docker-lane handshake helper instead of by creating a + permanent GitHub Actions runner service for each agent. - Service account requires access to VI fixtures and temporary directories. - Environment variables (system scope recommended): - `LV_BASE_VI`, `LV_HEAD_VI` (sample VIs). @@ -27,15 +30,46 @@ Minimal steps to provision a Windows runner suitable for LVCompare workflows. Test-Path 'C:\Program Files\National Instruments\Shared\LabVIEW Compare\LVCompare.exe' [Environment]::GetEnvironmentVariable('LV_BASE_VI', 'Machine') [Environment]::GetEnvironmentVariable('LV_HEAD_VI', 'Machine') +node tools/npm/run-script.mjs env:labview:2026:host-planes +node tools/npm/run-script.mjs priority:lane:docker:handshake -- --action request --lane-id docker-agent-check-01 --agent-id operator --agent-class other --capability docker-lane ``` Dispatch `Pester (self-hosted, real CLI)` manually to confirm environment validation and tests pass. +When the host is part of the canonical isolated lane group, treat the generated +host-plane report as the OS/build source of truth: + +- `tests/results/_agent/host-planes/labview-2026-host-plane-report.json` +- `host.osFingerprint.fingerprintSha256` +- `host.osFingerprint.isolatedLaneGroupId` +- `host.osFingerprint.canonical.version` +- `host.osFingerprint.canonical.buildNumber` +- `host.osFingerprint.canonical.ubr` + +Future host refreshes and isolated lane groups should compare against that +fingerprint before blaming LabVIEW, Docker, or runner drift on the workload +itself. + +When the host also carries deterministic compare tooling, the TestStand harness +is a supported native-plane consumer: + +- `pwsh -NoLogo -NoProfile -File tools/TestStand-CompareHarness.ps1` + `-BaseVi -HeadVi -OutputRoot` + `tests/results/teststand-session -Warmup detect -RenderReport` + +That harness does not define a separate runner class. It consumes one of the +native LabVIEW planes and should be attributed to the same host OS fingerprint +and isolated lane group. + ## Maintenance - Keep LabVIEW and Windows patched. - Monitor runner health (Actions → Runners). - Rotate PATs and verify secrets annually. - Periodically refresh fixture VIs and environment variables. +- After a Windows upgrade, rerun + `node tools/npm/run-script.mjs env:labview:2026:host-planes` and compare the + previous versus current `host.osFingerprint` values before reclassifying lane + regressions. A changed fingerprint means the canonical host OS baseline moved. Further reading: [`docs/E2E_TESTING_GUIDE.md`](./E2E_TESTING_GUIDE.md), [`docs/ENVIRONMENT.md`](./ENVIRONMENT.md). diff --git a/docs/SINGLE_HOST_LABVIEW_2026_PLANES.md b/docs/SINGLE_HOST_LABVIEW_2026_PLANES.md index 1e7821dda..12a699ccf 100644 --- a/docs/SINGLE_HOST_LABVIEW_2026_PLANES.md +++ b/docs/SINGLE_HOST_LABVIEW_2026_PLANES.md @@ -16,6 +16,12 @@ Treat these four planes as distinct: Do not collapse them into a generic “LabVIEW 2026 host” concept. The repo contracts and artifacts are written so future agents can tell which plane actually produced the evidence. +Treat the TestStand harness as a host-plane consumer, not a fifth plane. It is a deterministic wrapper around +Windows-native LabVIEW warmup and LVCompare session capture, so its receipts still belong to one of the native planes +rather than to a separate execution category. + +TestStand is a Windows-only runtime surface. It should not be modeled as a Linux or Docker execution runtime. + ## Shadow policy `native-labview-2026-32` is a shadow acceleration surface, not an authoritative @@ -50,6 +56,109 @@ The native planes are also distinct: They may share supporting tooling paths, but they remain different host planes and must not be reported as one surface. +## Lease-backed lane protocol + +Shared host surfaces must use a software four-phase handshake before an agent treats a Docker or premium native lane as +owned: + +1. `request` +2. `grant` +3. `commit` plus `heartbeat` +4. `release` + +Use the checked-in helper when you need a replayable lease receipt: + +- `node tools/npm/run-script.mjs priority:lane:docker:handshake -- --action request` + `--lane-id docker-agent-epicurus-linux-01 --agent-id epicurus` + `--agent-class subagent --capability docker-lane` + +Use the execution-cell helper when an agent needs an isolated TestStand-owned native session cell: + +- `node tools/npm/run-script.mjs priority:lane:execution-cell -- --action request` + `--cell-id exec-cell-hooke-01 --agent-id hooke --agent-class subagent` + `--suite-class dual-plane-parity --plane-binding native-labview-2026-64` + `--capability teststand-harness` + +Use the bundle helper when one agent needs a Windows-native execution cell plus an isolated Docker lane under one +durable receipt: + +- `node tools/npm/run-script.mjs priority:lane:execution-cell:bundle -- --action request` + `--cell-id exec-cell-sagan-kernel-01 --lane-id docker-agent-sagan-kernel-01` + `--agent-id sagan --agent-class sagan --cell-class kernel-coordinator` + `--suite-class dual-plane-parity --plane-binding dual-plane-parity` + `--capability teststand-harness --capability docker-lane` + `--operator-authorization-ref budget-auth://operator/session-2026-03-24` + +At `commit`, the execution-cell lease and Docker-lane handshake should bind to each other reciprocally through their +child receipts. The execution-cell commit now records `dockerLaneId` plus `dockerLaneLeaseId`, and the Docker-lane +commit records `executionCellId` plus `executionCellLeaseId`. Premium activation is not complete until both receipts +carry those reciprocal ids for the same agent-owned host fingerprint. + +The resulting report is written to: + +- `tests/results/_agent/runtime/docker-lane-handshake.json` + +- `tests/results/_agent/runtime/execution-cell-lease.json` + +- `tests/results/_agent/runtime/execution-cell-bundle.json` + +The durable handshake state lives under the Git common-dir so clean worktrees share one lease view. + +### Premium Sagan dual-lane rule + +Only `sagan` may lease `docker-lane` and `native-labview-2026-32` simultaneously. + +- required capabilities: + - `docker-lane` + - `native-labview-2026-32` +- required authorization: + - `operatorAuthorizationRef` +- billable multiplier: + - `1.5x` the configured operator labor rate + +Subagents may lease isolated Docker lanes, but they must not activate the premium dual-lane combination. + +### Execution cells and harness instances + +Each agent should lease an execution cell before it launches a native TestStand compare session. + +- one agent -> one execution cell lease +- one execution cell -> one owning TestStand harness instance +- dual-plane parity -> one coordinator harness instance plus one child harness instance per native plane + +Project the execution-cell lease into the harness with: + +- `-ExecutionCellLeasePath` +- `-ExecutionCellId` +- `-ExecutionCellLeaseId` +- `-HarnessInstanceId` + +That keeps session receipts attributable to: + +- the agent +- the execution cell +- the owning harness instance +- the canonical host OS fingerprint + +Execution cells that use `teststand-compare-harness` must bind only to Windows-native planes: + +- `native-labview-2026-64` +- `native-labview-2026-32` +- `dual-plane-parity` + +Bindings like `docker-desktop/linux-container-2026` or other container/Linux plane identifiers are invalid for +TestStand-owned cells and should fail closed at lease grant time. + +When a single agent needs both a Windows-native execution cell and a Docker lane, prefer the bundle helper over +manual choreography. It keeps: + +- the execution cell receipt +- the Docker lane handshake +- premium Sagan rate enforcement +- rollback of partial grants + +under one machine-readable bundle report instead of leaving the allocation split across two separate ad hoc calls. + ## Authoritative entry points Use these commands as the checked-in operator surfaces: @@ -75,10 +184,20 @@ Use these commands as the checked-in operator surfaces: the recommendation that led to the applied bundle without leaving the checked-in report chain. 5. Fast Docker Desktop lane loops: + - `node tools/npm/run-script.mjs priority:lane:docker:handshake -- --action inspect --lane-id ` - `pwsh -NoLogo -NoProfile -File tools/Test-DockerDesktopFastLoop.ps1 -LaneScope linux -StepTimeoutSeconds 600` - `pwsh -NoLogo -NoProfile -File tools/Test-DockerDesktopFastLoop.ps1 -LaneScope windows -StepTimeoutSeconds 600` - `pwsh -NoLogo -NoProfile -File tools/Test-DockerDesktopFastLoop.ps1 -LaneScope both -StepTimeoutSeconds 600` -6. Differentiated diagnostics replay: +6. TestStand harness session wrapper: + - `pwsh -NoLogo -NoProfile -File tools/TestStand-CompareHarness.ps1 -BaseVi -HeadVi -OutputRoot tests/results/teststand-session -Warmup detect -RenderReport` + - Use this when the host plane needs a deterministic native compare session with a replayable `session-index.json`. + - To bind the session to an execution cell, add: + - `-ExecutionCellLeasePath -ExecutionCellId -ExecutionCellLeaseId -HarnessInstanceId ` + - For native LabVIEW 2026 x64/x32 parity on the same host, add: + - `-SuiteClass dual-plane-parity -LabVIEW64ExePath -LabVIEW32ExePath ` + - Dual-plane parity still treats the harness as a host-plane consumer. It does not create a new authority plane; + it produces a parity receipt across the two existing native planes. +7. Differentiated diagnostics replay: - `node tools/npm/run-script.mjs history:diagnostics:show -- --ResultsRoot tests/results/local-parity/windows` The replay helper is the fastest operator readback. It prints the host-plane report first and then the differentiated @@ -104,25 +223,29 @@ Use these artifacts as the machine-readable source of truth: - `tests/results/_agent/runtime/concurrent-lane-apply-receipt.json` 4. Concurrent lane status receipt: - `tests/results/_agent/runtime/concurrent-lane-status-receipt.json` -5. Fast-loop readiness envelope: +5. Docker lane handshake receipt: + - `tests/results/_agent/runtime/docker-lane-handshake.json` +6. Execution cell lease receipt: + - `tests/results/_agent/runtime/execution-cell-lease.json` +7. Execution cell bundle receipt: + - `tests/results/_agent/runtime/execution-cell-bundle.json` +8. Fast-loop readiness envelope: - `docker-runtime-fastloop-readiness.json` - `docker-runtime-fastloop-readiness.md` -6. Fast-loop proof bundle when produced: +9. Fast-loop proof bundle when produced: - `docker-fast-loop-proof-*.json` -7. Top-level fast-loop GitHub outputs when `tools/Test-DockerDesktopFastLoop.ps1` runs inside GitHub Actions: - - `docker-fast-loop-summary-path` - - `docker-fast-loop-status-path` - - `docker-fast-loop-host-plane-summary-path` - - `docker-fast-loop-host-plane-summary-status` - - `docker-fast-loop-host-plane-summary-sha256` - - `docker-fast-loop-host-plane-summary-reason` -8. Top-level fast-loop Step Summary when `tools/Test-DockerDesktopFastLoop.ps1` receives `-StepSummaryPath`: - - `Summary Path` - - `Status Path` - - `Host Plane Summary Path` - - `Host Plane Summary Status` - - `Host Plane Summary SHA-256` - - `Host Plane Summary Reason` +10. Top-level fast-loop GitHub outputs when + `tools/Test-DockerDesktopFastLoop.ps1` runs inside GitHub Actions: + `docker-fast-loop-summary-path`, `docker-fast-loop-status-path`, + `docker-fast-loop-host-plane-summary-path`, + `docker-fast-loop-host-plane-summary-status`, + `docker-fast-loop-host-plane-summary-sha256`, and + `docker-fast-loop-host-plane-summary-reason` +11. Top-level fast-loop Step Summary when + `tools/Test-DockerDesktopFastLoop.ps1` receives `-StepSummaryPath`: + `Summary Path`, `Status Path`, `Host Plane Summary Path`, + `Host Plane Summary Status`, `Host Plane Summary SHA-256`, and + `Host Plane Summary Reason` When the local fast loop runs, prefer the readiness envelope for lane verdicts and the host-plane report for the native 64-bit versus native 32-bit split. For Docker lane replay, use the readiness envelope together with @@ -135,6 +258,8 @@ Use the artifacts in this order: 1. `labview-2026-host-plane-report.json` - confirms the native `x64` and `x32` plane readiness - shows the host/runner identity + - records `host.osFingerprint` as the canonical Windows upgrade baseline for + the isolated lane group - records the mutually exclusive Docker pair and the candidate parallel pairs 2. `labview-2026-host-plane-summary.md` - records the operator-facing summary paired with the report @@ -154,33 +279,45 @@ Use the artifacts in this order: - records merge-queue-backed PR state when a PR can be resolved from the applied branch or explicit selector - keeps deferred manual Docker and host-native shadow lanes explicit for the orchestrator - records an orchestrator disposition so worker-slot release decisions do not depend on ad hoc GitHub polling -6. `docker-runtime-fastloop-readiness.json` +6. `docker-lane-handshake.json` + - records request, grant, commit/heartbeat, and release state for one isolated Docker lane + - projects `host.osFingerprint.isolatedLaneGroupId` into the lease + - records whether premium Sagan dual-lane mode was requested or granted + - records the billable operator-equivalent rate derived from the operator cost profile +7. `execution-cell-bundle.json` + - records one agent-owned allocation across a Windows-native execution cell and an optional isolated Docker lane + - records the effective billable rate once, even when both child resources are active + - rolls back partial grants so failed bundle admission does not strand half-allocated state + - keeps premium Sagan dual-lane authorization and Windows-native TestStand requirements in one receipt +8. `docker-runtime-fastloop-readiness.json` - records the fast-loop verdict and lane outcomes - carries the differentiated Docker Desktop plane projection - records `hostPlaneSummary.path`, `hostPlaneSummary.status`, and `hostPlaneSummary.sha256` - records whether Docker exclusivity was required and whether it was satisfied -7. `docker-fast-loop-proof-*.json` +9. `docker-fast-loop-proof-*.json` - records `hostPlaneSummaryPath` - records `hostPlaneSummaryProvenance` - records `hashes.hostPlaneSummarySha256` - projects GitHub outputs: - `docker-fast-loop-proof-host-plane-summary-path` - `docker-fast-loop-proof-host-plane-summary-sha256` -8. Top-level `tools/Test-DockerDesktopFastLoop.ps1` GitHub outputs - - project `docker-fast-loop-summary-path` and `docker-fast-loop-status-path` - - project `docker-fast-loop-host-plane-summary-path` - - project `docker-fast-loop-host-plane-summary-status` - - project `docker-fast-loop-host-plane-summary-sha256` - - project `docker-fast-loop-host-plane-summary-reason` - - keep success and fail-closed summary provenance available to downstream workflow consumers without reopening JSON -9. Top-level `tools/Test-DockerDesktopFastLoop.ps1` Step Summary - - appends `### Docker Fast Loop Summary` - - prints the same summary path and status path surfaced through GitHub outputs - - prints host-plane summary path, status, SHA-256, and fail-closed reason - - preserves the missing-summary reason before the script throws -10. `history:diagnostics:show` - - replays the same distinction in console form for the operator - - prints `[host-plane-split][summary] status= sha256=` when summary provenance exists +10. Top-level `tools/Test-DockerDesktopFastLoop.ps1` GitHub outputs: + project `docker-fast-loop-summary-path`, + `docker-fast-loop-status-path`, `docker-fast-loop-host-plane-summary-path`, + `docker-fast-loop-host-plane-summary-status`, + `docker-fast-loop-host-plane-summary-sha256`, and + `docker-fast-loop-host-plane-summary-reason`. Keep success and fail-closed + summary provenance available to downstream workflow consumers without + reopening JSON. +11. Top-level `tools/Test-DockerDesktopFastLoop.ps1` Step Summary: + append `### Docker Fast Loop Summary`, print the same summary path and + status path surfaced through GitHub outputs, print host-plane summary path, + status, SHA-256, and fail-closed reason, and preserve the missing-summary + reason before the script throws. +12. `history:diagnostics:show`: + replay the same distinction in console form for the operator and print + `[host-plane-split][summary] status= sha256=` when + summary provenance exists. If any of those surfaces disagree on the selected plane or exclusivity state, stop and treat the run as not yet trustworthy. @@ -203,6 +340,26 @@ trustworthy. merge-queued, or fully settled without raw GitHub polling. 8. When summarizing a run, name the exact plane identifier instead of saying “host” or “Docker” without qualification. 9. Do not treat `native-labview-2026-32` as a release or CI authority surface; it is a shadow accelerator only. +10. Treat `tools/TestStand-CompareHarness.ps1` as a deterministic consumer of a native plane. Its `session-index.json` + is useful evidence, but it does not create a new authority plane. +11. Use the Docker-lane handshake before assigning an isolated Docker lane to a background agent so exclusivity, + billable rate, and host fingerprint stay replayable. +12. When a parity run needs both native LabVIEW 2026 planes at once, use `-SuiteClass dual-plane-parity` and keep the + output tied to the same `host.osFingerprint.isolatedLaneGroupId` as the surrounding host-plane receipts. +13. Only Sagan may request simultaneous `docker-lane` plus + `native-labview-2026-32`, and that request must carry an explicit + `operatorAuthorizationRef`. +14. Use `priority:lane:execution-cell:bundle` when one agent needs both a + Windows-native TestStand cell and an isolated Docker lane. It is the + preferred control surface for Sagan kernel cells and for future per-agent + execution cells that need container-local tooling alongside native Windows + LabVIEW work. +15. Compare `host.osFingerprint.fingerprintSha256` before and after host + upgrades. If it changes, treat the new value as a moved canonical host OS + baseline rather than attributing the drift to the workload first. +16. Use `host.osFingerprint.isolatedLaneGroupId` as the replayable identifier + for this canonical Windows baseline when documenting or comparing isolated + local lane groups. ## Related contracts @@ -210,10 +367,16 @@ trustworthy. - [concurrent-lane-apply-receipt-v1.schema.json](schemas/concurrent-lane-apply-receipt-v1.schema.json) - [concurrent-lane-status-receipt-v1.schema.json](schemas/concurrent-lane-status-receipt-v1.schema.json) - [concurrent-lane-plan-v1.schema.json](schemas/concurrent-lane-plan-v1.schema.json) +- [docker-lane-handshake-v1.schema.json](schemas/docker-lane-handshake-v1.schema.json) +- [docker-lane-handshake-report-v1.schema.json](schemas/docker-lane-handshake-report-v1.schema.json) +- [execution-cell-bundle-report-v1.schema.json](schemas/execution-cell-bundle-report-v1.schema.json) - [labview-2026-host-plane-report-v1.schema.json](schemas/labview-2026-host-plane-report-v1.schema.json) - [Write-LabVIEW2026HostPlaneDiagnostics.ps1](../tools/Write-LabVIEW2026HostPlaneDiagnostics.ps1) - [concurrent-lane-apply.mjs](../tools/priority/concurrent-lane-apply.mjs) - [concurrent-lane-status.mjs](../tools/priority/concurrent-lane-status.mjs) +- [execution-cell-bundle.mjs](../tools/priority/execution-cell-bundle.mjs) - [concurrent-lane-plan.mjs](../tools/priority/concurrent-lane-plan.mjs) +- [docker-lane-handshake.mjs](../tools/priority/docker-lane-handshake.mjs) - [Test-DockerDesktopFastLoop.ps1](../tools/Test-DockerDesktopFastLoop.ps1) +- [TestStand-CompareHarness.ps1](../tools/TestStand-CompareHarness.ps1) - [Show-DockerFastLoopDiagnostics.ps1](../tools/Show-DockerFastLoopDiagnostics.ps1) diff --git a/docs/TESTSTAND_INTEGRATION_PLAN.md b/docs/TESTSTAND_INTEGRATION_PLAN.md index 27347b9c4..3f9a4896e 100644 --- a/docs/TESTSTAND_INTEGRATION_PLAN.md +++ b/docs/TESTSTAND_INTEGRATION_PLAN.md @@ -41,9 +41,83 @@ stays available even when fresh harness artefacts are not present. `tests/results/teststand-session/session-index.json` produced locally, allowing agents to verify shape changes without rerunning LabVIEW. +TestStand itself is Windows-only. Treat it as a Windows-native execution-cell runtime, not as a Linux or Docker +runtime. Linux/container planes may still participate elsewhere in the host fabric, but not as TestStand-owned harness +cells. + Validate the session index with `node tools/npm/run-script.mjs session:teststand:validate` so schema regressions surface immediately when the harness outputs change. +### 1.2.1 Dual-plane parity command + +The harness now also supports an opt-in LabVIEW 2026 native parity suite that launches both native planes +simultaneously and writes a parity-aware `session-index.json`. + +```powershell +pwsh -NoLogo -NoProfile -File tools/TestStand-CompareHarness.ps1 ` + -BaseVi (Resolve-Path .\VI1.vi) ` + -HeadVi (Resolve-Path .\VI2.vi) ` + -OutputRoot tests/results/teststand-session ` + -SuiteClass dual-plane-parity ` + -LabVIEW64ExePath 'C:\Program Files\National Instruments\LabVIEW 2026\LabVIEW.exe' ` + -LabVIEW32ExePath 'C:\Program Files (x86)\National Instruments\LabVIEW 2026\LabVIEW.exe' ` + -Warmup detect ` + -RenderReport +``` + +Parity sessions use `schema = teststand-compare-session/v2` and include: + +- `suiteClass = dual-plane-parity` +- `primaryPlane = native-labview-2026-64` +- `requestedSimultaneous = true` +- `processModel.runtimeSurface = windows-native-teststand` +- `processModel.processModelClass = parallel-process-model` +- `planes.x64` / `planes.x32` single-plane session records +- `parity.status`, `mismatchCount`, and field-level mismatch details + +`Run-DX.ps1` forwards the same contract through: + +- `-TestStandSuiteClass dual-plane-parity` +- `-LabVIEW64ExePath` +- `-LabVIEW32ExePath` + +This keeps dual-plane parity runs visible in `tests/results/_agent/dx-status.json` without changing the default +single-plane TestStand path. + +### 1.2.2 Execution-cell-owned harness instances + +Treat the harness as an execution-cell consumer, not as ambient host state. + +- each agent leases one execution cell +- that execution cell owns its harness instance +- dual-plane parity keeps one coordinator harness instance plus one child harness instance per plane + +The harness now accepts: + +- `-ExecutionCellLeasePath` +- `-ExecutionCellId` +- `-ExecutionCellLeaseId` +- `-HarnessInstanceId` + +Session receipts project both: + +- `executionCell` +- `harnessInstance` +- `processModel` + +That keeps TestStand evidence attributable to one agent-owned memory/process boundary instead of an unscoped host run. + +The explicit process-model contract now makes 3 things machine-readable: + +- `executionCell.runtimeSurface = windows-native-teststand` +- `harnessInstance.processModelClass = sequential-process-model|parallel-process-model` +- `processModel.rootHarnessInstanceId` / `planeCount` so coordinator-owned parity runs stay attributable even when child + plane receipts are inspected independently + +When the owning agent also needs an isolated Docker lane for repeatable adjunct tooling, prefer +`priority:lane:execution-cell:bundle` over separate lease calls. That bundle projects one effective billable rate, +rolls back partial allocations, and keeps the Windows-only TestStand contract attached to the same cell-owned receipt. + > **Note**: `-CloseLabVIEW` / `-CloseLVCompare` now queue post-run cleanup requests. The helpers do not invoke the close > scripts inline; instead `tools/Post-Run-Cleanup.ps1` consumes the requests after `Invoke-PesterTests.ps1` completes, > guaranteeing a single LabVIEWCLI invocation per job. @@ -109,8 +183,9 @@ deterministic: ### 2.2 Telemetry -- Update `tools/Ensure-SessionIndex.ps1` to recognise the `teststand-compare-session/v1` schema and emit summary lines - (`exit`, `diff`, elapsed seconds). +- Update `tools/Ensure-SessionIndex.ps1` to recognise both `teststand-compare-session/v1` and + `teststand-compare-session/v2`, emitting summary lines (`exit`, `diff`, elapsed seconds) and parity status when the + dual-plane suite is used. - Amend the Summary appender to group TestStand runs under a dedicated heading (`### TestStand Compare Session`). - Capture the warmup/compare NDJSON files via the artifact manifest so they are available for debugging. diff --git a/docs/archive/releases/RELEASE_NOTES_v0.6.4-rc.2.md b/docs/archive/releases/RELEASE_NOTES_v0.6.4-rc.2.md new file mode 100644 index 000000000..8509383e5 --- /dev/null +++ b/docs/archive/releases/RELEASE_NOTES_v0.6.4-rc.2.md @@ -0,0 +1,42 @@ + +# Release v0.6.4-rc.2 + +Highlights + +- The next RC publishes the producer-owned Docker contract in + `CompareVI.Tools`. + - `comparevi-tools-release.json` now carries + `consumerContract.capabilities.dockerProfile`. + - The same payload now exposes the producer-owned + `consumerContract.dockerImageContract` source needed by downstream Docker + distributors. +- `v0.6.4-rc.2` is the honest follow-up to the published `v0.6.4-rc.1` bundle. + - `v0.6.4-rc.1` is now authoritative for the producer-native `vi-history` + contract. + - It still predates commit `5969b9114cafdab989dadb70c5bec188b07a3996`, so its + published bundle does not include the new docker-profile capability. +- The template Docker-profile rail stays dependency-driven. + - `LabviewGitHubCiTemplate#20` remains blocked until this RC is published and + the producer bundle proves the Docker contract authoritatively. + - The template should consume the published producer contract, not invent a + template-local Docker image convention. + +Upgrade Notes + +- This is a release candidate. The final `v0.6.4` release still depends on RC + validation, authoritative bundle publication, and the template follow-up + consuming the published contract cleanly. +- The replay-routing repair from `#1942` stays part of the release story, but it + is no longer the active publication blocker. The remaining release objective + is publishing the newer producer contract on the next RC identity. + +Validation Checklist + +- [x] `node tools/npm/run-script.mjs release:branch -- 0.6.4-rc.2` +- [ ] Live hosted RC validation on `release/v0.6.4-rc.2` +- [ ] `node tools/npm/run-script.mjs release:finalize -- 0.6.4-rc.2` +- [ ] `node tools/npm/run-script.mjs priority:release:conductor -- --apply --channel rc --version 0.6.4-rc.2` +- [ ] `node tools/npm/run-script.mjs priority:release:published:bundle` +- [ ] Published `CompareVI.Tools-v0.6.4-rc.2.zip` proves both: + - producer-native `vi-history` + - producer-owned `dockerProfile` diff --git a/docs/documentation-manifest.json b/docs/documentation-manifest.json index c12af1157..2b3862a5f 100644 --- a/docs/documentation-manifest.json +++ b/docs/documentation-manifest.json @@ -2,7 +2,7 @@ "$schema": "./schemas/documentation-manifest-v1.schema.json", "schema": "documentation-manifest-v1", "version": "1.0.0", - "updated": "2026-03-22T23:35:00Z", + "updated": "2026-03-24T02:15:00Z", "entries": [ { "name": "Root Entry Points", @@ -31,6 +31,33 @@ "docs/release/TAG_PREP_CHECKLIST.md" ] }, + { + "name": "Release Signing Readiness Contracts", + "category": "supporting", + "status": "reference", + "description": "Workflow-owned tag-signing readiness report contract, focused tests, and runbook references used to surface signing capability as an explicit external blocker before release attempts.", + "files": [ + "docs/RELEASE_OPERATIONS_RUNBOOK.md", + "docs/RELEASE_PROMOTION_CONTRACT.md", + "docs/schemas/release-signing-readiness-report-v1.schema.json", + "tools/priority/release-signing-readiness.mjs", + "tools/priority/__tests__/release-signing-readiness.test.mjs", + "tools/priority/__tests__/release-signing-readiness-schema.test.mjs" + ] + }, + { + "name": "Published CompareVI.Tools Bundle Observer", + "category": "supporting", + "status": "reference", + "description": "Published-release observer that downloads the latest CompareVI.Tools asset and checks whether the producer-native vi-history contract is live for downstream template distribution.", + "files": [ + "docs/RELEASE_PROMOTION_CONTRACT.md", + "docs/schemas/release-published-bundle-observer-report-v1.schema.json", + "tools/priority/release-published-bundle-observer.mjs", + "tools/priority/__tests__/release-published-bundle-observer.test.mjs", + "tools/priority/__tests__/release-published-bundle-observer-schema.test.mjs" + ] + }, { "name": "Docs Tree", "category": "docs", @@ -134,6 +161,8 @@ "docs/schemas/agent-cost-turn-v1.schema.json", "docs/schemas/agent-cost-rollup-v1.schema.json", "docs/schemas/average-issue-cost-scorecard-v1.schema.json", + "docs/schemas/github-comment-budget-hook-policy-v1.schema.json", + "docs/schemas/github-comment-budget-hook-report-v1.schema.json", "docs/schemas/operator-cost-profile-v1.schema.json", "docs/schemas/pr-spend-projection-v1.schema.json", "tools/priority/__fixtures__/agent-cost-rollup/live-turn-estimated.json", @@ -142,13 +171,17 @@ "tools/priority/__fixtures__/agent-cost-rollup/invoice-turn-next-baseline.json", "tools/priority/__fixtures__/agent-cost-rollup/invoice-turn-baseline-reconciled.json", "tools/priority/__fixtures__/agent-cost-rollup/private-invoice-metadata-sample.json", + "tools/policy/github-comment-budget-hook.json", "tools/policy/operator-cost-profile.json", "tools/priority/agent-cost-invoice-normalize.mjs", "tools/priority/agent-cost-invoice-turn.mjs", "tools/priority/agent-cost-turn.mjs", "tools/priority/agent-cost-rollup.mjs", "tools/priority/average-issue-cost-scorecard.mjs", + "tools/priority/github-comment-budget-hook.mjs", "tools/priority/pr-spend-projection.mjs", + "tools/priority/__tests__/github-comment-budget-hook.test.mjs", + "tools/priority/__tests__/github-comment-budget-hook-schema.test.mjs", "tools/priority/__tests__/average-issue-cost-scorecard.test.mjs", "tools/priority/__tests__/average-issue-cost-scorecard-schema.test.mjs", "tools/priority/__tests__/agent-cost-invoice-normalize.test.mjs", @@ -435,7 +468,7 @@ "name": "Host Plane Diagnostics Contracts", "category": "supporting", "status": "reference", - "description": "Machine-readable report and summary contracts, VI-history local runtime-plane receipts, helper entrypoints, fast-loop provenance surfaces, and the single-host runbook for the LabVIEW 2026 host-plane split.", + "description": "Machine-readable report and summary contracts, VI-history local runtime-plane receipts, lease-backed Docker-lane handshake contracts, TestStand harness host-plane attribution, helper entrypoints, fast-loop provenance surfaces, and the single-host runbook for the LabVIEW 2026 host-plane split.", "files": [ "docs/DEVELOPER_GUIDE.md", "docs/SINGLE_HOST_LABVIEW_2026_PLANES.md", @@ -447,18 +480,34 @@ "docs/schemas/comparevi-local-runtime-health-v1.schema.json", "docs/schemas/comparevi-local-runtime-lease-v1.schema.json", "docs/schemas/comparevi-local-runtime-state-v1.schema.json", + "docs/schemas/docker-lane-handshake-v1.schema.json", + "docs/schemas/docker-lane-handshake-report-v1.schema.json", + "docs/schemas/execution-cell-bundle-report-v1.schema.json", + "docs/schemas/execution-cell-lease-v1.schema.json", + "docs/schemas/execution-cell-lease-report-v1.schema.json", "docs/schemas/labview-2026-host-plane-report-v1.schema.json", "tests/VIHistoryLocalAcceleration.Tests.ps1", + "tests/TestStand-CompareHarness.Tests.ps1", "tools/Build-VIHistoryDevImage.ps1", "tools/Invoke-VIHistoryLocalOperatorSession.ps1", "tools/Invoke-VIHistoryLocalRefinement.ps1", + "tools/TestStand-CompareHarness.ps1", "tools/Manage-VIHistoryRuntimeInDocker.ps1", "tools/LabVIEW2026HostPlaneDiagnostics.psm1", "tools/Show-DockerFastLoopDiagnostics.ps1", "tools/Test-DockerDesktopFastLoop.ps1", "tools/Write-DockerFastLoopProof.ps1", "tools/Write-DockerFastLoopReadiness.ps1", - "tools/Write-LabVIEW2026HostPlaneDiagnostics.ps1" + "tools/Write-LabVIEW2026HostPlaneDiagnostics.ps1", + "tools/priority/docker-lane-handshake.mjs", + "tools/priority/execution-cell-bundle.mjs", + "tools/priority/execution-cell-lease.mjs", + "tools/priority/__tests__/docker-lane-handshake.test.mjs", + "tools/priority/__tests__/docker-lane-handshake-schema.test.mjs", + "tools/priority/__tests__/execution-cell-bundle.test.mjs", + "tools/priority/__tests__/execution-cell-bundle-schema.test.mjs", + "tools/priority/__tests__/execution-cell-lease.test.mjs", + "tools/priority/__tests__/execution-cell-lease-schema.test.mjs" ] }, { diff --git a/docs/knowledgebase/Agent-Cost-Telemetry-Surfaces.md b/docs/knowledgebase/Agent-Cost-Telemetry-Surfaces.md index c88387ccd..f7957de58 100644 --- a/docs/knowledgebase/Agent-Cost-Telemetry-Surfaces.md +++ b/docs/knowledgebase/Agent-Cost-Telemetry-Surfaces.md @@ -258,6 +258,47 @@ There is now a local-only normalization helper for private invoice metadata: - helper: `tools/priority/agent-cost-invoice-normalize.mjs` This helper intentionally does not scrape PDFs directly in the stable slice. + +## Durable GitHub Comment Budget Hook + +Automation-authored GitHub comments now have a checked-in budget attestation +surface so cost state survives session compaction and comment history remains a +durable breadcrumb for later agents. + +- schema: `docs/schemas/github-comment-budget-hook-policy-v1.schema.json` +- schema: `docs/schemas/github-comment-budget-hook-report-v1.schema.json` +- policy: `tools/policy/github-comment-budget-hook.json` +- helper: `tools/priority/github-comment-budget-hook.mjs` +- npm surface: `priority:cost:comment-hook` +- wrappers: + - `tools/Post-IssueComment.ps1` + - `tools/Post-PullRequestComment.ps1` + +The hook appends a machine-readable and human-readable budget block to GitHub +comments with these markers: + +- `` +- `` + +The hook projects: + +- token spend +- observed operator-equivalent labor +- observed blended lower-bound spend +- operator budget cap / remaining lower bound +- operational invoice-turn remainder +- reserved calibration funding window state +- live/background/total turn counts + +The checked-in policy keeps the calibration window reserved instead of silently +consuming it. The current intent is: + +- operational invoice turn may spend +- calibration invoice turn remains on hold + +Use the wrappers by default so GitHub issue and PR comments pick up the durable +budget hook automatically. Pass `-SkipBudgetHook` only for narrow test or +break-glass cases where the attestation must be suppressed deliberately. Instead, it normalizes a local private metadata JSON payload into a checked-in invoice-turn contract. That keeps raw invoice documents out of the repository while still reducing manual transcription drift. diff --git a/docs/knowledgebase/Agent-Handoff-Surfaces.md b/docs/knowledgebase/Agent-Handoff-Surfaces.md index aef0e817a..b7078a2b8 100644 --- a/docs/knowledgebase/Agent-Handoff-Surfaces.md +++ b/docs/knowledgebase/Agent-Handoff-Surfaces.md @@ -23,7 +23,12 @@ entrypoint and machine-generated live state. template pivot readiness. - It refreshes `tests/results/_agent/handoff/autonomous-governor-summary.json`, which is the top-level machine-readable rollup for the autonomous governor's - current mode, wake disposition, funding-quality posture, and next owner. + current mode, wake disposition, funding-quality posture, release-signing + readiness, and next owner. +- It refreshes `tests/results/_agent/handoff/sagan-context-concentrator.json`, + which is the machine-readable hot/warm memory concentrator for the current + standing issue, current owner decision, recent subagent episodes, and durable + blocker context. - It refreshes `tests/results/_agent/handoff/autonomous-governor-portfolio-summary.json`, which is the cross-repo machine-readable rollup for compare, canonical @@ -43,6 +48,9 @@ entrypoint and machine-generated live state. - `node tools/npm/run-script.mjs priority:handoff` imports the handoff bundle and prints the entrypoint index, standing-priority snapshot, and other current summaries. +- `node tools/npm/run-script.mjs priority:context:concentrate` + rebuilds the compact context concentrator directly when you need the + synthesized hot/warm memory view without the full handoff bundle. - `node tools/npm/run-script.mjs priority:handoff-tests` exercises the contract lane used to keep these handoff surfaces from drifting. @@ -58,8 +66,10 @@ entrypoint and machine-generated live state. - `tests/results/_agent/handoff/entrypoint-status.json` - `tests/results/_agent/handoff/monitoring-mode.json` - `tests/results/_agent/handoff/autonomous-governor-summary.json` +- `tests/results/_agent/handoff/sagan-context-concentrator.json` - `tests/results/_agent/handoff/autonomous-governor-portfolio-summary.json` - `tests/results/_agent/handoff/downstream-repo-graph-truth.json` +- `tests/results/_agent/release/release-signing-readiness.json` - `tests/results/_agent/handoff/docker-review-loop-summary.json` - `tests/results/_agent/handoff/*.json` - `tests/results/_agent/sessions/*.json` @@ -81,6 +91,9 @@ entrypoint and machine-generated live state. event-driven monitoring mode. - `tests/results/_agent/handoff/autonomous-governor-summary.json` is the top-level operating summary for the autonomous governor. +- `tests/results/_agent/handoff/sagan-context-concentrator.json` is the compact + durable memory view that keeps subagent findings, hot blockers, and current + owner decisions close to the active handoff surface. - `tests/results/_agent/handoff/autonomous-governor-portfolio-summary.json` is the cross-repo operating summary for compare, canonical template, and proving forks together. @@ -92,8 +105,14 @@ entrypoint and machine-generated live state. - merge-queue-owned PR waits, including the next wake condition and PR URL - template pivot readiness - current governor mode and next owner + - `vi-history` distributor dependency status between compare and the + canonical template - latest wake lifecycle terminal state - funding-quality posture for the latest wake + - release-signing readiness, including explicit external blockers when workflow + signing material is absent + - concentrated subagent episode memory, including hot working set, + warm recent memory, archive count, and lower-bound blended spend - cross-repo owner and next-owner decisions - repo graph truth for producer lineage, canonical development, and consumer proving - wake conditions that should reopen compare or template work diff --git a/docs/knowledgebase/CrossRepo-VIHistory.md b/docs/knowledgebase/CrossRepo-VIHistory.md index 9a88f7bd6..f78df3315 100644 --- a/docs/knowledgebase/CrossRepo-VIHistory.md +++ b/docs/knowledgebase/CrossRepo-VIHistory.md @@ -114,6 +114,11 @@ The capability contract tells downstream distributors: - `consumerContract.localOperatorSession` - `consumerContract.diagnosticsCommentRenderer` - `consumerContract.hostedNiLinuxRunner` +- the template Docker profile should resolve the Producer-published Docker + image contract from + `consumerContract.capabilities.dockerProfile.authoritativeImageContractSource`, + which currently points at `consumerContract.dockerImageContract` inside the + same immutable `comparevi-tools-release.json` payload That keeps the producer/distributor boundary clean: @@ -122,6 +127,11 @@ That keeps the producer/distributor boundary clean: - generated repositories consume the pinned release surface instead of vendoring compare internals +The autonomous governor portfolio also treats that producer/distributor link as +an explicit dependency. Compare remains the current owner until the signed +producer-native `CompareVI.Tools` release is ready, and only then does the +portfolio hand off the next-owner route to `LabviewGitHubCiTemplate`. + For hosted GitHub runner diagnostics, use the same extracted bundle root as `COMPAREVI_SCRIPTS_ROOT` and resolve the NI Linux runner from `tools/Run-NILinuxContainerCompare.ps1`. Keep its adjacent support scripts diff --git a/docs/knowledgebase/GitHub-Intake-Layer.md b/docs/knowledgebase/GitHub-Intake-Layer.md index d89e60c6c..32ad4ba4c 100644 --- a/docs/knowledgebase/GitHub-Intake-Layer.md +++ b/docs/knowledgebase/GitHub-Intake-Layer.md @@ -137,6 +137,11 @@ instead of inferring the correct path from prose alone. gh issue comment 875 --body-file issue-comment.md ``` + `Post-IssueComment.ps1` now appends the durable budget hook by default so + automation-authored comments retain spend state even after session compaction. + Use `-SkipBudgetHook` only when a test or narrow break-glass path needs the + raw body unchanged. + - Pull-request comments: ```powershell @@ -144,6 +149,9 @@ instead of inferring the correct path from prose alone. gh pr comment 875 --repo owner/repo --body-file pr-comment.md ``` + `Post-PullRequestComment.ps1` follows the same default and appends the durable + budget hook unless `-SkipBudgetHook` is explicit. + - PR bodies: ```powershell @@ -234,4 +242,5 @@ Human-authored PRs should use the `human-change` template so they do not acciden For issue creation, issue comments, PR comments, and PR creation in mixed WSL/Windows shells, prefer `--body-file` over inline multiline `--body` strings. For comments, prefer `tools/Post-IssueComment.ps1` and `tools/Post-PullRequestComment.ps1` so PowerShell lanes always route through a temporary or explicit body file. That -keeps quoting deterministic and aligns with the guidance in `AGENTS.md`. +keeps quoting deterministic, appends the durable budget hook by default, and +aligns with the guidance in `AGENTS.md`. diff --git a/docs/release/PR_NOTES.md b/docs/release/PR_NOTES.md index 7a7330646..9933943e8 100644 --- a/docs/release/PR_NOTES.md +++ b/docs/release/PR_NOTES.md @@ -1,39 +1,38 @@ -# Release v0.6.4-rc.1 - PR Notes Helper +# Release v0.6.4-rc.2 - PR Notes Helper -Reference sheet for refining the `v0.6.4-rc.1` release PR and draft release. -This RC is about hardening the hosted-first release conductor, keeping the -template conveyor pinned and green, and proving the repo can reach a truthful -queue-empty state before the template pivot. +Reference sheet for refining the `v0.6.4-rc.2` release PR and draft release. +This RC is about publishing the merged producer-owned Docker contract, keeping +the template conveyor pinned and green, and finishing the compare-side release +identity shift needed to unblock the template Docker-profile consumer rail. ## 1. Summary -Release `v0.6.4-rc.1` focuses on four themes: +Release `v0.6.4-rc.2` focuses on four themes: -- **Hosted-first release gating**: the release conductor now aligns `release/*` - policy, live GitHub rulesets, and the actual RC PR gate so finalize depends - on the checks that really matter. +- **Producer-owned Docker contract publication**: the next RC publishes + `consumerContract.capabilities.dockerProfile` and + `consumerContract.dockerImageContract` in the authoritative + `CompareVI.Tools` release bundle. +- **Honest RC identity shift**: `v0.6.4-rc.1` is now authoritative for + producer-native `vi-history`, but a fresh RC is needed because that published + bundle predates the merged Docker-profile contract. - **Template conveyor as a pinned dependency**: `LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate@v0.1.1` is treated as an immutable dependency and is revalidated through the cookiecutter conveyor. -- **Continuity/control-plane hardening**: standing rotation, detached - bootstrap, linked worktree release helpers, and queue-empty proof surfaces - now support an unattended RC cut. -- **Capital deployment telemetry**: PR/issue spend, invoice-turn attribution, - and template verification cost provenance stay visible while the RC moves. +- **Continuity/control-plane hardening**: replay automation routing, standing + release continuity, and published-bundle observation stay aligned while the + RC moves toward authoritative Docker-contract publication. ## 2. RC Highlights -- Release and feature dry-run helpers are worktree-safe, so `release:branch`, - `release:finalize`, `feature:branch:dry`, and `feature:finalize:dry` behave - correctly when `develop` is attached elsewhere. -- `priority:pivot:template` now treats `queue-empty` as authoritative and only - blocks on real remaining gates. -- The downstream proving rail fails closed when template verification is - missing, stale, or drifted from the pinned template dependency. -- Hosted release readiness now evaluates the live `validate.yml` and - `fixture-drift.yml` evidence lanes rather than retired self-hosted compare - workflows. +- The published bundle observer now proves `v0.6.4-rc.1` is authoritative for + producer-native `vi-history`, which narrows the remaining release gap to the + newer Docker-profile contract. +- The next RC carries the merged producer Docker contract from `#1940` / `#1941` + onto the release surface instead of relying on template-local conventions. +- The downstream template rail remains pinned and blocked cleanly until the new + producer contract is published. ## 3. Validation Snapshot @@ -43,33 +42,36 @@ Release `v0.6.4-rc.1` focuses on four themes: - `smoke-gate` - `Policy Guard (Upstream) / policy-guard` - `commit-integrity` -- [ ] Latest `fixture-drift.yml` run for `release/v0.6.4-rc.1` is green and +- [ ] Latest `fixture-drift.yml` run for `release/v0.6.4-rc.2` is green and uploads the NI Linux review-suite evidence bundle. - [ ] Latest template verification report stays `pass` for `LabviewGitHubCiTemplate@v0.1.1`. -- [ ] `node tools/npm/run-script.mjs release:finalize -- 0.6.4-rc.1` completes +- [ ] `node tools/npm/run-script.mjs release:finalize -- 0.6.4-rc.2` completes from a clean helper lane and writes fresh finalize metadata under `tests/results/_agent/release/`. +- [ ] `node tools/npm/run-script.mjs priority:release:published:bundle` flips + to the new RC and proves the producer-owned Docker contract is present. ## 4. Reviewer Focus - Confirm `CHANGELOG.md`, this helper, `TAG_PREP_CHECKLIST.md`, and - `../archive/releases/RELEASE_NOTES_v0.6.4-rc.1.md` all reference - `v0.6.4-rc.1` consistently. -- Review the hosted-first release gate adjustments: - - `tools/policy/branch-required-checks.json` - - `tools/priority/lib/release-pr-checks.mjs` - - `tools/priority/lib/release-compare-evidence.mjs` + `../archive/releases/RELEASE_NOTES_v0.6.4-rc.2.md` all reference + `v0.6.4-rc.2` consistently. +- Review the producer Docker-contract publication surfaces: + - `tools/Publish-CompareVIToolsArtifact.ps1` + - `docs/schemas/comparevi-tools-release-manifest-v1.schema.json` + - `docs/schemas/comparevi-tools-docker-profile-capability-v1.schema.json` + - `docs/schemas/comparevi-tools-docker-image-contract-v1.schema.json` - Check that the release branch verification helper is validating real RC - assets instead of stale historical release docs. -- Check that the fixture-drift hosted Linux lane remains deterministic on - `release/*` branches while keeping the VI history safeguard honest. + materials instead of stale historical release docs. ## 5. Follow-Up After RC -1. Cut the final `v0.6.4` release once RC evidence is stable. -2. Re-run the template pivot gate after the RC version is published. -3. Keep the downstream proving rail pinned to the released template tag until a - deliberate dependency bump is queued. +1. Re-run the published-bundle observer after publication and confirm the + producer-owned Docker contract is authoritative. +2. Start `LabviewGitHubCiTemplate#20` only after that published contract is + real. +3. Cut the final `v0.6.4` release once the RC evidence and template follow-up + both settle. ---- Updated: 2026-03-22 (aligned with the `v0.6.4-rc.1` release candidate). +--- Updated: 2026-03-24 (aligned with the `v0.6.4-rc.2` release candidate). diff --git a/docs/release/TAG_PREP_CHECKLIST.md b/docs/release/TAG_PREP_CHECKLIST.md index a8ab2289f..6b5b95fde 100644 --- a/docs/release/TAG_PREP_CHECKLIST.md +++ b/docs/release/TAG_PREP_CHECKLIST.md @@ -1,14 +1,15 @@ -# v0.6.4-rc.1 Tag Preparation Checklist +# v0.6.4-rc.2 Tag Preparation Checklist -Helper reference for cutting the `v0.6.4-rc.1` release candidate. Aligns with +Helper reference for cutting the `v0.6.4-rc.2` release candidate. Aligns with the archived release notes -(`../archive/releases/RELEASE_NOTES_v0.6.4-rc.1.md`) and the RC cut issue -(`#1797`). Update or archive once the release candidate is live. +(`../archive/releases/RELEASE_NOTES_v0.6.4-rc.2.md`) and the standing compare +publication rail (`#1877`). Update or archive once the release candidate is +live. ## 1. Pre-flight Verification -- [ ] Work from `release/v0.6.4-rc.1` (or the latest RC helper lane) and ensure +- [ ] Work from `release/v0.6.4-rc.2` (or the latest RC helper lane) and ensure it contains all RC-targeted changes. - [ ] CI is green on the RC branch (Validate, Fixture Drift Validation, Cookiecutter Bootstrap, and any active proving workflows). @@ -24,10 +25,14 @@ the archived release notes ## 2. Version & Metadata Consistency - [ ] `CHANGELOG.md` contains a finalized - `## [v0.6.4-rc.1] - 2026-03-22` section. -- [ ] Release docs reference `v0.6.4-rc.1` consistently where the RC is + `## [v0.6.4-rc.2] - 2026-03-24` section. +- [ ] Release docs reference `v0.6.4-rc.2` consistently where the RC is intentionally named. -- [ ] `package.json` version is `0.6.4-rc.1` and matches the release notes. +- [ ] `package.json` version is `0.6.4-rc.2` and matches the release notes. +- [ ] The release materials explain why this RC exists: + - `v0.6.4-rc.1` is authoritative for producer-native `vi-history` + - `v0.6.4-rc.2` is the first RC meant to publish the producer-owned + docker-profile contract - [ ] Regenerate `docs/action-outputs.md` if outputs changed (`node tools/npm/run-script.mjs generate:outputs`) and confirm `action.yml` matches the documented inputs/outputs. @@ -52,7 +57,7 @@ the archived release notes ## 5. Release Materials Review - [ ] `PR_NOTES.md`, this checklist, and - `../archive/releases/RELEASE_NOTES_v0.6.4-rc.1.md` are consistent. + `../archive/releases/RELEASE_NOTES_v0.6.4-rc.2.md` are consistent. - [ ] Helper docs reflect the hosted-first RC flow: - `docs/knowledgebase/FEATURE_BRANCH_POLICY.md` - `docs/knowledgebase/VICompare-Refs-Workflow.md` @@ -62,34 +67,49 @@ the archived release notes ## 6. Tag Creation +- [ ] Verify signed-tag readiness before push: + +```pwsh +node tools/npm/run-script.mjs priority:release:signing:readiness +node tools/npm/run-script.mjs priority:release:conductor -- --apply --channel rc --version 0.6.4-rc.2 +``` + +- [ ] Confirm `tests/results/_agent/release/release-signing-readiness.json` + does not report `externalBlocker` before retrying authoritative release + publication. +- [ ] Confirm `tests/results/_agent/release/release-conductor-report.json` reports + `status: pass` before pushing the RC tag. - [ ] Create an annotated RC tag: ```pwsh -git tag -a v0.6.4-rc.1 -m "v0.6.4-rc.1: hosted-first release conductor hardening" +git tag -a v0.6.4-rc.2 -m "v0.6.4-rc.2: publish producer docker-profile contract" ``` - [ ] Push the tag: ```pwsh -git push origin v0.6.4-rc.1 +git push origin v0.6.4-rc.2 ``` ## 7. GitHub Release Draft Suggested draft-release outline: -1. Summary: hosted-first release conductor hardening, template conveyor - dependency, continuity/control-plane fixes. -2. Upgrade notes: release helper safety, RC gate alignment, queue-empty pivot - proof. +1. Summary: producer-owned Docker contract publication, template conveyor + dependency, and release-surface continuity. +2. Upgrade notes: `v0.6.4-rc.1` already published the producer-native + `vi-history` contract; `v0.6.4-rc.2` carries the producer-owned Docker + contract on the next authoritative RC. 3. Validation snapshot: required checks, Fixture Drift Validation, - Cookiecutter Bootstrap, and template verification. -4. Known issues / follow-ups: final `v0.6.4` cut, remaining non-RC backlog. + Cookiecutter Bootstrap, template verification, and published-bundle + observation. +4. Known issues / follow-ups: final `v0.6.4` cut and template `#20` promotion + after authoritative publication proof. 5. Rollback: link to `ROLLBACK_PLAN.md`. ## 8. Post-Tag Actions -- [ ] Run `node tools/npm/run-script.mjs release:finalize -- 0.6.4-rc.1` from a +- [ ] Run `node tools/npm/run-script.mjs release:finalize -- 0.6.4-rc.2` from a clean helper lane to fast-forward `main` and `develop`, then record the finalize metadata. - [ ] Refresh `priority:pivot:template` after the RC version lands so the pivot @@ -99,18 +119,21 @@ Suggested draft-release outline: ## 9. Validation After Publish -- [ ] Install the action via `@v0.6.4-rc.1` in a sample workflow and confirm a +- [ ] Install the action via `@v0.6.4-rc.2` in a sample workflow and confirm a compare using the canonical fixtures succeeds. - [ ] Exercise the pinned template conveyor and downstream proving rail against the RC release summary. +- [ ] Re-run `node tools/npm/run-script.mjs priority:release:published:bundle` + and confirm the published `CompareVI.Tools` bundle carries + `consumerContract.capabilities.dockerProfile`. - [ ] Re-run the LabVIEW CLI wrapper path to ensure rogue detection and cleanup guard stay green. ## 10. Communication -- [ ] Announce the RC cut, calling out the hosted-first release conductor, - template dependency, and continuity hardening. -- [ ] Remind consumers that the final pivot to `LabviewGitHubCiTemplate` - remains gated on an RC release summary and a future agent handoff. +- [ ] Announce the RC cut, calling out the published producer-owned Docker + contract and the template dependency it unlocks. +- [ ] Remind consumers that `LabviewGitHubCiTemplate#20` only starts after the + published bundle proves the producer contract authoritatively. ---- Updated: 2026-03-22 (revamped for the `v0.6.4-rc.1` release cycle). +--- Updated: 2026-03-24 (revamped for the `v0.6.4-rc.2` release cycle). diff --git a/docs/schema/generated/teststand-compare-session.schema.json b/docs/schema/generated/teststand-compare-session.schema.json index 82d5ae3ba..c1521ace5 100644 --- a/docs/schema/generated/teststand-compare-session.schema.json +++ b/docs/schema/generated/teststand-compare-session.schema.json @@ -6,7 +6,10 @@ "properties": { "schema": { "type": "string", - "const": "teststand-compare-session/v1" + "enum": [ + "teststand-compare-session/v1", + "teststand-compare-session/v2" + ] }, "at": { "type": "string", @@ -151,6 +154,33 @@ }, "additionalProperties": false }, + "staging": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "root": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "enabled", + "root" + ], + "additionalProperties": false + }, + "allowSameLeaf": { + "type": "boolean" + }, "policy": { "type": "string", "enum": [ @@ -220,6 +250,507 @@ "type": "null" } ] + }, + "executionCell": { + "anyOf": [ + { + "type": "object", + "properties": { + "cellId": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "leaseId": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "leasePath": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "agentId": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "agentClass": { + "anyOf": [ + { + "type": "string", + "enum": [ + "sagan", + "subagent", + "other" + ] + }, + { + "type": "null" + } + ] + }, + "cellClass": { + "anyOf": [ + { + "type": "string", + "enum": [ + "worker", + "coordinator", + "kernel-coordinator" + ] + }, + { + "type": "null" + } + ] + }, + "suiteClass": { + "anyOf": [ + { + "type": "string", + "enum": [ + "single-compare", + "dual-plane-parity" + ] + }, + { + "type": "null" + } + ] + }, + "planeBinding": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "runtimeSurface": { + "anyOf": [ + { + "type": "string", + "const": "windows-native-teststand" + }, + { + "type": "null" + } + ] + }, + "premiumSaganMode": { + "type": "boolean" + }, + "operatorAuthorizationRef": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "workingRoot": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "artifactRoot": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "isolatedLaneGroupId": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "hostOsFingerprintSha256": { + "anyOf": [ + { + "type": "string", + "pattern": "^[A-Fa-f0-9]{64}$" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "harnessInstance": { + "anyOf": [ + { + "type": "object", + "properties": { + "harnessKind": { + "type": "string", + "minLength": 1 + }, + "instanceId": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "enum": [ + "single-plane", + "coordinator", + "plane-child" + ] + }, + "processModelClass": { + "type": "string", + "enum": [ + "sequential-process-model", + "parallel-process-model" + ] + }, + "planeBinding": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "parentInstanceId": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "harnessKind", + "instanceId", + "role", + "processModelClass" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "processModel": { + "type": "object", + "properties": { + "runtimeSurface": { + "type": "string", + "const": "windows-native-teststand" + }, + "processModelClass": { + "type": "string", + "enum": [ + "sequential-process-model", + "parallel-process-model" + ] + }, + "windowsOnly": { + "type": "boolean", + "const": true + }, + "rootHarnessInstanceId": { + "type": "string", + "minLength": 1 + }, + "planeCount": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ + "runtimeSurface", + "processModelClass", + "windowsOnly", + "rootHarnessInstanceId", + "planeCount" + ], + "additionalProperties": false + }, + "suiteClass": { + "type": "string", + "enum": [ + "single-compare", + "dual-plane-parity" + ] + }, + "primaryPlane": { + "type": "string", + "minLength": 1 + }, + "requestedSimultaneous": { + "type": "boolean" + }, + "planes": { + "type": "object", + "properties": { + "x64": { + "type": "object", + "properties": { + "plane": { + "type": "string", + "minLength": 1 + }, + "architecture": { + "type": "string", + "enum": [ + "32-bit", + "64-bit" + ] + }, + "labviewExePath": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "outputRoot": { + "type": "string", + "minLength": 1 + }, + "warmup": { + "type": "object", + "properties": { + "mode": { + "$ref": "#/definitions/teststand-compare-session/properties/warmup/properties/mode" + }, + "events": { + "$ref": "#/definitions/teststand-compare-session/properties/warmup/properties/events" + } + }, + "required": [ + "mode", + "events" + ], + "additionalProperties": false + }, + "compare": { + "$ref": "#/definitions/teststand-compare-session/properties/compare" + }, + "outcome": { + "$ref": "#/definitions/teststand-compare-session/properties/outcome" + }, + "error": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "exitCode": { + "type": "number" + }, + "executionCell": { + "anyOf": [ + { + "$ref": "#/definitions/teststand-compare-session/properties/executionCell/anyOf/0" + }, + { + "type": "null" + } + ] + }, + "harnessInstance": { + "anyOf": [ + { + "$ref": "#/definitions/teststand-compare-session/properties/harnessInstance/anyOf/0" + }, + { + "type": "null" + } + ] + }, + "processModel": { + "$ref": "#/definitions/teststand-compare-session/properties/processModel" + } + }, + "required": [ + "plane", + "architecture", + "outputRoot", + "warmup", + "compare", + "outcome", + "exitCode" + ], + "additionalProperties": false + }, + "x32": { + "$ref": "#/definitions/teststand-compare-session/properties/planes/properties/x64" + } + }, + "required": [ + "x64", + "x32" + ], + "additionalProperties": false + }, + "parity": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "match", + "mismatch", + "incomplete" + ] + }, + "comparedFields": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "exitCodeParity": { + "type": [ + "boolean", + "null" + ] + }, + "diffParity": { + "type": [ + "boolean", + "null" + ] + }, + "mismatchCount": { + "type": "integer", + "minimum": 0 + }, + "mismatches": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "minLength": 1 + }, + "x64": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "x32": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + } + }, + "required": [ + "status", + "comparedFields", + "mismatchCount", + "mismatches" + ], + "additionalProperties": false } }, "required": [ diff --git a/docs/schemas/autonomous-governor-portfolio-summary-report-v1.schema.json b/docs/schemas/autonomous-governor-portfolio-summary-report-v1.schema.json index 7edff0fb2..4b2370c87 100644 --- a/docs/schemas/autonomous-governor-portfolio-summary-report-v1.schema.json +++ b/docs/schemas/autonomous-governor-portfolio-summary-report-v1.schema.json @@ -51,7 +51,13 @@ "queueHandoffStatus", "queueHandoffNextWakeCondition", "queueHandoffPrUrl", - "queueAuthoritySource" + "queueAuthoritySource", + "executionTopology", + "executionBundleStatus", + "executionBundlePlaneBinding", + "executionBundlePremiumSaganMode", + "executionBundleReciprocalLinkReady", + "executionBundleEffectiveBillableRateUsdPerHour" ], "properties": { "repository": { "type": ["string", "null"] }, @@ -64,13 +70,19 @@ "queueHandoffStatus": { "type": ["string", "null"] }, "queueHandoffNextWakeCondition": { "type": ["string", "null"] }, "queueHandoffPrUrl": { "type": ["string", "null"] }, - "queueAuthoritySource": { "type": ["string", "null"] } + "queueAuthoritySource": { "type": ["string", "null"] }, + "executionTopology": { "$ref": "#/$defs/executionTopology" }, + "executionBundleStatus": { "type": ["string", "null"] }, + "executionBundlePlaneBinding": { "type": ["string", "null"] }, + "executionBundlePremiumSaganMode": { "type": "boolean" }, + "executionBundleReciprocalLinkReady": { "type": "boolean" }, + "executionBundleEffectiveBillableRateUsdPerHour": { "type": ["number", "null"], "minimum": 0 } } }, "portfolio": { "type": "object", "additionalProperties": false, - "required": ["repositoryCount", "repositories", "unsupportedPaths"], + "required": ["repositoryCount", "repositories", "dependencies", "unsupportedPaths"], "properties": { "repositoryCount": { "type": "integer", @@ -175,6 +187,49 @@ } } }, + "dependencies": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "status", + "ownerRepository", + "dependentRepository", + "requiredCapability", + "source", + "releaseSigningStatus", + "releasePublicationState", + "publishedBundleState", + "publishedBundleReleaseTag", + "publishedBundleAuthoritativeConsumerPin", + "signingCapabilityState", + "signingAuthorityState", + "releaseConductorApplyState", + "externalBlocker", + "detail" + ], + "properties": { + "id": { "type": "string" }, + "status": { "type": "string", "enum": ["ready", "blocked", "unknown"] }, + "ownerRepository": { "type": ["string", "null"] }, + "dependentRepository": { "type": ["string", "null"] }, + "requiredCapability": { "type": "string" }, + "source": { "type": "string" }, + "releaseSigningStatus": { "type": ["string", "null"] }, + "releasePublicationState": { "type": ["string", "null"] }, + "publishedBundleState": { "type": ["string", "null"] }, + "publishedBundleReleaseTag": { "type": ["string", "null"] }, + "publishedBundleAuthoritativeConsumerPin": { "type": ["string", "null"] }, + "signingCapabilityState": { "type": ["string", "null"] }, + "signingAuthorityState": { "type": ["string", "null"] }, + "releaseConductorApplyState": { "type": ["string", "null"] }, + "externalBlocker": { "type": ["string", "null"] }, + "detail": { "type": "string" } + } + } + }, "unsupportedPaths": { "type": "array", "items": { @@ -207,6 +262,33 @@ "queueHandoffNextWakeCondition", "queueHandoffPrUrl", "queueAuthoritySource", + "executionTopologyStatus", + "executionTopologyExecutionPlane", + "executionTopologyProviderId", + "executionTopologyWorkerSlotId", + "executionTopologyActiveLogicalLaneCount", + "executionTopologySeededLogicalLaneCount", + "executionTopologyRuntimeSurface", + "executionTopologyProcessModelClass", + "executionTopologyWindowsOnly", + "executionTopologyRequestedSimultaneous", + "executionTopologyCellClass", + "executionTopologySuiteClass", + "executionTopologyOperatorAuthorizationRef", + "executionBundleStatus", + "executionBundlePlaneBinding", + "executionBundlePremiumSaganMode", + "executionBundleReciprocalLinkReady", + "executionBundleEffectiveBillableRateUsdPerHour", + "viHistoryDistributorDependencyStatus", + "viHistoryDistributorDependencyTargetRepository", + "viHistoryDistributorDependencyExternalBlocker", + "viHistoryDistributorDependencyPublicationState", + "viHistoryDistributorDependencyPublishedBundleState", + "viHistoryDistributorDependencyPublishedBundleReleaseTag", + "viHistoryDistributorDependencyAuthoritativeConsumerPin", + "viHistoryDistributorDependencySigningAuthorityState", + "viHistoryDistributorDependencyReleaseConductorApplyState", "portfolioWakeConditionCount", "triggeredWakeConditions" ], @@ -227,6 +309,33 @@ "queueHandoffNextWakeCondition": { "type": ["string", "null"] }, "queueHandoffPrUrl": { "type": ["string", "null"] }, "queueAuthoritySource": { "type": ["string", "null"] }, + "executionTopologyStatus": { "type": ["string", "null"] }, + "executionTopologyExecutionPlane": { "type": ["string", "null"] }, + "executionTopologyProviderId": { "type": ["string", "null"] }, + "executionTopologyWorkerSlotId": { "type": ["string", "null"] }, + "executionTopologyActiveLogicalLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "executionTopologySeededLogicalLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "executionTopologyRuntimeSurface": { "type": ["string", "null"] }, + "executionTopologyProcessModelClass": { "type": ["string", "null"] }, + "executionTopologyWindowsOnly": { "type": "boolean" }, + "executionTopologyRequestedSimultaneous": { "type": "boolean" }, + "executionTopologyCellClass": { "type": ["string", "null"] }, + "executionTopologySuiteClass": { "type": ["string", "null"] }, + "executionTopologyOperatorAuthorizationRef": { "type": ["string", "null"] }, + "executionBundleStatus": { "type": ["string", "null"] }, + "executionBundlePlaneBinding": { "type": ["string", "null"] }, + "executionBundlePremiumSaganMode": { "type": "boolean" }, + "executionBundleReciprocalLinkReady": { "type": "boolean" }, + "executionBundleEffectiveBillableRateUsdPerHour": { "type": ["number", "null"], "minimum": 0 }, + "viHistoryDistributorDependencyStatus": { "type": "string", "enum": ["ready", "blocked", "unknown"] }, + "viHistoryDistributorDependencyTargetRepository": { "type": ["string", "null"] }, + "viHistoryDistributorDependencyExternalBlocker": { "type": ["string", "null"] }, + "viHistoryDistributorDependencyPublicationState": { "type": ["string", "null"] }, + "viHistoryDistributorDependencyPublishedBundleState": { "type": ["string", "null"] }, + "viHistoryDistributorDependencyPublishedBundleReleaseTag": { "type": ["string", "null"] }, + "viHistoryDistributorDependencyAuthoritativeConsumerPin": { "type": ["string", "null"] }, + "viHistoryDistributorDependencySigningAuthorityState": { "type": ["string", "null"] }, + "viHistoryDistributorDependencyReleaseConductorApplyState": { "type": ["string", "null"] }, "portfolioWakeConditionCount": { "type": "integer", "minimum": 0 }, "triggeredWakeConditions": { "type": "array", @@ -234,5 +343,131 @@ } } } + }, + "$defs": { + "executionBundle": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "planeBinding", + "cellClass", + "suiteClass", + "premiumSaganMode", + "reciprocalLinkReady", + "effectiveBillableRateUsdPerHour", + "executionCellLeaseId", + "dockerLaneLeaseId", + "harnessKind", + "harnessInstanceId", + "operatorAuthorizationRef", + "cellId", + "laneId", + "isolatedLaneGroupId", + "fingerprintSha256" + ], + "properties": { + "status": { "type": ["string", "null"] }, + "planeBinding": { "type": ["string", "null"] }, + "cellClass": { "type": ["string", "null"] }, + "suiteClass": { "type": ["string", "null"] }, + "premiumSaganMode": { "type": "boolean" }, + "reciprocalLinkReady": { "type": "boolean" }, + "effectiveBillableRateUsdPerHour": { "type": ["number", "null"], "minimum": 0 }, + "executionCellLeaseId": { "type": ["string", "null"] }, + "dockerLaneLeaseId": { "type": ["string", "null"] }, + "harnessKind": { "type": ["string", "null"] }, + "harnessInstanceId": { "type": ["string", "null"] }, + "operatorAuthorizationRef": { "type": ["string", "null"] }, + "cellId": { "type": ["string", "null"] }, + "laneId": { "type": ["string", "null"] }, + "isolatedLaneGroupId": { "type": ["string", "null"] }, + "fingerprintSha256": { "type": ["string", "null"] } + } + }, + "executionTopologyLogicalLaneActivation": { + "type": "object", + "additionalProperties": false, + "required": ["activeLaneCount", "seededLaneCount", "catalogCount"], + "properties": { + "activeLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "seededLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "catalogCount": { "type": "integer", "minimum": 0 } + } + }, + "executionTopologyProviderDispatch": { + "type": "object", + "additionalProperties": false, + "required": [ + "providerId", + "providerKind", + "executionPlane", + "assignmentMode", + "dispatchSurface", + "completionMode", + "workerSlotId", + "dispatchStatus", + "completionStatus", + "failureClass" + ], + "properties": { + "providerId": { "type": ["string", "null"] }, + "providerKind": { "type": ["string", "null"] }, + "executionPlane": { "type": ["string", "null"] }, + "assignmentMode": { "type": ["string", "null"] }, + "dispatchSurface": { "type": ["string", "null"] }, + "completionMode": { "type": ["string", "null"] }, + "workerSlotId": { "type": ["string", "null"] }, + "dispatchStatus": { "type": ["string", "null"] }, + "completionStatus": { "type": ["string", "null"] }, + "failureClass": { "type": ["string", "null"] } + } + }, + "executionTopology": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "executionPlane", + "providerId", + "workerSlotId", + "activeLogicalLaneCount", + "seededLogicalLaneCount", + "catalogCount", + "runtimeSurface", + "processModelClass", + "windowsOnly", + "requestedSimultaneous", + "cellClass", + "suiteClass", + "operatorAuthorizationRef", + "premiumSaganMode", + "reciprocalLinkReady", + "logicalLaneActivation", + "providerDispatch", + "executionBundle" + ], + "properties": { + "status": { "type": ["string", "null"] }, + "executionPlane": { "type": ["string", "null"] }, + "providerId": { "type": ["string", "null"] }, + "workerSlotId": { "type": ["string", "null"] }, + "activeLogicalLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "seededLogicalLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "catalogCount": { "type": "integer", "minimum": 0 }, + "runtimeSurface": { "type": ["string", "null"] }, + "processModelClass": { "type": ["string", "null"] }, + "windowsOnly": { "type": "boolean" }, + "requestedSimultaneous": { "type": "boolean" }, + "cellClass": { "type": ["string", "null"] }, + "suiteClass": { "type": ["string", "null"] }, + "operatorAuthorizationRef": { "type": ["string", "null"] }, + "premiumSaganMode": { "type": "boolean" }, + "reciprocalLinkReady": { "type": "boolean" }, + "logicalLaneActivation": { "$ref": "#/$defs/executionTopologyLogicalLaneActivation" }, + "providerDispatch": { "$ref": "#/$defs/executionTopologyProviderDispatch" }, + "executionBundle": { "$ref": "#/$defs/executionBundle" } + } + } } } diff --git a/docs/schemas/autonomous-governor-summary-report-v1.schema.json b/docs/schemas/autonomous-governor-summary-report-v1.schema.json index 6e57c5c4e..31bd61c34 100644 --- a/docs/schemas/autonomous-governor-summary-report-v1.schema.json +++ b/docs/schemas/autonomous-governor-summary-report-v1.schema.json @@ -37,7 +37,8 @@ "monitoringModePath", "wakeLifecyclePath", "wakeInvestmentAccountingPath", - "deliveryRuntimeStatePath" + "deliveryRuntimeStatePath", + "releaseSigningReadinessPath" ], "properties": { "queueEmptyReportPath": { "type": "string", "minLength": 1 }, @@ -45,7 +46,8 @@ "monitoringModePath": { "type": "string", "minLength": 1 }, "wakeLifecyclePath": { "type": "string", "minLength": 1 }, "wakeInvestmentAccountingPath": { "type": "string", "minLength": 1 }, - "deliveryRuntimeStatePath": { "type": "string", "minLength": 1 } + "deliveryRuntimeStatePath": { "type": "string", "minLength": 1 }, + "releaseSigningReadinessPath": { "type": "string", "minLength": 1 } } }, "compare": { @@ -55,6 +57,7 @@ "queueState", "continuity", "monitoringMode", + "releaseSigningReadiness", "deliveryRuntime", "queueAuthority" ], @@ -62,6 +65,7 @@ "queueState": { "$ref": "#/$defs/queueState" }, "continuity": { "$ref": "#/$defs/continuity" }, "monitoringMode": { "$ref": "#/$defs/monitoringMode" }, + "releaseSigningReadiness": { "$ref": "#/$defs/releaseSigningReadiness" }, "deliveryRuntime": { "$ref": "#/$defs/deliveryRuntime" }, "queueAuthority": { "$ref": "#/$defs/queueAuthority" } } @@ -134,6 +138,8 @@ "outcome", "blockerClass", "nextWakeCondition", + "executionTopology", + "executionBundle", "queueAuthorityRefresh", "prUrl", "issueNumber", @@ -157,12 +163,52 @@ "outcome": { "type": ["string", "null"] }, "blockerClass": { "type": ["string", "null"] }, "nextWakeCondition": { "type": ["string", "null"] }, + "executionTopology": { "$ref": "#/$defs/executionTopology" }, + "executionBundle": { "$ref": "#/$defs/executionBundle" }, "queueAuthorityRefresh": { "$ref": "#/$defs/queueAuthorityRefresh" }, "prUrl": { "type": ["string", "null"] }, "issueNumber": { "type": ["integer", "null"], "minimum": 1 }, "reason": { "type": ["string", "null"] } } }, + "releaseSigningReadiness": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "codePathState", + "signingCapabilityState", + "signingAuthorityState", + "releaseConductorApplyState", + "publicationState", + "publishedBundleState", + "publishedBundleReleaseTag", + "publishedBundleAuthoritativeConsumerPin", + "externalBlocker", + "blockerCount" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "pass", + "warn", + "fail", + "missing" + ] + }, + "codePathState": { "type": ["string", "null"] }, + "signingCapabilityState": { "type": ["string", "null"] }, + "signingAuthorityState": { "type": ["string", "null"] }, + "releaseConductorApplyState": { "type": ["string", "null"] }, + "publicationState": { "type": ["string", "null"] }, + "publishedBundleState": { "type": ["string", "null"] }, + "publishedBundleReleaseTag": { "type": ["string", "null"] }, + "publishedBundleAuthoritativeConsumerPin": { "type": ["string", "null"] }, + "externalBlocker": { "type": ["string", "null"] }, + "blockerCount": { "type": "integer", "minimum": 0 } + } + }, "queueAuthority": { "type": "object", "additionalProperties": false, @@ -281,6 +327,32 @@ "wakeTerminalState", "monitoringStatus", "futureAgentAction", + "releaseSigningStatus", + "releaseSigningAuthorityState", + "releaseConductorApplyState", + "releaseSigningExternalBlocker", + "releasePublicationState", + "releasePublishedBundleState", + "releasePublishedBundleReleaseTag", + "releasePublishedBundleAuthoritativeConsumerPin", + "executionTopologyStatus", + "executionTopologyExecutionPlane", + "executionTopologyProviderId", + "executionTopologyWorkerSlotId", + "executionTopologyActiveLogicalLaneCount", + "executionTopologySeededLogicalLaneCount", + "executionTopologyRuntimeSurface", + "executionTopologyProcessModelClass", + "executionTopologyWindowsOnly", + "executionTopologyRequestedSimultaneous", + "executionTopologyCellClass", + "executionTopologySuiteClass", + "executionTopologyOperatorAuthorizationRef", + "executionBundleStatus", + "executionBundlePlaneBinding", + "executionBundlePremiumSaganMode", + "executionBundleReciprocalLinkReady", + "executionBundleEffectiveBillableRateUsdPerHour", "queueHandoffStatus", "queueHandoffNextWakeCondition", "queueHandoffPrUrl", @@ -320,6 +392,40 @@ "wakeTerminalState": { "type": ["string", "null"] }, "monitoringStatus": { "type": ["string", "null"] }, "futureAgentAction": { "type": ["string", "null"] }, + "releaseSigningStatus": { + "type": "string", + "enum": [ + "pass", + "warn", + "fail", + "missing" + ] + }, + "releaseSigningAuthorityState": { "type": ["string", "null"] }, + "releaseConductorApplyState": { "type": ["string", "null"] }, + "releaseSigningExternalBlocker": { "type": ["string", "null"] }, + "releasePublicationState": { "type": ["string", "null"] }, + "releasePublishedBundleState": { "type": ["string", "null"] }, + "releasePublishedBundleReleaseTag": { "type": ["string", "null"] }, + "releasePublishedBundleAuthoritativeConsumerPin": { "type": ["string", "null"] }, + "executionTopologyStatus": { "type": ["string", "null"] }, + "executionTopologyExecutionPlane": { "type": ["string", "null"] }, + "executionTopologyProviderId": { "type": ["string", "null"] }, + "executionTopologyWorkerSlotId": { "type": ["string", "null"] }, + "executionTopologyActiveLogicalLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "executionTopologySeededLogicalLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "executionTopologyRuntimeSurface": { "type": ["string", "null"] }, + "executionTopologyProcessModelClass": { "type": ["string", "null"] }, + "executionTopologyWindowsOnly": { "type": "boolean" }, + "executionTopologyRequestedSimultaneous": { "type": "boolean" }, + "executionTopologyCellClass": { "type": ["string", "null"] }, + "executionTopologySuiteClass": { "type": ["string", "null"] }, + "executionTopologyOperatorAuthorizationRef": { "type": ["string", "null"] }, + "executionBundleStatus": { "type": ["string", "null"] }, + "executionBundlePlaneBinding": { "type": ["string", "null"] }, + "executionBundlePremiumSaganMode": { "type": "boolean" }, + "executionBundleReciprocalLinkReady": { "type": "boolean" }, + "executionBundleEffectiveBillableRateUsdPerHour": { "type": ["number", "null"], "minimum": 0 }, "queueHandoffStatus": { "type": "string", "enum": [ @@ -382,6 +488,130 @@ "autoMergeEnabled": { "type": ["boolean", "null"] }, "mergedAt": { "type": ["string", "null"] } } + }, + "executionBundle": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "planeBinding", + "cellClass", + "suiteClass", + "premiumSaganMode", + "reciprocalLinkReady", + "effectiveBillableRateUsdPerHour", + "executionCellLeaseId", + "dockerLaneLeaseId", + "harnessKind", + "harnessInstanceId", + "operatorAuthorizationRef", + "cellId", + "laneId", + "isolatedLaneGroupId", + "fingerprintSha256" + ], + "properties": { + "status": { "type": ["string", "null"] }, + "planeBinding": { "type": ["string", "null"] }, + "cellClass": { "type": ["string", "null"] }, + "suiteClass": { "type": ["string", "null"] }, + "premiumSaganMode": { "type": "boolean" }, + "reciprocalLinkReady": { "type": "boolean" }, + "effectiveBillableRateUsdPerHour": { "type": ["number", "null"], "minimum": 0 }, + "executionCellLeaseId": { "type": ["string", "null"] }, + "dockerLaneLeaseId": { "type": ["string", "null"] }, + "harnessKind": { "type": ["string", "null"] }, + "harnessInstanceId": { "type": ["string", "null"] }, + "operatorAuthorizationRef": { "type": ["string", "null"] }, + "cellId": { "type": ["string", "null"] }, + "laneId": { "type": ["string", "null"] }, + "isolatedLaneGroupId": { "type": ["string", "null"] }, + "fingerprintSha256": { "type": ["string", "null"] } + } + }, + "executionTopologyLogicalLaneActivation": { + "type": "object", + "additionalProperties": false, + "required": ["activeLaneCount", "seededLaneCount", "catalogCount"], + "properties": { + "activeLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "seededLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "catalogCount": { "type": "integer", "minimum": 0 } + } + }, + "executionTopologyProviderDispatch": { + "type": "object", + "additionalProperties": false, + "required": [ + "providerId", + "providerKind", + "executionPlane", + "assignmentMode", + "dispatchSurface", + "completionMode", + "workerSlotId", + "dispatchStatus", + "completionStatus", + "failureClass" + ], + "properties": { + "providerId": { "type": ["string", "null"] }, + "providerKind": { "type": ["string", "null"] }, + "executionPlane": { "type": ["string", "null"] }, + "assignmentMode": { "type": ["string", "null"] }, + "dispatchSurface": { "type": ["string", "null"] }, + "completionMode": { "type": ["string", "null"] }, + "workerSlotId": { "type": ["string", "null"] }, + "dispatchStatus": { "type": ["string", "null"] }, + "completionStatus": { "type": ["string", "null"] }, + "failureClass": { "type": ["string", "null"] } + } + }, + "executionTopology": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "executionPlane", + "providerId", + "workerSlotId", + "activeLogicalLaneCount", + "seededLogicalLaneCount", + "catalogCount", + "runtimeSurface", + "processModelClass", + "windowsOnly", + "requestedSimultaneous", + "cellClass", + "suiteClass", + "operatorAuthorizationRef", + "premiumSaganMode", + "reciprocalLinkReady", + "logicalLaneActivation", + "providerDispatch", + "executionBundle" + ], + "properties": { + "status": { "type": ["string", "null"] }, + "executionPlane": { "type": ["string", "null"] }, + "providerId": { "type": ["string", "null"] }, + "workerSlotId": { "type": ["string", "null"] }, + "activeLogicalLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "seededLogicalLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "catalogCount": { "type": "integer", "minimum": 0 }, + "runtimeSurface": { "type": ["string", "null"] }, + "processModelClass": { "type": ["string", "null"] }, + "windowsOnly": { "type": "boolean" }, + "requestedSimultaneous": { "type": "boolean" }, + "cellClass": { "type": ["string", "null"] }, + "suiteClass": { "type": ["string", "null"] }, + "operatorAuthorizationRef": { "type": ["string", "null"] }, + "premiumSaganMode": { "type": "boolean" }, + "reciprocalLinkReady": { "type": "boolean" }, + "logicalLaneActivation": { "$ref": "#/$defs/executionTopologyLogicalLaneActivation" }, + "providerDispatch": { "$ref": "#/$defs/executionTopologyProviderDispatch" }, + "executionBundle": { "$ref": "#/$defs/executionBundle" } + } } } } diff --git a/docs/schemas/comparevi-tools-docker-image-contract-v1.schema.json b/docs/schemas/comparevi-tools-docker-image-contract-v1.schema.json new file mode 100644 index 000000000..d8fe8cd87 --- /dev/null +++ b/docs/schemas/comparevi-tools-docker-image-contract-v1.schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://labview-community-ci-cd.github.io/compare-vi-cli-action/schemas/comparevi-tools-docker-image-contract-v1.schema.json", + "title": "CompareVI.Tools Docker Image Contract v1", + "type": "object", + "required": [ + "schema", + "schemaUrl", + "images", + "notes" + ], + "properties": { + "schema": { + "const": "comparevi-tools/docker-image-contract@v1" + }, + "schemaUrl": { + "type": "string" + }, + "images": { + "type": "object", + "required": [ + "hostedNiLinuxRunner" + ], + "properties": { + "hostedNiLinuxRunner": { + "type": "object", + "required": [ + "imageRef", + "consumerRole", + "notes" + ], + "properties": { + "imageRef": { + "type": "string" + }, + "consumerRole": { + "type": "string", + "enum": [ + "hosted-ni-linux-runner" + ] + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/comparevi-tools-docker-profile-capability-v1.schema.json b/docs/schemas/comparevi-tools-docker-profile-capability-v1.schema.json new file mode 100644 index 000000000..568ed87be --- /dev/null +++ b/docs/schemas/comparevi-tools-docker-profile-capability-v1.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://labview-community-ci-cd.github.io/compare-vi-cli-action/schemas/comparevi-tools-docker-profile-capability-v1.schema.json", + "title": "CompareVI.Tools Docker Profile Capability v1", + "type": "object", + "required": [ + "schema", + "capabilityId", + "displayName", + "distributionRole", + "distributionModel", + "bundleMetadataPath", + "bundleImportPath", + "releaseAssetPattern", + "authoritativeConsumerPinFieldPath", + "authoritativeConsumerPinKindFieldPath", + "authoritativeImageContractSource", + "notes" + ], + "properties": { + "schema": { + "const": "comparevi-tools/docker-profile-capability@v1" + }, + "capabilityId": { + "const": "docker-profile" + }, + "displayName": { + "type": "string" + }, + "distributionRole": { + "type": "string", + "enum": [ + "upstream-producer" + ] + }, + "distributionModel": { + "type": "string", + "enum": [ + "release-bundle" + ] + }, + "bundleMetadataPath": { + "type": "string" + }, + "bundleImportPath": { + "type": "string" + }, + "releaseAssetPattern": { + "type": "string" + }, + "authoritativeConsumerPinFieldPath": { + "type": "string" + }, + "authoritativeConsumerPinKindFieldPath": { + "type": "string" + }, + "authoritativeImageContractSource": { + "type": "string" + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/comparevi-tools-release-manifest-v1.schema.json b/docs/schemas/comparevi-tools-release-manifest-v1.schema.json index a990c87dd..8d8c5459b 100644 --- a/docs/schemas/comparevi-tools-release-manifest-v1.schema.json +++ b/docs/schemas/comparevi-tools-release-manifest-v1.schema.json @@ -178,7 +178,8 @@ "capabilities": { "type": "object", "required": [ - "viHistory" + "viHistory", + "dockerProfile" ], "properties": { "viHistory": { @@ -270,6 +271,71 @@ } }, "additionalProperties": false + }, + "dockerProfile": { + "type": "object", + "required": [ + "schema", + "capabilityId", + "displayName", + "distributionRole", + "distributionModel", + "bundleMetadataPath", + "bundleImportPath", + "releaseAssetPattern", + "authoritativeConsumerPinFieldPath", + "authoritativeConsumerPinKindFieldPath", + "authoritativeImageContractSource", + "notes" + ], + "properties": { + "schema": { + "const": "comparevi-tools/docker-profile-capability@v1" + }, + "capabilityId": { + "const": "docker-profile" + }, + "displayName": { + "type": "string" + }, + "distributionRole": { + "type": "string", + "enum": [ + "upstream-producer" + ] + }, + "distributionModel": { + "type": "string", + "enum": [ + "release-bundle" + ] + }, + "bundleMetadataPath": { + "type": "string" + }, + "bundleImportPath": { + "type": "string" + }, + "releaseAssetPattern": { + "type": "string" + }, + "authoritativeConsumerPinFieldPath": { + "type": "string" + }, + "authoritativeConsumerPinKindFieldPath": { + "type": "string" + }, + "authoritativeImageContractSource": { + "type": "string" + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -473,6 +539,65 @@ } }, "additionalProperties": false + }, + "dockerImageContract": { + "type": "object", + "required": [ + "schema", + "schemaUrl", + "images", + "notes" + ], + "properties": { + "schema": { + "const": "comparevi-tools/docker-image-contract@v1" + }, + "schemaUrl": { + "type": "string" + }, + "images": { + "type": "object", + "required": [ + "hostedNiLinuxRunner" + ], + "properties": { + "hostedNiLinuxRunner": { + "type": "object", + "required": [ + "imageRef", + "consumerRole", + "notes" + ], + "properties": { + "imageRef": { + "type": "string" + }, + "consumerRole": { + "type": "string", + "enum": [ + "hosted-ni-linux-runner" + ] + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/docs/schemas/concurrent-lane-status-receipt-v1.schema.json b/docs/schemas/concurrent-lane-status-receipt-v1.schema.json index 57bd571fd..d34158a6c 100644 --- a/docs/schemas/concurrent-lane-status-receipt-v1.schema.json +++ b/docs/schemas/concurrent-lane-status-receipt-v1.schema.json @@ -246,6 +246,50 @@ } } }, + "executionBundle": { + "type": ["object", "null"], + "additionalProperties": false, + "required": [ + "path", + "schema", + "status", + "cellId", + "laneId", + "cellClass", + "suiteClass", + "executionCellLeaseId", + "dockerLaneLeaseId", + "harnessKind", + "harnessInstanceId", + "planeBinding", + "premiumSaganMode", + "reciprocalLinkReady", + "effectiveBillableRateUsdPerHour", + "operatorAuthorizationRef", + "isolatedLaneGroupId", + "fingerprintSha256" + ], + "properties": { + "path": { "type": ["string", "null"] }, + "schema": { "type": ["string", "null"] }, + "status": { "type": ["string", "null"] }, + "cellId": { "type": ["string", "null"] }, + "laneId": { "type": ["string", "null"] }, + "cellClass": { "type": ["string", "null"] }, + "suiteClass": { "type": ["string", "null"] }, + "executionCellLeaseId": { "type": ["string", "null"] }, + "dockerLaneLeaseId": { "type": ["string", "null"] }, + "harnessKind": { "type": ["string", "null"] }, + "harnessInstanceId": { "type": ["string", "null"] }, + "planeBinding": { "type": ["string", "null"] }, + "premiumSaganMode": { "type": "boolean" }, + "reciprocalLinkReady": { "type": "boolean" }, + "effectiveBillableRateUsdPerHour": { "type": ["number", "null"] }, + "operatorAuthorizationRef": { "type": ["string", "null"] }, + "isolatedLaneGroupId": { "type": ["string", "null"] }, + "fingerprintSha256": { "type": ["string", "null"] } + } + }, "laneStatuses": { "type": "array", "items": { @@ -352,6 +396,9 @@ "deferredLaneCount", "manualLaneCount", "shadowLaneCount", + "executionBundleStatus", + "executionBundleReciprocalLinkReady", + "executionBundlePremiumSaganMode", "idleClassificationCoverage", "pullRequestStatus", "orchestratorDisposition" @@ -396,6 +443,15 @@ "type": "integer", "minimum": 0 }, + "executionBundleStatus": { + "type": ["string", "null"] + }, + "executionBundleReciprocalLinkReady": { + "type": "boolean" + }, + "executionBundlePremiumSaganMode": { + "type": "boolean" + }, "idleClassificationCoverage": { "type": "object", "additionalProperties": false, diff --git a/docs/schemas/delivery-agent-runtime-state-v1.schema.json b/docs/schemas/delivery-agent-runtime-state-v1.schema.json index f72b3aa9a..3f4aa961c 100644 --- a/docs/schemas/delivery-agent-runtime-state-v1.schema.json +++ b/docs/schemas/delivery-agent-runtime-state-v1.schema.json @@ -322,6 +322,34 @@ "requiresLocalCheckout": { "type": ["boolean", "null"] } } }, + "executionTopology": { + "type": ["object", "null"], + "additionalProperties": true, + "properties": { + "status": { "type": ["string", "null"] }, + "executionPlane": { "type": ["string", "null"] }, + "providerId": { "type": ["string", "null"] }, + "workerSlotId": { "type": ["string", "null"] }, + "cellId": { "type": ["string", "null"] }, + "laneId": { "type": ["string", "null"] }, + "cellClass": { "type": ["string", "null"] }, + "suiteClass": { "type": ["string", "null"] }, + "planeBinding": { "type": ["string", "null"] }, + "harnessKind": { "type": ["string", "null"] }, + "harnessInstanceId": { "type": ["string", "null"] }, + "executionCellLeaseId": { "type": ["string", "null"] }, + "dockerLaneLeaseId": { "type": ["string", "null"] }, + "premiumSaganMode": { "type": "boolean" }, + "reciprocalLinkReady": { "type": "boolean" }, + "operatorAuthorizationRef": { "type": ["string", "null"] }, + "activeLogicalLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "seededLogicalLaneCount": { "type": ["integer", "null"], "minimum": 0 }, + "runtimeSurface": { "type": ["string", "null"] }, + "processModelClass": { "type": ["string", "null"] }, + "windowsOnly": { "type": "boolean" }, + "requestedSimultaneous": { "type": "boolean" } + } + }, "providerDispatch": { "type": ["object", "null"], "additionalProperties": true, diff --git a/docs/schemas/docker-lane-handshake-report-v1.schema.json b/docs/schemas/docker-lane-handshake-report-v1.schema.json new file mode 100644 index 000000000..6a657a279 --- /dev/null +++ b/docs/schemas/docker-lane-handshake-report-v1.schema.json @@ -0,0 +1,117 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.comparevi.dev/docker-lane-handshake-report-v1.schema.json", + "title": "Docker Lane Handshake Report v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "generatedAt", + "action", + "status", + "laneId", + "handshakePath", + "policy", + "handshake", + "summary" + ], + "properties": { + "schema": { + "const": "priority/docker-lane-handshake-report@v1" + }, + "generatedAt": { + "type": "string", + "format": "date-time" + }, + "action": { + "enum": ["request", "grant", "commit", "heartbeat", "release", "inspect"] + }, + "status": { + "enum": [ + "requested", + "granted", + "committed", + "released", + "renewed", + "active", + "busy", + "denied", + "not-found", + "mismatch", + "invalid-state", + "stale" + ] + }, + "laneId": { + "type": "string", + "minLength": 1 + }, + "handshakePath": { + "type": "string", + "minLength": 1 + }, + "policy": { + "type": "object", + "additionalProperties": false, + "required": ["operatorId", "currency", "laborRateUsdPerHour", "premiumSaganRateMultiplier"], + "properties": { + "operatorId": { "type": ["string", "null"] }, + "currency": { "type": ["string", "null"] }, + "laborRateUsdPerHour": { "type": ["number", "null"], "minimum": 0 }, + "premiumSaganRateMultiplier": { "type": "number", "minimum": 1 } + } + }, + "handshake": { + "anyOf": [ + { "type": "null" }, + { "$ref": "docker-lane-handshake-v1.schema.json" } + ] + }, + "summary": { + "type": "object", + "additionalProperties": false, + "required": [ + "handshakeState", + "leaseId", + "holder", + "premiumSaganMode", + "billableRateMultiplier", + "billableRateUsdPerHour", + "operatorAuthorizationRef", + "isolatedLaneGroupId", + "fingerprintSha256", + "linkedExecutionCellId", + "linkedExecutionCellLeaseId", + "isStale", + "ageSeconds", + "ttlSeconds", + "denialReasons", + "observations" + ], + "properties": { + "handshakeState": { "type": ["string", "null"] }, + "leaseId": { "type": ["string", "null"] }, + "holder": { "type": ["string", "null"] }, + "premiumSaganMode": { "type": "boolean" }, + "billableRateMultiplier": { "type": ["number", "null"], "minimum": 1 }, + "billableRateUsdPerHour": { "type": ["number", "null"], "minimum": 0 }, + "operatorAuthorizationRef": { "type": ["string", "null"] }, + "isolatedLaneGroupId": { "type": ["string", "null"] }, + "fingerprintSha256": { "type": ["string", "null"] }, + "linkedExecutionCellId": { "type": ["string", "null"] }, + "linkedExecutionCellLeaseId": { "type": ["string", "null"] }, + "isStale": { "type": "boolean" }, + "ageSeconds": { "type": ["number", "null"], "minimum": 0 }, + "ttlSeconds": { "type": ["integer", "null"], "minimum": 1 }, + "denialReasons": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "observations": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + } + } + } +} diff --git a/docs/schemas/docker-lane-handshake-v1.schema.json b/docs/schemas/docker-lane-handshake-v1.schema.json new file mode 100644 index 000000000..a8044c9ec --- /dev/null +++ b/docs/schemas/docker-lane-handshake-v1.schema.json @@ -0,0 +1,172 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.comparevi.dev/docker-lane-handshake-v1.schema.json", + "title": "Docker Lane Handshake v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "generatedAt", + "laneId", + "resourceKind", + "state", + "sequence", + "heartbeatAt", + "host", + "request", + "grant", + "commit", + "release" + ], + "properties": { + "schema": { + "const": "priority/docker-lane-handshake@v1" + }, + "generatedAt": { + "type": "string", + "format": "date-time" + }, + "laneId": { + "type": "string", + "minLength": 1 + }, + "resourceKind": { + "const": "docker-lane" + }, + "state": { + "enum": ["requested", "granted", "active", "released"] + }, + "sequence": { + "type": "integer", + "minimum": 1 + }, + "heartbeatAt": { + "type": "string", + "format": "date-time" + }, + "host": { + "anyOf": [ + { "type": "null" }, + { + "type": "object", + "additionalProperties": false, + "required": ["isolatedLaneGroupId", "fingerprintSha256", "platform", "computerName", "canonical"], + "properties": { + "isolatedLaneGroupId": { "type": ["string", "null"] }, + "fingerprintSha256": { "type": ["string", "null"] }, + "platform": { "type": ["string", "null"] }, + "computerName": { "type": ["string", "null"] }, + "canonical": { + "type": "object", + "additionalProperties": false, + "required": ["version", "buildNumber", "ubr"], + "properties": { + "version": { "type": ["string", "null"] }, + "buildNumber": { "type": ["string", "null"] }, + "ubr": { "type": ["integer", "null"], "minimum": 0 } + } + } + } + } + ] + }, + "request": { + "type": "object", + "additionalProperties": false, + "required": [ + "requestId", + "requestedAt", + "agentId", + "agentClass", + "capabilities", + "premiumDualLaneRequested", + "operatorId", + "operatorAuthorizationRef" + ], + "properties": { + "requestId": { "type": "string", "minLength": 1 }, + "requestedAt": { "type": "string", "format": "date-time" }, + "agentId": { "type": "string", "minLength": 1 }, + "agentClass": { "enum": ["sagan", "subagent", "other"] }, + "capabilities": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "premiumDualLaneRequested": { "type": "boolean" }, + "operatorId": { "type": ["string", "null"] }, + "operatorAuthorizationRef": { "type": ["string", "null"] } + } + }, + "grant": { + "anyOf": [ + { "type": "null" }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "grantedAt", + "grantor", + "leaseId", + "ttlSeconds", + "grantedCapabilities", + "billableRateMultiplier", + "billableRateUsdPerHour", + "premiumSaganMode", + "policyDecision", + "operatorAuthorizationRef" + ], + "properties": { + "grantedAt": { "type": "string", "format": "date-time" }, + "grantor": { "type": "string", "minLength": 1 }, + "leaseId": { "type": "string", "minLength": 1 }, + "ttlSeconds": { "type": "integer", "minimum": 1 }, + "grantedCapabilities": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "billableRateMultiplier": { "type": "number", "minimum": 1 }, + "billableRateUsdPerHour": { "type": "number", "minimum": 0 }, + "premiumSaganMode": { "type": "boolean" }, + "policyDecision": { "enum": ["ordinary-docker-lane", "sagan-premium-dual-lane"] }, + "operatorAuthorizationRef": { "type": ["string", "null"] } + } + } + ] + }, + "commit": { + "anyOf": [ + { "type": "null" }, + { + "type": "object", + "additionalProperties": false, + "required": ["committedAt", "executionCellId", "executionCellLeaseId"], + "properties": { + "committedAt": { "type": "string", "format": "date-time" }, + "executionCellId": { "type": ["string", "null"] }, + "executionCellLeaseId": { "type": ["string", "null"] } + } + } + ] + }, + "release": { + "anyOf": [ + { "type": "null" }, + { + "type": "object", + "additionalProperties": false, + "required": ["releasedAt", "finalStatus", "artifactPaths"], + "properties": { + "releasedAt": { "type": "string", "format": "date-time" }, + "finalStatus": { "type": "string", "minLength": 1 }, + "artifactPaths": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + } + } + ] + } + } +} diff --git a/docs/schemas/execution-cell-bundle-report-v1.schema.json b/docs/schemas/execution-cell-bundle-report-v1.schema.json new file mode 100644 index 000000000..3e9543b1e --- /dev/null +++ b/docs/schemas/execution-cell-bundle-report-v1.schema.json @@ -0,0 +1,315 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.comparevi.dev/execution-cell-bundle-report-v1.schema.json", + "title": "Execution Cell Bundle Report", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "generatedAt", + "action", + "status", + "cellId", + "laneId", + "outputPath", + "executionCellReportPath", + "dockerLaneReportPath", + "executionCell", + "dockerLane", + "rollbacks", + "summary" + ], + "properties": { + "schema": { + "const": "priority/execution-cell-bundle-report@v1" + }, + "generatedAt": { + "type": "string", + "format": "date-time" + }, + "action": { + "type": "string", + "enum": [ + "request", + "grant", + "commit", + "heartbeat", + "release", + "inspect" + ] + }, + "status": { + "type": "string", + "enum": [ + "requested", + "granted", + "committed", + "renewed", + "released", + "active", + "busy", + "denied", + "not-found", + "mismatch", + "invalid-state", + "stale", + "partial" + ] + }, + "cellId": { + "type": "string", + "minLength": 1 + }, + "laneId": { + "type": [ + "string", + "null" + ] + }, + "outputPath": { + "type": "string", + "minLength": 1 + }, + "executionCellReportPath": { + "type": [ + "string", + "null" + ] + }, + "dockerLaneReportPath": { + "type": [ + "string", + "null" + ] + }, + "executionCell": { + "oneOf": [ + { + "$ref": "execution-cell-lease-report-v1.schema.json" + }, + { + "type": "null" + } + ] + }, + "dockerLane": { + "oneOf": [ + { + "$ref": "docker-lane-handshake-report-v1.schema.json" + }, + { + "type": "null" + } + ] + }, + "rollbacks": { + "type": "object", + "additionalProperties": false, + "required": [ + "executionCell", + "dockerLane" + ], + "properties": { + "executionCell": { + "oneOf": [ + { + "$ref": "execution-cell-lease-report-v1.schema.json" + }, + { + "type": "null" + } + ] + }, + "dockerLane": { + "oneOf": [ + { + "$ref": "docker-lane-handshake-report-v1.schema.json" + }, + { + "type": "null" + } + ] + } + } + }, + "summary": { + "type": "object", + "additionalProperties": false, + "required": [ + "holder", + "agentClass", + "cellClass", + "suiteClass", + "planeBinding", + "harnessKind", + "harnessInstanceId", + "executionCellLeaseId", + "dockerLaneLeaseId", + "linkedExecutionCellId", + "linkedExecutionCellLeaseId", + "linkedDockerLaneId", + "linkedDockerLaneLeaseId", + "reciprocalLinkReady", + "dockerRequested", + "windowsNativeTestStand", + "effectiveBillableRateMultiplier", + "effectiveBillableRateUsdPerHour", + "premiumSaganMode", + "operatorAuthorizationRef", + "isolatedLaneGroupId", + "fingerprintSha256", + "capabilities", + "denialReasons", + "observations" + ], + "properties": { + "holder": { + "type": [ + "string", + "null" + ] + }, + "agentClass": { + "type": [ + "string", + "null" + ] + }, + "cellClass": { + "type": [ + "string", + "null" + ] + }, + "suiteClass": { + "type": [ + "string", + "null" + ] + }, + "planeBinding": { + "type": [ + "string", + "null" + ] + }, + "harnessKind": { + "type": [ + "string", + "null" + ] + }, + "harnessInstanceId": { + "type": [ + "string", + "null" + ] + }, + "executionCellLeaseId": { + "type": [ + "string", + "null" + ] + }, + "dockerLaneLeaseId": { + "type": [ + "string", + "null" + ] + }, + "linkedExecutionCellId": { + "type": [ + "string", + "null" + ] + }, + "linkedExecutionCellLeaseId": { + "type": [ + "string", + "null" + ] + }, + "linkedDockerLaneId": { + "type": [ + "string", + "null" + ] + }, + "linkedDockerLaneLeaseId": { + "type": [ + "string", + "null" + ] + }, + "reciprocalLinkReady": { + "type": "boolean" + }, + "dockerRequested": { + "type": "boolean" + }, + "windowsNativeTestStand": { + "type": [ + "boolean", + "null" + ] + }, + "effectiveBillableRateMultiplier": { + "type": [ + "number", + "null" + ], + "minimum": 0 + }, + "effectiveBillableRateUsdPerHour": { + "type": [ + "number", + "null" + ], + "minimum": 0 + }, + "premiumSaganMode": { + "type": "boolean" + }, + "operatorAuthorizationRef": { + "type": [ + "string", + "null" + ] + }, + "isolatedLaneGroupId": { + "type": [ + "string", + "null" + ] + }, + "fingerprintSha256": { + "type": [ + "string", + "null" + ] + }, + "capabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "denialReasons": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "observations": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + } + } + } +} diff --git a/docs/schemas/execution-cell-lease-report-v1.schema.json b/docs/schemas/execution-cell-lease-report-v1.schema.json new file mode 100644 index 000000000..a4c2e4fc1 --- /dev/null +++ b/docs/schemas/execution-cell-lease-report-v1.schema.json @@ -0,0 +1,277 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.comparevi.dev/execution-cell-lease-report-v1.schema.json", + "title": "Execution Cell Lease Report", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "generatedAt", + "action", + "status", + "cellId", + "leasePath", + "lease", + "summary", + "policy" + ], + "properties": { + "schema": { + "const": "priority/execution-cell-lease-report@v1" + }, + "generatedAt": { + "type": "string", + "format": "date-time" + }, + "action": { + "type": "string", + "enum": [ + "request", + "grant", + "commit", + "heartbeat", + "release", + "inspect" + ] + }, + "status": { + "type": "string", + "enum": [ + "requested", + "granted", + "committed", + "renewed", + "released", + "active", + "busy", + "denied", + "not-found", + "mismatch", + "invalid-state", + "stale" + ] + }, + "cellId": { + "type": "string", + "minLength": 1 + }, + "leasePath": { + "type": "string", + "minLength": 1 + }, + "lease": { + "oneOf": [ + { + "$ref": "execution-cell-lease-v1.schema.json" + }, + { + "type": "null" + } + ] + }, + "policy": { + "type": "object", + "additionalProperties": false, + "required": [ + "operatorId", + "currency", + "laborRateUsdPerHour" + ], + "properties": { + "operatorId": { + "type": [ + "string", + "null" + ] + }, + "currency": { + "type": "string", + "minLength": 1 + }, + "laborRateUsdPerHour": { + "type": [ + "number", + "null" + ], + "minimum": 0 + } + } + }, + "summary": { + "type": "object", + "additionalProperties": false, + "required": [ + "leaseState", + "leaseId", + "holder", + "agentClass", + "cellClass", + "harnessKind", + "harnessInstanceId", + "suiteClass", + "planeBinding", + "premiumSaganMode", + "billableRateMultiplier", + "billableRateUsdPerHour", + "operatorAuthorizationRef", + "isolatedLaneGroupId", + "fingerprintSha256", + "linkedDockerLaneId", + "linkedDockerLaneLeaseId", + "workingRoot", + "artifactRoot", + "isStale", + "ageSeconds", + "ttlSeconds", + "denialReasons", + "observations" + ], + "properties": { + "leaseState": { + "type": [ + "string", + "null" + ] + }, + "leaseId": { + "type": [ + "string", + "null" + ] + }, + "holder": { + "type": [ + "string", + "null" + ] + }, + "agentClass": { + "type": [ + "string", + "null" + ] + }, + "cellClass": { + "type": [ + "string", + "null" + ] + }, + "harnessKind": { + "type": [ + "string", + "null" + ] + }, + "harnessInstanceId": { + "type": [ + "string", + "null" + ] + }, + "suiteClass": { + "type": [ + "string", + "null" + ] + }, + "planeBinding": { + "type": [ + "string", + "null" + ] + }, + "premiumSaganMode": { + "type": "boolean" + }, + "billableRateMultiplier": { + "type": [ + "number", + "null" + ], + "minimum": 0 + }, + "billableRateUsdPerHour": { + "type": [ + "number", + "null" + ], + "minimum": 0 + }, + "operatorAuthorizationRef": { + "type": [ + "string", + "null" + ] + }, + "isolatedLaneGroupId": { + "type": [ + "string", + "null" + ] + }, + "fingerprintSha256": { + "type": [ + "string", + "null" + ] + }, + "linkedDockerLaneId": { + "type": [ + "string", + "null" + ] + }, + "linkedDockerLaneLeaseId": { + "type": [ + "string", + "null" + ] + }, + "workingRoot": { + "type": [ + "string", + "null" + ] + }, + "artifactRoot": { + "type": [ + "string", + "null" + ] + }, + "isStale": { + "type": "boolean" + }, + "ageSeconds": { + "type": [ + "number", + "null" + ], + "minimum": 0 + }, + "ttlSeconds": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "denialReasons": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "observations": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + } + } + } +} diff --git a/docs/schemas/execution-cell-lease-v1.schema.json b/docs/schemas/execution-cell-lease-v1.schema.json new file mode 100644 index 000000000..121ea84e2 --- /dev/null +++ b/docs/schemas/execution-cell-lease-v1.schema.json @@ -0,0 +1,365 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.comparevi.dev/execution-cell-lease-v1.schema.json", + "title": "Execution Cell Lease", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "generatedAt", + "cellId", + "resourceKind", + "state", + "sequence", + "heartbeatAt", + "request", + "grant", + "commit", + "release" + ], + "properties": { + "schema": { + "const": "priority/execution-cell-lease@v1" + }, + "generatedAt": { + "type": "string", + "format": "date-time" + }, + "cellId": { + "type": "string", + "minLength": 1 + }, + "resourceKind": { + "const": "execution-cell" + }, + "state": { + "type": "string", + "enum": [ + "requested", + "granted", + "active", + "released" + ] + }, + "sequence": { + "type": "integer", + "minimum": 1 + }, + "heartbeatAt": { + "type": "string", + "format": "date-time" + }, + "host": { + "type": [ + "object", + "null" + ], + "additionalProperties": false, + "required": [ + "isolatedLaneGroupId", + "fingerprintSha256", + "platform", + "computerName", + "canonical" + ], + "properties": { + "isolatedLaneGroupId": { + "type": [ + "string", + "null" + ] + }, + "fingerprintSha256": { + "type": [ + "string", + "null" + ] + }, + "platform": { + "type": [ + "string", + "null" + ] + }, + "computerName": { + "type": [ + "string", + "null" + ] + }, + "canonical": { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "buildNumber", + "ubr" + ], + "properties": { + "version": { + "type": [ + "string", + "null" + ] + }, + "buildNumber": { + "type": [ + "string", + "null" + ] + }, + "ubr": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + } + } + } + } + }, + "request": { + "type": "object", + "additionalProperties": false, + "required": [ + "requestId", + "requestedAt", + "agentId", + "agentClass", + "cellClass", + "suiteClass", + "planeBinding", + "harnessKind", + "capabilities", + "premiumDualLaneRequested", + "operatorId", + "operatorAuthorizationRef", + "workingRoot", + "artifactRoot" + ], + "properties": { + "requestId": { + "type": "string", + "minLength": 1 + }, + "requestedAt": { + "type": "string", + "format": "date-time" + }, + "agentId": { + "type": [ + "string", + "null" + ] + }, + "agentClass": { + "type": "string", + "enum": [ + "sagan", + "subagent", + "other" + ] + }, + "cellClass": { + "type": "string", + "enum": [ + "worker", + "coordinator", + "kernel-coordinator" + ] + }, + "suiteClass": { + "type": [ + "string", + "null" + ] + }, + "planeBinding": { + "type": [ + "string", + "null" + ] + }, + "harnessKind": { + "type": "string", + "minLength": 1 + }, + "capabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "premiumDualLaneRequested": { + "type": "boolean" + }, + "operatorId": { + "type": [ + "string", + "null" + ] + }, + "operatorAuthorizationRef": { + "type": [ + "string", + "null" + ] + }, + "workingRoot": { + "type": [ + "string", + "null" + ] + }, + "artifactRoot": { + "type": [ + "string", + "null" + ] + } + } + }, + "grant": { + "type": [ + "object", + "null" + ], + "additionalProperties": false, + "required": [ + "grantedAt", + "grantor", + "leaseId", + "ttlSeconds", + "premiumDualLaneRequested", + "premiumSaganMode", + "policyDecision", + "grantedCapabilities", + "billableRateMultiplier", + "billableRateUsdPerHour" + ], + "properties": { + "grantedAt": { + "type": "string", + "format": "date-time" + }, + "grantor": { + "type": "string", + "minLength": 1 + }, + "leaseId": { + "type": "string", + "minLength": 1 + }, + "ttlSeconds": { + "type": "integer", + "minimum": 1 + }, + "premiumDualLaneRequested": { + "type": "boolean" + }, + "premiumSaganMode": { + "type": "boolean" + }, + "policyDecision": { + "type": "string", + "minLength": 1 + }, + "grantedCapabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "billableRateMultiplier": { + "type": "number", + "minimum": 0 + }, + "billableRateUsdPerHour": { + "type": [ + "number", + "null" + ], + "minimum": 0 + } + } + }, + "commit": { + "type": [ + "object", + "null" + ], + "additionalProperties": false, + "required": [ + "committedAt", + "harnessInstanceId", + "dockerLaneId", + "dockerLaneLeaseId", + "workingRoot", + "artifactRoot" + ], + "properties": { + "committedAt": { + "type": "string", + "format": "date-time" + }, + "harnessInstanceId": { + "type": [ + "string", + "null" + ] + }, + "dockerLaneId": { + "type": [ + "string", + "null" + ] + }, + "dockerLaneLeaseId": { + "type": [ + "string", + "null" + ] + }, + "workingRoot": { + "type": [ + "string", + "null" + ] + }, + "artifactRoot": { + "type": [ + "string", + "null" + ] + } + } + }, + "release": { + "type": [ + "object", + "null" + ], + "additionalProperties": false, + "required": [ + "releasedAt", + "artifactPaths" + ], + "properties": { + "releasedAt": { + "type": "string", + "format": "date-time" + }, + "artifactPaths": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + } + } + } +} diff --git a/docs/schemas/github-comment-budget-hook-policy-v1.schema.json b/docs/schemas/github-comment-budget-hook-policy-v1.schema.json new file mode 100644 index 000000000..5eb2d3dc4 --- /dev/null +++ b/docs/schemas/github-comment-budget-hook-policy-v1.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.labview-community-cicd.example/compare-vi-cli-action/github-comment-budget-hook-policy-v1.schema.json", + "title": "GitHub Comment Budget Hook Policy v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "costRollupPath", + "materializationPolicyPath", + "materializationReportPath", + "outputPath", + "markdownOutputPath", + "operatorBudgetCapUsd", + "materializeCostRollup", + "reservedFundingPurposes", + "reservedActivationStates" + ], + "properties": { + "schema": { "const": "priority/github-comment-budget-hook-policy@v1" }, + "costRollupPath": { "type": "string", "minLength": 1 }, + "materializationPolicyPath": { "type": "string", "minLength": 1 }, + "materializationReportPath": { "type": "string", "minLength": 1 }, + "outputPath": { "type": "string", "minLength": 1 }, + "markdownOutputPath": { "type": "string", "minLength": 1 }, + "operatorBudgetCapUsd": { "type": "number", "minimum": 0 }, + "materializeCostRollup": { "type": "boolean" }, + "reservedFundingPurposes": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "reservedActivationStates": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + } +} diff --git a/docs/schemas/github-comment-budget-hook-report-v1.schema.json b/docs/schemas/github-comment-budget-hook-report-v1.schema.json new file mode 100644 index 000000000..9506f490e --- /dev/null +++ b/docs/schemas/github-comment-budget-hook-report-v1.schema.json @@ -0,0 +1,146 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.labview-community-cicd.example/compare-vi-cli-action/github-comment-budget-hook-report-v1.schema.json", + "title": "GitHub Comment Budget Hook Report v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "generatedAt", + "repository", + "target", + "summary", + "turns", + "funding", + "source", + "blockers" + ], + "properties": { + "schema": { "const": "priority/github-comment-budget-hook@v1" }, + "generatedAt": { "type": "string", "format": "date-time" }, + "repository": { "type": ["string", "null"] }, + "target": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "number"], + "properties": { + "kind": { "type": "string", "minLength": 1 }, + "number": { "type": ["integer", "null"], "minimum": 1 } + } + }, + "summary": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "recommendation", + "tokenSpendUsd", + "operatorLaborObservedUsd", + "operatorLaborMissingTurnCount", + "observedBlendedLowerBoundUsd", + "knownBlendedUsd", + "operatorBudgetCapUsd", + "operatorBudgetRemainingLowerBoundUsd", + "operatorBudgetRemainingStatus" + ], + "properties": { + "status": { "type": "string", "enum": ["pass", "warn", "blocked"] }, + "recommendation": { "type": "string", "minLength": 1 }, + "tokenSpendUsd": { "type": "number", "minimum": 0 }, + "operatorLaborObservedUsd": { "type": "number", "minimum": 0 }, + "operatorLaborMissingTurnCount": { "type": "integer", "minimum": 0 }, + "observedBlendedLowerBoundUsd": { "type": "number", "minimum": 0 }, + "knownBlendedUsd": { "type": ["number", "null"], "minimum": 0 }, + "operatorBudgetCapUsd": { "type": ["number", "null"], "minimum": 0 }, + "operatorBudgetRemainingLowerBoundUsd": { "type": ["number", "null"], "minimum": 0 }, + "operatorBudgetRemainingStatus": { "type": "string", "enum": ["observed", "lower-bound", "unknown"] } + } + }, + "turns": { + "type": "object", + "additionalProperties": false, + "required": ["totalTurns", "liveTurnCount", "backgroundTurnCount"], + "properties": { + "totalTurns": { "type": "integer", "minimum": 0 }, + "liveTurnCount": { "type": "integer", "minimum": 0 }, + "backgroundTurnCount": { "type": "integer", "minimum": 0 } + } + }, + "funding": { + "type": "object", + "additionalProperties": false, + "required": ["billingWindow", "reservedFunding"], + "properties": { + "billingWindow": { + "type": ["object", "null"], + "additionalProperties": false, + "required": ["invoiceTurnId", "invoiceId", "fundingPurpose", "activationState", "prepaidUsd", "tokenSpendUsd", "remainingUsd", "pricingBasis", "selectionMode", "selectionReason"], + "properties": { + "invoiceTurnId": { "type": ["string", "null"] }, + "invoiceId": { "type": ["string", "null"] }, + "fundingPurpose": { "type": ["string", "null"] }, + "activationState": { "type": ["string", "null"] }, + "prepaidUsd": { "type": ["number", "null"], "minimum": 0 }, + "tokenSpendUsd": { "type": "number", "minimum": 0 }, + "remainingUsd": { "type": ["number", "null"], "minimum": 0 }, + "pricingBasis": { "type": ["string", "null"] }, + "selectionMode": { "type": ["string", "null"] }, + "selectionReason": { "type": ["string", "null"] } + } + }, + "reservedFunding": { + "type": "object", + "additionalProperties": false, + "required": ["count", "totalReservedUsd", "windows"], + "properties": { + "count": { "type": "integer", "minimum": 0 }, + "totalReservedUsd": { "type": "number", "minimum": 0 }, + "windows": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["invoiceTurnId", "invoiceId", "fundingPurpose", "activationState", "prepaidUsd", "operatorNote"], + "properties": { + "invoiceTurnId": { "type": ["string", "null"] }, + "invoiceId": { "type": ["string", "null"] }, + "fundingPurpose": { "type": ["string", "null"] }, + "activationState": { "type": ["string", "null"] }, + "prepaidUsd": { "type": ["number", "null"], "minimum": 0 }, + "operatorNote": { "type": ["string", "null"] } + } + } + } + } + } + } + }, + "source": { + "type": "object", + "additionalProperties": false, + "required": ["policyPath", "costRollupPath", "costRollupMaterialized", "costRollupMaterializationReportPath", "operatorCostProfilePath", "outputPath", "markdownOutputPath"], + "properties": { + "policyPath": { "type": ["string", "null"] }, + "costRollupPath": { "type": ["string", "null"] }, + "costRollupMaterialized": { "type": "boolean" }, + "costRollupMaterializationReportPath": { "type": ["string", "null"] }, + "operatorCostProfilePath": { "type": ["string", "null"] }, + "outputPath": { "type": ["string", "null"] }, + "markdownOutputPath": { "type": ["string", "null"] } + } + }, + "blockers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["code", "message", "details"], + "properties": { + "code": { "type": "string", "minLength": 1 }, + "message": { "type": "string", "minLength": 1 }, + "details": { "type": ["string", "null"] } + } + } + } + } +} diff --git a/docs/schemas/labview-2026-host-plane-report-v1.schema.json b/docs/schemas/labview-2026-host-plane-report-v1.schema.json index b12e59526..7c677a0a9 100644 --- a/docs/schemas/labview-2026-host-plane-report-v1.schema.json +++ b/docs/schemas/labview-2026-host-plane-report-v1.schema.json @@ -23,10 +23,11 @@ }, "host": { "type": "object", - "required": ["os", "computerName"], + "required": ["os", "computerName", "osFingerprint"], "properties": { "os": { "type": "string" }, - "computerName": { "type": "string" } + "computerName": { "type": "string" }, + "osFingerprint": { "$ref": "#/definitions/osFingerprint" } }, "additionalProperties": false }, @@ -183,6 +184,94 @@ } }, "additionalProperties": false + }, + "osFingerprint": { + "type": "object", + "required": [ + "role", + "comparisonScope", + "platform", + "fingerprintSha256", + "isolatedLaneGroupId", + "canonical", + "advisory", + "sources" + ], + "properties": { + "role": { "const": "canonical-host-baseline" }, + "comparisonScope": { "const": "isolated-lane-group" }, + "platform": { "type": "string" }, + "fingerprintSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "isolatedLaneGroupId": { + "type": "string", + "pattern": "^host-os-fingerprint:[a-f0-9]{64}$" + }, + "canonical": { + "type": "object", + "required": [ + "version", + "buildNumber", + "ubr", + "displayVersion", + "editionId", + "installationType", + "architecture", + "systemType", + "buildLabEx" + ], + "properties": { + "version": { "type": "string" }, + "buildNumber": { "type": "string" }, + "ubr": { "type": "integer", "minimum": 0 }, + "displayVersion": { "type": "string" }, + "editionId": { "type": "string" }, + "installationType": { "type": "string" }, + "architecture": { "type": "string" }, + "systemType": { "type": "string" }, + "buildLabEx": { "type": "string" } + }, + "additionalProperties": false + }, + "advisory": { + "type": "object", + "required": [ + "caption", + "productName", + "currentVersionCompatibility", + "brandingMismatch", + "installDate", + "lastBootUpTime" + ], + "properties": { + "caption": { "type": "string" }, + "productName": { "type": "string" }, + "currentVersionCompatibility": { "type": "string" }, + "brandingMismatch": { "type": "boolean" }, + "installDate": { "type": "string" }, + "lastBootUpTime": { "type": "string" } + }, + "additionalProperties": false + }, + "sources": { + "type": "object", + "required": ["registryPath", "cimClass", "systemClass", "comparisonFields"], + "properties": { + "registryPath": { "type": "string" }, + "cimClass": { "type": "string" }, + "systemClass": { "type": "string" }, + "comparisonFields": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/docs/schemas/loop-final-status-v1.schema.json b/docs/schemas/loop-final-status-v1.schema.json index 006d9ea22..c4fc3257d 100644 --- a/docs/schemas/loop-final-status-v1.schema.json +++ b/docs/schemas/loop-final-status-v1.schema.json @@ -17,7 +17,31 @@ "histogram": {"type": ["array","null"], "items": {"type": "object"}}, "diffSummaryEmitted": {"type": "boolean"}, "basePath": {"type": "string"}, - "headPath": {"type": "string"} + "headPath": {"type": "string"}, + "harness": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "output": {"type": "string"}, + "suiteClass": {"type": "string"}, + "runtimeSurface": {"const": "windows-native-teststand"}, + "processModelClass": { + "type": "string", + "enum": ["sequential-process-model", "parallel-process-model"] + }, + "windowsOnly": {"type": "boolean"}, + "requestedSimultaneous": {"type": "boolean"}, + "cellClass": {"type": ["string", "null"]}, + "operatorAuthorizationRef": {"type": ["string", "null"]}, + "premiumSaganMode": {"type": "boolean"}, + "executionCellLeasePath": {"type": "string"}, + "executionCellId": {"type": "string"}, + "executionCellLeaseId": {"type": "string"}, + "harnessInstanceId": {"type": "string"} + }, + "required": ["path", "output", "suiteClass", "runtimeSurface", "processModelClass", "windowsOnly", "requestedSimultaneous"], + "additionalProperties": true + } }, "additionalProperties": true } diff --git a/docs/schemas/release-conductor-report-v1.schema.json b/docs/schemas/release-conductor-report-v1.schema.json index d7391ed03..f0179c964 100644 --- a/docs/schemas/release-conductor-report-v1.schema.json +++ b/docs/schemas/release-conductor-report-v1.schema.json @@ -46,8 +46,13 @@ "targetTag", "proposalOnly", "tagCreated", + "tagPushed", "tagError", - "signingMaterial" + "tagPushError", + "tagPushRemote", + "signingMaterial", + "repair", + "publicationReplay" ], "properties": { "stream": { "type": "string" }, @@ -56,15 +61,94 @@ "targetTag": { "type": ["string", "null"] }, "proposalOnly": { "type": "boolean" }, "tagCreated": { "type": "boolean" }, + "tagPushed": { "type": "boolean" }, "tagError": { "type": ["string", "null"] }, + "tagPushError": { "type": ["string", "null"] }, + "tagPushRemote": { + "type": "object", + "additionalProperties": false, + "required": ["remoteName", "remoteSlug", "source"], + "properties": { + "remoteName": { "type": ["string", "null"] }, + "remoteSlug": { "type": ["string", "null"] }, + "source": { "type": "string" } + } + }, "signingMaterial": { "type": "object", "additionalProperties": false, - "required": ["available", "signingKey", "source"], + "required": ["available", "signingKey", "source", "backend", "identity"], "properties": { "available": { "type": "boolean" }, "signingKey": { "type": ["string", "null"] }, - "source": { "type": "string" } + "source": { "type": "string" }, + "backend": { "type": "string" }, + "identity": { + "type": "object", + "additionalProperties": false, + "required": ["available", "name", "email", "source", "login", "accountId"], + "properties": { + "available": { "type": "boolean" }, + "name": { "type": ["string", "null"] }, + "email": { "type": ["string", "null"] }, + "source": { "type": "string" }, + "login": { "type": ["string", "null"] }, + "accountId": { "type": ["string", "null"] } + } + } + } + }, + "repair": { + "type": "object", + "additionalProperties": false, + "required": [ + "requested", + "status", + "remoteTagRef", + "remoteTagExists", + "remoteTagAnnotated", + "remoteTagObjectOid", + "remoteTargetCommitOid", + "localTagPresent", + "localTagDeleted", + "tagRecreated", + "pushLeaseExpectedOid", + "lookupError" + ], + "properties": { + "requested": { "type": "boolean" }, + "status": { + "type": "string", + "enum": ["not-requested", "repair-available", "ready", "repaired", "blocked"] + }, + "remoteTagRef": { "type": ["string", "null"] }, + "remoteTagExists": { "type": "boolean" }, + "remoteTagAnnotated": { "type": ["boolean", "null"] }, + "remoteTagObjectOid": { "type": ["string", "null"] }, + "remoteTargetCommitOid": { "type": ["string", "null"] }, + "localTagPresent": { "type": "boolean" }, + "localTagDeleted": { "type": "boolean" }, + "tagRecreated": { "type": "boolean" }, + "pushLeaseExpectedOid": { "type": ["string", "null"] }, + "lookupError": { "type": ["string", "null"] } + } + }, + "publicationReplay": { + "type": "object", + "additionalProperties": false, + "required": ["requested", "workflow", "ref", "tagInputName", "tagInputValue", "dispatched", "status", "error"], + "properties": { + "requested": { "type": "boolean" }, + "workflow": { "type": "string" }, + "ref": { "type": ["string", "null"] }, + "tagInputName": { "type": ["string", "null"] }, + "tagInputValue": { "type": ["string", "null"] }, + "dispatched": { "type": "boolean" }, + "status": { + "type": "string", + "enum": ["not-requested", "blocked", "dispatched", "dispatch-failed"] + }, + "error": { "type": ["string", "null"] } } } } diff --git a/docs/schemas/release-published-bundle-observer-report-v1.schema.json b/docs/schemas/release-published-bundle-observer-report-v1.schema.json new file mode 100644 index 000000000..295ba50e3 --- /dev/null +++ b/docs/schemas/release-published-bundle-observer-report-v1.schema.json @@ -0,0 +1,196 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://labview-community-ci-cd.github.io/compare-vi-cli-action/schemas/release-published-bundle-observer-report-v1.schema.json", + "title": "Release Published Bundle Observer Report", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "generatedAt", + "repository", + "inputs", + "selection", + "bundle", + "bundleContract", + "summary" + ], + "properties": { + "schema": { + "const": "priority/release-published-bundle-observer-report@v1" + }, + "generatedAt": { + "type": "string", + "format": "date-time" + }, + "repository": { + "type": "string", + "minLength": 1 + }, + "inputs": { + "type": "object", + "additionalProperties": false, + "required": ["requestedTag", "resultsDir"], + "properties": { + "requestedTag": { "type": ["string", "null"] }, + "resultsDir": { "type": "string", "minLength": 1 } + } + }, + "selection": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "releaseTag", + "publishedAt", + "releaseName", + "releaseId", + "prerelease", + "draft", + "assetName", + "assetId" + ], + "properties": { + "status": { + "type": "string", + "enum": ["selected", "release-unobserved", "release-not-found", "asset-missing"] + }, + "releaseTag": { "type": ["string", "null"] }, + "publishedAt": { "type": ["string", "null"], "format": "date-time" }, + "releaseName": { "type": ["string", "null"] }, + "releaseId": { "type": ["integer", "null"] }, + "prerelease": { "type": ["boolean", "null"] }, + "draft": { "type": ["boolean", "null"] }, + "assetName": { "type": ["string", "null"] }, + "assetId": { "type": ["integer", "null"] } + } + }, + "bundle": { + "type": "object", + "additionalProperties": false, + "required": ["status", "archivePath", "extractionRoot", "downloadDirectory"], + "properties": { + "status": { + "type": "string", + "enum": ["not-downloaded", "downloaded", "extracted", "download-failed", "extract-failed"] + }, + "archivePath": { "type": ["string", "null"] }, + "extractionRoot": { "type": ["string", "null"] }, + "downloadDirectory": { "type": "string", "minLength": 1 }, + "error": { "type": ["string", "null"] } + } + }, + "bundleContract": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "metadataPath", + "schema", + "authoritativeConsumerPin", + "authoritativeConsumerPinKind", + "capabilityId", + "distributionRole", + "distributionModel", + "bundleImportPath", + "bundleImportPathExists", + "releaseAssetPattern", + "contractPathResolutions", + "dockerCapabilityId", + "dockerDistributionRole", + "dockerDistributionModel", + "authoritativeImageContractSource", + "authoritativeImageContractSourceResolved", + "dockerBundleImportPath", + "dockerBundleImportPathExists", + "dockerReleaseAssetPattern", + "dockerImageContractSchema", + "metadataPresent", + "metadataSchemaMatches", + "viHistoryCapabilityPresent", + "viHistoryCapabilityProducerNative", + "dockerProfileCapabilityPresent", + "dockerProfileCapabilityProducerNative", + "bundleContractPinResolved", + "bundleContractPathsResolved" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "release-unobserved", + "unobserved", + "metadata-missing", + "producer-native-incomplete", + "producer-native-ready", + "download-failed", + "extract-failed" + ] + }, + "metadataPath": { "type": ["string", "null"] }, + "schema": { "type": ["string", "null"] }, + "authoritativeConsumerPin": { "type": ["string", "null"] }, + "authoritativeConsumerPinKind": { "type": ["string", "null"] }, + "capabilityId": { "type": ["string", "null"] }, + "distributionRole": { "type": ["string", "null"] }, + "distributionModel": { "type": ["string", "null"] }, + "bundleImportPath": { "type": ["string", "null"] }, + "bundleImportPathExists": { "type": "boolean" }, + "releaseAssetPattern": { "type": ["string", "null"] }, + "contractPathResolutions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "path", "resolved"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "path": { "type": "string", "minLength": 1 }, + "resolved": { "type": "boolean" } + } + } + }, + "dockerCapabilityId": { "type": ["string", "null"] }, + "dockerDistributionRole": { "type": ["string", "null"] }, + "dockerDistributionModel": { "type": ["string", "null"] }, + "authoritativeImageContractSource": { "type": ["string", "null"] }, + "authoritativeImageContractSourceResolved": { "type": "boolean" }, + "dockerBundleImportPath": { "type": ["string", "null"] }, + "dockerBundleImportPathExists": { "type": "boolean" }, + "dockerReleaseAssetPattern": { "type": ["string", "null"] }, + "dockerImageContractSchema": { "type": ["string", "null"] }, + "metadataPresent": { "type": "boolean" }, + "metadataSchemaMatches": { "type": "boolean" }, + "viHistoryCapabilityPresent": { "type": "boolean" }, + "viHistoryCapabilityProducerNative": { "type": "boolean" }, + "dockerProfileCapabilityPresent": { "type": "boolean" }, + "dockerProfileCapabilityProducerNative": { "type": "boolean" }, + "bundleContractPinResolved": { "type": "boolean" }, + "bundleContractPathsResolved": { "type": "boolean" } + } + }, + "summary": { + "type": "object", + "additionalProperties": false, + "required": ["status", "releaseTag", "assetName", "publishedAt", "authoritativeConsumerPin"], + "properties": { + "status": { + "type": "string", + "enum": [ + "release-unobserved", + "release-not-found", + "asset-missing", + "download-failed", + "extract-failed", + "metadata-missing", + "producer-native-incomplete", + "producer-native-ready" + ] + }, + "releaseTag": { "type": ["string", "null"] }, + "assetName": { "type": ["string", "null"] }, + "publishedAt": { "type": ["string", "null"], "format": "date-time" }, + "authoritativeConsumerPin": { "type": ["string", "null"] } + } + } + } +} diff --git a/docs/schemas/release-signing-readiness-report-v1.schema.json b/docs/schemas/release-signing-readiness-report-v1.schema.json new file mode 100644 index 000000000..7b84e4396 --- /dev/null +++ b/docs/schemas/release-signing-readiness-report-v1.schema.json @@ -0,0 +1,356 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://labview-community-ci-cd.github.io/compare-vi-cli-action/schemas/release-signing-readiness-report-v1.schema.json", + "title": "Release Signing Readiness Report", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "generatedAt", + "repository", + "inputs", + "workflowContract", + "secretInventory", + "releaseConductorApply", + "signingAuthority", + "publication", + "publishedBundleObserver", + "summary", + "blockers" + ], + "properties": { + "schema": { + "const": "priority/release-signing-readiness-report@v1" + }, + "generatedAt": { + "type": "string", + "format": "date-time" + }, + "repository": { + "type": "string", + "minLength": 1 + }, + "inputs": { + "type": "object", + "additionalProperties": false, + "required": [ + "releaseConductorReportPath", + "releasePublishedBundleObserverPath" + ], + "properties": { + "releaseConductorReportPath": { + "type": "string", + "minLength": 1 + }, + "releasePublishedBundleObserverPath": { + "type": "string", + "minLength": 1 + } + } + }, + "workflowContract": { + "type": "object", + "additionalProperties": false, + "required": [ + "ready", + "workflowPath", + "reasons" + ], + "properties": { + "ready": { "type": "boolean" }, + "workflowPath": { "type": "string", "minLength": 1 }, + "reasons": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + } + }, + "secretInventory": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "requiredSecretPresent", + "optionalPublicKeyPresent", + "listedSecretCount", + "listedSecretNames", + "source", + "error" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "configured", + "missing", + "unverifiable" + ] + }, + "requiredSecretPresent": { "type": ["boolean", "null"] }, + "optionalPublicKeyPresent": { "type": ["boolean", "null"] }, + "listedSecretCount": { "type": ["integer", "null"], "minimum": 0 }, + "listedSecretNames": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "source": { "type": "string", "minLength": 1 }, + "error": { "type": ["string", "null"] } + } + }, + "releaseConductorApply": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "variablePresent", + "enabled", + "configuredValue", + "listedVariableCount", + "listedVariableNames", + "source", + "error" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "enabled", + "disabled", + "unverifiable" + ] + }, + "variablePresent": { "type": ["boolean", "null"] }, + "enabled": { "type": ["boolean", "null"] }, + "configuredValue": { "type": ["string", "null"] }, + "listedVariableCount": { "type": ["integer", "null"], "minimum": 0 }, + "listedVariableNames": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "source": { "type": "string", "minLength": 1 }, + "error": { "type": ["string", "null"] } + } + }, + "signingAuthority": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "requiredScope", + "scopeAvailable", + "listedKeyCount", + "source", + "error" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "ready", + "keys-missing", + "scope-missing", + "unverifiable" + ] + }, + "requiredScope": { "type": "string", "minLength": 1 }, + "scopeAvailable": { "type": ["boolean", "null"] }, + "listedKeyCount": { "type": ["integer", "null"], "minimum": 0 }, + "source": { "type": "string", "minLength": 1 }, + "error": { "type": ["string", "null"] } + } + }, + "publication": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "tagCreated", + "tagPushed", + "targetTag" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "unobserved", + "not-attempted", + "tag-created-not-pushed", + "authoritative-publication-successful" + ] + }, + "tagCreated": { "type": "boolean" }, + "tagPushed": { "type": "boolean" }, + "targetTag": { "type": ["string", "null"] } + } + }, + "publishedBundleObserver": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "releaseTag", + "assetName", + "publishedAt", + "authoritativeConsumerPin" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "unobserved", + "release-unobserved", + "release-not-found", + "asset-missing", + "download-failed", + "extract-failed", + "metadata-missing", + "producer-native-incomplete", + "producer-native-ready" + ] + }, + "releaseTag": { "type": ["string", "null"] }, + "assetName": { "type": ["string", "null"] }, + "publishedAt": { "type": ["string", "null"], "format": "date-time" }, + "authoritativeConsumerPin": { "type": ["string", "null"] } + } + }, + "summary": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "codePathState", + "signingCapabilityState", + "signingAuthorityState", + "releaseConductorApplyState", + "publicationState", + "publishedBundleState", + "publishedBundleReleaseTag", + "publishedBundleAuthoritativeConsumerPin", + "externalBlocker", + "blockerCount" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "pass", + "warn" + ] + }, + "codePathState": { + "type": "string", + "enum": [ + "ready", + "missing-contract" + ] + }, + "signingCapabilityState": { + "type": "string", + "enum": [ + "configured", + "missing", + "unverifiable" + ] + }, + "signingAuthorityState": { + "type": "string", + "enum": [ + "ready", + "keys-missing", + "scope-missing", + "unverifiable" + ] + }, + "releaseConductorApplyState": { + "type": "string", + "enum": [ + "enabled", + "disabled", + "unverifiable" + ] + }, + "publicationState": { + "type": "string", + "enum": [ + "unobserved", + "not-attempted", + "tag-created-not-pushed", + "authoritative-publication-successful" + ] + }, + "publishedBundleState": { + "type": "string", + "enum": [ + "unobserved", + "release-unobserved", + "release-not-found", + "asset-missing", + "download-failed", + "extract-failed", + "metadata-missing", + "producer-native-incomplete", + "producer-native-ready" + ] + }, + "publishedBundleReleaseTag": { + "type": ["string", "null"] + }, + "publishedBundleAuthoritativeConsumerPin": { + "type": ["string", "null"] + }, + "externalBlocker": { + "type": [ + "string", + "null" + ], + "enum": [ + "workflow-signing-secret-missing", + "workflow-signing-secret-unverifiable", + "workflow-signing-admin-scope-missing", + "workflow-signing-key-missing", + "workflow-signing-authority-unverifiable", + "release-conductor-apply-disabled", + "release-conductor-apply-unverifiable", + null + ] + }, + "blockerCount": { "type": "integer", "minimum": 0 } + } + }, + "blockers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "workflow-signing-contract-missing", + "workflow-signing-secret-missing", + "workflow-signing-secret-unverifiable", + "workflow-signing-admin-scope-missing", + "workflow-signing-key-missing", + "workflow-signing-authority-unverifiable", + "release-conductor-apply-disabled", + "release-conductor-apply-unverifiable", + "published-bundle-release-unobserved", + "published-bundle-release-not-found", + "published-bundle-asset-missing", + "published-bundle-download-failed", + "published-bundle-extract-failed", + "published-bundle-metadata-missing", + "published-bundle-producer-native-incomplete" + ] + }, + "message": { "type": "string", "minLength": 1 } + } + } + } + } +} diff --git a/docs/schemas/sagan-context-concentrator-report-v1.schema.json b/docs/schemas/sagan-context-concentrator-report-v1.schema.json new file mode 100644 index 000000000..2ccc390dc --- /dev/null +++ b/docs/schemas/sagan-context-concentrator-report-v1.schema.json @@ -0,0 +1,311 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://labview-community-ci-cd.github.io/compare-vi-cli-action/schemas/sagan-context-concentrator-report-v1.schema.json", + "title": "Sagan Context Concentrator Report v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "generatedAt", + "repository", + "inputs", + "sources", + "focus", + "memory", + "episodes", + "cost", + "summary" + ], + "properties": { + "schema": { + "const": "priority/sagan-context-concentrator-report@v1" + }, + "generatedAt": { + "type": "string", + "format": "date-time" + }, + "repository": { + "type": ["string", "null"] + }, + "inputs": { + "type": "object", + "additionalProperties": false, + "required": [ + "priorityCachePath", + "governorSummaryPath", + "governorPortfolioSummaryPath", + "monitoringModePath", + "operatorSteeringEventPath", + "episodeDirectoryPath" + ], + "properties": { + "priorityCachePath": { "type": ["string", "null"] }, + "governorSummaryPath": { "type": ["string", "null"] }, + "governorPortfolioSummaryPath": { "type": ["string", "null"] }, + "monitoringModePath": { "type": ["string", "null"] }, + "operatorSteeringEventPath": { "type": ["string", "null"] }, + "episodeDirectoryPath": { "type": ["string", "null"] } + } + }, + "sources": { + "type": "object", + "additionalProperties": false, + "required": [ + "priorityCache", + "governorSummary", + "governorPortfolioSummary", + "monitoringMode", + "operatorSteeringEvent", + "episodeDirectory" + ], + "properties": { + "priorityCache": { "$ref": "#/$defs/sourcePathState" }, + "governorSummary": { "$ref": "#/$defs/sourcePathState" }, + "governorPortfolioSummary": { "$ref": "#/$defs/sourcePathState" }, + "monitoringMode": { "$ref": "#/$defs/sourcePathState" }, + "operatorSteeringEvent": { "$ref": "#/$defs/sourcePathState" }, + "episodeDirectory": { + "type": "object", + "additionalProperties": false, + "required": ["path", "exists", "fileCount", "validEpisodeCount", "invalidEpisodeCount"], + "properties": { + "path": { "type": ["string", "null"] }, + "exists": { "type": "boolean" }, + "fileCount": { "type": "integer", "minimum": 0 }, + "validEpisodeCount": { "type": "integer", "minimum": 0 }, + "invalidEpisodeCount": { "type": "integer", "minimum": 0 } + } + } + } + }, + "focus": { + "type": "object", + "additionalProperties": false, + "required": [ + "activeIssue", + "currentOwnerRepository", + "nextOwnerRepository", + "nextAction", + "governorMode", + "monitoringStatus" + ], + "properties": { + "activeIssue": { + "type": ["object", "null"], + "additionalProperties": false, + "required": ["number", "title", "url", "state", "repository"], + "properties": { + "number": { "type": "integer", "minimum": 1 }, + "title": { "type": ["string", "null"] }, + "url": { "type": ["string", "null"] }, + "state": { "type": ["string", "null"] }, + "repository": { "type": ["string", "null"] } + } + }, + "currentOwnerRepository": { "type": ["string", "null"] }, + "nextOwnerRepository": { "type": ["string", "null"] }, + "nextAction": { "type": ["string", "null"] }, + "governorMode": { "type": ["string", "null"] }, + "monitoringStatus": { "type": ["string", "null"] } + } + }, + "memory": { + "type": "object", + "additionalProperties": false, + "required": ["hotWorkingSet", "warmMemory", "archiveCount"], + "properties": { + "hotWorkingSet": { + "type": "array", + "items": { "$ref": "#/$defs/memoryItem" } + }, + "warmMemory": { + "type": "array", + "items": { "$ref": "#/$defs/memoryItem" } + }, + "archiveCount": { "type": "integer", "minimum": 0 } + } + }, + "episodes": { + "type": "object", + "additionalProperties": false, + "required": [ + "totalCount", + "validCount", + "invalidCount", + "invalidEpisodes", + "byStatus", + "byAgent", + "recent" + ], + "properties": { + "totalCount": { "type": "integer", "minimum": 0 }, + "validCount": { "type": "integer", "minimum": 0 }, + "invalidCount": { "type": "integer", "minimum": 0 }, + "invalidEpisodes": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["path", "error"], + "properties": { + "path": { "type": "string" }, + "error": { "type": "string" } + } + } + }, + "byStatus": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["status", "count"], + "properties": { + "status": { "type": "string" }, + "count": { "type": "integer", "minimum": 0 } + } + } + }, + "byAgent": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["agentId", "agentName", "count"], + "properties": { + "agentId": { "type": "string" }, + "agentName": { "type": ["string", "null"] }, + "count": { "type": "integer", "minimum": 0 } + } + } + }, + "recent": { + "type": "array", + "items": { "$ref": "#/$defs/episodeDigest" } + } + } + }, + "cost": { + "type": "object", + "additionalProperties": false, + "required": [ + "episodeCountWithCost", + "tokenUsd", + "operatorLaborUsd", + "blendedLowerBoundUsd", + "observedDurationSeconds" + ], + "properties": { + "episodeCountWithCost": { "type": "integer", "minimum": 0 }, + "tokenUsd": { "type": "number", "minimum": 0 }, + "operatorLaborUsd": { "type": "number", "minimum": 0 }, + "blendedLowerBoundUsd": { "type": "number", "minimum": 0 }, + "observedDurationSeconds": { "type": "number", "minimum": 0 } + } + }, + "summary": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "concentrationStatus", + "currentOwnerRepository", + "nextOwnerRepository", + "nextAction", + "activeIssueNumber", + "hotWorkingSetCount", + "warmMemoryCount", + "archiveCount", + "blockerCount", + "recentEpisodeCount", + "blendedLowerBoundUsd" + ], + "properties": { + "status": { "type": "string", "enum": ["active", "monitoring"] }, + "concentrationStatus": { "type": "string", "enum": ["pass", "warn", "incomplete"] }, + "currentOwnerRepository": { "type": ["string", "null"] }, + "nextOwnerRepository": { "type": ["string", "null"] }, + "nextAction": { "type": ["string", "null"] }, + "activeIssueNumber": { "type": ["integer", "null"], "minimum": 1 }, + "hotWorkingSetCount": { "type": "integer", "minimum": 0 }, + "warmMemoryCount": { "type": "integer", "minimum": 0 }, + "archiveCount": { "type": "integer", "minimum": 0 }, + "blockerCount": { "type": "integer", "minimum": 0 }, + "recentEpisodeCount": { "type": "integer", "minimum": 0 }, + "blendedLowerBoundUsd": { "type": "number", "minimum": 0 } + } + } + }, + "$defs": { + "sourcePathState": { + "type": "object", + "additionalProperties": false, + "required": ["path", "exists"], + "properties": { + "path": { "type": ["string", "null"] }, + "exists": { "type": "boolean" } + } + }, + "memoryItem": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "kind", + "label", + "status", + "detail", + "sourcePath", + "updatedAt", + "issueNumber", + "repository", + "agentName", + "nextAction" + ], + "properties": { + "id": { "type": ["string", "null"] }, + "kind": { "type": ["string", "null"] }, + "label": { "type": ["string", "null"] }, + "status": { "type": ["string", "null"] }, + "detail": { "type": ["string", "null"] }, + "sourcePath": { "type": ["string", "null"] }, + "updatedAt": { "type": ["string", "null"] }, + "issueNumber": { "type": ["integer", "null"], "minimum": 1 }, + "repository": { "type": ["string", "null"] }, + "agentName": { "type": ["string", "null"] }, + "nextAction": { "type": ["string", "null"] } + } + }, + "episodeDigest": { + "type": "object", + "additionalProperties": false, + "required": [ + "episodeId", + "generatedAt", + "agentId", + "agentName", + "agentRole", + "status", + "taskSummary", + "nextAction", + "blocker", + "executionPlane", + "dockerLaneId", + "sourcePath" + ], + "properties": { + "episodeId": { "type": ["string", "null"] }, + "generatedAt": { "type": ["string", "null"] }, + "agentId": { "type": ["string", "null"] }, + "agentName": { "type": ["string", "null"] }, + "agentRole": { "type": ["string", "null"] }, + "status": { "type": ["string", "null"] }, + "taskSummary": { "type": ["string", "null"] }, + "nextAction": { "type": ["string", "null"] }, + "blocker": { "type": ["string", "null"] }, + "executionPlane": { "type": ["string", "null"] }, + "dockerLaneId": { "type": ["string", "null"] }, + "sourcePath": { "type": ["string", "null"] } + } + } + } +} diff --git a/docs/schemas/subagent-episode-report-v1.schema.json b/docs/schemas/subagent-episode-report-v1.schema.json new file mode 100644 index 000000000..1636a936c --- /dev/null +++ b/docs/schemas/subagent-episode-report-v1.schema.json @@ -0,0 +1,121 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://labview-community-ci-cd.github.io/compare-vi-cli-action/schemas/subagent-episode-report-v1.schema.json", + "title": "Subagent Episode Report v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "generatedAt", + "repository", + "inputs", + "episodeId", + "agent", + "task", + "execution", + "summary", + "evidence", + "cost" + ], + "properties": { + "schema": { + "const": "priority/subagent-episode-report@v1" + }, + "generatedAt": { + "type": "string", + "format": "date-time" + }, + "repository": { + "type": ["string", "null"] + }, + "inputs": { + "type": "object", + "additionalProperties": false, + "required": ["sourcePath"], + "properties": { + "sourcePath": { + "type": ["string", "null"] + } + } + }, + "episodeId": { + "type": "string", + "minLength": 1 + }, + "agent": { + "type": "object", + "additionalProperties": false, + "required": ["id", "name", "role", "model"], + "properties": { + "id": { "type": ["string", "null"] }, + "name": { "type": ["string", "null"] }, + "role": { "type": ["string", "null"] }, + "model": { "type": ["string", "null"] } + } + }, + "task": { + "type": "object", + "additionalProperties": false, + "required": ["summary", "class", "issueNumber", "issueUrl"], + "properties": { + "summary": { "type": "string", "minLength": 1 }, + "class": { "type": ["string", "null"] }, + "issueNumber": { "type": ["integer", "null"], "minimum": 1 }, + "issueUrl": { "type": ["string", "null"] } + } + }, + "execution": { + "type": "object", + "additionalProperties": false, + "required": ["status", "lane", "branch", "executionPlane", "dockerLaneId", "hostCapabilityLeaseId"], + "properties": { + "status": { "type": "string", "minLength": 1 }, + "lane": { "type": ["string", "null"] }, + "branch": { "type": ["string", "null"] }, + "executionPlane": { "type": ["string", "null"] }, + "dockerLaneId": { "type": ["string", "null"] }, + "hostCapabilityLeaseId": { "type": ["string", "null"] } + } + }, + "summary": { + "type": "object", + "additionalProperties": false, + "required": ["status", "outcome", "blocker", "nextAction", "detail"], + "properties": { + "status": { "type": "string", "minLength": 1 }, + "outcome": { "type": ["string", "null"] }, + "blocker": { "type": ["string", "null"] }, + "nextAction": { "type": ["string", "null"] }, + "detail": { "type": ["string", "null"] } + } + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "required": ["filesTouched", "receipts", "commands", "notes"], + "properties": { + "filesTouched": { "$ref": "#/$defs/stringArray" }, + "receipts": { "$ref": "#/$defs/stringArray" }, + "commands": { "$ref": "#/$defs/stringArray" }, + "notes": { "$ref": "#/$defs/stringArray" } + } + }, + "cost": { + "type": "object", + "additionalProperties": false, + "required": ["observedDurationSeconds", "tokenUsd", "operatorLaborUsd", "blendedLowerBoundUsd"], + "properties": { + "observedDurationSeconds": { "type": ["number", "null"] }, + "tokenUsd": { "type": ["number", "null"] }, + "operatorLaborUsd": { "type": ["number", "null"] }, + "blendedLowerBoundUsd": { "type": ["number", "null"] } + } + } + }, + "$defs": { + "stringArray": { + "type": "array", + "items": { "type": "string" } + } + } +} diff --git a/fixtures/teststand-session/session-index.dual-plane-parity.json b/fixtures/teststand-session/session-index.dual-plane-parity.json new file mode 100644 index 000000000..3370a0a02 --- /dev/null +++ b/fixtures/teststand-session/session-index.dual-plane-parity.json @@ -0,0 +1,202 @@ +{ + "schema": "teststand-compare-session/v2", + "at": "2026-03-24T00:30:00.000Z", + "suiteClass": "dual-plane-parity", + "primaryPlane": "native-labview-2026-64", + "requestedSimultaneous": true, + "warmup": { + "mode": "skip", + "events": null + }, + "compare": { + "events": "planes/x64/compare/compare-events.ndjson", + "capture": "planes/x64/compare/lvcompare-capture.json", + "report": true, + "command": "LabVIEWCLI64 CreateComparisonReport", + "cliPath": "C:/Program Files (x86)/National Instruments/Shared/LabVIEW CLI/LabVIEWCLI.exe", + "policy": "cli-only", + "mode": "labview-cli", + "autoCli": false, + "sameName": false, + "timeoutSeconds": 600 + }, + "outcome": { + "exitCode": 0, + "seconds": 9.2, + "command": "LabVIEWCLI64 CreateComparisonReport", + "diff": false + }, + "error": null, + "executionCell": { + "cellId": "exec-cell-sagan-01", + "leaseId": "lease-sagan-01", + "leasePath": "tests/results/_agent/runtime/execution-cell-sagan-01.json", + "agentId": "sagan", + "agentClass": "sagan", + "cellClass": "kernel-coordinator", + "suiteClass": "dual-plane-parity", + "planeBinding": "dual-plane-parity", + "runtimeSurface": "windows-native-teststand", + "premiumSaganMode": false, + "operatorAuthorizationRef": null, + "workingRoot": "tests/results/teststand-session", + "artifactRoot": "tests/results/teststand-session", + "isolatedLaneGroupId": "host-os-fingerprint:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210", + "hostOsFingerprintSha256": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" + }, + "harnessInstance": { + "harnessKind": "teststand-compare-harness", + "instanceId": "ts-harness-sagan-01", + "role": "coordinator", + "processModelClass": "parallel-process-model", + "planeBinding": "dual-plane-parity", + "parentInstanceId": null + }, + "processModel": { + "runtimeSurface": "windows-native-teststand", + "processModelClass": "parallel-process-model", + "windowsOnly": true, + "rootHarnessInstanceId": "ts-harness-sagan-01", + "planeCount": 2 + }, + "planes": { + "x64": { + "plane": "native-labview-2026-64", + "architecture": "64-bit", + "labviewExePath": "C:/Program Files/National Instruments/LabVIEW 2026/LabVIEW.exe", + "outputRoot": "planes/x64", + "warmup": { + "mode": "skip", + "events": null + }, + "compare": { + "events": "planes/x64/compare/compare-events.ndjson", + "capture": "planes/x64/compare/lvcompare-capture.json", + "report": true, + "command": "LabVIEWCLI64 CreateComparisonReport", + "cliPath": "C:/Program Files (x86)/National Instruments/Shared/LabVIEW CLI/LabVIEWCLI.exe", + "policy": "cli-only", + "mode": "labview-cli", + "autoCli": false, + "sameName": false, + "timeoutSeconds": 600 + }, + "outcome": { + "exitCode": 0, + "seconds": 9.2, + "command": "LabVIEWCLI64 CreateComparisonReport", + "diff": false + }, + "error": null, + "exitCode": 0, + "executionCell": { + "cellId": "exec-cell-sagan-01", + "leaseId": "lease-sagan-01", + "leasePath": "tests/results/_agent/runtime/execution-cell-sagan-01.json", + "agentId": "sagan", + "agentClass": "sagan", + "cellClass": "kernel-coordinator", + "suiteClass": "dual-plane-parity", + "planeBinding": "native-labview-2026-64", + "runtimeSurface": "windows-native-teststand", + "premiumSaganMode": false, + "operatorAuthorizationRef": null, + "workingRoot": "tests/results/teststand-session/planes/x64", + "artifactRoot": "tests/results/teststand-session/planes/x64", + "isolatedLaneGroupId": "host-os-fingerprint:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210", + "hostOsFingerprintSha256": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" + }, + "harnessInstance": { + "harnessKind": "teststand-compare-harness", + "instanceId": "ts-harness-sagan-01-x64", + "role": "plane-child", + "processModelClass": "parallel-process-model", + "planeBinding": "native-labview-2026-64", + "parentInstanceId": "ts-harness-sagan-01" + }, + "processModel": { + "runtimeSurface": "windows-native-teststand", + "processModelClass": "parallel-process-model", + "windowsOnly": true, + "rootHarnessInstanceId": "ts-harness-sagan-01", + "planeCount": 2 + } + }, + "x32": { + "plane": "native-labview-2026-32", + "architecture": "32-bit", + "labviewExePath": "C:/Program Files (x86)/National Instruments/LabVIEW 2026/LabVIEW.exe", + "outputRoot": "planes/x32", + "warmup": { + "mode": "skip", + "events": null + }, + "compare": { + "events": "planes/x32/compare/compare-events.ndjson", + "capture": "planes/x32/compare/lvcompare-capture.json", + "report": true, + "command": "LabVIEWCLI32 CreateComparisonReport", + "cliPath": "C:/Program Files (x86)/National Instruments/Shared/LabVIEW CLI/LabVIEWCLI.exe", + "policy": "cli-only", + "mode": "labview-cli", + "autoCli": false, + "sameName": false, + "timeoutSeconds": 600 + }, + "outcome": { + "exitCode": 0, + "seconds": 9.8, + "command": "LabVIEWCLI32 CreateComparisonReport", + "diff": false + }, + "error": null, + "exitCode": 0, + "executionCell": { + "cellId": "exec-cell-sagan-01", + "leaseId": "lease-sagan-01", + "leasePath": "tests/results/_agent/runtime/execution-cell-sagan-01.json", + "agentId": "sagan", + "agentClass": "sagan", + "cellClass": "kernel-coordinator", + "suiteClass": "dual-plane-parity", + "planeBinding": "native-labview-2026-32", + "runtimeSurface": "windows-native-teststand", + "premiumSaganMode": false, + "operatorAuthorizationRef": null, + "workingRoot": "tests/results/teststand-session/planes/x32", + "artifactRoot": "tests/results/teststand-session/planes/x32", + "isolatedLaneGroupId": "host-os-fingerprint:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210", + "hostOsFingerprintSha256": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" + }, + "harnessInstance": { + "harnessKind": "teststand-compare-harness", + "instanceId": "ts-harness-sagan-01-x32", + "role": "plane-child", + "processModelClass": "parallel-process-model", + "planeBinding": "native-labview-2026-32", + "parentInstanceId": "ts-harness-sagan-01" + }, + "processModel": { + "runtimeSurface": "windows-native-teststand", + "processModelClass": "parallel-process-model", + "windowsOnly": true, + "rootHarnessInstanceId": "ts-harness-sagan-01", + "planeCount": 2 + } + } + }, + "parity": { + "status": "match", + "comparedFields": [ + "outcome.exitCode", + "outcome.diff", + "compare.report", + "compare.mode", + "compare.policy" + ], + "exitCodeParity": true, + "diffParity": true, + "mismatchCount": 0, + "mismatches": [] + } +} diff --git a/fixtures/teststand-session/session-index.json b/fixtures/teststand-session/session-index.json index 26eae5343..48a13acc4 100644 --- a/fixtures/teststand-session/session-index.json +++ b/fixtures/teststand-session/session-index.json @@ -54,5 +54,37 @@ "command": "lvcompare --diff --nobdcosm --nofppos --noattr --out compare-report.html VI1.vi VI2.vi", "diff": false }, - "error": null + "error": null, + "executionCell": { + "cellId": "exec-cell-hooke-01", + "leaseId": "lease-hooke-01", + "leasePath": "tests/results/_agent/runtime/execution-cell-hooke-01.json", + "agentId": "hooke", + "agentClass": "subagent", + "cellClass": "worker", + "suiteClass": "single-compare", + "planeBinding": "native-labview-2025-64", + "runtimeSurface": "windows-native-teststand", + "premiumSaganMode": false, + "operatorAuthorizationRef": null, + "workingRoot": "tests/results/teststand-session", + "artifactRoot": "tests/results/teststand-session", + "isolatedLaneGroupId": "host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "hostOsFingerprintSha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + }, + "harnessInstance": { + "harnessKind": "teststand-compare-harness", + "instanceId": "ts-harness-hooke-01", + "role": "single-plane", + "processModelClass": "sequential-process-model", + "planeBinding": "native-labview-2025-64", + "parentInstanceId": null + }, + "processModel": { + "runtimeSurface": "windows-native-teststand", + "processModelClass": "sequential-process-model", + "windowsOnly": true, + "rootHarnessInstanceId": "ts-harness-hooke-01", + "planeCount": 1 + } } diff --git a/package-lock.json b/package-lock.json index bd19b367e..1fa871b76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "compare-vi-cli-action", - "version": "0.6.3", + "version": "0.6.4-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "compare-vi-cli-action", - "version": "0.6.3", + "version": "0.6.4-rc.1", "license": "MIT", "dependencies": { "argparse": "^2.0.1", diff --git a/package.json b/package.json index cb1e0dcee..a87f9e0e7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "compare-vi-cli-action", "private": true, - "version": "0.6.4-rc.1", + "version": "0.6.4-rc.2", "license": "MIT", "type": "module", "scripts": { @@ -87,6 +87,8 @@ "priority:monitoring:mode": "node tools/priority/handoff-monitoring-mode.mjs", "priority:governor:summary": "node tools/priority/autonomous-governor-summary.mjs", "priority:governor:portfolio": "node tools/priority/autonomous-governor-portfolio-summary.mjs", + "priority:subagent:episode": "node tools/priority/subagent-episode.mjs", + "priority:context:concentrate": "node tools/priority/sagan-context-concentrator.mjs", "priority:monitoring:inject-work": "node tools/priority/monitoring-work-injection.mjs", "priority:wake:adjudicate": "node tools/priority/wake-adjudication.mjs", "priority:wake:accounting": "node tools/priority/wake-investment-accounting.mjs", @@ -97,6 +99,9 @@ "priority:lease": "node tools/priority/agent-writer-lease.mjs", "priority:continuity": "node tools/priority/continuity-telemetry.mjs", "priority:lane:marketplace": "node tools/priority/lane-marketplace.mjs", + "priority:lane:docker:handshake": "node tools/priority/docker-lane-handshake.mjs", + "priority:lane:execution-cell": "node tools/priority/execution-cell-lease.mjs", + "priority:lane:execution-cell:bundle": "node tools/priority/execution-cell-bundle.mjs", "priority:lane:concurrency:plan": "node tools/priority/concurrent-lane-plan.mjs", "priority:lane:concurrency:apply": "node tools/priority/concurrent-lane-apply.mjs", "priority:lane:concurrency:status": "node tools/priority/concurrent-lane-status.mjs", @@ -128,6 +133,8 @@ "priority:throughput:scorecard": "node tools/priority/throughput-scorecard.mjs", "priority:release:cadence": "node tools/priority/release-cadence-check.mjs", "priority:release:conductor": "node tools/priority/release-conductor.mjs", + "priority:release:signing:readiness": "node tools/priority/release-signing-readiness.mjs", + "priority:release:published:bundle": "node tools/priority/release-published-bundle-observer.mjs", "priority:remediation:slo": "node tools/priority/remediation-slo-evaluator.mjs", "priority:weekly:scorecard": "node tools/priority/weekly-scorecard.mjs", "priority:security:audit": "node tools/priority/dependency-audit.mjs", @@ -139,6 +146,7 @@ "priority:cost:turn": "node tools/priority/agent-cost-turn.mjs", "priority:cost:rollup": "node tools/priority/agent-cost-rollup.mjs", "priority:cost:rollup:materialize": "node tools/priority/materialize-agent-cost-rollup.mjs", + "priority:cost:comment-hook": "node tools/priority/github-comment-budget-hook.mjs", "priority:cost:pr-spend": "node tools/priority/pr-spend-projection.mjs", "priority:model:select": "node tools/priority/live-agent-model-selection.mjs", "priority:onboard:downstream": "node tools/priority/downstream-onboarding.mjs", @@ -236,7 +244,7 @@ "session-index:test": "node tools/npm/run-script.mjs build && node --test src/session-index/__tests__/*.test.mjs", "session-index:v2-contract": "pwsh -NoLogo -NoProfile -File tools/Test-SessionIndexV2Contract.ps1", "session-index:validate": "node tools/npm/run-script.mjs schema:validate -- --schema docs/schema/generated/session-index-v2.schema.json --data tests/results/_agent/session-index-v2-sample.json --optional", - "session:teststand:validate": "node tools/npm/run-script.mjs schema:validate -- --schema docs/schema/generated/teststand-compare-session.schema.json --data fixtures/teststand-session/session-index.json --data tests/results/teststand-session/session-index.json --optional", + "session:teststand:validate": "node tools/npm/run-script.mjs schema:validate -- --schema docs/schema/generated/teststand-compare-session.schema.json --data fixtures/teststand-session/session-index.json --data fixtures/teststand-session/session-index.dual-plane-parity.json --data tests/results/teststand-session/session-index.json --optional", "smoke:vi-history": "pwsh -NoLogo -NoProfile -File tools/Test-PRVIHistorySmoke.ps1", "smoke:vi-stage": "pwsh -NoLogo -NoProfile -File tools/Test-PRVIStagingSmoke.ps1", "tests:comparevi": "pwsh -NoLogo -NoProfile -File Invoke-PesterTests.ps1 -IncludePatterns CompareVI*", diff --git a/scripts/Run-AutonomousIntegrationLoop.ps1 b/scripts/Run-AutonomousIntegrationLoop.ps1 index f0638bf7e..2dd5292d9 100644 --- a/scripts/Run-AutonomousIntegrationLoop.ps1 +++ b/scripts/Run-AutonomousIntegrationLoop.ps1 @@ -138,13 +138,22 @@ param( , [string]$TestStandHarnessPath = $env:LOOP_TESTSTAND_HARNESS_PATH , [string]$TestStandOutputRoot = $env:LOOP_TESTSTAND_OUTPUT_ROOT , [ValidateSet('detect','spawn','skip')][string]$TestStandWarmup = $( if ($env:LOOP_TESTSTAND_WARMUP) { $env:LOOP_TESTSTAND_WARMUP } else { 'skip' } ) + , [ValidateSet('single-compare','dual-plane-parity')][string]$TestStandSuiteClass = $( if ($env:LOOP_TESTSTAND_SUITE_CLASS) { $env:LOOP_TESTSTAND_SUITE_CLASS } else { 'single-compare' } ) , [switch]$TestStandRenderReport , [switch]$TestStandCloseLabVIEW , [switch]$TestStandCloseLVCompare , [int]$TestStandTimeoutSeconds = ($env:LOOP_TESTSTAND_TIMEOUT_SECONDS -as [int]) , [switch]$TestStandDisableTimeout , [string]$TestStandLabVIEWPath = $env:LOOP_TESTSTAND_LABVIEW_PATH +, [string]$TestStandLabVIEW64Path = $env:LOOP_TESTSTAND_LABVIEW64_PATH +, [string]$TestStandLabVIEW32Path = $env:LOOP_TESTSTAND_LABVIEW32_PATH , [string]$TestStandLVComparePath = $env:LOOP_TESTSTAND_LVCOMPARE_PATH +, [string]$TestStandAgentId = $env:LOOP_TESTSTAND_AGENT_ID +, [string]$TestStandAgentClass = $env:LOOP_TESTSTAND_AGENT_CLASS +, [string]$TestStandExecutionCellLeasePath = $env:LOOP_TESTSTAND_EXECUTION_CELL_LEASE_PATH +, [string]$TestStandExecutionCellId = $env:LOOP_TESTSTAND_EXECUTION_CELL_ID +, [string]$TestStandExecutionCellLeaseId = $env:LOOP_TESTSTAND_EXECUTION_CELL_LEASE_ID +, [string]$TestStandHarnessInstanceId = $env:LOOP_TESTSTAND_HARNESS_INSTANCE_ID , [switch]$TestStandReplaceFlags ) @@ -154,6 +163,62 @@ function Set-LoopExit { exit $Code } +function Get-ExecutionCellLeaseMetadata { + param([string]$LeasePath) + + $metadata = [ordered]@{ + cellClass = $null + suiteClass = $null + operatorAuthorizationRef = $null + premiumSaganMode = $false + } + + if ([string]::IsNullOrWhiteSpace($LeasePath)) { + return [pscustomobject]$metadata + } + + try { + $resolvedLeasePath = (Resolve-Path -LiteralPath $LeasePath -ErrorAction Stop).Path + $payload = Get-Content -LiteralPath $resolvedLeasePath -Raw | ConvertFrom-Json -ErrorAction Stop + $summary = if ($payload -and $payload.PSObject.Properties.Name -contains 'summary') { $payload.summary } else { $null } + $lease = if ($payload -and $payload.PSObject.Properties.Name -contains 'lease') { $payload.lease } else { $null } + $request = if ($lease -and $lease.PSObject.Properties.Name -contains 'request') { $lease.request } else { $null } + $grant = if ($lease -and $lease.PSObject.Properties.Name -contains 'grant') { $lease.grant } else { $null } + + $summaryCellClass = if ($summary) { $summary.cellClass } else { $null } + $requestCellClass = if ($request) { $request.cellClass } else { $null } + foreach ($candidate in @($summaryCellClass, $requestCellClass)) { + if (-not [string]::IsNullOrWhiteSpace($candidate)) { + $metadata.cellClass = [string]$candidate + break + } + } + $summarySuiteClass = if ($summary) { $summary.suiteClass } else { $null } + $requestSuiteClass = if ($request) { $request.suiteClass } else { $null } + foreach ($candidate in @($summarySuiteClass, $requestSuiteClass)) { + if (-not [string]::IsNullOrWhiteSpace($candidate)) { + $metadata.suiteClass = [string]$candidate + break + } + } + $summaryOperatorAuthorizationRef = if ($summary) { $summary.operatorAuthorizationRef } else { $null } + $requestOperatorAuthorizationRef = if ($request) { $request.operatorAuthorizationRef } else { $null } + foreach ($candidate in @($summaryOperatorAuthorizationRef, $requestOperatorAuthorizationRef)) { + if (-not [string]::IsNullOrWhiteSpace($candidate)) { + $metadata.operatorAuthorizationRef = [string]$candidate + break + } + } + if ($summary -and $summary.PSObject.Properties.Name -contains 'premiumSaganMode') { + $metadata.premiumSaganMode = [bool]$summary.premiumSaganMode + } elseif ($grant -and $grant.PSObject.Properties.Name -contains 'premiumSaganMode') { + $metadata.premiumSaganMode = [bool]$grant.premiumSaganMode + } + } catch {} + + return [pscustomobject]$metadata +} + # Defaults / fallbacks if (-not $MaxIterations) { $MaxIterations = 1 } if ($null -eq $IntervalSeconds) { $IntervalSeconds = 0 } @@ -277,8 +342,17 @@ if ($UseTestStandHarness) { $disableTimeout = [bool]$TestStandDisableTimeout $timeoutValue = if ($PSBoundParameters.ContainsKey('TestStandTimeoutSeconds') -or $env:LOOP_TESTSTAND_TIMEOUT_SECONDS) { $TestStandTimeoutSeconds } else { $null } $labviewPath = $TestStandLabVIEWPath + $labview64Path = $TestStandLabVIEW64Path + $labview32Path = $TestStandLabVIEW32Path $lvcomparePath = $TestStandLVComparePath $replaceFlags = [bool]$TestStandReplaceFlags + $executionCellLeasePath = $TestStandExecutionCellLeasePath + $executionCellId = $TestStandExecutionCellId + $executionCellLeaseId = $TestStandExecutionCellLeaseId + $executionCellLeaseMetadata = Get-ExecutionCellLeaseMetadata -LeasePath $executionCellLeasePath + $agentId = $TestStandAgentId + $agentClass = $TestStandAgentClass + $harnessInstanceId = $TestStandHarnessInstanceId $harnessIteration = [ref]0 $executor = { @@ -298,7 +372,16 @@ if ($UseTestStandHarness) { Warmup = $warmupMode } if ($labviewPath) { $harnessParams.LabVIEWExePath = $labviewPath } + if ($labview64Path) { $harnessParams.LabVIEW64ExePath = $labview64Path } + if ($labview32Path) { $harnessParams.LabVIEW32ExePath = $labview32Path } if ($lvcomparePath) { $harnessParams.LVComparePath = $lvcomparePath } + if ($TestStandSuiteClass -ne 'single-compare') { $harnessParams.SuiteClass = $TestStandSuiteClass } + if ($agentId) { $harnessParams.AgentId = $agentId } + if ($agentClass) { $harnessParams.AgentClass = $agentClass } + if ($executionCellLeasePath) { $harnessParams.ExecutionCellLeasePath = $executionCellLeasePath } + if ($executionCellId) { $harnessParams.ExecutionCellId = $executionCellId } + if ($executionCellLeaseId) { $harnessParams.ExecutionCellLeaseId = $executionCellLeaseId } + if ($harnessInstanceId) { $harnessParams.HarnessInstanceId = $harnessInstanceId } if ($renderReport) { $harnessParams.RenderReport = $true } if ($closeLabVIEW) { $harnessParams.CloseLabVIEW = $true } if ($closeLVCompare) { $harnessParams.CloseLVCompare = $true } @@ -344,11 +427,28 @@ if ($UseTestStandHarness) { path = $resolvedHarness output = $resolvedOutputRoot warmup = $warmupMode + suiteClass = $TestStandSuiteClass + runtimeSurface = 'windows-native-teststand' + processModelClass = if ($TestStandSuiteClass -eq 'dual-plane-parity') { 'parallel-process-model' } else { 'sequential-process-model' } + windowsOnly = $true + requestedSimultaneous = ($TestStandSuiteClass -eq 'dual-plane-parity') renderReport = $renderReport closeLabVIEW = $closeLabVIEW closeLVCompare = $closeLVCompare disableTimeout = $disableTimeout timeout = $timeoutValue + labviewPath = $labviewPath + labview64Path = $labview64Path + labview32Path = $labview32Path + agentId = $agentId + agentClass = $agentClass + cellClass = $executionCellLeaseMetadata.cellClass + operatorAuthorizationRef = $executionCellLeaseMetadata.operatorAuthorizationRef + premiumSaganMode = [bool]$executionCellLeaseMetadata.premiumSaganMode + executionCellLeasePath = $executionCellLeasePath + executionCellId = $executionCellId + executionCellLeaseId = $executionCellLeaseId + harnessInstanceId = $harnessInstanceId } } @@ -453,6 +553,8 @@ function Invoke-LabVIEWCloser { } } +$shouldCloseLabVIEW = (-not $UseTestStandHarness) -and (-not $executor) + function Ensure-JsonLog { param([string]$Path) if (-not $Path) { return } @@ -517,6 +619,9 @@ if ($UseTestStandHarness -and $harnessPlan) { $planPayload.harnessOutput = $harnessPlan.output $planPayload.harnessWarmup = $harnessPlan.warmup $planPayload.harnessRenderReport = $harnessPlan.renderReport + $planPayload.harnessRuntimeSurface = $harnessPlan.runtimeSurface + $planPayload.harnessProcessModelClass = $harnessPlan.processModelClass + $planPayload.harnessRequestedSimultaneous = $harnessPlan.requestedSimultaneous } Write-JsonEvent 'plan' $planPayload @@ -533,6 +638,8 @@ if ($DryRun) { if ($UseTestStandHarness -and $harnessPlan) { $dryRunPayload.harnessPath = $harnessPlan.path $dryRunPayload.harnessOutput = $harnessPlan.output + $dryRunPayload.harnessRuntimeSurface = $harnessPlan.runtimeSurface + $dryRunPayload.harnessProcessModelClass = $harnessPlan.processModelClass } Write-JsonEvent 'dryRun' $dryRunPayload Set-LoopExit 0 @@ -541,7 +648,9 @@ if ($DryRun) { try { $result = Invoke-IntegrationCompareLoop @invokeParams } catch { - Invoke-LabVIEWCloser -Context 'invoke-exception' + if ($shouldCloseLabVIEW) { + Invoke-LabVIEWCloser -Context 'invoke-exception' + } throw } Write-JsonEvent 'result' (@{ iterations=$result.Iterations; diffs=$result.DiffCount; errors=$result.ErrorCount; succeeded=$result.Succeeded }) @@ -564,6 +673,24 @@ if ($FinalStatusJsonPath) { basePath = $result.BasePath headPath = $result.HeadPath } + if ($UseTestStandHarness -and $harnessPlan) { + $obj.harness = [ordered]@{ + path = $harnessPlan.path + output = $harnessPlan.output + suiteClass = $harnessPlan.suiteClass + runtimeSurface = $harnessPlan.runtimeSurface + processModelClass = $harnessPlan.processModelClass + windowsOnly = $harnessPlan.windowsOnly + requestedSimultaneous = $harnessPlan.requestedSimultaneous + cellClass = $harnessPlan.cellClass + operatorAuthorizationRef = $harnessPlan.operatorAuthorizationRef + premiumSaganMode = $harnessPlan.premiumSaganMode + executionCellLeasePath = $harnessPlan.executionCellLeasePath + executionCellId = $harnessPlan.executionCellId + executionCellLeaseId = $harnessPlan.executionCellLeaseId + harnessInstanceId = $harnessPlan.harnessInstanceId + } + } $json = $obj | ConvertTo-Json -Depth 5 $finalDir = Split-Path -Parent $FinalStatusJsonPath if ($finalDir -and -not (Test-Path $finalDir)) { New-Item -ItemType Directory -Path $finalDir | Out-Null } @@ -595,7 +722,9 @@ if (-not $NoStepSummary -and $env:GITHUB_STEP_SUMMARY -and $result.DiffSummary) Write-Detail 'Step summary append skipped (suppressed or not in Actions).' 'Debug' } -Invoke-LabVIEWCloser -Context 'post-loop' +if ($shouldCloseLabVIEW) { + Invoke-LabVIEWCloser -Context 'post-loop' +} # Exit code semantics: 0 when succeeded (even if diffs unless FailOnDiff terminated early), 1 if any errors encountered if (-not $result.Succeeded) { Set-LoopExit 1 } diff --git a/tests/AgentHandoff.Local.Tests.ps1 b/tests/AgentHandoff.Local.Tests.ps1 index ddeb5a778..a00415fa3 100644 --- a/tests/AgentHandoff.Local.Tests.ps1 +++ b/tests/AgentHandoff.Local.Tests.ps1 @@ -29,4 +29,23 @@ Describe 'Local Agent Handoff' -Tag 'Unit' { $planeTransition.schema | Should -Be 'agent-handoff/plane-transition-v1' $planeTransition.status | Should -Not -BeNullOrEmpty } + + It 'declares governor execution process-model fields in the handoff printer' { + $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path + $script = Join-Path $repoRoot 'tools' 'Print-AgentHandoff.ps1' + Test-Path -LiteralPath $script | Should -BeTrue + $content = Get-Content -LiteralPath $script -Raw + $content | Should -Match 'execSurf\s*:' + $content | Should -Match 'executionTopologyRuntimeSurface' + $content | Should -Match 'execProc\s*:' + $content | Should -Match 'executionTopologyProcessModelClass' + $content | Should -Match 'execSim\s*:' + $content | Should -Match 'executionTopologyRequestedSimultaneous' + $content | Should -Match 'execCell\s*:' + $content | Should -Match 'executionTopologyCellClass' + $content | Should -Match 'execSuite\s*:' + $content | Should -Match 'executionTopologySuiteClass' + $content | Should -Match 'execAuth\s*:' + $content | Should -Match 'executionTopologyOperatorAuthorizationRef' + } } diff --git a/tests/CompareVITools.Artifact.Tests.ps1 b/tests/CompareVITools.Artifact.Tests.ps1 index b1f7384ab..e84f84cc4 100644 --- a/tests/CompareVITools.Artifact.Tests.ps1 +++ b/tests/CompareVITools.Artifact.Tests.ps1 @@ -113,6 +113,18 @@ Describe 'CompareVI.Tools artifact publishing' -Tag 'REQ:DOTNET_CLI_RELEASE_ASSE $metadata.consumerContract.capabilities.viHistory.contractPaths.hostedNiLinuxRunner | Should -Be 'consumerContract.hostedNiLinuxRunner' ((@($metadata.consumerContract.capabilities.viHistory.notes) -join [Environment]::NewLine)) | Should -Match 'LabviewGitHubCiTemplate' ((@($metadata.consumerContract.capabilities.viHistory.notes) -join [Environment]::NewLine)) | Should -Match 'authoritativeConsumerPin' + $metadata.consumerContract.capabilities.dockerProfile.schema | Should -Be 'comparevi-tools/docker-profile-capability@v1' + $metadata.consumerContract.capabilities.dockerProfile.capabilityId | Should -Be 'docker-profile' + $metadata.consumerContract.capabilities.dockerProfile.displayName | Should -Be 'Docker Profile' + $metadata.consumerContract.capabilities.dockerProfile.distributionRole | Should -Be 'upstream-producer' + $metadata.consumerContract.capabilities.dockerProfile.distributionModel | Should -Be 'release-bundle' + $metadata.consumerContract.capabilities.dockerProfile.bundleMetadataPath | Should -Be 'comparevi-tools-release.json' + $metadata.consumerContract.capabilities.dockerProfile.bundleImportPath | Should -Be 'tools/CompareVI.Tools/CompareVI.Tools.psd1' + $metadata.consumerContract.capabilities.dockerProfile.releaseAssetPattern | Should -Be 'CompareVI.Tools-v.zip' + $metadata.consumerContract.capabilities.dockerProfile.authoritativeConsumerPinFieldPath | Should -Be 'versionContract.authoritativeConsumerPin' + $metadata.consumerContract.capabilities.dockerProfile.authoritativeConsumerPinKindFieldPath | Should -Be 'versionContract.authoritativeConsumerPinKind' + $metadata.consumerContract.capabilities.dockerProfile.authoritativeImageContractSource | Should -Be 'consumerContract.dockerImageContract' + ((@($metadata.consumerContract.capabilities.dockerProfile.notes) -join [Environment]::NewLine)) | Should -Match 'Producer-published Docker image contract' $metadata.consumerContract.historyFacade.schema | Should -Be 'comparevi-tools/history-facade@v1' $metadata.consumerContract.historyFacade.exportedFunction | Should -Be 'Invoke-CompareVIHistoryFacade' $metadata.consumerContract.historyFacade.resultsRelativePath | Should -Be 'history-summary.json' @@ -163,6 +175,11 @@ Describe 'CompareVI.Tools artifact publishing' -Tag 'REQ:DOTNET_CLI_RELEASE_ASSE ) $metadata.consumerContract.hostedNiLinuxRunner.captureFileName | Should -Be 'ni-linux-container-capture.json' $metadata.consumerContract.hostedNiLinuxRunner.defaultImage | Should -Be 'nationalinstruments/labview:2026q1-linux' + $metadata.consumerContract.dockerImageContract.schema | Should -Be 'comparevi-tools/docker-image-contract@v1' + $metadata.consumerContract.dockerImageContract.schemaUrl | Should -Be 'https://labview-community-ci-cd.github.io/compare-vi-cli-action/schemas/comparevi-tools-docker-image-contract-v1.schema.json' + $metadata.consumerContract.dockerImageContract.images.hostedNiLinuxRunner.imageRef | Should -Be 'nationalinstruments/labview:2026q1-linux' + $metadata.consumerContract.dockerImageContract.images.hostedNiLinuxRunner.consumerRole | Should -Be 'hosted-ni-linux-runner' + ((@($metadata.consumerContract.dockerImageContract.notes) -join [Environment]::NewLine)) | Should -Match 'authoritative Producer-published image contract' $archivePath = Join-Path $outDir $metadata.bundle.archiveName Test-Path -LiteralPath $archivePath | Should -BeTrue @@ -219,6 +236,9 @@ Describe 'CompareVI.Tools artifact publishing' -Tag 'REQ:DOTNET_CLI_RELEASE_ASSE $archiveMetadata.bundle.files.Count | Should -BeGreaterThan 5 $archiveMetadata.consumerContract.capabilities.viHistory.schema | Should -Be 'comparevi-tools/vi-history-capability@v1' $archiveMetadata.consumerContract.capabilities.viHistory.contractPaths.historyFacade | Should -Be 'consumerContract.historyFacade' + $archiveMetadata.consumerContract.capabilities.dockerProfile.schema | Should -Be 'comparevi-tools/docker-profile-capability@v1' + $archiveMetadata.consumerContract.capabilities.dockerProfile.authoritativeImageContractSource | Should -Be 'consumerContract.dockerImageContract' + $archiveMetadata.consumerContract.dockerImageContract.images.hostedNiLinuxRunner.imageRef | Should -Be 'nationalinstruments/labview:2026q1-linux' @($archiveMetadata.bundle.files.path) | Should -Contain 'tools/Build-VIHistoryDevImage.ps1' @($archiveMetadata.bundle.files.path) | Should -Contain 'tools/Invoke-VIHistoryLocalOperatorSession.ps1' @($archiveMetadata.bundle.files.path) | Should -Contain 'tools/Invoke-VIHistoryLocalRefinement.ps1' diff --git a/tests/Import-HandoffState.Tests.ps1 b/tests/Import-HandoffState.Tests.ps1 index 53ee9ba56..70a501d15 100644 --- a/tests/Import-HandoffState.Tests.ps1 +++ b/tests/Import-HandoffState.Tests.ps1 @@ -204,11 +204,63 @@ Describe 'Import-HandoffState' -Tag 'Unit' { monitoringModePath = 'tests/results/_agent/handoff/monitoring-mode.json' wakeLifecyclePath = 'tests/results/_agent/issue/wake-lifecycle.json' wakeInvestmentAccountingPath = 'tests/results/_agent/capital/wake-investment-accounting.json' + deliveryRuntimeStatePath = 'tests/results/_agent/runtime/delivery-agent-state.json' + releaseSigningReadinessPath = 'tests/results/_agent/release/release-signing-readiness.json' } compare = [ordered]@{ queueState = [ordered]@{ status = 'queue-empty'; reason = 'queue-empty'; openIssueCount = 11; ready = $true } continuity = [ordered]@{ status = 'maintained'; turnBoundary = 'safe-idle'; supervisionState = 'safe-idle'; operatorPromptRequiredToResume = $false } monitoringMode = [ordered]@{ status = 'active'; futureAgentAction = 'future-agent-may-pivot'; wakeConditionCount = 0 } + releaseSigningReadiness = [ordered]@{ + status = 'warn' + codePathState = 'ready' + signingCapabilityState = 'missing' + publicationState = 'tag-created-not-pushed' + publishedBundleState = 'producer-native-incomplete' + publishedBundleReleaseTag = 'v0.6.3-tools.14' + publishedBundleAuthoritativeConsumerPin = $null + externalBlocker = 'workflow-signing-secret-missing' + blockerCount = 1 + } + deliveryRuntime = [ordered]@{ + status = 'none' + runtimeStatus = $null + laneLifecycle = $null + actionType = $null + outcome = $null + blockerClass = $null + nextWakeCondition = $null + queueAuthorityRefresh = [ordered]@{ + attempted = $false + status = $null + reason = $null + summaryPath = $null + mergeSummaryPath = $null + receiptGeneratedAt = $null + receiptStatus = $null + receiptReason = $null + evidenceFreshness = $null + nextWakeCondition = $null + mergeStateStatus = $null + isInMergeQueue = $null + autoMergeEnabled = $null + mergedAt = $null + } + prUrl = $null + issueNumber = $null + reason = $null + } + queueAuthority = [ordered]@{ + status = 'none' + source = 'none' + nextWakeCondition = $null + summaryPath = $null + promotionStatus = $null + mergeStateStatus = $null + isInMergeQueue = $false + autoMergeEnabled = $false + prUrl = $null + } } wake = [ordered]@{ terminalState = 'compare-work' @@ -247,6 +299,16 @@ Describe 'Import-HandoffState' -Tag 'Unit' { wakeTerminalState = 'compare-work' monitoringStatus = 'active' futureAgentAction = 'future-agent-may-pivot' + releaseSigningStatus = 'warn' + releaseSigningExternalBlocker = 'workflow-signing-secret-missing' + releasePublicationState = 'tag-created-not-pushed' + releasePublishedBundleState = 'producer-native-incomplete' + releasePublishedBundleReleaseTag = 'v0.6.3-tools.14' + releasePublishedBundleAuthoritativeConsumerPin = $null + queueHandoffStatus = 'none' + queueHandoffNextWakeCondition = $null + queueHandoffPrUrl = $null + queueAuthoritySource = 'none' } } | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $handoffDir 'autonomous-governor-summary.json') -Encoding utf8 @@ -255,6 +317,10 @@ Describe 'Import-HandoffState' -Tag 'Unit' { $output | Should -Match '\[handoff\] Autonomous governor summary' $output | Should -Match 'mode\s+: compare-governance-work' $output | Should -Match 'next\s+: continue-compare-governance-work' + $output | Should -Match 'release\s+: warn' + $output | Should -Match 'blocker\s+: workflow-signing-secret-missing' + $output | Should -Match 'bundle\s+: producer-native-incomplete' + $output | Should -Match 'bundleTag: v0.6.3-tools.14' $global:HandoffAutonomousGovernorSummary.schema | Should -Be 'priority/autonomous-governor-summary-report@v1' Remove-Variable -Name HandoffAutonomousGovernorSummary -Scope Global -ErrorAction SilentlyContinue @@ -283,10 +349,32 @@ Describe 'Import-HandoffState' -Tag 'Unit' { futureAgentAction = 'stay-in-compare-monitoring' governorMode = 'compare-governance-work' nextAction = 'continue-compare-governance-work' + queueHandoffStatus = $null + queueHandoffNextWakeCondition = $null + queueHandoffPrUrl = $null + queueAuthoritySource = $null } portfolio = [ordered]@{ repositoryCount = 4 repositories = @() + dependencies = @( + [ordered]@{ + id = 'vi-history-producer-native-distributor' + status = 'blocked' + ownerRepository = 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + dependentRepository = 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate' + requiredCapability = 'vi-history' + source = 'compare-release-signing-readiness' + releaseSigningStatus = 'warn' + releasePublicationState = 'unobserved' + publishedBundleState = 'producer-native-incomplete' + publishedBundleReleaseTag = 'v0.6.3-tools.14' + publishedBundleAuthoritativeConsumerPin = $null + signingCapabilityState = 'missing' + externalBlocker = 'workflow-signing-secret-missing' + detail = 'awaiting-compare-release-signing-blocker-clear' + } + ) unsupportedPaths = @() } summary = [ordered]@{ @@ -299,6 +387,17 @@ Describe 'Import-HandoffState' -Tag 'Unit' { templateMonitoringStatus = 'pass' supportedProofStatus = 'pass' repoGraphStatus = 'pass' + queueHandoffStatus = $null + queueHandoffNextWakeCondition = $null + queueHandoffPrUrl = $null + queueAuthoritySource = $null + viHistoryDistributorDependencyStatus = 'blocked' + viHistoryDistributorDependencyTargetRepository = 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate' + viHistoryDistributorDependencyExternalBlocker = 'workflow-signing-secret-missing' + viHistoryDistributorDependencyPublicationState = 'unobserved' + viHistoryDistributorDependencyPublishedBundleState = 'producer-native-incomplete' + viHistoryDistributorDependencyPublishedBundleReleaseTag = 'v0.6.3-tools.14' + viHistoryDistributorDependencyAuthoritativeConsumerPin = $null portfolioWakeConditionCount = 3 triggeredWakeConditions = @( 'compare-queue-not-empty', @@ -313,8 +412,120 @@ Describe 'Import-HandoffState' -Tag 'Unit' { $output | Should -Match '\[handoff\] Governor portfolio summary' $output | Should -Match 'mode\s+: compare-governance-work' $output | Should -Match 'proof\s+: pass' + $output | Should -Match 'vhist\s+: blocked' + $output | Should -Match 'vhistRepo: LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate' + $output | Should -Match 'vhistBlk : workflow-signing-secret-missing' + $output | Should -Match 'vhistPub : producer-native-incomplete' + $output | Should -Match 'vhistTag : v0.6.3-tools.14' $global:HandoffAutonomousGovernorPortfolioSummary.schema | Should -Be 'priority/autonomous-governor-portfolio-summary-report@v1' Remove-Variable -Name HandoffAutonomousGovernorPortfolioSummary -Scope Global -ErrorAction SilentlyContinue } + + It 'surfaces context concentrator summary when present' { + $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path + $scriptPath = Join-Path $repoRoot 'tools' 'priority' 'Import-HandoffState.ps1' + $handoffDir = Join-Path $TestDrive 'handoff' + New-Item -ItemType Directory -Force -Path $handoffDir | Out-Null + + [ordered]@{ + schema = 'priority/sagan-context-concentrator-report@v1' + generatedAt = '2026-03-23T23:25:00Z' + repository = 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + inputs = [ordered]@{ + priorityCachePath = '.agent_priority_cache.json' + governorSummaryPath = 'tests/results/_agent/handoff/autonomous-governor-summary.json' + governorPortfolioSummaryPath = 'tests/results/_agent/handoff/autonomous-governor-portfolio-summary.json' + monitoringModePath = 'tests/results/_agent/handoff/monitoring-mode.json' + operatorSteeringEventPath = 'tests/results/_agent/handoff/operator-steering-event.json' + episodeDirectoryPath = 'tests/results/_agent/memory/subagent-episodes' + } + sources = [ordered]@{ + priorityCache = [ordered]@{ path = '.agent_priority_cache.json'; exists = $true } + governorSummary = [ordered]@{ path = 'tests/results/_agent/handoff/autonomous-governor-summary.json'; exists = $true } + governorPortfolioSummary = [ordered]@{ path = 'tests/results/_agent/handoff/autonomous-governor-portfolio-summary.json'; exists = $true } + monitoringMode = [ordered]@{ path = 'tests/results/_agent/handoff/monitoring-mode.json'; exists = $true } + operatorSteeringEvent = [ordered]@{ path = 'tests/results/_agent/handoff/operator-steering-event.json'; exists = $false } + episodeDirectory = [ordered]@{ + path = 'tests/results/_agent/memory/subagent-episodes' + exists = $true + fileCount = 2 + validEpisodeCount = 2 + invalidEpisodeCount = 0 + } + } + focus = [ordered]@{ + activeIssue = [ordered]@{ + number = 1909 + title = '[governor]: build Sagan context concentrator for durable subagent memory' + url = 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/issues/1909' + state = 'OPEN' + repository = 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + } + currentOwnerRepository = 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + nextOwnerRepository = 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + nextAction = 'merge concentrator handoff support' + governorMode = 'compare-governance-work' + monitoringStatus = 'active' + } + memory = [ordered]@{ + hotWorkingSet = @( + [ordered]@{ + id = 'issue-1909' + kind = 'active-issue' + label = '#1909: [governor]: build Sagan context concentrator for durable subagent memory' + status = 'OPEN' + detail = 'Current standing-priority objective' + sourcePath = '.agent_priority_cache.json' + updatedAt = '2026-03-23T23:24:00Z' + issueNumber = 1909 + repository = 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + agentName = $null + nextAction = 'merge concentrator handoff support' + } + ) + warmMemory = @() + archiveCount = 1 + } + episodes = [ordered]@{ + totalCount = 2 + validCount = 2 + invalidCount = 0 + invalidEpisodes = @() + byStatus = @([ordered]@{ status = 'reported'; count = 2 }) + byAgent = @([ordered]@{ agentId = 'euler-id'; agentName = 'Euler'; count = 1 }) + recent = @() + } + cost = [ordered]@{ + episodeCountWithCost = 2 + tokenUsd = 0.12 + operatorLaborUsd = 10.416667 + blendedLowerBoundUsd = 10.536667 + observedDurationSeconds = 150 + } + summary = [ordered]@{ + status = 'active' + concentrationStatus = 'pass' + currentOwnerRepository = 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + nextOwnerRepository = 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + nextAction = 'merge concentrator handoff support' + activeIssueNumber = 1909 + hotWorkingSetCount = 1 + warmMemoryCount = 0 + archiveCount = 1 + blockerCount = 0 + recentEpisodeCount = 2 + blendedLowerBoundUsd = 10.536667 + } + } | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $handoffDir 'sagan-context-concentrator.json') -Encoding utf8 + + $output = & $scriptPath -HandoffDir $handoffDir *>&1 | Out-String + + $output | Should -Match '\[handoff\] Context concentrator' + $output | Should -Match 'issue\s+: #1909' + $output | Should -Match 'hot/warm\s+: 1/0' + $global:HandoffContextConcentrator.schema | Should -Be 'priority/sagan-context-concentrator-report@v1' + + Remove-Variable -Name HandoffContextConcentrator -Scope Global -ErrorAction SilentlyContinue + } } diff --git a/tests/Post-IssueComment.Tests.ps1 b/tests/Post-IssueComment.Tests.ps1 index 2c5d5135c..b8c466f09 100644 --- a/tests/Post-IssueComment.Tests.ps1 +++ b/tests/Post-IssueComment.Tests.ps1 @@ -46,15 +46,15 @@ $payload | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $env:GH_CAPTURE_PA $bodyText = "Comment from file`n" [System.IO.File]::WriteAllText($bodyPath, $bodyText) - & $scriptPath -Issue 1396 -BodyFile $bodyPath -Quiet + & $scriptPath -Issue 1396 -BodyFile $bodyPath -SkipBudgetHook -Quiet $capture = Get-Content -LiteralPath $script:capturePath -Raw | ConvertFrom-Json -ErrorAction Stop $capture.args[0] | Should -Be 'issue' $capture.args[1] | Should -Be 'comment' $capture.args[2] | Should -Be '1396' $capture.args | Should -Contain '--body-file' - $capture.bodyPath | Should -Be (Resolve-Path -LiteralPath $bodyPath).Path - $capture.bodyContent | Should -BeExactly $bodyText + [string]::IsNullOrWhiteSpace($capture.bodyPath) | Should -BeFalse + $capture.bodyContent.TrimEnd("`r", "`n") | Should -BeExactly $bodyText.TrimEnd("`r", "`n") } It 'routes inline body text through a temporary body file' { @@ -63,7 +63,7 @@ Continuity line `upstream/develop...HEAD` '@.TrimEnd("`r", "`n") - & $scriptPath -Issue 1396 -Body $bodyText -Quiet + & $scriptPath -Issue 1396 -Body $bodyText -SkipBudgetHook -Quiet $capture = Get-Content -LiteralPath $script:capturePath -Raw | ConvertFrom-Json -ErrorAction Stop $capture.args | Should -Contain '--body-file' @@ -76,10 +76,28 @@ Continuity line $bodyPath = Join-Path $TestDrive 'edit-last.md' Set-Content -LiteralPath $bodyPath -Value 'Edit last comment' -Encoding utf8 - & $scriptPath -Issue 1396 -BodyFile $bodyPath -EditLast -Quiet + & $scriptPath -Issue 1396 -BodyFile $bodyPath -EditLast -SkipBudgetHook -Quiet $capture = Get-Content -LiteralPath $script:capturePath -Raw | ConvertFrom-Json -ErrorAction Stop $capture.args | Should -Contain '--edit-last' $capture.args | Should -Contain '--body-file' } + + It 'appends the budget hook when a stub markdown hook file is supplied' { + $bodyPath = Join-Path $TestDrive 'comment.md' + $hookPath = Join-Path $TestDrive 'hook.md' + Set-Content -LiteralPath $bodyPath -Value 'Body before hook' -Encoding utf8 + Set-Content -LiteralPath $hookPath -Value @' + +_Budget hook_: blended lower bound `$42.500000`. + +'@ -Encoding utf8 + + & $scriptPath -Issue 1396 -BodyFile $bodyPath -BudgetHookMarkdownFile $hookPath -Quiet + + $capture = Get-Content -LiteralPath $script:capturePath -Raw | ConvertFrom-Json -ErrorAction Stop + $capture.bodyContent | Should -Match '' + $capture.bodyContent | Should -Match 'Body before hook' + $capture.bodyContent | Should -Match 'blended lower bound' + } } diff --git a/tests/Post-PullRequestComment.Tests.ps1 b/tests/Post-PullRequestComment.Tests.ps1 index 1ea86e8bd..1362e93ab 100644 --- a/tests/Post-PullRequestComment.Tests.ps1 +++ b/tests/Post-PullRequestComment.Tests.ps1 @@ -46,7 +46,7 @@ $payload | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $env:GH_CAPTURE_PA $bodyText = "PR comment from file`n" [System.IO.File]::WriteAllText($bodyPath, $bodyText) - & $scriptPath -PullRequest 1396 -Repo 'LabVIEW-Community-CI-CD/compare-vi-cli-action' -BodyFile $bodyPath -Quiet + & $scriptPath -PullRequest 1396 -Repo 'LabVIEW-Community-CI-CD/compare-vi-cli-action' -BodyFile $bodyPath -SkipBudgetHook -Quiet $capture = Get-Content -LiteralPath $script:capturePath -Raw | ConvertFrom-Json -ErrorAction Stop $capture.args[0] | Should -Be 'pr' @@ -55,8 +55,8 @@ $payload | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $env:GH_CAPTURE_PA $capture.args | Should -Contain '--repo' $capture.args | Should -Contain 'LabVIEW-Community-CI-CD/compare-vi-cli-action' $capture.args | Should -Contain '--body-file' - $capture.bodyPath | Should -Be (Resolve-Path -LiteralPath $bodyPath).Path - $capture.bodyContent | Should -BeExactly $bodyText + [string]::IsNullOrWhiteSpace($capture.bodyPath) | Should -BeFalse + $capture.bodyContent.TrimEnd("`r", "`n") | Should -BeExactly $bodyText.TrimEnd("`r", "`n") } It 'routes inline PR comment text through a temporary body file' { @@ -65,7 +65,7 @@ Continuity line `upstream/develop...HEAD` '@.TrimEnd("`r", "`n") - & $scriptPath -PullRequest 1396 -Repo 'LabVIEW-Community-CI-CD/compare-vi-cli-action' -Body $bodyText -Quiet + & $scriptPath -PullRequest 1396 -Repo 'LabVIEW-Community-CI-CD/compare-vi-cli-action' -Body $bodyText -SkipBudgetHook -Quiet $capture = Get-Content -LiteralPath $script:capturePath -Raw | ConvertFrom-Json -ErrorAction Stop $capture.args | Should -Contain '--body-file' @@ -78,10 +78,28 @@ Continuity line $bodyPath = Join-Path $TestDrive 'edit-last.md' Set-Content -LiteralPath $bodyPath -Value 'Edit last PR comment' -Encoding utf8 - & $scriptPath -PullRequest 1396 -Repo 'LabVIEW-Community-CI-CD/compare-vi-cli-action' -BodyFile $bodyPath -EditLast -Quiet + & $scriptPath -PullRequest 1396 -Repo 'LabVIEW-Community-CI-CD/compare-vi-cli-action' -BodyFile $bodyPath -EditLast -SkipBudgetHook -Quiet $capture = Get-Content -LiteralPath $script:capturePath -Raw | ConvertFrom-Json -ErrorAction Stop $capture.args | Should -Contain '--edit-last' $capture.args | Should -Contain '--body-file' } + + It 'appends the budget hook when a stub markdown hook file is supplied' { + $bodyPath = Join-Path $TestDrive 'comment.md' + $hookPath = Join-Path $TestDrive 'hook.md' + Set-Content -LiteralPath $bodyPath -Value 'Body before hook' -Encoding utf8 + Set-Content -LiteralPath $hookPath -Value @' + +_Budget hook_: operator cap `$50000.000000`. + +'@ -Encoding utf8 + + & $scriptPath -PullRequest 1396 -Repo 'LabVIEW-Community-CI-CD/compare-vi-cli-action' -BodyFile $bodyPath -BudgetHookMarkdownFile $hookPath -Quiet + + $capture = Get-Content -LiteralPath $script:capturePath -Raw | ConvertFrom-Json -ErrorAction Stop + $capture.bodyContent | Should -Match '' + $capture.bodyContent | Should -Match 'Body before hook' + $capture.bodyContent | Should -Match 'operator cap' + } } diff --git a/tests/Run-AutonomousIntegrationLoop.Tests.ps1 b/tests/Run-AutonomousIntegrationLoop.Tests.ps1 index 6a25a01dd..0b576e71c 100644 --- a/tests/Run-AutonomousIntegrationLoop.Tests.ps1 +++ b/tests/Run-AutonomousIntegrationLoop.Tests.ps1 @@ -77,8 +77,20 @@ Describe 'Run-AutonomousIntegrationLoop TestStand harness mode' -Tag 'Unit' { New-Item -ItemType Directory -Path $outDir -Force | Out-Null $base = Join-Path $outDir 'BaseHarness.vi' $head = Join-Path $outDir 'HeadHarness.vi' + $leasePath = Join-Path $outDir 'execution-cell.json' New-Item -ItemType File -Path $base -Force | Out-Null New-Item -ItemType File -Path $head -Force | Out-Null + @' +{ + "schema": "priority/execution-cell-lease-report@v1", + "summary": { + "cellClass": "worker", + "suiteClass": "dual-plane-parity", + "operatorAuthorizationRef": "budget-auth://operator/session-2026-03-24", + "premiumSaganMode": false + } +} +'@ | Set-Content -LiteralPath $leasePath -Encoding UTF8 $harnessStub = Join-Path $outDir 'TestStand-CompareHarness.ps1' $logPath = Join-Path $outDir 'harness-log.ndjson' @@ -88,9 +100,18 @@ param( [string]`$BaseVi, [string]`$HeadVi, [Alias('LabVIEWPath')][string]`$LabVIEWExePath, + [string]`$LabVIEW64ExePath, + [string]`$LabVIEW32ExePath, [Alias('LVCompareExePath')][string]`$LVComparePath, + [string]`$AgentId, + [string]`$AgentClass, + [string]`$ExecutionCellLeasePath, + [string]`$ExecutionCellId, + [string]`$ExecutionCellLeaseId, + [string]`$HarnessInstanceId, [string]`$OutputRoot, [ValidateSet('detect','spawn','skip')][string]`$Warmup, + [ValidateSet('single-compare','dual-plane-parity')][string]`$SuiteClass = 'single-compare', [string[]]`$Flags, [switch]`$RenderReport, [switch]`$CloseLabVIEW, @@ -108,6 +129,16 @@ if (`$logDir -and -not (Test-Path `$logDir)) { New-Item -ItemType Directory -Pat head = `$HeadVi output = `$OutputRoot warmup = `$Warmup + suiteClass = `$SuiteClass + labviewExe = `$LabVIEWExePath + labview64Exe = `$LabVIEW64ExePath + labview32Exe = `$LabVIEW32ExePath + agentId = `$AgentId + agentClass = `$AgentClass + executionCellLeasePath = `$ExecutionCellLeasePath + executionCellId = `$ExecutionCellId + executionCellLeaseId = `$ExecutionCellLeaseId + harnessInstanceId = `$HarnessInstanceId flags = @(`$Flags) renderReport = `$RenderReport.IsPresent closeLabVIEW = `$CloseLabVIEW.IsPresent @@ -126,7 +157,7 @@ exit 0 try { $runner = Join-Path $outDir 'runner-harness.ps1' $runnerContent = @" -& '$scriptPath' -Base '$base' -Head '$head' -MaxIterations 2 -IntervalSeconds 0 -LogVerbosity Quiet -LvCompareArgs '-foo 1 -bar' -UseTestStandHarness -TestStandHarnessPath '$harnessStub' -TestStandOutputRoot '$outputRoot' -TestStandWarmup detect -TestStandRenderReport -TestStandCloseLabVIEW -TestStandCloseLVCompare -TestStandTimeoutSeconds 45 -TestStandReplaceFlags -FinalStatusJsonPath '$outDir/final.json' +& '$scriptPath' -Base '$base' -Head '$head' -MaxIterations 2 -IntervalSeconds 0 -LogVerbosity Quiet -LvCompareArgs '-foo 1 -bar' -UseTestStandHarness -TestStandHarnessPath '$harnessStub' -TestStandOutputRoot '$outputRoot' -TestStandWarmup detect -TestStandSuiteClass dual-plane-parity -TestStandLabVIEW64Path 'C:\Program Files\National Instruments\LabVIEW 2026\LabVIEW.exe' -TestStandLabVIEW32Path 'C:\Program Files (x86)\National Instruments\LabVIEW 2026\LabVIEW.exe' -TestStandAgentId 'hooke' -TestStandAgentClass 'subagent' -TestStandExecutionCellLeasePath '$leasePath' -TestStandExecutionCellId 'exec-cell-hooke-loop-01' -TestStandExecutionCellLeaseId 'lease-hooke-loop-01' -TestStandHarnessInstanceId 'ts-loop-hooke-01' -TestStandRenderReport -TestStandCloseLabVIEW -TestStandCloseLVCompare -TestStandTimeoutSeconds 45 -TestStandReplaceFlags -FinalStatusJsonPath '$outDir/final.json' exit `$LASTEXITCODE "@ Set-Content -LiteralPath $runner -Encoding UTF8 -Value $runnerContent @@ -140,12 +171,37 @@ exit `$LASTEXITCODE $entries[0].output | Should -Match 'iteration-0001$' $entries[1].output | Should -Match 'iteration-0002$' $entries | ForEach-Object { $_.warmup } | Sort-Object -Unique | Should -Be @('detect') + $entries | ForEach-Object { $_.suiteClass } | Sort-Object -Unique | Should -Be @('dual-plane-parity') + $entries | ForEach-Object { $_.labview64Exe } | Sort-Object -Unique | Should -Be @('C:\Program Files\National Instruments\LabVIEW 2026\LabVIEW.exe') + $entries | ForEach-Object { $_.labview32Exe } | Sort-Object -Unique | Should -Be @('C:\Program Files (x86)\National Instruments\LabVIEW 2026\LabVIEW.exe') + $entries | ForEach-Object { $_.agentId } | Sort-Object -Unique | Should -Be @('hooke') + $entries | ForEach-Object { $_.agentClass } | Sort-Object -Unique | Should -Be @('subagent') + $entries | ForEach-Object { $_.executionCellLeasePath } | Sort-Object -Unique | Should -Be @($leasePath) + $entries | ForEach-Object { $_.executionCellId } | Sort-Object -Unique | Should -Be @('exec-cell-hooke-loop-01') + $entries | ForEach-Object { $_.executionCellLeaseId } | Sort-Object -Unique | Should -Be @('lease-hooke-loop-01') + $entries | ForEach-Object { $_.harnessInstanceId } | Sort-Object -Unique | Should -Be @('ts-loop-hooke-01') $entries | ForEach-Object { $_.renderReport } | Sort-Object -Unique | Should -Be @($true) $entries | ForEach-Object { $_.closeLabVIEW } | Sort-Object -Unique | Should -Be @($true) $entries | ForEach-Object { $_.closeLVCompare } | Sort-Object -Unique | Should -Be @($true) $entries | ForEach-Object { [int]$_.timeout } | Sort-Object -Unique | Should -Be @(45) $entries | ForEach-Object { $_.replaceFlags } | Sort-Object -Unique | Should -Be @($true) $entries | ForEach-Object { $_.flags } | ForEach-Object { $_ } | Sort-Object -Unique | Should -Be @('-bar','-foo','1') + + $finalStatus = Get-Content -LiteralPath (Join-Path $outDir 'final.json') -Raw | ConvertFrom-Json + $finalStatus.harness.path | Should -Be $harnessStub + $finalStatus.harness.output | Should -Be $outputRoot + $finalStatus.harness.suiteClass | Should -Be 'dual-plane-parity' + $finalStatus.harness.runtimeSurface | Should -Be 'windows-native-teststand' + $finalStatus.harness.processModelClass | Should -Be 'parallel-process-model' + $finalStatus.harness.windowsOnly | Should -BeTrue + $finalStatus.harness.requestedSimultaneous | Should -BeTrue + $finalStatus.harness.cellClass | Should -Be 'worker' + $finalStatus.harness.operatorAuthorizationRef | Should -Be 'budget-auth://operator/session-2026-03-24' + $finalStatus.harness.premiumSaganMode | Should -BeFalse + $finalStatus.harness.executionCellLeasePath | Should -Be $leasePath + $finalStatus.harness.executionCellId | Should -Be 'exec-cell-hooke-loop-01' + $finalStatus.harness.executionCellLeaseId | Should -Be 'lease-hooke-loop-01' + $finalStatus.harness.harnessInstanceId | Should -Be 'ts-loop-hooke-01' } finally { Remove-Item Env:HARNESS_LOG -ErrorAction SilentlyContinue diff --git a/tests/Run-DX.Tests.ps1 b/tests/Run-DX.Tests.ps1 index 069193c41..cc8e6dcb9 100644 --- a/tests/Run-DX.Tests.ps1 +++ b/tests/Run-DX.Tests.ps1 @@ -18,12 +18,21 @@ Describe 'Run-DX.ps1 (TestStand staging)' -Tag 'Unit' { New-Item -ItemType Directory -Path 'tools' | Out-Null Copy-Item -LiteralPath $script:RunDxPath -Destination 'tools/Run-DX.ps1' Copy-Item -LiteralPath $script:StageScriptPath -Destination 'tools/Stage-CompareInputs.ps1' + $runDxContent = Get-Content -LiteralPath (Join-Path $work 'tools/Run-DX.ps1') -Raw + $runDxContent = $runDxContent -replace '(?m)^exit \$exit$', 'return $exit' + Set-Content -LiteralPath (Join-Path $work 'tools/Run-DX.ps1') -Value $runDxContent -Encoding UTF8 $harnessStub = @' param( [string]$BaseVi, [string]$HeadVi, [string]$OutputRoot, [string]$StagingRoot, + [string]$AgentId, + [string]$AgentClass, + [string]$ExecutionCellLeasePath, + [string]$ExecutionCellId, + [string]$ExecutionCellLeaseId, + [string]$HarnessInstanceId, [switch]$SameNameHint, [switch]$AllowSameLeaf, [string]$NoiseProfile, @@ -34,6 +43,12 @@ $log = [ordered]@{ base = $BaseVi head = $HeadVi stagingRoot = $StagingRoot + agentId = $AgentId + agentClass = $AgentClass + executionCellLeasePath = $ExecutionCellLeasePath + executionCellId = $ExecutionCellId + executionCellLeaseId = $ExecutionCellLeaseId + harnessInstanceId = $HarnessInstanceId sameNameHint = $SameNameHint.IsPresent allowSameLeaf = $AllowSameLeaf.IsPresent noiseProfile = $NoiseProfile @@ -42,6 +57,8 @@ $log = [ordered]@{ $log | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $OutputRoot 'harness-log.json') -Encoding utf8 $session = [ordered]@{ schema = 'teststand-compare-session/v1' + suiteClass = 'single-compare' + requestedSimultaneous = $false warmup = @{ mode = $Warmup events = $null @@ -60,6 +77,30 @@ $session = [ordered]@{ } outcome = $null error = $null + executionCell = @{ + cellId = $ExecutionCellId + leaseId = $ExecutionCellLeaseId + leasePath = $ExecutionCellLeasePath + agentId = $AgentId + agentClass = $AgentClass + cellClass = 'worker' + suiteClass = 'single-compare' + operatorAuthorizationRef = 'budget-auth://operator/session-2026-03-24' + premiumSaganMode = $false + } + harnessInstance = @{ + harnessKind = 'teststand-compare-harness' + instanceId = $HarnessInstanceId + role = 'single-plane' + processModelClass = 'sequential-process-model' + } + processModel = @{ + runtimeSurface = 'windows-native-teststand' + processModelClass = 'sequential-process-model' + windowsOnly = $true + rootHarnessInstanceId = $HarnessInstanceId + planeCount = 1 + } } $session | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $OutputRoot 'session-index.json') -Encoding utf8 exit 0 @@ -78,13 +119,20 @@ exit 0 $outputRoot = Join-Path $work 'results' $runDx = Join-Path $work 'tools/Run-DX.ps1' - & pwsh -NoLogo -NoProfile -File $runDx ` + $result = & $runDx ` -Suite TestStand ` -BaseVi $baseVi ` -HeadVi $headVi ` -OutputRoot $outputRoot ` - -Warmup skip *> $null - $LASTEXITCODE | Should -Be 0 + -ResultsPath $outputRoot ` + -Warmup skip ` + -AgentId hooke ` + -AgentClass subagent ` + -ExecutionCellLeasePath 'E:\comparevi-lanes\cells\hooke-01\execution-cell.json' ` + -ExecutionCellId 'exec-cell-hooke-01' ` + -ExecutionCellLeaseId 'lease-hooke-01' ` + -HarnessInstanceId 'harness-hooke-01' + $result | Should -Be 0 $logPath = Join-Path $outputRoot 'harness-log.json' Test-Path -LiteralPath $logPath | Should -BeTrue @@ -96,6 +144,11 @@ exit 0 $log.sameNameHint | Should -BeTrue $log.allowSameLeaf | Should -BeFalse $log.stagingRoot | Should -Not -BeNullOrEmpty + $log.agentId | Should -Be 'hooke' + $log.agentClass | Should -Be 'subagent' + $log.executionCellId | Should -Be 'exec-cell-hooke-01' + $log.executionCellLeaseId | Should -Be 'lease-hooke-01' + $log.harnessInstanceId | Should -Be 'harness-hooke-01' $log.noiseProfile | Should -Be 'full' Test-Path -LiteralPath $log.stagingRoot | Should -BeFalse @@ -104,6 +157,23 @@ exit 0 $session.compare.staging.enabled | Should -BeTrue $session.compare.staging.root | Should -Be $log.stagingRoot $session.compare.allowSameLeaf | Should -BeFalse + $session.executionCell.cellId | Should -Be 'exec-cell-hooke-01' + $session.executionCell.leaseId | Should -Be 'lease-hooke-01' + $session.harnessInstance.instanceId | Should -Be 'harness-hooke-01' + $session.processModel.runtimeSurface | Should -Be 'windows-native-teststand' + $session.processModel.processModelClass | Should -Be 'sequential-process-model' + $statusPath = Join-Path $outputRoot '_agent/dx-status.json' + $status = Get-Content -LiteralPath $statusPath -Raw | ConvertFrom-Json + $status.executionTopology.suiteClass | Should -Be 'single-compare' + $status.executionTopology.runtimeSurface | Should -Be 'windows-native-teststand' + $status.executionTopology.processModelClass | Should -Be 'sequential-process-model' + $status.executionTopology.requestedSimultaneous | Should -BeFalse + $status.executionTopology.cellClass | Should -Be 'worker' + $status.executionTopology.operatorAuthorizationRef | Should -Be 'budget-auth://operator/session-2026-03-24' + $status.executionTopology.premiumSaganMode | Should -BeFalse + $status.executionTopology.harnessKind | Should -Be 'teststand-compare-harness' + $status.executionTopology.executionCellId | Should -Be 'exec-cell-hooke-01' + $status.executionTopology.executionCellLeaseId | Should -Be 'lease-hooke-01' } finally { Pop-Location } } @@ -116,12 +186,21 @@ exit 0 New-Item -ItemType Directory -Path 'tools' | Out-Null Copy-Item -LiteralPath $script:RunDxPath -Destination 'tools/Run-DX.ps1' Copy-Item -LiteralPath $script:StageScriptPath -Destination 'tools/Stage-CompareInputs.ps1' + $runDxContent = Get-Content -LiteralPath (Join-Path $work 'tools/Run-DX.ps1') -Raw + $runDxContent = $runDxContent -replace '(?m)^exit \$exit$', 'return $exit' + Set-Content -LiteralPath (Join-Path $work 'tools/Run-DX.ps1') -Value $runDxContent -Encoding UTF8 $harnessStub = @' param( [string]$BaseVi, [string]$HeadVi, [string]$OutputRoot, [string]$StagingRoot, + [string]$AgentId, + [string]$AgentClass, + [string]$ExecutionCellLeasePath, + [string]$ExecutionCellId, + [string]$ExecutionCellLeaseId, + [string]$HarnessInstanceId, [switch]$SameNameHint, [switch]$AllowSameLeaf, [string]$NoiseProfile, @@ -132,6 +211,12 @@ $log = [ordered]@{ base = $BaseVi head = $HeadVi stagingRoot = $StagingRoot + agentId = $AgentId + agentClass = $AgentClass + executionCellLeasePath = $ExecutionCellLeasePath + executionCellId = $ExecutionCellId + executionCellLeaseId = $ExecutionCellLeaseId + harnessInstanceId = $HarnessInstanceId sameNameHint = $SameNameHint.IsPresent allowSameLeaf = $AllowSameLeaf.IsPresent noiseProfile = $NoiseProfile @@ -140,6 +225,8 @@ $log = [ordered]@{ $log | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $OutputRoot 'harness-log.json') -Encoding utf8 $session = [ordered]@{ schema = 'teststand-compare-session/v1' + suiteClass = 'single-compare' + requestedSimultaneous = $false warmup = @{ mode = $Warmup events = $null @@ -158,6 +245,30 @@ $session = [ordered]@{ } outcome = $null error = $null + executionCell = @{ + cellId = $ExecutionCellId + leaseId = $ExecutionCellLeaseId + leasePath = $ExecutionCellLeasePath + agentId = $AgentId + agentClass = $AgentClass + cellClass = 'worker' + suiteClass = 'single-compare' + operatorAuthorizationRef = $null + premiumSaganMode = $false + } + harnessInstance = @{ + harnessKind = 'teststand-compare-harness' + instanceId = $HarnessInstanceId + role = 'single-plane' + processModelClass = 'sequential-process-model' + } + processModel = @{ + runtimeSurface = 'windows-native-teststand' + processModelClass = 'sequential-process-model' + windowsOnly = $true + rootHarnessInstanceId = $HarnessInstanceId + planeCount = 1 + } } $session | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $OutputRoot 'session-index.json') -Encoding utf8 exit 0 @@ -174,15 +285,16 @@ exit 0 $outputRoot = Join-Path $work 'results' $runDx = Join-Path $work 'tools/Run-DX.ps1' - & pwsh -NoLogo -NoProfile -File $runDx ` + $result = & $runDx ` -Suite TestStand ` -BaseVi $baseVi ` -HeadVi $headVi ` -OutputRoot $outputRoot ` + -ResultsPath $outputRoot ` -Warmup detect ` -UseRawPaths ` - -NoiseProfile legacy *> $null - $LASTEXITCODE | Should -Be 0 + -NoiseProfile legacy + $result | Should -Be 0 $logPath = Join-Path $outputRoot 'harness-log.json' Test-Path -LiteralPath $logPath | Should -BeTrue @@ -202,5 +314,38 @@ exit 0 } finally { Pop-Location } } -} + It 'declares dual-plane parity forwarding and status projection in the wrapper contract' { + $content = Get-Content -LiteralPath $script:RunDxPath -Raw + + $content | Should -Match '\[string\]\$LabVIEW64ExePath' + $content | Should -Match '\[string\]\$LabVIEW32ExePath' + $content | Should -Match '\[string\]\$AgentId' + $content | Should -Match '\[string\]\$AgentClass' + $content | Should -Match '\[string\]\$ExecutionCellLeasePath' + $content | Should -Match '\[string\]\$ExecutionCellId' + $content | Should -Match '\[string\]\$ExecutionCellLeaseId' + $content | Should -Match '\[string\]\$HarnessInstanceId' + $content | Should -Match "\[ValidateSet\('single-compare','dual-plane-parity'\)\]\s*\[string\]\`$TestStandSuiteClass" + $content | Should -Match '\$hParams\.LabVIEW64ExePath\s*=\s*\$LabVIEW64ExePath' + $content | Should -Match '\$hParams\.LabVIEW32ExePath\s*=\s*\$LabVIEW32ExePath' + $content | Should -Match '\$hParams\.SuiteClass\s*=\s*\$TestStandSuiteClass' + $content | Should -Match '\$hParams\.AgentId\s*=\s*\$AgentId' + $content | Should -Match '\$hParams\.AgentClass\s*=\s*\$AgentClass' + $content | Should -Match '\$hParams\.ExecutionCellLeasePath\s*=\s*\$ExecutionCellLeasePath' + $content | Should -Match '\$hParams\.ExecutionCellId\s*=\s*\$ExecutionCellId' + $content | Should -Match '\$hParams\.ExecutionCellLeaseId\s*=\s*\$ExecutionCellLeaseId' + $content | Should -Match '\$hParams\.HarnessInstanceId\s*=\s*\$HarnessInstanceId' + $content | Should -Match 'suiteClass\s*=\s*if\s*\(\$session\.PSObject\.Properties\.Name\s*-contains\s*''suiteClass''\)' + $content | Should -Match 'primaryPlane\s*=\s*Get-SessionValue\s+\$session\s+''primaryPlane''' + $content | Should -Match 'Get-SessionBoolValue' + $content | Should -Match '\$requestedSimultaneous\s*=\s*\$false' + $content | Should -Match 'requestedSimultaneous\s*=\s*\$requestedSimultaneous' + $content | Should -Match 'executionTopology\s*=\s*@\{' + $content | Should -Match 'executionCell\s*=\s*Get-SessionValue\s+\$session\s+''executionCell''' + $content | Should -Match 'harnessInstance\s*=\s*Get-SessionValue\s+\$session\s+''harnessInstance''' + $content | Should -Match 'processModel\s*=\s*Get-SessionValue\s+\$session\s+''processModel''' + $content | Should -Match 'parity\s*=\s*Get-SessionValue\s+\$session\s+''parity''' + $content | Should -Match 'planes\s*=\s*Get-SessionValue\s+\$session\s+''planes''' + } +} diff --git a/tests/TestHelpers.Schema.ps1 b/tests/TestHelpers.Schema.ps1 index 19132d61f..a7d4fc5ab 100644 --- a/tests/TestHelpers.Schema.ps1 +++ b/tests/TestHelpers.Schema.ps1 @@ -26,7 +26,7 @@ if (-not (Get-Variable -Name JsonShapeSpecs -Scope Script -ErrorAction SilentlyC $script:JsonShapeSpecs['FinalStatus'] = [pscustomobject]@{ Required = @('schema','timestamp','iterations','diffs','errors','succeeded') - Optional = @('averageSeconds','totalSeconds','percentiles','histogram','diffSummaryEmitted','basePath','headPath') + Optional = @('averageSeconds','totalSeconds','percentiles','histogram','diffSummaryEmitted','basePath','headPath','harness') Types = @{ schema = { param($v) $v -is [string] -and $v -eq 'loop-final-status-v1' } timestamp = { param($v) ($v -is [string] -or $v -is [datetime]) } @@ -41,6 +41,7 @@ $script:JsonShapeSpecs['FinalStatus'] = [pscustomobject]@{ diffSummaryEmitted= { param($v) -not $v -or $v -is [bool] } basePath = { param($v) -not $v -or $v -is [string] } headPath = { param($v) -not $v -or $v -is [string] } + harness = { param($v) -not $v -or ($v -is [hashtable] -or $v -is [pscustomobject]) } } } diff --git a/tests/TestStand-CompareHarness.Tests.ps1 b/tests/TestStand-CompareHarness.Tests.ps1 index cfe70b198..c37c3a203 100644 --- a/tests/TestStand-CompareHarness.Tests.ps1 +++ b/tests/TestStand-CompareHarness.Tests.ps1 @@ -67,11 +67,34 @@ exit 0 $outputRoot = Join-Path $work 'results' $harness = Join-Path $work 'tools\TestStand-CompareHarness.ps1' $stageDir = Join-Path $work 'stage' + $leasePath = Join-Path $work 'execution-cell.json' New-Item -ItemType Directory -Path $stageDir | Out-Null $stagedBase = Join-Path $stageDir 'Base.vi' $stagedHead = Join-Path $stageDir 'Head.vi' Copy-Item -LiteralPath $baseReal -Destination $stagedBase -Force Copy-Item -LiteralPath $headReal -Destination $stagedHead -Force + @{ + schema = 'priority/execution-cell-lease@v1' + cellId = 'exec-cell-hooke-01' + host = @{ + isolatedLaneGroupId = 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + fingerprintSha256 = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + } + request = @{ + agentId = 'hooke' + agentClass = 'subagent' + cellClass = 'worker' + suiteClass = 'single-compare' + planeBinding = 'native-labview-2025-64' + harnessKind = 'teststand-compare-harness' + workingRoot = $outputRoot + artifactRoot = $outputRoot + } + grant = @{ + leaseId = 'lease-hooke-01' + premiumSaganMode = $false + } + } | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $leasePath -Encoding UTF8 & pwsh -NoLogo -NoProfile -File $harness ` -BaseVi $stagedBase ` @@ -83,7 +106,9 @@ exit 0 -CloseLabVIEW ` -CloseLVCompare ` -StagingRoot $stageDir ` - -SameNameHint *> $null + -SameNameHint ` + -ExecutionCellLeasePath $leasePath ` + -HarnessInstanceId 'ts-harness-hooke-01' *> $null $invokeLogPath = Get-ChildItem -Path $outputRoot -Recurse -Filter 'invoke-args.json' | Select-Object -First 1 $invokeLogPath | Should -Not -BeNullOrEmpty @@ -100,6 +125,22 @@ exit 0 $indexData.compare.sameName | Should -BeTrue $indexData.compare.staging.enabled | Should -BeTrue $indexData.compare.staging.root | Should -Be $stageDir + $indexData.executionCell.cellId | Should -Be 'exec-cell-hooke-01' + $indexData.executionCell.leaseId | Should -Be 'lease-hooke-01' + $indexData.executionCell.agentId | Should -Be 'hooke' + $indexData.executionCell.agentClass | Should -Be 'subagent' + $indexData.executionCell.cellClass | Should -Be 'worker' + $indexData.executionCell.runtimeSurface | Should -Be 'windows-native-teststand' + $indexData.executionCell.premiumSaganMode | Should -BeFalse + $indexData.executionCell.operatorAuthorizationRef | Should -BeNullOrEmpty + $indexData.harnessInstance.instanceId | Should -Be 'ts-harness-hooke-01' + $indexData.harnessInstance.role | Should -Be 'single-plane' + $indexData.harnessInstance.processModelClass | Should -Be 'sequential-process-model' + $indexData.processModel.runtimeSurface | Should -Be 'windows-native-teststand' + $indexData.processModel.processModelClass | Should -Be 'sequential-process-model' + $indexData.processModel.windowsOnly | Should -BeTrue + $indexData.processModel.rootHarnessInstanceId | Should -Be 'ts-harness-hooke-01' + $indexData.processModel.planeCount | Should -Be 1 } finally { Pop-Location } } @@ -194,3 +235,202 @@ exit 0 } } +Describe 'TestStand-CompareHarness.ps1 (dual-plane parity)' -Tag 'Unit' { + It 'runs LabVIEW 2026 x64 and x32 sessions simultaneously and emits a parity session index' { + $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path + $baseDir = Join-Path $TestDrive 'dual-base' + $headDir = Join-Path $TestDrive 'dual-head' + New-Item -ItemType Directory -Path $baseDir, $headDir | Out-Null + $baseVi = Join-Path $baseDir 'Base.vi' + $headVi = Join-Path $headDir 'Head.vi' + Set-Content -LiteralPath $baseVi -Value 'base' -Encoding UTF8 + Set-Content -LiteralPath $headVi -Value 'head' -Encoding UTF8 + + $work = Join-Path $TestDrive 'harness-dual-plane' + New-Item -ItemType Directory -Path $work | Out-Null + Push-Location $work + try { + New-Item -ItemType Directory -Path 'tools' | Out-Null + Copy-Item -LiteralPath (Join-Path $repoRoot 'tools\TestStand-CompareHarness.ps1') -Destination 'tools\TestStand-CompareHarness.ps1' + + Set-Content -LiteralPath 'tools/Warmup-LabVIEWRuntime.ps1' -Encoding UTF8 -Value @' +param( + [string]$LabVIEWPath, + [string]$JsonLogPath, + [string]$SupportedBitness +) +if ($JsonLogPath) { + $dir = Split-Path -Parent $JsonLogPath + if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } + (@{ type = 'warmup'; bitness = $SupportedBitness; labview = $LabVIEWPath } | ConvertTo-Json -Compress) | Set-Content -LiteralPath $JsonLogPath -Encoding utf8 +} +exit 0 +'@ + + $invokeStub = @' +param( + [string]$BaseVi, + [string]$HeadVi, + [Alias('LabVIEWPath')] + [string]$LabVIEWExePath, + [Alias('LVCompareExePath')] + [string]$LVComparePath, + [string]$OutputDir, + [switch]$RenderReport, + [string]$JsonLogPath, + [object]$Flags, + [string]$NoiseProfile, + [string]$LabVIEWBitness +) +if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null } +if ($JsonLogPath) { + '{}' | Set-Content -LiteralPath $JsonLogPath -Encoding utf8 +} +$capture = [ordered]@{ + exitCode = 0 + seconds = if ($LabVIEWBitness -eq '32') { 1.32 } else { 1.64 } + command = "stub-$LabVIEWBitness" + cliPath = "C:\Program Files\National Instruments\Shared\LabVIEW CLI\$LabVIEWBitness\LabVIEWCLI.exe" + environment = @{ + cli = @{ + path = "C:\Program Files\National Instruments\Shared\LabVIEW CLI\$LabVIEWBitness\LabVIEWCLI.exe" + version = '26.0.0f0' + reportType = 'html' + reportPath = 'compare-report.html' + status = 'ok' + message = "compare completed for $LabVIEWBitness" + } + } +} +$capture | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $OutputDir 'lvcompare-capture.json') -Encoding utf8 +if ($RenderReport) { + Set-Content -LiteralPath (Join-Path $OutputDir 'compare-report.html') -Value "" -Encoding utf8 +} +exit 0 +'@ + Set-Content -LiteralPath 'tools/Invoke-LVCompare.ps1' -Value $invokeStub -Encoding UTF8 + Set-Content -LiteralPath 'tools/Close-LVCompare.ps1' -Value "param() exit 0" -Encoding UTF8 + Set-Content -LiteralPath 'tools/Close-LabVIEW.ps1' -Value "param() exit 0" -Encoding UTF8 + + $outputRoot = Join-Path $work 'results' + $harness = Join-Path $work 'tools\TestStand-CompareHarness.ps1' + $leasePath = Join-Path $work 'execution-cell.json' + @{ + schema = 'priority/execution-cell-lease@v1' + cellId = 'exec-cell-sagan-01' + host = @{ + isolatedLaneGroupId = 'host-os-fingerprint:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210' + fingerprintSha256 = 'fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210' + } + request = @{ + agentId = 'sagan' + agentClass = 'sagan' + cellClass = 'kernel-coordinator' + suiteClass = 'dual-plane-parity' + planeBinding = 'dual-plane-parity' + harnessKind = 'teststand-compare-harness' + workingRoot = $outputRoot + artifactRoot = $outputRoot + } + grant = @{ + leaseId = 'lease-sagan-01' + premiumSaganMode = $false + } + } | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $leasePath -Encoding UTF8 + & pwsh -NoLogo -NoProfile -File $harness ` + -BaseVi $baseVi ` + -HeadVi $headVi ` + -OutputRoot $outputRoot ` + -SuiteClass dual-plane-parity ` + -LabVIEW64ExePath 'C:\Program Files\National Instruments\LabVIEW 2026\LabVIEW.exe' ` + -LabVIEW32ExePath 'C:\Program Files (x86)\National Instruments\LabVIEW 2026\LabVIEW.exe' ` + -Warmup detect ` + -RenderReport ` + -ExecutionCellLeasePath $leasePath ` + -HarnessInstanceId 'ts-harness-sagan-01' *> $null + + $LASTEXITCODE | Should -Be 0 + + $sessionIndex = Join-Path $outputRoot 'session-index.json' + Test-Path -LiteralPath $sessionIndex | Should -BeTrue + $indexData = Get-Content -LiteralPath $sessionIndex -Raw | ConvertFrom-Json -Depth 12 + $indexData.schema | Should -Be 'teststand-compare-session/v2' + $indexData.suiteClass | Should -Be 'dual-plane-parity' + $indexData.primaryPlane | Should -Be 'native-labview-2026-64' + $indexData.requestedSimultaneous | Should -BeTrue + $indexData.executionCell.cellId | Should -Be 'exec-cell-sagan-01' + $indexData.executionCell.leaseId | Should -Be 'lease-sagan-01' + $indexData.executionCell.agentId | Should -Be 'sagan' + $indexData.executionCell.agentClass | Should -Be 'sagan' + $indexData.executionCell.cellClass | Should -Be 'kernel-coordinator' + $indexData.executionCell.runtimeSurface | Should -Be 'windows-native-teststand' + $indexData.executionCell.premiumSaganMode | Should -BeFalse + $indexData.executionCell.operatorAuthorizationRef | Should -BeNullOrEmpty + $indexData.harnessInstance.instanceId | Should -Be 'ts-harness-sagan-01' + $indexData.harnessInstance.role | Should -Be 'coordinator' + $indexData.harnessInstance.processModelClass | Should -Be 'parallel-process-model' + $indexData.processModel.runtimeSurface | Should -Be 'windows-native-teststand' + $indexData.processModel.processModelClass | Should -Be 'parallel-process-model' + $indexData.processModel.windowsOnly | Should -BeTrue + $indexData.processModel.rootHarnessInstanceId | Should -Be 'ts-harness-sagan-01' + $indexData.processModel.planeCount | Should -Be 2 + $indexData.parity.status | Should -Be 'match' + $indexData.parity.mismatchCount | Should -Be 0 + $indexData.planes.x64.plane | Should -Be 'native-labview-2026-64' + $indexData.planes.x32.plane | Should -Be 'native-labview-2026-32' + $indexData.planes.x64.architecture | Should -Be '64-bit' + $indexData.planes.x32.architecture | Should -Be '32-bit' + $indexData.planes.x64.labviewExePath | Should -Be 'C:\Program Files\National Instruments\LabVIEW 2026\LabVIEW.exe' + $indexData.planes.x32.labviewExePath | Should -Be 'C:\Program Files (x86)\National Instruments\LabVIEW 2026\LabVIEW.exe' + $indexData.planes.x64.outcome.exitCode | Should -Be 0 + $indexData.planes.x32.outcome.exitCode | Should -Be 0 + $indexData.planes.x64.compare.report | Should -BeTrue + $indexData.planes.x32.compare.report | Should -BeTrue + $indexData.planes.x64.compare.policy | Should -Be 'cli-only' + $indexData.planes.x32.compare.policy | Should -Be 'cli-only' + $indexData.planes.x64.compare.mode | Should -Be 'labview-cli' + $indexData.planes.x32.compare.mode | Should -Be 'labview-cli' + $indexData.planes.x64.executionCell.cellId | Should -Be 'exec-cell-sagan-01' + $indexData.planes.x32.executionCell.cellId | Should -Be 'exec-cell-sagan-01' + $indexData.planes.x64.executionCell.cellClass | Should -Be 'kernel-coordinator' + $indexData.planes.x32.executionCell.cellClass | Should -Be 'kernel-coordinator' + $indexData.planes.x64.executionCell.runtimeSurface | Should -Be 'windows-native-teststand' + $indexData.planes.x32.executionCell.runtimeSurface | Should -Be 'windows-native-teststand' + $indexData.planes.x64.executionCell.premiumSaganMode | Should -BeFalse + $indexData.planes.x32.executionCell.premiumSaganMode | Should -BeFalse + $indexData.planes.x64.harnessInstance.role | Should -Be 'plane-child' + $indexData.planes.x32.harnessInstance.role | Should -Be 'plane-child' + $indexData.planes.x64.harnessInstance.processModelClass | Should -Be 'parallel-process-model' + $indexData.planes.x32.harnessInstance.processModelClass | Should -Be 'parallel-process-model' + $indexData.planes.x64.harnessInstance.parentInstanceId | Should -Be 'ts-harness-sagan-01' + $indexData.planes.x32.harnessInstance.parentInstanceId | Should -Be 'ts-harness-sagan-01' + $indexData.planes.x64.harnessInstance.instanceId | Should -Be 'ts-harness-sagan-01-x64' + $indexData.planes.x32.harnessInstance.instanceId | Should -Be 'ts-harness-sagan-01-x32' + $indexData.planes.x64.processModel.runtimeSurface | Should -Be 'windows-native-teststand' + $indexData.planes.x32.processModel.runtimeSurface | Should -Be 'windows-native-teststand' + $indexData.planes.x64.processModel.processModelClass | Should -Be 'parallel-process-model' + $indexData.planes.x32.processModel.processModelClass | Should -Be 'parallel-process-model' + $indexData.planes.x64.processModel.rootHarnessInstanceId | Should -Be 'ts-harness-sagan-01' + $indexData.planes.x32.processModel.rootHarnessInstanceId | Should -Be 'ts-harness-sagan-01' + $indexData.planes.x64.processModel.planeCount | Should -Be 2 + $indexData.planes.x32.processModel.planeCount | Should -Be 2 + Test-Path -LiteralPath (Join-Path $outputRoot 'planes\x64\session-index.json') | Should -BeTrue + Test-Path -LiteralPath (Join-Path $outputRoot 'planes\x32\session-index.json') | Should -BeTrue + $x64Child = Get-Content -LiteralPath (Join-Path $outputRoot 'planes\x64\session-index.json') -Raw | ConvertFrom-Json -Depth 12 + $x32Child = Get-Content -LiteralPath (Join-Path $outputRoot 'planes\x32\session-index.json') -Raw | ConvertFrom-Json -Depth 12 + $x64Child.executionCell.cellId | Should -Be 'exec-cell-sagan-01' + $x32Child.executionCell.cellId | Should -Be 'exec-cell-sagan-01' + $x64Child.executionCell.cellClass | Should -Be 'kernel-coordinator' + $x32Child.executionCell.cellClass | Should -Be 'kernel-coordinator' + $x64Child.executionCell.runtimeSurface | Should -Be 'windows-native-teststand' + $x32Child.executionCell.runtimeSurface | Should -Be 'windows-native-teststand' + $x64Child.harnessInstance.instanceId | Should -Be 'ts-harness-sagan-01-x64' + $x32Child.harnessInstance.instanceId | Should -Be 'ts-harness-sagan-01-x32' + $x64Child.harnessInstance.processModelClass | Should -Be 'parallel-process-model' + $x32Child.harnessInstance.processModelClass | Should -Be 'parallel-process-model' + $x64Child.processModel.rootHarnessInstanceId | Should -Be 'ts-harness-sagan-01' + $x32Child.processModel.rootHarnessInstanceId | Should -Be 'ts-harness-sagan-01' + } + finally { Pop-Location } + } +} diff --git a/tests/Write-LabVIEW2026HostPlaneDiagnostics.Tests.ps1 b/tests/Write-LabVIEW2026HostPlaneDiagnostics.Tests.ps1 index 775781468..f84fc2640 100644 --- a/tests/Write-LabVIEW2026HostPlaneDiagnostics.Tests.ps1 +++ b/tests/Write-LabVIEW2026HostPlaneDiagnostics.Tests.ps1 @@ -48,6 +48,13 @@ Describe 'Write-LabVIEW2026HostPlaneDiagnostics.ps1' -Tag 'Unit' { $report.schema | Should -Be 'labview-2026-host-plane-report@v1' $report.runner.hostIsRunner | Should -BeTrue $report.runner.runnerName | Should -Not -BeNullOrEmpty + $report.host.osFingerprint.role | Should -Be 'canonical-host-baseline' + $report.host.osFingerprint.comparisonScope | Should -Be 'isolated-lane-group' + $report.host.osFingerprint.fingerprintSha256 | Should -Match '^[a-f0-9]{64}$' + $report.host.osFingerprint.isolatedLaneGroupId | Should -Match '^host-os-fingerprint:[a-f0-9]{64}$' + $report.host.osFingerprint.canonical.version | Should -Not -BeNullOrEmpty + $report.host.osFingerprint.canonical.buildNumber | Should -Not -BeNullOrEmpty + $report.host.osFingerprint.canonical.architecture | Should -Not -BeNullOrEmpty $report.policy.authoritativePlanes | Should -Contain 'docker-desktop/linux-container-2026' $report.policy.authoritativePlanes | Should -Contain 'docker-desktop/windows-container-2026' $report.policy.hostNativeShadowPlane.plane | Should -Be 'native-labview-2026-32' @@ -68,6 +75,10 @@ Describe 'Write-LabVIEW2026HostPlaneDiagnostics.ps1' -Tag 'Unit' { Test-Path -LiteralPath $summaryPath | Should -BeTrue $summary = Get-Content -LiteralPath $summaryPath -Raw $summary | Should -Match '# LabVIEW 2026 Host Plane Summary' + $summary | Should -Match '- Canonical host OS:' + $summary | Should -Match '- Host OS fingerprint SHA-256:' + $summary | Should -Match '- Isolated lane group ID:' + $summary | Should -Match '- Host OS branding:' $summary | Should -Match '- Native 64-bit: `ready`' $summary | Should -Match '- Native 32-bit: `ready`' $summary | Should -Match 'Host-native 32-bit shadow: `acceleration-surface`' @@ -141,5 +152,9 @@ Describe 'Write-LabVIEW2026HostPlaneDiagnostics.ps1' -Tag 'Unit' { $outputText | Should -Match 'labview-2026-native-64-status=ready' $outputText | Should -Match 'labview-2026-native-32-status=ready' $outputText | Should -Match 'labview-2026-native-parallel-supported=True' + $outputText | Should -Match 'labview-2026-host-os-fingerprint-sha256=' + $outputText | Should -Match 'labview-2026-host-isolated-lane-group-id=' + $outputText | Should -Match 'labview-2026-host-os-version=' + $outputText | Should -Match 'labview-2026-host-os-build=' } } diff --git a/tests/Write-SessionIndexV2CutoverReadiness.Tests.ps1 b/tests/Write-SessionIndexV2CutoverReadiness.Tests.ps1 index f8398126a..7c882a86e 100644 --- a/tests/Write-SessionIndexV2CutoverReadiness.Tests.ps1 +++ b/tests/Write-SessionIndexV2CutoverReadiness.Tests.ps1 @@ -136,11 +136,13 @@ $($consumerMatrixLines -join "`n") ## Burn-in tracking "@ - $checklistLines = foreach ($item in $RemainingChecklistItems) { - "- [ ] $item" - } - if ($checklistLines.Count -eq 0) { - $checklistLines = '- [x] Remove v1 generation from producer paths/workflows.' + $checklistLines = @( + foreach ($item in $RemainingChecklistItems) { + "- [ ] $item" + } + ) + if ($checklistLines.Length -eq 0) { + $checklistLines = @('- [x] Remove v1 generation from producer paths/workflows.') } Write-TextFile -Path $deprecationPath -Content (@" diff --git a/tools/CompareVI.Tools/CompareVI.Tools.psd1 b/tools/CompareVI.Tools/CompareVI.Tools.psd1 index 3a55566f1..fe08f4ad9 100644 --- a/tools/CompareVI.Tools/CompareVI.Tools.psd1 +++ b/tools/CompareVI.Tools/CompareVI.Tools.psd1 @@ -21,7 +21,7 @@ PSData = @{ Tags = @('CompareVI','LabVIEW','VIHistory') ProjectUri = 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action' - Prerelease = 'rc.1' + Prerelease = 'rc.2' } } } diff --git a/tools/LabVIEW2026HostPlaneDiagnostics.psm1 b/tools/LabVIEW2026HostPlaneDiagnostics.psm1 index c8f573afb..9895fb872 100644 --- a/tools/LabVIEW2026HostPlaneDiagnostics.psm1 +++ b/tools/LabVIEW2026HostPlaneDiagnostics.psm1 @@ -25,6 +25,147 @@ function Convert-ToPathString { return $Value.Trim() } +function Get-OptionalMemberValue { + param( + [AllowNull()]$Object, + [Parameter(Mandatory)][string]$Name + ) + + if ($null -eq $Object) { + return $null + } + + $property = $Object.PSObject.Properties[$Name] + if ($property) { + return $property.Value + } + + return $null +} + +function Convert-ToCanonicalString { + param([AllowNull()]$Value) + + if ($null -eq $Value) { + return '' + } + + return ([string]$Value).Trim() +} + +function Get-Sha256Hex { + param([Parameter(Mandatory)][string]$Value) + + $bytes = [System.Text.Encoding]::UTF8.GetBytes($Value) + $hash = [System.Security.Cryptography.SHA256]::HashData($bytes) + return ([System.BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant() +} + +function Get-HostOperatingSystemFingerprint { + $registryPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' + $currentVersion = $null + $osInfo = $null + $computerSystem = $null + + if ($IsWindows) { + try { + $currentVersion = Get-ItemProperty -Path $registryPath + } catch {} + + try { + $osInfo = Get-CimInstance -ClassName Win32_OperatingSystem + } catch {} + + try { + $computerSystem = Get-CimInstance -ClassName Win32_ComputerSystem + } catch {} + } + + $platform = if ($IsWindows) { 'windows' } else { 'non-windows' } + $version = if ($IsWindows) { + Convert-ToCanonicalString (Get-OptionalMemberValue -Object $osInfo -Name 'Version') + } else { + Convert-ToCanonicalString ([System.Runtime.InteropServices.RuntimeInformation]::OSDescription) + } + $buildNumber = if ($IsWindows) { + $value = Convert-ToCanonicalString (Get-OptionalMemberValue -Object $osInfo -Name 'BuildNumber') + if ([string]::IsNullOrWhiteSpace($value)) { + $value = Convert-ToCanonicalString (Get-OptionalMemberValue -Object $currentVersion -Name 'CurrentBuildNumber') + } + $value + } else { + '' + } + $ubrRaw = if ($IsWindows) { Get-OptionalMemberValue -Object $currentVersion -Name 'UBR' } else { $null } + $ubr = 0 + if ($null -ne $ubrRaw -and [int]::TryParse(([string]$ubrRaw), [ref]$ubr)) { + $ubr = [int]$ubr + } + + $canonical = [ordered]@{ + version = $version + buildNumber = $buildNumber + ubr = $ubr + displayVersion = if ($IsWindows) { Convert-ToCanonicalString (Get-OptionalMemberValue -Object $currentVersion -Name 'DisplayVersion') } else { '' } + editionId = if ($IsWindows) { Convert-ToCanonicalString (Get-OptionalMemberValue -Object $currentVersion -Name 'EditionID') } else { '' } + installationType = if ($IsWindows) { Convert-ToCanonicalString (Get-OptionalMemberValue -Object $currentVersion -Name 'InstallationType') } else { '' } + architecture = if ($IsWindows) { + Convert-ToCanonicalString (Get-OptionalMemberValue -Object $osInfo -Name 'OSArchitecture') + } else { + Convert-ToCanonicalString ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) + } + systemType = if ($IsWindows) { Convert-ToCanonicalString (Get-OptionalMemberValue -Object $computerSystem -Name 'SystemType') } else { '' } + buildLabEx = if ($IsWindows) { Convert-ToCanonicalString (Get-OptionalMemberValue -Object $currentVersion -Name 'BuildLabEx') } else { '' } + } + + $fingerprintPayload = [ordered]@{ + platform = $platform + comparisonScope = 'isolated-lane-group' + canonical = $canonical + } + $fingerprintSha256 = Get-Sha256Hex -Value (($fingerprintPayload | ConvertTo-Json -Depth 8 -Compress)) + + $caption = if ($IsWindows) { Convert-ToCanonicalString (Get-OptionalMemberValue -Object $osInfo -Name 'Caption') } else { Convert-ToCanonicalString ([System.Runtime.InteropServices.RuntimeInformation]::OSDescription) } + $productName = if ($IsWindows) { Convert-ToCanonicalString (Get-OptionalMemberValue -Object $currentVersion -Name 'ProductName') } else { '' } + $brandingMismatch = $false + if (-not [string]::IsNullOrWhiteSpace($caption) -and -not [string]::IsNullOrWhiteSpace($productName)) { + $brandingMismatch = -not [string]::Equals($caption, $productName, [System.StringComparison]::OrdinalIgnoreCase) + } + + return [pscustomobject][ordered]@{ + role = 'canonical-host-baseline' + comparisonScope = 'isolated-lane-group' + platform = $platform + fingerprintSha256 = $fingerprintSha256 + isolatedLaneGroupId = "host-os-fingerprint:$fingerprintSha256" + canonical = [pscustomobject]$canonical + advisory = [pscustomobject][ordered]@{ + caption = $caption + productName = $productName + currentVersionCompatibility = if ($IsWindows) { Convert-ToCanonicalString (Get-OptionalMemberValue -Object $currentVersion -Name 'CurrentVersion') } else { '' } + brandingMismatch = $brandingMismatch + installDate = if ($IsWindows -and (Get-OptionalMemberValue -Object $osInfo -Name 'InstallDate')) { ([datetime](Get-OptionalMemberValue -Object $osInfo -Name 'InstallDate')).ToString('o') } else { '' } + lastBootUpTime = if ($IsWindows -and (Get-OptionalMemberValue -Object $osInfo -Name 'LastBootUpTime')) { ([datetime](Get-OptionalMemberValue -Object $osInfo -Name 'LastBootUpTime')).ToString('o') } else { '' } + } + sources = [pscustomobject][ordered]@{ + registryPath = if ($IsWindows) { $registryPath } else { '' } + cimClass = if ($IsWindows) { 'Win32_OperatingSystem' } else { '' } + systemClass = if ($IsWindows) { 'Win32_ComputerSystem' } else { '' } + comparisonFields = @( + 'version', + 'buildNumber', + 'ubr', + 'displayVersion', + 'editionId', + 'installationType', + 'architecture', + 'systemType', + 'buildLabEx' + ) + } + } +} + function Get-HostPlaneIssues { param( [bool]$HasLabVIEW, @@ -161,6 +302,7 @@ function Get-LabVIEW2026HostPlaneReport { host = [ordered]@{ os = if ($IsWindows) { 'windows' } else { 'non-windows' } computerName = [Environment]::MachineName + osFingerprint = Get-HostOperatingSystemFingerprint } runner = [ordered]@{ hostIsRunner = $true diff --git a/tools/Post-IssueComment.ps1 b/tools/Post-IssueComment.ps1 index 3000f2b67..11888b950 100644 --- a/tools/Post-IssueComment.ps1 +++ b/tools/Post-IssueComment.ps1 @@ -11,6 +11,8 @@ param( [string]$Body, [switch]$EditLast, + [switch]$SkipBudgetHook, + [string]$BudgetHookMarkdownFile, [switch]$Quiet ) @@ -25,6 +27,123 @@ function Ensure-Gh { Ensure-Gh +$script:CommentBudgetHookStartMarker = '' +$script:CommentBudgetHookEndMarker = '' +$script:RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path + +function Remove-CommentBudgetHook { + param( + [AllowNull()] + [string]$BodyText + ) + + if ([string]::IsNullOrWhiteSpace($BodyText)) { + return '' + } + + $startIndex = $BodyText.IndexOf($script:CommentBudgetHookStartMarker, [System.StringComparison]::Ordinal) + if ($startIndex -lt 0) { + return $BodyText.TrimEnd("`r", "`n") + } + + $endIndex = $BodyText.IndexOf($script:CommentBudgetHookEndMarker, $startIndex, [System.StringComparison]::Ordinal) + if ($endIndex -lt 0) { + return $BodyText.TrimEnd("`r", "`n") + } + + $prefix = $BodyText.Substring(0, $startIndex).TrimEnd("`r", "`n") + $suffix = $BodyText.Substring($endIndex + $script:CommentBudgetHookEndMarker.Length).TrimStart("`r", "`n") + + if (-not [string]::IsNullOrWhiteSpace($prefix) -and -not [string]::IsNullOrWhiteSpace($suffix)) { + return ($prefix + "`n`n" + $suffix).TrimEnd("`r", "`n") + } + + return ($prefix + $suffix).TrimEnd("`r", "`n") +} + +function New-CommentBudgetHookFailureMarkdown { + param( + [Parameter(Mandatory=$true)] + [string]$Message + ) + + $sanitizedMessage = ($Message -replace '\s+', ' ').Trim() + return @( + $script:CommentBudgetHookStartMarker + "_Budget hook_: unavailable (`comment-budget-hook-generation-failed`): $sanitizedMessage." + $script:CommentBudgetHookEndMarker + ) -join "`n" +} + +function Get-CommentBudgetHookMarkdown { + param( + [Parameter(Mandatory=$true)] + [ValidateSet('issue', 'pr')] + [string]$TargetKind, + + [Parameter(Mandatory=$true)] + [int]$TargetNumber, + + [string]$Repo, + + [string]$MarkdownFile, + + [switch]$SkipHook + ) + + if ($SkipHook) { + return '' + } + + if (-not [string]::IsNullOrWhiteSpace($MarkdownFile)) { + return (Get-Content -LiteralPath (Resolve-Path -LiteralPath $MarkdownFile -ErrorAction Stop).Path -Raw) + } + + $hookScriptPath = Join-Path $script:RepoRoot 'tools' 'priority' 'github-comment-budget-hook.mjs' + $hookMarkdownPath = Join-Path $script:RepoRoot 'tests' 'results' '_agent' 'cost' 'github-comment-budget-hook.md' + $hookArgs = @( + $hookScriptPath, + '--repo-root', $script:RepoRoot, + '--target-kind', $TargetKind, + '--target-number', $TargetNumber.ToString(), + '--markdown-output', $hookMarkdownPath + ) + if (-not [string]::IsNullOrWhiteSpace($Repo)) { + $hookArgs += @('--repo', $Repo.Trim()) + } + + try { + & node @hookArgs | Out-Null + if (-not $?) { + $exitCode = if (Test-Path variable:LASTEXITCODE) { $LASTEXITCODE } else { $null } + throw "github-comment-budget-hook exited with code $exitCode." + } + return (Get-Content -LiteralPath $hookMarkdownPath -Raw) + } catch { + return New-CommentBudgetHookFailureMarkdown -Message $_.Exception.Message + } +} + +function Merge-CommentBudgetHook { + param( + [AllowNull()] + [string]$BodyText, + + [AllowNull()] + [string]$HookMarkdown + ) + + $cleanBody = Remove-CommentBudgetHook -BodyText $BodyText + if ([string]::IsNullOrWhiteSpace($HookMarkdown)) { + return $cleanBody + } + $normalizedHook = $HookMarkdown.TrimEnd("`r", "`n") + if ([string]::IsNullOrWhiteSpace($cleanBody)) { + return $normalizedHook + } + return ($cleanBody.TrimEnd("`r", "`n") + "`n`n" + $normalizedHook).TrimEnd("`r", "`n") +} + function Invoke-GhIssueComment { param( [Parameter(Mandatory=$true)] @@ -49,16 +168,27 @@ if ($EditLast) { switch ($PSCmdlet.ParameterSetName) { 'BodyFile' { $resolved = Resolve-Path -LiteralPath $BodyFile -ErrorAction Stop - $args = $issueArg + @('--body-file', $resolved.Path) + $bodyText = Get-Content -LiteralPath $resolved.Path -Raw + $hookMarkdown = Get-CommentBudgetHookMarkdown -TargetKind issue -TargetNumber $Issue -MarkdownFile $BudgetHookMarkdownFile -SkipHook:$SkipBudgetHook + $mergedBody = Merge-CommentBudgetHook -BodyText $bodyText -HookMarkdown $hookMarkdown + $temp = [System.IO.Path]::GetTempFileName() + Set-Content -LiteralPath $temp -Value $mergedBody -Encoding utf8 + $args = $issueArg + @('--body-file', $temp) if (-not $Quiet) { Write-Host ("Posting comment from file '{0}' to issue #{1}..." -f $resolved.Path, $Issue) } - Invoke-GhIssueComment -Arguments $args + try { + Invoke-GhIssueComment -Arguments $args + } finally { + Remove-Item -LiteralPath $temp -ErrorAction SilentlyContinue + } } 'Body' { $temp = [System.IO.Path]::GetTempFileName() try { - Set-Content -LiteralPath $temp -Value $Body -Encoding utf8 + $hookMarkdown = Get-CommentBudgetHookMarkdown -TargetKind issue -TargetNumber $Issue -MarkdownFile $BudgetHookMarkdownFile -SkipHook:$SkipBudgetHook + $mergedBody = Merge-CommentBudgetHook -BodyText $Body -HookMarkdown $hookMarkdown + Set-Content -LiteralPath $temp -Value $mergedBody -Encoding utf8 $args = $issueArg + @('--body-file', $temp) if (-not $Quiet) { Write-Host ("Posting comment to issue #{0} using temporary body file..." -f $Issue) diff --git a/tools/Post-PullRequestComment.ps1 b/tools/Post-PullRequestComment.ps1 index ed8acfdb0..6b663b776 100644 --- a/tools/Post-PullRequestComment.ps1 +++ b/tools/Post-PullRequestComment.ps1 @@ -14,6 +14,8 @@ param( [string]$Body, [switch]$EditLast, + [switch]$SkipBudgetHook, + [string]$BudgetHookMarkdownFile, [switch]$Quiet ) @@ -44,6 +46,123 @@ function Invoke-GhPullRequestComment { Ensure-Gh +$script:CommentBudgetHookStartMarker = '' +$script:CommentBudgetHookEndMarker = '' +$script:RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path + +function Remove-CommentBudgetHook { + param( + [AllowNull()] + [string]$BodyText + ) + + if ([string]::IsNullOrWhiteSpace($BodyText)) { + return '' + } + + $startIndex = $BodyText.IndexOf($script:CommentBudgetHookStartMarker, [System.StringComparison]::Ordinal) + if ($startIndex -lt 0) { + return $BodyText.TrimEnd("`r", "`n") + } + + $endIndex = $BodyText.IndexOf($script:CommentBudgetHookEndMarker, $startIndex, [System.StringComparison]::Ordinal) + if ($endIndex -lt 0) { + return $BodyText.TrimEnd("`r", "`n") + } + + $prefix = $BodyText.Substring(0, $startIndex).TrimEnd("`r", "`n") + $suffix = $BodyText.Substring($endIndex + $script:CommentBudgetHookEndMarker.Length).TrimStart("`r", "`n") + + if (-not [string]::IsNullOrWhiteSpace($prefix) -and -not [string]::IsNullOrWhiteSpace($suffix)) { + return ($prefix + "`n`n" + $suffix).TrimEnd("`r", "`n") + } + + return ($prefix + $suffix).TrimEnd("`r", "`n") +} + +function New-CommentBudgetHookFailureMarkdown { + param( + [Parameter(Mandatory=$true)] + [string]$Message + ) + + $sanitizedMessage = ($Message -replace '\s+', ' ').Trim() + return @( + $script:CommentBudgetHookStartMarker + "_Budget hook_: unavailable (`comment-budget-hook-generation-failed`): $sanitizedMessage." + $script:CommentBudgetHookEndMarker + ) -join "`n" +} + +function Get-CommentBudgetHookMarkdown { + param( + [Parameter(Mandatory=$true)] + [ValidateSet('issue', 'pr')] + [string]$TargetKind, + + [Parameter(Mandatory=$true)] + [int]$TargetNumber, + + [string]$Repo, + + [string]$MarkdownFile, + + [switch]$SkipHook + ) + + if ($SkipHook) { + return '' + } + + if (-not [string]::IsNullOrWhiteSpace($MarkdownFile)) { + return (Get-Content -LiteralPath (Resolve-Path -LiteralPath $MarkdownFile -ErrorAction Stop).Path -Raw) + } + + $hookScriptPath = Join-Path $script:RepoRoot 'tools' 'priority' 'github-comment-budget-hook.mjs' + $hookMarkdownPath = Join-Path $script:RepoRoot 'tests' 'results' '_agent' 'cost' 'github-comment-budget-hook.md' + $hookArgs = @( + $hookScriptPath, + '--repo-root', $script:RepoRoot, + '--target-kind', $TargetKind, + '--target-number', $TargetNumber.ToString(), + '--markdown-output', $hookMarkdownPath + ) + if (-not [string]::IsNullOrWhiteSpace($Repo)) { + $hookArgs += @('--repo', $Repo.Trim()) + } + + try { + & node @hookArgs | Out-Null + if (-not $?) { + $exitCode = if (Test-Path variable:LASTEXITCODE) { $LASTEXITCODE } else { $null } + throw "github-comment-budget-hook exited with code $exitCode." + } + return (Get-Content -LiteralPath $hookMarkdownPath -Raw) + } catch { + return New-CommentBudgetHookFailureMarkdown -Message $_.Exception.Message + } +} + +function Merge-CommentBudgetHook { + param( + [AllowNull()] + [string]$BodyText, + + [AllowNull()] + [string]$HookMarkdown + ) + + $cleanBody = Remove-CommentBudgetHook -BodyText $BodyText + if ([string]::IsNullOrWhiteSpace($HookMarkdown)) { + return $cleanBody + } + $normalizedHook = $HookMarkdown.TrimEnd("`r", "`n") + if ([string]::IsNullOrWhiteSpace($cleanBody)) { + return $normalizedHook + } + return ($cleanBody.TrimEnd("`r", "`n") + "`n`n" + $normalizedHook).TrimEnd("`r", "`n") +} + $commentArgs = @('pr', 'comment', $PullRequest.ToString()) if (-not [string]::IsNullOrWhiteSpace($Repo)) { $commentArgs += @('--repo', $Repo.Trim()) @@ -55,16 +174,27 @@ if ($EditLast) { switch ($PSCmdlet.ParameterSetName) { 'BodyFile' { $resolved = Resolve-Path -LiteralPath $BodyFile -ErrorAction Stop - $args = $commentArgs + @('--body-file', $resolved.Path) + $bodyText = Get-Content -LiteralPath $resolved.Path -Raw + $hookMarkdown = Get-CommentBudgetHookMarkdown -TargetKind pr -TargetNumber $PullRequest -Repo $Repo -MarkdownFile $BudgetHookMarkdownFile -SkipHook:$SkipBudgetHook + $mergedBody = Merge-CommentBudgetHook -BodyText $bodyText -HookMarkdown $hookMarkdown + $temp = [System.IO.Path]::GetTempFileName() + Set-Content -LiteralPath $temp -Value $mergedBody -Encoding utf8 + $args = $commentArgs + @('--body-file', $temp) if (-not $Quiet) { Write-Host ("Posting comment from file '{0}' to PR #{1}..." -f $resolved.Path, $PullRequest) } - Invoke-GhPullRequestComment -Arguments $args + try { + Invoke-GhPullRequestComment -Arguments $args + } finally { + Remove-Item -LiteralPath $temp -ErrorAction SilentlyContinue + } } 'Body' { $temp = [System.IO.Path]::GetTempFileName() try { - Set-Content -LiteralPath $temp -Value $Body -Encoding utf8 + $hookMarkdown = Get-CommentBudgetHookMarkdown -TargetKind pr -TargetNumber $PullRequest -Repo $Repo -MarkdownFile $BudgetHookMarkdownFile -SkipHook:$SkipBudgetHook + $mergedBody = Merge-CommentBudgetHook -BodyText $Body -HookMarkdown $hookMarkdown + Set-Content -LiteralPath $temp -Value $mergedBody -Encoding utf8 $args = $commentArgs + @('--body-file', $temp) if (-not $Quiet) { Write-Host ("Posting comment to PR #{0} using temporary body file..." -f $PullRequest) diff --git a/tools/Print-AgentHandoff.ps1 b/tools/Print-AgentHandoff.ps1 index 823b322a1..37e559b83 100644 --- a/tools/Print-AgentHandoff.ps1 +++ b/tools/Print-AgentHandoff.ps1 @@ -1293,19 +1293,27 @@ try { $templateVerificationSyncScript = Join-Path $repoRoot 'tools' 'priority' 'sync-template-agent-verification-report.mjs' $templatePivotGateScript = Join-Path $repoRoot 'tools' 'priority' 'template-pivot-gate.mjs' $monitoringModeScript = Join-Path $repoRoot 'tools' 'priority' 'handoff-monitoring-mode.mjs' + $releasePublishedBundleObserverScript = Join-Path $repoRoot 'tools' 'priority' 'release-published-bundle-observer.mjs' + $releaseSigningReadinessScript = Join-Path $repoRoot 'tools' 'priority' 'release-signing-readiness.mjs' $governorSummaryScript = Join-Path $repoRoot 'tools' 'priority' 'autonomous-governor-summary.mjs' $governorPortfolioSummaryScript = Join-Path $repoRoot 'tools' 'priority' 'autonomous-governor-portfolio-summary.mjs' + $contextConcentratorScript = Join-Path $repoRoot 'tools' 'priority' 'sagan-context-concentrator.mjs' $nodeCmd = Get-Command node -ErrorAction SilentlyContinue if ($nodeCmd) { $promotionDir = Join-Path $ResultsRoot '_agent/promotion' + $releaseDir = Join-Path $ResultsRoot '_agent/release' $handoffDir = Join-Path $ResultsRoot '_agent/handoff' New-Item -ItemType Directory -Force -Path $promotionDir | Out-Null + New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null New-Item -ItemType Directory -Force -Path $handoffDir | Out-Null $templateVerificationSeedPath = Join-Path $promotionDir 'template-agent-verification-report.json' $templateVerificationOverlayPath = Join-Path $promotionDir 'template-agent-verification-report.local.json' $templateVerificationSyncPath = Join-Path $promotionDir 'template-agent-verification-sync.json' $templatePivotGatePath = Join-Path $promotionDir 'template-pivot-gate-report.json' + $releaseConductorReportPath = Join-Path $releaseDir 'release-conductor-report.json' + $releasePublishedBundleObserverPath = Join-Path $releaseDir 'release-published-bundle-observer.json' + $releaseSigningReadinessPath = Join-Path $releaseDir 'release-signing-readiness.json' $queueEmptyReportPath = Join-Path $repoRoot 'tests/results/_agent/issue/no-standing-priority.json' $entrypointStatusPath = Join-Path $ResultsRoot '_agent/handoff/entrypoint-status.json' $continuitySummaryPath = Join-Path $ResultsRoot '_agent/handoff/continuity-summary.json' @@ -1313,6 +1321,7 @@ try { $monitoringModePath = Join-Path $handoffDir 'monitoring-mode.json' $governorSummaryPath = Join-Path $handoffDir 'autonomous-governor-summary.json' $governorPortfolioSummaryPath = Join-Path $handoffDir 'autonomous-governor-portfolio-summary.json' + $contextConcentratorPath = Join-Path $handoffDir 'sagan-context-concentrator.json' if (Test-Path -LiteralPath $repoGraphTruthScript -PathType Leaf) { & $nodeCmd.Source $repoGraphTruthScript ` @@ -1346,12 +1355,27 @@ try { --output $monitoringModePath | Out-Host } + if (Test-Path -LiteralPath $releasePublishedBundleObserverScript -PathType Leaf) { + & $nodeCmd.Source $releasePublishedBundleObserverScript ` + --repo-root $repoRoot ` + --output $releasePublishedBundleObserverPath | Out-Host + } + + if (Test-Path -LiteralPath $releaseSigningReadinessScript -PathType Leaf) { + & $nodeCmd.Source $releaseSigningReadinessScript ` + --repo-root $repoRoot ` + --release-conductor-report $releaseConductorReportPath ` + --release-published-bundle-observer $releasePublishedBundleObserverPath ` + --output $releaseSigningReadinessPath | Out-Host + } + if (Test-Path -LiteralPath $governorSummaryScript -PathType Leaf) { & $nodeCmd.Source $governorSummaryScript ` --repo-root $repoRoot ` --queue-empty-report $queueEmptyReportPath ` --continuity-summary $continuitySummaryPath ` --monitoring-mode $monitoringModePath ` + --release-signing-readiness $releaseSigningReadinessPath ` --output $governorSummaryPath | Out-Host } @@ -1363,6 +1387,18 @@ try { --repo-graph-truth $repoGraphTruthPath ` --output $governorPortfolioSummaryPath | Out-Host } + + if (Test-Path -LiteralPath $contextConcentratorScript -PathType Leaf) { + & $nodeCmd.Source $contextConcentratorScript ` + --repo-root $repoRoot ` + --priority-cache (Join-Path $repoRoot '.agent_priority_cache.json') ` + --governor-summary $governorSummaryPath ` + --governor-portfolio-summary $governorPortfolioSummaryPath ` + --monitoring-mode $monitoringModePath ` + --operator-steering-event (Join-Path $handoffDir 'operator-steering-event.json') ` + --episode-directory (Join-Path $ResultsRoot '_agent/memory/subagent-episodes') ` + --output $contextConcentratorPath | Out-Host + } } } catch { Write-Warning ("Failed to refresh monitoring-mode handoff state: {0}" -f $_.Exception.Message) @@ -1517,6 +1553,21 @@ try { Write-Host (" next : {0}" -f (Format-NullableValue $governor.summary.nextAction)) Write-Host (" signal : {0}" -f (Format-NullableValue $governor.summary.signalQuality)) Write-Host (" queue : {0}" -f (Format-NullableValue $governor.summary.queueState)) + if ($governor.summary.PSObject.Properties['releaseSigningStatus']) { + Write-Host (" signing : {0}" -f (Format-NullableValue $governor.summary.releaseSigningStatus)) + if ($governor.summary.PSObject.Properties['releaseSigningExternalBlocker'] -and $governor.summary.releaseSigningExternalBlocker) { + Write-Host (" blocker : {0}" -f (Format-NullableValue $governor.summary.releaseSigningExternalBlocker)) + } + if ($governor.summary.PSObject.Properties['releasePublicationState'] -and $governor.summary.releasePublicationState) { + Write-Host (" publish : {0}" -f (Format-NullableValue $governor.summary.releasePublicationState)) + } + if ($governor.summary.PSObject.Properties['releasePublishedBundleState'] -and $governor.summary.releasePublishedBundleState) { + Write-Host (" bundle : {0}" -f (Format-NullableValue $governor.summary.releasePublishedBundleState)) + } + if ($governor.summary.PSObject.Properties['releasePublishedBundleReleaseTag'] -and $governor.summary.releasePublishedBundleReleaseTag) { + Write-Host (" bundleTag: {0}" -f (Format-NullableValue $governor.summary.releasePublishedBundleReleaseTag)) + } + } if ($governor.summary.nextOwnerRepository) { Write-Host (" nextRepo : {0}" -f (Format-NullableValue $governor.summary.nextOwnerRepository)) } @@ -1532,6 +1583,39 @@ try { Write-Host (" pr : {0}" -f (Format-NullableValue $governor.summary.queueHandoffPrUrl)) } } + if ($governor.summary.PSObject.Properties['executionTopologyStatus'] -and $governor.summary.executionTopologyStatus) { + Write-Host (" execTopo : {0}" -f (Format-NullableValue $governor.summary.executionTopologyStatus)) + Write-Host (" execProv : {0}" -f (Format-NullableValue $governor.summary.executionTopologyProviderId)) + Write-Host (" execSlot : {0}" -f (Format-NullableValue $governor.summary.executionTopologyWorkerSlotId)) + Write-Host (" execLanes: {0}/{1}" -f + (Format-NullableValue $governor.summary.executionTopologyActiveLogicalLaneCount), + (Format-NullableValue $governor.summary.executionTopologySeededLogicalLaneCount)) + if ($governor.summary.PSObject.Properties['executionTopologyRuntimeSurface'] -and $governor.summary.executionTopologyRuntimeSurface) { + Write-Host (" execSurf : {0}" -f (Format-NullableValue $governor.summary.executionTopologyRuntimeSurface)) + } + if ($governor.summary.PSObject.Properties['executionTopologyProcessModelClass'] -and $governor.summary.executionTopologyProcessModelClass) { + Write-Host (" execProc : {0}" -f (Format-NullableValue $governor.summary.executionTopologyProcessModelClass)) + } + if ($governor.summary.PSObject.Properties['executionTopologyRequestedSimultaneous'] -and $governor.summary.executionTopologyRequestedSimultaneous) { + Write-Host (" execSim : {0}" -f (Format-NullableValue $governor.summary.executionTopologyRequestedSimultaneous)) + } + if ($governor.summary.PSObject.Properties['executionTopologyCellClass'] -and $governor.summary.executionTopologyCellClass) { + Write-Host (" execCell : {0}" -f (Format-NullableValue $governor.summary.executionTopologyCellClass)) + } + if ($governor.summary.PSObject.Properties['executionTopologySuiteClass'] -and $governor.summary.executionTopologySuiteClass) { + Write-Host (" execSuite: {0}" -f (Format-NullableValue $governor.summary.executionTopologySuiteClass)) + } + if ($governor.summary.PSObject.Properties['executionTopologyOperatorAuthorizationRef'] -and $governor.summary.executionTopologyOperatorAuthorizationRef) { + Write-Host (" execAuth : {0}" -f (Format-NullableValue $governor.summary.executionTopologyOperatorAuthorizationRef)) + } + } + if ($governor.summary.PSObject.Properties['executionBundleStatus'] -and $governor.summary.executionBundleStatus) { + Write-Host (" exec : {0}" -f (Format-NullableValue $governor.summary.executionBundleStatus)) + Write-Host (" execPlan : {0}" -f (Format-NullableValue $governor.summary.executionBundlePlaneBinding)) + Write-Host (" execPrem : {0}" -f (Format-NullableValue $governor.summary.executionBundlePremiumSaganMode)) + Write-Host (" execLink : {0}" -f (Format-NullableValue $governor.summary.executionBundleReciprocalLinkReady)) + Write-Host (" execRate : {0}" -f (Format-NullableValue $governor.summary.executionBundleEffectiveBillableRateUsdPerHour)) + } if ($env:GITHUB_STEP_SUMMARY) { $governorLines = @( '### Autonomous Governor', @@ -1542,6 +1626,21 @@ try { ('- Signal quality: {0}' -f (Format-NullableValue $governor.summary.signalQuality)), ('- Queue state: {0}' -f (Format-NullableValue $governor.summary.queueState)) ) + if ($governor.summary.PSObject.Properties['releaseSigningStatus']) { + $governorLines += ('- Release signing: {0}' -f (Format-NullableValue $governor.summary.releaseSigningStatus)) + if ($governor.summary.PSObject.Properties['releaseSigningExternalBlocker'] -and $governor.summary.releaseSigningExternalBlocker) { + $governorLines += ('- Release blocker: {0}' -f (Format-NullableValue $governor.summary.releaseSigningExternalBlocker)) + } + if ($governor.summary.PSObject.Properties['releasePublicationState'] -and $governor.summary.releasePublicationState) { + $governorLines += ('- Release publication: {0}' -f (Format-NullableValue $governor.summary.releasePublicationState)) + } + if ($governor.summary.PSObject.Properties['releasePublishedBundleState'] -and $governor.summary.releasePublishedBundleState) { + $governorLines += ('- Published bundle: {0}' -f (Format-NullableValue $governor.summary.releasePublishedBundleState)) + } + if ($governor.summary.PSObject.Properties['releasePublishedBundleReleaseTag'] -and $governor.summary.releasePublishedBundleReleaseTag) { + $governorLines += ('- Published bundle tag: {0}' -f (Format-NullableValue $governor.summary.releasePublishedBundleReleaseTag)) + } + } if ($governor.summary.nextOwnerRepository) { $governorLines += ('- Next owner: {0}' -f (Format-NullableValue $governor.summary.nextOwnerRepository)) } @@ -1557,6 +1656,39 @@ try { $governorLines += ('- Queue PR: {0}' -f (Format-NullableValue $governor.summary.queueHandoffPrUrl)) } } + if ($governor.summary.PSObject.Properties['executionTopologyStatus'] -and $governor.summary.executionTopologyStatus) { + $governorLines += ('- Execution topology: {0}' -f (Format-NullableValue $governor.summary.executionTopologyStatus)) + $governorLines += ('- Execution provider: {0}' -f (Format-NullableValue $governor.summary.executionTopologyProviderId)) + $governorLines += ('- Execution worker slot: {0}' -f (Format-NullableValue $governor.summary.executionTopologyWorkerSlotId)) + $governorLines += ('- Execution logical lanes active/seeded: {0}/{1}' -f + (Format-NullableValue $governor.summary.executionTopologyActiveLogicalLaneCount), + (Format-NullableValue $governor.summary.executionTopologySeededLogicalLaneCount)) + if ($governor.summary.PSObject.Properties['executionTopologyRuntimeSurface'] -and $governor.summary.executionTopologyRuntimeSurface) { + $governorLines += ('- Execution runtime surface: {0}' -f (Format-NullableValue $governor.summary.executionTopologyRuntimeSurface)) + } + if ($governor.summary.PSObject.Properties['executionTopologyProcessModelClass'] -and $governor.summary.executionTopologyProcessModelClass) { + $governorLines += ('- Execution process model: {0}' -f (Format-NullableValue $governor.summary.executionTopologyProcessModelClass)) + } + if ($governor.summary.PSObject.Properties['executionTopologyRequestedSimultaneous'] -and $governor.summary.executionTopologyRequestedSimultaneous) { + $governorLines += ('- Execution simultaneous: {0}' -f (Format-NullableValue $governor.summary.executionTopologyRequestedSimultaneous)) + } + if ($governor.summary.PSObject.Properties['executionTopologyCellClass'] -and $governor.summary.executionTopologyCellClass) { + $governorLines += ('- Execution cell class: {0}' -f (Format-NullableValue $governor.summary.executionTopologyCellClass)) + } + if ($governor.summary.PSObject.Properties['executionTopologySuiteClass'] -and $governor.summary.executionTopologySuiteClass) { + $governorLines += ('- Execution suite class: {0}' -f (Format-NullableValue $governor.summary.executionTopologySuiteClass)) + } + if ($governor.summary.PSObject.Properties['executionTopologyOperatorAuthorizationRef'] -and $governor.summary.executionTopologyOperatorAuthorizationRef) { + $governorLines += ('- Execution operator authorization: {0}' -f (Format-NullableValue $governor.summary.executionTopologyOperatorAuthorizationRef)) + } + } + if ($governor.summary.PSObject.Properties['executionBundleStatus'] -and $governor.summary.executionBundleStatus) { + $governorLines += ('- Execution bundle: {0}' -f (Format-NullableValue $governor.summary.executionBundleStatus)) + $governorLines += ('- Execution plane: {0}' -f (Format-NullableValue $governor.summary.executionBundlePlaneBinding)) + $governorLines += ('- Premium Sagan mode: {0}' -f (Format-NullableValue $governor.summary.executionBundlePremiumSaganMode)) + $governorLines += ('- Execution reciprocal link: {0}' -f (Format-NullableValue $governor.summary.executionBundleReciprocalLinkReady)) + $governorLines += ('- Execution effective rate USD/hr: {0}' -f (Format-NullableValue $governor.summary.executionBundleEffectiveBillableRateUsdPerHour)) + } ($governorLines -join "`n") | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 } } @@ -1575,6 +1707,21 @@ try { Write-Host (" next : {0}" -f (Format-NullableValue $portfolio.summary.nextAction)) Write-Host (" template : {0}" -f (Format-NullableValue $portfolio.summary.templateMonitoringStatus)) Write-Host (" proof : {0}" -f (Format-NullableValue $portfolio.summary.supportedProofStatus)) + if ($portfolio.summary.PSObject.Properties['viHistoryDistributorDependencyStatus']) { + Write-Host (" vhist : {0}" -f (Format-NullableValue $portfolio.summary.viHistoryDistributorDependencyStatus)) + if ($portfolio.summary.PSObject.Properties['viHistoryDistributorDependencyTargetRepository'] -and $portfolio.summary.viHistoryDistributorDependencyTargetRepository) { + Write-Host (" vhistRepo: {0}" -f (Format-NullableValue $portfolio.summary.viHistoryDistributorDependencyTargetRepository)) + } + if ($portfolio.summary.PSObject.Properties['viHistoryDistributorDependencyExternalBlocker'] -and $portfolio.summary.viHistoryDistributorDependencyExternalBlocker) { + Write-Host (" vhistBlk : {0}" -f (Format-NullableValue $portfolio.summary.viHistoryDistributorDependencyExternalBlocker)) + } + if ($portfolio.summary.PSObject.Properties['viHistoryDistributorDependencyPublishedBundleState'] -and $portfolio.summary.viHistoryDistributorDependencyPublishedBundleState) { + Write-Host (" vhistPub : {0}" -f (Format-NullableValue $portfolio.summary.viHistoryDistributorDependencyPublishedBundleState)) + } + if ($portfolio.summary.PSObject.Properties['viHistoryDistributorDependencyPublishedBundleReleaseTag'] -and $portfolio.summary.viHistoryDistributorDependencyPublishedBundleReleaseTag) { + Write-Host (" vhistTag : {0}" -f (Format-NullableValue $portfolio.summary.viHistoryDistributorDependencyPublishedBundleReleaseTag)) + } + } if ($portfolio.summary.nextOwnerRepository) { Write-Host (" nextRepo : {0}" -f (Format-NullableValue $portfolio.summary.nextOwnerRepository)) } @@ -1586,6 +1733,39 @@ try { Write-Host (" queueSrc : {0}" -f (Format-NullableValue $portfolio.summary.queueAuthoritySource)) } } + if ($portfolio.summary.PSObject.Properties['executionTopologyStatus'] -and $portfolio.summary.executionTopologyStatus) { + Write-Host (" execTopo : {0}" -f (Format-NullableValue $portfolio.summary.executionTopologyStatus)) + Write-Host (" execProv : {0}" -f (Format-NullableValue $portfolio.summary.executionTopologyProviderId)) + Write-Host (" execSlot : {0}" -f (Format-NullableValue $portfolio.summary.executionTopologyWorkerSlotId)) + Write-Host (" execLanes: {0}/{1}" -f + (Format-NullableValue $portfolio.summary.executionTopologyActiveLogicalLaneCount), + (Format-NullableValue $portfolio.summary.executionTopologySeededLogicalLaneCount)) + if ($portfolio.summary.PSObject.Properties['executionTopologyRuntimeSurface'] -and $portfolio.summary.executionTopologyRuntimeSurface) { + Write-Host (" execSurf : {0}" -f (Format-NullableValue $portfolio.summary.executionTopologyRuntimeSurface)) + } + if ($portfolio.summary.PSObject.Properties['executionTopologyProcessModelClass'] -and $portfolio.summary.executionTopologyProcessModelClass) { + Write-Host (" execProc : {0}" -f (Format-NullableValue $portfolio.summary.executionTopologyProcessModelClass)) + } + if ($portfolio.summary.PSObject.Properties['executionTopologyRequestedSimultaneous'] -and $portfolio.summary.executionTopologyRequestedSimultaneous) { + Write-Host (" execSim : {0}" -f (Format-NullableValue $portfolio.summary.executionTopologyRequestedSimultaneous)) + } + if ($portfolio.summary.PSObject.Properties['executionTopologyCellClass'] -and $portfolio.summary.executionTopologyCellClass) { + Write-Host (" execCell : {0}" -f (Format-NullableValue $portfolio.summary.executionTopologyCellClass)) + } + if ($portfolio.summary.PSObject.Properties['executionTopologySuiteClass'] -and $portfolio.summary.executionTopologySuiteClass) { + Write-Host (" execSuite: {0}" -f (Format-NullableValue $portfolio.summary.executionTopologySuiteClass)) + } + if ($portfolio.summary.PSObject.Properties['executionTopologyOperatorAuthorizationRef'] -and $portfolio.summary.executionTopologyOperatorAuthorizationRef) { + Write-Host (" execAuth : {0}" -f (Format-NullableValue $portfolio.summary.executionTopologyOperatorAuthorizationRef)) + } + } + if ($portfolio.summary.PSObject.Properties['executionBundleStatus'] -and $portfolio.summary.executionBundleStatus) { + Write-Host (" exec : {0}" -f (Format-NullableValue $portfolio.summary.executionBundleStatus)) + Write-Host (" execPlan : {0}" -f (Format-NullableValue $portfolio.summary.executionBundlePlaneBinding)) + Write-Host (" execPrem : {0}" -f (Format-NullableValue $portfolio.summary.executionBundlePremiumSaganMode)) + Write-Host (" execLink : {0}" -f (Format-NullableValue $portfolio.summary.executionBundleReciprocalLinkReady)) + Write-Host (" execRate : {0}" -f (Format-NullableValue $portfolio.summary.executionBundleEffectiveBillableRateUsdPerHour)) + } if ($env:GITHUB_STEP_SUMMARY) { $portfolioLines = @( '### Governor Portfolio', @@ -1596,6 +1776,21 @@ try { ('- Template monitoring: {0}' -f (Format-NullableValue $portfolio.summary.templateMonitoringStatus)), ('- Supported proof: {0}' -f (Format-NullableValue $portfolio.summary.supportedProofStatus)) ) + if ($portfolio.summary.PSObject.Properties['viHistoryDistributorDependencyStatus']) { + $portfolioLines += ('- VI-history dependency: {0}' -f (Format-NullableValue $portfolio.summary.viHistoryDistributorDependencyStatus)) + if ($portfolio.summary.PSObject.Properties['viHistoryDistributorDependencyTargetRepository'] -and $portfolio.summary.viHistoryDistributorDependencyTargetRepository) { + $portfolioLines += ('- VI-history target: {0}' -f (Format-NullableValue $portfolio.summary.viHistoryDistributorDependencyTargetRepository)) + } + if ($portfolio.summary.PSObject.Properties['viHistoryDistributorDependencyExternalBlocker'] -and $portfolio.summary.viHistoryDistributorDependencyExternalBlocker) { + $portfolioLines += ('- VI-history blocker: {0}' -f (Format-NullableValue $portfolio.summary.viHistoryDistributorDependencyExternalBlocker)) + } + if ($portfolio.summary.PSObject.Properties['viHistoryDistributorDependencyPublishedBundleState'] -and $portfolio.summary.viHistoryDistributorDependencyPublishedBundleState) { + $portfolioLines += ('- VI-history published bundle: {0}' -f (Format-NullableValue $portfolio.summary.viHistoryDistributorDependencyPublishedBundleState)) + } + if ($portfolio.summary.PSObject.Properties['viHistoryDistributorDependencyPublishedBundleReleaseTag'] -and $portfolio.summary.viHistoryDistributorDependencyPublishedBundleReleaseTag) { + $portfolioLines += ('- VI-history published bundle tag: {0}' -f (Format-NullableValue $portfolio.summary.viHistoryDistributorDependencyPublishedBundleReleaseTag)) + } + } if ($portfolio.summary.nextOwnerRepository) { $portfolioLines += ('- Next owner: {0}' -f (Format-NullableValue $portfolio.summary.nextOwnerRepository)) } @@ -1607,6 +1802,39 @@ try { $portfolioLines += ('- Queue source: {0}' -f (Format-NullableValue $portfolio.summary.queueAuthoritySource)) } } + if ($portfolio.summary.PSObject.Properties['executionTopologyStatus'] -and $portfolio.summary.executionTopologyStatus) { + $portfolioLines += ('- Execution topology: {0}' -f (Format-NullableValue $portfolio.summary.executionTopologyStatus)) + $portfolioLines += ('- Execution provider: {0}' -f (Format-NullableValue $portfolio.summary.executionTopologyProviderId)) + $portfolioLines += ('- Execution worker slot: {0}' -f (Format-NullableValue $portfolio.summary.executionTopologyWorkerSlotId)) + $portfolioLines += ('- Execution logical lanes active/seeded: {0}/{1}' -f + (Format-NullableValue $portfolio.summary.executionTopologyActiveLogicalLaneCount), + (Format-NullableValue $portfolio.summary.executionTopologySeededLogicalLaneCount)) + if ($portfolio.summary.PSObject.Properties['executionTopologyRuntimeSurface'] -and $portfolio.summary.executionTopologyRuntimeSurface) { + $portfolioLines += ('- Execution runtime surface: {0}' -f (Format-NullableValue $portfolio.summary.executionTopologyRuntimeSurface)) + } + if ($portfolio.summary.PSObject.Properties['executionTopologyProcessModelClass'] -and $portfolio.summary.executionTopologyProcessModelClass) { + $portfolioLines += ('- Execution process model: {0}' -f (Format-NullableValue $portfolio.summary.executionTopologyProcessModelClass)) + } + if ($portfolio.summary.PSObject.Properties['executionTopologyRequestedSimultaneous'] -and $portfolio.summary.executionTopologyRequestedSimultaneous) { + $portfolioLines += ('- Execution simultaneous: {0}' -f (Format-NullableValue $portfolio.summary.executionTopologyRequestedSimultaneous)) + } + if ($portfolio.summary.PSObject.Properties['executionTopologyCellClass'] -and $portfolio.summary.executionTopologyCellClass) { + $portfolioLines += ('- Execution cell class: {0}' -f (Format-NullableValue $portfolio.summary.executionTopologyCellClass)) + } + if ($portfolio.summary.PSObject.Properties['executionTopologySuiteClass'] -and $portfolio.summary.executionTopologySuiteClass) { + $portfolioLines += ('- Execution suite class: {0}' -f (Format-NullableValue $portfolio.summary.executionTopologySuiteClass)) + } + if ($portfolio.summary.PSObject.Properties['executionTopologyOperatorAuthorizationRef'] -and $portfolio.summary.executionTopologyOperatorAuthorizationRef) { + $portfolioLines += ('- Execution operator authorization: {0}' -f (Format-NullableValue $portfolio.summary.executionTopologyOperatorAuthorizationRef)) + } + } + if ($portfolio.summary.PSObject.Properties['executionBundleStatus'] -and $portfolio.summary.executionBundleStatus) { + $portfolioLines += ('- Execution bundle: {0}' -f (Format-NullableValue $portfolio.summary.executionBundleStatus)) + $portfolioLines += ('- Execution plane: {0}' -f (Format-NullableValue $portfolio.summary.executionBundlePlaneBinding)) + $portfolioLines += ('- Premium Sagan mode: {0}' -f (Format-NullableValue $portfolio.summary.executionBundlePremiumSaganMode)) + $portfolioLines += ('- Execution reciprocal link: {0}' -f (Format-NullableValue $portfolio.summary.executionBundleReciprocalLinkReady)) + $portfolioLines += ('- Execution effective rate USD/hr: {0}' -f (Format-NullableValue $portfolio.summary.executionBundleEffectiveBillableRateUsdPerHour)) + } ($portfolioLines -join "`n") | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 } } @@ -1614,6 +1842,49 @@ try { Write-Warning ("Failed to display governor portfolio summary: {0}" -f $_.Exception.Message) } +try { + $contextConcentratorPath = Join-Path $ResultsRoot '_agent/handoff/sagan-context-concentrator.json' + if (Test-Path -LiteralPath $contextConcentratorPath -PathType Leaf) { + $concentrator = Get-Content -LiteralPath $contextConcentratorPath -Raw | ConvertFrom-Json -ErrorAction Stop + Write-Host '' + Write-Host '[Context Concentrator]' -ForegroundColor Cyan + Write-Host (" status : {0}" -f (Format-NullableValue $concentrator.summary.concentrationStatus)) + if ($concentrator.summary.activeIssueNumber) { + Write-Host (" issue : #{0}" -f (Format-NullableValue $concentrator.summary.activeIssueNumber)) + } + Write-Host (" owner : {0}" -f (Format-NullableValue $concentrator.summary.currentOwnerRepository)) + Write-Host (" next : {0}" -f (Format-NullableValue $concentrator.summary.nextAction)) + Write-Host (" hot/warm : {0}/{1}" -f (Format-NullableValue $concentrator.summary.hotWorkingSetCount), (Format-NullableValue $concentrator.summary.warmMemoryCount)) + Write-Host (" archive : {0}" -f (Format-NullableValue $concentrator.summary.archiveCount)) + Write-Host (" blockers : {0}" -f (Format-NullableValue $concentrator.summary.blockerCount)) + Write-Host (' spend : ${0}' -f (Format-NullableValue $concentrator.summary.blendedLowerBoundUsd)) + foreach ($entry in @($concentrator.memory.hotWorkingSet | Select-Object -First 3)) { + Write-Host (" - {0} [{1}]" -f (Format-NullableValue $entry.label), (Format-NullableValue $entry.status)) + } + if ($env:GITHUB_STEP_SUMMARY) { + $activeIssueLabel = if ($concentrator.summary.activeIssueNumber) { + "#$($concentrator.summary.activeIssueNumber)" + } else { + 'n/a' + } + $contextLines = @( + '### Context Concentrator', + '', + ('- Status: {0}' -f (Format-NullableValue $concentrator.summary.concentrationStatus)), + ('- Active issue: {0}' -f $activeIssueLabel), + ('- Current owner: {0}' -f (Format-NullableValue $concentrator.summary.currentOwnerRepository)), + ('- Next action: {0}' -f (Format-NullableValue $concentrator.summary.nextAction)), + ('- Hot/warm/archive: {0}/{1}/{2}' -f (Format-NullableValue $concentrator.summary.hotWorkingSetCount), (Format-NullableValue $concentrator.summary.warmMemoryCount), (Format-NullableValue $concentrator.summary.archiveCount)), + ('- Blockers: {0}' -f (Format-NullableValue $concentrator.summary.blockerCount)), + ('- Blended lower-bound spend: ${0}' -f (Format-NullableValue $concentrator.summary.blendedLowerBoundUsd)) + ) + ($contextLines -join "`n") | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + } + } +} catch { + Write-Warning ("Failed to display context concentrator summary: {0}" -f $_.Exception.Message) +} + try { $steeringPath = Join-Path $ResultsRoot '_agent/handoff/operator-steering-event.json' if (Test-Path -LiteralPath $steeringPath -PathType Leaf) { diff --git a/tools/Publish-CompareVIToolsArtifact.ps1 b/tools/Publish-CompareVIToolsArtifact.ps1 index 0e10c05f7..8cfba1556 100644 --- a/tools/Publish-CompareVIToolsArtifact.ps1 +++ b/tools/Publish-CompareVIToolsArtifact.ps1 @@ -336,6 +336,8 @@ try { ) } + $hostedNiLinuxDefaultImage = 'nationalinstruments/labview:2026q1-linux' + $consumerContractMetadata = [ordered]@{ capabilities = [ordered]@{ viHistory = [ordered]@{ @@ -362,6 +364,24 @@ try { 'The capability contract declares compare-vi-cli-action as the upstream producer; downstream repositories should distribute or consume the capability, not vendor the full backend control plane.' ) } + dockerProfile = [ordered]@{ + schema = 'comparevi-tools/docker-profile-capability@v1' + capabilityId = 'docker-profile' + displayName = 'Docker Profile' + distributionRole = 'upstream-producer' + distributionModel = 'release-bundle' + bundleMetadataPath = 'comparevi-tools-release.json' + bundleImportPath = 'tools/CompareVI.Tools/CompareVI.Tools.psd1' + releaseAssetPattern = 'CompareVI.Tools-v.zip' + authoritativeConsumerPinFieldPath = 'versionContract.authoritativeConsumerPin' + authoritativeConsumerPinKindFieldPath = 'versionContract.authoritativeConsumerPinKind' + authoritativeImageContractSource = 'consumerContract.dockerImageContract' + notes = @( + 'Use this capability record when a downstream distributor needs a Producer-published Docker image contract without inventing template-local image conventions.', + 'Resolve the immutable downstream pin from versionContract.authoritativeConsumerPin and then read the docker image contract from the authoritativeImageContractSource path in the same comparevi-tools-release.json payload.', + 'The docker-profile capability publishes contract metadata only; downstream repositories should consume Producer-owned image contract surfaces instead of vendoring compare runtime orchestration.' + ) + } } historyFacade = [ordered]@{ schema = 'comparevi-tools/history-facade@v1' @@ -481,7 +501,7 @@ try { 'tools/Compare-ExitCodeClassifier.ps1' ) captureFileName = 'ni-linux-container-capture.json' - defaultImage = 'nationalinstruments/labview:2026q1-linux' + defaultImage = $hostedNiLinuxDefaultImage notes = @( 'Hosted Linux consumers can resolve the runner from COMPAREVI_SCRIPTS_ROOT without a full backend checkout.', 'Keep the entry script and support scripts adjacent inside the extracted bundle so runtime guard and exit-code classification remain available.', @@ -489,6 +509,23 @@ try { 'The first Windows mirror proof slice also ships `Test-WindowsNI2026q1HostPreflight.ps1` and `Run-NIWindowsContainerCompare.ps1` so Windows-host consumers can validate the pinned headless NI image without a full backend checkout.' ) } + dockerImageContract = [ordered]@{ + schema = 'comparevi-tools/docker-image-contract@v1' + schemaUrl = 'https://labview-community-ci-cd.github.io/compare-vi-cli-action/schemas/comparevi-tools-docker-image-contract-v1.schema.json' + images = [ordered]@{ + hostedNiLinuxRunner = [ordered]@{ + imageRef = $hostedNiLinuxDefaultImage + consumerRole = 'hosted-ni-linux-runner' + notes = @( + 'Use this image reference when a downstream Docker-profile consumer needs the authoritative Producer-published Linux container image for hosted NI compare execution.' + ) + } + } + notes = @( + 'Treat this object as the authoritative Producer-published image contract for downstream Docker-profile consumers.', + 'Resolve it from consumerContract.capabilities.dockerProfile.authoritativeImageContractSource under the same immutable comparevi-tools-release.json payload.' + ) + } } $bundleMetadata = [ordered]@{ diff --git a/tools/Run-DX.ps1 b/tools/Run-DX.ps1 index 82d1eaae6..c9c2fc780 100644 --- a/tools/Run-DX.ps1 +++ b/tools/Run-DX.ps1 @@ -15,6 +15,8 @@ param( [string]$BaseVi, [string]$HeadVi, [string]$LabVIEWExePath, + [string]$LabVIEW64ExePath, + [string]$LabVIEW32ExePath, [string]$LVComparePath, [string]$OutputRoot = 'tests/results/teststand-session', [string[]]$Flags, @@ -23,6 +25,14 @@ param( [string]$NoiseProfile = 'full', [ValidateSet('detect','spawn','skip')] [string]$Warmup = 'detect', + [ValidateSet('single-compare','dual-plane-parity')] + [string]$TestStandSuiteClass = 'single-compare', + [string]$AgentId, + [string]$AgentClass, + [string]$ExecutionCellLeasePath, + [string]$ExecutionCellId, + [string]$ExecutionCellLeaseId, + [string]$HarnessInstanceId, [switch]$RenderReport, [switch]$CloseLabVIEW, [switch]$CloseLVCompare, @@ -52,6 +62,24 @@ function Write-DxLine([string]$msg,[string]$kind='info'){ Write-Host ("[dx] {0} {1}" -f $kind,$msg) } +function Get-SessionBoolValue($Source, [string]$PropertyName, [bool]$Default = $false) { + try { + if ($Source -and $Source.PSObject.Properties.Name -contains $PropertyName) { + return [bool]$Source.$PropertyName + } + } catch {} + return $Default +} + +function Get-SessionValue($Source, [string]$PropertyName, $Default = $null) { + try { + if ($Source -and $Source.PSObject.Properties.Name -contains $PropertyName) { + return $Source.$PropertyName + } + } catch {} + return $Default +} + # Apply DX toggles if (-not $env:DX_CONSOLE_LEVEL) { $env:DX_CONSOLE_LEVEL = 'concise' } if (-not $env:DX_CONSOLE_PREFERRED) { $env:DX_CONSOLE_PREFERRED = '1' } @@ -144,7 +172,16 @@ $harness = Join-Path $repoRoot 'tools/TestStand-CompareHarness.ps1' if ($sameNameCollision) { $hParams.SameNameHint = $true } if ($LabVIEWExePath) { $hParams.LabVIEWExePath = $LabVIEWExePath } + if ($LabVIEW64ExePath) { $hParams.LabVIEW64ExePath = $LabVIEW64ExePath } + if ($LabVIEW32ExePath) { $hParams.LabVIEW32ExePath = $LabVIEW32ExePath } if ($LVComparePath) { $hParams.LVComparePath = $LVComparePath } + $hParams.SuiteClass = $TestStandSuiteClass + if ($AgentId) { $hParams.AgentId = $AgentId } + if ($AgentClass) { $hParams.AgentClass = $AgentClass } + if ($ExecutionCellLeasePath) { $hParams.ExecutionCellLeasePath = $ExecutionCellLeasePath } + if ($ExecutionCellId) { $hParams.ExecutionCellId = $ExecutionCellId } + if ($ExecutionCellLeaseId) { $hParams.ExecutionCellLeaseId = $ExecutionCellLeaseId } + if ($HarnessInstanceId) { $hParams.HarnessInstanceId = $HarnessInstanceId } if ($PSBoundParameters.ContainsKey('Flags')) { $hParams.Flags = $Flags } if ($ReplaceFlags) { $hParams.ReplaceFlags = $true } if ($RenderReport) { $hParams.RenderReport = $true } @@ -201,11 +238,37 @@ $harness = Join-Path $repoRoot 'tools/TestStand-CompareHarness.ps1' } if ($session) { try { + $requestedSimultaneous = $false + if ($session.PSObject.Properties.Name -contains 'requestedSimultaneous') { + $requestedSimultaneous = [bool]$session.requestedSimultaneous + } elseif ($session.processModel -and $session.processModel.PSObject.Properties.Name -contains 'processModelClass') { + $requestedSimultaneous = ($session.processModel.processModelClass -eq 'parallel-process-model') + } $statusEnvelope.session = @{ - outcome = $session.outcome - error = $session.error - compare = $session.compare - content = $session.content + suiteClass = Get-SessionValue $session 'suiteClass' + primaryPlane = Get-SessionValue $session 'primaryPlane' + requestedSimultaneous = $requestedSimultaneous + outcome = Get-SessionValue $session 'outcome' + error = Get-SessionValue $session 'error' + executionCell = Get-SessionValue $session 'executionCell' + harnessInstance = Get-SessionValue $session 'harnessInstance' + processModel = Get-SessionValue $session 'processModel' + compare = Get-SessionValue $session 'compare' + content = Get-SessionValue $session 'content' + parity = Get-SessionValue $session 'parity' + planes = Get-SessionValue $session 'planes' + } + $statusEnvelope.executionTopology = @{ + suiteClass = if ($session.PSObject.Properties.Name -contains 'suiteClass') { $session.suiteClass } elseif (Get-SessionValue $session 'executionCell') { (Get-SessionValue (Get-SessionValue $session 'executionCell') 'suiteClass') } else { $null } + runtimeSurface = Get-SessionValue (Get-SessionValue $session 'processModel') 'runtimeSurface' + processModelClass = Get-SessionValue (Get-SessionValue $session 'processModel') 'processModelClass' + requestedSimultaneous = $requestedSimultaneous + cellClass = Get-SessionValue (Get-SessionValue $session 'executionCell') 'cellClass' + operatorAuthorizationRef = Get-SessionValue (Get-SessionValue $session 'executionCell') 'operatorAuthorizationRef' + premiumSaganMode = if (Get-SessionValue $session 'executionCell') { Get-SessionBoolValue (Get-SessionValue $session 'executionCell') 'premiumSaganMode' } else { $false } + harnessKind = Get-SessionValue (Get-SessionValue $session 'harnessInstance') 'harnessKind' + executionCellId = Get-SessionValue (Get-SessionValue $session 'executionCell') 'cellId' + executionCellLeaseId = Get-SessionValue (Get-SessionValue $session 'executionCell') 'leaseId' } } catch {} } diff --git a/tools/Test-AgentHandoffEntryPoint.ps1 b/tools/Test-AgentHandoffEntryPoint.ps1 index 92cca6b9a..78e772c6f 100644 --- a/tools/Test-AgentHandoffEntryPoint.ps1 +++ b/tools/Test-AgentHandoffEntryPoint.ps1 @@ -37,6 +37,7 @@ $requiredArtifacts = @( 'tests/results/_agent/handoff/continuity-summary.json', 'tests/results/_agent/handoff/entrypoint-status.json', 'tests/results/_agent/handoff/monitoring-mode.json', + 'tests/results/_agent/handoff/sagan-context-concentrator.json', 'tests/results/_agent/handoff/autonomous-governor-portfolio-summary.json', 'tests/results/_agent/handoff/*.json', 'tests/results/_agent/sessions/*.json' @@ -51,6 +52,7 @@ $commandCatalog = [ordered]@{ monitoringMode = 'node tools/npm/run-script.mjs priority:monitoring:mode' governorSummary = 'node tools/npm/run-script.mjs priority:governor:summary' governorPortfolio = 'node tools/npm/run-script.mjs priority:governor:portfolio' + contextConcentrator = 'node tools/npm/run-script.mjs priority:context:concentrate' } $artifactCatalog = [ordered]@{ @@ -62,6 +64,7 @@ $artifactCatalog = [ordered]@{ entrypointStatus = 'tests/results/_agent/handoff/entrypoint-status.json' monitoringMode = 'tests/results/_agent/handoff/monitoring-mode.json' autonomousGovernorSummary = 'tests/results/_agent/handoff/autonomous-governor-summary.json' + saganContextConcentrator = 'tests/results/_agent/handoff/sagan-context-concentrator.json' autonomousGovernorPortfolioSummary = 'tests/results/_agent/handoff/autonomous-governor-portfolio-summary.json' handoffGlob = 'tests/results/_agent/handoff/*.json' sessionGlob = 'tests/results/_agent/sessions/*.json' diff --git a/tools/TestStand-CompareHarness.ps1 b/tools/TestStand-CompareHarness.ps1 index a7d6e6102..f889668ca 100644 --- a/tools/TestStand-CompareHarness.ps1 +++ b/tools/TestStand-CompareHarness.ps1 @@ -7,44 +7,8 @@ Invoke-LVCompare.ps1 to perform a deterministic compare, and finally optional close helpers. Writes a session-index.json with pointers to emitted crumbs and artifacts. -.PARAMETER BaseVi - Base VI path. - -.PARAMETER HeadVi - Head VI path. - -.PARAMETER LabVIEWExePath - Path to LabVIEW.exe (pinned version/bitness recommended). - -.PARAMETER LVCompareExePath - Path to LVCompare.exe (defaults to canonical install when omitted). - -.PARAMETER OutputRoot - Root folder for all outputs (default tests/results/teststand-session). - -.PARAMETER Warmup - Controls LabVIEW warmup behaviour. `detect` (default) warms up when the helper - script is available, `spawn` forces a fresh warmup cycle (StopAfterWarmup), - and `skip` bypasses warmup entirely. - -.PARAMETER RenderReport - Generate compare-report.html during compare. - -.PARAMETER Flags - Additional LVCompare flags forwarded to Invoke-LVCompare.ps1. - -.PARAMETER ReplaceFlags - Replace the default LVCompare flags with the provided -Flags values. - -.PARAMETER NoiseProfile - Selects which LVCompare ignore bundle to apply when -ReplaceFlags is omitted. - Defaults to 'full' for complete compare detail; pass 'legacy' to restore the historical suppression bundle. - -.PARAMETER CloseLabVIEW - Attempt graceful LabVIEW close via tools/Close-LabVIEW.ps1 at the end. - -.PARAMETER CloseLVCompare - Attempt LVCompare cleanup via tools/Close-LVCompare.ps1 at the end. + Revision 2 adds an opt-in dual-plane native parity suite for LabVIEW 2026 x64/x32. + The legacy single-plane session contract remains the default. #> [CmdletBinding()] param( @@ -52,6 +16,8 @@ param( [Parameter(Mandatory)][string]$HeadVi, [Alias('LabVIEWPath')] [string]$LabVIEWExePath, +[ValidateSet('32','64')] +[string]$LabVIEWBitness = '64', [Alias('LVCompareExePath')] [string]$LVComparePath, [string]$OutputRoot = 'tests/results/teststand-session', @@ -68,7 +34,22 @@ param( [switch]$DisableTimeout, [string]$StagingRoot, [switch]$SameNameHint, -[switch]$AllowSameLeaf +[switch]$AllowSameLeaf, +[ValidateSet('single-compare','dual-plane-parity')] +[string]$SuiteClass = 'single-compare', +[string]$LabVIEW64ExePath, +[string]$LabVIEW32ExePath, +[switch]$InternalSinglePlane, +[ValidateSet('x64','x32')] +[string]$InternalPlaneKey, +[string]$AgentId = $env:CODEX_AGENT_ID, +[string]$AgentClass = $env:CODEX_AGENT_CLASS, +[string]$ExecutionCellLeasePath = $env:TESTSTAND_EXECUTION_CELL_LEASE_PATH, +[string]$ExecutionCellId = $env:TESTSTAND_EXECUTION_CELL_ID, +[string]$ExecutionCellLeaseId = $env:TESTSTAND_EXECUTION_CELL_LEASE_ID, +[string]$ExecutionCellSuiteClass = $env:TESTSTAND_EXECUTION_CELL_SUITE_CLASS, +[string]$HarnessInstanceId = $env:TESTSTAND_HARNESS_INSTANCE_ID, +[string]$ParentHarnessInstanceId = $env:TESTSTAND_PARENT_HARNESS_INSTANCE_ID ) Set-StrictMode -Version Latest @@ -78,6 +59,35 @@ try { Import-Module ThreadJob -ErrorAction SilentlyContinue } catch {} function New-Dir([string]$p){ if (-not (Test-Path $p)) { New-Item -ItemType Directory -Path $p -Force | Out-Null } } +function Resolve-AbsolutePath { + param( + [Parameter(Mandatory)][string]$RepoRoot, + [Parameter(Mandatory)][string]$Candidate + ) + + if ([System.IO.Path]::IsPathRooted($Candidate)) { + return $Candidate + } + + return (Join-Path $RepoRoot $Candidate) +} + +function Resolve-LabVIEW2026Path { + param([ValidateSet('32','64')][string]$Bitness) + + $root = if ($Bitness -eq '32') { ${env:ProgramFiles(x86)} } else { ${env:ProgramFiles} } + if ([string]::IsNullOrWhiteSpace($root)) { + return $null + } + return (Join-Path $root 'National Instruments\LabVIEW 2026\LabVIEW.exe') +} + +function Convert-ToArchitectureLabel { + param([ValidateSet('32','64')][string]$Bitness) + if ($Bitness -eq '32') { return '32-bit' } + return '64-bit' +} + function Invoke-WithTimeout { param( [scriptblock]$Block, @@ -110,172 +120,776 @@ function Invoke-WithTimeout { } } -$repo = (Resolve-Path '.').Path +function New-SessionOutcome { + param([AllowNull()]$Capture) -# Resolve OutputRoot to absolute path for deterministic writes -if (-not ([System.IO.Path]::IsPathRooted($OutputRoot))) { - $OutputRoot = Join-Path $repo $OutputRoot -} + if ($null -eq $Capture) { + return $null + } -$paths = [ordered]@{ - warmupDir = Join-Path $OutputRoot '_warmup' - compareDir = Join-Path $OutputRoot 'compare' + return [ordered]@{ + exitCode = [int]$Capture.exitCode + seconds = [double]$Capture.seconds + command = $Capture.command + diff = [bool]($Capture.exitCode -eq 1) + } } -New-Dir $paths.warmupDir -New-Dir $paths.compareDir - -$baseLeaf = Split-Path -Path $BaseVi -Leaf -$headLeaf = Split-Path -Path $HeadVi -Leaf -$sameName = [string]::Equals($baseLeaf, $headLeaf, [System.StringComparison]::OrdinalIgnoreCase) -$baseResolved = (Resolve-Path -LiteralPath $BaseVi -ErrorAction Stop).Path -$headResolved = (Resolve-Path -LiteralPath $HeadVi -ErrorAction Stop).Path -if ($baseResolved -ne $headResolved) { - $baseResolvedLeaf = Split-Path -Path $baseResolved -Leaf - $headResolvedLeaf = Split-Path -Path $headResolved -Leaf - if ([string]::Equals($baseResolvedLeaf, $headResolvedLeaf, [System.StringComparison]::OrdinalIgnoreCase) -and -not $AllowSameLeaf.IsPresent) { - throw ("LVCompare limitation: staged inputs must have distinct filenames. Received '{0}' and '{1}'." -f $BaseVi, $HeadVi) + +function Read-JsonFileIfPresent { + param([AllowNull()][string]$Path) + + if ([string]::IsNullOrWhiteSpace($Path)) { + return $null + } + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + return $null + } + + try { + return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -Depth 20 + } catch { + return $null } } -if ($SameNameHint.IsPresent) { - $sameName = $true + +function Get-FirstNonEmptyText { + param([AllowNull()][object[]]$Values) + + foreach ($value in @($Values)) { + if ($null -eq $value) { + continue + } + $text = [string]$value + if (-not [string]::IsNullOrWhiteSpace($text)) { + return $text + } + } + + return $null } -$rawPolicy = $env:LVCI_COMPARE_POLICY -$policy = if ([string]::IsNullOrWhiteSpace($rawPolicy)) { 'cli-only' } else { $rawPolicy } -$rawMode = $env:LVCI_COMPARE_MODE -$mode = if ([string]::IsNullOrWhiteSpace($rawMode)) { 'labview-cli' } else { $rawMode } -$autoCli = $false -if ($sameName -and $policy -ne 'lv-only') { - $autoCli = $true - if ($Warmup -ne 'skip') { - Write-Host "Harness: skipping warmup for same-name VIs (CLI path auto-selected)." -ForegroundColor Gray - $Warmup = 'skip' + +function Resolve-TestStandExecutionCellContext { + param( + [AllowNull()][string]$ExecutionCellLeasePath, + [AllowNull()][string]$ExecutionCellId, + [AllowNull()][string]$ExecutionCellLeaseId, + [AllowNull()][string]$ExecutionCellSuiteClass, + [AllowNull()][string]$HarnessInstanceId, + [AllowNull()][string]$ParentHarnessInstanceId, + [AllowNull()][string]$AgentId, + [AllowNull()][string]$AgentClass, + [AllowNull()][string]$SuiteClass, + [AllowNull()][string]$PlaneName, + [AllowNull()][string]$Role, + [Parameter(Mandatory)][string]$OutputRoot + ) + + $resolvedLeasePath = if ([string]::IsNullOrWhiteSpace($ExecutionCellLeasePath)) { + $null + } else { + $resolvedLease = Resolve-Path -LiteralPath $ExecutionCellLeasePath -ErrorAction SilentlyContinue + if ($resolvedLease) { $resolvedLease.Path } else { $ExecutionCellLeasePath } + } + $lease = Read-JsonFileIfPresent -Path $resolvedLeasePath + $leaseRequest = if ($lease -and $lease.PSObject.Properties['request']) { $lease.request } else { $null } + $leaseGrant = if ($lease -and $lease.PSObject.Properties['grant']) { $lease.grant } else { $null } + $leaseCommit = if ($lease -and $lease.PSObject.Properties['commit']) { $lease.commit } else { $null } + $leaseHost = if ($lease -and $lease.PSObject.Properties['host']) { $lease.host } else { $null } + $leaseCellId = if ($lease -and $lease.PSObject.Properties['cellId']) { $lease.cellId } else { $null } + $leaseGrantLeaseId = if ($leaseGrant -and $leaseGrant.PSObject.Properties['leaseId']) { $leaseGrant.leaseId } else { $null } + $leaseRequestAgentId = if ($leaseRequest -and $leaseRequest.PSObject.Properties['agentId']) { $leaseRequest.agentId } else { $null } + $leaseRequestAgentClass = if ($leaseRequest -and $leaseRequest.PSObject.Properties['agentClass']) { $leaseRequest.agentClass } else { $null } + $leaseRequestCellClass = if ($leaseRequest -and $leaseRequest.PSObject.Properties['cellClass']) { $leaseRequest.cellClass } else { $null } + $leaseRequestSuiteClass = if ($leaseRequest -and $leaseRequest.PSObject.Properties['suiteClass']) { $leaseRequest.suiteClass } else { $null } + $leaseRequestPlaneBinding = if ($leaseRequest -and $leaseRequest.PSObject.Properties['planeBinding']) { $leaseRequest.planeBinding } else { $null } + $leaseRequestWorkingRoot = if ($leaseRequest -and $leaseRequest.PSObject.Properties['workingRoot']) { $leaseRequest.workingRoot } else { $null } + $leaseRequestArtifactRoot = if ($leaseRequest -and $leaseRequest.PSObject.Properties['artifactRoot']) { $leaseRequest.artifactRoot } else { $null } + $leaseRequestHarnessKind = if ($leaseRequest -and $leaseRequest.PSObject.Properties['harnessKind']) { $leaseRequest.harnessKind } else { $null } + $leaseRequestOperatorAuthorizationRef = if ($leaseRequest -and $leaseRequest.PSObject.Properties['operatorAuthorizationRef']) { $leaseRequest.operatorAuthorizationRef } else { $null } + $leaseCommitWorkingRoot = if ($leaseCommit -and $leaseCommit.PSObject.Properties['workingRoot']) { $leaseCommit.workingRoot } else { $null } + $leaseCommitArtifactRoot = if ($leaseCommit -and $leaseCommit.PSObject.Properties['artifactRoot']) { $leaseCommit.artifactRoot } else { $null } + $leaseHostIsolatedLaneGroupId = if ($leaseHost -and $leaseHost.PSObject.Properties['isolatedLaneGroupId']) { $leaseHost.isolatedLaneGroupId } else { $null } + $leaseHostFingerprintSha256 = if ($leaseHost -and $leaseHost.PSObject.Properties['fingerprintSha256']) { $leaseHost.fingerprintSha256 } else { $null } + $leaseGrantPremiumSaganMode = if ($leaseGrant -and $leaseGrant.PSObject.Properties['premiumSaganMode']) { $leaseGrant.premiumSaganMode } else { $null } + + $resolvedCellId = Get-FirstNonEmptyText @($ExecutionCellId, $leaseCellId) + $resolvedLeaseId = Get-FirstNonEmptyText @($ExecutionCellLeaseId, $leaseGrantLeaseId) + $resolvedAgentId = Get-FirstNonEmptyText @($AgentId, $leaseRequestAgentId) + $resolvedAgentClass = (Get-FirstNonEmptyText @($AgentClass, $leaseRequestAgentClass)) + if ([string]::IsNullOrWhiteSpace($resolvedAgentClass)) { + $resolvedAgentClass = 'subagent' + } + $resolvedSuiteClass = Get-FirstNonEmptyText @($ExecutionCellSuiteClass, $leaseRequestSuiteClass, $SuiteClass) + $resolvedPlaneBinding = if ([string]::IsNullOrWhiteSpace($PlaneName)) { + Get-FirstNonEmptyText @($leaseRequestPlaneBinding, $(if ($resolvedSuiteClass -eq 'dual-plane-parity') { 'dual-plane-parity' } else { $null })) + } else { + $PlaneName + } + $resolvedWorkingRoot = Get-FirstNonEmptyText @($leaseCommitWorkingRoot, $leaseRequestWorkingRoot, $OutputRoot) + $resolvedArtifactRoot = Get-FirstNonEmptyText @($leaseCommitArtifactRoot, $leaseRequestArtifactRoot, $OutputRoot) + $resolvedHarnessKind = Get-FirstNonEmptyText @($leaseRequestHarnessKind, 'teststand-compare-harness') + $resolvedRole = Get-FirstNonEmptyText @($Role, $(if (-not [string]::IsNullOrWhiteSpace($ParentHarnessInstanceId)) { 'plane-child' } elseif ($resolvedSuiteClass -eq 'dual-plane-parity' -and [string]::IsNullOrWhiteSpace($PlaneName)) { 'coordinator' } else { 'single-plane' })) + $resolvedProcessModelClass = if ($resolvedSuiteClass -eq 'dual-plane-parity') { + 'parallel-process-model' + } else { + 'sequential-process-model' + } + + $planeSuffix = if ($PlaneName -match '2026-64$') { + 'x64' + } elseif ($PlaneName -match '2026-32$') { + 'x32' + } else { + 'plane' + } + $resolvedHarnessInstanceId = Get-FirstNonEmptyText @( + $HarnessInstanceId, + $(if (-not [string]::IsNullOrWhiteSpace($ParentHarnessInstanceId)) { '{0}-{1}' -f $ParentHarnessInstanceId, $planeSuffix } else { $null }), + $(if (-not [string]::IsNullOrWhiteSpace($resolvedCellId)) { '{0}-{1}' -f $resolvedHarnessKind, $resolvedCellId } else { $null }), + $(if (-not [string]::IsNullOrWhiteSpace($PlaneName)) { '{0}-{1}' -f $resolvedHarnessKind, $planeSuffix } else { $resolvedHarnessKind }) + ) + + $executionCell = $null + if (-not [string]::IsNullOrWhiteSpace($resolvedCellId) -or -not [string]::IsNullOrWhiteSpace($resolvedLeaseId) -or -not [string]::IsNullOrWhiteSpace($resolvedAgentId) -or -not [string]::IsNullOrWhiteSpace($resolvedLeasePath)) { + $executionCell = [ordered]@{ + cellId = $resolvedCellId + leaseId = $resolvedLeaseId + leasePath = $resolvedLeasePath + agentId = $resolvedAgentId + agentClass = $resolvedAgentClass + cellClass = Get-FirstNonEmptyText @($leaseRequestCellClass) + suiteClass = $resolvedSuiteClass + planeBinding = $resolvedPlaneBinding + runtimeSurface = 'windows-native-teststand' + premiumSaganMode = if ($null -eq $leaseGrantPremiumSaganMode) { $false } else { [bool]$leaseGrantPremiumSaganMode } + operatorAuthorizationRef = Get-FirstNonEmptyText @($leaseRequestOperatorAuthorizationRef) + workingRoot = $resolvedWorkingRoot + artifactRoot = $resolvedArtifactRoot + isolatedLaneGroupId = Get-FirstNonEmptyText @($leaseHostIsolatedLaneGroupId) + hostOsFingerprintSha256 = Get-FirstNonEmptyText @($leaseHostFingerprintSha256) + } + } + + $harnessInstance = [ordered]@{ + harnessKind = $resolvedHarnessKind + instanceId = $resolvedHarnessInstanceId + role = $resolvedRole + processModelClass = $resolvedProcessModelClass + planeBinding = $resolvedPlaneBinding + parentInstanceId = Get-FirstNonEmptyText @($ParentHarnessInstanceId) + } + + $processModel = [ordered]@{ + runtimeSurface = 'windows-native-teststand' + processModelClass = $resolvedProcessModelClass + windowsOnly = $true + rootHarnessInstanceId = Get-FirstNonEmptyText @($ParentHarnessInstanceId, $resolvedHarnessInstanceId) + planeCount = if ($resolvedSuiteClass -eq 'dual-plane-parity') { 2 } else { 1 } + } + + return [pscustomobject]@{ + executionCell = $executionCell + harnessInstance = $harnessInstance + processModel = $processModel } } -if ($policy -eq 'cli-only') { - if ($Warmup -ne 'skip') { - Write-Host "Harness: skipping warmup (headless CLI default policy)." -ForegroundColor Gray - $Warmup = 'skip' + +function Invoke-TestStandSinglePlaneSession { + param( + [Parameter(Mandatory)][string]$RepoRoot, + [Parameter(Mandatory)][string]$BaseVi, + [Parameter(Mandatory)][string]$HeadVi, + [AllowNull()][string]$LabVIEWExePath, + [ValidateSet('32','64')][string]$LabVIEWBitness = '64', + [AllowNull()][string]$LVComparePath, + [Parameter(Mandatory)][string]$OutputRoot, + [ValidateSet('detect','spawn','skip')][string]$Warmup, + [AllowNull()][string]$SuiteClass, + [AllowNull()][string[]]$Flags, + [bool]$ReplaceFlags, + [ValidateSet('full','legacy')][string]$NoiseProfile, + [bool]$RenderReport, + [bool]$CloseLabVIEW, + [bool]$CloseLVCompare, + [int]$TimeoutSeconds, + [bool]$DisableTimeout, + [AllowNull()][string]$StagingRoot, + [bool]$SameNameHint, + [bool]$AllowSameLeaf, + [AllowNull()][string]$PlaneName, + [AllowNull()][string]$AgentId, + [AllowNull()][string]$AgentClass, + [AllowNull()][string]$ExecutionCellLeasePath, + [AllowNull()][string]$ExecutionCellId, + [AllowNull()][string]$ExecutionCellLeaseId, + [AllowNull()][string]$ExecutionCellSuiteClass, + [AllowNull()][string]$HarnessInstanceId, + [AllowNull()][string]$ParentHarnessInstanceId, + [AllowNull()][string]$HarnessRole + ) + + $resolvedOutputRoot = Resolve-AbsolutePath -RepoRoot $RepoRoot -Candidate $OutputRoot + $cellLeaseContext = Resolve-TestStandExecutionCellContext -ExecutionCellLeasePath $ExecutionCellLeasePath -ExecutionCellId $ExecutionCellId -ExecutionCellLeaseId $ExecutionCellLeaseId -ExecutionCellSuiteClass $ExecutionCellSuiteClass -HarnessInstanceId $HarnessInstanceId -ParentHarnessInstanceId $ParentHarnessInstanceId -AgentId $AgentId -AgentClass $AgentClass -SuiteClass $SuiteClass -PlaneName $PlaneName -Role $HarnessRole -OutputRoot $resolvedOutputRoot + $paths = [ordered]@{ + warmupDir = Join-Path $resolvedOutputRoot '_warmup' + compareDir = Join-Path $resolvedOutputRoot 'compare' + } + New-Dir $paths.warmupDir + New-Dir $paths.compareDir + + $baseLeaf = Split-Path -Path $BaseVi -Leaf + $headLeaf = Split-Path -Path $HeadVi -Leaf + $sameName = [string]::Equals($baseLeaf, $headLeaf, [System.StringComparison]::OrdinalIgnoreCase) + $baseResolved = (Resolve-Path -LiteralPath $BaseVi -ErrorAction Stop).Path + $headResolved = (Resolve-Path -LiteralPath $HeadVi -ErrorAction Stop).Path + if ($baseResolved -ne $headResolved) { + $baseResolvedLeaf = Split-Path -Path $baseResolved -Leaf + $headResolvedLeaf = Split-Path -Path $headResolved -Leaf + if ([string]::Equals($baseResolvedLeaf, $headResolvedLeaf, [System.StringComparison]::OrdinalIgnoreCase) -and -not $AllowSameLeaf) { + throw ("LVCompare limitation: staged inputs must have distinct filenames. Received '{0}' and '{1}'." -f $BaseVi, $HeadVi) + } + } + if ($SameNameHint) { + $sameName = $true + } + + $rawPolicy = $env:LVCI_COMPARE_POLICY + $policy = if ([string]::IsNullOrWhiteSpace($rawPolicy)) { 'cli-only' } else { $rawPolicy } + $rawMode = $env:LVCI_COMPARE_MODE + $mode = if ([string]::IsNullOrWhiteSpace($rawMode)) { 'labview-cli' } else { $rawMode } + $autoCli = $false + $effectiveWarmup = $Warmup + if ($sameName -and $policy -ne 'lv-only') { + $autoCli = $true + if ($effectiveWarmup -ne 'skip') { + Write-Host "Harness: skipping warmup for same-name VIs (CLI path auto-selected)." -ForegroundColor Gray + $effectiveWarmup = 'skip' + } + } + if ($policy -eq 'cli-only') { + if ($effectiveWarmup -ne 'skip') { + Write-Host "Harness: skipping warmup (headless CLI default policy)." -ForegroundColor Gray + $effectiveWarmup = 'skip' + } + } + if ([string]::IsNullOrWhiteSpace($rawPolicy)) { + try { [System.Environment]::SetEnvironmentVariable('LVCI_COMPARE_POLICY', $policy, 'Process') } catch {} + } + if ([string]::IsNullOrWhiteSpace($rawMode)) { + try { [System.Environment]::SetEnvironmentVariable('LVCI_COMPARE_MODE', $mode, 'Process') } catch {} + } + + $warmupLog = Join-Path $paths.warmupDir 'labview-runtime.ndjson' + $compareLog = Join-Path $paths.compareDir 'compare-events.ndjson' + $capPath = Join-Path $paths.compareDir 'lvcompare-capture.json' + $reportPath = Join-Path $paths.compareDir 'compare-report.html' + $cap = $null + $warmupRan = $false + $err = $null + $closeLVCompareScript = Join-Path $RepoRoot 'tools' 'Close-LVCompare.ps1' + $closeLabVIEWScript = Join-Path $RepoRoot 'tools' 'Close-LabVIEW.ps1' + $effectiveTimeout = if ($DisableTimeout) { 0 } else { [Math]::Max(0, [int]$TimeoutSeconds) } + + try { + if ($effectiveWarmup -ne 'skip') { + $warmupScript = Join-Path $RepoRoot 'tools' 'Warmup-LabVIEWRuntime.ps1' + if (-not (Test-Path -LiteralPath $warmupScript)) { throw "Warmup-LabVIEWRuntime.ps1 not found at $warmupScript" } + $warmParams = @{ JsonLogPath = $warmupLog; SupportedBitness = $LabVIEWBitness } + if ($LabVIEWExePath) { $warmParams.LabVIEWPath = $LabVIEWExePath } + $warmupRunner = { + param($warmupScriptPath, $warmupParameters) + & $warmupScriptPath @warmupParameters | Out-Null + } + try { + Invoke-WithTimeout -Block $warmupRunner -TimeoutSeconds $effectiveTimeout -Stage 'warmup' -DisableTimeout:$DisableTimeout -ArgumentList @($warmupScript, $warmParams) | Out-Null + $warmupRan = $true + } catch { + $err = $_.Exception.Message + throw + } + } + + $invoke = Join-Path $RepoRoot 'tools' 'Invoke-LVCompare.ps1' + if (-not (Test-Path -LiteralPath $invoke)) { throw "Invoke-LVCompare.ps1 not found at $invoke" } + $invokeParams = @{ + BaseVi = $BaseVi + HeadVi = $HeadVi + OutputDir = $paths.compareDir + JsonLogPath = $compareLog + RenderReport = $RenderReport + NoiseProfile = $NoiseProfile + LabVIEWBitness = $LabVIEWBitness + } + if ($LabVIEWExePath) { $invokeParams.LabVIEWExePath = $LabVIEWExePath } + if ($LVComparePath) { $invokeParams.LVComparePath = $LVComparePath } + if ($Flags) { $invokeParams.Flags = $Flags } + if ($ReplaceFlags) { $invokeParams.ReplaceFlags = $true } + if ($AllowSameLeaf) { $invokeParams.AllowSameLeaf = $true } + $compareRunner = { + param($invokePath, $invokeParameters) + & $invokePath @invokeParameters | Out-Null + } + Invoke-WithTimeout -Block $compareRunner -TimeoutSeconds $effectiveTimeout -Stage 'compare' -DisableTimeout:$DisableTimeout -ArgumentList @($invoke, $invokeParams) | Out-Null + if (Test-Path -LiteralPath $capPath) { $cap = Get-Content -LiteralPath $capPath -Raw | ConvertFrom-Json } + } catch { + $err = $_.Exception.Message + } finally { + if ($CloseLVCompare -and (Test-Path -LiteralPath $closeLVCompareScript)) { + try { & $closeLVCompareScript | Out-Null } catch {} + } + if ($CloseLabVIEW -and (Test-Path -LiteralPath $closeLabVIEWScript)) { + try { + $closeParams = @{ SupportedBitness = $LabVIEWBitness } + if ($LabVIEWExePath) { $closeParams.LabVIEWExePath = $LabVIEWExePath } + & $closeLabVIEWScript @closeParams | Out-Null + } catch {} + } + } + + $reportExists = Test-Path -LiteralPath $reportPath -PathType Leaf + $warmupNode = [ordered]@{ + mode = $effectiveWarmup + events = if ($warmupRan) { $warmupLog } else { $null } } + $compareNode = [ordered]@{ + events = $compareLog + capture = $capPath + report = $reportExists + } + $compareNode.staging = [ordered]@{ + enabled = [bool]([string]::IsNullOrWhiteSpace($StagingRoot) -eq $false) + root = if ([string]::IsNullOrWhiteSpace($StagingRoot)) { $null } else { $StagingRoot } + } + $compareNode.allowSameLeaf = $AllowSameLeaf + if ($cap) { + if ($cap.PSObject.Properties['command']) { $compareNode.command = $cap.command } + if ($cap.PSObject.Properties['cliPath']) { $compareNode.cliPath = $cap.cliPath } + if ($cap.PSObject.Properties['environment']) { + $envNode = $cap.environment + if ($envNode -and $envNode.PSObject.Properties['cli']) { + $compareNode.cli = $envNode.cli + } + } + } + $compareNode.autoCli = $autoCli + $compareNode.sameName = $sameName + $compareNode.timeoutSeconds = $effectiveTimeout + if ($env:LVCI_COMPARE_POLICY) { $compareNode.policy = $env:LVCI_COMPARE_POLICY } + if ($env:LVCI_COMPARE_MODE) { $compareNode.mode = $env:LVCI_COMPARE_MODE } + + $planeRecord = [ordered]@{ + plane = if ([string]::IsNullOrWhiteSpace($PlaneName)) { $null } else { $PlaneName } + architecture = Convert-ToArchitectureLabel -Bitness $LabVIEWBitness + labviewExePath = if ([string]::IsNullOrWhiteSpace($LabVIEWExePath)) { $null } else { $LabVIEWExePath } + outputRoot = $resolvedOutputRoot + warmup = $warmupNode + compare = $compareNode + outcome = New-SessionOutcome -Capture $cap + error = $err + exitCode = if ($cap) { [int]$cap.exitCode } else { 1 } + executionCell = $cellLeaseContext.executionCell + harnessInstance = $cellLeaseContext.harnessInstance + processModel = $cellLeaseContext.processModel + } + + return [pscustomobject]$planeRecord } -if ([string]::IsNullOrWhiteSpace($rawPolicy)) { - try { [System.Environment]::SetEnvironmentVariable('LVCI_COMPARE_POLICY', $policy, 'Process') } catch {} + +function Write-TestStandV1SessionIndex { + param( + [Parameter(Mandatory)][string]$OutputRoot, + [Parameter(Mandatory)][object]$PlaneSession + ) + + $index = [ordered]@{ + schema = 'teststand-compare-session/v1' + at = (Get-Date).ToString('o') + warmup = $PlaneSession.warmup + compare = $PlaneSession.compare + outcome = $PlaneSession.outcome + error = $PlaneSession.error + executionCell = $PlaneSession.executionCell + harnessInstance = $PlaneSession.harnessInstance + processModel = $PlaneSession.processModel + } + + $indexPath = Join-Path $OutputRoot 'session-index.json' + New-Dir $OutputRoot + $index | ConvertTo-Json -Depth 8 | Out-File -LiteralPath $indexPath -Encoding utf8 } -if ([string]::IsNullOrWhiteSpace($rawMode)) { - try { [System.Environment]::SetEnvironmentVariable('LVCI_COMPARE_MODE', $mode, 'Process') } catch {} + +function Convert-ToParityComparableValue { + param($Value) + if ($null -eq $Value) { return $null } + return $Value } -$warmupLog = Join-Path $paths.warmupDir 'labview-runtime.ndjson' -$compareLog = Join-Path $paths.compareDir 'compare-events.ndjson' -$capPath = Join-Path $paths.compareDir 'lvcompare-capture.json' -$reportPath = Join-Path $paths.compareDir 'compare-report.html' -$cap = $null -$warmupRan = $false -$err = $null -$closeLVCompareScript = Join-Path $repo 'tools' 'Close-LVCompare.ps1' -$closeLabVIEWScript = Join-Path $repo 'tools' 'Close-LabVIEW.ps1' -$effectiveTimeout = if ($DisableTimeout) { 0 } else { [Math]::Max(0, [int]$TimeoutSeconds) } - -try { - # 1) Warmup LabVIEW runtime (optional) - if ($Warmup -ne 'skip') { - $warmupScript = Join-Path $repo 'tools' 'Warmup-LabVIEWRuntime.ps1' - if (-not (Test-Path -LiteralPath $warmupScript)) { throw "Warmup-LabVIEWRuntime.ps1 not found at $warmupScript" } - $warmParams = @{ JsonLogPath = $warmupLog } - if ($LabVIEWExePath) { $warmParams.LabVIEWPath = $LabVIEWExePath } - $warmupRunner = { - param($warmupScriptPath, $warmupParameters) - & $warmupScriptPath @warmupParameters | Out-Null +function New-DualPlaneParitySummary { + param( + [Parameter(Mandatory)][object]$X64Session, + [Parameter(Mandatory)][object]$X32Session + ) + + $mismatches = New-Object System.Collections.Generic.List[object] + $comparedFields = @('outcome.exitCode', 'outcome.diff', 'compare.report', 'compare.mode', 'compare.policy') + + foreach ($field in $comparedFields) { + $x64Value = switch ($field) { + 'outcome.exitCode' { Convert-ToParityComparableValue $X64Session.outcome.exitCode } + 'outcome.diff' { Convert-ToParityComparableValue $X64Session.outcome.diff } + 'compare.report' { Convert-ToParityComparableValue $X64Session.compare.report } + 'compare.mode' { Convert-ToParityComparableValue $X64Session.compare.mode } + 'compare.policy' { Convert-ToParityComparableValue $X64Session.compare.policy } + default { $null } } - try { - Invoke-WithTimeout -Block $warmupRunner -TimeoutSeconds $effectiveTimeout -Stage 'warmup' -DisableTimeout:$DisableTimeout -ArgumentList @($warmupScript, $warmParams) | Out-Null - $warmupRan = $true - } catch { - $err = $_.Exception.Message - throw + $x32Value = switch ($field) { + 'outcome.exitCode' { Convert-ToParityComparableValue $X32Session.outcome.exitCode } + 'outcome.diff' { Convert-ToParityComparableValue $X32Session.outcome.diff } + 'compare.report' { Convert-ToParityComparableValue $X32Session.compare.report } + 'compare.mode' { Convert-ToParityComparableValue $X32Session.compare.mode } + 'compare.policy' { Convert-ToParityComparableValue $X32Session.compare.policy } + default { $null } + } + + if ($x64Value -ne $x32Value) { + $mismatches.Add([ordered]@{ field = $field; x64 = $x64Value; x32 = $x32Value }) | Out-Null } } - # 2) Invoke LVCompare (deterministic) - $invoke = Join-Path $repo 'tools' 'Invoke-LVCompare.ps1' - if (-not (Test-Path -LiteralPath $invoke)) { throw "Invoke-LVCompare.ps1 not found at $invoke" } - $invokeParams = @{ - BaseVi = $BaseVi - HeadVi = $HeadVi - OutputDir = $paths.compareDir - JsonLogPath = $compareLog - RenderReport = $RenderReport.IsPresent - NoiseProfile = $NoiseProfile - } - if ($LabVIEWExePath) { $invokeParams.LabVIEWExePath = $LabVIEWExePath } - if ($LVComparePath) { $invokeParams.LVComparePath = $LVComparePath } - if ($Flags) { $invokeParams.Flags = $Flags } - if ($ReplaceFlags) { $invokeParams.ReplaceFlags = $true } - if ($AllowSameLeaf.IsPresent) { $invokeParams.AllowSameLeaf = $true } - $compareRunner = { - param($invokePath, $invokeParameters) - & $invokePath @invokeParameters | Out-Null - } - Invoke-WithTimeout -Block $compareRunner -TimeoutSeconds $effectiveTimeout -Stage 'compare' -DisableTimeout:$DisableTimeout -ArgumentList @($invoke, $invokeParams) | Out-Null - if (Test-Path -LiteralPath $capPath) { $cap = Get-Content -LiteralPath $capPath -Raw | ConvertFrom-Json } -} catch { $err = $_.Exception.Message } -finally { - if ($CloseLVCompare -and (Test-Path -LiteralPath $closeLVCompareScript)) { - try { & $closeLVCompareScript | Out-Null } catch {} - } - if ($CloseLabVIEW -and (Test-Path -LiteralPath $closeLabVIEWScript)) { - try { & $closeLabVIEWScript -MinimumSupportedLVVersion '2025' -SupportedBitness '64' | Out-Null } catch {} + $incomplete = ( + ($null -eq $X64Session.outcome) -or + ($null -eq $X32Session.outcome) -or + (-not [string]::IsNullOrWhiteSpace([string]$X64Session.error)) -or + (-not [string]::IsNullOrWhiteSpace([string]$X32Session.error)) + ) + + $status = if ($incomplete) { + 'incomplete' + } elseif ($mismatches.Count -gt 0) { + 'mismatch' + } else { + 'match' } -} -# 4) Session index (always write) -$reportExists = Test-Path -LiteralPath $reportPath -PathType Leaf -$warmupNode = [ordered]@{ - mode = $Warmup - events = if ($warmupRan) { $warmupLog } else { $null } + return [ordered]@{ + status = $status + comparedFields = $comparedFields + exitCodeParity = if ($null -eq $X64Session.outcome -or $null -eq $X32Session.outcome) { $null } else { [bool]($X64Session.outcome.exitCode -eq $X32Session.outcome.exitCode) } + diffParity = if ($null -eq $X64Session.outcome -or $null -eq $X32Session.outcome) { $null } else { [bool]($X64Session.outcome.diff -eq $X32Session.outcome.diff) } + mismatchCount = $mismatches.Count + mismatches = @($mismatches.ToArray()) + } } -$compareNode = [ordered]@{ - events = $compareLog - capture = $capPath - report = $reportExists + +function Wait-ForChildProcesses { + param( + [Parameter(Mandatory)][System.Diagnostics.Process[]]$Processes, + [int]$TimeoutSeconds = 0 + ) + + $deadline = if ($TimeoutSeconds -gt 0) { (Get-Date).AddSeconds($TimeoutSeconds) } else { $null } + while ($true) { + $active = @($Processes | Where-Object { -not $_.HasExited }) + if ($active.Count -eq 0) { + break + } + if ($deadline -and (Get-Date) -gt $deadline) { + foreach ($proc in $active) { + try { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } catch {} + } + throw (New-Object System.TimeoutException("Dual-plane parity suite exceeded ${TimeoutSeconds}s")) + } + Start-Sleep -Milliseconds 250 + foreach ($proc in $Processes) { + try { $null = $proc.Refresh() } catch {} + } + } } -$compareNode.staging = [ordered]@{ - enabled = [bool]([string]::IsNullOrWhiteSpace($StagingRoot) -eq $false) - root = if ([string]::IsNullOrWhiteSpace($StagingRoot)) { $null } else { $StagingRoot } + +function Start-DualPlaneChildProcess { + param( + [Parameter(Mandatory)][string]$ScriptPath, + [Parameter(Mandatory)][string]$PlaneKey, + [Parameter(Mandatory)][string]$BaseVi, + [Parameter(Mandatory)][string]$HeadVi, + [Parameter(Mandatory)][string]$OutputRoot, + [Parameter(Mandatory)][string]$LabVIEWExePath, + [Parameter(Mandatory)][ValidateSet('32','64')][string]$LabVIEWBitness, + [AllowNull()][string]$LVComparePath, + [ValidateSet('detect','spawn','skip')][string]$Warmup, + [AllowNull()][string[]]$Flags, + [bool]$ReplaceFlags, + [ValidateSet('full','legacy')][string]$NoiseProfile, + [bool]$RenderReport, + [bool]$CloseLabVIEW, + [bool]$CloseLVCompare, + [int]$TimeoutSeconds, + [bool]$DisableTimeout, + [AllowNull()][string]$StagingRoot, + [bool]$SameNameHint, + [bool]$AllowSameLeaf, + [AllowNull()][string]$AgentId, + [AllowNull()][string]$AgentClass, + [AllowNull()][string]$ExecutionCellLeasePath, + [AllowNull()][string]$ExecutionCellId, + [AllowNull()][string]$ExecutionCellLeaseId, + [AllowNull()][string]$ExecutionCellSuiteClass, + [AllowNull()][string]$ParentHarnessInstanceId + ) + + $pwsh = (Get-Command pwsh -ErrorAction Stop).Source + $args = New-Object System.Collections.Generic.List[string] + $args.Add('-NoLogo') | Out-Null + $args.Add('-NoProfile') | Out-Null + $args.Add('-File') | Out-Null + $args.Add($ScriptPath) | Out-Null + $args.Add('-BaseVi') | Out-Null + $args.Add($BaseVi) | Out-Null + $args.Add('-HeadVi') | Out-Null + $args.Add($HeadVi) | Out-Null + $args.Add('-OutputRoot') | Out-Null + $args.Add($OutputRoot) | Out-Null + $args.Add('-LabVIEWExePath') | Out-Null + $args.Add($LabVIEWExePath) | Out-Null + $args.Add('-LabVIEWBitness') | Out-Null + $args.Add($LabVIEWBitness) | Out-Null + $args.Add('-Warmup') | Out-Null + $args.Add($Warmup) | Out-Null + $args.Add('-NoiseProfile') | Out-Null + $args.Add($NoiseProfile) | Out-Null + $args.Add('-SuiteClass') | Out-Null + $args.Add('single-compare') | Out-Null + $args.Add('-InternalSinglePlane') | Out-Null + $args.Add('-InternalPlaneKey') | Out-Null + $args.Add($PlaneKey) | Out-Null + + if ($LVComparePath) { + $args.Add('-LVComparePath') | Out-Null + $args.Add($LVComparePath) | Out-Null + } + if ($Flags) { + foreach ($flag in $Flags) { + $args.Add('-Flags') | Out-Null + $args.Add($flag) | Out-Null + } + } + if ($ReplaceFlags) { $args.Add('-ReplaceFlags') | Out-Null } + if ($RenderReport) { $args.Add('-RenderReport') | Out-Null } + if ($CloseLabVIEW) { $args.Add('-CloseLabVIEW') | Out-Null } + if ($CloseLVCompare) { $args.Add('-CloseLVCompare') | Out-Null } + if ($DisableTimeout) { $args.Add('-DisableTimeout') | Out-Null } else { + $args.Add('-TimeoutSeconds') | Out-Null + $args.Add([string]$TimeoutSeconds) | Out-Null + } + if ($StagingRoot) { + $args.Add('-StagingRoot') | Out-Null + $args.Add($StagingRoot) | Out-Null + } + if ($AgentId) { + $args.Add('-AgentId') | Out-Null + $args.Add($AgentId) | Out-Null + } + if ($AgentClass) { + $args.Add('-AgentClass') | Out-Null + $args.Add($AgentClass) | Out-Null + } + if ($ExecutionCellLeasePath) { + $args.Add('-ExecutionCellLeasePath') | Out-Null + $args.Add($ExecutionCellLeasePath) | Out-Null + } + if ($ExecutionCellId) { + $args.Add('-ExecutionCellId') | Out-Null + $args.Add($ExecutionCellId) | Out-Null + } + if ($ExecutionCellLeaseId) { + $args.Add('-ExecutionCellLeaseId') | Out-Null + $args.Add($ExecutionCellLeaseId) | Out-Null + } + if ($ExecutionCellSuiteClass) { + $args.Add('-ExecutionCellSuiteClass') | Out-Null + $args.Add($ExecutionCellSuiteClass) | Out-Null + } + if ($ParentHarnessInstanceId) { + $args.Add('-ParentHarnessInstanceId') | Out-Null + $args.Add($ParentHarnessInstanceId) | Out-Null + } + if ($SameNameHint) { $args.Add('-SameNameHint') | Out-Null } + if ($AllowSameLeaf) { $args.Add('-AllowSameLeaf') | Out-Null } + + return Start-Process -FilePath $pwsh -ArgumentList @($args.ToArray()) -PassThru -WindowStyle Hidden } -$compareNode.allowSameLeaf = $AllowSameLeaf.IsPresent -if ($cap) { - if ($cap.PSObject.Properties['command']) { $compareNode.command = $cap.command } - if ($cap.PSObject.Properties['cliPath']) { $compareNode.cliPath = $cap.cliPath } - if ($cap.PSObject.Properties['environment']) { - $envNode = $cap.environment - if ($envNode -and $envNode.PSObject.Properties['cli']) { - $compareNode.cli = $envNode.cli + +function Invoke-DualPlaneParitySuite { + param( + [Parameter(Mandatory)][string]$RepoRoot, + [Parameter(Mandatory)][string]$ScriptPath, + [Parameter(Mandatory)][string]$BaseVi, + [Parameter(Mandatory)][string]$HeadVi, + [Parameter(Mandatory)][string]$OutputRoot, + [AllowNull()][string]$DefaultLabVIEWExePath, + [AllowNull()][string]$LabVIEW64ExePath, + [AllowNull()][string]$LabVIEW32ExePath, + [AllowNull()][string]$LVComparePath, + [ValidateSet('detect','spawn','skip')][string]$Warmup, + [AllowNull()][string[]]$Flags, + [bool]$ReplaceFlags, + [ValidateSet('full','legacy')][string]$NoiseProfile, + [bool]$RenderReport, + [bool]$CloseLabVIEW, + [bool]$CloseLVCompare, + [int]$TimeoutSeconds, + [bool]$DisableTimeout, + [AllowNull()][string]$StagingRoot, + [bool]$SameNameHint, + [bool]$AllowSameLeaf, + [AllowNull()][string]$AgentId, + [AllowNull()][string]$AgentClass, + [AllowNull()][string]$ExecutionCellLeasePath, + [AllowNull()][string]$ExecutionCellId, + [AllowNull()][string]$ExecutionCellLeaseId, + [AllowNull()][string]$HarnessInstanceId + ) + + $resolvedOutputRoot = Resolve-AbsolutePath -RepoRoot $RepoRoot -Candidate $OutputRoot + New-Dir $resolvedOutputRoot + $dualPlaneContext = Resolve-TestStandExecutionCellContext -ExecutionCellLeasePath $ExecutionCellLeasePath -ExecutionCellId $ExecutionCellId -ExecutionCellLeaseId $ExecutionCellLeaseId -ExecutionCellSuiteClass 'dual-plane-parity' -HarnessInstanceId $HarnessInstanceId -AgentId $AgentId -AgentClass $AgentClass -SuiteClass 'dual-plane-parity' -PlaneName $null -Role 'coordinator' -OutputRoot $resolvedOutputRoot + + $x64LabVIEW = if ([string]::IsNullOrWhiteSpace($LabVIEW64ExePath)) { + if ([string]::IsNullOrWhiteSpace($DefaultLabVIEWExePath)) { Resolve-LabVIEW2026Path -Bitness '64' } else { $DefaultLabVIEWExePath } + } else { + $LabVIEW64ExePath + } + $x32LabVIEW = if ([string]::IsNullOrWhiteSpace($LabVIEW32ExePath)) { Resolve-LabVIEW2026Path -Bitness '32' } else { $LabVIEW32ExePath } + + $planesRoot = Join-Path $resolvedOutputRoot 'planes' + $x64Root = Join-Path $planesRoot 'x64' + $x32Root = Join-Path $planesRoot 'x32' + New-Dir $x64Root + New-Dir $x32Root + + $suiteTimeout = if ($DisableTimeout -or $TimeoutSeconds -le 0) { 0 } else { [Math]::Max(30, $TimeoutSeconds + 30) } + + $x64Process = Start-DualPlaneChildProcess -ScriptPath $ScriptPath -PlaneKey 'x64' -BaseVi $BaseVi -HeadVi $HeadVi -OutputRoot $x64Root -LabVIEWExePath $x64LabVIEW -LabVIEWBitness '64' -LVComparePath $LVComparePath -Warmup $Warmup -Flags $Flags -ReplaceFlags:$ReplaceFlags -NoiseProfile $NoiseProfile -RenderReport:$RenderReport -CloseLabVIEW:$CloseLabVIEW -CloseLVCompare:$CloseLVCompare -TimeoutSeconds $TimeoutSeconds -DisableTimeout:$DisableTimeout -StagingRoot $StagingRoot -SameNameHint:$SameNameHint -AllowSameLeaf:$AllowSameLeaf -AgentId $AgentId -AgentClass $AgentClass -ExecutionCellLeasePath $ExecutionCellLeasePath -ExecutionCellId $ExecutionCellId -ExecutionCellLeaseId $ExecutionCellLeaseId -ExecutionCellSuiteClass 'dual-plane-parity' -ParentHarnessInstanceId $dualPlaneContext.harnessInstance.instanceId + $x32Process = Start-DualPlaneChildProcess -ScriptPath $ScriptPath -PlaneKey 'x32' -BaseVi $BaseVi -HeadVi $HeadVi -OutputRoot $x32Root -LabVIEWExePath $x32LabVIEW -LabVIEWBitness '32' -LVComparePath $LVComparePath -Warmup $Warmup -Flags $Flags -ReplaceFlags:$ReplaceFlags -NoiseProfile $NoiseProfile -RenderReport:$RenderReport -CloseLabVIEW:$CloseLabVIEW -CloseLVCompare:$CloseLVCompare -TimeoutSeconds $TimeoutSeconds -DisableTimeout:$DisableTimeout -StagingRoot $StagingRoot -SameNameHint:$SameNameHint -AllowSameLeaf:$AllowSameLeaf -AgentId $AgentId -AgentClass $AgentClass -ExecutionCellLeasePath $ExecutionCellLeasePath -ExecutionCellId $ExecutionCellId -ExecutionCellLeaseId $ExecutionCellLeaseId -ExecutionCellSuiteClass 'dual-plane-parity' -ParentHarnessInstanceId $dualPlaneContext.harnessInstance.instanceId + + Wait-ForChildProcesses -Processes @($x64Process, $x32Process) -TimeoutSeconds $suiteTimeout + + $x64IndexPath = Join-Path $x64Root 'session-index.json' + $x32IndexPath = Join-Path $x32Root 'session-index.json' + if (-not (Test-Path -LiteralPath $x64IndexPath -PathType Leaf)) { + throw "Dual-plane parity suite missing x64 session index at $x64IndexPath" + } + if (-not (Test-Path -LiteralPath $x32IndexPath -PathType Leaf)) { + throw "Dual-plane parity suite missing x32 session index at $x32IndexPath" + } + + $x64Index = Get-Content -LiteralPath $x64IndexPath -Raw | ConvertFrom-Json -Depth 12 + $x32Index = Get-Content -LiteralPath $x32IndexPath -Raw | ConvertFrom-Json -Depth 12 + + $x64Session = [pscustomobject][ordered]@{ + plane = 'native-labview-2026-64' + architecture = '64-bit' + labviewExePath = $x64LabVIEW + outputRoot = $x64Root + warmup = $x64Index.warmup + compare = $x64Index.compare + outcome = $x64Index.outcome + error = $x64Index.error + exitCode = if ($null -ne $x64Index.outcome) { [int]$x64Index.outcome.exitCode } else { if ($x64Process.ExitCode -is [int]) { [int]$x64Process.ExitCode } else { 1 } } + executionCell = $x64Index.executionCell + harnessInstance = $x64Index.harnessInstance + processModel = $x64Index.processModel + } + $x32Session = [pscustomobject][ordered]@{ + plane = 'native-labview-2026-32' + architecture = '32-bit' + labviewExePath = $x32LabVIEW + outputRoot = $x32Root + warmup = $x32Index.warmup + compare = $x32Index.compare + outcome = $x32Index.outcome + error = $x32Index.error + exitCode = if ($null -ne $x32Index.outcome) { [int]$x32Index.outcome.exitCode } else { if ($x32Process.ExitCode -is [int]) { [int]$x32Process.ExitCode } else { 1 } } + executionCell = $x32Index.executionCell + harnessInstance = $x32Index.harnessInstance + processModel = $x32Index.processModel + } + + $parity = New-DualPlaneParitySummary -X64Session $x64Session -X32Session $x32Session + + $topError = if ($parity.status -eq 'incomplete') { + @($x64Session.error, $x32Session.error | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }) -join '; ' + } else { + $null + } + + $topIndex = [ordered]@{ + schema = 'teststand-compare-session/v2' + at = (Get-Date).ToString('o') + suiteClass = 'dual-plane-parity' + primaryPlane = 'native-labview-2026-64' + requestedSimultaneous = $true + warmup = $x64Session.warmup + compare = $x64Session.compare + outcome = $x64Session.outcome + error = $topError + executionCell = $dualPlaneContext.executionCell + harnessInstance = $dualPlaneContext.harnessInstance + processModel = $dualPlaneContext.processModel + planes = [ordered]@{ + x64 = $x64Session + x32 = $x32Session } + parity = $parity + } + + $indexPath = Join-Path $resolvedOutputRoot 'session-index.json' + $topIndex | ConvertTo-Json -Depth 12 | Out-File -LiteralPath $indexPath -Encoding utf8 + + $exitCode = switch ($parity.status) { + 'match' { if ($null -ne $x64Session.outcome) { [int]$x64Session.outcome.exitCode } else { 1 } } + 'mismatch' { 2 } + default { 1 } } + + Write-Host ("TestStand Dual-Plane Parity result: status={0} x64={1} x32={2} index={3}" -f $parity.status, $x64Session.exitCode, $x32Session.exitCode, $indexPath) -ForegroundColor Yellow + exit $exitCode } -$compareNode.autoCli = $autoCli -$compareNode.sameName = $sameName -$compareNode.timeoutSeconds = $effectiveTimeout -if ($env:LVCI_COMPARE_POLICY) { $compareNode.policy = $env:LVCI_COMPARE_POLICY } -if ($env:LVCI_COMPARE_MODE) { $compareNode.mode = $env:LVCI_COMPARE_MODE } - -$index = [ordered]@{ - schema = 'teststand-compare-session/v1' - at = (Get-Date).ToString('o') - warmup = $warmupNode - compare = $compareNode - outcome = if ($cap) { - @{ exitCode=[int]$cap.exitCode; seconds=[double]$cap.seconds; command=$cap.command; diff=([bool]($cap.exitCode -eq 1)) } - } else { $null } - error = $err + +$repo = (Resolve-Path '.').Path + +if ($InternalSinglePlane -and [string]::IsNullOrWhiteSpace($InternalPlaneKey) -eq $false) { + if ($InternalPlaneKey -eq 'x32' -and -not $PSBoundParameters.ContainsKey('LabVIEWBitness')) { + $LabVIEWBitness = '32' + } + if ($InternalPlaneKey -eq 'x64' -and -not $PSBoundParameters.ContainsKey('LabVIEWBitness')) { + $LabVIEWBitness = '64' + } +} + +if (-not [System.IO.Path]::IsPathRooted($OutputRoot)) { + $OutputRoot = Join-Path $repo $OutputRoot } -$indexPath = Join-Path $OutputRoot 'session-index.json' -New-Dir $OutputRoot -$index | ConvertTo-Json -Depth 6 | Out-File -LiteralPath $indexPath -Encoding utf8 -$exitCode = if ($cap) { [int]$cap.exitCode } else { 1 } -$diffDisplay = if ($index.outcome) { $index.outcome.diff } else { 'unknown' } -$exitDisplay = if ($index.outcome) { $index.outcome.exitCode } else { 'n/a' } +if ($SuiteClass -eq 'dual-plane-parity' -and -not $InternalSinglePlane) { + Invoke-DualPlaneParitySuite -RepoRoot $repo -ScriptPath $PSCommandPath -BaseVi $BaseVi -HeadVi $HeadVi -OutputRoot $OutputRoot -DefaultLabVIEWExePath $LabVIEWExePath -LabVIEW64ExePath $LabVIEW64ExePath -LabVIEW32ExePath $LabVIEW32ExePath -LVComparePath $LVComparePath -Warmup $Warmup -Flags $Flags -ReplaceFlags:$ReplaceFlags -NoiseProfile $NoiseProfile -RenderReport:$RenderReport -CloseLabVIEW:$CloseLabVIEW -CloseLVCompare:$CloseLVCompare -TimeoutSeconds $TimeoutSeconds -DisableTimeout:$DisableTimeout -StagingRoot $StagingRoot -SameNameHint:$SameNameHint -AllowSameLeaf:$AllowSameLeaf -AgentId $AgentId -AgentClass $AgentClass -ExecutionCellLeasePath $ExecutionCellLeasePath -ExecutionCellId $ExecutionCellId -ExecutionCellLeaseId $ExecutionCellLeaseId -HarnessInstanceId $HarnessInstanceId + return +} + +$planeName = switch ($InternalPlaneKey) { + 'x64' { 'native-labview-2026-64' } + 'x32' { 'native-labview-2026-32' } + default { $null } +} +$harnessRole = if ($InternalSinglePlane -and -not [string]::IsNullOrWhiteSpace($InternalPlaneKey)) { 'plane-child' } else { 'single-plane' } + +$singlePlaneSession = Invoke-TestStandSinglePlaneSession -RepoRoot $repo -BaseVi $BaseVi -HeadVi $HeadVi -LabVIEWExePath $LabVIEWExePath -LabVIEWBitness $LabVIEWBitness -LVComparePath $LVComparePath -OutputRoot $OutputRoot -Warmup $Warmup -Flags $Flags -ReplaceFlags:$ReplaceFlags -NoiseProfile $NoiseProfile -RenderReport:$RenderReport -CloseLabVIEW:$CloseLabVIEW -CloseLVCompare:$CloseLVCompare -TimeoutSeconds $TimeoutSeconds -DisableTimeout:$DisableTimeout -StagingRoot $StagingRoot -SameNameHint:$SameNameHint -AllowSameLeaf:$AllowSameLeaf -PlaneName $planeName -AgentId $AgentId -AgentClass $AgentClass -ExecutionCellLeasePath $ExecutionCellLeasePath -ExecutionCellId $ExecutionCellId -ExecutionCellLeaseId $ExecutionCellLeaseId -ExecutionCellSuiteClass $ExecutionCellSuiteClass -HarnessInstanceId $HarnessInstanceId -ParentHarnessInstanceId $ParentHarnessInstanceId -HarnessRole $harnessRole -SuiteClass $SuiteClass + +Write-TestStandV1SessionIndex -OutputRoot $OutputRoot -PlaneSession $singlePlaneSession + +$capPath = if ($singlePlaneSession.compare) { $singlePlaneSession.compare.capture } else { $null } +$diffDisplay = if ($singlePlaneSession.outcome) { $singlePlaneSession.outcome.diff } else { 'unknown' } +$exitDisplay = if ($singlePlaneSession.outcome) { $singlePlaneSession.outcome.exitCode } else { 'n/a' } Write-Host ("TestStand Compare Harness result: exit={0} diff={1} capture={2}" -f $exitDisplay, $diffDisplay, $capPath) -ForegroundColor Yellow -exit $exitCode +exit $singlePlaneSession.exitCode diff --git a/tools/Write-LabVIEW2026HostPlaneDiagnostics.ps1 b/tools/Write-LabVIEW2026HostPlaneDiagnostics.ps1 index 51cc89434..9e6e8ced8 100644 --- a/tools/Write-LabVIEW2026HostPlaneDiagnostics.ps1 +++ b/tools/Write-LabVIEW2026HostPlaneDiagnostics.ps1 @@ -109,6 +109,10 @@ function New-HostPlaneSummaryMarkdown { ) $runner = Get-ObjectValue -Object $Report -Name 'runner' + $hostInfo = Get-ObjectValue -Object $Report -Name 'host' + $osFingerprint = Get-ObjectValue -Object $hostInfo -Name 'osFingerprint' + $osCanonical = Get-ObjectValue -Object $osFingerprint -Name 'canonical' + $osAdvisory = Get-ObjectValue -Object $osFingerprint -Name 'advisory' $native = Get-ObjectValue -Object $Report -Name 'native' $executionPolicy = Get-ObjectValue -Object $Report -Name 'executionPolicy' $policy = Get-ObjectValue -Object $Report -Name 'policy' @@ -135,6 +139,20 @@ function New-HostPlaneSummaryMarkdown { '', ('- Report: `{0}`' -f $ReportPath), ('- Runner: `{0}` (hostIsRunner={1})' -f ([string](Get-ObjectValue -Object $runner -Name 'runnerName')), ([string][bool](Get-ObjectValue -Object $runner -Name 'hostIsRunner'))), + ('- Canonical host OS: `{0}` version=`{1}` build=`{2}` ubr=`{3}` displayVersion=`{4}` edition=`{5}` architecture=`{6}`' -f ` + ([string](Get-ObjectValue -Object $osFingerprint -Name 'platform')), ` + ([string](Get-ObjectValue -Object $osCanonical -Name 'version')), ` + ([string](Get-ObjectValue -Object $osCanonical -Name 'buildNumber')), ` + ([string](Get-ObjectValue -Object $osCanonical -Name 'ubr')), ` + ([string](Get-ObjectValue -Object $osCanonical -Name 'displayVersion')), ` + ([string](Get-ObjectValue -Object $osCanonical -Name 'editionId')), ` + ([string](Get-ObjectValue -Object $osCanonical -Name 'architecture'))), + ('- Host OS fingerprint SHA-256: `{0}`' -f ([string](Get-ObjectValue -Object $osFingerprint -Name 'fingerprintSha256'))), + ('- Isolated lane group ID: `{0}`' -f ([string](Get-ObjectValue -Object $osFingerprint -Name 'isolatedLaneGroupId'))), + ('- Host OS branding: caption=`{0}` registryProduct=`{1}` mismatch={2}' -f ` + ([string](Get-ObjectValue -Object $osAdvisory -Name 'caption')), ` + ([string](Get-ObjectValue -Object $osAdvisory -Name 'productName')), ` + ([string][bool](Get-ObjectValue -Object $osAdvisory -Name 'brandingMismatch'))), ('- Native 64-bit: `{0}`' -f ([string](Get-ObjectValue -Object $x64Plane -Name 'status'))), ('- Native 32-bit: `{0}`' -f ([string](Get-ObjectValue -Object $x32Plane -Name 'status'))), ('- Parallel native support: `{0}`' -f ([string][bool](Get-ObjectValue -Object $native -Name 'parallelLabVIEWSupported'))), @@ -203,6 +221,10 @@ Write-GitHubOutput -Key 'labview-2026-host-plane-summary-path' -Value $summaryRe Write-GitHubOutput -Key 'labview-2026-native-64-status' -Value ([string]$report.native.planes.x64.status) -Path $GitHubOutputPath Write-GitHubOutput -Key 'labview-2026-native-32-status' -Value ([string]$report.native.planes.x32.status) -Path $GitHubOutputPath Write-GitHubOutput -Key 'labview-2026-native-parallel-supported' -Value ([string][bool]$report.native.parallelLabVIEWSupported) -Path $GitHubOutputPath +Write-GitHubOutput -Key 'labview-2026-host-os-fingerprint-sha256' -Value ([string]$report.host.osFingerprint.fingerprintSha256) -Path $GitHubOutputPath +Write-GitHubOutput -Key 'labview-2026-host-isolated-lane-group-id' -Value ([string]$report.host.osFingerprint.isolatedLaneGroupId) -Path $GitHubOutputPath +Write-GitHubOutput -Key 'labview-2026-host-os-version' -Value ([string]$report.host.osFingerprint.canonical.version) -Path $GitHubOutputPath +Write-GitHubOutput -Key 'labview-2026-host-os-build' -Value ([string]$report.host.osFingerprint.canonical.buildNumber) -Path $GitHubOutputPath if ($PassThru) { Write-Output $report diff --git a/tools/policy/github-comment-budget-hook.json b/tools/policy/github-comment-budget-hook.json new file mode 100644 index 000000000..8fe08f642 --- /dev/null +++ b/tools/policy/github-comment-budget-hook.json @@ -0,0 +1,16 @@ +{ + "schema": "priority/github-comment-budget-hook-policy@v1", + "costRollupPath": "tests/results/_agent/cost/agent-cost-rollup.json", + "materializationPolicyPath": "tools/policy/agent-cost-rollup-materialization.json", + "materializationReportPath": "tests/results/_agent/cost/agent-cost-rollup-materialization.json", + "outputPath": "tests/results/_agent/cost/github-comment-budget-hook.json", + "markdownOutputPath": "tests/results/_agent/cost/github-comment-budget-hook.md", + "operatorBudgetCapUsd": 50000, + "materializeCostRollup": true, + "reservedFundingPurposes": [ + "calibration" + ], + "reservedActivationStates": [ + "hold" + ] +} diff --git a/tools/priority/Import-HandoffState.ps1 b/tools/priority/Import-HandoffState.ps1 index 607458f40..1d93baa89 100644 --- a/tools/priority/Import-HandoffState.ps1 +++ b/tools/priority/Import-HandoffState.ps1 @@ -46,6 +46,7 @@ $continuitySummary = Read-HandoffJson -Name 'continuity-summary.json' $monitoringMode = Read-HandoffJson -Name 'monitoring-mode.json' $governorSummary = Read-HandoffJson -Name 'autonomous-governor-summary.json' $governorPortfolioSummary = Read-HandoffJson -Name 'autonomous-governor-portfolio-summary.json' +$contextConcentrator = Read-HandoffJson -Name 'sagan-context-concentrator.json' $operatorSteeringEvent = Read-HandoffJson -Name 'operator-steering-event.json' if ($issueSummary) { @@ -277,6 +278,18 @@ if ($governorSummary) { Write-Host (" next : {0}" -f (Format-NullableValue $governorSummary.summary.nextAction)) Write-Host (" queue : {0}" -f (Format-NullableValue $governorSummary.summary.queueState)) Write-Host (" signal : {0}" -f (Format-NullableValue $governorSummary.summary.signalQuality)) + if ($governorSummary.summary.PSObject.Properties['releaseSigningStatus']) { + Write-Host (" release : {0}" -f (Format-NullableValue $governorSummary.summary.releaseSigningStatus)) + if ($governorSummary.summary.PSObject.Properties['releaseSigningExternalBlocker'] -and $governorSummary.summary.releaseSigningExternalBlocker) { + Write-Host (" blocker : {0}" -f (Format-NullableValue $governorSummary.summary.releaseSigningExternalBlocker)) + } + if ($governorSummary.summary.PSObject.Properties['releasePublishedBundleState'] -and $governorSummary.summary.releasePublishedBundleState) { + Write-Host (" bundle : {0}" -f (Format-NullableValue $governorSummary.summary.releasePublishedBundleState)) + } + if ($governorSummary.summary.PSObject.Properties['releasePublishedBundleReleaseTag'] -and $governorSummary.summary.releasePublishedBundleReleaseTag) { + Write-Host (" bundleTag: {0}" -f (Format-NullableValue $governorSummary.summary.releasePublishedBundleReleaseTag)) + } + } if ($governorSummary.summary.nextOwnerRepository) { Write-Host (" nextRepo : {0}" -f (Format-NullableValue $governorSummary.summary.nextOwnerRepository)) } @@ -304,6 +317,21 @@ if ($governorPortfolioSummary) { Write-Host (" next : {0}" -f (Format-NullableValue $governorPortfolioSummary.summary.nextAction)) Write-Host (" template : {0}" -f (Format-NullableValue $governorPortfolioSummary.summary.templateMonitoringStatus)) Write-Host (" proof : {0}" -f (Format-NullableValue $governorPortfolioSummary.summary.supportedProofStatus)) + if ($governorPortfolioSummary.summary.PSObject.Properties['viHistoryDistributorDependencyStatus']) { + Write-Host (" vhist : {0}" -f (Format-NullableValue $governorPortfolioSummary.summary.viHistoryDistributorDependencyStatus)) + if ($governorPortfolioSummary.summary.PSObject.Properties['viHistoryDistributorDependencyTargetRepository'] -and $governorPortfolioSummary.summary.viHistoryDistributorDependencyTargetRepository) { + Write-Host (" vhistRepo: {0}" -f (Format-NullableValue $governorPortfolioSummary.summary.viHistoryDistributorDependencyTargetRepository)) + } + if ($governorPortfolioSummary.summary.PSObject.Properties['viHistoryDistributorDependencyExternalBlocker'] -and $governorPortfolioSummary.summary.viHistoryDistributorDependencyExternalBlocker) { + Write-Host (" vhistBlk : {0}" -f (Format-NullableValue $governorPortfolioSummary.summary.viHistoryDistributorDependencyExternalBlocker)) + } + if ($governorPortfolioSummary.summary.PSObject.Properties['viHistoryDistributorDependencyPublishedBundleState'] -and $governorPortfolioSummary.summary.viHistoryDistributorDependencyPublishedBundleState) { + Write-Host (" vhistPub : {0}" -f (Format-NullableValue $governorPortfolioSummary.summary.viHistoryDistributorDependencyPublishedBundleState)) + } + if ($governorPortfolioSummary.summary.PSObject.Properties['viHistoryDistributorDependencyPublishedBundleReleaseTag'] -and $governorPortfolioSummary.summary.viHistoryDistributorDependencyPublishedBundleReleaseTag) { + Write-Host (" vhistTag : {0}" -f (Format-NullableValue $governorPortfolioSummary.summary.viHistoryDistributorDependencyPublishedBundleReleaseTag)) + } + } if ($governorPortfolioSummary.summary.nextOwnerRepository) { Write-Host (" nextRepo : {0}" -f (Format-NullableValue $governorPortfolioSummary.summary.nextOwnerRepository)) } @@ -317,6 +345,23 @@ if ($governorPortfolioSummary) { } Set-Variable -Name HandoffAutonomousGovernorPortfolioSummary -Scope Global -Value $governorPortfolioSummary -Force } +if ($contextConcentrator) { + Write-Host '[handoff] Context concentrator' -ForegroundColor Cyan + Write-Host (" status : {0}" -f (Format-NullableValue $contextConcentrator.summary.concentrationStatus)) + if ($contextConcentrator.summary.activeIssueNumber) { + Write-Host (" issue : #{0}" -f (Format-NullableValue $contextConcentrator.summary.activeIssueNumber)) + } + Write-Host (" owner : {0}" -f (Format-NullableValue $contextConcentrator.summary.currentOwnerRepository)) + Write-Host (" next : {0}" -f (Format-NullableValue $contextConcentrator.summary.nextAction)) + Write-Host (" hot/warm : {0}/{1}" -f (Format-NullableValue $contextConcentrator.summary.hotWorkingSetCount), (Format-NullableValue $contextConcentrator.summary.warmMemoryCount)) + Write-Host (" archive : {0}" -f (Format-NullableValue $contextConcentrator.summary.archiveCount)) + Write-Host (" blockers : {0}" -f (Format-NullableValue $contextConcentrator.summary.blockerCount)) + Write-Host (' spend : ${0}' -f (Format-NullableValue $contextConcentrator.summary.blendedLowerBoundUsd)) + foreach ($entry in @($contextConcentrator.memory.hotWorkingSet | Select-Object -First 3)) { + Write-Host (" - {0} [{1}]" -f (Format-NullableValue $entry.label), (Format-NullableValue $entry.status)) + } + Set-Variable -Name HandoffContextConcentrator -Scope Global -Value $contextConcentrator -Force +} if ($operatorSteeringEvent) { Write-Host '[handoff] Operator steering event' -ForegroundColor Cyan Write-Host (" steering : {0}" -f (Format-NullableValue $operatorSteeringEvent.steeringKind)) diff --git a/tools/priority/__tests__/autonomous-governor-portfolio-summary-schema.test.mjs b/tools/priority/__tests__/autonomous-governor-portfolio-summary-schema.test.mjs index 63a7b674c..89d3ef601 100644 --- a/tools/priority/__tests__/autonomous-governor-portfolio-summary-schema.test.mjs +++ b/tools/priority/__tests__/autonomous-governor-portfolio-summary-schema.test.mjs @@ -67,6 +67,19 @@ test('autonomous governor portfolio summary schema validates a generated report' queueState: { status: 'queue-empty', reason: 'queue-empty', openIssueCount: 0, ready: true }, continuity: { status: 'maintained', turnBoundary: 'safe-idle', supervisionState: 'idle-monitoring', operatorPromptRequiredToResume: false }, monitoringMode: { status: 'active', futureAgentAction: 'future-agent-may-pivot', wakeConditionCount: 0 }, + releaseSigningReadiness: { + status: 'missing', + codePathState: null, + signingCapabilityState: null, + signingAuthorityState: null, + releaseConductorApplyState: null, + publicationState: null, + publishedBundleState: null, + publishedBundleReleaseTag: null, + publishedBundleAuthoritativeConsumerPin: null, + externalBlocker: null, + blockerCount: 0 + }, deliveryRuntime: { status: 'checks-pending', runtimeStatus: 'waiting-ci', @@ -117,6 +130,14 @@ test('autonomous governor portfolio summary schema validates a generated report' wakeTerminalState: 'monitoring', monitoringStatus: 'active', futureAgentAction: 'future-agent-may-pivot', + releaseSigningStatus: 'missing', + releaseSigningAuthorityState: null, + releaseConductorApplyState: null, + releaseSigningExternalBlocker: null, + releasePublicationState: null, + releasePublishedBundleState: null, + releasePublishedBundleReleaseTag: null, + releasePublishedBundleAuthoritativeConsumerPin: null, queueHandoffStatus: 'checks-pending', queueHandoffNextWakeCondition: 'checks-green', queueHandoffPrUrl: 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/pull/1864', diff --git a/tools/priority/__tests__/autonomous-governor-portfolio-summary.test.mjs b/tools/priority/__tests__/autonomous-governor-portfolio-summary.test.mjs index 289d36950..d064ad9e9 100644 --- a/tools/priority/__tests__/autonomous-governor-portfolio-summary.test.mjs +++ b/tools/priority/__tests__/autonomous-governor-portfolio-summary.test.mjs @@ -42,6 +42,74 @@ function createCompareGovernorSummary(overrides = {}) { status: 'blocked', futureAgentAction: 'stay-in-compare-monitoring', wakeConditionCount: 3 + }, + releaseSigningReadiness: { + status: 'warn', + codePathState: 'ready', + signingCapabilityState: 'missing', + signingAuthorityState: 'scope-missing', + releaseConductorApplyState: 'disabled', + publicationState: 'unobserved', + publishedBundleState: 'producer-native-incomplete', + publishedBundleReleaseTag: 'v0.6.3-tools.14', + publishedBundleAuthoritativeConsumerPin: null, + externalBlocker: 'workflow-signing-secret-missing' + }, + deliveryRuntime: { + executionTopology: { + status: 'bundle-committed', + executionPlane: 'hosted', + providerId: 'hosted-github-workflow', + workerSlotId: 'worker-slot-2', + activeLogicalLaneCount: 2, + seededLogicalLaneCount: 4, + catalogCount: 4, + runtimeSurface: 'windows-native-teststand', + processModelClass: 'parallel-process-model', + windowsOnly: true, + requestedSimultaneous: true, + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + premiumSaganMode: true, + reciprocalLinkReady: true, + logicalLaneActivation: { + activeLaneCount: 2, + seededLaneCount: 4, + catalogCount: 4 + }, + providerDispatch: { + providerId: 'hosted-github-workflow', + providerKind: 'hosted-github-workflow', + executionPlane: 'hosted', + assignmentMode: 'async-validation', + dispatchSurface: 'github-actions', + completionMode: 'async', + workerSlotId: 'worker-slot-2', + dispatchStatus: 'completed', + completionStatus: 'waiting', + failureClass: null + }, + executionBundle: { + status: 'committed', + planeBinding: 'dual-plane-parity', + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + harnessKind: 'teststand-compare-harness', + premiumSaganMode: true, + reciprocalLinkReady: true, + effectiveBillableRateUsdPerHour: 375, + executionCellLeaseId: 'exec-lease-123', + dockerLaneLeaseId: 'docker-lease-456', + harnessInstanceId: 'ts-harness-01', + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + cellId: 'cell-sagan-kernel', + laneId: 'docker-lane-01', + isolatedLaneGroupId: + 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + } + } } }, wake: { @@ -84,7 +152,33 @@ function createCompareGovernorSummary(overrides = {}) { queueHandoffStatus: 'checks-pending', queueHandoffNextWakeCondition: 'checks-green', queueHandoffPrUrl: 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/pull/1864', - queueAuthoritySource: 'delivery-runtime' + queueAuthoritySource: 'delivery-runtime', + executionTopologyStatus: 'bundle-committed', + executionTopologyExecutionPlane: 'hosted', + executionTopologyProviderId: 'hosted-github-workflow', + executionTopologyWorkerSlotId: 'worker-slot-2', + executionTopologyActiveLogicalLaneCount: 2, + executionTopologySeededLogicalLaneCount: 4, + executionTopologyRuntimeSurface: 'windows-native-teststand', + executionTopologyProcessModelClass: 'parallel-process-model', + executionTopologyWindowsOnly: true, + executionTopologyRequestedSimultaneous: true, + executionTopologyCellClass: 'kernel-coordinator', + executionTopologySuiteClass: 'dual-plane-parity', + executionTopologyOperatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + executionBundleStatus: 'committed', + executionBundlePlaneBinding: 'dual-plane-parity', + executionBundlePremiumSaganMode: true, + executionBundleReciprocalLinkReady: true, + executionBundleEffectiveBillableRateUsdPerHour: 375, + releaseSigningStatus: 'warn', + releaseSigningAuthorityState: 'scope-missing', + releaseConductorApplyState: 'disabled', + releaseSigningExternalBlocker: 'workflow-signing-secret-missing', + releasePublicationState: 'unobserved', + releasePublishedBundleState: 'producer-native-incomplete', + releasePublishedBundleReleaseTag: 'v0.6.3-tools.14', + releasePublishedBundleAuthoritativeConsumerPin: null }, ...overrides }; @@ -306,9 +400,85 @@ test('runAutonomousGovernorPortfolioSummary keeps compare as owner during active assert.equal(report.summary.queueHandoffStatus, 'checks-pending'); assert.equal(report.summary.queueHandoffNextWakeCondition, 'checks-green'); assert.equal(report.summary.queueAuthoritySource, 'delivery-runtime'); + assert.equal(report.summary.executionTopologyStatus, 'bundle-committed'); + assert.equal(report.summary.executionTopologyExecutionPlane, 'hosted'); + assert.equal(report.summary.executionTopologyProviderId, 'hosted-github-workflow'); + assert.equal(report.summary.executionTopologyWorkerSlotId, 'worker-slot-2'); + assert.equal(report.summary.executionTopologyActiveLogicalLaneCount, 2); + assert.equal(report.summary.executionTopologySeededLogicalLaneCount, 4); + assert.equal(report.summary.executionTopologyRuntimeSurface, 'windows-native-teststand'); + assert.equal(report.summary.executionTopologyProcessModelClass, 'parallel-process-model'); + assert.equal(report.summary.executionTopologyWindowsOnly, true); + assert.equal(report.summary.executionTopologyRequestedSimultaneous, true); + assert.equal(report.summary.executionTopologyCellClass, 'kernel-coordinator'); + assert.equal(report.summary.executionTopologySuiteClass, 'dual-plane-parity'); + assert.equal(report.summary.executionTopologyOperatorAuthorizationRef, 'budget-auth://operator/session-2026-03-24'); + assert.equal(report.summary.executionBundleStatus, 'committed'); + assert.equal(report.summary.executionBundlePlaneBinding, 'dual-plane-parity'); + assert.equal(report.summary.executionBundlePremiumSaganMode, true); + assert.equal(report.summary.executionBundleReciprocalLinkReady, true); + assert.equal(report.summary.executionBundleEffectiveBillableRateUsdPerHour, 375); + assert.equal(report.summary.viHistoryDistributorDependencyStatus, 'blocked'); + assert.equal( + report.summary.viHistoryDistributorDependencyExternalBlocker, + 'workflow-signing-secret-missing' + ); + assert.equal(report.summary.viHistoryDistributorDependencyPublicationState, 'unobserved'); + assert.equal(report.summary.viHistoryDistributorDependencyPublishedBundleState, 'producer-native-incomplete'); + assert.equal(report.summary.viHistoryDistributorDependencyPublishedBundleReleaseTag, 'v0.6.3-tools.14'); + assert.equal(report.summary.viHistoryDistributorDependencyAuthoritativeConsumerPin, null); + assert.equal(report.summary.viHistoryDistributorDependencySigningAuthorityState, 'scope-missing'); + assert.equal(report.summary.viHistoryDistributorDependencyReleaseConductorApplyState, 'disabled'); assert.equal(report.compare.queueHandoffPrUrl, 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/pull/1864'); assert.equal(report.compare.queueAuthoritySource, 'delivery-runtime'); + assert.equal(report.compare.executionTopology.status, 'bundle-committed'); + assert.equal(report.compare.executionTopology.executionPlane, 'hosted'); + assert.equal(report.compare.executionTopology.providerId, 'hosted-github-workflow'); + assert.equal(report.compare.executionTopology.workerSlotId, 'worker-slot-2'); + assert.equal(report.compare.executionTopology.activeLogicalLaneCount, 2); + assert.equal(report.compare.executionTopology.seededLogicalLaneCount, 4); + assert.equal(report.compare.executionTopology.runtimeSurface, 'windows-native-teststand'); + assert.equal(report.compare.executionTopology.processModelClass, 'parallel-process-model'); + assert.equal(report.compare.executionTopology.windowsOnly, true); + assert.equal(report.compare.executionTopology.requestedSimultaneous, true); + assert.equal(report.compare.executionTopology.cellClass, 'kernel-coordinator'); + assert.equal(report.compare.executionTopology.suiteClass, 'dual-plane-parity'); + assert.equal(report.compare.executionTopology.operatorAuthorizationRef, 'budget-auth://operator/session-2026-03-24'); + assert.equal(report.compare.executionTopology.logicalLaneActivation.catalogCount, 4); + assert.equal(report.compare.executionTopology.providerDispatch.dispatchStatus, 'completed'); + assert.equal(report.compare.executionTopology.executionBundle.status, 'committed'); + assert.equal(report.compare.executionTopology.executionBundle.cellClass, 'kernel-coordinator'); + assert.equal(report.compare.executionTopology.executionBundle.suiteClass, 'dual-plane-parity'); + assert.equal( + report.compare.executionTopology.executionBundle.operatorAuthorizationRef, + 'budget-auth://operator/session-2026-03-24' + ); + assert.equal(report.compare.executionBundleStatus, 'committed'); + assert.equal(report.compare.executionBundlePlaneBinding, 'dual-plane-parity'); + assert.equal(report.compare.executionBundlePremiumSaganMode, true); + assert.equal(report.compare.executionBundleReciprocalLinkReady, true); + assert.equal(report.compare.executionBundleEffectiveBillableRateUsdPerHour, 375); assert.equal(report.portfolio.repositoryCount, 4); + assert.deepEqual(report.portfolio.dependencies, [ + { + id: 'vi-history-producer-native-distributor', + status: 'blocked', + ownerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + dependentRepository: 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate', + requiredCapability: 'vi-history', + source: 'compare-release-signing-readiness', + releaseSigningStatus: 'warn', + releasePublicationState: 'unobserved', + publishedBundleState: 'producer-native-incomplete', + publishedBundleReleaseTag: 'v0.6.3-tools.14', + publishedBundleAuthoritativeConsumerPin: null, + signingCapabilityState: 'missing', + signingAuthorityState: 'scope-missing', + releaseConductorApplyState: 'disabled', + externalBlocker: 'workflow-signing-secret-missing', + detail: 'awaiting-producer-native-bundle-publication' + } + ]); assert.deepEqual(report.portfolio.repositories.find((entry) => entry.id === 'compare').triggeredWakeConditions, [ 'compare-queue-not-empty', 'compare-continuity-not-safe-idle', @@ -334,6 +504,15 @@ test('runAutonomousGovernorPortfolioSummary routes ownership to canonical templa status: 'blocked', futureAgentAction: 'reopen-template-monitoring-work', wakeConditionCount: 1 + }, + releaseSigningReadiness: { + status: 'warn', + codePathState: 'ready', + signingCapabilityState: 'missing', + signingAuthorityState: 'scope-missing', + releaseConductorApplyState: 'disabled', + publicationState: 'unobserved', + externalBlocker: 'workflow-signing-secret-missing' } }, wake: { @@ -360,7 +539,16 @@ test('runAutonomousGovernorPortfolioSummary routes ownership to canonical templa continuityStatus: 'maintained', wakeTerminalState: 'monitoring', monitoringStatus: 'blocked', - futureAgentAction: 'reopen-template-monitoring-work' + futureAgentAction: 'reopen-template-monitoring-work', + releaseSigningStatus: 'warn', + releaseSigningAuthorityState: 'scope-missing', + releaseConductorApplyState: 'disabled', + releaseSigningExternalBlocker: 'workflow-signing-secret-missing', + releasePublicationState: 'unobserved', + queueHandoffStatus: 'none', + queueHandoffNextWakeCondition: null, + queueHandoffPrUrl: null, + queueAuthoritySource: 'none' } }); const monitoringMode = createMonitoringMode({ @@ -425,3 +613,194 @@ test('runAutonomousGovernorPortfolioSummary routes ownership to canonical templa ['template-canonical-open-issues'] ); }); + +test('runAutonomousGovernorPortfolioSummary keeps next owner on compare while vi-history distributor dependency is blocked', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'governor-portfolio-vi-history-blocked-')); + const compareSummary = createCompareGovernorSummary({ + compare: { + queueState: { status: 'queue-empty', reason: 'queue-empty', openIssueCount: 0, ready: true }, + continuity: { + status: 'maintained', + turnBoundary: 'safe-idle', + supervisionState: 'idle-monitoring', + operatorPromptRequiredToResume: false + }, + monitoringMode: { + status: 'active', + futureAgentAction: 'future-agent-may-pivot', + wakeConditionCount: 0 + }, + releaseSigningReadiness: { + status: 'warn', + codePathState: 'ready', + signingCapabilityState: 'missing', + signingAuthorityState: 'scope-missing', + releaseConductorApplyState: 'disabled', + publicationState: 'unobserved', + externalBlocker: 'workflow-signing-secret-missing' + } + }, + wake: { + terminalState: 'monitoring', + currentStage: 'monitoring', + classification: null, + decision: null, + monitoringStatus: 'active', + authoritativeTier: null, + blockedLowerTierEvidence: false, + replayMatched: false, + replayAuthorityCompatible: null, + issueNumber: null, + issueUrl: null, + recommendedOwnerRepository: null + }, + summary: { + governorMode: 'monitoring-active', + currentOwnerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + nextOwnerRepository: 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate', + nextAction: 'future-agent-may-pivot', + signalQuality: 'idle-monitoring', + queueState: 'queue-empty', + continuityStatus: 'maintained', + wakeTerminalState: 'monitoring', + monitoringStatus: 'active', + futureAgentAction: 'future-agent-may-pivot', + queueHandoffStatus: 'none', + queueHandoffNextWakeCondition: null, + queueHandoffPrUrl: null, + queueAuthoritySource: 'none', + releaseSigningStatus: 'warn', + releaseSigningAuthorityState: 'scope-missing', + releaseConductorApplyState: 'disabled', + releaseSigningExternalBlocker: 'workflow-signing-secret-missing', + releasePublicationState: 'unobserved' + } + }); + const monitoringMode = createMonitoringMode({ + compare: { + queueState: { reportPath: 'tests/results/_agent/issue/no-standing-priority.json', ready: true, status: 'queue-empty', detail: 'queue-empty' }, + continuity: { reportPath: 'tests/results/_agent/handoff/continuity-summary.json', ready: true, status: 'maintained', detail: 'safe-idle' }, + pivotGate: { reportPath: 'tests/results/_agent/promotion/template-pivot-gate-report.json', ready: true, status: 'ready', detail: 'future-agent-may-pivot' }, + readyForMonitoring: true + }, + summary: { + status: 'active', + futureAgentAction: 'future-agent-may-pivot', + wakeConditionCount: 0, + triggeredWakeConditions: [] + } + }); + + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'handoff', 'autonomous-governor-summary.json'), compareSummary); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'handoff', 'monitoring-mode.json'), monitoringMode); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'handoff', 'downstream-repo-graph-truth.json'), createRepoGraphTruth()); + + const { report } = await runAutonomousGovernorPortfolioSummary({ repoRoot: tmpDir }); + + assert.equal(report.summary.governorMode, 'monitoring-active'); + assert.equal(report.summary.currentOwnerRepository, 'LabVIEW-Community-CI-CD/compare-vi-cli-action'); + assert.equal(report.summary.nextOwnerRepository, 'LabVIEW-Community-CI-CD/compare-vi-cli-action'); + assert.equal(report.summary.nextAction, 'complete-compare-vi-history-producer-release'); + assert.equal(report.summary.ownerDecisionSource, 'compare-vi-history-distributor-dependency'); + assert.equal(report.summary.viHistoryDistributorDependencyStatus, 'blocked'); +}); + +test('runAutonomousGovernorPortfolioSummary flips next owner to template once vi-history distributor dependency is ready', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'governor-portfolio-vi-history-ready-')); + const compareSummary = createCompareGovernorSummary({ + compare: { + queueState: { status: 'queue-empty', reason: 'queue-empty', openIssueCount: 0, ready: true }, + continuity: { + status: 'maintained', + turnBoundary: 'safe-idle', + supervisionState: 'idle-monitoring', + operatorPromptRequiredToResume: false + }, + monitoringMode: { + status: 'active', + futureAgentAction: 'future-agent-may-pivot', + wakeConditionCount: 0 + }, + releaseSigningReadiness: { + status: 'pass', + codePathState: 'ready', + signingCapabilityState: 'configured', + signingAuthorityState: 'ready', + releaseConductorApplyState: 'enabled', + publicationState: 'producer-native-ready', + publishedBundleState: 'producer-native-ready', + publishedBundleReleaseTag: 'v0.6.4-rc.1-tools.1', + publishedBundleAuthoritativeConsumerPin: 'v0.6.4-rc.1-tools.1', + externalBlocker: null + } + }, + wake: { + terminalState: 'monitoring', + currentStage: 'monitoring', + classification: null, + decision: null, + monitoringStatus: 'active', + authoritativeTier: null, + blockedLowerTierEvidence: false, + replayMatched: false, + replayAuthorityCompatible: null, + issueNumber: null, + issueUrl: null, + recommendedOwnerRepository: null + }, + summary: { + governorMode: 'monitoring-active', + currentOwnerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + nextOwnerRepository: 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate', + nextAction: 'future-agent-may-pivot', + signalQuality: 'idle-monitoring', + queueState: 'queue-empty', + continuityStatus: 'maintained', + wakeTerminalState: 'monitoring', + monitoringStatus: 'active', + futureAgentAction: 'future-agent-may-pivot', + queueHandoffStatus: 'none', + queueHandoffNextWakeCondition: null, + queueHandoffPrUrl: null, + queueAuthoritySource: 'none', + releaseSigningStatus: 'pass', + releaseSigningAuthorityState: 'ready', + releaseConductorApplyState: 'enabled', + releaseSigningExternalBlocker: null, + releasePublicationState: 'producer-native-ready', + releasePublishedBundleState: 'producer-native-ready', + releasePublishedBundleReleaseTag: 'v0.6.4-rc.1-tools.1', + releasePublishedBundleAuthoritativeConsumerPin: 'v0.6.4-rc.1-tools.1' + } + }); + const monitoringMode = createMonitoringMode({ + compare: { + queueState: { reportPath: 'tests/results/_agent/issue/no-standing-priority.json', ready: true, status: 'queue-empty', detail: 'queue-empty' }, + continuity: { reportPath: 'tests/results/_agent/handoff/continuity-summary.json', ready: true, status: 'maintained', detail: 'safe-idle' }, + pivotGate: { reportPath: 'tests/results/_agent/promotion/template-pivot-gate-report.json', ready: true, status: 'ready', detail: 'future-agent-may-pivot' }, + readyForMonitoring: true + }, + summary: { + status: 'active', + futureAgentAction: 'future-agent-may-pivot', + wakeConditionCount: 0, + triggeredWakeConditions: [] + } + }); + + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'handoff', 'autonomous-governor-summary.json'), compareSummary); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'handoff', 'monitoring-mode.json'), monitoringMode); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'handoff', 'downstream-repo-graph-truth.json'), createRepoGraphTruth()); + + const { report } = await runAutonomousGovernorPortfolioSummary({ repoRoot: tmpDir }); + + assert.equal(report.summary.governorMode, 'monitoring-active'); + assert.equal(report.summary.currentOwnerRepository, 'LabVIEW-Community-CI-CD/compare-vi-cli-action'); + assert.equal(report.summary.nextOwnerRepository, 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate'); + assert.equal(report.summary.nextAction, 'future-agent-may-pivot'); + assert.equal(report.summary.ownerDecisionSource, 'compare-monitoring-mode'); + assert.equal(report.summary.viHistoryDistributorDependencyStatus, 'ready'); + assert.equal(report.summary.viHistoryDistributorDependencyPublishedBundleState, 'producer-native-ready'); + assert.equal(report.summary.viHistoryDistributorDependencyPublishedBundleReleaseTag, 'v0.6.4-rc.1-tools.1'); + assert.equal(report.summary.viHistoryDistributorDependencyAuthoritativeConsumerPin, 'v0.6.4-rc.1-tools.1'); +}); diff --git a/tools/priority/__tests__/autonomous-governor-summary.test.mjs b/tools/priority/__tests__/autonomous-governor-summary.test.mjs index e4b669425..0fc0c5a62 100644 --- a/tools/priority/__tests__/autonomous-governor-summary.test.mjs +++ b/tools/priority/__tests__/autonomous-governor-summary.test.mjs @@ -102,11 +102,94 @@ function createWakeInvestmentAccounting() { }; } +function createReleaseSigningReadiness(overrides = {}) { + return { + schema: 'priority/release-signing-readiness-report@v1', + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + workflowContract: { + ready: true, + workflowPath: '.github/workflows/release-conductor.yml', + reasons: [] + }, + secretInventory: { + status: 'missing', + requiredSecretPresent: false, + optionalPublicKeyPresent: false, + listedSecretCount: 3, + listedSecretNames: ['AUTO_APPROVE_TOKEN', 'GH_POLICY_TOKEN', 'GH_TOKEN'], + source: 'github-actions-secrets-api', + error: null + }, + releaseConductorApply: { + status: 'disabled', + variablePresent: false, + enabled: false, + configuredValue: null, + listedVariableCount: 0, + listedVariableNames: [], + source: 'github-actions-variables-api', + error: null + }, + signingAuthority: { + status: 'scope-missing', + requiredScope: 'admin:ssh_signing_key', + scopeAvailable: false, + listedKeyCount: null, + source: 'github-user-ssh-signing-keys-api', + error: 'This API operation needs the \"admin:ssh_signing_key\" scope.' + }, + publication: { + status: 'tag-created-not-pushed', + tagCreated: true, + tagPushed: false, + targetTag: 'v0.6.4-rc.1' + }, + summary: { + status: 'warn', + codePathState: 'ready', + signingCapabilityState: 'missing', + signingAuthorityState: 'scope-missing', + releaseConductorApplyState: 'disabled', + publicationState: 'tag-created-not-pushed', + publishedBundleState: 'producer-native-incomplete', + publishedBundleReleaseTag: 'v0.6.3-tools.14', + publishedBundleAuthoritativeConsumerPin: null, + externalBlocker: 'workflow-signing-secret-missing', + blockerCount: 3 + }, + blockers: [ + { + code: 'workflow-signing-secret-missing', + message: 'RELEASE_TAG_SIGNING_PRIVATE_KEY is not configured for the repository Actions secrets surface.' + }, + { + code: 'release-conductor-apply-disabled', + message: 'RELEASE_CONDUCTOR_ENABLED is not set to 1 for the repository Actions variable surface.' + }, + { + code: 'workflow-signing-admin-scope-missing', + message: 'admin:ssh_signing_key is not available to the current automation identity, so SSH signing-key authority cannot be verified or managed.' + } + ], + ...overrides + }; +} + function createDeliveryRuntimeState(overrides = {}) { return { schema: 'priority/delivery-agent-runtime-state@v1', status: 'waiting-ci', laneLifecycle: 'waiting-ci', + logicalLaneActivation: { + seededLaneCount: 4, + activeLaneCount: 2, + catalog: [ + { id: 'logical-lane-01', activationState: 'active' }, + { id: 'logical-lane-02', activationState: 'active' }, + { id: 'logical-lane-03', activationState: 'seeded' }, + { id: 'logical-lane-04', activationState: 'seeded' } + ] + }, queueAuthorityRefresh: { attempted: false, status: null, @@ -131,7 +214,65 @@ function createDeliveryRuntimeState(overrides = {}) { outcome: 'waiting-ci', blockerClass: 'none', nextWakeCondition: 'checks-green', - reason: 'Waiting for hosted checks to finish before merge queue advances.' + reason: 'Waiting for hosted checks to finish before merge queue advances.', + providerDispatch: { + providerId: 'hosted-github-workflow', + providerKind: 'hosted-github-workflow', + executionPlane: 'hosted', + assignmentMode: 'async-validation', + dispatchSurface: 'github-actions', + completionMode: 'async', + workerSlotId: 'worker-slot-2', + dispatchStatus: 'completed', + completionStatus: 'waiting', + failureClass: null + }, + executionTopology: { + status: 'bundle-committed', + executionPlane: 'hosted', + providerId: 'hosted-github-workflow', + workerSlotId: 'worker-slot-2', + activeLogicalLaneCount: 2, + seededLogicalLaneCount: 4, + catalogCount: 4, + runtimeSurface: 'windows-native-teststand', + processModelClass: 'parallel-process-model', + windowsOnly: true, + requestedSimultaneous: true, + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + premiumSaganMode: true, + reciprocalLinkReady: true, + executionCellLeaseId: 'exec-lease-123', + dockerLaneLeaseId: 'docker-lease-456', + harnessKind: 'teststand-compare-harness', + harnessInstanceId: 'ts-harness-01', + cellId: 'cell-sagan-kernel', + laneId: 'docker-lane-01', + planeBinding: 'dual-plane-parity' + }, + concurrentLaneStatus: { + executionBundle: { + status: 'committed', + planeBinding: 'dual-plane-parity', + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + harnessKind: 'teststand-compare-harness', + premiumSaganMode: true, + reciprocalLinkReady: true, + effectiveBillableRateUsdPerHour: 375, + executionCellLeaseId: 'exec-lease-123', + dockerLaneLeaseId: 'docker-lease-456', + harnessInstanceId: 'ts-harness-01', + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + cellId: 'cell-sagan-kernel', + laneId: 'docker-lane-01', + isolatedLaneGroupId: + 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + } + } }, ...overrides }; @@ -253,6 +394,9 @@ test('runAutonomousGovernorSummary reports compare governance work when the late assert.equal(report.summary.nextAction, 'continue-compare-governance-work'); assert.equal(report.summary.signalQuality, 'validated-governance-work'); assert.equal(report.funding.invoiceTurnId, 'invoice-turn-2026-03-HQ1VJLMV-0027'); + assert.equal(report.compare.releaseSigningReadiness.status, 'missing'); + assert.equal(report.summary.releaseSigningStatus, 'missing'); + assert.equal(report.summary.releaseSigningExternalBlocker, null); }); test('runAutonomousGovernorSummary reports monitoring-active when no wake lifecycle exists', async () => { @@ -270,10 +414,44 @@ test('runAutonomousGovernorSummary reports monitoring-active when no wake lifecy assert.equal(report.summary.nextOwnerRepository, 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate'); assert.equal(report.wake.terminalState, null); assert.equal(report.compare.deliveryRuntime.status, 'none'); + assert.equal(report.compare.releaseSigningReadiness.status, 'missing'); assert.equal(report.compare.deliveryRuntime.queueAuthorityRefresh.attempted, false); assert.equal(report.compare.deliveryRuntime.queueAuthorityRefresh.status, null); assert.equal(report.summary.queueHandoffStatus, 'none'); assert.equal(report.summary.queueAuthoritySource, 'none'); + assert.equal(report.summary.releaseSigningStatus, 'missing'); +}); + +test('runAutonomousGovernorSummary carries explicit release signing blocker state into the governor summary', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'governor-summary-release-signing-')); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'issue', 'no-standing-priority.json'), createQueueEmpty()); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'handoff', 'continuity-summary.json'), createContinuitySummary()); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'handoff', 'monitoring-mode.json'), createMonitoringMode()); + writeJson( + path.join(tmpDir, 'tests', 'results', '_agent', 'release', 'release-signing-readiness.json'), + createReleaseSigningReadiness() + ); + + const { report } = await runAutonomousGovernorSummary({ repoRoot: tmpDir }); + + assert.equal(report.compare.releaseSigningReadiness.status, 'warn'); + assert.equal(report.compare.releaseSigningReadiness.codePathState, 'ready'); + assert.equal(report.compare.releaseSigningReadiness.signingCapabilityState, 'missing'); + assert.equal(report.compare.releaseSigningReadiness.signingAuthorityState, 'scope-missing'); + assert.equal(report.compare.releaseSigningReadiness.releaseConductorApplyState, 'disabled'); + assert.equal(report.compare.releaseSigningReadiness.publicationState, 'tag-created-not-pushed'); + assert.equal(report.compare.releaseSigningReadiness.publishedBundleState, 'producer-native-incomplete'); + assert.equal(report.compare.releaseSigningReadiness.publishedBundleReleaseTag, 'v0.6.3-tools.14'); + assert.equal(report.compare.releaseSigningReadiness.publishedBundleAuthoritativeConsumerPin, null); + assert.equal(report.compare.releaseSigningReadiness.externalBlocker, 'workflow-signing-secret-missing'); + assert.equal(report.summary.releaseSigningStatus, 'warn'); + assert.equal(report.summary.releaseSigningAuthorityState, 'scope-missing'); + assert.equal(report.summary.releaseConductorApplyState, 'disabled'); + assert.equal(report.summary.releaseSigningExternalBlocker, 'workflow-signing-secret-missing'); + assert.equal(report.summary.releasePublicationState, 'tag-created-not-pushed'); + assert.equal(report.summary.releasePublishedBundleState, 'producer-native-incomplete'); + assert.equal(report.summary.releasePublishedBundleReleaseTag, 'v0.6.3-tools.14'); + assert.equal(report.summary.releasePublishedBundleAuthoritativeConsumerPin, null); }); test('runAutonomousGovernorSummary carries queue-owned delivery runtime state into the governor summary', async () => { @@ -297,14 +475,177 @@ test('runAutonomousGovernorSummary carries queue-owned delivery runtime state in assert.equal(report.compare.deliveryRuntime.laneLifecycle, 'waiting-ci'); assert.equal(report.compare.deliveryRuntime.nextWakeCondition, 'checks-green'); assert.equal(report.compare.deliveryRuntime.prUrl, 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/pull/1864'); + assert.equal(report.compare.deliveryRuntime.executionTopology.status, 'bundle-committed'); + assert.equal(report.compare.deliveryRuntime.executionTopology.executionPlane, 'hosted'); + assert.equal(report.compare.deliveryRuntime.executionTopology.providerId, 'hosted-github-workflow'); + assert.equal(report.compare.deliveryRuntime.executionTopology.workerSlotId, 'worker-slot-2'); + assert.equal(report.compare.deliveryRuntime.executionTopology.activeLogicalLaneCount, 2); + assert.equal(report.compare.deliveryRuntime.executionTopology.seededLogicalLaneCount, 4); + assert.equal(report.compare.deliveryRuntime.executionTopology.catalogCount, 4); + assert.equal(report.compare.deliveryRuntime.executionTopology.runtimeSurface, 'windows-native-teststand'); + assert.equal(report.compare.deliveryRuntime.executionTopology.processModelClass, 'parallel-process-model'); + assert.equal(report.compare.deliveryRuntime.executionTopology.windowsOnly, true); + assert.equal(report.compare.deliveryRuntime.executionTopology.requestedSimultaneous, true); + assert.equal(report.compare.deliveryRuntime.executionTopology.cellClass, 'kernel-coordinator'); + assert.equal(report.compare.deliveryRuntime.executionTopology.suiteClass, 'dual-plane-parity'); + assert.equal( + report.compare.deliveryRuntime.executionTopology.operatorAuthorizationRef, + 'budget-auth://operator/session-2026-03-24' + ); + assert.equal(report.compare.deliveryRuntime.executionTopology.logicalLaneActivation.activeLaneCount, 2); + assert.equal(report.compare.deliveryRuntime.executionTopology.providerDispatch.dispatchStatus, 'completed'); + assert.equal(report.compare.deliveryRuntime.executionTopology.executionBundle.status, 'committed'); + assert.equal(report.compare.deliveryRuntime.executionBundle.status, 'committed'); + assert.equal(report.compare.deliveryRuntime.executionBundle.planeBinding, 'dual-plane-parity'); + assert.equal(report.compare.deliveryRuntime.executionBundle.cellClass, 'kernel-coordinator'); + assert.equal(report.compare.deliveryRuntime.executionBundle.suiteClass, 'dual-plane-parity'); + assert.equal(report.compare.deliveryRuntime.executionBundle.premiumSaganMode, true); + assert.equal(report.compare.deliveryRuntime.executionBundle.reciprocalLinkReady, true); + assert.equal(report.compare.deliveryRuntime.executionBundle.effectiveBillableRateUsdPerHour, 375); + assert.equal( + report.compare.deliveryRuntime.executionBundle.operatorAuthorizationRef, + 'budget-auth://operator/session-2026-03-24' + ); assert.equal(report.compare.deliveryRuntime.queueAuthorityRefresh.attempted, false); assert.equal(report.compare.deliveryRuntime.queueAuthorityRefresh.summaryPath, null); + assert.equal(report.summary.executionTopologyStatus, 'bundle-committed'); + assert.equal(report.summary.executionTopologyExecutionPlane, 'hosted'); + assert.equal(report.summary.executionTopologyProviderId, 'hosted-github-workflow'); + assert.equal(report.summary.executionTopologyWorkerSlotId, 'worker-slot-2'); + assert.equal(report.summary.executionTopologyActiveLogicalLaneCount, 2); + assert.equal(report.summary.executionTopologySeededLogicalLaneCount, 4); + assert.equal(report.summary.executionTopologyRuntimeSurface, 'windows-native-teststand'); + assert.equal(report.summary.executionTopologyProcessModelClass, 'parallel-process-model'); + assert.equal(report.summary.executionTopologyWindowsOnly, true); + assert.equal(report.summary.executionTopologyRequestedSimultaneous, true); + assert.equal(report.summary.executionTopologyCellClass, 'kernel-coordinator'); + assert.equal(report.summary.executionTopologySuiteClass, 'dual-plane-parity'); + assert.equal(report.summary.executionTopologyOperatorAuthorizationRef, 'budget-auth://operator/session-2026-03-24'); + assert.equal(report.summary.executionBundleStatus, 'committed'); + assert.equal(report.summary.executionBundlePlaneBinding, 'dual-plane-parity'); + assert.equal(report.summary.executionBundlePremiumSaganMode, true); + assert.equal(report.summary.executionBundleReciprocalLinkReady, true); + assert.equal(report.summary.executionBundleEffectiveBillableRateUsdPerHour, 375); assert.equal(report.summary.queueHandoffStatus, 'checks-pending'); assert.equal(report.summary.queueHandoffNextWakeCondition, 'checks-green'); assert.equal(report.summary.queueHandoffPrUrl, 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/pull/1864'); assert.equal(report.summary.queueAuthoritySource, 'delivery-runtime'); }); +test('runAutonomousGovernorSummary prefers concentrated delivery execution topology over raw bundle-derived conflicts', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'governor-summary-execution-topology-preference-')); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'issue', 'no-standing-priority.json'), createQueueEmpty()); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'handoff', 'continuity-summary.json'), createContinuitySummary()); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'handoff', 'monitoring-mode.json'), createMonitoringMode()); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'issue', 'wake-lifecycle.json'), createWakeLifecycle()); + writeJson( + path.join(tmpDir, 'tests', 'results', '_agent', 'capital', 'wake-investment-accounting.json'), + createWakeInvestmentAccounting() + ); + writeJson( + path.join(tmpDir, 'tests', 'results', '_agent', 'runtime', 'delivery-agent-state.json'), + createDeliveryRuntimeState({ + activeLane: { + issue: 1863, + prUrl: 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/pull/1864', + laneLifecycle: 'waiting-ci', + actionType: 'merge-pr', + outcome: 'waiting-ci', + blockerClass: 'none', + nextWakeCondition: 'checks-green', + reason: 'Waiting for hosted checks to finish before merge queue advances.', + providerDispatch: { + providerId: 'hosted-github-workflow', + providerKind: 'hosted-github-workflow', + executionPlane: 'hosted', + assignmentMode: 'async-validation', + dispatchSurface: 'github-actions', + completionMode: 'async', + workerSlotId: 'worker-slot-2', + dispatchStatus: 'completed', + completionStatus: 'waiting', + failureClass: null + }, + executionTopology: { + status: 'provider-waiting', + executionPlane: 'local', + providerId: 'local-codex', + workerSlotId: 'worker-slot-9', + activeLogicalLaneCount: 1, + seededLogicalLaneCount: 2, + catalogCount: 2, + runtimeSurface: 'windows-native-teststand', + processModelClass: 'sequential-process-model', + windowsOnly: true, + requestedSimultaneous: false, + cellClass: 'worker-cell', + suiteClass: 'single-plane-review', + operatorAuthorizationRef: 'budget-auth://operator/session-override', + premiumSaganMode: false, + reciprocalLinkReady: false, + executionCellLeaseId: 'exec-lease-override', + dockerLaneLeaseId: 'docker-lease-override', + harnessKind: 'teststand-compare-harness', + harnessInstanceId: 'ts-harness-override', + cellId: 'cell-worker-09', + laneId: 'docker-lane-09', + planeBinding: 'native-labview-2026-64' + }, + concurrentLaneStatus: { + executionBundle: { + status: 'committed', + planeBinding: 'dual-plane-parity', + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + harnessKind: 'teststand-compare-harness', + premiumSaganMode: true, + reciprocalLinkReady: true, + effectiveBillableRateUsdPerHour: 375, + executionCellLeaseId: 'exec-lease-123', + dockerLaneLeaseId: 'docker-lease-456', + harnessInstanceId: 'ts-harness-01', + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + cellId: 'cell-sagan-kernel', + laneId: 'docker-lane-01' + } + } + } + }) + ); + + const { report } = await runAutonomousGovernorSummary({ repoRoot: tmpDir }); + + assert.equal(report.compare.deliveryRuntime.executionTopology.status, 'provider-waiting'); + assert.equal(report.compare.deliveryRuntime.executionTopology.executionPlane, 'local'); + assert.equal(report.compare.deliveryRuntime.executionTopology.providerId, 'local-codex'); + assert.equal(report.compare.deliveryRuntime.executionTopology.workerSlotId, 'worker-slot-9'); + assert.equal(report.compare.deliveryRuntime.executionTopology.activeLogicalLaneCount, 1); + assert.equal(report.compare.deliveryRuntime.executionTopology.seededLogicalLaneCount, 2); + assert.equal(report.compare.deliveryRuntime.executionTopology.catalogCount, 2); + assert.equal(report.compare.deliveryRuntime.executionTopology.processModelClass, 'sequential-process-model'); + assert.equal(report.compare.deliveryRuntime.executionTopology.requestedSimultaneous, false); + assert.equal(report.compare.deliveryRuntime.executionTopology.cellClass, 'worker-cell'); + assert.equal(report.compare.deliveryRuntime.executionTopology.suiteClass, 'single-plane-review'); + assert.equal( + report.compare.deliveryRuntime.executionTopology.operatorAuthorizationRef, + 'budget-auth://operator/session-override' + ); + assert.equal(report.compare.deliveryRuntime.executionTopology.providerDispatch.dispatchStatus, 'completed'); + assert.equal(report.compare.deliveryRuntime.executionTopology.executionBundle.status, 'committed'); + assert.equal(report.compare.deliveryRuntime.executionBundle.status, 'committed'); + assert.equal(report.summary.executionTopologyStatus, 'provider-waiting'); + assert.equal(report.summary.executionTopologyExecutionPlane, 'local'); + assert.equal(report.summary.executionTopologyProviderId, 'local-codex'); + assert.equal(report.summary.executionTopologyWorkerSlotId, 'worker-slot-9'); + assert.equal(report.summary.executionTopologyActiveLogicalLaneCount, 1); + assert.equal(report.summary.executionTopologySeededLogicalLaneCount, 2); + assert.equal(report.summary.executionTopologyProcessModelClass, 'sequential-process-model'); + assert.equal(report.summary.executionTopologyRequestedSimultaneous, false); + assert.equal(report.summary.executionTopologyCellClass, 'worker-cell'); + assert.equal(report.summary.executionTopologySuiteClass, 'single-plane-review'); + assert.equal(report.summary.executionTopologyOperatorAuthorizationRef, 'budget-auth://operator/session-override'); +}); + test('runAutonomousGovernorSummary exposes queue authority refresh telemetry from delivery runtime state', async () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'governor-summary-runtime-refresh-')); writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'issue', 'no-standing-priority.json'), createQueueEmpty()); diff --git a/tools/priority/__tests__/concurrent-lane-status-schema.test.mjs b/tools/priority/__tests__/concurrent-lane-status-schema.test.mjs index 733ebcca8..83c19c95d 100644 --- a/tools/priority/__tests__/concurrent-lane-status-schema.test.mjs +++ b/tools/priority/__tests__/concurrent-lane-status-schema.test.mjs @@ -24,6 +24,7 @@ test('concurrent lane status schema validates the generated receipt', async () = const schema = JSON.parse(await readFile(schemaPath, 'utf8')); const tempDir = mkdtempSync(path.join(os.tmpdir(), 'concurrent-lane-status-schema-')); const applyReceiptPath = path.join(tempDir, 'apply.json'); + const executionBundleReceiptPath = path.join(tempDir, 'execution-cell-bundle.json'); writeJson(applyReceiptPath, { schema: 'priority/concurrent-lane-apply-receipt@v1', generatedAt: '2026-03-21T00:00:00.000Z', @@ -87,9 +88,37 @@ test('concurrent lane status schema validates the generated receipt', async () = shadowLaneIds: [] } }); + writeJson(executionBundleReceiptPath, { + schema: 'priority/execution-cell-bundle-report@v1', + status: 'granted', + cellId: 'cell-sagan-kernel', + laneId: 'docker-lane-01', + summary: { + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + executionCellLeaseId: 'exec-lease-123', + dockerLaneLeaseId: 'docker-lease-456', + harnessKind: 'teststand-compare-harness', + harnessInstanceId: 'ts-harness-01', + planeBinding: 'dual-plane-parity', + premiumSaganMode: true, + reciprocalLinkReady: false, + effectiveBillableRateUsdPerHour: 375, + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + } + }); const { receipt } = await observeConcurrentLaneStatus( - parseArgs(['node', 'concurrent-lane-status.mjs', '--apply-receipt', applyReceiptPath]), + parseArgs([ + 'node', + 'concurrent-lane-status.mjs', + '--apply-receipt', + applyReceiptPath, + '--execution-bundle-receipt', + executionBundleReceiptPath + ]), { ensureGhCliFn: () => {}, getRepoRootFn: () => tempDir, @@ -107,6 +136,8 @@ test('concurrent lane status schema validates the generated receipt', async () = addFormats(ajv); const validate = ajv.compile(schema); assert.equal(validate(receipt), true, JSON.stringify(validate.errors, null, 2)); + assert.equal(receipt.executionBundle.status, 'granted'); + assert.equal(receipt.summary.executionBundlePremiumSaganMode, true); assert.equal(receipt.plan.schema, 'priority/concurrent-lane-plan@v1'); assert.equal(receipt.plan.source, 'file'); assert.equal(receipt.plan.recommendedBundleId, 'hosted-only-proof'); diff --git a/tools/priority/__tests__/concurrent-lane-status.test.mjs b/tools/priority/__tests__/concurrent-lane-status.test.mjs index 126ab8a4c..e1b9d428f 100644 --- a/tools/priority/__tests__/concurrent-lane-status.test.mjs +++ b/tools/priority/__tests__/concurrent-lane-status.test.mjs @@ -149,13 +149,51 @@ function createApplyReceipt(overrides = {}) { test('observeConcurrentLaneStatus projects active hosted lanes and queued PR merge state', async () => { const tempDir = createTempDir(); const applyReceiptPath = path.join(tempDir, 'tests', 'results', '_agent', 'runtime', 'concurrent-lane-apply-receipt.json'); + const executionBundleReceiptPath = path.join( + tempDir, + 'tests', + 'results', + '_agent', + 'runtime', + 'execution-cell-bundle.json' + ); const outputPath = path.join(tempDir, 'tests', 'results', '_agent', 'runtime', 'concurrent-lane-status-receipt.json'); writeJson(applyReceiptPath, createApplyReceipt()); + writeJson(executionBundleReceiptPath, { + schema: 'priority/execution-cell-bundle-report@v1', + status: 'committed', + cellId: 'cell-sagan-kernel', + laneId: 'docker-lane-01', + summary: { + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + executionCellLeaseId: 'exec-lease-123', + dockerLaneLeaseId: 'docker-lease-456', + harnessKind: 'teststand-compare-harness', + harnessInstanceId: 'ts-harness-01', + planeBinding: 'dual-plane-parity', + premiumSaganMode: true, + reciprocalLinkReady: true, + effectiveBillableRateUsdPerHour: 375, + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + } + }); const ghJsonCalls = []; const ghGraphqlCalls = []; const { receipt, outputPath: writtenPath } = await observeConcurrentLaneStatus( - parseArgs(['node', 'concurrent-lane-status.mjs', '--apply-receipt', applyReceiptPath, '--output', outputPath]), + parseArgs([ + 'node', + 'concurrent-lane-status.mjs', + '--apply-receipt', + applyReceiptPath, + '--execution-bundle-receipt', + executionBundleReceiptPath, + '--output', + outputPath + ]), { ensureGhCliFn: () => {}, getRepoRootFn: () => tempDir, @@ -227,9 +265,19 @@ test('observeConcurrentLaneStatus projects active hosted lanes and queued PR mer assert.equal(receipt.plan.selectedBundleId, 'hosted-plus-manual-linux-docker'); assert.equal(receipt.hostedRun.observationStatus, 'active'); assert.equal(receipt.pullRequest.observationStatus, 'queued'); + assert.equal(receipt.executionBundle.status, 'committed'); + assert.equal(receipt.executionBundle.cellClass, 'kernel-coordinator'); + assert.equal(receipt.executionBundle.suiteClass, 'dual-plane-parity'); + assert.equal(receipt.executionBundle.harnessKind, 'teststand-compare-harness'); + assert.equal(receipt.executionBundle.reciprocalLinkReady, true); + assert.equal(receipt.executionBundle.premiumSaganMode, true); + assert.equal(receipt.executionBundle.operatorAuthorizationRef, 'budget-auth://operator/session-2026-03-24'); assert.equal(receipt.summary.orchestratorDisposition, 'wait-hosted-run'); assert.equal(receipt.summary.activeLaneCount, 2); assert.equal(receipt.summary.deferredLaneCount, 1); + assert.equal(receipt.summary.executionBundleStatus, 'committed'); + assert.equal(receipt.summary.executionBundleReciprocalLinkReady, true); + assert.equal(receipt.summary.executionBundlePremiumSaganMode, true); assert.equal(receipt.laneStatuses[0].idleClassification, null); assert.equal(receipt.laneStatuses[2].idleClassification?.state, 'waiting-merge'); assert.equal(receipt.summary.idleClassificationCoverage.managedLaneCount, 3); @@ -353,6 +401,7 @@ test('observeConcurrentLaneStatus settles completed hosted runs and keeps deferr ); assert.equal(receipt.status, 'settled'); + assert.equal(receipt.executionBundle, null); assert.equal(receipt.plan.path, 'tests/results/_agent/runtime/concurrent-lane-plan.json'); assert.equal(receipt.plan.schema, 'priority/concurrent-lane-plan@v1'); assert.equal(receipt.plan.source, 'file'); @@ -398,6 +447,7 @@ test('observeConcurrentLaneStatus fails closed when hosted workflow observation ); assert.equal(receipt.status, 'failed'); + assert.equal(receipt.executionBundle, null); assert.equal(receipt.hostedRun.observationStatus, 'failed'); assert.equal(receipt.summary.orchestratorDisposition, 'hold-investigate'); assert.match(receipt.observationErrors[0] ?? '', /rate limited/i); diff --git a/tools/priority/__tests__/delivery-agent-schema.test.mjs b/tools/priority/__tests__/delivery-agent-schema.test.mjs index 5e7df30f7..64bd4d633 100644 --- a/tools/priority/__tests__/delivery-agent-schema.test.mjs +++ b/tools/priority/__tests__/delivery-agent-schema.test.mjs @@ -792,6 +792,44 @@ test('delivery-agent runtime state schema validates persisted runtime state', as completionMode: 'async', selectedSlotId: 'worker-slot-2', requiresLocalCheckout: false + }, + concurrentLaneStatus: { + receiptPath: 'tests/results/_agent/runtime/concurrent-lane-status-receipt.json', + status: 'settled', + selectedBundleId: 'hosted-plus-manual-linux-docker', + executionBundle: { + status: 'committed', + cellId: 'cell-sagan-kernel', + laneId: 'docker-lane-01', + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + executionCellLeaseId: 'exec-lease-123', + dockerLaneLeaseId: 'docker-lease-456', + harnessKind: 'teststand-compare-harness', + harnessInstanceId: 'ts-harness-01', + planeBinding: 'dual-plane-parity', + premiumSaganMode: true, + reciprocalLinkReady: true, + effectiveBillableRateUsdPerHour: 375, + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + isolatedLaneGroupId: + 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + }, + summary: { + laneCount: 3, + activeLaneCount: 2, + completedLaneCount: 0, + failedLaneCount: 0, + deferredLaneCount: 1, + manualLaneCount: 1, + shadowLaneCount: 0, + executionBundleStatus: 'committed', + executionBundleReciprocalLinkReady: true, + executionBundlePremiumSaganMode: true, + pullRequestStatus: 'queued', + orchestratorDisposition: 'wait-hosted-run' + } } } } @@ -955,6 +993,38 @@ test('delivery-agent runtime state schema validates persisted runtime state', as assert.equal(state.activeLane.workerProviderSelection.selectedProviderId, 'hosted-github-workflow'); assert.equal(state.activeLane.providerDispatch.providerId, 'hosted-github-workflow'); assert.equal(state.activeLane.providerDispatch.workerSlotId, 'worker-slot-2'); + assert.equal(state.activeLane.executionTopology.status, 'bundle-committed'); + assert.equal(state.activeLane.executionTopology.executionPlane, 'hosted'); + assert.equal(state.activeLane.executionTopology.providerId, 'hosted-github-workflow'); + assert.equal(state.activeLane.executionTopology.workerSlotId, 'worker-slot-2'); + assert.equal(state.activeLane.executionTopology.cellId, 'cell-sagan-kernel'); + assert.equal(state.activeLane.executionTopology.laneId, 'docker-lane-01'); + assert.equal(state.activeLane.executionTopology.cellClass, 'kernel-coordinator'); + assert.equal(state.activeLane.executionTopology.suiteClass, 'dual-plane-parity'); + assert.equal(state.activeLane.executionTopology.planeBinding, 'dual-plane-parity'); + assert.equal(state.activeLane.executionTopology.harnessKind, 'teststand-compare-harness'); + assert.equal(state.activeLane.executionTopology.harnessInstanceId, 'ts-harness-01'); + assert.equal(state.activeLane.executionTopology.executionCellLeaseId, 'exec-lease-123'); + assert.equal(state.activeLane.executionTopology.dockerLaneLeaseId, 'docker-lease-456'); + assert.equal(state.activeLane.executionTopology.premiumSaganMode, true); + assert.equal(state.activeLane.executionTopology.reciprocalLinkReady, true); + assert.equal(state.activeLane.executionTopology.operatorAuthorizationRef, 'budget-auth://operator/session-2026-03-24'); + assert.equal(state.activeLane.executionTopology.runtimeSurface, 'windows-native-teststand'); + assert.equal(state.activeLane.executionTopology.processModelClass, 'parallel-process-model'); + assert.equal(state.activeLane.executionTopology.windowsOnly, true); + assert.equal(state.activeLane.executionTopology.requestedSimultaneous, true); + assert.equal(state.activeLane.concurrentLaneStatus.executionBundle.status, 'committed'); + assert.equal(state.activeLane.concurrentLaneStatus.executionBundle.planeBinding, 'dual-plane-parity'); + assert.equal(state.activeLane.concurrentLaneStatus.executionBundle.cellClass, 'kernel-coordinator'); + assert.equal(state.activeLane.concurrentLaneStatus.executionBundle.suiteClass, 'dual-plane-parity'); + assert.equal(state.activeLane.concurrentLaneStatus.executionBundle.harnessKind, 'teststand-compare-harness'); + assert.equal(state.activeLane.concurrentLaneStatus.executionBundle.premiumSaganMode, true); + assert.equal(state.activeLane.concurrentLaneStatus.executionBundle.reciprocalLinkReady, true); + assert.equal(state.activeLane.concurrentLaneStatus.executionBundle.effectiveBillableRateUsdPerHour, 375); + assert.equal( + state.activeLane.concurrentLaneStatus.executionBundle.operatorAuthorizationRef, + 'budget-auth://operator/session-2026-03-24' + ); assert.equal(state.activeLane.planeTransition.from, 'origin'); assert.equal(state.activeLane.planeTransition.to, 'upstream'); assert.equal(state.artifacts.planeTransition.action, 'promote'); diff --git a/tools/priority/__tests__/docker-lane-handshake-schema.test.mjs b/tools/priority/__tests__/docker-lane-handshake-schema.test.mjs new file mode 100644 index 000000000..8356ac13f --- /dev/null +++ b/tools/priority/__tests__/docker-lane-handshake-schema.test.mjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import Ajv2020 from 'ajv/dist/2020.js'; +import addFormats from 'ajv-formats'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); + +test('docker lane handshake report schema validates a premium Sagan dual-lane grant receipt', async () => { + const stateSchemaPath = path.join(repoRoot, 'docs', 'schemas', 'docker-lane-handshake-v1.schema.json'); + const reportSchemaPath = path.join(repoRoot, 'docs', 'schemas', 'docker-lane-handshake-report-v1.schema.json'); + const stateSchema = JSON.parse(await readFile(stateSchemaPath, 'utf8')); + const reportSchema = JSON.parse(await readFile(reportSchemaPath, 'utf8')); + + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv); + ajv.addSchema(stateSchema, stateSchema.$id); + const validate = ajv.compile(reportSchema); + + const report = { + schema: 'priority/docker-lane-handshake-report@v1', + generatedAt: '2026-03-23T23:55:00.000Z', + action: 'grant', + status: 'granted', + laneId: 'docker-agent-sagan-dual-01', + handshakePath: 'C:/repo/.git/docker-lane-handshakes/docker-agent-sagan-dual-01.json', + policy: { + operatorId: 'sergio', + currency: 'USD', + laborRateUsdPerHour: 250, + premiumSaganRateMultiplier: 1.5 + }, + handshake: { + schema: 'priority/docker-lane-handshake@v1', + generatedAt: '2026-03-23T23:55:00.000Z', + laneId: 'docker-agent-sagan-dual-01', + resourceKind: 'docker-lane', + state: 'granted', + sequence: 2, + heartbeatAt: '2026-03-23T23:55:00.000Z', + host: { + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + platform: 'windows', + computerName: 'canonical-builder', + canonical: { + version: '10.0.26200', + buildNumber: '26200', + ubr: 8037 + } + }, + request: { + requestId: 'request-123', + requestedAt: '2026-03-23T23:54:00.000Z', + agentId: 'sagan', + agentClass: 'sagan', + capabilities: ['docker-lane', 'native-labview-2026-32'], + premiumDualLaneRequested: true, + operatorId: 'sergio', + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-23' + }, + grant: { + grantedAt: '2026-03-23T23:55:00.000Z', + grantor: 'sagan-governor', + leaseId: 'lease-123', + ttlSeconds: 1800, + grantedCapabilities: ['docker-lane', 'native-labview-2026-32'], + billableRateMultiplier: 1.5, + billableRateUsdPerHour: 375, + premiumSaganMode: true, + policyDecision: 'sagan-premium-dual-lane', + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-23' + }, + commit: null, + release: null + }, + summary: { + handshakeState: 'granted', + leaseId: 'lease-123', + holder: 'sagan', + premiumSaganMode: true, + billableRateMultiplier: 1.5, + billableRateUsdPerHour: 375, + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-23', + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + linkedExecutionCellId: null, + linkedExecutionCellLeaseId: null, + isStale: false, + ageSeconds: 0, + ttlSeconds: 1800, + denialReasons: [], + observations: [] + } + }; + + assert.equal(validate(report), true, JSON.stringify(validate.errors, null, 2)); +}); diff --git a/tools/priority/__tests__/docker-lane-handshake.test.mjs b/tools/priority/__tests__/docker-lane-handshake.test.mjs new file mode 100644 index 000000000..b6d181af7 --- /dev/null +++ b/tools/priority/__tests__/docker-lane-handshake.test.mjs @@ -0,0 +1,441 @@ +#!/usr/bin/env node + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { + DEFAULT_TTL_SECONDS, + DOCKER_LANE_CAPABILITY, + NATIVE_LV32_CAPABILITY, + PREMIUM_RATE_MULTIPLIER, + handshakePathForLane, + isHandshakeStale, + main, + runDockerLaneHandshake +} from '../docker-lane-handshake.mjs'; + +async function withTempDir(prefix, fn) { + const root = await fs.mkdtemp(path.join(os.tmpdir(), `${prefix}-`)); + try { + return await fn(root); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } +} + +async function writeJson(filePath, payload) { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); +} + +function createHostPlaneReport() { + return { + schema: 'labview-2026-host-plane-report@v1', + host: { + computerName: 'canonical-builder', + osFingerprint: { + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + platform: 'windows', + canonical: { + version: '10.0.26200', + buildNumber: '26200', + ubr: 8037 + } + } + } + }; +} + +function createOperatorCostProfile() { + return { + schema: 'priority/operator-cost-profile@v1', + currency: 'USD', + defaultOperatorId: 'sergio', + operators: [ + { + id: 'sergio', + laborRateUsdPerHour: 250, + active: true + } + ] + }; +} + +test('request creates a docker-lane handshake with host isolated-lane fingerprint context', async () => { + await withTempDir('docker-lane-handshake-request', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + const report = await runDockerLaneHandshake({ + action: 'request', + laneId: 'docker-agent-epicurus-linux-01', + agentId: 'epicurus', + agentClass: 'subagent', + capabilities: [DOCKER_LANE_CAPABILITY], + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot: path.join(root, 'handshakes'), + repoRoot: root, + now: new Date('2026-03-23T23:50:00.000Z') + }); + + assert.equal(report.status, 'requested'); + assert.equal(report.handshake.state, 'requested'); + assert.equal(report.handshake.host.isolatedLaneGroupId, createHostPlaneReport().host.osFingerprint.isolatedLaneGroupId); + assert.equal(report.handshake.request.premiumDualLaneRequested, false); + }); +}); + +test('grant computes ordinary subagent docker rate from operator profile', async () => { + await withTempDir('docker-lane-handshake-grant-ordinary', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const handshakeRoot = path.join(root, 'handshakes'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + await runDockerLaneHandshake({ + action: 'request', + laneId: 'docker-agent-singer-linux-01', + agentId: 'singer', + agentClass: 'subagent', + capabilities: [DOCKER_LANE_CAPABILITY], + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-23T23:50:00.000Z') + }); + + const report = await runDockerLaneHandshake({ + action: 'grant', + laneId: 'docker-agent-singer-linux-01', + agentId: 'singer', + agentClass: 'subagent', + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-23T23:51:00.000Z') + }); + + assert.equal(report.status, 'granted'); + assert.equal(report.handshake.grant.billableRateMultiplier, 1); + assert.equal(report.handshake.grant.billableRateUsdPerHour, 250); + assert.equal(report.handshake.grant.premiumSaganMode, false); + assert.equal(report.handshake.grant.ttlSeconds, DEFAULT_TTL_SECONDS); + }); +}); + +test('grant denies premium dual-lane requests for subagents', async () => { + await withTempDir('docker-lane-handshake-deny-subagent-premium', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const handshakeRoot = path.join(root, 'handshakes'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + await runDockerLaneHandshake({ + action: 'request', + laneId: 'docker-agent-hooke-linux-01', + agentId: 'hooke', + agentClass: 'subagent', + capabilities: [DOCKER_LANE_CAPABILITY, NATIVE_LV32_CAPABILITY], + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-23T23:50:00.000Z') + }); + + const report = await runDockerLaneHandshake({ + action: 'grant', + laneId: 'docker-agent-hooke-linux-01', + agentId: 'hooke', + agentClass: 'subagent', + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-23T23:51:00.000Z') + }); + + assert.equal(report.status, 'denied'); + assert.match(report.summary.denialReasons.join('\n'), /premium-sagan-only/); + }); +}); + +test('grant requires operator authorization for premium Sagan dual-lane mode and computes 1.5x rate when authorized', async () => { + await withTempDir('docker-lane-handshake-sagan-premium', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const handshakeRoot = path.join(root, 'handshakes'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + await runDockerLaneHandshake({ + action: 'request', + laneId: 'docker-agent-sagan-dual-01', + agentId: 'sagan', + agentClass: 'sagan', + capabilities: [DOCKER_LANE_CAPABILITY, NATIVE_LV32_CAPABILITY], + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-23T23:50:00.000Z') + }); + + const denied = await runDockerLaneHandshake({ + action: 'grant', + laneId: 'docker-agent-sagan-dual-01', + agentId: 'sagan', + agentClass: 'sagan', + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-23T23:51:00.000Z') + }); + assert.equal(denied.status, 'denied'); + assert.match(denied.summary.denialReasons.join('\n'), /operator-authorization-required/); + + const requestPath = handshakePathForLane('docker-agent-sagan-dual-01', handshakeRoot); + const existing = JSON.parse(await fs.readFile(requestPath, 'utf8')); + existing.request.operatorAuthorizationRef = 'budget-auth://operator/session-2026-03-23'; + await writeJson(requestPath, existing); + + const granted = await runDockerLaneHandshake({ + action: 'grant', + laneId: 'docker-agent-sagan-dual-01', + agentId: 'sagan', + agentClass: 'sagan', + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-23T23:52:00.000Z') + }); + + assert.equal(granted.status, 'granted'); + assert.equal(granted.handshake.grant.premiumSaganMode, true); + assert.equal(granted.handshake.grant.billableRateMultiplier, PREMIUM_RATE_MULTIPLIER); + assert.equal(granted.handshake.grant.billableRateUsdPerHour, 375); + assert.equal(granted.summary.handshakeState, 'granted'); + assert.equal(granted.summary.leaseId, granted.handshake.grant.leaseId); + }); +}); + +test('commit heartbeat and release keep the same handshake and permit active inspection', async () => { + await withTempDir('docker-lane-handshake-life-cycle', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const handshakeRoot = path.join(root, 'handshakes'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + await runDockerLaneHandshake({ + action: 'request', + laneId: 'docker-agent-mill-linux-01', + agentId: 'mill', + agentClass: 'subagent', + capabilities: [DOCKER_LANE_CAPABILITY], + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-23T23:50:00.000Z') + }); + + const granted = await runDockerLaneHandshake({ + action: 'grant', + laneId: 'docker-agent-mill-linux-01', + agentId: 'mill', + agentClass: 'subagent', + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-23T23:51:00.000Z') + }); + + const committed = await runDockerLaneHandshake({ + action: 'commit', + laneId: 'docker-agent-mill-linux-01', + agentId: 'mill', + leaseId: granted.handshake.grant.leaseId, + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-23T23:52:00.000Z') + }); + assert.equal(committed.status, 'committed'); + assert.equal(committed.handshake.state, 'active'); + assert.equal(committed.summary.linkedExecutionCellId, null); + assert.equal(committed.summary.linkedExecutionCellLeaseId, null); + + const renewed = await runDockerLaneHandshake({ + action: 'heartbeat', + laneId: 'docker-agent-mill-linux-01', + agentId: 'mill', + leaseId: granted.handshake.grant.leaseId, + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-23T23:53:00.000Z') + }); + assert.equal(renewed.status, 'renewed'); + assert.equal(renewed.handshake.state, 'active'); + + const activeInspect = await runDockerLaneHandshake({ + action: 'inspect', + laneId: 'docker-agent-mill-linux-01', + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-23T23:53:30.000Z') + }); + assert.equal(activeInspect.status, 'active'); + assert.equal(isHandshakeStale(activeInspect.handshake, Date.parse('2026-03-23T23:53:30.000Z')), false); + + const release = await runDockerLaneHandshake({ + action: 'release', + laneId: 'docker-agent-mill-linux-01', + agentId: 'mill', + leaseId: granted.handshake.grant.leaseId, + artifactPaths: ['tests/results/_agent/runtime/docker-lane-proof.json'], + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-23T23:54:00.000Z') + }); + assert.equal(release.status, 'released'); + assert.equal(release.handshake.state, 'released'); + assert.equal(release.summary.handshakeState, 'released'); + assert.deepEqual(release.handshake.release.artifactPaths, ['tests/results/_agent/runtime/docker-lane-proof.json']); + }); +}); + +test('commit binds docker lane to a linked execution-cell report with the same agent and host fingerprint', async () => { + await withTempDir('docker-lane-handshake-linked-cell', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const handshakeRoot = path.join(root, 'handshakes'); + const executionCellReportPath = path.join(root, 'execution-cell-report.json'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + await writeJson(executionCellReportPath, { + schema: 'priority/execution-cell-lease-report@v1', + cellId: 'exec-cell-boyle-02', + lease: { + cellId: 'exec-cell-boyle-02', + host: createHostPlaneReport().host.osFingerprint, + request: { + agentId: 'boyle', + planeBinding: 'native-labview-2026-64', + harnessKind: 'teststand-compare-harness' + }, + grant: { leaseId: 'exec-lease-123' } + }, + summary: { + holder: 'boyle', + leaseId: 'exec-lease-123', + harnessKind: 'teststand-compare-harness', + planeBinding: 'native-labview-2026-64', + isolatedLaneGroupId: createHostPlaneReport().host.osFingerprint.isolatedLaneGroupId, + fingerprintSha256: createHostPlaneReport().host.osFingerprint.fingerprintSha256 + } + }); + + await runDockerLaneHandshake({ + action: 'request', + laneId: 'docker-agent-boyle-02', + agentId: 'boyle', + agentClass: 'subagent', + capabilities: [DOCKER_LANE_CAPABILITY], + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-24T00:20:00.000Z') + }); + const granted = await runDockerLaneHandshake({ + action: 'grant', + laneId: 'docker-agent-boyle-02', + agentId: 'boyle', + agentClass: 'subagent', + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-24T00:21:00.000Z') + }); + + const committed = await runDockerLaneHandshake({ + action: 'commit', + laneId: 'docker-agent-boyle-02', + agentId: 'boyle', + leaseId: granted.handshake.grant.leaseId, + executionCellReportPath, + hostPlaneReportPath, + operatorCostProfilePath, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-24T00:22:00.000Z') + }); + + assert.equal(committed.status, 'committed'); + assert.equal(committed.handshake.commit.executionCellId, 'exec-cell-boyle-02'); + assert.equal(committed.handshake.commit.executionCellLeaseId, 'exec-lease-123'); + assert.equal(committed.summary.linkedExecutionCellId, 'exec-cell-boyle-02'); + assert.equal(committed.summary.linkedExecutionCellLeaseId, 'exec-lease-123'); + }); +}); + +test('docker lane handshake CLI main writes a request receipt', async () => { + await withTempDir('docker-lane-handshake-cli', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const outputPath = path.join(root, 'docker-lane-handshake-cli.json'); + const handshakeRoot = path.join(root, 'docker-lane-handshakes'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + const exitCode = await main([ + 'node', + path.join(root, 'docker-lane-handshake.mjs'), + '--action', + 'request', + '--lane-id', + 'docker-agent-boyle-05', + '--agent-id', + 'boyle', + '--agent-class', + 'subagent', + '--capability', + 'docker-lane', + '--host-plane-report', + hostPlaneReportPath, + '--operator-cost-profile', + operatorCostProfilePath, + '--handshake-root', + handshakeRoot, + '--output', + outputPath + ]); + + assert.equal(exitCode, 0); + const receipt = JSON.parse(await fs.readFile(outputPath, 'utf8')); + assert.equal(receipt.status, 'requested'); + assert.equal(receipt.laneId, 'docker-agent-boyle-05'); + assert.equal(receipt.summary.handshakeState, 'requested'); + }); +}); diff --git a/tools/priority/__tests__/execution-cell-bundle-schema.test.mjs b/tools/priority/__tests__/execution-cell-bundle-schema.test.mjs new file mode 100644 index 000000000..4a190392d --- /dev/null +++ b/tools/priority/__tests__/execution-cell-bundle-schema.test.mjs @@ -0,0 +1,247 @@ +#!/usr/bin/env node + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import Ajv2020 from 'ajv/dist/2020.js'; +import addFormats from 'ajv-formats'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); + +test('execution cell bundle report schema validates a premium Sagan kernel receipt', async () => { + const executionCellStateSchema = JSON.parse( + await readFile(path.join(repoRoot, 'docs', 'schemas', 'execution-cell-lease-v1.schema.json'), 'utf8') + ); + const executionCellReportSchema = JSON.parse( + await readFile(path.join(repoRoot, 'docs', 'schemas', 'execution-cell-lease-report-v1.schema.json'), 'utf8') + ); + const dockerStateSchema = JSON.parse( + await readFile(path.join(repoRoot, 'docs', 'schemas', 'docker-lane-handshake-v1.schema.json'), 'utf8') + ); + const dockerReportSchema = JSON.parse( + await readFile(path.join(repoRoot, 'docs', 'schemas', 'docker-lane-handshake-report-v1.schema.json'), 'utf8') + ); + const bundleSchema = JSON.parse( + await readFile(path.join(repoRoot, 'docs', 'schemas', 'execution-cell-bundle-report-v1.schema.json'), 'utf8') + ); + + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv); + ajv.addSchema(executionCellStateSchema, executionCellStateSchema.$id); + ajv.addSchema(executionCellReportSchema, executionCellReportSchema.$id); + ajv.addSchema(dockerStateSchema, dockerStateSchema.$id); + ajv.addSchema(dockerReportSchema, dockerReportSchema.$id); + const validate = ajv.compile(bundleSchema); + + const report = { + schema: 'priority/execution-cell-bundle-report@v1', + generatedAt: '2026-03-24T02:10:00.000Z', + action: 'grant', + status: 'granted', + cellId: 'exec-cell-sagan-kernel-02', + laneId: 'docker-agent-sagan-kernel-02', + outputPath: 'tests/results/_agent/runtime/execution-cell-bundle.json', + executionCellReportPath: 'tests/results/_agent/runtime/execution-cell-lease.json', + dockerLaneReportPath: 'tests/results/_agent/runtime/docker-lane-handshake.json', + executionCell: { + schema: 'priority/execution-cell-lease-report@v1', + generatedAt: '2026-03-24T02:10:00.000Z', + action: 'grant', + status: 'granted', + cellId: 'exec-cell-sagan-kernel-02', + leasePath: 'C:/repo/.git/execution-cell-leases/exec-cell-sagan-kernel-02.json', + policy: { + operatorId: 'sergio', + currency: 'USD', + laborRateUsdPerHour: 250 + }, + lease: { + schema: 'priority/execution-cell-lease@v1', + generatedAt: '2026-03-24T02:10:00.000Z', + cellId: 'exec-cell-sagan-kernel-02', + resourceKind: 'execution-cell', + state: 'granted', + sequence: 2, + heartbeatAt: '2026-03-24T02:10:00.000Z', + host: { + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + platform: 'windows', + computerName: 'canonical-builder', + canonical: { + version: '10.0.26200', + buildNumber: '26200', + ubr: 8037 + } + }, + request: { + requestId: 'request-123', + requestedAt: '2026-03-24T02:09:00.000Z', + agentId: 'sagan', + agentClass: 'sagan', + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + planeBinding: 'dual-plane-parity', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'docker-lane', 'native-labview-2026-32'], + premiumDualLaneRequested: true, + operatorId: 'sergio', + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + workingRoot: 'E:/comparevi-lanes/cells/sagan-kernel-02/work', + artifactRoot: 'E:/comparevi-lanes/cells/sagan-kernel-02/artifacts' + }, + grant: { + grantedAt: '2026-03-24T02:10:00.000Z', + grantor: 'execution-cell-governor', + leaseId: 'lease-123', + ttlSeconds: 1800, + premiumDualLaneRequested: true, + premiumSaganMode: true, + policyDecision: 'sagan-premium-dual-lane', + grantedCapabilities: ['teststand-harness', 'docker-lane', 'native-labview-2026-32'], + billableRateMultiplier: 1.5, + billableRateUsdPerHour: 375 + }, + commit: null, + release: null + }, + summary: { + leaseState: 'granted', + leaseId: 'lease-123', + holder: 'sagan', + agentClass: 'sagan', + cellClass: 'kernel-coordinator', + harnessKind: 'teststand-compare-harness', + harnessInstanceId: null, + suiteClass: 'dual-plane-parity', + planeBinding: 'dual-plane-parity', + premiumSaganMode: true, + billableRateMultiplier: 1.5, + billableRateUsdPerHour: 375, + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + linkedDockerLaneId: null, + linkedDockerLaneLeaseId: null, + workingRoot: 'E:/comparevi-lanes/cells/sagan-kernel-02/work', + artifactRoot: 'E:/comparevi-lanes/cells/sagan-kernel-02/artifacts', + isStale: false, + ageSeconds: 0, + ttlSeconds: 1800, + denialReasons: [], + observations: [] + } + }, + dockerLane: { + schema: 'priority/docker-lane-handshake-report@v1', + generatedAt: '2026-03-24T02:10:00.000Z', + action: 'grant', + status: 'granted', + laneId: 'docker-agent-sagan-kernel-02', + handshakePath: 'C:/repo/.git/docker-lane-handshakes/docker-agent-sagan-kernel-02.json', + policy: { + operatorId: 'sergio', + currency: 'USD', + laborRateUsdPerHour: 250, + premiumSaganRateMultiplier: 1.5 + }, + handshake: { + schema: 'priority/docker-lane-handshake@v1', + generatedAt: '2026-03-24T02:10:00.000Z', + laneId: 'docker-agent-sagan-kernel-02', + resourceKind: 'docker-lane', + state: 'granted', + sequence: 2, + heartbeatAt: '2026-03-24T02:10:00.000Z', + host: { + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + platform: 'windows', + computerName: 'canonical-builder', + canonical: { + version: '10.0.26200', + buildNumber: '26200', + ubr: 8037 + } + }, + request: { + requestId: 'request-456', + requestedAt: '2026-03-24T02:09:00.000Z', + agentId: 'sagan', + agentClass: 'sagan', + capabilities: ['docker-lane', 'native-labview-2026-32', 'teststand-harness'], + premiumDualLaneRequested: true, + operatorId: 'sergio', + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24' + }, + grant: { + grantedAt: '2026-03-24T02:10:00.000Z', + grantor: 'sagan-governor', + leaseId: 'lease-456', + ttlSeconds: 1800, + grantedCapabilities: ['docker-lane', 'native-labview-2026-32', 'teststand-harness'], + billableRateMultiplier: 1.5, + billableRateUsdPerHour: 375, + premiumSaganMode: true, + policyDecision: 'sagan-premium-dual-lane', + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24' + }, + commit: null, + release: null + }, + summary: { + handshakeState: 'granted', + leaseId: 'lease-456', + holder: 'sagan', + premiumSaganMode: true, + billableRateMultiplier: 1.5, + billableRateUsdPerHour: 375, + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + linkedExecutionCellId: null, + linkedExecutionCellLeaseId: null, + isStale: false, + ageSeconds: 0, + ttlSeconds: 1800, + denialReasons: [], + observations: [] + } + }, + rollbacks: { + executionCell: null, + dockerLane: null + }, + summary: { + holder: 'sagan', + agentClass: 'sagan', + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + planeBinding: 'dual-plane-parity', + harnessKind: 'teststand-compare-harness', + harnessInstanceId: null, + executionCellLeaseId: 'lease-123', + dockerLaneLeaseId: 'lease-456', + linkedExecutionCellId: null, + linkedExecutionCellLeaseId: null, + linkedDockerLaneId: null, + linkedDockerLaneLeaseId: null, + reciprocalLinkReady: false, + dockerRequested: true, + windowsNativeTestStand: true, + effectiveBillableRateMultiplier: 1.5, + effectiveBillableRateUsdPerHour: 375, + premiumSaganMode: true, + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + capabilities: ['teststand-harness', 'docker-lane', 'native-labview-2026-32'], + denialReasons: [], + observations: ['agent-billed-once-at-effective-rate'] + } + }; + + assert.equal(validate(report), true, JSON.stringify(validate.errors, null, 2)); +}); diff --git a/tools/priority/__tests__/execution-cell-bundle.test.mjs b/tools/priority/__tests__/execution-cell-bundle.test.mjs new file mode 100644 index 000000000..d96d89794 --- /dev/null +++ b/tools/priority/__tests__/execution-cell-bundle.test.mjs @@ -0,0 +1,401 @@ +#!/usr/bin/env node + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { main, runExecutionCellBundle } from '../execution-cell-bundle.mjs'; + +async function withTempDir(prefix, fn) { + const root = await fs.mkdtemp(path.join(os.tmpdir(), `${prefix}-`)); + try { + return await fn(root); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } +} + +async function writeJson(filePath, payload) { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); +} + +function createHostPlaneReport() { + return { + schema: 'labview-2026-host-plane-report@v1', + host: { + computerName: 'canonical-builder', + osFingerprint: { + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + platform: 'windows', + canonical: { + version: '10.0.26200', + buildNumber: '26200', + ubr: 8037 + } + } + } + }; +} + +function createOperatorCostProfile() { + return { + schema: 'priority/operator-cost-profile@v1', + currency: 'USD', + defaultOperatorId: 'sergio', + operators: [ + { + id: 'sergio', + laborRateUsdPerHour: 250, + active: true + } + ] + }; +} + +test('execution-cell bundle grants coordinated worker cell and docker lane at ordinary rate', async () => { + await withTempDir('execution-cell-bundle-worker', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const leaseRoot = path.join(root, 'execution-cell-leases'); + const handshakeRoot = path.join(root, 'docker-lane-handshakes'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + const requested = await runExecutionCellBundle({ + action: 'request', + cellId: 'exec-cell-hooke-02', + laneId: 'docker-agent-hooke-02', + agentId: 'hooke', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'single-compare', + planeBinding: 'native-labview-2026-64', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'docker-lane'], + workingRoot: 'E:/comparevi-lanes/cells/hooke-02/work', + artifactRoot: 'E:/comparevi-lanes/cells/hooke-02/artifacts', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-24T02:00:00.000Z') + }); + + assert.equal(requested.status, 'requested'); + assert.equal(requested.summary.dockerRequested, true); + + const granted = await runExecutionCellBundle({ + action: 'grant', + cellId: 'exec-cell-hooke-02', + laneId: 'docker-agent-hooke-02', + agentId: 'hooke', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'single-compare', + planeBinding: 'native-labview-2026-64', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'docker-lane'], + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-24T02:01:00.000Z') + }); + + assert.equal(granted.status, 'granted'); + assert.equal(granted.executionCell.status, 'granted'); + assert.equal(granted.dockerLane.status, 'granted'); + assert.equal(granted.summary.effectiveBillableRateUsdPerHour, 250); + assert.equal(granted.summary.premiumSaganMode, false); + assert.equal(granted.summary.windowsNativeTestStand, true); + assert.equal(granted.summary.reciprocalLinkReady, false); + }); +}); + +test('execution-cell bundle infers premium Sagan dual-lane mode for dual-plane parity with docker', async () => { + await withTempDir('execution-cell-bundle-sagan-premium', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const leaseRoot = path.join(root, 'execution-cell-leases'); + const handshakeRoot = path.join(root, 'docker-lane-handshakes'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + await runExecutionCellBundle({ + action: 'request', + cellId: 'exec-cell-sagan-kernel-02', + laneId: 'docker-agent-sagan-kernel-02', + agentId: 'sagan', + agentClass: 'sagan', + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + planeBinding: 'dual-plane-parity', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'docker-lane'], + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + workingRoot: 'E:/comparevi-lanes/cells/sagan-kernel-02/work', + artifactRoot: 'E:/comparevi-lanes/cells/sagan-kernel-02/artifacts', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-24T02:02:00.000Z') + }); + + const granted = await runExecutionCellBundle({ + action: 'grant', + cellId: 'exec-cell-sagan-kernel-02', + laneId: 'docker-agent-sagan-kernel-02', + agentId: 'sagan', + agentClass: 'sagan', + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + planeBinding: 'dual-plane-parity', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'docker-lane'], + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-24T02:03:00.000Z') + }); + + assert.equal(granted.status, 'granted'); + assert.equal(granted.executionCell.status, 'granted'); + assert.equal(granted.dockerLane.status, 'granted'); + assert.equal(granted.summary.premiumSaganMode, true); + assert.equal(granted.summary.effectiveBillableRateUsdPerHour, 375); + assert.equal(granted.summary.reciprocalLinkReady, false); + assert.deepEqual( + granted.summary.capabilities.sort(), + ['docker-lane', 'native-labview-2026-32', 'teststand-harness'].sort() + ); + }); +}); + +test('execution-cell bundle denies linux-bound TestStand cells and rolls back requested docker lanes', async () => { + await withTempDir('execution-cell-bundle-linux-teststand', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const leaseRoot = path.join(root, 'execution-cell-leases'); + const handshakeRoot = path.join(root, 'docker-lane-handshakes'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + await runExecutionCellBundle({ + action: 'request', + cellId: 'exec-cell-mill-linux-01', + laneId: 'docker-agent-mill-linux-01', + agentId: 'mill', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'single-compare', + planeBinding: 'docker-desktop/linux-container-2026', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'docker-lane'], + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-24T02:04:00.000Z') + }); + + const denied = await runExecutionCellBundle({ + action: 'grant', + cellId: 'exec-cell-mill-linux-01', + laneId: 'docker-agent-mill-linux-01', + agentId: 'mill', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'single-compare', + planeBinding: 'docker-desktop/linux-container-2026', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'docker-lane'], + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-24T02:05:00.000Z') + }); + + assert.equal(denied.status, 'denied'); + assert.match(denied.summary.denialReasons.join('\n'), /teststand-windows-native-only/); + assert.equal(denied.rollbacks.dockerLane.status, 'released'); + }); +}); + +test('execution-cell bundle commits and releases both leases together', async () => { + await withTempDir('execution-cell-bundle-commit-release', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const leaseRoot = path.join(root, 'execution-cell-leases'); + const handshakeRoot = path.join(root, 'docker-lane-handshakes'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + await runExecutionCellBundle({ + action: 'request', + cellId: 'exec-cell-epicurus-03', + laneId: 'docker-agent-epicurus-03', + agentId: 'epicurus', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'single-compare', + planeBinding: 'native-labview-2026-64', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'docker-lane'], + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-24T02:06:00.000Z') + }); + await runExecutionCellBundle({ + action: 'grant', + cellId: 'exec-cell-epicurus-03', + laneId: 'docker-agent-epicurus-03', + agentId: 'epicurus', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'single-compare', + planeBinding: 'native-labview-2026-64', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'docker-lane'], + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-24T02:07:00.000Z') + }); + + const committed = await runExecutionCellBundle({ + action: 'commit', + cellId: 'exec-cell-epicurus-03', + laneId: 'docker-agent-epicurus-03', + agentId: 'epicurus', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'single-compare', + planeBinding: 'native-labview-2026-64', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'docker-lane'], + harnessInstanceId: 'harness-epicurus-03', + workingRoot: 'E:/comparevi-lanes/cells/epicurus-03/work', + artifactRoot: 'E:/comparevi-lanes/cells/epicurus-03/artifacts', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-24T02:08:00.000Z') + }); + + assert.equal(committed.status, 'committed'); + assert.equal(committed.executionCell.status, 'committed'); + assert.equal(committed.dockerLane.status, 'committed'); + assert.equal(committed.summary.executionCellLeaseId, committed.executionCell.summary.leaseId); + assert.equal(committed.summary.dockerLaneLeaseId, committed.dockerLane.summary.leaseId); + assert.equal(committed.summary.linkedExecutionCellId, 'exec-cell-epicurus-03'); + assert.equal(committed.summary.linkedDockerLaneId, 'docker-agent-epicurus-03'); + assert.equal(committed.executionCell.summary.linkedDockerLaneId, 'docker-agent-epicurus-03'); + assert.equal( + committed.executionCell.summary.linkedDockerLaneLeaseId, + committed.dockerLane.summary.leaseId + ); + assert.equal(committed.dockerLane.summary.linkedExecutionCellId, 'exec-cell-epicurus-03'); + assert.equal( + committed.dockerLane.summary.linkedExecutionCellLeaseId, + committed.executionCell.summary.leaseId + ); + assert.equal(committed.summary.reciprocalLinkReady, true); + + const released = await runExecutionCellBundle({ + action: 'release', + cellId: 'exec-cell-epicurus-03', + laneId: 'docker-agent-epicurus-03', + agentId: 'epicurus', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'single-compare', + planeBinding: 'native-labview-2026-64', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'docker-lane'], + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + handshakeRoot, + repoRoot: root, + now: new Date('2026-03-24T02:09:00.000Z') + }); + + assert.equal(released.status, 'released'); + assert.equal(released.executionCell.status, 'released'); + assert.equal(released.dockerLane.status, 'released'); + }); +}); + +test('execution-cell bundle CLI main writes a request receipt', async () => { + await withTempDir('execution-cell-bundle-cli', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const outputPath = path.join(root, 'execution-cell-bundle-cli.json'); + const leaseRoot = path.join(root, 'execution-cell-leases'); + const handshakeRoot = path.join(root, 'docker-lane-handshakes'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + const exitCode = await main([ + 'node', + path.join(root, 'execution-cell-bundle.mjs'), + '--action', + 'request', + '--cell-id', + 'exec-cell-boyle-04', + '--lane-id', + 'docker-agent-boyle-04', + '--agent-id', + 'boyle', + '--agent-class', + 'subagent', + '--cell-class', + 'worker', + '--suite-class', + 'single-compare', + '--plane-binding', + 'native-labview-2026-64', + '--capability', + 'teststand-harness', + '--capability', + 'docker-lane', + '--host-plane-report', + hostPlaneReportPath, + '--operator-cost-profile', + operatorCostProfilePath, + '--lease-root', + leaseRoot, + '--handshake-root', + handshakeRoot, + '--output', + outputPath + ]); + + assert.equal(exitCode, 0); + const receipt = JSON.parse(await fs.readFile(outputPath, 'utf8')); + assert.equal(receipt.status, 'requested'); + assert.equal(receipt.cellId, 'exec-cell-boyle-04'); + assert.equal(receipt.laneId, 'docker-agent-boyle-04'); + }); +}); diff --git a/tools/priority/__tests__/execution-cell-lease-schema.test.mjs b/tools/priority/__tests__/execution-cell-lease-schema.test.mjs new file mode 100644 index 000000000..881835172 --- /dev/null +++ b/tools/priority/__tests__/execution-cell-lease-schema.test.mjs @@ -0,0 +1,122 @@ +#!/usr/bin/env node + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import Ajv2020 from 'ajv/dist/2020.js'; +import addFormats from 'ajv-formats'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); + +test('execution cell lease report schema validates an active teststand harness lease', async () => { + const stateSchemaPath = path.join(repoRoot, 'docs', 'schemas', 'execution-cell-lease-v1.schema.json'); + const reportSchemaPath = path.join(repoRoot, 'docs', 'schemas', 'execution-cell-lease-report-v1.schema.json'); + const stateSchema = JSON.parse(await readFile(stateSchemaPath, 'utf8')); + const reportSchema = JSON.parse(await readFile(reportSchemaPath, 'utf8')); + + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv); + ajv.addSchema(stateSchema, stateSchema.$id); + const validate = ajv.compile(reportSchema); + + const report = { + schema: 'priority/execution-cell-lease-report@v1', + generatedAt: '2026-03-24T00:00:00.000Z', + action: 'commit', + status: 'committed', + cellId: 'exec-cell-hooke-01', + leasePath: 'C:/repo/.git/execution-cell-leases/exec-cell-hooke-01.json', + policy: { + operatorId: 'sergio', + currency: 'USD', + laborRateUsdPerHour: 250 + }, + lease: { + schema: 'priority/execution-cell-lease@v1', + generatedAt: '2026-03-24T00:00:00.000Z', + cellId: 'exec-cell-hooke-01', + resourceKind: 'execution-cell', + state: 'active', + sequence: 3, + heartbeatAt: '2026-03-24T00:00:00.000Z', + host: { + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + platform: 'windows', + computerName: 'canonical-builder', + canonical: { + version: '10.0.26200', + buildNumber: '26200', + ubr: 8037 + } + }, + request: { + requestId: 'request-123', + requestedAt: '2026-03-23T23:58:00.000Z', + agentId: 'hooke', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'dual-plane-parity', + planeBinding: 'native-labview-2026-dual', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'dual-plane-parity'], + premiumDualLaneRequested: false, + operatorId: 'sergio', + operatorAuthorizationRef: null, + workingRoot: 'E:/comparevi-lanes/cells/hooke-01/work', + artifactRoot: 'E:/comparevi-lanes/cells/hooke-01/artifacts' + }, + grant: { + grantedAt: '2026-03-23T23:59:00.000Z', + grantor: 'execution-cell-governor', + leaseId: 'lease-123', + ttlSeconds: 1800, + premiumDualLaneRequested: false, + premiumSaganMode: false, + policyDecision: 'ordinary-execution-cell', + grantedCapabilities: ['teststand-harness', 'dual-plane-parity'], + billableRateMultiplier: 1, + billableRateUsdPerHour: 250 + }, + commit: { + committedAt: '2026-03-24T00:00:00.000Z', + harnessInstanceId: 'harness-hooke-01', + dockerLaneId: 'docker-agent-hooke-01', + dockerLaneLeaseId: 'docker-lease-123', + workingRoot: 'E:/comparevi-lanes/cells/hooke-01/work', + artifactRoot: 'E:/comparevi-lanes/cells/hooke-01/artifacts' + }, + release: null + }, + summary: { + leaseState: 'active', + leaseId: 'lease-123', + holder: 'hooke', + agentClass: 'subagent', + cellClass: 'worker', + harnessKind: 'teststand-compare-harness', + harnessInstanceId: 'harness-hooke-01', + suiteClass: 'dual-plane-parity', + planeBinding: 'native-labview-2026-dual', + premiumSaganMode: false, + billableRateMultiplier: 1, + billableRateUsdPerHour: 250, + operatorAuthorizationRef: null, + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + linkedDockerLaneId: 'docker-agent-hooke-01', + linkedDockerLaneLeaseId: 'docker-lease-123', + workingRoot: 'E:/comparevi-lanes/cells/hooke-01/work', + artifactRoot: 'E:/comparevi-lanes/cells/hooke-01/artifacts', + isStale: false, + ageSeconds: 0, + ttlSeconds: 1800, + denialReasons: [], + observations: [] + } + }; + + assert.equal(validate(report), true, JSON.stringify(validate.errors, null, 2)); +}); diff --git a/tools/priority/__tests__/execution-cell-lease.test.mjs b/tools/priority/__tests__/execution-cell-lease.test.mjs new file mode 100644 index 000000000..d64e82fcf --- /dev/null +++ b/tools/priority/__tests__/execution-cell-lease.test.mjs @@ -0,0 +1,459 @@ +#!/usr/bin/env node + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { + DEFAULT_TTL_SECONDS, + isExecutionCellLeaseStale, + leasePathForCell, + runExecutionCellLease +} from '../execution-cell-lease.mjs'; + +async function withTempDir(prefix, fn) { + const root = await fs.mkdtemp(path.join(os.tmpdir(), `${prefix}-`)); + try { + return await fn(root); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } +} + +async function writeJson(filePath, payload) { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); +} + +function createHostPlaneReport() { + return { + schema: 'labview-2026-host-plane-report@v1', + host: { + computerName: 'canonical-builder', + osFingerprint: { + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + platform: 'windows', + canonical: { + version: '10.0.26200', + buildNumber: '26200', + ubr: 8037 + } + } + } + }; +} + +function createOperatorCostProfile() { + return { + schema: 'priority/operator-cost-profile@v1', + currency: 'USD', + defaultOperatorId: 'sergio', + operators: [ + { + id: 'sergio', + laborRateUsdPerHour: 250, + active: true + } + ] + }; +} + +test('request and grant create an execution cell lease with host fingerprint and operator rate', async () => { + await withTempDir('execution-cell-lease-request-grant', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const leaseRoot = path.join(root, 'leases'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + const requested = await runExecutionCellLease({ + action: 'request', + cellId: 'exec-cell-hooke-01', + agentId: 'hooke', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'dual-plane-parity', + planeBinding: 'native-labview-2026-dual', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'dual-plane-parity'], + workingRoot: 'E:/comparevi-lanes/cells/hooke-01/work', + artifactRoot: 'E:/comparevi-lanes/cells/hooke-01/artifacts', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T23:50:00.000Z') + }); + + assert.equal(requested.status, 'requested'); + assert.equal(requested.lease.request.agentId, 'hooke'); + assert.equal(requested.lease.request.cellClass, 'worker'); + assert.equal(requested.lease.request.suiteClass, 'dual-plane-parity'); + assert.equal(requested.lease.host.isolatedLaneGroupId, createHostPlaneReport().host.osFingerprint.isolatedLaneGroupId); + + const granted = await runExecutionCellLease({ + action: 'grant', + cellId: 'exec-cell-hooke-01', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T23:51:00.000Z') + }); + + assert.equal(granted.status, 'granted'); + assert.equal(granted.lease.grant.billableRateMultiplier, 1); + assert.equal(granted.lease.grant.billableRateUsdPerHour, 250); + assert.equal(granted.lease.grant.ttlSeconds, DEFAULT_TTL_SECONDS); + assert.equal(granted.lease.grant.premiumSaganMode, false); + }); +}); + +test('commit and release stamp the harness instance and artifact paths', async () => { + await withTempDir('execution-cell-lease-lifecycle', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const leaseRoot = path.join(root, 'leases'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + await runExecutionCellLease({ + action: 'request', + cellId: 'exec-cell-epicurus-02', + agentId: 'epicurus', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'single-compare', + planeBinding: 'native-labview-2026-64', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness'], + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T23:52:00.000Z') + }); + + const granted = await runExecutionCellLease({ + action: 'grant', + cellId: 'exec-cell-epicurus-02', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T23:53:00.000Z') + }); + + const committed = await runExecutionCellLease({ + action: 'commit', + cellId: 'exec-cell-epicurus-02', + leaseId: granted.lease.grant.leaseId, + harnessInstanceId: 'harness-epicurus-02', + workingRoot: 'E:/comparevi-lanes/cells/epicurus-02/work', + artifactRoot: 'E:/comparevi-lanes/cells/epicurus-02/artifacts', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T23:54:00.000Z') + }); + + assert.equal(committed.status, 'committed'); + assert.equal(committed.lease.state, 'active'); + assert.equal(committed.lease.commit.harnessInstanceId, 'harness-epicurus-02'); + assert.equal(committed.summary.linkedDockerLaneId, null); + assert.equal(committed.summary.linkedDockerLaneLeaseId, null); + + const released = await runExecutionCellLease({ + action: 'release', + cellId: 'exec-cell-epicurus-02', + leaseId: granted.lease.grant.leaseId, + artifactPaths: ['tests/results/_agent/runtime/teststand-session.json'], + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T23:55:00.000Z') + }); + + assert.equal(released.status, 'released'); + assert.equal(released.lease.release.artifactPaths[0], 'tests/results/_agent/runtime/teststand-session.json'); + }); +}); + +test('commit binds execution cell to a linked docker-lane report with the same agent and host fingerprint', async () => { + await withTempDir('execution-cell-lease-linked-docker', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const leaseRoot = path.join(root, 'leases'); + const dockerLaneReportPath = path.join(root, 'docker-lane-report.json'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + await writeJson(dockerLaneReportPath, { + schema: 'priority/docker-lane-handshake-report@v1', + laneId: 'docker-agent-boyle-01', + handshake: { + laneId: 'docker-agent-boyle-01', + host: createHostPlaneReport().host.osFingerprint, + request: { agentId: 'boyle' }, + grant: { leaseId: 'docker-lease-123' } + }, + summary: { + holder: 'boyle', + leaseId: 'docker-lease-123', + isolatedLaneGroupId: createHostPlaneReport().host.osFingerprint.isolatedLaneGroupId, + fingerprintSha256: createHostPlaneReport().host.osFingerprint.fingerprintSha256 + } + }); + + await runExecutionCellLease({ + action: 'request', + cellId: 'exec-cell-boyle-01', + agentId: 'boyle', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'single-compare', + planeBinding: 'native-labview-2026-64', + capabilities: ['teststand-harness'], + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-24T00:10:00.000Z') + }); + const granted = await runExecutionCellLease({ + action: 'grant', + cellId: 'exec-cell-boyle-01', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-24T00:11:00.000Z') + }); + + const committed = await runExecutionCellLease({ + action: 'commit', + cellId: 'exec-cell-boyle-01', + leaseId: granted.lease.grant.leaseId, + harnessInstanceId: 'harness-boyle-01', + dockerLaneReportPath, + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-24T00:12:00.000Z') + }); + + assert.equal(committed.status, 'committed'); + assert.equal(committed.lease.commit.dockerLaneId, 'docker-agent-boyle-01'); + assert.equal(committed.lease.commit.dockerLaneLeaseId, 'docker-lease-123'); + assert.equal(committed.summary.linkedDockerLaneId, 'docker-agent-boyle-01'); + assert.equal(committed.summary.linkedDockerLaneLeaseId, 'docker-lease-123'); + }); +}); + +test('inspect reports staleness for abandoned active execution cells', async () => { + await withTempDir('execution-cell-lease-stale', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const leaseRoot = path.join(root, 'leases'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + await runExecutionCellLease({ + action: 'request', + cellId: 'exec-cell-singer-03', + agentId: 'singer', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'single-compare', + planeBinding: 'native-labview-2026-32', + capabilities: ['teststand-harness'], + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T20:00:00.000Z') + }); + const granted = await runExecutionCellLease({ + action: 'grant', + cellId: 'exec-cell-singer-03', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T20:01:00.000Z') + }); + await runExecutionCellLease({ + action: 'commit', + cellId: 'exec-cell-singer-03', + leaseId: granted.lease.grant.leaseId, + harnessInstanceId: 'harness-singer-03', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T20:02:00.000Z') + }); + + const leasePath = leasePathForCell('exec-cell-singer-03', leaseRoot); + const existing = JSON.parse(await fs.readFile(leasePath, 'utf8')); + assert.equal(isExecutionCellLeaseStale(existing, Date.parse('2026-03-23T21:00:01.000Z')), true); + + const inspected = await runExecutionCellLease({ + action: 'inspect', + cellId: 'exec-cell-singer-03', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T21:00:01.000Z') + }); + + assert.equal(inspected.status, 'stale'); + assert.equal(inspected.summary.isStale, true); + }); +}); + +test('premium dual-lane kernel cell requires Sagan authorization and applies premium labor rate', async () => { + await withTempDir('execution-cell-lease-premium-sagan', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const leaseRoot = path.join(root, 'leases'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + await runExecutionCellLease({ + action: 'request', + cellId: 'exec-cell-sagan-kernel-01', + agentId: 'sagan', + agentClass: 'sagan', + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + planeBinding: 'dual-plane-parity', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness', 'docker-lane', 'native-labview-2026-32'], + operatorAuthorizationRef: 'operator-premium-approved-2026-03-23', + workingRoot: 'E:/comparevi-lanes/cells/sagan-kernel-01/work', + artifactRoot: 'E:/comparevi-lanes/cells/sagan-kernel-01/artifacts', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T23:56:00.000Z') + }); + + const granted = await runExecutionCellLease({ + action: 'grant', + cellId: 'exec-cell-sagan-kernel-01', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T23:57:00.000Z') + }); + + assert.equal(granted.status, 'granted'); + assert.equal(granted.lease.request.cellClass, 'kernel-coordinator'); + assert.equal(granted.lease.request.premiumDualLaneRequested, true); + assert.equal(granted.lease.grant.premiumSaganMode, true); + assert.equal(granted.lease.grant.policyDecision, 'sagan-premium-dual-lane'); + assert.deepEqual(granted.lease.grant.grantedCapabilities.sort(), [ + 'docker-lane', + 'native-labview-2026-32', + 'teststand-harness' + ]); + assert.equal(granted.lease.grant.billableRateMultiplier, 1.5); + assert.equal(granted.lease.grant.billableRateUsdPerHour, 375); + assert.equal(granted.summary.premiumSaganMode, true); + assert.equal(granted.summary.cellClass, 'kernel-coordinator'); + assert.equal(granted.summary.operatorAuthorizationRef, 'operator-premium-approved-2026-03-23'); + }); +}); + +test('premium dual-lane kernel cell is denied for non-Sagan worker requests', async () => { + await withTempDir('execution-cell-lease-premium-denied', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const leaseRoot = path.join(root, 'leases'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + await runExecutionCellLease({ + action: 'request', + cellId: 'exec-cell-hooke-kernel-01', + agentId: 'hooke', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'dual-plane-parity', + planeBinding: 'dual-plane-parity', + harnessKind: 'teststand-compare-harness', + capabilities: ['docker-lane', 'native-labview-2026-32'], + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T23:58:00.000Z') + }); + + const denied = await runExecutionCellLease({ + action: 'grant', + cellId: 'exec-cell-hooke-kernel-01', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-23T23:59:00.000Z') + }); + + assert.equal(denied.status, 'denied'); + assert.deepEqual(denied.summary.denialReasons.sort(), [ + 'operator-authorization-required', + 'premium-kernel-cell-required', + 'premium-sagan-only' + ]); + }); +}); + +test('teststand execution cells fail closed for linux or container plane bindings', async () => { + await withTempDir('execution-cell-lease-teststand-windows-only', async (root) => { + const hostPlaneReportPath = path.join(root, 'host-plane.json'); + const operatorCostProfilePath = path.join(root, 'operator-cost-profile.json'); + const leaseRoot = path.join(root, 'leases'); + await writeJson(hostPlaneReportPath, createHostPlaneReport()); + await writeJson(operatorCostProfilePath, createOperatorCostProfile()); + + await runExecutionCellLease({ + action: 'request', + cellId: 'exec-cell-hooke-linux-01', + agentId: 'hooke', + agentClass: 'subagent', + cellClass: 'worker', + suiteClass: 'single-compare', + planeBinding: 'docker-desktop/linux-container-2026', + harnessKind: 'teststand-compare-harness', + capabilities: ['teststand-harness'], + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-24T00:02:00.000Z') + }); + + const denied = await runExecutionCellLease({ + action: 'grant', + cellId: 'exec-cell-hooke-linux-01', + hostPlaneReportPath, + operatorCostProfilePath, + leaseRoot, + repoRoot: root, + now: new Date('2026-03-24T00:03:00.000Z') + }); + + assert.equal(denied.status, 'denied'); + assert.equal(denied.summary.denialReasons.includes('teststand-windows-native-only'), true); + }); +}); diff --git a/tools/priority/__tests__/github-comment-budget-hook-schema.test.mjs b/tools/priority/__tests__/github-comment-budget-hook-schema.test.mjs new file mode 100644 index 000000000..6e58d71ac --- /dev/null +++ b/tools/priority/__tests__/github-comment-budget-hook-schema.test.mjs @@ -0,0 +1,91 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { Ajv2020 } from 'ajv/dist/2020.js'; +import addFormats from 'ajv-formats'; + +import { runGitHubCommentBudgetHook } from '../github-comment-budget-hook.mjs'; + +const repoRoot = path.resolve(process.cwd()); + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function writeJson(filePath, payload) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); +} + +test('github-comment-budget-hook report and policy validate against checked-in schemas', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'github-comment-budget-hook-schema-')); + const repo = path.join(tempDir, 'repo'); + const policyPath = path.join(repo, 'tools', 'policy', 'github-comment-budget-hook.json'); + writeJson(policyPath, { + schema: 'priority/github-comment-budget-hook-policy@v1', + costRollupPath: 'tests/results/_agent/cost/agent-cost-rollup.json', + materializationPolicyPath: 'tools/policy/agent-cost-rollup-materialization.json', + materializationReportPath: 'tests/results/_agent/cost/agent-cost-rollup-materialization.json', + outputPath: 'tests/results/_agent/cost/github-comment-budget-hook.json', + markdownOutputPath: 'tests/results/_agent/cost/github-comment-budget-hook.md', + operatorBudgetCapUsd: 50000, + materializeCostRollup: true, + reservedFundingPurposes: ['calibration'], + reservedActivationStates: ['hold'] + }); + + const { report } = runGitHubCommentBudgetHook( + { + repoRoot: repo, + policyPath, + targetKind: 'pr', + targetNumber: 1908 + }, + { + runMaterializeAgentCostRollupFn: ({ costRollupPath, outputPath }) => { + writeJson(costRollupPath, { + schema: 'priority/agent-cost-rollup@v1', + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + summary: { + metrics: { + totalTurns: 1, + liveTurnCount: 0, + backgroundTurnCount: 1, + totalUsd: 1.5, + operatorLaborUsd: 4, + operatorLaborMissingTurnCount: 0, + blendedTotalUsd: 5.5, + estimatedPrepaidUsdRemaining: 398.5 + }, + provenance: { + operatorProfiles: [{ operatorProfilePath: 'tools/policy/operator-cost-profile.json' }], + invoiceTurns: [] + } + }, + billingWindow: { + invoiceTurnId: 'invoice-turn-2026-03-HQ1VJLMV-0027', + invoiceId: 'HQ1VJLMV-0027', + fundingPurpose: 'operational', + activationState: 'active', + prepaidUsd: 400, + pricingBasis: 'prepaid-credit', + selection: { mode: 'hold', reason: null } + } + }); + writeJson(outputPath, { schema: 'priority/agent-cost-rollup-materialization@v1', summary: { status: 'pass' } }); + return { costRollupPath, outputPath }; + } + } + ); + + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv); + const validatePolicy = ajv.compile(readJson(path.join(repoRoot, 'docs', 'schemas', 'github-comment-budget-hook-policy-v1.schema.json'))); + const validateReport = ajv.compile(readJson(path.join(repoRoot, 'docs', 'schemas', 'github-comment-budget-hook-report-v1.schema.json'))); + + assert.equal(validatePolicy(readJson(policyPath)), true, JSON.stringify(validatePolicy.errors, null, 2)); + assert.equal(validateReport(report), true, JSON.stringify(validateReport.errors, null, 2)); +}); diff --git a/tools/priority/__tests__/github-comment-budget-hook.test.mjs b/tools/priority/__tests__/github-comment-budget-hook.test.mjs new file mode 100644 index 000000000..3802ac3aa --- /dev/null +++ b/tools/priority/__tests__/github-comment-budget-hook.test.mjs @@ -0,0 +1,145 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { + COMMENT_HOOK_END_MARKER, + COMMENT_HOOK_START_MARKER, + appendBudgetHook, + runGitHubCommentBudgetHook, + stripExistingBudgetHook +} from '../github-comment-budget-hook.mjs'; + +function writeJson(filePath, payload) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); +} + +function createRollupFixture() { + return { + schema: 'priority/agent-cost-rollup@v1', + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + summary: { + metrics: { + totalTurns: 3, + liveTurnCount: 1, + backgroundTurnCount: 2, + totalUsd: 12.5, + operatorLaborUsd: 30, + operatorLaborMissingTurnCount: 1, + blendedTotalUsd: null, + estimatedPrepaidUsdRemaining: 387.5 + }, + provenance: { + operatorProfiles: [ + { + operatorProfilePath: 'tools/policy/operator-cost-profile.json' + } + ], + invoiceTurns: [ + { + invoiceTurnId: 'invoice-turn-2026-03-HQ1VJLMV-0027', + invoiceId: 'HQ1VJLMV-0027', + fundingPurpose: 'operational', + activationState: 'active', + prepaidUsd: 400, + operatorNote: 'Operational window.' + }, + { + invoiceTurnId: 'invoice-turn-2026-03-HQ1VJLMV-0028', + invoiceId: 'HQ1VJLMV-0028', + fundingPurpose: 'calibration', + activationState: 'hold', + prepaidUsd: 100, + operatorNote: 'Reserved calibration window.' + } + ] + } + }, + billingWindow: { + invoiceTurnId: 'invoice-turn-2026-03-HQ1VJLMV-0027', + invoiceId: 'HQ1VJLMV-0027', + fundingPurpose: 'operational', + activationState: 'active', + prepaidUsd: 400, + pricingBasis: 'prepaid-credit', + selection: { + mode: 'hold', + reason: 'Calibration funding window remains on hold before activation.' + } + } + }; +} + +test('stripExistingBudgetHook removes the previous budget block cleanly', () => { + const original = ['Intro line', '', COMMENT_HOOK_START_MARKER, 'old hook', COMMENT_HOOK_END_MARKER, '', 'Tail line'].join('\n'); + assert.equal(stripExistingBudgetHook(original), 'Intro line\n\nTail line'); +}); + +test('appendBudgetHook appends exactly one hook block', () => { + const hook = `${COMMENT_HOOK_START_MARKER}\nHook\n${COMMENT_HOOK_END_MARKER}`; + const once = appendBudgetHook('Hello', hook); + const twice = appendBudgetHook(once, hook); + assert.equal(once, twice); + assert.match(twice, /Hello/); + assert.match(twice, new RegExp(COMMENT_HOOK_START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); +}); + +test('runGitHubCommentBudgetHook emits a durable lower-bound budget hook with reserved calibration context', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'github-comment-budget-hook-')); + const repoRoot = path.join(tempDir, 'repo'); + fs.mkdirSync(repoRoot, { recursive: true }); + const policyPath = path.join(repoRoot, 'tools', 'policy', 'github-comment-budget-hook.json'); + const outputPath = path.join(repoRoot, 'tests', 'results', '_agent', 'cost', 'github-comment-budget-hook.json'); + const markdownOutputPath = path.join(repoRoot, 'tests', 'results', '_agent', 'cost', 'github-comment-budget-hook.md'); + const rollupPath = path.join(repoRoot, 'tests', 'results', '_agent', 'cost', 'agent-cost-rollup.json'); + + writeJson(policyPath, { + schema: 'priority/github-comment-budget-hook-policy@v1', + costRollupPath: 'tests/results/_agent/cost/agent-cost-rollup.json', + materializationPolicyPath: 'tools/policy/agent-cost-rollup-materialization.json', + materializationReportPath: 'tests/results/_agent/cost/agent-cost-rollup-materialization.json', + outputPath: 'tests/results/_agent/cost/github-comment-budget-hook.json', + markdownOutputPath: 'tests/results/_agent/cost/github-comment-budget-hook.md', + operatorBudgetCapUsd: 50000, + materializeCostRollup: true, + reservedFundingPurposes: ['calibration'], + reservedActivationStates: ['hold'] + }); + + const result = runGitHubCommentBudgetHook( + { + repoRoot, + policyPath, + targetKind: 'issue', + targetNumber: 1907 + }, + { + runMaterializeAgentCostRollupFn: ({ costRollupPath, outputPath: materializationPath }) => { + writeJson(costRollupPath, createRollupFixture()); + writeJson(materializationPath, { + schema: 'priority/agent-cost-rollup-materialization@v1', + summary: { status: 'pass' } + }); + return { + costRollupPath, + outputPath: materializationPath + }; + } + } + ); + + assert.equal(result.report.summary.status, 'warn'); + assert.equal(result.report.summary.operatorBudgetCapUsd, 50000); + assert.equal(result.report.summary.operatorBudgetRemainingStatus, 'lower-bound'); + assert.equal(result.report.summary.observedBlendedLowerBoundUsd, 42.5); + assert.equal(result.report.turns.backgroundTurnCount, 2); + assert.equal(result.report.funding.reservedFunding.count, 1); + assert.equal(result.report.funding.reservedFunding.totalReservedUsd, 100); + assert.equal(fs.existsSync(outputPath), true); + assert.equal(fs.existsSync(markdownOutputPath), true); + assert.match(result.markdown, /blended lower bound \$42\.500000/); + assert.match(result.markdown, /calibration reserve \$100\.000000/); +}); diff --git a/tools/priority/__tests__/handoff-entrypoint-contract.test.mjs b/tools/priority/__tests__/handoff-entrypoint-contract.test.mjs index 2d6b17a0c..984b6b74b 100644 --- a/tools/priority/__tests__/handoff-entrypoint-contract.test.mjs +++ b/tools/priority/__tests__/handoff-entrypoint-contract.test.mjs @@ -31,6 +31,7 @@ test('AGENT_HANDOFF stays bounded and points agents to live state artifacts', () assert.match(handoff, /tests\/results\/_agent\/handoff\/entrypoint-status\.json/); assert.match(handoff, /tests\/results\/_agent\/handoff\/monitoring-mode\.json/); assert.match(handoff, /tests\/results\/_agent\/handoff\/autonomous-governor-summary\.json/); + assert.match(handoff, /tests\/results\/_agent\/handoff\/sagan-context-concentrator\.json/); assert.match(handoff, /tests\/results\/_agent\/handoff\/autonomous-governor-portfolio-summary\.json/); assert.match(handoff, /tests\/results\/_agent\/handoff\/\*\.json/); assert.match(handoff, /tests\/results\/_agent\/sessions\/\*\.json/); @@ -62,18 +63,26 @@ test('handoff entrypoint contract is wired into automation and operator docs', ( assert.match(printHandoff, /continuity-summary\.json/); assert.match(printHandoff, /handoff-monitoring-mode\.mjs/); assert.match(printHandoff, /monitoring-mode\.json/); + assert.match(printHandoff, /release-published-bundle-observer\.mjs/); + assert.match(printHandoff, /release-published-bundle-observer\.json/); + assert.match(printHandoff, /release-signing-readiness\.mjs/); + assert.match(printHandoff, /release-signing-readiness\.json/); assert.match(printHandoff, /autonomous-governor-summary\.mjs/); assert.match(printHandoff, /autonomous-governor-summary\.json/); assert.match(printHandoff, /autonomous-governor-portfolio-summary\.mjs/); assert.match(printHandoff, /autonomous-governor-portfolio-summary\.json/); + assert.match(printHandoff, /sagan-context-concentrator\.mjs/); + assert.match(printHandoff, /sagan-context-concentrator\.json/); assert.match(printHandoff, /docker-review-loop-summary\.json/); assert.match(importHandoff, /entrypoint-status\.json/); assert.match(importHandoff, /continuity-summary\.json/); assert.match(importHandoff, /monitoring-mode\.json/); assert.match(importHandoff, /autonomous-governor-summary\.json/); assert.match(importHandoff, /autonomous-governor-portfolio-summary\.json/); + assert.match(importHandoff, /sagan-context-concentrator\.json/); assert.match(importHandoff, /\[handoff\] Autonomous governor summary/); assert.match(importHandoff, /\[handoff\] Governor portfolio summary/); + assert.match(importHandoff, /\[handoff\] Context concentrator/); assert.match(importHandoff, /\[handoff\] Monitoring mode/); assert.match(importHandoff, /\[handoff\] Continuity summary/); assert.match(importHandoff, /docker-review-loop-summary\.json/); @@ -94,8 +103,12 @@ test('handoff entrypoint contract is wired into automation and operator docs', ( assert.match(handoffGuide, /monitoring-mode\.json/); assert.match(handoffGuide, /autonomous-governor-summary\.json/); assert.match(handoffGuide, /autonomous-governor-portfolio-summary\.json/); + assert.match(handoffGuide, /sagan-context-concentrator\.json/); assert.match(handoffGuide, /docker-review-loop-summary\.json/); + assert.match(handoffGuide, /release-signing/i); assert.match(handoffGuide, /priority:handoff/); + assert.match(handoffGuide, /priority:context:concentrate/); + assert.match(handoffGuide, /subagent/i); assert.match(handoffGuide, /queue-empty/); assert.match(handoffGuide, /future agents may pivot/i); }); diff --git a/tools/priority/__tests__/labview-2026-host-plane-report-schema.test.mjs b/tools/priority/__tests__/labview-2026-host-plane-report-schema.test.mjs index 376eb36ff..8dd548f3b 100644 --- a/tools/priority/__tests__/labview-2026-host-plane-report-schema.test.mjs +++ b/tools/priority/__tests__/labview-2026-host-plane-report-schema.test.mjs @@ -16,7 +16,52 @@ test('labview-2026 host plane schema validates the shadow-plane policy contract' const report = { schema: 'labview-2026-host-plane-report@v1', generatedAt: '2026-03-21T00:00:00.000Z', - host: { os: 'windows', computerName: 'builder' }, + host: { + os: 'windows', + computerName: 'builder', + osFingerprint: { + role: 'canonical-host-baseline', + comparisonScope: 'isolated-lane-group', + platform: 'windows', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + canonical: { + version: '10.0.26200', + buildNumber: '26200', + ubr: 8037, + displayVersion: '25H2', + editionId: 'Professional', + installationType: 'Client', + architecture: '64-bit', + systemType: 'x64-based PC', + buildLabEx: '26100.1.amd64fre.ge_release.240331-1435' + }, + advisory: { + caption: 'Microsoft Windows 11 Pro', + productName: 'Windows 10 Pro', + currentVersionCompatibility: '6.3', + brandingMismatch: true, + installDate: '2026-02-14T03:49:47.0000000-08:00', + lastBootUpTime: '2026-03-20T09:06:51.0000000-07:00' + }, + sources: { + registryPath: 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion', + cimClass: 'Win32_OperatingSystem', + systemClass: 'Win32_ComputerSystem', + comparisonFields: [ + 'version', + 'buildNumber', + 'ubr', + 'displayVersion', + 'editionId', + 'installationType', + 'architecture', + 'systemType', + 'buildLabEx' + ] + } + } + }, runner: { hostIsRunner: true, runnerName: 'builder', githubActions: false }, docker: { operatorLabels: ['linux-docker-fast-loop', 'windows-docker-fast-loop', 'dual-docker-fast-loop'] diff --git a/tools/priority/__tests__/labview-2026-host-plane-runbook.test.mjs b/tools/priority/__tests__/labview-2026-host-plane-runbook.test.mjs index cd30fce3d..2ee3d28db 100644 --- a/tools/priority/__tests__/labview-2026-host-plane-runbook.test.mjs +++ b/tools/priority/__tests__/labview-2026-host-plane-runbook.test.mjs @@ -27,12 +27,19 @@ test('single-host runbook points to the authoritative commands and artifacts', ( const runbook = readFile(runbookPath); assert.match(runbook, /node tools\/npm\/run-script\.mjs env:labview:2026:host-planes/); + assert.match(runbook, /node tools\/npm\/run-script\.mjs priority:lane:docker:handshake/); assert.match(runbook, /pwsh -NoLogo -NoProfile -File tools\/Test-DockerDesktopFastLoop\.ps1 -LaneScope linux -StepTimeoutSeconds 600/); assert.match(runbook, /pwsh -NoLogo -NoProfile -File tools\/Test-DockerDesktopFastLoop\.ps1 -LaneScope windows -StepTimeoutSeconds 600/); assert.match(runbook, /pwsh -NoLogo -NoProfile -File tools\/Test-DockerDesktopFastLoop\.ps1 -LaneScope both -StepTimeoutSeconds 600/); + assert.match(runbook, /tools\/TestStand-CompareHarness\.ps1/); assert.match(runbook, /node tools\/npm\/run-script\.mjs history:diagnostics:show -- --ResultsRoot tests\/results\/local-parity\/windows/); assert.match(runbook, /labview-2026-host-plane-report\.json/); assert.match(runbook, /labview-2026-host-plane-summary\.md/); + assert.match(runbook, /docker-lane-handshake\.json/); + assert.match(runbook, /Only `sagan` may lease `docker-lane` and `native-labview-2026-32` simultaneously/); + assert.match(runbook, /operatorAuthorizationRef/); + assert.match(runbook, /host\.osFingerprint/); + assert.match(runbook, /fingerprintSha256/); assert.match(runbook, /docker-runtime-fastloop-readiness\.json/); assert.match(runbook, /docker-fast-loop-summary-path/); assert.match(runbook, /docker-fast-loop-status-path/); @@ -66,6 +73,8 @@ test('developer guide and documentation manifest point back to the single-host r assert.ok(entry, 'documentation manifest should include the host-plane diagnostics entry'); assert.ok(entry.files.includes('docs/SINGLE_HOST_LABVIEW_2026_PLANES.md')); assert.ok(entry.files.includes('docs/DEVELOPER_GUIDE.md')); + assert.ok(entry.files.includes('tools/priority/docker-lane-handshake.mjs')); + assert.ok(entry.files.includes('tools/TestStand-CompareHarness.ps1')); assert.ok(entry.files.includes('tools/Write-DockerFastLoopReadiness.ps1')); assert.ok(entry.files.includes('tools/Write-DockerFastLoopProof.ps1')); assert.ok(entry.files.includes('tools/Show-DockerFastLoopDiagnostics.ps1')); diff --git a/tools/priority/__tests__/monitoring-work-injection-schema.test.mjs b/tools/priority/__tests__/monitoring-work-injection-schema.test.mjs index 3b4fc7d5f..d5ef8e7ea 100644 --- a/tools/priority/__tests__/monitoring-work-injection-schema.test.mjs +++ b/tools/priority/__tests__/monitoring-work-injection-schema.test.mjs @@ -124,11 +124,30 @@ test('monitoring work injection report matches schema', async () => { monitoringStatus: 'active', futureAgentAction: 'future-agent-may-pivot', governorMode: 'compare-governance-work', - nextAction: 'continue-compare-governance-work' + nextAction: 'continue-compare-governance-work', + queueHandoffStatus: null, + queueHandoffNextWakeCondition: null, + queueHandoffPrUrl: null, + queueAuthoritySource: null }, portfolio: { repositoryCount: 4, repositories: [], + dependencies: [ + { + id: 'vi-history-producer-native-distributor', + status: 'unknown', + ownerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + dependentRepository: 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate', + requiredCapability: 'vi-history', + source: 'compare-release-signing-readiness', + releaseSigningStatus: null, + releasePublicationState: null, + signingCapabilityState: null, + externalBlocker: null, + detail: 'fixture' + } + ], unsupportedPaths: [] }, summary: { @@ -141,6 +160,14 @@ test('monitoring work injection report matches schema', async () => { templateMonitoringStatus: 'pass', supportedProofStatus: 'pass', repoGraphStatus: 'pass', + queueHandoffStatus: null, + queueHandoffNextWakeCondition: null, + queueHandoffPrUrl: null, + queueAuthoritySource: null, + viHistoryDistributorDependencyStatus: 'unknown', + viHistoryDistributorDependencyTargetRepository: 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate', + viHistoryDistributorDependencyExternalBlocker: null, + viHistoryDistributorDependencyPublicationState: null, portfolioWakeConditionCount: 0, triggeredWakeConditions: [] } diff --git a/tools/priority/__tests__/monitoring-work-injection.test.mjs b/tools/priority/__tests__/monitoring-work-injection.test.mjs index 465701d83..bdef2b0ad 100644 --- a/tools/priority/__tests__/monitoring-work-injection.test.mjs +++ b/tools/priority/__tests__/monitoring-work-injection.test.mjs @@ -64,7 +64,10 @@ function createGovernorPortfolioSummary({ governorMode = 'compare-governance-work', nextAction = 'continue-compare-governance-work', ownerDecisionSource = 'compare-governor-summary', - status = 'active' + status = 'active', + viHistoryDistributorDependencyStatus = 'unknown', + viHistoryDistributorDependencyExternalBlocker = null, + viHistoryDistributorDependencyPublicationState = null } = {}) { return { schema: 'priority/autonomous-governor-portfolio-summary-report@v1', @@ -81,11 +84,30 @@ function createGovernorPortfolioSummary({ monitoringStatus: 'active', futureAgentAction: 'future-agent-may-pivot', governorMode, - nextAction + nextAction, + queueHandoffStatus: null, + queueHandoffNextWakeCondition: null, + queueHandoffPrUrl: null, + queueAuthoritySource: null }, portfolio: { repositoryCount: 4, repositories: [], + dependencies: [ + { + id: 'vi-history-producer-native-distributor', + status: viHistoryDistributorDependencyStatus, + ownerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + dependentRepository: 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate', + requiredCapability: 'vi-history', + source: 'compare-release-signing-readiness', + releaseSigningStatus: null, + releasePublicationState: viHistoryDistributorDependencyPublicationState, + signingCapabilityState: null, + externalBlocker: viHistoryDistributorDependencyExternalBlocker, + detail: 'fixture' + } + ], unsupportedPaths: [] }, summary: { @@ -98,6 +120,14 @@ function createGovernorPortfolioSummary({ templateMonitoringStatus: 'pass', supportedProofStatus: 'pass', repoGraphStatus: 'pass', + queueHandoffStatus: null, + queueHandoffNextWakeCondition: null, + queueHandoffPrUrl: null, + queueAuthoritySource: null, + viHistoryDistributorDependencyStatus, + viHistoryDistributorDependencyTargetRepository: 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate', + viHistoryDistributorDependencyExternalBlocker, + viHistoryDistributorDependencyPublicationState, portfolioWakeConditionCount: 0, triggeredWakeConditions: [] } diff --git a/tools/priority/__tests__/pr-spend-projection.test.mjs b/tools/priority/__tests__/pr-spend-projection.test.mjs index e39be7198..6e0f22115 100644 --- a/tools/priority/__tests__/pr-spend-projection.test.mjs +++ b/tools/priority/__tests__/pr-spend-projection.test.mjs @@ -325,7 +325,10 @@ test('runPrSpendProjection writes JSON and markdown outputs and can upsert a com posted = { repo, prNumber, body }; return { posted: true, mode: 'update-existing-marker-comment' }; }, - lookupCurrentLoginFn: () => 'automation-user' + lookupCurrentLoginFn: () => 'automation-user', + runGitHubCommentBudgetHookFn: () => ({ + markdown: '\n_Budget hook_: stub.\n\n' + }) } ); @@ -409,7 +412,10 @@ test('runPrSpendProjection materializes a missing cost rollup before projection' outputPath: materializationReportPath, costRollupPath: requestedPath }; - } + }, + runGitHubCommentBudgetHookFn: () => ({ + markdown: '\n_Budget hook_: stub.\n\n' + }) } ); @@ -490,7 +496,10 @@ test('runPrSpendProjection rematerializes when an existing rollup has no matchin outputPath: requestedOutputPath, costRollupPath: requestedPath }; - } + }, + runGitHubCommentBudgetHookFn: () => ({ + markdown: '\n_Budget hook_: stub.\n\n' + }) } ); diff --git a/tools/priority/__tests__/release-conductor-workflow-contract.test.mjs b/tools/priority/__tests__/release-conductor-workflow-contract.test.mjs index d416ae1cd..2a808b2c6 100644 --- a/tools/priority/__tests__/release-conductor-workflow-contract.test.mjs +++ b/tools/priority/__tests__/release-conductor-workflow-contract.test.mjs @@ -12,9 +12,33 @@ test('release conductor workflow keeps workflow_run proposal-only when apply mod const workflowPath = path.join(repoRoot, '.github', 'workflows', 'release-conductor.yml'); const workflow = await readFile(workflowPath, 'utf8'); + assert.match(workflow, /repair_existing_tag:/); + assert.match(workflow, /description:\s+'Repair an existing authoritative tag as a signed annotated tag'/); assert.match(workflow, /RELEASE_CONDUCTOR_ENABLED:\s+\$\{\{\s*vars\.RELEASE_CONDUCTOR_ENABLED \|\| '0'\s*\}\}/); + assert.match(workflow, /name:\s+Configure release tag signing material/); + assert.match(workflow, /if \[\[ -z "\$\{RELEASE_TAG_SIGNING_PRIVATE_KEY:-\}" \]\]; then/); + assert.match(workflow, /if \[\[ -z "\$\{GH_TOKEN:-\}" \]\]; then/); + assert.match(workflow, /RELEASE_TAG_SIGNING_IDENTITY_NAME:\s+\$\{\{\s*vars\.RELEASE_TAG_SIGNING_IDENTITY_NAME \|\| ''\s*\}\}/); + assert.match(workflow, /RELEASE_TAG_SIGNING_IDENTITY_EMAIL:\s+\$\{\{\s*vars\.RELEASE_TAG_SIGNING_IDENTITY_EMAIL \|\| ''\s*\}\}/); + assert.match(workflow, /signing_login="\$\(gh api user --jq '\.login'\)"/); + assert.match(workflow, /signing_id="\$\(gh api user --jq '\.id'\)"/); + assert.match(workflow, /git config gpg\.format ssh/); + assert.match(workflow, /git config user\.signingkey "\$public_key_path"/); + assert.match(workflow, /git config user\.name "\$signing_name"/); + assert.match(workflow, /git config user\.email "\$signing_email"/); + assert.match(workflow, /RELEASE_TAG_SIGNING_BACKEND=ssh/); + assert.match(workflow, /RELEASE_TAG_SIGNING_SOURCE=workflow-secret/); + assert.match(workflow, /RELEASE_TAG_SIGNING_IDENTITY_SOURCE=\$identity_source/); assert.match( workflow, /elseif \(\$eventName -eq 'workflow_run'\) \{\s+\$apply = \$conductorEnabled\s+if \(-not \$apply\) \{\s+Write-Host 'Release conductor apply mode disabled; workflow_run will remain proposal-only\.'\s+\}\s+\}/ms ); + assert.match(workflow, /RELEASE_TAG_SIGNING_BACKEND:\s+\$\{\{\s*env\.RELEASE_TAG_SIGNING_BACKEND \|\| ''\s*\}\}/); + assert.match(workflow, /RELEASE_TAG_SIGNING_SOURCE:\s+\$\{\{\s*env\.RELEASE_TAG_SIGNING_SOURCE \|\| ''\s*\}\}/); + assert.match( + workflow, + /node tools\/npm\/run-script\.mjs priority:queue:supervisor -- --dry-run --report tests\/results\/_agent\/queue\/queue-supervisor-report\.json/ + ); + assert.match(workflow, /if \('\$\{\{\s*inputs\.repair_existing_tag\s*\}\}' -eq 'true'\) \{\s+\$args \+= '--repair-existing-tag'\s+\}/ms); + assert.match(workflow, /tests\/results\/_agent\/queue\/queue-supervisor-report\.json/); }); diff --git a/tools/priority/__tests__/release-conductor.test.mjs b/tools/priority/__tests__/release-conductor.test.mjs index 1b1a317c3..4878089a1 100644 --- a/tools/priority/__tests__/release-conductor.test.mjs +++ b/tools/priority/__tests__/release-conductor.test.mjs @@ -40,6 +40,7 @@ test('parseArgs applies defaults and supports burst-style apply flags', () => { const defaults = parseArgs(['node', 'release-conductor.mjs']); assert.equal(defaults.apply, false); assert.equal(defaults.dryRun, true); + assert.equal(defaults.repairExistingTag, false); assert.equal(defaults.channel, 'stable'); assert.equal(defaults.dwellMinutes, 60); @@ -47,6 +48,7 @@ test('parseArgs applies defaults and supports burst-style apply flags', () => { 'node', 'release-conductor.mjs', '--apply', + '--repair-existing-tag', '--repo', 'owner/repo', '--channel', @@ -60,6 +62,7 @@ test('parseArgs applies defaults and supports burst-style apply flags', () => { ]); assert.equal(parsed.apply, true); assert.equal(parsed.dryRun, false); + assert.equal(parsed.repairExistingTag, true); assert.equal(parsed.repo, 'owner/repo'); assert.equal(parsed.channel, 'rc'); assert.equal(parsed.version, '0.8.0-rc.1'); @@ -90,6 +93,34 @@ test('gate evaluators classify pass/fail deterministically', () => { assert.equal(queueFail.status, 'fail'); assert.ok(queueFail.reasons.includes('queue-paused')); + const queueIdlePass = evaluateQueueHealthGate({ + exists: true, + error: null, + payload: { + paused: true, + pausedReasons: ['success-rate-below-threshold'], + throughputController: { mode: 'stabilize' }, + runtimeFleet: { + totals: { + queued: 0, + inProgress: 0, + stalled: 0 + } + }, + queueInventory: { + mergeQueueOccupancy: 0, + readyQueuedCount: 0 + }, + summary: { + quarantinedCount: 0 + } + } + }); + assert.equal(queueIdlePass.status, 'pass'); + assert.equal(queueIdlePass.paused, true); + assert.equal(queueIdlePass.controllerMode, 'stabilize'); + assert.deepEqual(queueIdlePass.reasons, ['release-safe-idle-queue-pause']); + const policyPass = evaluatePolicySnapshotGate({ exists: true, error: null, @@ -118,6 +149,18 @@ test('gate evaluators classify pass/fail deterministically', () => { }); assert.equal(quarantineFail.status, 'fail'); assert.equal(quarantineFail.staleCount, 1); + + const quarantineUnavailable = evaluateQuarantineGate({ + now, + staleHours: 12, + queueReportEnvelope: { + exists: false, + error: null, + payload: null + } + }); + assert.equal(quarantineUnavailable.status, 'fail'); + assert.equal(quarantineUnavailable.staleHours, 12); }); test('runReleaseConductor blocks apply when release conductor flag is disabled', async () => { @@ -348,7 +391,7 @@ test('runReleaseConductor still blocks dry-run when the dwell window contains wo assert.ok(report.decision.blockers.some((entry) => entry.code === 'green-dwell-failed')); }); -test('runReleaseConductor creates signed tag when apply is enabled and signing key is available', async () => { +test('runReleaseConductor creates and publishes a signed tag when apply is enabled and signing key is available', async () => { const readJsonOptionalFn = async (filePath) => { const normalized = String(filePath); if (normalized.includes('queue-supervisor-report.json')) { @@ -386,11 +429,36 @@ test('runReleaseConductor creates signed tag when apply is enabled and signing k const runCommandFn = (command, args) => { commandCalls.push({ command, args }); if (command === 'git' && args[0] === 'config') { - return { status: 0, stdout: 'ABC123', stderr: '' }; + if (args[2] === 'user.signingkey') { + return { status: 0, stdout: '/tmp/release-signing.pub', stderr: '' }; + } + if (args[2] === 'gpg.format') { + return { status: 0, stdout: 'ssh', stderr: '' }; + } + if (args[2] === 'remote.upstream.url') { + return { status: 0, stdout: 'https://github.com/owner/repo.git', stderr: '' }; + } + return { status: 1, stdout: '', stderr: 'missing config' }; + } + if (command === 'git' && args[0] === 'ls-remote') { + return { + status: 0, + stdout: [ + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\trefs/tags/v0.8.0-rc.1', + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\trefs/tags/v0.8.0-rc.1^{}' + ].join('\n'), + stderr: '' + }; + } + if (command === 'git' && args[0] === 'rev-parse') { + return { status: 0, stdout: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n', stderr: '' }; } if (command === 'git' && args[0] === 'tag') { return { status: 0, stdout: '', stderr: '' }; } + if (command === 'git' && args[0] === 'push') { + return { status: 0, stdout: '', stderr: '' }; + } return { status: 0, stdout: '', stderr: '' }; }; @@ -425,10 +493,678 @@ test('runReleaseConductor creates signed tag when apply is enabled and signing k assert.equal(report.decision.status, 'pass'); assert.equal(report.release.proposalOnly, false); assert.equal(report.release.tagCreated, true); + assert.equal(report.release.tagPushed, true); + assert.equal(report.release.tagPushRemote.remoteName, 'upstream'); + assert.equal(report.release.signingMaterial.backend, 'ssh'); + assert.ok(commandCalls.some((entry) => entry.command === 'git' && entry.args[0] === 'tag')); + assert.ok(commandCalls.some((entry) => entry.command === 'git' && entry.args[0] === 'push')); +}); + +test('runReleaseConductor allows apply when queue pause is only an idle success-rate throttle', async () => { + const readJsonOptionalFn = async (filePath) => { + const normalized = String(filePath); + if (normalized.includes('queue-supervisor-report.json')) { + return { + exists: true, + error: null, + path: filePath, + payload: { + paused: true, + pausedReasons: ['success-rate-below-threshold'], + throughputController: { mode: 'stabilize' }, + runtimeFleet: { + totals: { + queued: 0, + inProgress: 0, + stalled: 0 + } + }, + queueInventory: { + mergeQueueOccupancy: 0, + readyQueuedCount: 0 + }, + summary: { + quarantinedCount: 0 + }, + retryHistory: {} + } + }; + } + return { + exists: true, + error: null, + path: filePath, + payload: { + schema: 'priority/policy-live-state@v1', + generatedAt: '2026-03-06T10:00:00Z', + state: {} + } + }; + }; + + const runGhJsonFn = (args) => { + if (args[0] === 'api') { + return makeWorkflowRunsResponse(String(args[1])); + } + throw new Error(`unexpected gh args: ${args.join(' ')}`); + }; + + const commandCalls = []; + const runCommandFn = (command, args) => { + commandCalls.push({ command, args }); + if (command === 'git' && args[0] === 'config') { + if (args[2] === 'user.signingkey') { + return { status: 0, stdout: '/tmp/release-signing.pub', stderr: '' }; + } + if (args[2] === 'gpg.format') { + return { status: 0, stdout: 'ssh', stderr: '' }; + } + if (args[2] === 'remote.upstream.url') { + return { status: 0, stdout: 'https://github.com/owner/repo.git', stderr: '' }; + } + return { status: 1, stdout: '', stderr: 'missing config' }; + } + if (command === 'git' && args[0] === 'tag') { + return { status: 0, stdout: '', stderr: '' }; + } + if (command === 'git' && args[0] === 'push') { + return { status: 0, stdout: '', stderr: '' }; + } + return { status: 0, stdout: '', stderr: '' }; + }; + + const { report, exitCode } = await runReleaseConductor({ + repoRoot: process.cwd(), + now: new Date('2026-03-06T12:00:00.000Z'), + args: { + apply: true, + dryRun: false, + repairExistingTag: false, + reportPath: 'tests/results/_agent/release/release-conductor-report.json', + queueReportPath: 'tests/results/_agent/queue/queue-supervisor-report.json', + policySnapshotPath: 'tests/results/_agent/policy/policy-state-snapshot.json', + repo: 'owner/repo', + stream: 'comparevi-cli', + channel: 'rc', + version: '0.8.0-rc.1', + dwellMinutes: 60, + quarantineStaleHours: 24, + help: false + }, + environment: { + GITHUB_REPOSITORY: 'owner/repo', + RELEASE_CONDUCTOR_ENABLED: '1' + }, + runGhJsonFn, + runCommandFn, + readJsonOptionalFn, + writeReportFn: async (reportPath) => reportPath + }); + + assert.equal(exitCode, 0); + assert.equal(report.gates.queueHealth.status, 'pass'); + assert.deepEqual(report.gates.queueHealth.reasons, ['release-safe-idle-queue-pause']); + assert.equal(report.release.proposalOnly, false); assert.ok(commandCalls.some((entry) => entry.command === 'git' && entry.args[0] === 'tag')); + assert.ok(commandCalls.some((entry) => entry.command === 'git' && entry.args[0] === 'push')); +}); + +test('runReleaseConductor blocks apply when authoritative tag already exists and repair mode is not requested', async () => { + const readJsonOptionalFn = async (filePath) => { + const normalized = String(filePath); + if (normalized.includes('queue-supervisor-report.json')) { + return { + exists: true, + error: null, + path: filePath, + payload: { + paused: false, + throughputController: { mode: 'healthy' }, + retryHistory: {} + } + }; + } + return { + exists: true, + error: null, + path: filePath, + payload: { + schema: 'priority/policy-live-state@v1', + generatedAt: '2026-03-06T10:00:00Z', + state: {} + } + }; + }; + + const runGhJsonFn = (args) => { + if (args[0] === 'api') { + return makeWorkflowRunsResponse(String(args[1])); + } + throw new Error(`unexpected gh args: ${args.join(' ')}`); + }; + + const commandCalls = []; + const runCommandFn = (command, args) => { + commandCalls.push({ command, args }); + if (command === 'git' && args[0] === 'config') { + if (args[2] === 'user.signingkey') { + return { status: 0, stdout: '/tmp/release-signing.pub', stderr: '' }; + } + if (args[2] === 'gpg.format') { + return { status: 0, stdout: 'ssh', stderr: '' }; + } + if (args[2] === 'remote.upstream.url') { + return { status: 0, stdout: 'https://github.com/owner/repo.git', stderr: '' }; + } + return { status: 1, stdout: '', stderr: 'missing config' }; + } + if (command === 'git' && args[0] === 'ls-remote') { + return { + status: 0, + stdout: [ + '1111111111111111111111111111111111111111\trefs/tags/v0.8.0-rc.1', + '2222222222222222222222222222222222222222\trefs/tags/v0.8.0-rc.1^{}' + ].join('\n'), + stderr: '' + }; + } + if (command === 'git' && args[0] === 'rev-parse') { + return { status: 1, stdout: '', stderr: '' }; + } + return { status: 0, stdout: '', stderr: '' }; + }; + + const { report, exitCode } = await runReleaseConductor({ + repoRoot: process.cwd(), + now: new Date('2026-03-06T12:00:00.000Z'), + args: { + apply: true, + dryRun: false, + repairExistingTag: false, + reportPath: 'tests/results/_agent/release/release-conductor-report.json', + queueReportPath: 'tests/results/_agent/queue/queue-supervisor-report.json', + policySnapshotPath: 'tests/results/_agent/policy/policy-state-snapshot.json', + repo: 'owner/repo', + stream: 'comparevi-cli', + channel: 'rc', + version: '0.8.0-rc.1', + dwellMinutes: 60, + quarantineStaleHours: 24, + help: false + }, + environment: { + GITHUB_REPOSITORY: 'owner/repo', + RELEASE_CONDUCTOR_ENABLED: '1' + }, + runGhJsonFn, + runCommandFn, + readJsonOptionalFn, + writeReportFn: async (reportPath) => reportPath + }); + + assert.equal(exitCode, 1); + assert.equal(report.release.repair.status, 'repair-available'); + assert.equal(report.release.repair.remoteTagExists, true); + assert.equal(report.release.repair.remoteTargetCommitOid, '2222222222222222222222222222222222222222'); + assert.ok(report.decision.blockers.some((entry) => entry.code === 'existing-tag-requires-repair-mode')); + assert.equal(commandCalls.some((entry) => entry.command === 'git' && entry.args[0] === 'tag'), false); +}); + +test('runReleaseConductor reports a repair plan in dry-run for an existing authoritative tag', async () => { + const readJsonOptionalFn = async (filePath) => { + const normalized = String(filePath); + if (normalized.includes('queue-supervisor-report.json')) { + return { + exists: true, + error: null, + path: filePath, + payload: { + paused: false, + throughputController: { mode: 'healthy' }, + retryHistory: {} + } + }; + } + return { + exists: true, + error: null, + path: filePath, + payload: { + schema: 'priority/policy-live-state@v1', + generatedAt: '2026-03-06T10:00:00Z', + state: {} + } + }; + }; + + const runGhJsonFn = (args) => { + if (args[0] === 'api') { + return makeWorkflowRunsResponse(String(args[1])); + } + throw new Error(`unexpected gh args: ${args.join(' ')}`); + }; + + const runCommandFn = (command, args) => { + if (command === 'git' && args[0] === 'config') { + if (args[2] === 'remote.upstream.url') { + return { status: 0, stdout: 'https://github.com/owner/repo.git', stderr: '' }; + } + return { status: 1, stdout: '', stderr: 'missing config' }; + } + if (command === 'git' && args[0] === 'ls-remote') { + return { + status: 0, + stdout: [ + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\trefs/tags/v0.8.0-rc.1', + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\trefs/tags/v0.8.0-rc.1^{}' + ].join('\n'), + stderr: '' + }; + } + if (command === 'git' && args[0] === 'rev-parse') { + return { status: 0, stdout: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n', stderr: '' }; + } + return { status: 0, stdout: '', stderr: '' }; + }; + + const { report, exitCode } = await runReleaseConductor({ + repoRoot: process.cwd(), + now: new Date('2026-03-06T12:00:00.000Z'), + args: { + apply: false, + dryRun: true, + repairExistingTag: true, + reportPath: 'tests/results/_agent/release/release-conductor-report.json', + queueReportPath: 'tests/results/_agent/queue/queue-supervisor-report.json', + policySnapshotPath: 'tests/results/_agent/policy/policy-state-snapshot.json', + repo: 'owner/repo', + stream: 'comparevi-cli', + channel: 'rc', + version: '0.8.0-rc.1', + dwellMinutes: 60, + quarantineStaleHours: 24, + help: false + }, + environment: { + GITHUB_REPOSITORY: 'owner/repo', + RELEASE_CONDUCTOR_ENABLED: '0' + }, + runGhJsonFn, + runCommandFn, + readJsonOptionalFn, + writeReportFn: async (reportPath) => reportPath + }); + + assert.equal(exitCode, 0); + assert.equal(report.decision.status, 'pass'); + assert.equal(report.release.repair.requested, true); + assert.equal(report.release.repair.status, 'ready'); + assert.equal(report.release.repair.remoteTagExists, true); + assert.equal(report.release.repair.remoteTagAnnotated, true); + assert.equal(report.release.repair.remoteTagObjectOid, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + assert.equal(report.release.repair.remoteTargetCommitOid, 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'); + assert.equal(report.release.repair.localTagPresent, true); + assert.equal(report.release.proposalOnly, true); }); -test('runReleaseConductor stays proposal-only when signing material is unavailable', async () => { +test('runReleaseConductor repairs an existing authoritative tag when repair mode is enabled', async () => { + const readJsonOptionalFn = async (filePath) => { + const normalized = String(filePath); + if (normalized.includes('queue-supervisor-report.json')) { + return { + exists: true, + error: null, + path: filePath, + payload: { + paused: false, + throughputController: { mode: 'healthy' }, + retryHistory: {} + } + }; + } + return { + exists: true, + error: null, + path: filePath, + payload: { + schema: 'priority/policy-live-state@v1', + generatedAt: '2026-03-06T10:00:00Z', + state: {} + } + }; + }; + + const runGhJsonFn = (args) => { + if (args[0] === 'api') { + return makeWorkflowRunsResponse(String(args[1])); + } + throw new Error(`unexpected gh args: ${args.join(' ')}`); + }; + + const commandCalls = []; + const runCommandFn = (command, args) => { + commandCalls.push({ command, args }); + if (command === 'git' && args[0] === 'config') { + if (args[2] === 'user.signingkey') { + return { status: 0, stdout: '/tmp/release-signing.pub', stderr: '' }; + } + if (args[2] === 'gpg.format') { + return { status: 0, stdout: 'ssh', stderr: '' }; + } + if (args[2] === 'remote.upstream.url') { + return { status: 0, stdout: 'https://github.com/owner/repo.git', stderr: '' }; + } + return { status: 1, stdout: '', stderr: 'missing config' }; + } + if (command === 'git' && args[0] === 'ls-remote') { + return { + status: 0, + stdout: [ + '1111111111111111111111111111111111111111\trefs/tags/v0.8.0-rc.1', + '2222222222222222222222222222222222222222\trefs/tags/v0.8.0-rc.1^{}' + ].join('\n'), + stderr: '' + }; + } + if (command === 'git' && args[0] === 'rev-parse') { + return { status: 0, stdout: '1111111111111111111111111111111111111111\n', stderr: '' }; + } + if (command === 'git' && args[0] === 'tag' && args[1] === '-d') { + return { status: 0, stdout: `Deleted tag '${args[2]}'`, stderr: '' }; + } + if (command === 'git' && args[0] === 'tag') { + return { status: 0, stdout: '', stderr: '' }; + } + if (command === 'git' && args[0] === 'push') { + return { status: 0, stdout: '', stderr: '' }; + } + return { status: 0, stdout: '', stderr: '' }; + }; + + const { report, exitCode } = await runReleaseConductor({ + repoRoot: process.cwd(), + now: new Date('2026-03-06T12:00:00.000Z'), + args: { + apply: true, + dryRun: false, + repairExistingTag: true, + reportPath: 'tests/results/_agent/release/release-conductor-report.json', + queueReportPath: 'tests/results/_agent/queue/queue-supervisor-report.json', + policySnapshotPath: 'tests/results/_agent/policy/policy-state-snapshot.json', + repo: 'owner/repo', + stream: 'comparevi-cli', + channel: 'rc', + version: '0.8.0-rc.1', + dwellMinutes: 60, + quarantineStaleHours: 24, + help: false + }, + environment: { + GITHUB_REPOSITORY: 'owner/repo', + RELEASE_CONDUCTOR_ENABLED: '1' + }, + runGhJsonFn, + runCommandFn, + readJsonOptionalFn, + writeReportFn: async (reportPath) => reportPath + }); + + assert.equal(exitCode, 0); + assert.equal(report.decision.status, 'pass'); + assert.equal(report.release.proposalOnly, false); + assert.equal(report.release.tagCreated, true); + assert.equal(report.release.tagPushed, true); + assert.equal(report.release.repair.status, 'repaired'); + assert.equal(report.release.repair.localTagDeleted, true); + assert.equal(report.release.repair.tagRecreated, true); + assert.equal(report.release.publicationReplay.requested, true); + assert.equal(report.release.publicationReplay.status, 'dispatched'); + assert.equal(report.release.publicationReplay.dispatched, true); + assert.equal(report.release.publicationReplay.ref, 'develop'); + assert.equal(report.release.publicationReplay.tagInputName, 'release_tag'); + assert.equal(report.release.publicationReplay.tagInputValue, 'v0.8.0-rc.1'); + assert.equal( + commandCalls.some( + (entry) => + entry.command === 'git' && + entry.args[0] === 'push' && + entry.args[1] === '--force-with-lease=refs/tags/v0.8.0-rc.1:1111111111111111111111111111111111111111' + ), + true + ); + assert.equal( + commandCalls.some( + (entry) => + entry.command === 'git' && + entry.args[0] === 'tag' && + entry.args[1] === '-s' && + entry.args[2] === '-f' && + entry.args[3] === 'v0.8.0-rc.1' && + entry.args[4] === '2222222222222222222222222222222222222222' + ), + true + ); + assert.equal( + commandCalls.some( + (entry) => + entry.command === 'gh' && + entry.args[0] === 'workflow' && + entry.args[1] === 'run' && + entry.args[2] === 'release.yml' && + entry.args[3] === '--ref' && + entry.args[4] === 'develop' && + entry.args[5] === '-f' && + entry.args[6] === 'release_tag=v0.8.0-rc.1' + ), + true + ); +}); + +test('runReleaseConductor fails apply when repaired tag publication replay dispatch fails', async () => { + const readJsonOptionalFn = async (filePath) => { + const normalized = String(filePath); + if (normalized.includes('queue-supervisor-report.json')) { + return { + exists: true, + error: null, + path: filePath, + payload: { + paused: false, + throughputController: { mode: 'healthy' }, + retryHistory: {} + } + }; + } + return { + exists: true, + error: null, + path: filePath, + payload: { + schema: 'priority/policy-live-state@v1', + generatedAt: '2026-03-06T10:00:00Z', + state: {} + } + }; + }; + + const runGhJsonFn = (args) => { + if (args[0] === 'api') { + return makeWorkflowRunsResponse(String(args[1])); + } + throw new Error(`unexpected gh args: ${args.join(' ')}`); + }; + + const runCommandFn = (command, args) => { + if (command === 'git' && args[0] === 'config') { + if (args[2] === 'user.signingkey') { + return { status: 0, stdout: '/tmp/release-signing.pub', stderr: '' }; + } + if (args[2] === 'gpg.format') { + return { status: 0, stdout: 'ssh', stderr: '' }; + } + if (args[2] === 'remote.upstream.url') { + return { status: 0, stdout: 'https://github.com/owner/repo.git', stderr: '' }; + } + return { status: 1, stdout: '', stderr: 'missing config' }; + } + if (command === 'git' && args[0] === 'ls-remote') { + return { + status: 0, + stdout: [ + '1111111111111111111111111111111111111111\trefs/tags/v0.8.0-rc.1', + '2222222222222222222222222222222222222222\trefs/tags/v0.8.0-rc.1^{}' + ].join('\n'), + stderr: '' + }; + } + if (command === 'git' && args[0] === 'rev-parse') { + return { status: 0, stdout: '1111111111111111111111111111111111111111\n', stderr: '' }; + } + if (command === 'git' && args[0] === 'tag' && args[1] === '-d') { + return { status: 0, stdout: `Deleted tag '${args[2]}'`, stderr: '' }; + } + if (command === 'git' && args[0] === 'tag') { + return { status: 0, stdout: '', stderr: '' }; + } + if (command === 'git' && args[0] === 'push') { + return { status: 0, stdout: '', stderr: '' }; + } + if (command === 'gh' && args[0] === 'workflow' && args[1] === 'run') { + return { status: 1, stdout: '', stderr: 'dispatch denied' }; + } + return { status: 0, stdout: '', stderr: '' }; + }; + + const { report, exitCode } = await runReleaseConductor({ + repoRoot: process.cwd(), + now: new Date('2026-03-06T12:00:00.000Z'), + args: { + apply: true, + dryRun: false, + repairExistingTag: true, + reportPath: 'tests/results/_agent/release/release-conductor-report.json', + queueReportPath: 'tests/results/_agent/queue/queue-supervisor-report.json', + policySnapshotPath: 'tests/results/_agent/policy/policy-state-snapshot.json', + repo: 'owner/repo', + stream: 'comparevi-cli', + channel: 'rc', + version: '0.8.0-rc.1', + dwellMinutes: 60, + quarantineStaleHours: 24, + help: false + }, + environment: { + GITHUB_REPOSITORY: 'owner/repo', + RELEASE_CONDUCTOR_ENABLED: '1' + }, + runGhJsonFn, + runCommandFn, + readJsonOptionalFn, + writeReportFn: async (reportPath) => reportPath + }); + + assert.equal(exitCode, 1); + assert.equal(report.decision.status, 'fail'); + assert.equal(report.release.repair.status, 'repaired'); + assert.equal(report.release.publicationReplay.requested, true); + assert.equal(report.release.publicationReplay.status, 'dispatch-failed'); + assert.equal(report.release.publicationReplay.dispatched, false); + assert.equal(report.release.publicationReplay.ref, 'develop'); + assert.equal(report.release.publicationReplay.tagInputName, 'release_tag'); + assert.equal(report.release.publicationReplay.tagInputValue, 'v0.8.0-rc.1'); + assert.equal(report.release.publicationReplay.error, 'dispatch denied'); + assert.ok(report.decision.blockers.some((entry) => entry.code === 'release-replay-dispatch-failed')); +}); + +test('runReleaseConductor fails apply when signed tag push remote is unavailable', async () => { + const readJsonOptionalFn = async (filePath) => { + const normalized = String(filePath); + if (normalized.includes('queue-supervisor-report.json')) { + return { + exists: true, + error: null, + path: filePath, + payload: { + paused: false, + throughputController: { mode: 'healthy' }, + retryHistory: {} + } + }; + } + return { + exists: true, + error: null, + path: filePath, + payload: { + schema: 'priority/policy-live-state@v1', + generatedAt: '2026-03-06T10:00:00Z', + state: {} + } + }; + }; + + const runGhJsonFn = (args) => { + if (args[0] === 'api') { + return makeWorkflowRunsResponse(String(args[1])); + } + throw new Error(`unexpected gh args: ${args.join(' ')}`); + }; + + const commandCalls = []; + const runCommandFn = (command, args) => { + commandCalls.push({ command, args }); + if (command === 'git' && args[0] === 'config') { + if (args[2] === 'user.signingkey') { + return { status: 0, stdout: '/tmp/release-signing.pub', stderr: '' }; + } + if (args[2] === 'gpg.format') { + return { status: 0, stdout: 'ssh', stderr: '' }; + } + return { status: 1, stdout: '', stderr: 'missing config' }; + } + if (command === 'git' && args[0] === 'tag') { + return { status: 0, stdout: '', stderr: '' }; + } + return { status: 0, stdout: '', stderr: '' }; + }; + + const { report, exitCode } = await runReleaseConductor({ + repoRoot: process.cwd(), + now: new Date('2026-03-06T12:00:00.000Z'), + args: { + apply: true, + dryRun: false, + reportPath: 'tests/results/_agent/release/release-conductor-report.json', + queueReportPath: 'tests/results/_agent/queue/queue-supervisor-report.json', + policySnapshotPath: 'tests/results/_agent/policy/policy-state-snapshot.json', + repo: 'owner/repo', + stream: 'comparevi-cli', + channel: 'stable', + version: '0.8.0', + dwellMinutes: 60, + quarantineStaleHours: 24, + help: false + }, + environment: { + GITHUB_REPOSITORY: 'owner/repo', + RELEASE_CONDUCTOR_ENABLED: '1' + }, + runGhJsonFn, + runCommandFn, + readJsonOptionalFn, + writeReportFn: async (reportPath) => reportPath + }); + + assert.equal(exitCode, 1); + assert.equal(report.decision.status, 'fail'); + assert.equal(report.release.tagCreated, true); + assert.equal(report.release.tagPushed, false); + assert.equal(report.release.tagPushRemote.remoteName, null); + assert.ok(report.decision.blockers.some((entry) => entry.code === 'tag-push-remote-missing')); + assert.equal(commandCalls.some((entry) => entry.command === 'git' && entry.args[0] === 'push'), false); +}); + +test('runReleaseConductor blocks apply when signing material is unavailable', async () => { const readJsonOptionalFn = async (filePath) => { const normalized = String(filePath); if (normalized.includes('queue-supervisor-report.json')) { @@ -498,9 +1234,11 @@ test('runReleaseConductor stays proposal-only when signing material is unavailab writeReportFn: async (reportPath) => reportPath }); - assert.equal(exitCode, 0); - assert.equal(report.decision.status, 'pass'); + assert.equal(exitCode, 1); + assert.equal(report.decision.status, 'fail'); assert.equal(report.release.proposalOnly, true); assert.equal(report.release.tagCreated, false); + assert.equal(report.release.signingMaterial.available, false); + assert.ok(report.decision.blockers.some((entry) => entry.code === 'tag-signing-material-missing')); assert.equal(commandCalls.some((entry) => entry.command === 'git' && entry.args[0] === 'tag'), false); }); diff --git a/tools/priority/__tests__/release-published-bundle-observer-schema.test.mjs b/tools/priority/__tests__/release-published-bundle-observer-schema.test.mjs new file mode 100644 index 000000000..e6100b50b --- /dev/null +++ b/tools/priority/__tests__/release-published-bundle-observer-schema.test.mjs @@ -0,0 +1,140 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { runReleasePublishedBundleObserver } from '../release-published-bundle-observer.mjs'; + +function toGlobPath(filePath) { + return path.resolve(filePath).replace(/\\/g, '/'); +} + +function resolveValidatorRepoRoot(repoRoot) { + const localValidatorOk = + fs.existsSync(path.join(repoRoot, 'dist', 'tools', 'schemas', 'validate-json.js')) && + fs.existsSync(path.join(repoRoot, 'node_modules', 'ajv', 'package.json')) && + fs.existsSync(path.join(repoRoot, 'node_modules', 'argparse', 'package.json')); + if (localValidatorOk) { + return repoRoot; + } + const candidates = [ + path.resolve(repoRoot, '..', 'compare-monitoring-canonical'), + path.resolve(repoRoot, '..', '1843-wake-lifecycle-state-machine') + ]; + return ( + candidates.find( + (candidate) => + fs.existsSync(path.join(candidate, 'dist', 'tools', 'schemas', 'validate-json.js')) && + fs.existsSync(path.join(candidate, 'node_modules', 'ajv', 'package.json')) && + fs.existsSync(path.join(candidate, 'node_modules', 'argparse', 'package.json')) + ) || repoRoot + ); +} + +function runSchemaValidate(repoRoot, schemaPath, dataPath) { + const validatorRepoRoot = resolveValidatorRepoRoot(repoRoot); + execFileSync('node', ['dist/tools/schemas/validate-json.js', '--schema', toGlobPath(schemaPath), '--data', toGlobPath(dataPath)], { + cwd: validatorRepoRoot, + stdio: 'pipe' + }); +} + +function writeJson(filePath, payload) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); +} + +test('release published bundle observer report matches schema', async () => { + const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'release-published-bundle-observer-schema-')); + const bundleRoot = path.join(tmpDir, 'bundle', 'CompareVI.Tools-v0.6.4-rc.1'); + const archivePath = path.join(tmpDir, 'download', 'CompareVI.Tools-v0.6.4-rc.1.zip'); + + fs.mkdirSync(path.join(bundleRoot, 'tools', 'CompareVI.Tools'), { recursive: true }); + fs.mkdirSync(path.dirname(archivePath), { recursive: true }); + fs.writeFileSync(archivePath, 'zip-placeholder', 'utf8'); + fs.writeFileSync(path.join(bundleRoot, 'tools', 'CompareVI.Tools', 'CompareVI.Tools.psd1'), '@{}', 'utf8'); + writeJson(path.join(bundleRoot, 'comparevi-tools-release.json'), { + schema: 'comparevi-tools-release-manifest@v1', + versionContract: { + authoritativeConsumerPin: '0.6.4-rc.1', + authoritativeConsumerPinKind: 'release-version' + }, + consumerContract: { + historyFacade: { schema: 'comparevi-tools/history-facade@v1' }, + localRuntimeProfiles: { schema: 'comparevi-tools/runtime-profiles@v1' }, + localOperatorSession: { schema: 'comparevi-tools/local-operator-session@v1' }, + diagnosticsCommentRenderer: { schema: 'comparevi-tools/diagnostics-comment-renderer@v1' }, + hostedNiLinuxRunner: { schema: 'comparevi-tools/hosted-ni-linux-runner@v1' }, + dockerImageContract: { + schema: 'comparevi-tools/docker-image-contract@v1', + images: { + hostedNiLinuxRunner: { + imageRef: 'nationalinstruments/labview:2026q1-linux' + } + } + }, + capabilities: { + viHistory: { + schema: 'comparevi-tools/vi-history-capability@v1', + capabilityId: 'vi-history', + distributionRole: 'upstream-producer', + distributionModel: 'release-bundle', + bundleImportPath: 'tools/CompareVI.Tools/CompareVI.Tools.psd1', + releaseAssetPattern: 'CompareVI.Tools-v.zip', + contractPaths: { + historyFacade: 'consumerContract.historyFacade', + localRuntimeProfiles: 'consumerContract.localRuntimeProfiles', + localOperatorSession: 'consumerContract.localOperatorSession', + diagnosticsCommentRenderer: 'consumerContract.diagnosticsCommentRenderer', + hostedNiLinuxRunner: 'consumerContract.hostedNiLinuxRunner' + } + }, + dockerProfile: { + schema: 'comparevi-tools/docker-profile-capability@v1', + capabilityId: 'docker-profile', + distributionRole: 'upstream-producer', + distributionModel: 'release-bundle', + bundleImportPath: 'tools/CompareVI.Tools/CompareVI.Tools.psd1', + releaseAssetPattern: 'CompareVI.Tools-v.zip', + authoritativeImageContractSource: 'consumerContract.dockerImageContract' + } + } + } + }); + + const outputPath = path.join(tmpDir, 'tests', 'results', '_agent', 'release', 'release-published-bundle-observer.json'); + const { report } = await runReleasePublishedBundleObserver( + { + repoRoot: tmpDir, + repo: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + outputPath + }, + { + now: new Date('2026-03-23T19:19:00Z'), + runGhJsonFn: () => [ + { + id: 70, + tag_name: 'v0.6.4-rc.1', + name: 'v0.6.4-rc.1', + draft: false, + prerelease: true, + published_at: '2026-03-23T19:12:00Z', + assets: [{ name: 'CompareVI.Tools-v0.6.4-rc.1.zip', id: 71 }] + } + ], + downloadAssetFn: () => archivePath, + extractArchiveFn: () => bundleRoot + } + ); + + runSchemaValidate( + repoRoot, + path.join(repoRoot, 'docs', 'schemas', 'release-published-bundle-observer-report-v1.schema.json'), + outputPath + ); + assert.equal(report.schema, 'priority/release-published-bundle-observer-report@v1'); +}); diff --git a/tools/priority/__tests__/release-published-bundle-observer.test.mjs b/tools/priority/__tests__/release-published-bundle-observer.test.mjs new file mode 100644 index 000000000..9b5b0a85e --- /dev/null +++ b/tools/priority/__tests__/release-published-bundle-observer.test.mjs @@ -0,0 +1,228 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import { + DEFAULT_OUTPUT_PATH, + DEFAULT_RESULTS_DIR, + parseArgs, + runReleasePublishedBundleObserver +} from '../release-published-bundle-observer.mjs'; + +function writeJson(filePath, payload) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); +} + +test('parseArgs keeps defaults and accepts overrides', () => { + const parsed = parseArgs([ + 'node', + 'release-published-bundle-observer.mjs', + '--repo-root', + 'C:/repo', + '--repo', + 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + '--tag', + 'v0.6.4-rc.1', + '--output', + 'custom/published-bundle.json', + '--results-dir', + 'custom/results' + ]); + + assert.equal(parsed.repoRoot, 'C:/repo'); + assert.equal(parsed.repo, 'LabVIEW-Community-CI-CD/compare-vi-cli-action'); + assert.equal(parsed.tag, 'v0.6.4-rc.1'); + assert.equal(parsed.outputPath, 'custom/published-bundle.json'); + assert.equal(parsed.resultsDir, 'custom/results'); + assert.equal(DEFAULT_OUTPUT_PATH, path.join('tests', 'results', '_agent', 'release', 'release-published-bundle-observer.json')); + assert.equal(DEFAULT_RESULTS_DIR, path.join('tests', 'results', '_agent', 'release', 'published-bundle-observer')); +}); + +test('runReleasePublishedBundleObserver reports release-unobserved when no published releases exist', async () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'published-bundle-observer-empty-')); + + const result = await runReleasePublishedBundleObserver( + { + repoRoot, + repo: 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + }, + { + now: new Date('2026-03-23T19:15:00Z'), + runGhJsonFn: () => [] + } + ); + + assert.equal(result.exitCode, 1); + assert.equal(result.report.selection.status, 'release-unobserved'); + assert.equal(result.report.summary.status, 'release-unobserved'); +}); + +test('runReleasePublishedBundleObserver reports asset-missing when the release lacks CompareVI.Tools bundle', async () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'published-bundle-observer-asset-missing-')); + + const result = await runReleasePublishedBundleObserver( + { + repoRoot, + repo: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + tag: 'v0.6.4-rc.1' + }, + { + now: new Date('2026-03-23T19:16:00Z'), + runGhJsonFn: () => [ + { + tag_name: 'v0.6.4-rc.1', + published_at: '2026-03-23T19:10:00Z', + assets: [{ name: 'comparevi-cli-v0.6.4-rc.1.zip', id: 11 }] + } + ] + } + ); + + assert.equal(result.exitCode, 1); + assert.equal(result.report.selection.status, 'asset-missing'); + assert.equal(result.report.summary.status, 'asset-missing'); + assert.equal(result.report.selection.releaseTag, 'v0.6.4-rc.1'); +}); + +test('runReleasePublishedBundleObserver certifies producer-native-ready bundle metadata from the published asset', async () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'published-bundle-observer-pass-')); + const bundleRoot = path.join(repoRoot, 'tmp', 'bundle', 'CompareVI.Tools-v0.6.4-rc.1'); + const archivePath = path.join(repoRoot, 'tmp', 'download', 'CompareVI.Tools-v0.6.4-rc.1.zip'); + fs.mkdirSync(path.join(bundleRoot, 'tools', 'CompareVI.Tools'), { recursive: true }); + fs.mkdirSync(path.dirname(archivePath), { recursive: true }); + fs.writeFileSync(archivePath, 'zip-placeholder', 'utf8'); + fs.writeFileSync(path.join(bundleRoot, 'tools', 'CompareVI.Tools', 'CompareVI.Tools.psd1'), '@{}', 'utf8'); + writeJson(path.join(bundleRoot, 'comparevi-tools-release.json'), { + schema: 'comparevi-tools-release-manifest@v1', + versionContract: { + authoritativeConsumerPin: '0.6.4-rc.1', + authoritativeConsumerPinKind: 'release-version' + }, + consumerContract: { + historyFacade: { schema: 'comparevi-tools/history-facade@v1' }, + localRuntimeProfiles: { schema: 'comparevi-tools/runtime-profiles@v1' }, + localOperatorSession: { schema: 'comparevi-tools/local-operator-session@v1' }, + diagnosticsCommentRenderer: { schema: 'comparevi-tools/diagnostics-comment-renderer@v1' }, + hostedNiLinuxRunner: { schema: 'comparevi-tools/hosted-ni-linux-runner@v1' }, + dockerImageContract: { + schema: 'comparevi-tools/docker-image-contract@v1', + images: { + hostedNiLinuxRunner: { + imageRef: 'nationalinstruments/labview:2026q1-linux' + } + } + }, + capabilities: { + viHistory: { + schema: 'comparevi-tools/vi-history-capability@v1', + capabilityId: 'vi-history', + distributionRole: 'upstream-producer', + distributionModel: 'release-bundle', + bundleImportPath: 'tools/CompareVI.Tools/CompareVI.Tools.psd1', + releaseAssetPattern: 'CompareVI.Tools-v.zip', + contractPaths: { + historyFacade: 'consumerContract.historyFacade', + localRuntimeProfiles: 'consumerContract.localRuntimeProfiles', + localOperatorSession: 'consumerContract.localOperatorSession', + diagnosticsCommentRenderer: 'consumerContract.diagnosticsCommentRenderer', + hostedNiLinuxRunner: 'consumerContract.hostedNiLinuxRunner' + } + }, + dockerProfile: { + schema: 'comparevi-tools/docker-profile-capability@v1', + capabilityId: 'docker-profile', + distributionRole: 'upstream-producer', + distributionModel: 'release-bundle', + bundleImportPath: 'tools/CompareVI.Tools/CompareVI.Tools.psd1', + releaseAssetPattern: 'CompareVI.Tools-v.zip', + authoritativeImageContractSource: 'consumerContract.dockerImageContract' + } + } + } + }); + + const result = await runReleasePublishedBundleObserver( + { + repoRoot, + repo: 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + }, + { + now: new Date('2026-03-23T19:17:00Z'), + runGhJsonFn: () => [ + { + id: 55, + tag_name: 'v0.6.4-rc.1', + name: 'v0.6.4-rc.1', + draft: false, + prerelease: true, + published_at: '2026-03-23T19:12:00Z', + assets: [{ name: 'CompareVI.Tools-v0.6.4-rc.1.zip', id: 77 }] + } + ], + downloadAssetFn: () => archivePath, + extractArchiveFn: () => bundleRoot + } + ); + + assert.equal(result.exitCode, 0); + assert.equal(result.report.selection.status, 'selected'); + assert.equal(result.report.bundle.status, 'extracted'); + assert.equal(result.report.bundleContract.status, 'producer-native-ready'); + assert.equal(result.report.bundleContract.authoritativeConsumerPin, '0.6.4-rc.1'); + assert.equal(result.report.bundleContract.viHistoryCapabilityPresent, true); + assert.equal(result.report.bundleContract.dockerProfileCapabilityPresent, true); + assert.equal(result.report.bundleContract.authoritativeImageContractSource, 'consumerContract.dockerImageContract'); + assert.equal(result.report.bundleContract.authoritativeImageContractSourceResolved, true); + assert.equal(result.report.bundleContract.dockerImageContractSchema, 'comparevi-tools/docker-image-contract@v1'); + assert.equal(result.report.summary.status, 'producer-native-ready'); +}); + +test('runReleasePublishedBundleObserver reports producer-native-incomplete when vi-history capability is missing from published metadata', async () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'published-bundle-observer-incomplete-')); + const bundleRoot = path.join(repoRoot, 'tmp', 'bundle', 'CompareVI.Tools-v0.6.3-tools.14'); + const archivePath = path.join(repoRoot, 'tmp', 'download', 'CompareVI.Tools-v0.6.3-tools.14.zip'); + fs.mkdirSync(bundleRoot, { recursive: true }); + fs.mkdirSync(path.dirname(archivePath), { recursive: true }); + fs.writeFileSync(archivePath, 'zip-placeholder', 'utf8'); + writeJson(path.join(bundleRoot, 'comparevi-tools-release.json'), { + schema: 'comparevi-tools-release-manifest@v1', + versionContract: { + authoritativeConsumerPin: 'v0.6.3-tools.14', + authoritativeConsumerPinKind: 'release-tag' + }, + consumerContract: { + historyFacade: { schema: 'comparevi-tools/history-facade@v1' }, + capabilities: {} + } + }); + + const result = await runReleasePublishedBundleObserver( + { + repoRoot, + repo: 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + }, + { + now: new Date('2026-03-23T19:18:00Z'), + runGhJsonFn: () => [ + { + id: 56, + tag_name: 'v0.6.3', + draft: false, + prerelease: false, + published_at: '2026-03-21T19:12:00Z', + assets: [{ name: 'CompareVI.Tools-v0.6.3-tools.14.zip', id: 88 }] + } + ], + downloadAssetFn: () => archivePath, + extractArchiveFn: () => bundleRoot + } + ); + + assert.equal(result.exitCode, 1); + assert.equal(result.report.bundleContract.status, 'producer-native-incomplete'); + assert.equal(result.report.bundleContract.viHistoryCapabilityPresent, false); + assert.equal(result.report.summary.status, 'producer-native-incomplete'); +}); diff --git a/tools/priority/__tests__/release-signing-readiness-schema.test.mjs b/tools/priority/__tests__/release-signing-readiness-schema.test.mjs new file mode 100644 index 000000000..e4328a285 --- /dev/null +++ b/tools/priority/__tests__/release-signing-readiness-schema.test.mjs @@ -0,0 +1,101 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { runReleaseSigningReadiness } from '../release-signing-readiness.mjs'; + +function toGlobPath(filePath) { + return path.resolve(filePath).replace(/\\/g, '/'); +} + +function resolveValidatorRepoRoot(repoRoot) { + const localValidatorOk = + fs.existsSync(path.join(repoRoot, 'dist', 'tools', 'schemas', 'validate-json.js')) && + fs.existsSync(path.join(repoRoot, 'node_modules', 'ajv', 'package.json')) && + fs.existsSync(path.join(repoRoot, 'node_modules', 'argparse', 'package.json')); + if (localValidatorOk) { + return repoRoot; + } + const candidates = [ + path.resolve(repoRoot, '..', 'compare-monitoring-canonical'), + path.resolve(repoRoot, '..', '1843-wake-lifecycle-state-machine') + ]; + return ( + candidates.find( + (candidate) => + fs.existsSync(path.join(candidate, 'dist', 'tools', 'schemas', 'validate-json.js')) && + fs.existsSync(path.join(candidate, 'node_modules', 'ajv', 'package.json')) && + fs.existsSync(path.join(candidate, 'node_modules', 'argparse', 'package.json')) + ) || repoRoot + ); +} + +function runSchemaValidate(repoRoot, schemaPath, dataPath) { + const validatorRepoRoot = resolveValidatorRepoRoot(repoRoot); + execFileSync('node', ['dist/tools/schemas/validate-json.js', '--schema', toGlobPath(schemaPath), '--data', toGlobPath(dataPath)], { + cwd: validatorRepoRoot, + stdio: 'pipe' + }); +} + +function writeText(filePath, content) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, 'utf8'); +} + +test('release signing readiness report matches schema', async () => { + const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'release-signing-readiness-schema-')); + + writeText( + path.join(tmpDir, '.github', 'workflows', 'release-conductor.yml'), + [ + 'name: release-conductor', + 'jobs:', + ' release:', + ' steps:', + ' - name: Configure release tag signing material', + ' run: |', + ' echo RELEASE_TAG_SIGNING_PRIVATE_KEY', + ' echo RELEASE_TAG_SIGNING_IDENTITY_NAME', + ' echo RELEASE_TAG_SIGNING_IDENTITY_EMAIL', + " signing_login=\"$(gh api user --jq '.login')\"", + ' git config gpg.format ssh', + ' git config user.signingkey "$public_key_path"', + ' git config user.name "$signing_name"', + ' git config user.email "$signing_email"' + ].join('\n') + ); + + const outputPath = path.join(tmpDir, 'tests', 'results', '_agent', 'release', 'release-signing-readiness.json'); + const { report } = await runReleaseSigningReadiness( + { + repoRoot: tmpDir, + repo: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + outputPath + }, + { + now: new Date('2026-03-23T17:30:00Z'), + runGhJsonFn: (args) => { + const endpoint = args[1] ?? ''; + if (endpoint.includes('/actions/secrets')) { + return { secrets: [{ name: 'RELEASE_TAG_SIGNING_PRIVATE_KEY' }] }; + } + if (endpoint.includes('/actions/variables')) { + return { variables: [{ name: 'RELEASE_CONDUCTOR_ENABLED', value: '1' }] }; + } + if (endpoint.startsWith('user/ssh_signing_keys')) { + return [{ id: 1, title: 'compare-release-signing' }]; + } + throw new Error(`Unexpected endpoint: ${endpoint}`); + } + } + ); + + runSchemaValidate(repoRoot, path.join(repoRoot, 'docs', 'schemas', 'release-signing-readiness-report-v1.schema.json'), outputPath); + assert.equal(report.schema, 'priority/release-signing-readiness-report@v1'); +}); diff --git a/tools/priority/__tests__/release-signing-readiness.test.mjs b/tools/priority/__tests__/release-signing-readiness.test.mjs new file mode 100644 index 000000000..3e89cf875 --- /dev/null +++ b/tools/priority/__tests__/release-signing-readiness.test.mjs @@ -0,0 +1,259 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import { + DEFAULT_OUTPUT_PATH, + DEFAULT_RELEASE_CONDUCTOR_REPORT_PATH, + DEFAULT_RELEASE_PUBLISHED_BUNDLE_OBSERVER_PATH, + parseArgs, + REQUIRED_SIGNING_SECRET, + OPTIONAL_SIGNING_SECRET, + runReleaseSigningReadiness +} from '../release-signing-readiness.mjs'; + +function writeText(filePath, content) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, 'utf8'); +} + +function writeJson(filePath, payload) { + writeText(filePath, `${JSON.stringify(payload, null, 2)}\n`); +} + +function createPublishedBundleObserver(overrides = {}) { + return { + schema: 'priority/release-published-bundle-observer-report@v1', + generatedAt: '2026-03-23T17:19:30Z', + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + inputs: { + requestedTag: null, + resultsDir: 'tests/results/_agent/release/published-bundle-observer' + }, + selection: { + status: 'selected', + releaseTag: 'v0.6.4-rc.1-tools.1', + publishedAt: '2026-03-23T17:19:00Z', + releaseName: 'v0.6.4-rc.1-tools.1', + releaseId: 123, + prerelease: true, + draft: false, + assetName: 'CompareVI.Tools-v0.6.4-rc.1-tools.1.zip', + assetId: 456 + }, + bundle: { + status: 'extracted', + archivePath: 'tests/results/_agent/release/published-bundle-observer/download/CompareVI.Tools-v0.6.4-rc.1-tools.1.zip', + extractionRoot: 'tests/results/_agent/release/published-bundle-observer/bundle/CompareVI.Tools-v0.6.4-rc.1-tools.1', + downloadDirectory: 'tests/results/_agent/release/published-bundle-observer/download' + }, + bundleContract: { + status: 'producer-native-ready', + metadataPath: + 'tests/results/_agent/release/published-bundle-observer/bundle/CompareVI.Tools-v0.6.4-rc.1-tools.1/comparevi-tools-release.json', + schema: 'comparevi-tools-release-manifest@v1', + authoritativeConsumerPin: 'v0.6.4-rc.1-tools.1', + authoritativeConsumerPinKind: 'release-tag', + capabilityId: 'vi-history', + distributionRole: 'upstream-producer', + distributionModel: 'release-bundle', + bundleImportPath: '.github/workflows/vi-history.yml', + bundleImportPathExists: true, + releaseAssetPattern: 'CompareVI.Tools-v*.zip', + contractPathResolutions: [], + metadataPresent: true, + metadataSchemaMatches: true, + viHistoryCapabilityPresent: true, + viHistoryCapabilityProducerNative: true, + bundleContractPinResolved: true, + bundleContractPathsResolved: true + }, + summary: { + status: 'producer-native-ready', + releaseTag: 'v0.6.4-rc.1-tools.1', + assetName: 'CompareVI.Tools-v0.6.4-rc.1-tools.1.zip', + publishedAt: '2026-03-23T17:19:00Z', + authoritativeConsumerPin: 'v0.6.4-rc.1-tools.1' + }, + ...overrides + }; +} + +function seedWorkflowContract(repoRoot) { + writeText( + path.join(repoRoot, '.github', 'workflows', 'release-conductor.yml'), + [ + 'name: release-conductor', + 'jobs:', + ' release:', + ' steps:', + ' - name: Configure release tag signing material', + ' run: |', + ' echo RELEASE_TAG_SIGNING_PRIVATE_KEY', + ' echo RELEASE_TAG_SIGNING_IDENTITY_NAME', + ' echo RELEASE_TAG_SIGNING_IDENTITY_EMAIL', + " signing_login=\"$(gh api user --jq '.login')\"", + ' git config gpg.format ssh', + ' git config user.signingkey "$public_key_path"', + ' git config user.name "$signing_name"', + ' git config user.email "$signing_email"' + ].join('\n') + ); +} + +test('parseArgs keeps defaults and accepts overrides', () => { + const parsed = parseArgs([ + 'node', + 'release-signing-readiness.mjs', + '--repo-root', + 'C:/repo', + '--repo', + 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + '--output', + 'custom/release-signing-readiness.json' + ]); + + assert.equal(parsed.repoRoot, 'C:/repo'); + assert.equal(parsed.repo, 'LabVIEW-Community-CI-CD/compare-vi-cli-action'); + assert.equal(parsed.outputPath, 'custom/release-signing-readiness.json'); + assert.equal( + DEFAULT_OUTPUT_PATH, + path.join('tests', 'results', '_agent', 'release', 'release-signing-readiness.json') + ); + assert.equal( + DEFAULT_RELEASE_CONDUCTOR_REPORT_PATH, + path.join('tests', 'results', '_agent', 'release', 'release-conductor-report.json') + ); + assert.equal( + DEFAULT_RELEASE_PUBLISHED_BUNDLE_OBSERVER_PATH, + path.join('tests', 'results', '_agent', 'release', 'release-published-bundle-observer.json') + ); +}); + +test('runReleaseSigningReadiness reports explicit external blocker when workflow secret is missing', async () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'release-signing-readiness-missing-')); + seedWorkflowContract(repoRoot); + writeJson(path.join(repoRoot, DEFAULT_RELEASE_PUBLISHED_BUNDLE_OBSERVER_PATH), createPublishedBundleObserver({ + bundleContract: { + ...createPublishedBundleObserver().bundleContract, + status: 'producer-native-incomplete', + authoritativeConsumerPin: null, + authoritativeConsumerPinKind: null, + capabilityId: null, + distributionRole: null, + distributionModel: null, + bundleImportPath: null, + bundleImportPathExists: false, + releaseAssetPattern: null, + viHistoryCapabilityPresent: false, + viHistoryCapabilityProducerNative: false, + bundleContractPinResolved: false + }, + summary: { + ...createPublishedBundleObserver().summary, + status: 'producer-native-incomplete', + authoritativeConsumerPin: null + } + })); + + const result = await runReleaseSigningReadiness( + { + repoRoot, + repo: 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + }, + { + now: new Date('2026-03-23T17:20:00Z'), + runGhJsonFn: (args) => { + const endpoint = args[1] ?? ''; + if (endpoint.includes('/actions/secrets')) { + return { secrets: [{ name: 'GH_TOKEN' }] }; + } + if (endpoint.includes('/actions/variables')) { + return { variables: [] }; + } + if (endpoint.startsWith('user/ssh_signing_keys')) { + throw new Error('This API operation needs the "admin:ssh_signing_key" scope.'); + } + throw new Error(`Unexpected endpoint: ${endpoint}`); + } + } + ); + + assert.equal(result.exitCode, 1); + assert.equal(result.report.summary.status, 'warn'); + assert.equal(result.report.summary.codePathState, 'ready'); + assert.equal(result.report.summary.signingCapabilityState, 'missing'); + assert.equal(result.report.summary.signingAuthorityState, 'scope-missing'); + assert.equal(result.report.summary.releaseConductorApplyState, 'disabled'); + assert.equal(result.report.summary.publicationState, 'unobserved'); + assert.equal(result.report.summary.publishedBundleState, 'producer-native-incomplete'); + assert.equal(result.report.summary.publishedBundleReleaseTag, 'v0.6.4-rc.1-tools.1'); + assert.equal(result.report.summary.publishedBundleAuthoritativeConsumerPin, null); + assert.equal(result.report.summary.externalBlocker, 'workflow-signing-secret-missing'); + assert.equal(result.report.secretInventory.requiredSecretPresent, false); + assert.equal(result.report.releaseConductorApply.status, 'disabled'); + assert.equal(result.report.signingAuthority.status, 'scope-missing'); + assert.equal(result.report.publishedBundleObserver.status, 'producer-native-incomplete'); + assert.deepEqual(result.report.blockers.map((entry) => entry.code), [ + 'workflow-signing-secret-missing', + 'release-conductor-apply-disabled', + 'workflow-signing-admin-scope-missing', + 'published-bundle-producer-native-incomplete' + ]); +}); + +test('runReleaseSigningReadiness reports publication success when signing capability is configured', async () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'release-signing-readiness-pass-')); + seedWorkflowContract(repoRoot); + writeJson(path.join(repoRoot, DEFAULT_RELEASE_CONDUCTOR_REPORT_PATH), { + release: { + tagCreated: true, + tagPushed: true, + targetTag: 'v0.6.4-rc.1' + } + }); + writeJson(path.join(repoRoot, DEFAULT_RELEASE_PUBLISHED_BUNDLE_OBSERVER_PATH), createPublishedBundleObserver()); + + const result = await runReleaseSigningReadiness( + { + repoRoot, + repo: 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + }, + { + now: new Date('2026-03-23T17:21:00Z'), + runGhJsonFn: (args) => { + const endpoint = args[1] ?? ''; + if (endpoint.includes('/actions/secrets')) { + return { secrets: [{ name: REQUIRED_SIGNING_SECRET }, { name: OPTIONAL_SIGNING_SECRET }] }; + } + if (endpoint.includes('/actions/variables')) { + return { variables: [{ name: 'RELEASE_CONDUCTOR_ENABLED', value: '1' }] }; + } + if (endpoint.startsWith('user/ssh_signing_keys')) { + return [{ id: 1, title: 'compare-release-signing' }]; + } + throw new Error(`Unexpected endpoint: ${endpoint}`); + } + } + ); + + assert.equal(result.exitCode, 0); + assert.equal(result.report.summary.status, 'pass'); + assert.equal(result.report.summary.codePathState, 'ready'); + assert.equal(result.report.summary.signingCapabilityState, 'configured'); + assert.equal(result.report.summary.signingAuthorityState, 'ready'); + assert.equal(result.report.summary.releaseConductorApplyState, 'enabled'); + assert.equal(result.report.summary.publicationState, 'authoritative-publication-successful'); + assert.equal(result.report.summary.publishedBundleState, 'producer-native-ready'); + assert.equal(result.report.summary.publishedBundleReleaseTag, 'v0.6.4-rc.1-tools.1'); + assert.equal(result.report.summary.publishedBundleAuthoritativeConsumerPin, 'v0.6.4-rc.1-tools.1'); + assert.equal(result.report.summary.externalBlocker, null); + assert.equal(result.report.secretInventory.requiredSecretPresent, true); + assert.equal(result.report.releaseConductorApply.enabled, true); + assert.equal(result.report.signingAuthority.listedKeyCount, 1); + assert.equal(result.report.publication.targetTag, 'v0.6.4-rc.1'); + assert.equal(result.report.publishedBundleObserver.status, 'producer-native-ready'); + assert.deepEqual(result.report.blockers, []); +}); diff --git a/tools/priority/__tests__/release-trust-remediation.test.mjs b/tools/priority/__tests__/release-trust-remediation.test.mjs new file mode 100644 index 000000000..4e50551cb --- /dev/null +++ b/tools/priority/__tests__/release-trust-remediation.test.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile, mkdtemp, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { buildReleaseTrustRemediationMarkdown, runReleaseTrustRemediation } from '../release-trust-remediation.mjs'; + +test('buildReleaseTrustRemediationMarkdown emits repair guidance for unsigned or lightweight tags', () => { + const markdown = buildReleaseTrustRemediationMarkdown({ + tagRef: 'v0.6.4-rc.1', + trustReport: { + failures: [{ code: 'tag-signature-unverified' }], + tagSignature: { + refName: 'v0.6.4-rc.1' + } + } + }); + + assert.match(markdown, /repair-eligible tag failures/); + assert.match(markdown, /`version = 0\.6\.4-rc\.1`/); + assert.match(markdown, /`repair_existing_tag = true`/); + assert.match(markdown, /Preserve tag identity and asset names/); +}); + +test('buildReleaseTrustRemediationMarkdown reports not-needed when no repair failures exist', () => { + const markdown = buildReleaseTrustRemediationMarkdown({ + tagRef: 'v0.6.4-rc.1', + trustReport: { + failures: [{ code: 'checksum-mismatch' }] + } + }); + + assert.match(markdown, /No repair-mode remediation is required/); + assert.doesNotMatch(markdown, /repair_existing_tag/); +}); + +test('runReleaseTrustRemediation writes markdown artifact and appends to workflow summary', async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'release-trust-remediation-')); + const trustPath = path.join(tempRoot, 'release-trust-gate.json'); + const outputPath = path.join(tempRoot, 'release-trust-remediation.md'); + const summaryPath = path.join(tempRoot, 'step-summary.md'); + + await writeFile( + trustPath, + JSON.stringify( + { + failures: [{ code: 'tag-not-annotated' }], + tagSignature: { + refName: 'v0.6.4-rc.1' + } + }, + null, + 2 + ) + ); + + const result = await runReleaseTrustRemediation({ + args: { + trustReportPath: trustPath, + outputPath, + summaryPath, + tagRef: 'v0.6.4-rc.1' + } + }); + + assert.equal(result.wroteSummary, true); + const output = await readFile(outputPath, 'utf8'); + const summary = await readFile(summaryPath, 'utf8'); + assert.match(output, /`repair_existing_tag = true`/); + assert.match(summary, /`repair_existing_tag = true`/); +}); diff --git a/tools/priority/__tests__/remote-utils.test.mjs b/tools/priority/__tests__/remote-utils.test.mjs index d8d349742..7bf9635f7 100644 --- a/tools/priority/__tests__/remote-utils.test.mjs +++ b/tools/priority/__tests__/remote-utils.test.mjs @@ -13,6 +13,7 @@ import { loadRepositoryGraphMetadata, ensureOriginFork, pushBranch, + buildGraphqlArgs, buildGhPrCreateArgs, buildGhPrEditArgs, buildGhPrListArgs, @@ -461,6 +462,36 @@ test('graphql PR helpers expose the same-owner fork mutation contract', () => { assert.equal(request.variables.draft, true); }); +test('buildGraphqlArgs preserves string variables and sends booleans as typed GraphQL fields', () => { + const args = buildGraphqlArgs('mutation Test { noop }', { + repositoryId: 'R_upstream', + headRepositoryId: 'R_fork', + headRefName: 'issue/963-org-owned-fork-pr-helper', + baseRefName: 'develop', + draft: false, + retryCount: 2 + }); + + assert.deepEqual(args, [ + 'api', + 'graphql', + '-f', + 'query=mutation Test { noop }', + '-f', + 'repositoryId=R_upstream', + '-f', + 'headRepositoryId=R_fork', + '-f', + 'headRefName=issue/963-org-owned-fork-pr-helper', + '-f', + 'baseRefName=develop', + '-F', + 'draft=false', + '-F', + 'retryCount=2' + ]); +}); + test('findExistingPullRequest matches the branch/base pair and same-owner cross-repo head', () => { const calls = []; const pullRequest = findExistingPullRequest( diff --git a/tools/priority/__tests__/runtime-supervisor-monitoring-work-injection.test.mjs b/tools/priority/__tests__/runtime-supervisor-monitoring-work-injection.test.mjs index ae543cd3c..151541cf9 100644 --- a/tools/priority/__tests__/runtime-supervisor-monitoring-work-injection.test.mjs +++ b/tools/priority/__tests__/runtime-supervisor-monitoring-work-injection.test.mjs @@ -8,7 +8,10 @@ function createGovernorPortfolioSummary({ nextOwnerRepository = 'LabVIEW-Community-CI-CD/compare-vi-cli-action', nextAction = 'continue-compare-governance-work', ownerDecisionSource = 'compare-governor-summary', - governorMode = 'compare-governance-work' + governorMode = 'compare-governance-work', + viHistoryDistributorDependencyStatus = 'unknown', + viHistoryDistributorDependencyExternalBlocker = null, + viHistoryDistributorDependencyPublicationState = null } = {}) { return { schema: 'priority/autonomous-governor-portfolio-summary-report@v1', @@ -25,11 +28,30 @@ function createGovernorPortfolioSummary({ monitoringStatus: 'active', futureAgentAction: 'future-agent-may-pivot', governorMode, - nextAction + nextAction, + queueHandoffStatus: null, + queueHandoffNextWakeCondition: null, + queueHandoffPrUrl: null, + queueAuthoritySource: null }, portfolio: { repositoryCount: 4, repositories: [], + dependencies: [ + { + id: 'vi-history-producer-native-distributor', + status: viHistoryDistributorDependencyStatus, + ownerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + dependentRepository: 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate', + requiredCapability: 'vi-history', + source: 'compare-release-signing-readiness', + releaseSigningStatus: null, + releasePublicationState: viHistoryDistributorDependencyPublicationState, + signingCapabilityState: null, + externalBlocker: viHistoryDistributorDependencyExternalBlocker, + detail: 'fixture' + } + ], unsupportedPaths: [] }, summary: { @@ -42,6 +64,14 @@ function createGovernorPortfolioSummary({ templateMonitoringStatus: 'pass', supportedProofStatus: 'pass', repoGraphStatus: 'pass', + queueHandoffStatus: null, + queueHandoffNextWakeCondition: null, + queueHandoffPrUrl: null, + queueAuthoritySource: null, + viHistoryDistributorDependencyStatus, + viHistoryDistributorDependencyTargetRepository: 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate', + viHistoryDistributorDependencyExternalBlocker, + viHistoryDistributorDependencyPublicationState, portfolioWakeConditionCount: 0, triggeredWakeConditions: [] } @@ -167,6 +197,44 @@ test('planCompareviRuntimeStep keeps queue-empty compare ownership as idle with ); }); +test('planCompareviRuntimeStep explains blocked vi-history distributor dependency during queue-empty compare ownership', async () => { + const decision = await compareviRuntimeTest.planCompareviRuntimeStep({ + repoRoot: '/tmp/repo', + env: { GITHUB_REPOSITORY: 'LabVIEW-Community-CI-CD/compare-vi-cli-action' }, + options: {}, + deps: { + loadDeliveryAgentPolicyFn: async () => ({ implementationRemote: 'origin' }), + runMonitoringWorkInjectionFn: async () => ({ + issueNumber: null, + outputPath: '/tmp/repo/tests/results/_agent/issue/monitoring-work-injection.json', + ledgerPath: '/tmp/repo/tests/results/_agent/ops/ops-decision-ledger.json' + }), + classifyNoStandingPriorityConditionFn: async () => ({ + status: 'classified', + reason: 'queue-empty', + openIssueCount: 0, + message: 'queue empty' + }), + resolveStandingPriorityForRepoFn: async () => ({ found: null }), + readGovernorPortfolioSummaryFn: async () => + createGovernorPortfolioSummary({ + nextAction: 'complete-compare-vi-history-producer-release', + ownerDecisionSource: 'compare-vi-history-distributor-dependency', + governorMode: 'monitoring-active', + viHistoryDistributorDependencyStatus: 'blocked', + viHistoryDistributorDependencyExternalBlocker: 'workflow-signing-secret-missing', + viHistoryDistributorDependencyPublicationState: 'unobserved' + }) + } + }); + + assert.equal(decision.outcome, 'idle'); + assert.match(decision.reason, /vi-history distributor dependency/i); + assert.match(decision.reason, /workflow-signing-secret-missing/i); + assert.equal(decision.artifacts.governorPortfolioHandoff.status, 'owner-match'); + assert.equal(decision.artifacts.governorPortfolioHandoff.viHistoryDistributorDependencyStatus, 'blocked'); +}); + test('planCompareviRuntimeStep describes repo-context pivot preparation when compare remains current owner but template is next', async () => { const decision = await compareviRuntimeTest.planCompareviRuntimeStep({ repoRoot: '/tmp/repo', diff --git a/tools/priority/__tests__/runtime-supervisor.test.mjs b/tools/priority/__tests__/runtime-supervisor.test.mjs index 6184e4a9c..41f1fe297 100644 --- a/tools/priority/__tests__/runtime-supervisor.test.mjs +++ b/tools/priority/__tests__/runtime-supervisor.test.mjs @@ -561,6 +561,26 @@ test('buildCompareviTaskPacket projects concurrent lane status receipts from the }, error: null }, + executionBundle: { + path: 'tests/results/_agent/runtime/execution-cell-bundle.json', + schema: 'priority/execution-cell-bundle-report@v1', + status: 'committed', + cellId: 'cell-sagan-kernel', + laneId: 'docker-lane-01', + cellClass: 'kernel-coordinator', + suiteClass: 'dual-plane-parity', + executionCellLeaseId: 'exec-lease-123', + dockerLaneLeaseId: 'docker-lease-456', + harnessKind: 'teststand-compare-harness', + harnessInstanceId: 'ts-harness-01', + planeBinding: 'dual-plane-parity', + premiumSaganMode: true, + reciprocalLinkReady: true, + effectiveBillableRateUsdPerHour: 375, + operatorAuthorizationRef: 'budget-auth://operator/session-2026-03-24', + isolatedLaneGroupId: 'host-os-fingerprint:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + fingerprintSha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + }, laneStatuses: [ { id: 'hosted-linux-proof', @@ -595,6 +615,9 @@ test('buildCompareviTaskPacket projects concurrent lane status receipts from the deferredLaneCount: 1, manualLaneCount: 1, shadowLaneCount: 0, + executionBundleStatus: 'committed', + executionBundleReciprocalLinkReady: true, + executionBundlePremiumSaganMode: true, pullRequestStatus: 'not-requested', orchestratorDisposition: 'wait-hosted-run' } @@ -763,7 +786,40 @@ test('buildCompareviTaskPacket projects concurrent lane status receipts from the assert.equal(packet.status, 'waiting-ci'); assert.equal(packet.evidence.delivery.laneLifecycle, 'waiting-ci'); + assert.equal(packet.evidence.delivery.executionTopology.status, 'bundle-committed'); + assert.equal(packet.evidence.delivery.executionTopology.executionPlane, 'hosted'); + assert.equal(packet.evidence.delivery.executionTopology.providerId, 'hosted-github-workflow'); + assert.equal(packet.evidence.delivery.executionTopology.workerSlotId, 'worker-slot-2'); + assert.equal(packet.evidence.delivery.executionTopology.cellId, 'cell-sagan-kernel'); + assert.equal(packet.evidence.delivery.executionTopology.laneId, 'docker-lane-01'); + assert.equal(packet.evidence.delivery.executionTopology.cellClass, 'kernel-coordinator'); + assert.equal(packet.evidence.delivery.executionTopology.suiteClass, 'dual-plane-parity'); + assert.equal(packet.evidence.delivery.executionTopology.planeBinding, 'dual-plane-parity'); + assert.equal(packet.evidence.delivery.executionTopology.harnessKind, 'teststand-compare-harness'); + assert.equal(packet.evidence.delivery.executionTopology.harnessInstanceId, 'ts-harness-01'); + assert.equal(packet.evidence.delivery.executionTopology.executionCellLeaseId, 'exec-lease-123'); + assert.equal(packet.evidence.delivery.executionTopology.dockerLaneLeaseId, 'docker-lease-456'); + assert.equal(packet.evidence.delivery.executionTopology.premiumSaganMode, true); + assert.equal(packet.evidence.delivery.executionTopology.reciprocalLinkReady, true); + assert.equal( + packet.evidence.delivery.executionTopology.operatorAuthorizationRef, + 'budget-auth://operator/session-2026-03-24' + ); + assert.equal(packet.evidence.delivery.executionTopology.runtimeSurface, 'windows-native-teststand'); + assert.equal(packet.evidence.delivery.executionTopology.processModelClass, 'parallel-process-model'); + assert.equal(packet.evidence.delivery.executionTopology.windowsOnly, true); + assert.equal(packet.evidence.delivery.executionTopology.requestedSimultaneous, true); + assert.equal(packet.evidence.delivery.concurrentLaneStatus.executionBundle.status, 'committed'); + assert.equal(packet.evidence.delivery.concurrentLaneStatus.executionBundle.cellClass, 'kernel-coordinator'); + assert.equal(packet.evidence.delivery.concurrentLaneStatus.executionBundle.suiteClass, 'dual-plane-parity'); + assert.equal(packet.evidence.delivery.concurrentLaneStatus.executionBundle.harnessKind, 'teststand-compare-harness'); + assert.equal(packet.evidence.delivery.concurrentLaneStatus.executionBundle.reciprocalLinkReady, true); + assert.equal( + packet.evidence.delivery.concurrentLaneStatus.executionBundle.operatorAuthorizationRef, + 'budget-auth://operator/session-2026-03-24' + ); assert.equal(packet.evidence.delivery.concurrentLaneStatus.summary.orchestratorDisposition, 'wait-hosted-run'); + assert.equal(packet.evidence.delivery.concurrentLaneStatus.summary.executionBundleStatus, 'committed'); assert.equal(packet.evidence.delivery.concurrentLaneStatus.summary.deferredLaneCount, 1); assert.equal(packet.evidence.delivery.workerProviderSelection.selectedAssignmentMode, 'async-validation'); assert.equal( @@ -7487,6 +7543,28 @@ test('persistDeliveryAgentRuntimeState keeps deferred concurrent lane obligation 'release-with-deferred-local' ); assert.equal(persistedState.activeLane.concurrentLaneStatus.summary.shadowLaneCount, 1); + assert.equal(persistedState.activeLane.executionTopology.status, 'logical-lanes-tracked'); + assert.equal(persistedState.activeLane.executionTopology.executionPlane, null); + assert.equal(persistedState.activeLane.executionTopology.providerId, null); + assert.equal(persistedState.activeLane.executionTopology.workerSlotId, null); + assert.equal(persistedState.activeLane.executionTopology.cellId, null); + assert.equal(persistedState.activeLane.executionTopology.laneId, null); + assert.equal(persistedState.activeLane.executionTopology.cellClass, null); + assert.equal(persistedState.activeLane.executionTopology.suiteClass, null); + assert.equal(persistedState.activeLane.executionTopology.planeBinding, null); + assert.equal(persistedState.activeLane.executionTopology.harnessKind, null); + assert.equal(persistedState.activeLane.executionTopology.harnessInstanceId, null); + assert.equal(persistedState.activeLane.executionTopology.executionCellLeaseId, null); + assert.equal(persistedState.activeLane.executionTopology.dockerLaneLeaseId, null); + assert.equal(persistedState.activeLane.executionTopology.premiumSaganMode, false); + assert.equal(persistedState.activeLane.executionTopology.reciprocalLinkReady, false); + assert.equal(persistedState.activeLane.executionTopology.operatorAuthorizationRef, null); + assert.equal(persistedState.activeLane.executionTopology.activeLogicalLaneCount, 4); + assert.equal(persistedState.activeLane.executionTopology.seededLogicalLaneCount, 4); + assert.equal(persistedState.activeLane.executionTopology.runtimeSurface, null); + assert.equal(persistedState.activeLane.executionTopology.processModelClass, null); + assert.equal(persistedState.activeLane.executionTopology.windowsOnly, false); + assert.equal(persistedState.activeLane.executionTopology.requestedSimultaneous, false); assert.equal( persistedState.artifacts.concurrentLaneApplyReceiptPath, 'tests/results/_agent/runtime/concurrent-lane-apply-receipt.json' diff --git a/tools/priority/__tests__/sagan-context-concentrator-schema.test.mjs b/tools/priority/__tests__/sagan-context-concentrator-schema.test.mjs new file mode 100644 index 000000000..127d4c6c2 --- /dev/null +++ b/tools/priority/__tests__/sagan-context-concentrator-schema.test.mjs @@ -0,0 +1,175 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import { Ajv2020 } from 'ajv/dist/2020.js'; +import addFormats from 'ajv-formats'; + +import { runSaganContextConcentrator } from '../sagan-context-concentrator.mjs'; + +const repoRoot = path.resolve(process.cwd()); + +function writeJson(filePath, payload) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); +} + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +test('sagan context concentrator report matches schema', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sagan-context-concentrator-schema-')); + writeJson(path.join(tmpDir, '.agent_priority_cache.json'), { + number: 1909, + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + title: '[governor]: build Sagan context concentrator for durable subagent memory', + url: 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/issues/1909', + state: 'OPEN' + }); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'handoff', 'autonomous-governor-summary.json'), { + schema: 'priority/autonomous-governor-summary-report@v1', + generatedAt: '2026-03-23T23:00:00Z', + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + inputs: { + queueEmptyReportPath: 'tests/results/_agent/issue/no-standing-priority.json', + continuitySummaryPath: 'tests/results/_agent/handoff/continuity-summary.json', + monitoringModePath: 'tests/results/_agent/handoff/monitoring-mode.json', + wakeLifecyclePath: 'tests/results/_agent/issue/wake-lifecycle.json', + wakeInvestmentAccountingPath: 'tests/results/_agent/capital/wake-investment-accounting.json', + deliveryRuntimeStatePath: 'tests/results/_agent/runtime/delivery-agent-state.json', + releaseSigningReadinessPath: 'tests/results/_agent/release/release-signing-readiness.json' + }, + compare: {}, + wake: {}, + funding: {}, + summary: { + governorMode: 'compare-governance-work', + currentOwnerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + nextOwnerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + nextAction: 'keep-building-concentrator', + queueState: 'active', + monitoringStatus: 'active', + releaseSigningStatus: 'missing' + } + }); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'handoff', 'autonomous-governor-portfolio-summary.json'), { + schema: 'priority/autonomous-governor-portfolio-summary-report@v1', + generatedAt: '2026-03-23T23:00:10Z', + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + inputs: { + compareGovernorSummaryPath: 'tests/results/_agent/handoff/autonomous-governor-summary.json', + monitoringModePath: 'tests/results/_agent/handoff/monitoring-mode.json', + repoGraphTruthPath: 'tests/results/_agent/handoff/downstream-repo-graph-truth.json' + }, + compare: {}, + portfolio: { + repositoryCount: 1, + repositories: [], + dependencies: [], + unsupportedPaths: [] + }, + summary: { + status: 'active', + governorMode: 'compare-governance-work', + currentOwnerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + nextOwnerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + nextAction: 'keep-building-concentrator', + ownerDecisionSource: 'compare-governor-summary', + templateMonitoringStatus: 'pass', + supportedProofStatus: 'pass', + repoGraphStatus: 'pass', + queueHandoffStatus: 'none', + queueHandoffNextWakeCondition: null, + queueHandoffPrUrl: null, + queueAuthoritySource: 'none', + viHistoryDistributorDependencyStatus: 'unknown', + viHistoryDistributorDependencyTargetRepository: null, + viHistoryDistributorDependencyExternalBlocker: null, + viHistoryDistributorDependencyPublicationState: null, + viHistoryDistributorDependencyPublishedBundleState: null, + viHistoryDistributorDependencyPublishedBundleReleaseTag: null, + viHistoryDistributorDependencyAuthoritativeConsumerPin: null, + viHistoryDistributorDependencySigningAuthorityState: null + } + }); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'handoff', 'monitoring-mode.json'), { + schema: 'agent-handoff/monitoring-mode-v1', + generatedAt: '2026-03-23T23:00:20Z', + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + policy: { + compareRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + }, + summary: { + status: 'active', + futureAgentAction: 'continue-compare-governance-work', + wakeConditionCount: 0 + } + }); + writeJson(path.join(tmpDir, 'tests', 'results', '_agent', 'memory', 'subagent-episodes', 'episode.json'), { + schema: 'priority/subagent-episode-report@v1', + generatedAt: '2026-03-23T22:59:00Z', + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + inputs: { + sourcePath: 'tmp/episode.json' + }, + episodeId: 'episode-1', + agent: { + id: 'euler-id', + name: 'Euler', + role: 'explorer', + model: 'gpt-5.4-mini' + }, + task: { + summary: 'Inspect concentrator seams', + class: 'exploration', + issueNumber: 1909, + issueUrl: 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/issues/1909' + }, + execution: { + status: 'completed', + lane: '1909-sagan-context-concentrator', + branch: 'issue/upstream-1909-sagan-context-concentrator', + executionPlane: 'windows-host', + dockerLaneId: 'docker-euler-001', + hostCapabilityLeaseId: 'lease-euler-001' + }, + summary: { + status: 'reported', + outcome: 'seams-found', + blocker: null, + nextAction: 'patch handoff', + detail: null + }, + evidence: { + filesTouched: [], + receipts: [], + commands: [], + notes: [] + }, + cost: { + observedDurationSeconds: 30, + tokenUsd: 0.02, + operatorLaborUsd: 2.083333, + blendedLowerBoundUsd: 2.103333 + } + }); + + const { report } = await runSaganContextConcentrator( + { + repoRoot: tmpDir + }, + { + now: new Date('2026-03-23T23:01:00Z') + } + ); + + const schema = readJson(path.join(repoRoot, 'docs', 'schemas', 'sagan-context-concentrator-report-v1.schema.json')); + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv); + const validate = ajv.compile(schema); + const valid = validate(report); + assert.equal(valid, true, JSON.stringify(validate.errors, null, 2)); +}); diff --git a/tools/priority/__tests__/sagan-context-concentrator.test.mjs b/tools/priority/__tests__/sagan-context-concentrator.test.mjs new file mode 100644 index 000000000..0c86f3776 --- /dev/null +++ b/tools/priority/__tests__/sagan-context-concentrator.test.mjs @@ -0,0 +1,259 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import { runSaganContextConcentrator } from '../sagan-context-concentrator.mjs'; + +function writeJson(filePath, payload) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); +} + +function createGovernorSummary() { + return { + schema: 'priority/autonomous-governor-summary-report@v1', + generatedAt: '2026-03-23T22:30:00Z', + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + inputs: { + queueEmptyReportPath: 'tests/results/_agent/issue/no-standing-priority.json', + continuitySummaryPath: 'tests/results/_agent/handoff/continuity-summary.json', + monitoringModePath: 'tests/results/_agent/handoff/monitoring-mode.json', + wakeLifecyclePath: 'tests/results/_agent/issue/wake-lifecycle.json', + wakeInvestmentAccountingPath: 'tests/results/_agent/capital/wake-investment-accounting.json', + deliveryRuntimeStatePath: 'tests/results/_agent/runtime/delivery-agent-state.json', + releaseSigningReadinessPath: 'tests/results/_agent/release/release-signing-readiness.json' + }, + compare: {}, + wake: {}, + funding: {}, + summary: { + governorMode: 'compare-governance-work', + currentOwnerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + nextOwnerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + nextAction: 'publish-producer-native-vi-history-bundle', + queueState: 'active', + monitoringStatus: 'active', + releaseSigningStatus: 'warn', + releaseSigningExternalBlocker: 'tag-signature-unverified', + releasePublicationState: 'tag-created-not-published', + releasePublishedBundleState: 'producer-native-incomplete', + releasePublishedBundleReleaseTag: 'v0.6.3-tools.14' + } + }; +} + +function createGovernorPortfolioSummary() { + return { + schema: 'priority/autonomous-governor-portfolio-summary-report@v1', + generatedAt: '2026-03-23T22:31:00Z', + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + inputs: { + compareGovernorSummaryPath: 'tests/results/_agent/handoff/autonomous-governor-summary.json', + monitoringModePath: 'tests/results/_agent/handoff/monitoring-mode.json', + repoGraphTruthPath: 'tests/results/_agent/handoff/downstream-repo-graph-truth.json' + }, + compare: {}, + portfolio: { + repositoryCount: 4, + repositories: [], + dependencies: [], + unsupportedPaths: [] + }, + summary: { + status: 'active', + governorMode: 'compare-governance-work', + currentOwnerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + nextOwnerRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + nextAction: 'publish-producer-native-vi-history-bundle', + ownerDecisionSource: 'compare-governor-summary', + templateMonitoringStatus: 'pass', + supportedProofStatus: 'pass', + repoGraphStatus: 'pass', + queueHandoffStatus: 'none', + queueHandoffNextWakeCondition: null, + queueHandoffPrUrl: null, + queueAuthoritySource: 'none', + viHistoryDistributorDependencyStatus: 'blocked', + viHistoryDistributorDependencyTargetRepository: 'LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate', + viHistoryDistributorDependencyExternalBlocker: 'producer-native-incomplete', + viHistoryDistributorDependencyPublicationState: 'tag-created-not-published', + viHistoryDistributorDependencyPublishedBundleState: 'producer-native-incomplete', + viHistoryDistributorDependencyPublishedBundleReleaseTag: 'v0.6.3-tools.14', + viHistoryDistributorDependencyAuthoritativeConsumerPin: null, + viHistoryDistributorDependencySigningAuthorityState: 'configured' + } + }; +} + +function createMonitoringMode() { + return { + schema: 'agent-handoff/monitoring-mode-v1', + generatedAt: '2026-03-23T22:32:00Z', + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + policy: { + compareRepository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action' + }, + summary: { + status: 'active', + futureAgentAction: 'remain-in-monitoring', + wakeConditionCount: 0 + } + }; +} + +function createEpisode(agentName, status, generatedAt, extra = {}) { + return { + schema: 'priority/subagent-episode-report@v1', + generatedAt, + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + inputs: { + sourcePath: `tmp/${agentName}.json` + }, + episodeId: `${agentName}-${generatedAt.replace(/[:.]/g, '-')}`, + agent: { + id: `${agentName.toLowerCase()}-id`, + name: agentName, + role: 'explorer', + model: 'gpt-5.4-mini' + }, + task: { + summary: extra.taskSummary || `Task from ${agentName}`, + class: 'exploration', + issueNumber: 1909, + issueUrl: 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/issues/1909' + }, + execution: { + status: 'completed', + lane: '1909-sagan-context-concentrator', + branch: 'issue/upstream-1909-sagan-context-concentrator', + executionPlane: extra.executionPlane || 'windows-host', + dockerLaneId: extra.dockerLaneId || null, + hostCapabilityLeaseId: null + }, + summary: { + status, + outcome: extra.outcome || null, + blocker: extra.blocker || null, + nextAction: extra.nextAction || null, + detail: extra.detail || null + }, + evidence: { + filesTouched: extra.filesTouched || [], + receipts: extra.receipts || [], + commands: [], + notes: [] + }, + cost: { + observedDurationSeconds: extra.durationSeconds || 60, + tokenUsd: extra.tokenUsd || 0.05, + operatorLaborUsd: extra.operatorLaborUsd || 4.166667, + blendedLowerBoundUsd: extra.blendedLowerBoundUsd || 4.216667 + } + }; +} + +test('runSaganContextConcentrator builds hot and warm memory from episodes and governor state', async () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sagan-context-concentrator-')); + writeJson(path.join(repoRoot, '.agent_priority_cache.json'), { + number: 1877, + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + title: '[release]: publish CompareVI.Tools bundle with native vi-history capability contract', + url: 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/issues/1877', + state: 'OPEN' + }); + writeJson( + path.join(repoRoot, 'tests', 'results', '_agent', 'handoff', 'autonomous-governor-summary.json'), + createGovernorSummary() + ); + writeJson( + path.join(repoRoot, 'tests', 'results', '_agent', 'handoff', 'autonomous-governor-portfolio-summary.json'), + createGovernorPortfolioSummary() + ); + writeJson( + path.join(repoRoot, 'tests', 'results', '_agent', 'handoff', 'monitoring-mode.json'), + createMonitoringMode() + ); + writeJson( + path.join(repoRoot, 'tests', 'results', '_agent', 'memory', 'subagent-episodes', 'euler.json'), + createEpisode('Euler', 'reported', '2026-03-23T22:20:00Z', { + blocker: 'handoff-seam-open', + nextAction: 'patch Print-AgentHandoff', + dockerLaneId: 'docker-euler-001', + tokenUsd: 0.08, + operatorLaborUsd: 6.25, + blendedLowerBoundUsd: 6.33 + }) + ); + writeJson( + path.join(repoRoot, 'tests', 'results', '_agent', 'memory', 'subagent-episodes', 'euclid.json'), + createEpisode('Euclid', 'reported', '2026-03-23T22:25:00Z', { + nextAction: 'reuse governor schema style', + tokenUsd: 0.05, + operatorLaborUsd: 4.166667, + blendedLowerBoundUsd: 4.216667 + }) + ); + writeJson( + path.join(repoRoot, 'tests', 'results', '_agent', 'memory', 'subagent-episodes', 'hooke.json'), + createEpisode('Hooke', 'completed', '2026-03-23T22:10:00Z', { + outcome: 'template-blocker-confirmed', + nextAction: 'hold template #18 until compare publication', + executionPlane: 'docker-lane', + dockerLaneId: 'docker-hooke-001' + }) + ); + + const { report, outputPath } = await runSaganContextConcentrator( + { + repoRoot + }, + { + now: new Date('2026-03-23T22:35:00Z') + } + ); + + assert.equal(report.schema, 'priority/sagan-context-concentrator-report@v1'); + assert.equal(report.focus.activeIssue.number, 1877); + assert.equal(report.summary.currentOwnerRepository, 'LabVIEW-Community-CI-CD/compare-vi-cli-action'); + assert.equal(report.summary.nextAction, 'publish-producer-native-vi-history-bundle'); + assert.equal(report.summary.hotWorkingSetCount, report.memory.hotWorkingSet.length); + assert.equal(report.episodes.validCount, 3); + assert.ok(report.episodes.byAgent.some((entry) => entry.agentName === 'Euler')); + assert.equal(report.cost.tokenUsd, 0.18); + assert.equal(report.cost.blendedLowerBoundUsd, 14.763334); + assert.ok(report.memory.hotWorkingSet.some((entry) => entry.kind === 'dependency')); + assert.ok(report.memory.hotWorkingSet.some((entry) => entry.agentName === 'Euler')); + assert.ok(report.memory.warmMemory.some((entry) => entry.agentName === 'Hooke')); + assert.ok(fs.existsSync(outputPath)); +}); + +test('runSaganContextConcentrator tolerates missing optional episode directory', async () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sagan-context-concentrator-empty-')); + writeJson( + path.join(repoRoot, 'tests', 'results', '_agent', 'handoff', 'autonomous-governor-summary.json'), + createGovernorSummary() + ); + writeJson( + path.join(repoRoot, 'tests', 'results', '_agent', 'handoff', 'autonomous-governor-portfolio-summary.json'), + createGovernorPortfolioSummary() + ); + writeJson( + path.join(repoRoot, 'tests', 'results', '_agent', 'handoff', 'monitoring-mode.json'), + createMonitoringMode() + ); + + const { report } = await runSaganContextConcentrator( + { + repoRoot + }, + { + now: new Date('2026-03-23T22:40:00Z') + } + ); + + assert.equal(report.episodes.totalCount, 0); + assert.equal(report.summary.concentrationStatus, 'pass'); + assert.equal(report.summary.hotWorkingSetCount >= 2, true); +}); diff --git a/tools/priority/__tests__/subagent-episode-schema.test.mjs b/tools/priority/__tests__/subagent-episode-schema.test.mjs new file mode 100644 index 000000000..0e61c3a86 --- /dev/null +++ b/tools/priority/__tests__/subagent-episode-schema.test.mjs @@ -0,0 +1,85 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import { Ajv2020 } from 'ajv/dist/2020.js'; +import addFormats from 'ajv-formats'; + +import { runSubagentEpisode } from '../subagent-episode.mjs'; + +const repoRoot = path.resolve(process.cwd()); + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +test('subagent episode report matches schema', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subagent-episode-schema-')); + const inputPath = path.join(tmpDir, 'episode-input.json'); + const outputPath = path.join(tmpDir, 'subagent-episode.json'); + + fs.writeFileSync( + inputPath, + `${JSON.stringify( + { + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + generatedAt: '2026-03-23T22:10:00Z', + agent: { + id: '019d1ba6-746c-72a2-b73c-3ebc239843f1', + name: 'Hooke', + role: 'explorer', + model: 'gpt-5.4-mini' + }, + task: { + summary: 'Verify template blocker state', + class: 'verification', + issueNumber: 18, + issueUrl: 'https://github.com/LabVIEW-Community-CI-CD/LabviewGitHubCiTemplate/issues/18' + }, + execution: { + status: 'completed', + lane: 'LabviewGitHubCiTemplate-18-producer-native-consumer', + executionPlane: 'windows-host' + }, + summary: { + status: 'reported', + outcome: 'template-blocker-confirmed', + blocker: 'compare-publication-pending', + nextAction: 'wait for producer-native release publication' + }, + evidence: { + receipts: ['tests/results/_agent/release/release-published-bundle-observer.json'] + }, + cost: { + observedDurationSeconds: 120, + tokenUsd: 0.08, + operatorLaborUsd: 8.333333, + blendedLowerBoundUsd: 8.413333 + } + }, + null, + 2 + )}\n`, + 'utf8' + ); + + const { report } = await runSubagentEpisode( + { + repoRoot: tmpDir, + inputPath, + outputPath + }, + { + now: new Date('2026-03-23T22:10:30Z') + } + ); + + const schema = readJson(path.join(repoRoot, 'docs', 'schemas', 'subagent-episode-report-v1.schema.json')); + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv); + const validate = ajv.compile(schema); + const valid = validate(report); + assert.equal(valid, true, JSON.stringify(validate.errors, null, 2)); +}); diff --git a/tools/priority/__tests__/subagent-episode.test.mjs b/tools/priority/__tests__/subagent-episode.test.mjs new file mode 100644 index 000000000..28f790aa9 --- /dev/null +++ b/tools/priority/__tests__/subagent-episode.test.mjs @@ -0,0 +1,133 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import { + REPORT_SCHEMA, + buildSubagentEpisodeReport, + parseArgs, + runSubagentEpisode +} from '../subagent-episode.mjs'; + +function writeJson(filePath, payload) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); +} + +test('parseArgs requires input and keeps defaults', () => { + const parsed = parseArgs([ + 'node', + 'subagent-episode.mjs', + '--input', + 'tmp/episode.json' + ]); + + assert.equal(parsed.inputPath, 'tmp/episode.json'); + assert.equal(parsed.outputPath, null); +}); + +test('buildSubagentEpisodeReport normalizes source payload', () => { + const report = buildSubagentEpisodeReport( + { + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + generatedAt: '2026-03-23T21:00:00Z', + agent: { + id: '019d11a9-3e6b-7073-b602-7a0a2085f106', + name: 'Euler', + role: 'explorer', + model: 'gpt-5.4-mini' + }, + task: { + summary: 'Inspect handoff seams', + class: 'exploration', + issueNumber: 1909, + issueUrl: 'https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/issues/1909' + }, + execution: { + status: 'completed', + lane: '1909-sagan-context-concentrator', + branch: 'issue/upstream-1909-sagan-context-concentrator', + executionPlane: 'windows-host', + dockerLaneId: 'docker-euler-001', + hostCapabilityLeaseId: 'lease-euler-001' + }, + summary: { + status: 'reported', + outcome: 'handoff-seams-identified', + blocker: null, + nextAction: 'wire concentrator into Print-AgentHandoff', + detail: 'Print-AgentHandoff and Import-HandoffState are the right seams.' + }, + evidence: { + filesTouched: ['tools/Print-AgentHandoff.ps1'], + receipts: ['tests/results/_agent/handoff/autonomous-governor-summary.json'], + commands: ['rg -n governor tools/Print-AgentHandoff.ps1'], + notes: ['Focus on handoff refresh and render path.'] + }, + cost: { + observedDurationSeconds: 90, + tokenUsd: 0.14, + operatorLaborUsd: 6.25, + blendedLowerBoundUsd: 6.39 + } + }, + { + repoRoot: 'E:/comparevi-lanes/1909-sagan-context-concentrator', + inputPath: 'tmp/subagent-input.json', + now: new Date('2026-03-23T21:05:00Z') + } + ); + + assert.equal(report.schema, REPORT_SCHEMA); + assert.equal(report.agent.name, 'Euler'); + assert.equal(report.task.issueNumber, 1909); + assert.equal(report.execution.dockerLaneId, 'docker-euler-001'); + assert.equal(report.summary.nextAction, 'wire concentrator into Print-AgentHandoff'); + assert.equal(report.cost.blendedLowerBoundUsd, 6.39); +}); + +test('runSubagentEpisode writes a normalized episode report', async () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'subagent-episode-')); + const inputPath = path.join(repoRoot, 'tmp', 'episode-input.json'); + writeJson(inputPath, { + repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action', + agent: { + id: '019d11a9-3e7f-7331-a692-63a1c6c78904', + name: 'Euclid', + role: 'explorer', + model: 'gpt-5.4-mini' + }, + task: { + summary: 'Catalog analogous receipt patterns', + class: 'exploration', + issueNumber: 1909 + }, + summary: { + status: 'reported', + outcome: 'receipt-templates-found', + nextAction: 'reuse governor summary schema style' + }, + evidence: { + notes: ['Use autonomous-governor-summary as the main receipt template.'] + } + }); + + const { report, outputPath } = await runSubagentEpisode( + { + repoRoot, + inputPath + }, + { + now: new Date('2026-03-23T22:00:00Z') + } + ); + + assert.ok(outputPath.includes('subagent-episodes')); + assert.equal(report.schema, REPORT_SCHEMA); + assert.equal(report.agent.name, 'Euclid'); + assert.equal(report.summary.status, 'reported'); + assert.equal(report.execution.status, 'completed'); + assert.ok(fs.existsSync(outputPath)); +}); diff --git a/tools/priority/__tests__/supply-chain-trust-gate.test.mjs b/tools/priority/__tests__/supply-chain-trust-gate.test.mjs index 821b7338f..72d039704 100644 --- a/tools/priority/__tests__/supply-chain-trust-gate.test.mjs +++ b/tools/priority/__tests__/supply-chain-trust-gate.test.mjs @@ -349,6 +349,14 @@ test('verifyReleaseTagSignature fails for unsigned tag', async () => { }); assert.ok(result.failures.some((failure) => failure.code === 'tag-signature-unverified')); + assert.ok( + result.failures.some( + (failure) => + failure.code === 'tag-signature-unverified' && + String(failure.hint).includes('priority:release:signing:readiness') && + String(failure.hint).includes('repair_existing_tag = true') + ) + ); assert.equal(result.status.verified, false); assert.equal(result.status.reason, 'unsigned'); }); @@ -380,6 +388,14 @@ test('verifyReleaseTagSignature fails for lightweight tag', async () => { }); assert.ok(result.failures.some((failure) => failure.code === 'tag-not-annotated')); + assert.ok( + result.failures.some( + (failure) => + failure.code === 'tag-not-annotated' && + String(failure.hint).includes('priority:release:signing:readiness') && + String(failure.hint).includes('repair_existing_tag = true') + ) + ); assert.equal(result.status.annotated, false); assert.equal(result.status.reason, 'not-annotated'); }); diff --git a/tools/priority/__tests__/workflow-pwsh-continuation-contract.test.mjs b/tools/priority/__tests__/workflow-pwsh-continuation-contract.test.mjs index 53446fbd4..4f82289cf 100644 --- a/tools/priority/__tests__/workflow-pwsh-continuation-contract.test.mjs +++ b/tools/priority/__tests__/workflow-pwsh-continuation-contract.test.mjs @@ -80,7 +80,7 @@ test('release workflow explicitly dispatches publish-tools-image with actions wr assert.ok(dispatchStep, 'release workflow should dispatch publish-tools-image explicitly'); assert.equal(dispatchStep.uses, 'actions/github-script@v8'); assert.match(dispatchStep.with.script, /workflow_id:\s*'publish-tools-image\.yml'/); - assert.match(dispatchStep.with.script, /ref:\s*'\$\{\{\s*github\.ref_name\s*\}\}'/); + assert.match(dispatchStep.with.script, /ref:\s*'develop'/); assert.match( dispatchStep.with.script, /const releaseVersion = '\$\{\{\s*steps\.comparevi_tools\.outputs\.comparevi_tools_release_version\s*\}\}';/ @@ -88,6 +88,23 @@ test('release workflow explicitly dispatches publish-tools-image with actions wr assert.match(dispatchStep.with.script, /const releaseChannel = releaseVersion\.includes\('-rc\.'\) \? 'rc' : 'stable';/); }); +test('release workflow remains tag-triggered and also supports workflow_dispatch replay for repaired tags', () => { + const workflowPath = path.join(workflowsRoot, 'release.yml'); + const workflowRaw = readFileSync(workflowPath, 'utf8'); + const workflow = yaml.load(workflowRaw); + + assert.deepEqual(workflow?.on?.push?.tags, ['v*']); + assert.ok(Object.prototype.hasOwnProperty.call(workflow?.on ?? {}, 'workflow_dispatch')); + assert.equal(workflow?.on?.workflow_dispatch?.inputs?.release_tag?.required, true); + assert.equal(workflow?.on?.workflow_dispatch?.inputs?.release_tag?.type, 'string'); + assert.match(workflowRaw, /name: Resolve release target tag/); + assert.match(workflowRaw, /tag='\$\{\{\s*inputs\.release_tag\s*\}\}'/); + assert.match(workflowRaw, /target_tag:\s*\$\{\{\s*steps\.release_target\.outputs\.tag\s*\}\}/); + assert.match(workflowRaw, /ref:\s*\$\{\{\s*steps\.release_target\.outputs\.tag\s*\}\}/); + assert.match(workflowRaw, /RELEASE_TAG:\s*\$\{\{\s*needs\.certification-matrix\.outputs\.target_tag\s*\}\}/); + assert.match(workflowRaw, /tag_name:\s*\$\{\{\s*env\.RELEASE_TAG\s*\}\}/); +}); + test('release workflow resolves downloaded artifacts through the shared helper before validation', () => { const workflowPath = path.join(workflowsRoot, 'release.yml'); const workflowRaw = readFileSync(workflowPath, 'utf8'); @@ -125,7 +142,8 @@ test('release workflow resolves downloaded artifacts through the shared helper b assert.match(workflowRaw, /name: Download release-review scenario artifacts/); assert.match(workflowRaw, /merge-multiple:\s*true/); assert.match(workflowRaw, /name: Resolve release source commit/); - assert.match(workflowRaw, /git rev-parse "\$\{\{\s*github\.ref_name\s*\}\}\^\{commit\}"/); + assert.match(workflowRaw, /git fetch --force --tags origin "refs\/tags\/\$\{RELEASE_TAG\}:refs\/tags\/\$\{RELEASE_TAG\}"/); + assert.match(workflowRaw, /git rev-parse "\$\{RELEASE_TAG\}\^\{commit\}"/); assert.match(workflowRaw, /name: Resolve downstream proving artifact selection/); assert.match(workflowRaw, /resolve-downstream-proving-artifact\.mjs/); assert.match(workflowRaw, /--workflow downstream-promotion\.yml/); @@ -155,6 +173,19 @@ test('release workflow resolves downloaded artifacts through the shared helper b assert.doesNotMatch(workflowRaw, /steps\.contract_artifacts\.outputs\.linux_tarball_path/); }); +test('release workflow appends repair-mode guidance for unsigned or lightweight tags', () => { + const workflowPath = path.join(workflowsRoot, 'release.yml'); + const workflowRaw = readFileSync(workflowPath, 'utf8'); + + assert.match(workflowRaw, /name: Append release trust remediation guidance/); + assert.match(workflowRaw, /node tools\/priority\/release-trust-remediation\.mjs/); + assert.match(workflowRaw, /--trust-report tests\/results\/_agent\/supply-chain\/release-trust-gate\.json/); + assert.match(workflowRaw, /--tag-ref "\$\{RELEASE_TAG\}"/); + assert.match(workflowRaw, /--output tests\/results\/_agent\/release\/release-trust-remediation\.md/); + assert.match(workflowRaw, /--summary "\$GITHUB_STEP_SUMMARY"/); + assert.match(workflowRaw, /tests\/results\/_agent\/release\/release-trust-remediation\.md/); +}); + test('monthly release workflow marks itself as the SLO remediation candidate', () => { const workflowPath = path.join(workflowsRoot, 'monthly-stability-release.yml'); const workflowRaw = readFileSync(workflowPath, 'utf8'); diff --git a/tools/priority/autonomous-governor-portfolio-summary.mjs b/tools/priority/autonomous-governor-portfolio-summary.mjs index d711f73ab..79d0fb8a3 100644 --- a/tools/priority/autonomous-governor-portfolio-summary.mjs +++ b/tools/priority/autonomous-governor-portfolio-summary.mjs @@ -140,6 +140,79 @@ function createWakeConditionsByRepository(triggeredWakeConditions) { }; } +function deriveViHistoryDistributorDependency(compareGovernorSummary, monitoringMode) { + const compareRepository = + asOptional(compareGovernorSummary?.summary?.currentOwnerRepository) || + asOptional(monitoringMode?.policy?.compareRepository) || + asOptional(compareGovernorSummary?.repository); + const dependentRepository = asOptional(monitoringMode?.policy?.pivotTargetRepository); + const releaseSigningReadiness = compareGovernorSummary?.compare?.releaseSigningReadiness; + const releaseSigningStatus = + asOptional(compareGovernorSummary?.summary?.releaseSigningStatus) || asOptional(releaseSigningReadiness?.status); + const releasePublicationState = + asOptional(compareGovernorSummary?.summary?.releasePublicationState) || + asOptional(releaseSigningReadiness?.publicationState); + const publishedBundleState = + asOptional(compareGovernorSummary?.summary?.releasePublishedBundleState) || + asOptional(releaseSigningReadiness?.publishedBundleState); + const publishedBundleReleaseTag = + asOptional(compareGovernorSummary?.summary?.releasePublishedBundleReleaseTag) || + asOptional(releaseSigningReadiness?.publishedBundleReleaseTag); + const publishedBundleAuthoritativeConsumerPin = + asOptional(compareGovernorSummary?.summary?.releasePublishedBundleAuthoritativeConsumerPin) || + asOptional(releaseSigningReadiness?.publishedBundleAuthoritativeConsumerPin); + const signingCapabilityState = asOptional(releaseSigningReadiness?.signingCapabilityState); + const signingAuthorityState = + asOptional(compareGovernorSummary?.summary?.releaseSigningAuthorityState) || + asOptional(releaseSigningReadiness?.signingAuthorityState); + const releaseConductorApplyState = + asOptional(compareGovernorSummary?.summary?.releaseConductorApplyState) || + asOptional(releaseSigningReadiness?.releaseConductorApplyState); + const externalBlocker = + asOptional(compareGovernorSummary?.summary?.releaseSigningExternalBlocker) || + asOptional(releaseSigningReadiness?.externalBlocker); + + let status = 'unknown'; + let detail = 'missing-release-signing-readiness'; + if (publishedBundleState === 'producer-native-ready' || releasePublicationState === 'producer-native-ready') { + status = 'ready'; + detail = 'producer-native-release-ready'; + } else if ( + publishedBundleState || + releaseSigningStatus || + releasePublicationState || + signingCapabilityState || + externalBlocker + ) { + status = 'blocked'; + detail = + publishedBundleState && publishedBundleState !== 'unobserved' + ? 'awaiting-producer-native-bundle-publication' + : externalBlocker + ? 'awaiting-compare-release-signing-blocker-clear' + : 'awaiting-producer-native-release-publication'; + } + + return { + id: 'vi-history-producer-native-distributor', + status, + ownerRepository: compareRepository, + dependentRepository, + requiredCapability: 'vi-history', + source: 'compare-release-signing-readiness', + releaseSigningStatus, + releasePublicationState, + publishedBundleState, + publishedBundleReleaseTag, + publishedBundleAuthoritativeConsumerPin, + signingCapabilityState, + signingAuthorityState, + releaseConductorApplyState, + externalBlocker, + detail + }; +} + function derivePortfolioMode(compareGovernorSummary, monitoringMode) { const compareMode = asOptional(compareGovernorSummary?.summary?.governorMode); const futureAgentAction = asOptional(monitoringMode?.summary?.futureAgentAction); @@ -151,7 +224,136 @@ function derivePortfolioMode(compareGovernorSummary, monitoringMode) { return compareMode || 'attention-required'; } -function deriveOwners(compareGovernorSummary, monitoringMode, portfolioMode) { +function deriveExecutionTopology(compareGovernorSummary) { + const executionTopology = compareGovernorSummary?.compare?.deliveryRuntime?.executionTopology; + if (executionTopology && typeof executionTopology === 'object' && !Array.isArray(executionTopology)) { + return { + status: asOptional(executionTopology.status), + executionPlane: asOptional(executionTopology.executionPlane), + providerId: asOptional(executionTopology.providerId), + workerSlotId: asOptional(executionTopology.workerSlotId), + activeLogicalLaneCount: Number.isInteger(executionTopology.activeLogicalLaneCount) + ? executionTopology.activeLogicalLaneCount + : null, + seededLogicalLaneCount: Number.isInteger(executionTopology.seededLogicalLaneCount) + ? executionTopology.seededLogicalLaneCount + : null, + catalogCount: Number.isInteger(executionTopology.catalogCount) ? executionTopology.catalogCount : 0, + runtimeSurface: asOptional(executionTopology.runtimeSurface), + processModelClass: asOptional(executionTopology.processModelClass), + windowsOnly: executionTopology.windowsOnly === true, + requestedSimultaneous: executionTopology.requestedSimultaneous === true, + cellClass: asOptional(executionTopology.cellClass), + suiteClass: asOptional(executionTopology.suiteClass), + operatorAuthorizationRef: asOptional(executionTopology.operatorAuthorizationRef), + premiumSaganMode: executionTopology.premiumSaganMode === true, + reciprocalLinkReady: executionTopology.reciprocalLinkReady === true, + logicalLaneActivation: { + activeLaneCount: Number.isInteger(executionTopology?.logicalLaneActivation?.activeLaneCount) + ? executionTopology.logicalLaneActivation.activeLaneCount + : null, + seededLaneCount: Number.isInteger(executionTopology?.logicalLaneActivation?.seededLaneCount) + ? executionTopology.logicalLaneActivation.seededLaneCount + : null, + catalogCount: Number.isInteger(executionTopology?.logicalLaneActivation?.catalogCount) + ? executionTopology.logicalLaneActivation.catalogCount + : 0 + }, + providerDispatch: { + providerId: asOptional(executionTopology?.providerDispatch?.providerId), + providerKind: asOptional(executionTopology?.providerDispatch?.providerKind), + executionPlane: asOptional(executionTopology?.providerDispatch?.executionPlane), + assignmentMode: asOptional(executionTopology?.providerDispatch?.assignmentMode), + dispatchSurface: asOptional(executionTopology?.providerDispatch?.dispatchSurface), + completionMode: asOptional(executionTopology?.providerDispatch?.completionMode), + workerSlotId: asOptional(executionTopology?.providerDispatch?.workerSlotId), + dispatchStatus: asOptional(executionTopology?.providerDispatch?.dispatchStatus), + completionStatus: asOptional(executionTopology?.providerDispatch?.completionStatus), + failureClass: asOptional(executionTopology?.providerDispatch?.failureClass) + }, + executionBundle: { + status: asOptional(executionTopology?.executionBundle?.status), + planeBinding: asOptional(executionTopology?.executionBundle?.planeBinding), + cellClass: asOptional(executionTopology?.executionBundle?.cellClass), + suiteClass: asOptional(executionTopology?.executionBundle?.suiteClass), + premiumSaganMode: executionTopology?.executionBundle?.premiumSaganMode === true, + reciprocalLinkReady: executionTopology?.executionBundle?.reciprocalLinkReady === true, + effectiveBillableRateUsdPerHour: Number.isFinite(executionTopology?.executionBundle?.effectiveBillableRateUsdPerHour) + ? executionTopology.executionBundle.effectiveBillableRateUsdPerHour + : null, + executionCellLeaseId: asOptional(executionTopology?.executionBundle?.executionCellLeaseId), + dockerLaneLeaseId: asOptional(executionTopology?.executionBundle?.dockerLaneLeaseId), + harnessKind: asOptional(executionTopology?.executionBundle?.harnessKind), + harnessInstanceId: asOptional(executionTopology?.executionBundle?.harnessInstanceId), + operatorAuthorizationRef: asOptional(executionTopology?.executionBundle?.operatorAuthorizationRef), + cellId: asOptional(executionTopology?.executionBundle?.cellId), + laneId: asOptional(executionTopology?.executionBundle?.laneId), + isolatedLaneGroupId: asOptional(executionTopology?.executionBundle?.isolatedLaneGroupId), + fingerprintSha256: asOptional(executionTopology?.executionBundle?.fingerprintSha256) + } + }; + } + + return { + status: asOptional(compareGovernorSummary?.summary?.executionBundleStatus), + executionPlane: asOptional(compareGovernorSummary?.summary?.executionBundlePlaneBinding), + providerId: null, + workerSlotId: null, + activeLogicalLaneCount: null, + seededLogicalLaneCount: null, + catalogCount: 0, + runtimeSurface: asOptional(compareGovernorSummary?.summary?.executionTopologyRuntimeSurface), + processModelClass: asOptional(compareGovernorSummary?.summary?.executionTopologyProcessModelClass), + windowsOnly: compareGovernorSummary?.summary?.executionTopologyWindowsOnly === true, + requestedSimultaneous: compareGovernorSummary?.summary?.executionTopologyRequestedSimultaneous === true, + cellClass: asOptional(compareGovernorSummary?.summary?.executionTopologyCellClass), + suiteClass: asOptional(compareGovernorSummary?.summary?.executionTopologySuiteClass), + operatorAuthorizationRef: asOptional(compareGovernorSummary?.summary?.executionTopologyOperatorAuthorizationRef), + premiumSaganMode: compareGovernorSummary?.summary?.executionBundlePremiumSaganMode === true, + reciprocalLinkReady: compareGovernorSummary?.summary?.executionBundleReciprocalLinkReady === true, + logicalLaneActivation: { + activeLaneCount: null, + seededLaneCount: null, + catalogCount: 0 + }, + providerDispatch: { + providerId: null, + providerKind: null, + executionPlane: null, + assignmentMode: null, + dispatchSurface: null, + completionMode: null, + workerSlotId: null, + dispatchStatus: null, + completionStatus: null, + failureClass: null + }, + executionBundle: { + status: asOptional(compareGovernorSummary?.summary?.executionBundleStatus), + planeBinding: asOptional(compareGovernorSummary?.summary?.executionBundlePlaneBinding), + cellClass: null, + suiteClass: null, + premiumSaganMode: compareGovernorSummary?.summary?.executionBundlePremiumSaganMode === true, + reciprocalLinkReady: compareGovernorSummary?.summary?.executionBundleReciprocalLinkReady === true, + effectiveBillableRateUsdPerHour: Number.isFinite( + compareGovernorSummary?.summary?.executionBundleEffectiveBillableRateUsdPerHour + ) + ? compareGovernorSummary.summary.executionBundleEffectiveBillableRateUsdPerHour + : null, + executionCellLeaseId: null, + dockerLaneLeaseId: null, + harnessKind: null, + harnessInstanceId: null, + operatorAuthorizationRef: null, + cellId: null, + laneId: null, + isolatedLaneGroupId: null, + fingerprintSha256: null + } + }; +} + +function deriveOwners(compareGovernorSummary, monitoringMode, portfolioMode, viHistoryDistributorDependency) { const compareRepository = asOptional(compareGovernorSummary?.summary?.currentOwnerRepository) || asOptional(monitoringMode?.policy?.compareRepository) || @@ -170,6 +372,22 @@ function deriveOwners(compareGovernorSummary, monitoringMode, portfolioMode) { } if (portfolioMode === 'monitoring-active') { + if ( + futureAgentAction === 'future-agent-may-pivot' && + viHistoryDistributorDependency?.status !== 'ready' && + viHistoryDistributorDependency?.dependentRepository === pivotTargetRepository + ) { + return { + currentOwnerRepository: compareRepository, + nextOwnerRepository: compareRepository, + nextAction: + viHistoryDistributorDependency.status === 'unknown' + ? 'refresh-compare-vi-history-distributor-dependency' + : 'complete-compare-vi-history-producer-release', + ownerDecisionSource: 'compare-vi-history-distributor-dependency' + }; + } + return { currentOwnerRepository: compareRepository, nextOwnerRepository: futureAgentAction === 'future-agent-may-pivot' ? pivotTargetRepository : compareRepository, @@ -295,13 +513,20 @@ function buildReport({ now }) { const portfolioMode = derivePortfolioMode(compareGovernorSummary, monitoringMode); - const ownerDecision = deriveOwners(compareGovernorSummary, monitoringMode, portfolioMode); + const viHistoryDistributorDependency = deriveViHistoryDistributorDependency(compareGovernorSummary, monitoringMode); + const ownerDecision = deriveOwners( + compareGovernorSummary, + monitoringMode, + portfolioMode, + viHistoryDistributorDependency + ); const repositoryEntries = deriveRepositoryEntries(repoGraphTruth, monitoringMode, compareGovernorSummary); const templateMonitoringStatus = deriveTemplateMonitoringStatus(repositoryEntries); const supportedProofStatus = deriveSupportedProofStatus(repositoryEntries); const triggeredWakeConditions = Array.isArray(monitoringMode?.summary?.triggeredWakeConditions) ? monitoringMode.summary.triggeredWakeConditions : []; + const executionTopology = deriveExecutionTopology(compareGovernorSummary); return { schema: 'priority/autonomous-governor-portfolio-summary-report@v1', @@ -324,11 +549,23 @@ function buildReport({ queueHandoffStatus: asOptional(compareGovernorSummary?.summary?.queueHandoffStatus), queueHandoffNextWakeCondition: asOptional(compareGovernorSummary?.summary?.queueHandoffNextWakeCondition), queueHandoffPrUrl: asOptional(compareGovernorSummary?.summary?.queueHandoffPrUrl), - queueAuthoritySource: asOptional(compareGovernorSummary?.summary?.queueAuthoritySource) + queueAuthoritySource: asOptional(compareGovernorSummary?.summary?.queueAuthoritySource), + executionTopology, + executionBundleStatus: asOptional(compareGovernorSummary?.summary?.executionBundleStatus), + executionBundlePlaneBinding: asOptional(compareGovernorSummary?.summary?.executionBundlePlaneBinding), + executionBundlePremiumSaganMode: compareGovernorSummary?.summary?.executionBundlePremiumSaganMode === true, + executionBundleReciprocalLinkReady: + compareGovernorSummary?.summary?.executionBundleReciprocalLinkReady === true, + executionBundleEffectiveBillableRateUsdPerHour: Number.isFinite( + compareGovernorSummary?.summary?.executionBundleEffectiveBillableRateUsdPerHour + ) + ? compareGovernorSummary.summary.executionBundleEffectiveBillableRateUsdPerHour + : null }, portfolio: { repositoryCount: repositoryEntries.length, repositories: repositoryEntries, + dependencies: [viHistoryDistributorDependency], unsupportedPaths: Array.isArray(monitoringMode?.templateMonitoring?.unsupportedPaths) ? monitoringMode.templateMonitoring.unsupportedPaths.map((entry) => ({ name: asOptional(entry?.name), @@ -351,6 +588,39 @@ function buildReport({ queueHandoffNextWakeCondition: asOptional(compareGovernorSummary?.summary?.queueHandoffNextWakeCondition), queueHandoffPrUrl: asOptional(compareGovernorSummary?.summary?.queueHandoffPrUrl), queueAuthoritySource: asOptional(compareGovernorSummary?.summary?.queueAuthoritySource), + executionTopologyStatus: executionTopology.status, + executionTopologyExecutionPlane: executionTopology.executionPlane, + executionTopologyProviderId: executionTopology.providerId, + executionTopologyWorkerSlotId: executionTopology.workerSlotId, + executionTopologyActiveLogicalLaneCount: executionTopology.activeLogicalLaneCount, + executionTopologySeededLogicalLaneCount: executionTopology.seededLogicalLaneCount, + executionTopologyRuntimeSurface: executionTopology.runtimeSurface, + executionTopologyProcessModelClass: executionTopology.processModelClass, + executionTopologyWindowsOnly: executionTopology.windowsOnly, + executionTopologyRequestedSimultaneous: executionTopology.requestedSimultaneous, + executionTopologyCellClass: executionTopology.cellClass, + executionTopologySuiteClass: executionTopology.suiteClass, + executionTopologyOperatorAuthorizationRef: executionTopology.operatorAuthorizationRef, + executionBundleStatus: asOptional(compareGovernorSummary?.summary?.executionBundleStatus), + executionBundlePlaneBinding: asOptional(compareGovernorSummary?.summary?.executionBundlePlaneBinding), + executionBundlePremiumSaganMode: compareGovernorSummary?.summary?.executionBundlePremiumSaganMode === true, + executionBundleReciprocalLinkReady: + compareGovernorSummary?.summary?.executionBundleReciprocalLinkReady === true, + executionBundleEffectiveBillableRateUsdPerHour: Number.isFinite( + compareGovernorSummary?.summary?.executionBundleEffectiveBillableRateUsdPerHour + ) + ? compareGovernorSummary.summary.executionBundleEffectiveBillableRateUsdPerHour + : null, + viHistoryDistributorDependencyStatus: viHistoryDistributorDependency.status, + viHistoryDistributorDependencyTargetRepository: viHistoryDistributorDependency.dependentRepository, + viHistoryDistributorDependencyExternalBlocker: viHistoryDistributorDependency.externalBlocker, + viHistoryDistributorDependencyPublicationState: viHistoryDistributorDependency.releasePublicationState, + viHistoryDistributorDependencyPublishedBundleState: viHistoryDistributorDependency.publishedBundleState, + viHistoryDistributorDependencyPublishedBundleReleaseTag: viHistoryDistributorDependency.publishedBundleReleaseTag, + viHistoryDistributorDependencyAuthoritativeConsumerPin: + viHistoryDistributorDependency.publishedBundleAuthoritativeConsumerPin, + viHistoryDistributorDependencySigningAuthorityState: viHistoryDistributorDependency.signingAuthorityState, + viHistoryDistributorDependencyReleaseConductorApplyState: viHistoryDistributorDependency.releaseConductorApplyState, portfolioWakeConditionCount: triggeredWakeConditions.length, triggeredWakeConditions } diff --git a/tools/priority/autonomous-governor-summary.mjs b/tools/priority/autonomous-governor-summary.mjs index e8d432739..0742d148b 100644 --- a/tools/priority/autonomous-governor-summary.mjs +++ b/tools/priority/autonomous-governor-summary.mjs @@ -56,6 +56,13 @@ export const DEFAULT_DELIVERY_RUNTIME_STATE_PATH = path.join( 'runtime', 'delivery-agent-state.json' ); +export const DEFAULT_RELEASE_SIGNING_READINESS_PATH = path.join( + 'tests', + 'results', + '_agent', + 'release', + 'release-signing-readiness.json' +); function asOptional(value) { if (value == null) { @@ -179,6 +186,310 @@ function parseBoolean(value) { return value === true; } +function coalesceBoolean(...values) { + for (const value of values) { + if (typeof value === 'boolean') { + return value; + } + } + return false; +} + +function normalizeLower(value) { + return typeof value === 'string' ? value.trim().toLowerCase() : ''; +} + +function deriveExecutionTopologyProcessModel(executionBundle) { + const planeBinding = asOptional(executionBundle?.planeBinding); + const normalizedPlaneBinding = normalizeLower(planeBinding); + const harnessKind = asOptional(executionBundle?.harnessKind); + const requestedSimultaneous = normalizedPlaneBinding === 'dual-plane-parity'; + const windowsNativeTestStand = + (!harnessKind || harnessKind === 'teststand-compare-harness') && + (requestedSimultaneous || normalizedPlaneBinding.startsWith('native-labview-')); + const runtimeSurface = windowsNativeTestStand ? 'windows-native-teststand' : null; + const processModelClass = !runtimeSurface + ? null + : requestedSimultaneous + ? 'parallel-process-model' + : 'sequential-process-model'; + + return { + runtimeSurface, + processModelClass, + windowsOnly: runtimeSurface === 'windows-native-teststand', + requestedSimultaneous + }; +} + +function deriveExecutionTopologyStatus({ activeLogicalLaneCount, seededLogicalLaneCount, providerDispatch, executionBundle }) { + const bundleStatus = asOptional(executionBundle?.status); + if (bundleStatus) { + return `bundle-${bundleStatus}`; + } + + const completionStatus = asOptional(providerDispatch?.completionStatus); + if (completionStatus) { + return `provider-${completionStatus}`; + } + + const dispatchStatus = asOptional(providerDispatch?.dispatchStatus); + if (dispatchStatus) { + return `provider-${dispatchStatus}`; + } + + if ((activeLogicalLaneCount ?? 0) > 0 || (seededLogicalLaneCount ?? 0) > 0) { + return 'logical-lanes-tracked'; + } + + return 'none'; +} + +function deriveExecutionTopology({ deliveryRuntimeState, activeLane, executionBundle }) { + const logicalLaneActivation = normalizeOptionalObject(deliveryRuntimeState?.logicalLaneActivation); + const logicalLaneCatalog = Array.isArray(logicalLaneActivation?.catalog) ? logicalLaneActivation.catalog : []; + const providerDispatch = + normalizeOptionalObject(activeLane?.providerDispatch) ?? + normalizeOptionalObject(deliveryRuntimeState?.artifacts?.providerDispatch); + const activeLaneExecutionTopology = normalizeOptionalObject(activeLane?.executionTopology); + const activeLogicalLaneCountFallback = Number.isInteger(logicalLaneActivation?.activeLaneCount) + ? logicalLaneActivation.activeLaneCount + : null; + const seededLogicalLaneCountFallback = Number.isInteger(logicalLaneActivation?.seededLaneCount) + ? logicalLaneActivation.seededLaneCount + : null; + + if (activeLaneExecutionTopology) { + const topologyLogicalLaneActivation = normalizeOptionalObject(activeLaneExecutionTopology.logicalLaneActivation); + const topologyProviderDispatch = normalizeOptionalObject(activeLaneExecutionTopology.providerDispatch); + const topologyExecutionBundle = normalizeOptionalObject(activeLaneExecutionTopology.executionBundle); + const effectiveProviderDispatch = topologyProviderDispatch ?? providerDispatch; + const effectiveExecutionBundle = topologyExecutionBundle ?? executionBundle; + const processModel = deriveExecutionTopologyProcessModel(effectiveExecutionBundle); + const activeLogicalLaneCount = Number.isInteger(activeLaneExecutionTopology.activeLogicalLaneCount) + ? activeLaneExecutionTopology.activeLogicalLaneCount + : Number.isInteger(topologyLogicalLaneActivation?.activeLaneCount) + ? topologyLogicalLaneActivation.activeLaneCount + : activeLogicalLaneCountFallback; + const seededLogicalLaneCount = Number.isInteger(activeLaneExecutionTopology.seededLogicalLaneCount) + ? activeLaneExecutionTopology.seededLogicalLaneCount + : Number.isInteger(topologyLogicalLaneActivation?.seededLaneCount) + ? topologyLogicalLaneActivation.seededLaneCount + : seededLogicalLaneCountFallback; + const catalogCount = Number.isInteger(activeLaneExecutionTopology.catalogCount) + ? activeLaneExecutionTopology.catalogCount + : Number.isInteger(topologyLogicalLaneActivation?.catalogCount) + ? topologyLogicalLaneActivation.catalogCount + : logicalLaneCatalog.length; + const executionPlane = + asOptional(activeLaneExecutionTopology.executionPlane) || + asOptional(topologyProviderDispatch?.executionPlane) || + asOptional(providerDispatch?.executionPlane) || + asOptional(activeLaneExecutionTopology.planeBinding) || + asOptional(topologyExecutionBundle?.planeBinding) || + asOptional(executionBundle?.planeBinding); + + return { + status: + asOptional(activeLaneExecutionTopology.status) || + deriveExecutionTopologyStatus({ + activeLogicalLaneCount, + seededLogicalLaneCount, + providerDispatch: effectiveProviderDispatch, + executionBundle: effectiveExecutionBundle + }), + executionPlane, + providerId: + asOptional(activeLaneExecutionTopology.providerId) || + asOptional(topologyProviderDispatch?.providerId) || + asOptional(providerDispatch?.providerId), + workerSlotId: + asOptional(activeLaneExecutionTopology.workerSlotId) || + asOptional(topologyProviderDispatch?.workerSlotId) || + asOptional(providerDispatch?.workerSlotId), + activeLogicalLaneCount, + seededLogicalLaneCount, + catalogCount, + runtimeSurface: asOptional(activeLaneExecutionTopology.runtimeSurface) || processModel.runtimeSurface, + processModelClass: asOptional(activeLaneExecutionTopology.processModelClass) || processModel.processModelClass, + windowsOnly: coalesceBoolean(activeLaneExecutionTopology.windowsOnly, processModel.windowsOnly), + requestedSimultaneous: coalesceBoolean( + activeLaneExecutionTopology.requestedSimultaneous, + processModel.requestedSimultaneous + ), + cellClass: + asOptional(activeLaneExecutionTopology.cellClass) || + asOptional(topologyExecutionBundle?.cellClass) || + asOptional(executionBundle?.cellClass), + suiteClass: + asOptional(activeLaneExecutionTopology.suiteClass) || + asOptional(topologyExecutionBundle?.suiteClass) || + asOptional(executionBundle?.suiteClass), + operatorAuthorizationRef: + asOptional(activeLaneExecutionTopology.operatorAuthorizationRef) || + asOptional(topologyExecutionBundle?.operatorAuthorizationRef) || + asOptional(executionBundle?.operatorAuthorizationRef), + premiumSaganMode: coalesceBoolean( + activeLaneExecutionTopology.premiumSaganMode, + topologyExecutionBundle?.premiumSaganMode, + executionBundle?.premiumSaganMode + ), + reciprocalLinkReady: coalesceBoolean( + activeLaneExecutionTopology.reciprocalLinkReady, + topologyExecutionBundle?.reciprocalLinkReady, + executionBundle?.reciprocalLinkReady + ), + logicalLaneActivation: { + activeLaneCount: activeLogicalLaneCount, + seededLaneCount: seededLogicalLaneCount, + catalogCount + }, + providerDispatch: { + providerId: asOptional(topologyProviderDispatch?.providerId) || asOptional(providerDispatch?.providerId), + providerKind: asOptional(topologyProviderDispatch?.providerKind) || asOptional(providerDispatch?.providerKind), + executionPlane, + assignmentMode: + asOptional(topologyProviderDispatch?.assignmentMode) || asOptional(providerDispatch?.assignmentMode), + dispatchSurface: + asOptional(topologyProviderDispatch?.dispatchSurface) || asOptional(providerDispatch?.dispatchSurface), + completionMode: + asOptional(topologyProviderDispatch?.completionMode) || asOptional(providerDispatch?.completionMode), + workerSlotId: + asOptional(topologyProviderDispatch?.workerSlotId) || asOptional(providerDispatch?.workerSlotId), + dispatchStatus: + asOptional(topologyProviderDispatch?.dispatchStatus) || asOptional(providerDispatch?.dispatchStatus), + completionStatus: + asOptional(topologyProviderDispatch?.completionStatus) || asOptional(providerDispatch?.completionStatus), + failureClass: + asOptional(topologyProviderDispatch?.failureClass) || asOptional(providerDispatch?.failureClass) + }, + executionBundle: { + status: asOptional(topologyExecutionBundle?.status) || asOptional(executionBundle?.status), + planeBinding: + asOptional(activeLaneExecutionTopology.planeBinding) || + asOptional(topologyExecutionBundle?.planeBinding) || + asOptional(executionBundle?.planeBinding), + cellClass: + asOptional(topologyExecutionBundle?.cellClass) || asOptional(executionBundle?.cellClass), + suiteClass: + asOptional(topologyExecutionBundle?.suiteClass) || asOptional(executionBundle?.suiteClass), + premiumSaganMode: coalesceBoolean( + topologyExecutionBundle?.premiumSaganMode, + executionBundle?.premiumSaganMode + ), + reciprocalLinkReady: coalesceBoolean( + topologyExecutionBundle?.reciprocalLinkReady, + executionBundle?.reciprocalLinkReady + ), + effectiveBillableRateUsdPerHour: Number.isFinite(topologyExecutionBundle?.effectiveBillableRateUsdPerHour) + ? topologyExecutionBundle.effectiveBillableRateUsdPerHour + : Number.isFinite(executionBundle?.effectiveBillableRateUsdPerHour) + ? executionBundle.effectiveBillableRateUsdPerHour + : null, + executionCellLeaseId: + asOptional(activeLaneExecutionTopology.executionCellLeaseId) || + asOptional(topologyExecutionBundle?.executionCellLeaseId) || + asOptional(executionBundle?.executionCellLeaseId), + dockerLaneLeaseId: + asOptional(activeLaneExecutionTopology.dockerLaneLeaseId) || + asOptional(topologyExecutionBundle?.dockerLaneLeaseId) || + asOptional(executionBundle?.dockerLaneLeaseId), + harnessKind: + asOptional(activeLaneExecutionTopology.harnessKind) || + asOptional(topologyExecutionBundle?.harnessKind) || + asOptional(executionBundle?.harnessKind), + harnessInstanceId: + asOptional(activeLaneExecutionTopology.harnessInstanceId) || + asOptional(topologyExecutionBundle?.harnessInstanceId) || + asOptional(executionBundle?.harnessInstanceId), + operatorAuthorizationRef: + asOptional(topologyExecutionBundle?.operatorAuthorizationRef) || + asOptional(executionBundle?.operatorAuthorizationRef), + cellId: + asOptional(activeLaneExecutionTopology.cellId) || + asOptional(topologyExecutionBundle?.cellId) || + asOptional(executionBundle?.cellId), + laneId: + asOptional(activeLaneExecutionTopology.laneId) || + asOptional(topologyExecutionBundle?.laneId) || + asOptional(executionBundle?.laneId), + isolatedLaneGroupId: + asOptional(topologyExecutionBundle?.isolatedLaneGroupId) || + asOptional(executionBundle?.isolatedLaneGroupId), + fingerprintSha256: + asOptional(topologyExecutionBundle?.fingerprintSha256) || + asOptional(executionBundle?.fingerprintSha256) + } + }; + } + + const processModel = deriveExecutionTopologyProcessModel(executionBundle); + const activeLogicalLaneCount = activeLogicalLaneCountFallback; + const seededLogicalLaneCount = seededLogicalLaneCountFallback; + const executionPlane = asOptional(providerDispatch?.executionPlane) || asOptional(executionBundle?.planeBinding); + + return { + status: deriveExecutionTopologyStatus({ + activeLogicalLaneCount, + seededLogicalLaneCount, + providerDispatch, + executionBundle + }), + executionPlane, + providerId: asOptional(providerDispatch?.providerId), + workerSlotId: asOptional(providerDispatch?.workerSlotId), + activeLogicalLaneCount, + seededLogicalLaneCount, + catalogCount: logicalLaneCatalog.length, + runtimeSurface: processModel.runtimeSurface, + processModelClass: processModel.processModelClass, + windowsOnly: processModel.windowsOnly, + requestedSimultaneous: processModel.requestedSimultaneous, + cellClass: asOptional(executionBundle?.cellClass), + suiteClass: asOptional(executionBundle?.suiteClass), + operatorAuthorizationRef: asOptional(executionBundle?.operatorAuthorizationRef), + premiumSaganMode: parseBoolean(executionBundle?.premiumSaganMode), + reciprocalLinkReady: parseBoolean(executionBundle?.reciprocalLinkReady), + logicalLaneActivation: { + activeLaneCount: activeLogicalLaneCount, + seededLaneCount: seededLogicalLaneCount, + catalogCount: logicalLaneCatalog.length + }, + providerDispatch: { + providerId: asOptional(providerDispatch?.providerId), + providerKind: asOptional(providerDispatch?.providerKind), + executionPlane, + assignmentMode: asOptional(providerDispatch?.assignmentMode), + dispatchSurface: asOptional(providerDispatch?.dispatchSurface), + completionMode: asOptional(providerDispatch?.completionMode), + workerSlotId: asOptional(providerDispatch?.workerSlotId), + dispatchStatus: asOptional(providerDispatch?.dispatchStatus), + completionStatus: asOptional(providerDispatch?.completionStatus), + failureClass: asOptional(providerDispatch?.failureClass) + }, + executionBundle: { + status: asOptional(executionBundle?.status), + planeBinding: asOptional(executionBundle?.planeBinding), + cellClass: asOptional(executionBundle?.cellClass), + suiteClass: asOptional(executionBundle?.suiteClass), + premiumSaganMode: parseBoolean(executionBundle?.premiumSaganMode), + reciprocalLinkReady: parseBoolean(executionBundle?.reciprocalLinkReady), + effectiveBillableRateUsdPerHour: Number.isFinite(executionBundle?.effectiveBillableRateUsdPerHour) + ? executionBundle.effectiveBillableRateUsdPerHour + : null, + executionCellLeaseId: asOptional(executionBundle?.executionCellLeaseId), + dockerLaneLeaseId: asOptional(executionBundle?.dockerLaneLeaseId), + harnessKind: asOptional(executionBundle?.harnessKind), + harnessInstanceId: asOptional(executionBundle?.harnessInstanceId), + operatorAuthorizationRef: asOptional(executionBundle?.operatorAuthorizationRef), + cellId: asOptional(executionBundle?.cellId), + laneId: asOptional(executionBundle?.laneId), + isolatedLaneGroupId: asOptional(executionBundle?.isolatedLaneGroupId), + fingerprintSha256: asOptional(executionBundle?.fingerprintSha256) + } + }; +} + export function parseArgs(argv = process.argv) { const args = argv.slice(2); const options = { @@ -189,6 +500,7 @@ export function parseArgs(argv = process.argv) { wakeLifecyclePath: DEFAULT_WAKE_LIFECYCLE_PATH, wakeInvestmentAccountingPath: DEFAULT_WAKE_INVESTMENT_ACCOUNTING_PATH, deliveryRuntimeStatePath: DEFAULT_DELIVERY_RUNTIME_STATE_PATH, + releaseSigningReadinessPath: DEFAULT_RELEASE_SIGNING_READINESS_PATH, outputPath: DEFAULT_OUTPUT_PATH, help: false }; @@ -201,6 +513,7 @@ export function parseArgs(argv = process.argv) { ['--wake-lifecycle', 'wakeLifecyclePath'], ['--wake-investment-accounting', 'wakeInvestmentAccountingPath'], ['--delivery-runtime-state', 'deliveryRuntimeStatePath'], + ['--release-signing-readiness', 'releaseSigningReadinessPath'], ['--output', 'outputPath'] ]); @@ -237,6 +550,7 @@ function printHelp() { ` --wake-lifecycle Wake lifecycle path (default: ${DEFAULT_WAKE_LIFECYCLE_PATH}).`, ` --wake-investment-accounting Wake investment accounting path (default: ${DEFAULT_WAKE_INVESTMENT_ACCOUNTING_PATH}).`, ` --delivery-runtime-state Delivery runtime state path (default: ${DEFAULT_DELIVERY_RUNTIME_STATE_PATH}).`, + ` --release-signing-readiness Release signing readiness path (default: ${DEFAULT_RELEASE_SIGNING_READINESS_PATH}).`, ` --output Output path (default: ${DEFAULT_OUTPUT_PATH}).`, ' -h, --help Show help.' ].forEach((line) => console.log(line)); @@ -328,6 +642,42 @@ function deriveFunding(wakeInvestmentAccounting) { }; } +function deriveReleaseSigningReadiness(releaseSigningReadinessReport) { + if (releaseSigningReadinessReport?.schema !== 'priority/release-signing-readiness-report@v1') { + return { + status: 'missing', + codePathState: null, + signingCapabilityState: null, + signingAuthorityState: null, + releaseConductorApplyState: null, + publicationState: null, + publishedBundleState: null, + publishedBundleReleaseTag: null, + publishedBundleAuthoritativeConsumerPin: null, + externalBlocker: null, + blockerCount: 0 + }; + } + + return { + status: asOptional(releaseSigningReadinessReport?.summary?.status) || 'missing', + codePathState: asOptional(releaseSigningReadinessReport?.summary?.codePathState), + signingCapabilityState: asOptional(releaseSigningReadinessReport?.summary?.signingCapabilityState), + signingAuthorityState: asOptional(releaseSigningReadinessReport?.summary?.signingAuthorityState), + releaseConductorApplyState: asOptional(releaseSigningReadinessReport?.summary?.releaseConductorApplyState), + publicationState: asOptional(releaseSigningReadinessReport?.summary?.publicationState), + publishedBundleState: asOptional(releaseSigningReadinessReport?.summary?.publishedBundleState), + publishedBundleReleaseTag: asOptional(releaseSigningReadinessReport?.summary?.publishedBundleReleaseTag), + publishedBundleAuthoritativeConsumerPin: asOptional( + releaseSigningReadinessReport?.summary?.publishedBundleAuthoritativeConsumerPin + ), + externalBlocker: asOptional(releaseSigningReadinessReport?.summary?.externalBlocker), + blockerCount: Number.isInteger(releaseSigningReadinessReport?.summary?.blockerCount) + ? releaseSigningReadinessReport.summary.blockerCount + : 0 + }; +} + function deriveDeliveryRuntime(deliveryRuntimeState) { const activeLane = deliveryRuntimeState?.activeLane || {}; const prUrl = asOptional(activeLane?.prUrl); @@ -337,6 +687,9 @@ function deriveDeliveryRuntime(deliveryRuntimeState) { const queueAuthorityRefresh = normalizeOptionalObject(activeLane?.queueAuthorityRefresh) ?? normalizeOptionalObject(deliveryRuntimeState?.queueAuthorityRefresh); + const concurrentLaneStatus = normalizeOptionalObject(activeLane?.concurrentLaneStatus); + const executionBundle = normalizeOptionalObject(concurrentLaneStatus?.executionBundle); + const executionTopology = deriveExecutionTopology({ deliveryRuntimeState, activeLane, executionBundle }); let status = 'none'; if (prUrl) { @@ -359,6 +712,27 @@ function deriveDeliveryRuntime(deliveryRuntimeState) { outcome, blockerClass, nextWakeCondition: asOptional(activeLane?.nextWakeCondition), + executionTopology, + executionBundle: { + status: asOptional(executionBundle?.status), + planeBinding: asOptional(executionBundle?.planeBinding), + cellClass: asOptional(executionBundle?.cellClass), + suiteClass: asOptional(executionBundle?.suiteClass), + premiumSaganMode: parseBoolean(executionBundle?.premiumSaganMode), + reciprocalLinkReady: parseBoolean(executionBundle?.reciprocalLinkReady), + effectiveBillableRateUsdPerHour: Number.isFinite(executionBundle?.effectiveBillableRateUsdPerHour) + ? executionBundle.effectiveBillableRateUsdPerHour + : null, + executionCellLeaseId: asOptional(executionBundle?.executionCellLeaseId), + dockerLaneLeaseId: asOptional(executionBundle?.dockerLaneLeaseId), + harnessKind: asOptional(executionBundle?.harnessKind), + harnessInstanceId: asOptional(executionBundle?.harnessInstanceId), + operatorAuthorizationRef: asOptional(executionBundle?.operatorAuthorizationRef), + cellId: asOptional(executionBundle?.cellId), + laneId: asOptional(executionBundle?.laneId), + isolatedLaneGroupId: asOptional(executionBundle?.isolatedLaneGroupId), + fingerprintSha256: asOptional(executionBundle?.fingerprintSha256) + }, queueAuthorityRefresh: { attempted: queueAuthorityRefresh?.attempted === true, status: asOptional(queueAuthorityRefresh?.status), @@ -607,6 +981,8 @@ function buildReport({ wakeInvestmentAccounting, deliveryRuntimeStatePath, deliveryRuntimeState, + releaseSigningReadinessPath, + releaseSigningReadinessReport, readOptionalJsonFn, now }) { @@ -620,6 +996,7 @@ function buildReport({ const continuity = deriveContinuity(continuitySummary, monitoringMode); const wake = deriveWake(wakeLifecycle); const funding = deriveFunding(wakeInvestmentAccounting); + const releaseSigningReadiness = deriveReleaseSigningReadiness(releaseSigningReadinessReport); const deliveryRuntime = deriveDeliveryRuntime(deliveryRuntimeState); const queueAuthority = deriveQueueAuthority({ repoRoot, @@ -642,7 +1019,8 @@ function buildReport({ monitoringModePath: toRelative(repoRoot, monitoringModePath), wakeLifecyclePath: toRelative(repoRoot, wakeLifecyclePath), wakeInvestmentAccountingPath: toRelative(repoRoot, wakeInvestmentAccountingPath), - deliveryRuntimeStatePath: toRelative(repoRoot, deliveryRuntimeStatePath) + deliveryRuntimeStatePath: toRelative(repoRoot, deliveryRuntimeStatePath), + releaseSigningReadinessPath: toRelative(repoRoot, releaseSigningReadinessPath) }, compare: { queueState, @@ -654,6 +1032,7 @@ function buildReport({ ? monitoringMode.summary.wakeConditionCount : null }, + releaseSigningReadiness, deliveryRuntime, queueAuthority }, @@ -670,6 +1049,32 @@ function buildReport({ wakeTerminalState: wake.terminalState, monitoringStatus: asOptional(monitoringMode?.summary?.status), futureAgentAction: asOptional(monitoringMode?.summary?.futureAgentAction), + releaseSigningStatus: releaseSigningReadiness.status, + releaseSigningAuthorityState: releaseSigningReadiness.signingAuthorityState, + releaseConductorApplyState: releaseSigningReadiness.releaseConductorApplyState, + releaseSigningExternalBlocker: releaseSigningReadiness.externalBlocker, + releasePublicationState: releaseSigningReadiness.publicationState, + releasePublishedBundleState: releaseSigningReadiness.publishedBundleState, + releasePublishedBundleReleaseTag: releaseSigningReadiness.publishedBundleReleaseTag, + releasePublishedBundleAuthoritativeConsumerPin: releaseSigningReadiness.publishedBundleAuthoritativeConsumerPin, + executionTopologyStatus: deliveryRuntime.executionTopology.status, + executionTopologyExecutionPlane: deliveryRuntime.executionTopology.executionPlane, + executionTopologyProviderId: deliveryRuntime.executionTopology.providerId, + executionTopologyWorkerSlotId: deliveryRuntime.executionTopology.workerSlotId, + executionTopologyActiveLogicalLaneCount: deliveryRuntime.executionTopology.activeLogicalLaneCount, + executionTopologySeededLogicalLaneCount: deliveryRuntime.executionTopology.seededLogicalLaneCount, + executionTopologyRuntimeSurface: deliveryRuntime.executionTopology.runtimeSurface, + executionTopologyProcessModelClass: deliveryRuntime.executionTopology.processModelClass, + executionTopologyWindowsOnly: deliveryRuntime.executionTopology.windowsOnly, + executionTopologyRequestedSimultaneous: deliveryRuntime.executionTopology.requestedSimultaneous, + executionTopologyCellClass: deliveryRuntime.executionTopology.cellClass, + executionTopologySuiteClass: deliveryRuntime.executionTopology.suiteClass, + executionTopologyOperatorAuthorizationRef: deliveryRuntime.executionTopology.operatorAuthorizationRef, + executionBundleStatus: deliveryRuntime.executionBundle.status, + executionBundlePlaneBinding: deliveryRuntime.executionBundle.planeBinding, + executionBundlePremiumSaganMode: deliveryRuntime.executionBundle.premiumSaganMode, + executionBundleReciprocalLinkReady: deliveryRuntime.executionBundle.reciprocalLinkReady, + executionBundleEffectiveBillableRateUsdPerHour: deliveryRuntime.executionBundle.effectiveBillableRateUsdPerHour, queueHandoffStatus: queueAuthority.status, queueHandoffNextWakeCondition: queueAuthority.nextWakeCondition, queueHandoffPrUrl: queueAuthority.prUrl, @@ -692,6 +1097,10 @@ export async function runAutonomousGovernorSummary(options = {}, deps = {}) { repoRoot, options.deliveryRuntimeStatePath || DEFAULT_DELIVERY_RUNTIME_STATE_PATH ); + const releaseSigningReadinessPath = path.resolve( + repoRoot, + options.releaseSigningReadinessPath || DEFAULT_RELEASE_SIGNING_READINESS_PATH + ); const outputPath = path.resolve(repoRoot, options.outputPath || DEFAULT_OUTPUT_PATH); const readOptionalJsonFn = deps.readOptionalJsonFn || readOptionalJson; @@ -704,6 +1113,7 @@ export async function runAutonomousGovernorSummary(options = {}, deps = {}) { const wakeLifecycle = readOptionalJsonFn(wakeLifecyclePath); const wakeInvestmentAccounting = readOptionalJsonFn(wakeInvestmentAccountingPath); const deliveryRuntimeState = readOptionalJsonFn(deliveryRuntimeStatePath); + const releaseSigningReadinessReport = readOptionalJsonFn(releaseSigningReadinessPath); if (queueEmptyReport) { ensureSchema(queueEmptyReport, queueEmptyReportPath, 'standing-priority/no-standing@v1'); @@ -723,6 +1133,13 @@ export async function runAutonomousGovernorSummary(options = {}, deps = {}) { if (deliveryRuntimeState) { ensureSchema(deliveryRuntimeState, deliveryRuntimeStatePath, 'priority/delivery-agent-runtime-state@v1'); } + if (releaseSigningReadinessReport) { + ensureSchema( + releaseSigningReadinessReport, + releaseSigningReadinessPath, + 'priority/release-signing-readiness-report@v1' + ); + } const report = buildReport({ repoRoot, @@ -738,6 +1155,8 @@ export async function runAutonomousGovernorSummary(options = {}, deps = {}) { wakeInvestmentAccounting, deliveryRuntimeStatePath, deliveryRuntimeState, + releaseSigningReadinessPath, + releaseSigningReadinessReport, readOptionalJsonFn, now }); diff --git a/tools/priority/concurrent-lane-status.mjs b/tools/priority/concurrent-lane-status.mjs index 651d412f1..99540e136 100644 --- a/tools/priority/concurrent-lane-status.mjs +++ b/tools/priority/concurrent-lane-status.mjs @@ -8,6 +8,10 @@ import { CONCURRENT_LANE_APPLY_RECEIPT_SCHEMA, DEFAULT_OUTPUT_PATH as DEFAULT_APPLY_RECEIPT_PATH } from './concurrent-lane-apply.mjs'; +import { + DEFAULT_OUTPUT_PATH as DEFAULT_EXECUTION_BUNDLE_RECEIPT_PATH, + EXECUTION_CELL_BUNDLE_REPORT_SCHEMA +} from './execution-cell-bundle.mjs'; import { ensureGhCli, resolveUpstream, runGhGraphql, runGhJson } from './lib/remote-utils.mjs'; import { getRepoRoot } from './lib/branch-utils.mjs'; @@ -54,6 +58,17 @@ async function readJsonRequired(filePath) { return JSON.parse(await fs.readFile(filePath, 'utf8')); } +async function readJsonIfPresent(filePath) { + try { + return JSON.parse(await fs.readFile(filePath, 'utf8')); + } catch (error) { + if (error?.code === 'ENOENT') { + return null; + } + throw error; + } +} + async function writeReceipt(outputPath, receipt) { const resolved = path.resolve(process.cwd(), outputPath); await fs.mkdir(path.dirname(resolved), { recursive: true }); @@ -187,6 +202,36 @@ function determineReceiptStatus({ applyReceipt, hostedObservationStatus, pullReq return 'settled'; } +function projectExecutionBundleReceipt(receiptPath, receipt) { + if (!receipt || receipt.schema !== EXECUTION_CELL_BUNDLE_REPORT_SCHEMA) { + return null; + } + + const summary = receipt.summary && typeof receipt.summary === 'object' ? receipt.summary : {}; + return { + path: toOptionalText(receiptPath), + schema: toOptionalText(receipt.schema), + status: toOptionalText(receipt.status), + cellId: toOptionalText(receipt.cellId), + laneId: toOptionalText(receipt.laneId), + cellClass: toOptionalText(summary.cellClass), + suiteClass: toOptionalText(summary.suiteClass), + executionCellLeaseId: toOptionalText(summary.executionCellLeaseId), + dockerLaneLeaseId: toOptionalText(summary.dockerLaneLeaseId), + harnessKind: toOptionalText(summary.harnessKind), + harnessInstanceId: toOptionalText(summary.harnessInstanceId), + planeBinding: toOptionalText(summary.planeBinding), + premiumSaganMode: summary.premiumSaganMode === true, + reciprocalLinkReady: summary.reciprocalLinkReady === true, + effectiveBillableRateUsdPerHour: Number.isFinite(summary.effectiveBillableRateUsdPerHour) + ? summary.effectiveBillableRateUsdPerHour + : null, + operatorAuthorizationRef: toOptionalText(summary.operatorAuthorizationRef), + isolatedLaneGroupId: toOptionalText(summary.isolatedLaneGroupId), + fingerprintSha256: toOptionalText(summary.fingerprintSha256) + }; +} + const IDLE_CLASSIFICATION_STATES = Object.freeze([ 'waiting-hosted', 'waiting-merge', @@ -367,6 +412,7 @@ export function parseArgs(argv = process.argv) { const args = argv.slice(2); const options = { applyReceiptPath: DEFAULT_APPLY_RECEIPT_PATH, + executionBundleReceiptPath: DEFAULT_EXECUTION_BUNDLE_RECEIPT_PATH, outputPath: DEFAULT_STATUS_OUTPUT_PATH, repo: null, pr: null, @@ -381,12 +427,20 @@ export function parseArgs(argv = process.argv) { options.help = true; continue; } - if (token === '--apply-receipt' || token === '--output' || token === '--repo' || token === '--pr' || token === '--ref') { + if ( + token === '--apply-receipt' || + token === '--execution-bundle-receipt' || + token === '--output' || + token === '--repo' || + token === '--pr' || + token === '--ref' + ) { if (!next || next.startsWith('-')) { throw new Error(`Missing value for ${token}.`); } index += 1; if (token === '--apply-receipt') options.applyReceiptPath = next; + if (token === '--execution-bundle-receipt') options.executionBundleReceiptPath = next; if (token === '--output') options.outputPath = next; if (token === '--repo') options.repo = next; if (token === '--pr') options.pr = next; @@ -690,6 +744,7 @@ function observePullRequest({ export function buildConcurrentLaneStatusReceipt({ applyReceiptPath, applyReceipt, + executionBundle, hostedRun, pullRequest, laneStatuses, @@ -727,6 +782,7 @@ export function buildConcurrentLaneStatusReceipt({ recommendedBundleId: toOptionalText(applyReceipt?.plan?.recommendedBundleId), selectedBundleId: toOptionalText(applyReceipt?.plan?.selectedBundle?.id) }, + executionBundle: executionBundle ?? null, hostedRun, pullRequest, laneStatuses: enrichedLaneStatuses, @@ -742,6 +798,9 @@ export function buildConcurrentLaneStatusReceipt({ deferredLaneCount, manualLaneCount: laneStatuses.filter((entry) => entry.executionPlane === 'local').length, shadowLaneCount: laneStatuses.filter((entry) => entry.executionPlane === 'local-shadow').length, + executionBundleStatus: toOptionalText(executionBundle?.status), + executionBundleReciprocalLinkReady: executionBundle?.reciprocalLinkReady === true, + executionBundlePremiumSaganMode: executionBundle?.premiumSaganMode === true, pullRequestStatus: pullRequest?.observationStatus ?? 'not-requested', idleClassificationCoverage, orchestratorDisposition: determineOrchestratorDisposition({ @@ -766,6 +825,7 @@ export async function observeConcurrentLaneStatus( ) { const repoRoot = getRepoRootFn(); const applyReceiptPath = path.resolve(repoRoot, options.applyReceiptPath); + const executionBundleReceiptPath = path.resolve(repoRoot, options.executionBundleReceiptPath); const applyReceipt = await readJsonRequired(applyReceiptPath); if (applyReceipt?.schema !== CONCURRENT_LANE_APPLY_RECEIPT_SCHEMA) { throw new Error( @@ -773,6 +833,11 @@ export async function observeConcurrentLaneStatus( ); } + const executionBundle = projectExecutionBundleReceipt( + executionBundleReceiptPath, + await readJsonIfPresent(executionBundleReceiptPath) + ); + const repository = toOptionalText(options.repo) ?? toOptionalText(applyReceipt?.repository) ?? @@ -835,6 +900,7 @@ export async function observeConcurrentLaneStatus( const receipt = buildConcurrentLaneStatusReceipt({ applyReceiptPath, applyReceipt, + executionBundle, hostedRun, pullRequest, laneStatuses, @@ -850,6 +916,9 @@ function printUsage() { console.log(''); console.log('Options:'); console.log(` --apply-receipt Concurrent lane apply receipt path (default: ${DEFAULT_APPLY_RECEIPT_PATH})`); + console.log( + ` --execution-bundle-receipt Execution bundle receipt path (default: ${DEFAULT_EXECUTION_BUNDLE_RECEIPT_PATH})` + ); console.log(` --output Status receipt path (default: ${DEFAULT_STATUS_OUTPUT_PATH})`); console.log(' --repo Repository override for hosted run and PR observation.'); console.log(' --pr Pull request number override for merge-queue observation.'); diff --git a/tools/priority/delivery-agent.mjs b/tools/priority/delivery-agent.mjs index 20bcedbe8..d07ab0db4 100644 --- a/tools/priority/delivery-agent.mjs +++ b/tools/priority/delivery-agent.mjs @@ -956,6 +956,120 @@ function buildWorkerProviderDispatchReceipt(providerSelection, { }; } +function coerceNonNegativeInteger(value) { + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed < 0) { + return null; + } + return parsed; +} + +function deriveExecutionTopologyProcessModel(executionBundle = null) { + const planeBinding = normalizeText(executionBundle?.planeBinding).toLowerCase(); + const harnessKind = normalizeText(executionBundle?.harnessKind); + const requestedSimultaneous = planeBinding === 'dual-plane-parity'; + const windowsNativeTestStand = + (!harnessKind || harnessKind === 'teststand-compare-harness') && + (requestedSimultaneous || planeBinding.startsWith('native-labview-')); + const runtimeSurface = windowsNativeTestStand ? 'windows-native-teststand' : null; + const processModelClass = !runtimeSurface + ? null + : requestedSimultaneous + ? 'parallel-process-model' + : 'sequential-process-model'; + + return { + runtimeSurface, + processModelClass, + windowsOnly: runtimeSurface === 'windows-native-teststand', + requestedSimultaneous + }; +} + +function deriveExecutionTopologyStatus({ + activeLogicalLaneCount = null, + seededLogicalLaneCount = null, + providerDispatch = null, + executionBundle = null +} = {}) { + const bundleStatus = normalizeText(executionBundle?.status); + if (bundleStatus) { + return `bundle-${bundleStatus}`; + } + + const completionStatus = normalizeText(providerDispatch?.completionStatus); + if (completionStatus) { + return `provider-${completionStatus}`; + } + + const dispatchStatus = normalizeText(providerDispatch?.dispatchStatus); + if (dispatchStatus) { + return `provider-${dispatchStatus}`; + } + + if ((activeLogicalLaneCount ?? 0) > 0 || (seededLogicalLaneCount ?? 0) > 0) { + return 'logical-lanes-tracked'; + } + + return 'none'; +} + +export function buildExecutionTopologyRuntimeState({ + logicalLaneActivation = null, + providerDispatch = null, + providerSelection = null, + workerSlotId = null, + concurrentLaneStatus = null +} = {}) { + const logicalLaneState = normalizeOptionalObject(logicalLaneActivation); + const executionBundle = normalizeOptionalObject(concurrentLaneStatus?.executionBundle); + const effectiveProviderDispatch = + normalizeOptionalObject(providerDispatch) ?? + buildWorkerProviderDispatchReceipt(providerSelection, { workerSlotId }); + + const activeLogicalLaneCount = coerceNonNegativeInteger(logicalLaneState?.activeLaneCount); + const seededLogicalLaneCount = coerceNonNegativeInteger(logicalLaneState?.seededLaneCount); + + if (!executionBundle && !effectiveProviderDispatch && activeLogicalLaneCount == null && seededLogicalLaneCount == null) { + return null; + } + + const processModel = deriveExecutionTopologyProcessModel(executionBundle); + + return { + status: deriveExecutionTopologyStatus({ + activeLogicalLaneCount, + seededLogicalLaneCount, + providerDispatch: effectiveProviderDispatch, + executionBundle + }), + executionPlane: + normalizeText(effectiveProviderDispatch?.executionPlane) || + normalizeText(executionBundle?.planeBinding) || + null, + providerId: normalizeText(effectiveProviderDispatch?.providerId) || null, + workerSlotId: normalizeText(effectiveProviderDispatch?.workerSlotId) || null, + cellId: normalizeText(executionBundle?.cellId) || null, + laneId: normalizeText(executionBundle?.laneId) || null, + cellClass: normalizeText(executionBundle?.cellClass) || null, + suiteClass: normalizeText(executionBundle?.suiteClass) || null, + planeBinding: normalizeText(executionBundle?.planeBinding) || null, + harnessKind: normalizeText(executionBundle?.harnessKind) || null, + harnessInstanceId: normalizeText(executionBundle?.harnessInstanceId) || null, + executionCellLeaseId: normalizeText(executionBundle?.executionCellLeaseId) || null, + dockerLaneLeaseId: normalizeText(executionBundle?.dockerLaneLeaseId) || null, + premiumSaganMode: executionBundle?.premiumSaganMode === true, + reciprocalLinkReady: executionBundle?.reciprocalLinkReady === true, + operatorAuthorizationRef: normalizeText(executionBundle?.operatorAuthorizationRef) || null, + activeLogicalLaneCount, + seededLogicalLaneCount, + runtimeSurface: processModel.runtimeSurface, + processModelClass: processModel.processModelClass, + windowsOnly: processModel.windowsOnly, + requestedSimultaneous: processModel.requestedSimultaneous + }; +} + function commandUsesLocalCollabOrchestrator(command = []) { return Array.isArray(command) ? command.some((entry) => normalizeText(entry).replace(/\\/g, '/').includes('tools/local-collab/orchestrator/run-phase.mjs')) @@ -2643,6 +2757,7 @@ function buildConcurrentLaneStatusRuntimeState({ taskPacket, executionReceipt }) const hostedRun = normalizeOptionalObject(concurrentLaneStatus.hostedRun); const pullRequest = normalizeOptionalObject(concurrentLaneStatus.pullRequest); const mergeQueue = normalizeOptionalObject(pullRequest?.mergeQueue); + const executionBundle = normalizeOptionalObject(concurrentLaneStatus.executionBundle); return { receiptPath: normalizeText(concurrentLaneStatus.receiptPath) || null, @@ -2671,6 +2786,28 @@ function buildConcurrentLaneStatusRuntimeState({ taskPacket, executionReceipt }) : null } : null, + executionBundle: executionBundle + ? { + status: normalizeText(executionBundle.status) || null, + cellId: normalizeText(executionBundle.cellId) || null, + laneId: normalizeText(executionBundle.laneId) || null, + cellClass: normalizeText(executionBundle.cellClass) || null, + suiteClass: normalizeText(executionBundle.suiteClass) || null, + executionCellLeaseId: normalizeText(executionBundle.executionCellLeaseId) || null, + dockerLaneLeaseId: normalizeText(executionBundle.dockerLaneLeaseId) || null, + harnessKind: normalizeText(executionBundle.harnessKind) || null, + harnessInstanceId: normalizeText(executionBundle.harnessInstanceId) || null, + planeBinding: normalizeText(executionBundle.planeBinding) || null, + premiumSaganMode: executionBundle.premiumSaganMode === true, + reciprocalLinkReady: executionBundle.reciprocalLinkReady === true, + effectiveBillableRateUsdPerHour: Number.isFinite(executionBundle.effectiveBillableRateUsdPerHour) + ? executionBundle.effectiveBillableRateUsdPerHour + : null, + operatorAuthorizationRef: normalizeText(executionBundle.operatorAuthorizationRef) || null, + isolatedLaneGroupId: normalizeText(executionBundle.isolatedLaneGroupId) || null, + fingerprintSha256: normalizeText(executionBundle.fingerprintSha256) || null + } + : null, summary: summary ? { laneCount: coercePositiveInteger(summary.laneCount) ?? 0, @@ -3369,6 +3506,13 @@ export function buildDeliveryAgentRuntimeRecord({ schedulerDecision, taskPacket }); + const executionTopology = buildExecutionTopologyRuntimeState({ + logicalLaneActivation, + providerDispatch, + providerSelection: workerProviderSelection, + workerSlotId: normalizeText(providerDispatch?.workerSlotId) || normalizeText(workerProviderSelection?.selectedSlotId) || null, + concurrentLaneStatus + }); const activeLane = { schema: DELIVERY_AGENT_LANE_STATE_SCHEMA, generatedAt: toIso(now), @@ -3400,6 +3544,7 @@ export function buildDeliveryAgentRuntimeRecord({ readyValidationClearance, concurrentLaneApply, concurrentLaneStatus, + executionTopology, liveAgentModelSelection, workerProviderSelection, providerDispatch diff --git a/tools/priority/docker-lane-handshake.mjs b/tools/priority/docker-lane-handshake.mjs new file mode 100644 index 000000000..e1c95804f --- /dev/null +++ b/tools/priority/docker-lane-handshake.mjs @@ -0,0 +1,802 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +import { defaultOwner } from './agent-writer-lease.mjs'; +import { resolveGitAdminPaths } from './lib/git-admin-paths.mjs'; + +const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(MODULE_DIR, '..', '..'); + +export const DOCKER_LANE_HANDSHAKE_SCHEMA = 'priority/docker-lane-handshake@v1'; +export const DOCKER_LANE_HANDSHAKE_REPORT_SCHEMA = 'priority/docker-lane-handshake-report@v1'; +export const DEFAULT_OUTPUT_PATH = path.join('tests', 'results', '_agent', 'runtime', 'docker-lane-handshake.json'); +export const DEFAULT_HOST_PLANE_REPORT_PATH = path.join( + 'tests', + 'results', + '_agent', + 'host-planes', + 'labview-2026-host-plane-report.json' +); +export const DEFAULT_OPERATOR_COST_PROFILE_PATH = path.join('tools', 'policy', 'operator-cost-profile.json'); +export const DEFAULT_TTL_SECONDS = 1800; +export const PREMIUM_RATE_MULTIPLIER = 1.5; +export const ORDINARY_RATE_MULTIPLIER = 1.0; +export const DOCKER_LANE_CAPABILITY = 'docker-lane'; +export const NATIVE_LV32_CAPABILITY = 'native-labview-2026-32'; + +export const STATUS = Object.freeze({ + requested: 'requested', + granted: 'granted', + committed: 'committed', + released: 'released', + renewed: 'renewed', + active: 'active', + busy: 'busy', + denied: 'denied', + notFound: 'not-found', + mismatch: 'mismatch', + invalidState: 'invalid-state', + stale: 'stale' +}); + +function normalizeText(value) { + if (value == null) { + return ''; + } + return String(value).trim(); +} + +function toOptionalText(value) { + const normalized = normalizeText(value); + return normalized || null; +} + +function normalizeAgentClass(value) { + const normalized = normalizeText(value).toLowerCase(); + if (['sagan', 'subagent', 'other'].includes(normalized)) { + return normalized; + } + return 'subagent'; +} + +function nowIso(now = new Date()) { + return now.toISOString(); +} + +function uniqueId(prefix = 'id', now = Date.now()) { + return `${prefix}-${now}-${Math.random().toString(16).slice(2, 10)}`; +} + +function sanitizeLaneId(laneId) { + return normalizeText(laneId).replace(/[^a-zA-Z0-9._-]+/g, '__'); +} + +function resolveDefaultHandshakeRoot(options = {}) { + try { + return path.join( + resolveGitAdminPaths({ + cwd: options.repoRoot || REPO_ROOT, + env: options.env || process.env, + spawnSyncFn: options.spawnSyncFn || spawnSync + }).gitCommonDir, + 'docker-lane-handshakes' + ); + } catch { + return path.join(options.repoRoot || REPO_ROOT, '.git', 'docker-lane-handshakes'); + } +} + +export function defaultHandshakeRoot(options = {}) { + return options.handshakeRoot || process.env.DOCKER_LANE_HANDSHAKE_ROOT || resolveDefaultHandshakeRoot(options); +} + +export function handshakePathForLane(laneId, handshakeRoot = defaultHandshakeRoot()) { + return path.join(handshakeRoot, `${sanitizeLaneId(laneId)}.json`); +} + +async function ensureParentDir(filePath) { + await fs.mkdir(path.dirname(filePath), { recursive: true }); +} + +async function readJsonIfPresent(filePath) { + try { + return JSON.parse(await fs.readFile(filePath, 'utf8')); + } catch (error) { + if (error?.code === 'ENOENT') { + return null; + } + throw error; + } +} + +async function writeJsonAtomic(filePath, payload) { + await ensureParentDir(filePath); + const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`; + const body = `${JSON.stringify(payload, null, 2)}\n`; + await fs.writeFile(tempPath, body, 'utf8'); + try { + await fs.rename(tempPath, filePath); + } finally { + await fs.rm(tempPath, { force: true }); + } +} + +function normalizeCapabilities(capabilities = []) { + const values = []; + for (const entry of capabilities) { + const normalized = normalizeText(entry); + if (normalized) { + values.push(normalized); + } + } + return [...new Set(values)]; +} + +function isPremiumDualLaneRequest(capabilities = []) { + const set = new Set(normalizeCapabilities(capabilities)); + return set.has(DOCKER_LANE_CAPABILITY) && set.has(NATIVE_LV32_CAPABILITY); +} + +function resolveHeartbeatTimestamp(handshake) { + return ( + handshake?.heartbeatAt || + handshake?.commit?.committedAt || + handshake?.grant?.grantedAt || + handshake?.request?.requestedAt || + null + ); +} + +function handshakeAgeSeconds(handshake, nowMs = Date.now()) { + const timestamp = resolveHeartbeatTimestamp(handshake); + if (!timestamp) { + return Number.POSITIVE_INFINITY; + } + const parsed = Date.parse(timestamp); + if (!Number.isFinite(parsed)) { + return Number.POSITIVE_INFINITY; + } + return Math.max(0, (nowMs - parsed) / 1000); +} + +function resolveTtlSeconds(handshake) { + return Number.isInteger(handshake?.grant?.ttlSeconds) ? handshake.grant.ttlSeconds : DEFAULT_TTL_SECONDS; +} + +export function isHandshakeStale(handshake, nowMs = Date.now()) { + if (!handshake || handshake.state === 'released') { + return false; + } + return handshakeAgeSeconds(handshake, nowMs) > resolveTtlSeconds(handshake); +} + +function buildHostContext(hostPlaneReport) { + const fingerprint = hostPlaneReport?.host?.osFingerprint; + if (!fingerprint || typeof fingerprint !== 'object') { + return { + context: null, + observations: ['host-os-fingerprint-missing'] + }; + } + + return { + context: { + isolatedLaneGroupId: toOptionalText(fingerprint.isolatedLaneGroupId), + fingerprintSha256: toOptionalText(fingerprint.fingerprintSha256), + platform: toOptionalText(fingerprint.platform), + computerName: toOptionalText(hostPlaneReport?.host?.computerName), + canonical: { + version: toOptionalText(fingerprint?.canonical?.version), + buildNumber: toOptionalText(fingerprint?.canonical?.buildNumber), + ubr: Number.isInteger(fingerprint?.canonical?.ubr) ? fingerprint.canonical.ubr : null + } + }, + observations: [] + }; +} + +function resolveOperatorProfile(profile, explicitOperatorId) { + const operators = Array.isArray(profile?.operators) ? profile.operators : []; + const desiredId = toOptionalText(explicitOperatorId) || toOptionalText(profile?.defaultOperatorId); + const activeOperators = operators.filter((entry) => entry?.active !== false); + const resolved = + activeOperators.find((entry) => normalizeText(entry?.id) === normalizeText(desiredId)) || + activeOperators[0] || + null; + + return { + operatorId: toOptionalText(resolved?.id) || desiredId, + currency: toOptionalText(profile?.currency) || 'USD', + laborRateUsdPerHour: Number.isFinite(resolved?.laborRateUsdPerHour) + ? Number(resolved.laborRateUsdPerHour) + : null + }; +} + +function buildGrantDecision(handshake, operatorProfile) { + const requestedCapabilities = normalizeCapabilities(handshake?.request?.capabilities); + const premiumDualLaneRequested = handshake?.request?.premiumDualLaneRequested === true; + const reasons = []; + + if (!requestedCapabilities.includes(DOCKER_LANE_CAPABILITY)) { + reasons.push('docker-lane-capability-required'); + } + if (premiumDualLaneRequested && handshake?.request?.agentClass !== 'sagan') { + reasons.push('premium-sagan-only'); + } + if (premiumDualLaneRequested && !toOptionalText(handshake?.request?.operatorAuthorizationRef)) { + reasons.push('operator-authorization-required'); + } + if (!Number.isFinite(operatorProfile?.laborRateUsdPerHour)) { + reasons.push('operator-cost-rate-unavailable'); + } + + const billableRateMultiplier = premiumDualLaneRequested ? PREMIUM_RATE_MULTIPLIER : ORDINARY_RATE_MULTIPLIER; + const billableRateUsdPerHour = Number.isFinite(operatorProfile?.laborRateUsdPerHour) + ? Number((operatorProfile.laborRateUsdPerHour * billableRateMultiplier).toFixed(3)) + : null; + + return { + allowed: reasons.length === 0, + reasons, + premiumDualLaneRequested, + premiumSaganMode: premiumDualLaneRequested, + policyDecision: premiumDualLaneRequested ? 'sagan-premium-dual-lane' : 'ordinary-docker-lane', + grantedCapabilities: requestedCapabilities, + billableRateMultiplier, + billableRateUsdPerHour + }; +} + +function buildBaseReport(action, laneId, handshakePath, generatedAt, handshake, extras = {}) { + const stale = isHandshakeStale(handshake, Date.parse(generatedAt)); + return { + schema: DOCKER_LANE_HANDSHAKE_REPORT_SCHEMA, + generatedAt, + action, + laneId, + handshakePath, + handshake, + summary: { + handshakeState: handshake?.state ?? null, + leaseId: toOptionalText(handshake?.grant?.leaseId), + holder: toOptionalText(handshake?.request?.agentId), + premiumSaganMode: + handshake?.grant?.premiumSaganMode === true || handshake?.request?.premiumDualLaneRequested === true, + billableRateMultiplier: Number.isFinite(handshake?.grant?.billableRateMultiplier) + ? handshake.grant.billableRateMultiplier + : null, + billableRateUsdPerHour: Number.isFinite(handshake?.grant?.billableRateUsdPerHour) + ? handshake.grant.billableRateUsdPerHour + : null, + operatorAuthorizationRef: toOptionalText(handshake?.request?.operatorAuthorizationRef), + isolatedLaneGroupId: toOptionalText(handshake?.host?.isolatedLaneGroupId), + fingerprintSha256: toOptionalText(handshake?.host?.fingerprintSha256), + linkedExecutionCellId: toOptionalText(handshake?.commit?.executionCellId), + linkedExecutionCellLeaseId: toOptionalText(handshake?.commit?.executionCellLeaseId), + isStale: stale, + ageSeconds: Number.isFinite(handshakeAgeSeconds(handshake, Date.parse(generatedAt))) + ? Number(handshakeAgeSeconds(handshake, Date.parse(generatedAt)).toFixed(3)) + : null, + ttlSeconds: handshake ? resolveTtlSeconds(handshake) : null, + denialReasons: [], + observations: [] + }, + ...extras + }; +} + +function isWindowsNativeExecutionCellLink(linkedExecutionCell) { + const harnessKind = normalizeText(linkedExecutionCell?.harnessKind); + const planeBinding = normalizeText(linkedExecutionCell?.planeBinding).toLowerCase(); + if (!harnessKind || harnessKind !== 'teststand-compare-harness') { + return false; + } + if (!planeBinding) { + return true; + } + return planeBinding === 'dual-plane-parity' || planeBinding.startsWith('native-labview-'); +} + +function resolveExecutionCellLinkContext(report) { + if (!report || typeof report !== 'object') { + return null; + } + const lease = report.lease; + if (!lease || typeof lease !== 'object') { + return null; + } + return { + cellId: toOptionalText(report?.cellId) || toOptionalText(lease?.cellId), + leaseId: toOptionalText(report?.summary?.leaseId) || toOptionalText(lease?.grant?.leaseId), + holder: toOptionalText(report?.summary?.holder) || toOptionalText(lease?.request?.agentId), + isolatedLaneGroupId: + toOptionalText(report?.summary?.isolatedLaneGroupId) || toOptionalText(lease?.host?.isolatedLaneGroupId), + fingerprintSha256: + toOptionalText(report?.summary?.fingerprintSha256) || toOptionalText(lease?.host?.fingerprintSha256), + planeBinding: toOptionalText(report?.summary?.planeBinding) || toOptionalText(lease?.request?.planeBinding), + harnessKind: toOptionalText(report?.summary?.harnessKind) || toOptionalText(lease?.request?.harnessKind) + }; +} + +function validateExecutionCellLink(handshake, linkedExecutionCell) { + if (!linkedExecutionCell) { + return ['execution-cell-report-invalid']; + } + + const reasons = []; + if (!linkedExecutionCell.cellId) { + reasons.push('execution-cell-id-missing'); + } + if (!linkedExecutionCell.leaseId) { + reasons.push('execution-cell-lease-id-missing'); + } + if (!linkedExecutionCell.holder) { + reasons.push('execution-cell-holder-missing'); + } + if (!linkedExecutionCell.isolatedLaneGroupId || !linkedExecutionCell.fingerprintSha256) { + reasons.push('execution-cell-host-fingerprint-missing'); + } + if (!isWindowsNativeExecutionCellLink(linkedExecutionCell)) { + reasons.push('execution-cell-not-windows-native-teststand'); + } + + const handshakeHolder = toOptionalText(handshake?.request?.agentId); + if (handshakeHolder && linkedExecutionCell.holder && linkedExecutionCell.holder !== handshakeHolder) { + reasons.push('execution-cell-owner-mismatch'); + } + + const handshakeIsolatedLaneGroupId = toOptionalText(handshake?.host?.isolatedLaneGroupId); + const handshakeFingerprintSha256 = toOptionalText(handshake?.host?.fingerprintSha256); + if ( + handshakeIsolatedLaneGroupId && + linkedExecutionCell.isolatedLaneGroupId && + linkedExecutionCell.isolatedLaneGroupId !== handshakeIsolatedLaneGroupId + ) { + reasons.push('execution-cell-isolated-lane-group-mismatch'); + } + if ( + handshakeFingerprintSha256 && + linkedExecutionCell.fingerprintSha256 && + linkedExecutionCell.fingerprintSha256 !== handshakeFingerprintSha256 + ) { + reasons.push('execution-cell-host-fingerprint-mismatch'); + } + + return reasons; +} + +function buildRequestRecord({ + laneId, + agentId, + agentClass, + capabilities, + operatorId, + operatorAuthorizationRef, + hostContext, + now, + requestId +}) { + const requestedCapabilities = normalizeCapabilities(capabilities); + const generatedAt = nowIso(now); + return { + schema: DOCKER_LANE_HANDSHAKE_SCHEMA, + generatedAt, + laneId, + resourceKind: DOCKER_LANE_CAPABILITY, + state: 'requested', + sequence: 1, + heartbeatAt: generatedAt, + host: hostContext, + request: { + requestId, + requestedAt: generatedAt, + agentId, + agentClass, + capabilities: requestedCapabilities, + premiumDualLaneRequested: isPremiumDualLaneRequest(requestedCapabilities), + operatorId: toOptionalText(operatorId), + operatorAuthorizationRef: toOptionalText(operatorAuthorizationRef) + }, + grant: null, + commit: null, + release: null + }; +} + +function cloneForTransition(handshake, now) { + return { + ...handshake, + generatedAt: nowIso(now), + sequence: Number.isInteger(handshake?.sequence) ? handshake.sequence + 1 : 1 + }; +} + +function printUsage() { + console.log('Usage: node tools/priority/docker-lane-handshake.mjs --action --lane-id [options]'); + console.log(''); + console.log('Options:'); + console.log(` --output Report output path (default: ${DEFAULT_OUTPUT_PATH}).`); + console.log(' --lane-id Logical isolated Docker lane id.'); + console.log(' --action Handshake action (request|grant|commit|heartbeat|release|inspect).'); + console.log(` --agent-id Request owner (default: ${defaultOwner()}).`); + console.log(' --agent-class Agent class (default: subagent).'); + console.log(` --host-plane-report Host-plane report path (default: ${DEFAULT_HOST_PLANE_REPORT_PATH}).`); + console.log(` --operator-cost-profile Operator cost profile path (default: ${DEFAULT_OPERATOR_COST_PROFILE_PATH}).`); + console.log(' --operator-id Operator id for billable-rate resolution.'); + console.log(' --operator-authorization-ref Authorization receipt/reference for premium Sagan mode.'); + console.log(` --ttl-seconds Grant TTL in seconds (default: ${DEFAULT_TTL_SECONDS}).`); + console.log(' --grantor Grantor id recorded on grant (default: sagan-governor).'); + console.log(' --lease-id Lease id matcher for commit/heartbeat/release.'); + console.log(' --execution-cell-report Execution-cell report used to bind a committed Docker lane to a cell.'); + console.log(' --capability Requested capability (repeatable).'); + console.log(` --premium-dual-lane Shortcut for ${DOCKER_LANE_CAPABILITY} + ${NATIVE_LV32_CAPABILITY}.`); + console.log(' --final-status Release final status (default: succeeded).'); + console.log(' --artifact-path Artifact path to attach on release (repeatable).'); + console.log(' --handshake-root Override handshake state directory.'); + console.log(' --help Show this help text and exit.'); +} + +export function parseArgs(argv = process.argv) { + const args = argv.slice(2); + const options = { + action: '', + laneId: '', + outputPath: DEFAULT_OUTPUT_PATH, + agentId: defaultOwner(), + agentClass: 'subagent', + hostPlaneReportPath: DEFAULT_HOST_PLANE_REPORT_PATH, + operatorCostProfilePath: DEFAULT_OPERATOR_COST_PROFILE_PATH, + operatorId: '', + operatorAuthorizationRef: '', + ttlSeconds: DEFAULT_TTL_SECONDS, + grantor: 'sagan-governor', + leaseId: '', + capabilities: [], + premiumDualLane: false, + finalStatus: 'succeeded', + artifactPaths: [], + handshakeRoot: '', + executionCellReportPath: '', + help: false + }; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + const next = args[index + 1]; + + if (token === '--help' || token === '-h') { + options.help = true; + continue; + } + + if (token === '--premium-dual-lane') { + options.premiumDualLane = true; + continue; + } + + if ( + token === '--action' || + token === '--lane-id' || + token === '--output' || + token === '--agent-id' || + token === '--agent-class' || + token === '--host-plane-report' || + token === '--operator-cost-profile' || + token === '--operator-id' || + token === '--operator-authorization-ref' || + token === '--ttl-seconds' || + token === '--grantor' || + token === '--lease-id' || + token === '--execution-cell-report' || + token === '--final-status' || + token === '--handshake-root' + ) { + if (!next || next.startsWith('-')) { + throw new Error(`Missing value for ${token}.`); + } + index += 1; + if (token === '--action') options.action = next; + if (token === '--lane-id') options.laneId = next; + if (token === '--output') options.outputPath = next; + if (token === '--agent-id') options.agentId = next; + if (token === '--agent-class') options.agentClass = normalizeAgentClass(next); + if (token === '--host-plane-report') options.hostPlaneReportPath = next; + if (token === '--operator-cost-profile') options.operatorCostProfilePath = next; + if (token === '--operator-id') options.operatorId = next; + if (token === '--operator-authorization-ref') options.operatorAuthorizationRef = next; + if (token === '--ttl-seconds') { + const ttl = Number.parseInt(next, 10); + if (!Number.isInteger(ttl) || ttl <= 0) { + throw new Error(`Invalid --ttl-seconds value '${next}'.`); + } + options.ttlSeconds = ttl; + } + if (token === '--grantor') options.grantor = next; + if (token === '--lease-id') options.leaseId = next; + if (token === '--execution-cell-report') options.executionCellReportPath = next; + if (token === '--final-status') options.finalStatus = next; + if (token === '--handshake-root') options.handshakeRoot = next; + continue; + } + + if (token === '--capability' || token === '--artifact-path') { + if (!next || next.startsWith('-')) { + throw new Error(`Missing value for ${token}.`); + } + index += 1; + if (token === '--capability') options.capabilities.push(next); + if (token === '--artifact-path') options.artifactPaths.push(next); + continue; + } + + throw new Error(`Unknown option: ${token}`); + } + + if (options.premiumDualLane) { + options.capabilities.push(DOCKER_LANE_CAPABILITY, NATIVE_LV32_CAPABILITY); + } + options.capabilities = normalizeCapabilities(options.capabilities); + + return options; +} + +async function writeReport(outputPath, report, repoRoot = process.cwd()) { + const resolved = path.resolve(repoRoot, outputPath); + await ensureParentDir(resolved); + await fs.writeFile(resolved, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); + return resolved; +} + +function withObservations(report, observations = []) { + return { + ...report, + summary: { + ...report.summary, + observations: [...report.summary.observations, ...observations.filter(Boolean)] + } + }; +} + +function withDenialReasons(report, reasons = []) { + return { + ...report, + summary: { + ...report.summary, + denialReasons: [...report.summary.denialReasons, ...reasons.filter(Boolean)] + } + }; +} + +export async function runDockerLaneHandshake(options = {}) { + const action = normalizeText(options.action).toLowerCase(); + const laneId = normalizeText(options.laneId); + const repoRoot = options.repoRoot || REPO_ROOT; + const agentId = toOptionalText(options.agentId) || defaultOwner(); + const agentClass = normalizeAgentClass(options.agentClass); + const handshakeRoot = defaultHandshakeRoot({ repoRoot, handshakeRoot: options.handshakeRoot }); + const handshakePath = handshakePathForLane(laneId, handshakeRoot); + const generatedAt = nowIso(options.now || new Date()); + const current = await readJsonIfPresent(handshakePath); + const hostPlaneReport = options.hostPlaneReport ?? (await readJsonIfPresent(path.resolve(repoRoot, options.hostPlaneReportPath || DEFAULT_HOST_PLANE_REPORT_PATH))); + const operatorCostProfile = + options.operatorCostProfile ?? (await readJsonIfPresent(path.resolve(repoRoot, options.operatorCostProfilePath || DEFAULT_OPERATOR_COST_PROFILE_PATH))); + const { context: hostContext, observations: hostObservations } = buildHostContext(hostPlaneReport); + const operatorProfile = resolveOperatorProfile(operatorCostProfile, options.operatorId); + const base = buildBaseReport(action, laneId, handshakePath, generatedAt, current, { + policy: { + operatorId: operatorProfile.operatorId, + currency: operatorProfile.currency, + laborRateUsdPerHour: operatorProfile.laborRateUsdPerHour, + premiumSaganRateMultiplier: PREMIUM_RATE_MULTIPLIER + } + }); + const reportWithHost = withObservations(base, hostObservations); + + if (!laneId) { + return withDenialReasons({ ...reportWithHost, status: STATUS.invalidState }, ['lane-id-required']); + } + + if (!['request', 'grant', 'commit', 'heartbeat', 'release', 'inspect'].includes(action)) { + return withDenialReasons({ ...reportWithHost, status: STATUS.invalidState }, ['action-invalid']); + } + + if (action === 'inspect') { + if (!current) { + return { ...reportWithHost, status: STATUS.notFound, handshake: null }; + } + return { ...reportWithHost, status: isHandshakeStale(current, Date.parse(generatedAt)) ? STATUS.stale : STATUS.active }; + } + + if (action === 'request') { + if (current && current.state !== 'released') { + const status = isHandshakeStale(current, Date.parse(generatedAt)) ? STATUS.stale : STATUS.busy; + return withDenialReasons({ ...reportWithHost, status }, [status === STATUS.stale ? 'stale-handshake-present' : 'lane-already-held']); + } + if (normalizeCapabilities(options.capabilities).length === 0) { + return withDenialReasons({ ...reportWithHost, status: STATUS.denied }, ['capabilities-required']); + } + + const next = buildRequestRecord({ + laneId, + agentId, + agentClass, + capabilities: options.capabilities, + operatorId: options.operatorId || operatorProfile.operatorId, + operatorAuthorizationRef: options.operatorAuthorizationRef, + hostContext, + now: options.now || new Date(), + requestId: options.requestId || uniqueId('request', Date.parse(generatedAt)) + }); + await writeJsonAtomic(handshakePath, next); + return buildResultReport(action, laneId, handshakePath, generatedAt, next, operatorProfile, hostObservations, STATUS.requested); + } + + if (!current) { + return { ...reportWithHost, status: STATUS.notFound, handshake: null }; + } + + const sameAgent = normalizeText(current?.request?.agentId) === agentId; + if (!sameAgent) { + return withDenialReasons({ ...reportWithHost, status: STATUS.mismatch }, ['request-owner-mismatch']); + } + + if (action === 'grant') { + if (current.state !== 'requested') { + return withDenialReasons({ ...reportWithHost, status: STATUS.invalidState }, ['grant-requires-requested-state']); + } + const decision = buildGrantDecision(current, operatorProfile); + if (!decision.allowed) { + return withDenialReasons({ ...reportWithHost, status: STATUS.denied }, decision.reasons); + } + + const next = cloneForTransition(current, options.now || new Date()); + next.state = 'granted'; + next.heartbeatAt = generatedAt; + next.host = hostContext || current.host || null; + next.grant = { + grantedAt: generatedAt, + grantor: toOptionalText(options.grantor) || 'sagan-governor', + leaseId: options.leaseId || uniqueId('lease', Date.parse(generatedAt)), + ttlSeconds: Number.isInteger(options.ttlSeconds) ? options.ttlSeconds : DEFAULT_TTL_SECONDS, + grantedCapabilities: decision.grantedCapabilities, + billableRateMultiplier: decision.billableRateMultiplier, + billableRateUsdPerHour: decision.billableRateUsdPerHour, + premiumSaganMode: decision.premiumSaganMode, + policyDecision: decision.policyDecision, + operatorAuthorizationRef: toOptionalText(current.request.operatorAuthorizationRef) + }; + await writeJsonAtomic(handshakePath, next); + return buildResultReport(action, laneId, handshakePath, generatedAt, next, operatorProfile, hostObservations, STATUS.granted); + } + + const leaseId = toOptionalText(options.leaseId); + const leaseIdMatches = !leaseId || normalizeText(current?.grant?.leaseId) === leaseId; + if (!leaseIdMatches) { + return withDenialReasons({ ...reportWithHost, status: STATUS.mismatch }, ['lease-id-mismatch']); + } + + if (action === 'commit') { + if (current.state !== 'granted') { + return withDenialReasons({ ...reportWithHost, status: STATUS.invalidState }, ['commit-requires-granted-state']); + } + + let linkedExecutionCell = null; + if (toOptionalText(options.executionCellReportPath)) { + linkedExecutionCell = resolveExecutionCellLinkContext( + await readJsonIfPresent(path.resolve(repoRoot, options.executionCellReportPath)) + ); + const linkReasons = validateExecutionCellLink(current, linkedExecutionCell); + if (linkReasons.length > 0) { + return withDenialReasons({ ...reportWithHost, status: STATUS.mismatch }, linkReasons); + } + } + + const next = cloneForTransition(current, options.now || new Date()); + next.state = 'active'; + next.heartbeatAt = generatedAt; + next.commit = { + committedAt: generatedAt, + executionCellId: linkedExecutionCell?.cellId || toOptionalText(current?.commit?.executionCellId), + executionCellLeaseId: linkedExecutionCell?.leaseId || toOptionalText(current?.commit?.executionCellLeaseId) + }; + await writeJsonAtomic(handshakePath, next); + const report = buildResultReport( + action, + laneId, + handshakePath, + generatedAt, + next, + operatorProfile, + hostObservations, + STATUS.committed + ); + if (linkedExecutionCell?.cellId) { + report.summary.observations.push('linked-execution-cell-commit'); + } + return report; + } + + if (action === 'heartbeat') { + if (current.state !== 'active') { + return withDenialReasons({ ...reportWithHost, status: STATUS.invalidState }, ['heartbeat-requires-active-state']); + } + const next = cloneForTransition(current, options.now || new Date()); + next.heartbeatAt = generatedAt; + await writeJsonAtomic(handshakePath, next); + return buildResultReport(action, laneId, handshakePath, generatedAt, next, operatorProfile, hostObservations, STATUS.renewed); + } + + if (action === 'release') { + if (!['requested', 'granted', 'active'].includes(current.state)) { + return withDenialReasons({ ...reportWithHost, status: STATUS.invalidState }, ['release-requires-live-state']); + } + const next = cloneForTransition(current, options.now || new Date()); + next.state = 'released'; + next.heartbeatAt = generatedAt; + next.release = { + releasedAt: generatedAt, + finalStatus: toOptionalText(options.finalStatus) || 'succeeded', + artifactPaths: normalizeCapabilities(options.artifactPaths) + }; + await writeJsonAtomic(handshakePath, next); + return buildResultReport(action, laneId, handshakePath, generatedAt, next, operatorProfile, hostObservations, STATUS.released); + } + + return withDenialReasons({ ...reportWithHost, status: STATUS.invalidState }, ['action-unhandled']); +} + +function exitCodeForStatus(status) { + if ([STATUS.requested, STATUS.granted, STATUS.committed, STATUS.released, STATUS.renewed, STATUS.active].includes(status)) { + return 0; + } + if (status === STATUS.notFound) { + return 0; + } + return 1; +} + +function buildResultReport(action, laneId, handshakePath, generatedAt, handshake, operatorProfile, hostObservations, status) { + const base = buildBaseReport(action, laneId, handshakePath, generatedAt, handshake, { + policy: { + operatorId: operatorProfile.operatorId, + currency: operatorProfile.currency, + laborRateUsdPerHour: operatorProfile.laborRateUsdPerHour, + premiumSaganRateMultiplier: PREMIUM_RATE_MULTIPLIER + } + }); + return { + ...withObservations(base, hostObservations), + status, + handshake + }; +} + +export async function main(argv = process.argv) { + const options = parseArgs(argv); + if (options.help) { + printUsage(); + return 0; + } + + const report = await runDockerLaneHandshake({ + ...options, + repoRoot: process.cwd() + }); + const outputPath = await writeReport(options.outputPath, report, process.cwd()); + console.log( + `[docker-lane-handshake] report: ${outputPath} status=${report.status} lane=${report.laneId} state=${report.summary.handshakeState ?? 'none'}` + ); + return exitCodeForStatus(report.status); +} + +const isEntrypoint = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +if (isEntrypoint) { + const exitCode = await main(process.argv); + process.exitCode = exitCode; +} diff --git a/tools/priority/execution-cell-bundle.mjs b/tools/priority/execution-cell-bundle.mjs new file mode 100644 index 000000000..7ae7a3a45 --- /dev/null +++ b/tools/priority/execution-cell-bundle.mjs @@ -0,0 +1,701 @@ +#!/usr/bin/env node + +import fs from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +import { + DEFAULT_OUTPUT_PATH as DEFAULT_DOCKER_REPORT_PATH, + DOCKER_LANE_CAPABILITY, + NATIVE_LV32_CAPABILITY, + runDockerLaneHandshake +} from './docker-lane-handshake.mjs'; +import { + DEFAULT_HARNESS_KIND, + DEFAULT_OUTPUT_PATH as DEFAULT_EXECUTION_CELL_REPORT_PATH, + runExecutionCellLease +} from './execution-cell-lease.mjs'; + +const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(MODULE_DIR, '..', '..'); + +export const EXECUTION_CELL_BUNDLE_REPORT_SCHEMA = 'priority/execution-cell-bundle-report@v1'; +export const DEFAULT_OUTPUT_PATH = path.join('tests', 'results', '_agent', 'runtime', 'execution-cell-bundle.json'); + +export const STATUS = Object.freeze({ + requested: 'requested', + granted: 'granted', + committed: 'committed', + renewed: 'renewed', + released: 'released', + active: 'active', + busy: 'busy', + denied: 'denied', + notFound: 'not-found', + mismatch: 'mismatch', + invalidState: 'invalid-state', + stale: 'stale', + partial: 'partial' +}); + +const SUCCESS_STATUS = new Set([ + STATUS.requested, + STATUS.granted, + STATUS.committed, + STATUS.renewed, + STATUS.released, + STATUS.active +]); + +function normalizeText(value) { + if (value == null) { + return ''; + } + return String(value).trim(); +} + +function toOptionalText(value) { + const normalized = normalizeText(value); + return normalized || null; +} + +function normalizeCapabilities(capabilities = []) { + const values = []; + for (const entry of capabilities) { + const normalized = normalizeText(entry); + if (normalized) { + values.push(normalized); + } + } + return [...new Set(values)]; +} + +function wantsDockerLane(options = {}) { + return Boolean(toOptionalText(options.laneId)) || normalizeCapabilities(options.capabilities).includes(DOCKER_LANE_CAPABILITY); +} + +function inferBundleCapabilities(capabilities = [], planeBinding, dockerRequested) { + const normalized = new Set(normalizeCapabilities(capabilities)); + const normalizedPlaneBinding = normalizeText(planeBinding).toLowerCase(); + if (dockerRequested) { + normalized.add(DOCKER_LANE_CAPABILITY); + } + if (normalizedPlaneBinding === 'native-labview-2026-32' || normalizedPlaneBinding === 'dual-plane-parity') { + normalized.add(NATIVE_LV32_CAPABILITY); + } + return [...normalized]; +} + +function isWindowsNativeTestStand(planeBinding, harnessKind) { + if (!toOptionalText(harnessKind)) { + return null; + } + if (normalizeText(harnessKind) !== DEFAULT_HARNESS_KIND) { + return null; + } + const normalizedPlaneBinding = normalizeText(planeBinding).toLowerCase(); + if (!normalizedPlaneBinding) { + return true; + } + return normalizedPlaneBinding === 'dual-plane-parity' || normalizedPlaneBinding.startsWith('native-labview-'); +} + +function isSuccessfulStatus(status) { + return SUCCESS_STATUS.has(normalizeText(status)); +} + +function firstFailureStatus(...reports) { + for (const report of reports) { + const status = normalizeText(report?.status); + if (status && !isSuccessfulStatus(status)) { + return status; + } + } + return null; +} + +function summarizeEffectiveRate(reports = []) { + const multipliers = reports + .map((report) => report?.summary?.billableRateMultiplier) + .filter((value) => Number.isFinite(value)); + const rates = reports + .map((report) => report?.summary?.billableRateUsdPerHour) + .filter((value) => Number.isFinite(value)); + return { + multiplier: multipliers.length > 0 ? Math.max(...multipliers) : null, + usdPerHour: rates.length > 0 ? Math.max(...rates) : null + }; +} + +function collectSummaryStrings(reports, field) { + const values = []; + for (const report of reports) { + const entries = report?.summary?.[field]; + if (Array.isArray(entries)) { + values.push(...entries.filter(Boolean)); + } + } + return [...new Set(values)]; +} + +function hasReciprocalBundleLink(executionCell, dockerLane, cellId, laneId) { + const executionSummary = executionCell?.summary ?? {}; + const dockerSummary = dockerLane?.summary ?? {}; + const executionLeaseId = normalizeText(executionSummary.leaseId); + const dockerLeaseId = normalizeText(dockerSummary.leaseId); + if (!executionLeaseId || !dockerLeaseId) { + return false; + } + + return ( + normalizeText(executionSummary.linkedDockerLaneId) === normalizeText(laneId) && + normalizeText(executionSummary.linkedDockerLaneLeaseId) === dockerLeaseId && + normalizeText(dockerSummary.linkedExecutionCellId) === normalizeText(cellId) && + normalizeText(dockerSummary.linkedExecutionCellLeaseId) === executionLeaseId + ); +} + +function resolveBundleStatus(action, executionCell, dockerLane, rollbacks) { + const failure = firstFailureStatus(executionCell, dockerLane, rollbacks.executionCell, rollbacks.dockerLane); + if (failure) { + return failure; + } + + if (!dockerLane) { + return normalizeText(executionCell?.status) || STATUS.notFound; + } + + if (action === 'inspect') { + const executionStatus = normalizeText(executionCell?.status); + const dockerStatus = normalizeText(dockerLane?.status); + if (executionStatus === STATUS.stale || dockerStatus === STATUS.stale) { + return STATUS.stale; + } + if (executionStatus === STATUS.active || dockerStatus === STATUS.active) { + return STATUS.active; + } + if (executionStatus === STATUS.granted || dockerStatus === STATUS.granted) { + return STATUS.granted; + } + if (executionStatus === STATUS.requested || dockerStatus === STATUS.requested) { + return STATUS.requested; + } + if (executionStatus === STATUS.released || dockerStatus === STATUS.released) { + return STATUS.released; + } + if (executionStatus === STATUS.notFound && dockerStatus === STATUS.notFound) { + return STATUS.notFound; + } + return STATUS.partial; + } + + if (normalizeText(executionCell?.status) === normalizeText(dockerLane?.status)) { + return normalizeText(executionCell?.status); + } + + if ( + action === 'release' && + [STATUS.released, STATUS.notFound].includes(normalizeText(executionCell?.status)) && + [STATUS.released, STATUS.notFound].includes(normalizeText(dockerLane?.status)) + ) { + return STATUS.released; + } + + return STATUS.partial; +} + +async function writeReport(outputPath, report) { + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await fs.writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); +} + +async function maybeReleaseExecutionCell(options, now, outputPath, leaseId) { + return runExecutionCellLease({ + action: 'release', + cellId: options.cellId, + leaseId, + artifactPaths: [], + hostPlaneReportPath: options.hostPlaneReportPath, + operatorCostProfilePath: options.operatorCostProfilePath, + leaseRoot: options.leaseRoot, + repoRoot: options.repoRoot, + now, + outputPath + }); +} + +async function maybeReleaseDockerLane(options, now, outputPath) { + const inspect = await runDockerLaneHandshake({ + action: 'inspect', + laneId: options.laneId, + agentId: options.agentId, + agentClass: options.agentClass, + hostPlaneReportPath: options.hostPlaneReportPath, + operatorCostProfilePath: options.operatorCostProfilePath, + handshakeRoot: options.handshakeRoot, + repoRoot: options.repoRoot, + now, + outputPath + }); + if (!inspect?.handshake || [STATUS.notFound, STATUS.released].includes(normalizeText(inspect.status))) { + return inspect; + } + return runDockerLaneHandshake({ + action: 'release', + laneId: options.laneId, + agentId: options.agentId, + agentClass: options.agentClass, + leaseId: inspect?.handshake?.grant?.leaseId, + artifactPaths: [], + hostPlaneReportPath: options.hostPlaneReportPath, + operatorCostProfilePath: options.operatorCostProfilePath, + handshakeRoot: options.handshakeRoot, + repoRoot: options.repoRoot, + now, + outputPath + }); +} + +function buildReport({ + action, + generatedAt, + cellId, + laneId, + outputPath, + dockerRequested, + capabilities, + executionCell, + dockerLane, + rollbacks +}) { + const reports = [executionCell, dockerLane].filter(Boolean); + const effectiveRate = summarizeEffectiveRate(reports); + const premiumSaganMode = + executionCell?.summary?.premiumSaganMode === true || dockerLane?.summary?.premiumSaganMode === true; + const observations = collectSummaryStrings(reports, 'observations'); + if (reports.length > 1 && effectiveRate.usdPerHour != null) { + observations.push('agent-billed-once-at-effective-rate'); + } + if (rollbacks.executionCell) { + observations.push(`execution-cell-rollback-${rollbacks.executionCell.status}`); + } + if (rollbacks.dockerLane) { + observations.push(`docker-lane-rollback-${rollbacks.dockerLane.status}`); + } + + return { + schema: EXECUTION_CELL_BUNDLE_REPORT_SCHEMA, + generatedAt, + action, + status: resolveBundleStatus(action, executionCell, dockerLane, rollbacks), + cellId, + laneId: toOptionalText(laneId), + outputPath, + executionCellReportPath: executionCell?.leasePath || null, + dockerLaneReportPath: dockerLane?.handshakePath || null, + executionCell, + dockerLane: dockerLane || null, + rollbacks: { + executionCell: rollbacks.executionCell || null, + dockerLane: rollbacks.dockerLane || null + }, + summary: { + holder: toOptionalText(executionCell?.summary?.holder) || toOptionalText(dockerLane?.summary?.holder), + agentClass: + toOptionalText(executionCell?.summary?.agentClass) || toOptionalText(dockerLane?.handshake?.request?.agentClass), + cellClass: toOptionalText(executionCell?.summary?.cellClass), + suiteClass: toOptionalText(executionCell?.summary?.suiteClass), + planeBinding: toOptionalText(executionCell?.summary?.planeBinding), + harnessKind: toOptionalText(executionCell?.summary?.harnessKind), + harnessInstanceId: toOptionalText(executionCell?.summary?.harnessInstanceId), + executionCellLeaseId: toOptionalText(executionCell?.summary?.leaseId), + dockerLaneLeaseId: toOptionalText(dockerLane?.summary?.leaseId), + linkedExecutionCellId: toOptionalText(dockerLane?.summary?.linkedExecutionCellId), + linkedExecutionCellLeaseId: toOptionalText(dockerLane?.summary?.linkedExecutionCellLeaseId), + linkedDockerLaneId: toOptionalText(executionCell?.summary?.linkedDockerLaneId), + linkedDockerLaneLeaseId: toOptionalText(executionCell?.summary?.linkedDockerLaneLeaseId), + reciprocalLinkReady: hasReciprocalBundleLink(executionCell, dockerLane, cellId, laneId), + dockerRequested, + windowsNativeTestStand: isWindowsNativeTestStand( + executionCell?.summary?.planeBinding, + executionCell?.summary?.harnessKind + ), + effectiveBillableRateMultiplier: effectiveRate.multiplier, + effectiveBillableRateUsdPerHour: effectiveRate.usdPerHour, + premiumSaganMode, + operatorAuthorizationRef: + toOptionalText(executionCell?.summary?.operatorAuthorizationRef) || + toOptionalText(dockerLane?.summary?.operatorAuthorizationRef), + isolatedLaneGroupId: + toOptionalText(executionCell?.summary?.isolatedLaneGroupId) || + toOptionalText(dockerLane?.summary?.isolatedLaneGroupId), + fingerprintSha256: + toOptionalText(executionCell?.summary?.fingerprintSha256) || + toOptionalText(dockerLane?.summary?.fingerprintSha256), + capabilities, + denialReasons: collectSummaryStrings([...reports, rollbacks.executionCell, rollbacks.dockerLane], 'denialReasons'), + observations: [...new Set(observations.filter(Boolean))] + } + }; +} + +function parseArgs(argv) { + const options = { + action: '', + cellId: '', + laneId: '', + agentId: '', + agentClass: '', + cellClass: '', + suiteClass: '', + planeBinding: '', + harnessKind: DEFAULT_HARNESS_KIND, + capabilities: [], + operatorId: '', + operatorAuthorizationRef: '', + workingRoot: '', + artifactRoot: '', + harnessInstanceId: '', + hostPlaneReportPath: '', + operatorCostProfilePath: '', + executionCellReportPath: '', + dockerLaneReportPath: '', + outputPath: '', + leaseRoot: '', + handshakeRoot: '', + artifactPaths: [], + help: false + }; + + for (let index = 2; index < argv.length; index += 1) { + const token = argv[index]; + const next = argv[index + 1]; + switch (token) { + case '--help': + case '-h': + options.help = true; + break; + case '--action': + options.action = next; + index += 1; + break; + case '--cell-id': + options.cellId = next; + index += 1; + break; + case '--lane-id': + options.laneId = next; + index += 1; + break; + case '--agent-id': + options.agentId = next; + index += 1; + break; + case '--agent-class': + options.agentClass = next; + index += 1; + break; + case '--cell-class': + options.cellClass = next; + index += 1; + break; + case '--suite-class': + options.suiteClass = next; + index += 1; + break; + case '--plane-binding': + options.planeBinding = next; + index += 1; + break; + case '--harness-kind': + options.harnessKind = next; + index += 1; + break; + case '--capability': + options.capabilities.push(next); + index += 1; + break; + case '--operator-id': + options.operatorId = next; + index += 1; + break; + case '--operator-authorization-ref': + options.operatorAuthorizationRef = next; + index += 1; + break; + case '--working-root': + options.workingRoot = next; + index += 1; + break; + case '--artifact-root': + options.artifactRoot = next; + index += 1; + break; + case '--harness-instance-id': + options.harnessInstanceId = next; + index += 1; + break; + case '--host-plane-report': + options.hostPlaneReportPath = next; + index += 1; + break; + case '--operator-cost-profile': + options.operatorCostProfilePath = next; + index += 1; + break; + case '--execution-cell-report': + options.executionCellReportPath = next; + index += 1; + break; + case '--docker-lane-report': + options.dockerLaneReportPath = next; + index += 1; + break; + case '--lease-root': + options.leaseRoot = next; + index += 1; + break; + case '--handshake-root': + options.handshakeRoot = next; + index += 1; + break; + case '--artifact-path': + options.artifactPaths.push(next); + index += 1; + break; + case '--output': + options.outputPath = next; + index += 1; + break; + default: + throw new Error(`Unknown argument '${token}'`); + } + } + + return options; +} + +function printUsage() { + console.log( + 'Usage: node tools/priority/execution-cell-bundle.mjs --action --cell-id [options]' + ); +} + +export async function runExecutionCellBundle(options = {}) { + const now = options.now || new Date(); + const generatedAt = now.toISOString(); + const repoRoot = path.resolve(options.repoRoot || REPO_ROOT); + const outputPath = path.resolve(repoRoot, options.outputPath || DEFAULT_OUTPUT_PATH); + const dockerRequested = wantsDockerLane(options); + const capabilities = inferBundleCapabilities(options.capabilities, options.planeBinding, dockerRequested); + + if (!toOptionalText(options.cellId)) { + const report = buildReport({ + action: options.action, + generatedAt, + cellId: '', + laneId: options.laneId, + outputPath, + dockerRequested, + capabilities, + executionCell: null, + dockerLane: null, + rollbacks: {} + }); + report.status = STATUS.invalidState; + report.summary.denialReasons = ['cell-id-required']; + await writeReport(outputPath, report); + return report; + } + + if (dockerRequested && !toOptionalText(options.laneId)) { + const report = buildReport({ + action: options.action, + generatedAt, + cellId: options.cellId, + laneId: '', + outputPath, + dockerRequested, + capabilities, + executionCell: null, + dockerLane: null, + rollbacks: {} + }); + report.status = STATUS.invalidState; + report.summary.denialReasons = ['lane-id-required']; + await writeReport(outputPath, report); + return report; + } + + const executionCellReportPath = path.resolve( + repoRoot, + options.executionCellReportPath || DEFAULT_EXECUTION_CELL_REPORT_PATH + ); + const dockerLaneReportPath = path.resolve(repoRoot, options.dockerLaneReportPath || DEFAULT_DOCKER_REPORT_PATH); + + const executionCellOptions = { + action: options.action, + cellId: options.cellId, + agentId: options.agentId, + agentClass: options.agentClass, + cellClass: options.cellClass, + suiteClass: options.suiteClass, + planeBinding: options.planeBinding, + harnessKind: options.harnessKind || DEFAULT_HARNESS_KIND, + capabilities, + operatorId: options.operatorId, + operatorAuthorizationRef: options.operatorAuthorizationRef, + workingRoot: options.workingRoot, + artifactRoot: options.artifactRoot, + harnessInstanceId: options.harnessInstanceId, + dockerLaneReportPath, + hostPlaneReportPath: options.hostPlaneReportPath, + operatorCostProfilePath: options.operatorCostProfilePath, + leaseRoot: options.leaseRoot, + repoRoot, + now, + artifactPaths: options.artifactPaths, + outputPath: executionCellReportPath + }; + + const dockerLaneOptions = dockerRequested + ? { + action: options.action, + laneId: options.laneId, + agentId: options.agentId, + agentClass: options.agentClass, + capabilities, + operatorId: options.operatorId, + operatorAuthorizationRef: options.operatorAuthorizationRef, + executionCellReportPath, + hostPlaneReportPath: options.hostPlaneReportPath, + operatorCostProfilePath: options.operatorCostProfilePath, + handshakeRoot: options.handshakeRoot, + repoRoot, + now, + artifactPaths: options.artifactPaths, + outputPath: dockerLaneReportPath + } + : null; + + let executionCell = null; + let dockerLane = null; + const rollbacks = {}; + + switch (normalizeText(options.action).toLowerCase()) { + case 'request': + executionCell = await runExecutionCellLease(executionCellOptions); + if (dockerLaneOptions) { + dockerLane = await runDockerLaneHandshake(dockerLaneOptions); + await writeReport(dockerLaneReportPath, dockerLane); + } + break; + case 'grant': + executionCell = await runExecutionCellLease(executionCellOptions); + if (dockerLaneOptions) { + if (normalizeText(executionCell?.status) === STATUS.granted) { + dockerLane = await runDockerLaneHandshake(dockerLaneOptions); + await writeReport(dockerLaneReportPath, dockerLane); + if (normalizeText(dockerLane?.status) !== STATUS.granted) { + rollbacks.executionCell = await maybeReleaseExecutionCell( + executionCellOptions, + now, + executionCellReportPath, + executionCell?.lease?.grant?.leaseId + ); + } + } else { + rollbacks.dockerLane = await maybeReleaseDockerLane(dockerLaneOptions, now, dockerLaneReportPath); + } + } + break; + case 'commit': + if (dockerLaneOptions) { + dockerLane = await runDockerLaneHandshake(dockerLaneOptions); + await writeReport(dockerLaneReportPath, dockerLane); + if (normalizeText(dockerLane?.status) === STATUS.committed) { + executionCell = await runExecutionCellLease(executionCellOptions); + if (normalizeText(executionCell?.status) !== STATUS.committed) { + rollbacks.dockerLane = await maybeReleaseDockerLane(dockerLaneOptions, now, dockerLaneReportPath); + } + } else { + executionCell = await runExecutionCellLease({ ...executionCellOptions, action: 'inspect' }); + } + } else { + executionCell = await runExecutionCellLease(executionCellOptions); + } + break; + case 'heartbeat': + if (dockerLaneOptions) { + dockerLane = await runDockerLaneHandshake(dockerLaneOptions); + await writeReport(dockerLaneReportPath, dockerLane); + } + executionCell = await runExecutionCellLease(executionCellOptions); + break; + case 'release': + if (dockerLaneOptions) { + dockerLane = await runDockerLaneHandshake(dockerLaneOptions); + await writeReport(dockerLaneReportPath, dockerLane); + } + executionCell = await runExecutionCellLease(executionCellOptions); + break; + case 'inspect': + executionCell = await runExecutionCellLease({ ...executionCellOptions, action: 'inspect' }); + if (dockerLaneOptions) { + dockerLane = await runDockerLaneHandshake({ ...dockerLaneOptions, action: 'inspect' }); + await writeReport(dockerLaneReportPath, dockerLane); + } + break; + default: + throw new Error(`Unsupported action '${options.action}'`); + } + + const report = buildReport({ + action: normalizeText(options.action).toLowerCase(), + generatedAt, + cellId: options.cellId, + laneId: options.laneId, + outputPath, + dockerRequested, + capabilities, + executionCell, + dockerLane, + rollbacks + }); + await writeReport(outputPath, report); + return report; +} + +function exitCodeForStatus(status) { + if ([STATUS.requested, STATUS.granted, STATUS.committed, STATUS.renewed, STATUS.released, STATUS.active].includes(status)) { + return 0; + } + if (status === STATUS.notFound) { + return 0; + } + return 1; +} + +export async function main(argv = process.argv) { + const options = parseArgs(argv); + if (options.help) { + printUsage(); + return 0; + } + + const report = await runExecutionCellBundle({ + ...options, + repoRoot: process.cwd() + }); + console.log( + `[execution-cell-bundle] report: ${path.resolve(process.cwd(), options.outputPath || DEFAULT_OUTPUT_PATH)} status=${report.status} cell=${report.cellId} lane=${report.laneId ?? 'none'}` + ); + return exitCodeForStatus(report.status); +} + +const isEntrypoint = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +if (isEntrypoint) { + const exitCode = await main(process.argv); + process.exitCode = exitCode; +} diff --git a/tools/priority/execution-cell-lease.mjs b/tools/priority/execution-cell-lease.mjs new file mode 100644 index 000000000..b9f02bd92 --- /dev/null +++ b/tools/priority/execution-cell-lease.mjs @@ -0,0 +1,802 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +import { resolveGitAdminPaths } from './lib/git-admin-paths.mjs'; + +const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(MODULE_DIR, '..', '..'); + +export const EXECUTION_CELL_LEASE_SCHEMA = 'priority/execution-cell-lease@v1'; +export const EXECUTION_CELL_LEASE_REPORT_SCHEMA = 'priority/execution-cell-lease-report@v1'; +export const DEFAULT_OUTPUT_PATH = path.join('tests', 'results', '_agent', 'runtime', 'execution-cell-lease.json'); +export const DEFAULT_HOST_PLANE_REPORT_PATH = path.join( + 'tests', + 'results', + '_agent', + 'host-planes', + 'labview-2026-host-plane-report.json' +); +export const DEFAULT_OPERATOR_COST_PROFILE_PATH = path.join('tools', 'policy', 'operator-cost-profile.json'); +export const DEFAULT_TTL_SECONDS = 1800; +export const DEFAULT_HARNESS_KIND = 'teststand-compare-harness'; +export const PREMIUM_RATE_MULTIPLIER = 1.5; +export const ORDINARY_RATE_MULTIPLIER = 1.0; +export const DOCKER_LANE_CAPABILITY = 'docker-lane'; +export const NATIVE_LV32_CAPABILITY = 'native-labview-2026-32'; +export const DEFAULT_CELL_CLASS = 'worker'; + +export const STATUS = Object.freeze({ + requested: 'requested', + granted: 'granted', + committed: 'committed', + renewed: 'renewed', + released: 'released', + active: 'active', + busy: 'busy', + denied: 'denied', + notFound: 'not-found', + mismatch: 'mismatch', + invalidState: 'invalid-state', + stale: 'stale' +}); + +export const CELL_CLASS = Object.freeze({ + worker: 'worker', + coordinator: 'coordinator', + kernelCoordinator: 'kernel-coordinator' +}); + +function normalizeText(value) { + if (value == null) { + return ''; + } + return String(value).trim(); +} + +function toOptionalText(value) { + const normalized = normalizeText(value); + return normalized || null; +} + +function normalizeAgentClass(value) { + const normalized = normalizeText(value).toLowerCase(); + if (['sagan', 'subagent', 'other'].includes(normalized)) { + return normalized; + } + return 'subagent'; +} + +function normalizeCellClass(value) { + const normalized = normalizeText(value).toLowerCase(); + if (Object.values(CELL_CLASS).includes(normalized)) { + return normalized; + } + return DEFAULT_CELL_CLASS; +} + +function nowIso(now = new Date()) { + return now.toISOString(); +} + +function uniqueId(prefix = 'id', now = Date.now()) { + return `${prefix}-${now}-${Math.random().toString(16).slice(2, 10)}`; +} + +function sanitizeCellId(cellId) { + return normalizeText(cellId).replace(/[^a-zA-Z0-9._-]+/g, '__'); +} + +function resolveDefaultLeaseRoot(options = {}) { + try { + return path.join( + resolveGitAdminPaths({ + cwd: options.repoRoot || REPO_ROOT, + env: options.env || process.env, + spawnSyncFn: options.spawnSyncFn || spawnSync + }).gitCommonDir, + 'execution-cell-leases' + ); + } catch { + return path.join(options.repoRoot || REPO_ROOT, '.git', 'execution-cell-leases'); + } +} + +export function defaultLeaseRoot(options = {}) { + return options.leaseRoot || process.env.EXECUTION_CELL_LEASE_ROOT || resolveDefaultLeaseRoot(options); +} + +export function leasePathForCell(cellId, leaseRoot = defaultLeaseRoot()) { + return path.join(leaseRoot, `${sanitizeCellId(cellId)}.json`); +} + +async function ensureParentDir(filePath) { + await fs.mkdir(path.dirname(filePath), { recursive: true }); +} + +async function readJsonIfPresent(filePath) { + try { + return JSON.parse(await fs.readFile(filePath, 'utf8')); + } catch (error) { + if (error?.code === 'ENOENT') { + return null; + } + throw error; + } +} + +async function writeJsonAtomic(filePath, payload) { + await ensureParentDir(filePath); + const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`; + const body = `${JSON.stringify(payload, null, 2)}\n`; + await fs.writeFile(tempPath, body, 'utf8'); + try { + await fs.rename(tempPath, filePath); + } finally { + await fs.rm(tempPath, { force: true }); + } +} + +function normalizeCapabilities(capabilities = []) { + const values = []; + for (const entry of capabilities) { + const normalized = normalizeText(entry); + if (normalized) { + values.push(normalized); + } + } + return [...new Set(values)]; +} + +function isPremiumDualLaneRequest(capabilities = []) { + const set = new Set(normalizeCapabilities(capabilities)); + return set.has(DOCKER_LANE_CAPABILITY) && set.has(NATIVE_LV32_CAPABILITY); +} + +function isWindowsNativeTestStandPlaneBinding(planeBinding) { + const normalized = normalizeText(planeBinding).toLowerCase(); + if (!normalized) { + return true; + } + return normalized === 'dual-plane-parity' || normalized.startsWith('native-labview-'); +} + +function resolveHeartbeatTimestamp(lease) { + return ( + lease?.heartbeatAt || + lease?.commit?.committedAt || + lease?.grant?.grantedAt || + lease?.request?.requestedAt || + null + ); +} + +function leaseAgeSeconds(lease, nowMs = Date.now()) { + const timestamp = resolveHeartbeatTimestamp(lease); + if (!timestamp) { + return Number.POSITIVE_INFINITY; + } + const parsed = Date.parse(timestamp); + if (!Number.isFinite(parsed)) { + return Number.POSITIVE_INFINITY; + } + return Math.max(0, (nowMs - parsed) / 1000); +} + +function resolveTtlSeconds(lease) { + return Number.isInteger(lease?.grant?.ttlSeconds) ? lease.grant.ttlSeconds : DEFAULT_TTL_SECONDS; +} + +export function isExecutionCellLeaseStale(lease, nowMs = Date.now()) { + if (!lease || lease.state === 'released') { + return false; + } + return leaseAgeSeconds(lease, nowMs) > resolveTtlSeconds(lease); +} + +function buildHostContext(hostPlaneReport) { + const fingerprint = hostPlaneReport?.host?.osFingerprint; + if (!fingerprint || typeof fingerprint !== 'object') { + return { + context: null, + observations: ['host-os-fingerprint-missing'] + }; + } + + return { + context: { + isolatedLaneGroupId: toOptionalText(fingerprint.isolatedLaneGroupId), + fingerprintSha256: toOptionalText(fingerprint.fingerprintSha256), + platform: toOptionalText(fingerprint.platform), + computerName: toOptionalText(hostPlaneReport?.host?.computerName), + canonical: { + version: toOptionalText(fingerprint?.canonical?.version), + buildNumber: toOptionalText(fingerprint?.canonical?.buildNumber), + ubr: Number.isInteger(fingerprint?.canonical?.ubr) ? fingerprint.canonical.ubr : null + } + }, + observations: [] + }; +} + +function resolveOperatorProfile(profile, explicitOperatorId) { + const operators = Array.isArray(profile?.operators) ? profile.operators : []; + const desiredId = toOptionalText(explicitOperatorId) || toOptionalText(profile?.defaultOperatorId); + const activeOperators = operators.filter((entry) => entry?.active !== false); + const resolved = + activeOperators.find((entry) => normalizeText(entry?.id) === normalizeText(desiredId)) || + activeOperators[0] || + null; + + return { + operatorId: toOptionalText(resolved?.id) || desiredId, + currency: toOptionalText(profile?.currency) || 'USD', + laborRateUsdPerHour: Number.isFinite(resolved?.laborRateUsdPerHour) + ? Number(resolved.laborRateUsdPerHour) + : null + }; +} + +function buildRequestRecord({ + cellId, + agentId, + agentClass, + cellClass, + suiteClass, + planeBinding, + harnessKind, + capabilities, + operatorId, + operatorAuthorizationRef, + workingRoot, + artifactRoot, + now, + requestId, + hostContext +}) { + const requestedCapabilities = normalizeCapabilities(capabilities); + const premiumDualLaneRequested = isPremiumDualLaneRequest(requestedCapabilities); + const generatedAt = nowIso(now); + return { + schema: EXECUTION_CELL_LEASE_SCHEMA, + generatedAt, + cellId, + resourceKind: 'execution-cell', + state: 'requested', + sequence: 1, + heartbeatAt: generatedAt, + host: hostContext, + request: { + requestId, + requestedAt: generatedAt, + agentId, + agentClass, + cellClass, + suiteClass: toOptionalText(suiteClass), + planeBinding: toOptionalText(planeBinding), + harnessKind: toOptionalText(harnessKind) || DEFAULT_HARNESS_KIND, + capabilities: requestedCapabilities, + premiumDualLaneRequested, + operatorId: toOptionalText(operatorId), + operatorAuthorizationRef: toOptionalText(operatorAuthorizationRef), + workingRoot: toOptionalText(workingRoot), + artifactRoot: toOptionalText(artifactRoot) + }, + grant: null, + commit: null, + release: null + }; +} + +function buildGrantDecision(lease, operatorProfile, nowMs = Date.now()) { + const reasons = []; + const stale = isExecutionCellLeaseStale(lease, nowMs); + const leaseState = toOptionalText(lease?.state); + const requestedCapabilities = normalizeCapabilities(lease?.request?.capabilities); + const premiumDualLaneRequested = lease?.request?.premiumDualLaneRequested === true; + const requestedCellClass = normalizeCellClass(lease?.request?.cellClass); + const requestedHarnessKind = toOptionalText(lease?.request?.harnessKind) || DEFAULT_HARNESS_KIND; + const requestedPlaneBinding = toOptionalText(lease?.request?.planeBinding); + if (leaseState === 'active' && !stale) { + reasons.push('execution-cell-already-active'); + } + if ( + requestedHarnessKind === DEFAULT_HARNESS_KIND && + !isWindowsNativeTestStandPlaneBinding(requestedPlaneBinding) + ) { + reasons.push('teststand-windows-native-only'); + } + if (requestedCellClass === CELL_CLASS.kernelCoordinator && lease?.request?.agentClass !== 'sagan') { + reasons.push('kernel-cell-sagan-only'); + } + if (premiumDualLaneRequested && lease?.request?.agentClass !== 'sagan') { + reasons.push('premium-sagan-only'); + } + if (premiumDualLaneRequested && requestedCellClass !== CELL_CLASS.kernelCoordinator) { + reasons.push('premium-kernel-cell-required'); + } + if (premiumDualLaneRequested && !toOptionalText(lease?.request?.operatorAuthorizationRef)) { + reasons.push('operator-authorization-required'); + } + if (!Number.isFinite(operatorProfile?.laborRateUsdPerHour)) { + reasons.push('operator-cost-rate-unavailable'); + } + + const billableRateMultiplier = premiumDualLaneRequested ? PREMIUM_RATE_MULTIPLIER : ORDINARY_RATE_MULTIPLIER; + return { + allowed: reasons.length === 0, + reasons, + premiumDualLaneRequested, + premiumSaganMode: premiumDualLaneRequested, + policyDecision: premiumDualLaneRequested ? 'sagan-premium-dual-lane' : 'ordinary-execution-cell', + grantedCapabilities: requestedCapabilities, + billableRateMultiplier, + billableRateUsdPerHour: Number.isFinite(operatorProfile?.laborRateUsdPerHour) + ? Number((operatorProfile.laborRateUsdPerHour * billableRateMultiplier).toFixed(3)) + : null + }; +} + +function buildBaseReport(action, cellId, leasePath, generatedAt, lease, extras = {}) { + const stale = isExecutionCellLeaseStale(lease, Date.parse(generatedAt)); + return { + schema: EXECUTION_CELL_LEASE_REPORT_SCHEMA, + generatedAt, + action, + cellId, + leasePath, + lease, + summary: { + leaseState: lease?.state ?? null, + leaseId: toOptionalText(lease?.grant?.leaseId), + holder: toOptionalText(lease?.request?.agentId), + agentClass: toOptionalText(lease?.request?.agentClass), + cellClass: toOptionalText(lease?.request?.cellClass), + harnessKind: toOptionalText(lease?.request?.harnessKind), + harnessInstanceId: toOptionalText(lease?.commit?.harnessInstanceId), + suiteClass: toOptionalText(lease?.request?.suiteClass), + planeBinding: toOptionalText(lease?.request?.planeBinding), + premiumSaganMode: + lease?.grant?.premiumSaganMode === true || lease?.request?.premiumDualLaneRequested === true, + billableRateMultiplier: Number.isFinite(lease?.grant?.billableRateMultiplier) + ? lease.grant.billableRateMultiplier + : null, + billableRateUsdPerHour: Number.isFinite(lease?.grant?.billableRateUsdPerHour) + ? lease.grant.billableRateUsdPerHour + : null, + operatorAuthorizationRef: toOptionalText(lease?.request?.operatorAuthorizationRef), + isolatedLaneGroupId: toOptionalText(lease?.host?.isolatedLaneGroupId), + fingerprintSha256: toOptionalText(lease?.host?.fingerprintSha256), + linkedDockerLaneId: toOptionalText(lease?.commit?.dockerLaneId), + linkedDockerLaneLeaseId: toOptionalText(lease?.commit?.dockerLaneLeaseId), + workingRoot: toOptionalText(lease?.commit?.workingRoot) || toOptionalText(lease?.request?.workingRoot), + artifactRoot: toOptionalText(lease?.commit?.artifactRoot) || toOptionalText(lease?.request?.artifactRoot), + isStale: stale, + ageSeconds: Number.isFinite(leaseAgeSeconds(lease, Date.parse(generatedAt))) + ? Number(leaseAgeSeconds(lease, Date.parse(generatedAt)).toFixed(3)) + : null, + ttlSeconds: lease ? resolveTtlSeconds(lease) : null, + denialReasons: [], + observations: [] + }, + ...extras + }; +} + +function resolveDockerLaneLinkContext(report) { + if (!report || typeof report !== 'object') { + return null; + } + const handshake = report.handshake; + if (!handshake || typeof handshake !== 'object') { + return null; + } + return { + laneId: toOptionalText(report?.laneId) || toOptionalText(handshake?.laneId), + leaseId: toOptionalText(report?.summary?.leaseId) || toOptionalText(handshake?.grant?.leaseId), + holder: toOptionalText(report?.summary?.holder) || toOptionalText(handshake?.request?.agentId), + isolatedLaneGroupId: + toOptionalText(report?.summary?.isolatedLaneGroupId) || toOptionalText(handshake?.host?.isolatedLaneGroupId), + fingerprintSha256: + toOptionalText(report?.summary?.fingerprintSha256) || toOptionalText(handshake?.host?.fingerprintSha256) + }; +} + +function validateDockerLaneLink(lease, linkedDockerLane) { + if (!linkedDockerLane) { + return ['docker-lane-report-invalid']; + } + + const reasons = []; + if (!linkedDockerLane.laneId) { + reasons.push('docker-lane-id-missing'); + } + if (!linkedDockerLane.leaseId) { + reasons.push('docker-lane-lease-id-missing'); + } + if (!linkedDockerLane.holder) { + reasons.push('docker-lane-holder-missing'); + } + if (!linkedDockerLane.isolatedLaneGroupId || !linkedDockerLane.fingerprintSha256) { + reasons.push('docker-lane-host-fingerprint-missing'); + } + + const leaseHolder = toOptionalText(lease?.request?.agentId); + if (leaseHolder && linkedDockerLane.holder && linkedDockerLane.holder !== leaseHolder) { + reasons.push('docker-lane-owner-mismatch'); + } + + const leaseIsolatedLaneGroupId = toOptionalText(lease?.host?.isolatedLaneGroupId); + const leaseFingerprintSha256 = toOptionalText(lease?.host?.fingerprintSha256); + if ( + leaseIsolatedLaneGroupId && + linkedDockerLane.isolatedLaneGroupId && + linkedDockerLane.isolatedLaneGroupId !== leaseIsolatedLaneGroupId + ) { + reasons.push('docker-lane-isolated-lane-group-mismatch'); + } + if ( + leaseFingerprintSha256 && + linkedDockerLane.fingerprintSha256 && + linkedDockerLane.fingerprintSha256 !== leaseFingerprintSha256 + ) { + reasons.push('docker-lane-host-fingerprint-mismatch'); + } + + return reasons; +} + +function parseArgs(argv) { + const result = { + action: 'inspect', + capabilities: [], + artifactPaths: [] + }; + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (token === '--action') { + result.action = argv[++index]; + } else if (token === '--cell-id') { + result.cellId = argv[++index]; + } else if (token === '--agent-id') { + result.agentId = argv[++index]; + } else if (token === '--agent-class') { + result.agentClass = argv[++index]; + } else if (token === '--cell-class') { + result.cellClass = argv[++index]; + } else if (token === '--suite-class') { + result.suiteClass = argv[++index]; + } else if (token === '--plane-binding') { + result.planeBinding = argv[++index]; + } else if (token === '--harness-kind') { + result.harnessKind = argv[++index]; + } else if (token === '--working-root') { + result.workingRoot = argv[++index]; + } else if (token === '--artifact-root') { + result.artifactRoot = argv[++index]; + } else if (token === '--docker-lane-report-path') { + result.dockerLaneReportPath = argv[++index]; + } else if (token === '--lease-id') { + result.leaseId = argv[++index]; + } else if (token === '--harness-instance-id') { + result.harnessInstanceId = argv[++index]; + } else if (token === '--operator-id') { + result.operatorId = argv[++index]; + } else if (token === '--operator-authorization-ref') { + result.operatorAuthorizationRef = argv[++index]; + } else if (token === '--host-plane-report-path') { + result.hostPlaneReportPath = argv[++index]; + } else if (token === '--operator-cost-profile-path') { + result.operatorCostProfilePath = argv[++index]; + } else if (token === '--output-path') { + result.outputPath = argv[++index]; + } else if (token === '--lease-root') { + result.leaseRoot = argv[++index]; + } else if (token === '--capability') { + result.capabilities.push(argv[++index]); + } else if (token === '--premium-dual-lane') { + result.capabilities.push(DOCKER_LANE_CAPABILITY); + result.capabilities.push(NATIVE_LV32_CAPABILITY); + } else if (token === '--artifact-path') { + result.artifactPaths.push(argv[++index]); + } + } + return result; +} + +async function loadOptionalJson(filePath) { + if (!filePath) { + return null; + } + return readJsonIfPresent(filePath); +} + +export async function runExecutionCellLease(options = {}) { + const action = toOptionalText(options.action) || 'inspect'; + const cellId = toOptionalText(options.cellId); + if (!cellId) { + throw new Error('cellId is required'); + } + + const repoRoot = options.repoRoot || REPO_ROOT; + const leaseRoot = defaultLeaseRoot({ ...options, repoRoot }); + const leasePath = leasePathForCell(cellId, leaseRoot); + const outputPath = path.resolve(repoRoot, options.outputPath || DEFAULT_OUTPUT_PATH); + const hostPlaneReportPath = path.resolve(repoRoot, options.hostPlaneReportPath || DEFAULT_HOST_PLANE_REPORT_PATH); + const operatorCostProfilePath = path.resolve( + repoRoot, + options.operatorCostProfilePath || DEFAULT_OPERATOR_COST_PROFILE_PATH + ); + const now = options.now || new Date(); + const generatedAt = nowIso(now); + + const hostPlaneReport = await loadOptionalJson(hostPlaneReportPath); + const operatorCostProfile = await loadOptionalJson(operatorCostProfilePath); + const hostContext = buildHostContext(hostPlaneReport); + const operatorProfile = resolveOperatorProfile(operatorCostProfile, options.operatorId); + const agentClass = normalizeAgentClass(options.agentClass); + const cellClass = normalizeCellClass(options.cellClass); + const capabilities = normalizeCapabilities(options.capabilities); + + const existing = await readJsonIfPresent(leasePath); + + if (action === 'request') { + const lease = buildRequestRecord({ + cellId, + agentId: toOptionalText(options.agentId), + agentClass, + cellClass, + suiteClass: options.suiteClass, + planeBinding: options.planeBinding, + harnessKind: options.harnessKind, + capabilities, + operatorId: operatorProfile.operatorId, + operatorAuthorizationRef: options.operatorAuthorizationRef, + workingRoot: options.workingRoot, + artifactRoot: options.artifactRoot, + now, + requestId: uniqueId('request', now.getTime()), + hostContext: hostContext.context + }); + + await writeJsonAtomic(leasePath, lease); + const report = buildBaseReport(action, cellId, leasePath, generatedAt, lease, { + status: STATUS.requested, + policy: { + operatorId: operatorProfile.operatorId, + currency: operatorProfile.currency, + laborRateUsdPerHour: operatorProfile.laborRateUsdPerHour + } + }); + report.summary.observations.push(...hostContext.observations); + await writeJsonAtomic(outputPath, report); + return report; + } + + if (!existing) { + const report = buildBaseReport(action, cellId, leasePath, generatedAt, null, { + status: STATUS.notFound, + policy: { + operatorId: operatorProfile.operatorId, + currency: operatorProfile.currency, + laborRateUsdPerHour: operatorProfile.laborRateUsdPerHour + } + }); + report.summary.observations.push('execution-cell-lease-missing'); + await writeJsonAtomic(outputPath, report); + return report; + } + + if (action === 'grant') { + const decision = buildGrantDecision(existing, operatorProfile, now.getTime()); + if (!decision.allowed) { + const report = buildBaseReport(action, cellId, leasePath, generatedAt, existing, { + status: decision.reasons.includes('execution-cell-already-active') ? STATUS.busy : STATUS.denied, + policy: { + operatorId: operatorProfile.operatorId, + currency: operatorProfile.currency, + laborRateUsdPerHour: operatorProfile.laborRateUsdPerHour + } + }); + report.summary.denialReasons.push(...decision.reasons); + await writeJsonAtomic(outputPath, report); + return report; + } + + const lease = { + ...existing, + generatedAt, + state: 'granted', + sequence: Number.isInteger(existing.sequence) ? existing.sequence + 1 : 2, + heartbeatAt: generatedAt, + host: hostContext.context || existing.host || null, + grant: { + grantedAt: generatedAt, + grantor: toOptionalText(options.grantor) || 'execution-cell-governor', + leaseId: uniqueId('lease', now.getTime()), + ttlSeconds: Number.isInteger(options.ttlSeconds) ? options.ttlSeconds : DEFAULT_TTL_SECONDS, + premiumDualLaneRequested: decision.premiumDualLaneRequested, + premiumSaganMode: decision.premiumSaganMode, + policyDecision: decision.policyDecision, + grantedCapabilities: decision.grantedCapabilities, + billableRateMultiplier: decision.billableRateMultiplier, + billableRateUsdPerHour: decision.billableRateUsdPerHour + } + }; + + await writeJsonAtomic(leasePath, lease); + const report = buildBaseReport(action, cellId, leasePath, generatedAt, lease, { + status: STATUS.granted, + policy: { + operatorId: operatorProfile.operatorId, + currency: operatorProfile.currency, + laborRateUsdPerHour: operatorProfile.laborRateUsdPerHour + } + }); + report.summary.observations.push(...hostContext.observations); + await writeJsonAtomic(outputPath, report); + return report; + } + + if (action === 'commit' || action === 'heartbeat') { + const leaseId = toOptionalText(options.leaseId); + if (leaseId && leaseId !== toOptionalText(existing?.grant?.leaseId)) { + const report = buildBaseReport(action, cellId, leasePath, generatedAt, existing, { + status: STATUS.mismatch, + policy: { + operatorId: operatorProfile.operatorId, + currency: operatorProfile.currency, + laborRateUsdPerHour: operatorProfile.laborRateUsdPerHour + } + }); + report.summary.denialReasons.push('lease-id-mismatch'); + await writeJsonAtomic(outputPath, report); + return report; + } + + if (toOptionalText(existing.state) !== 'granted' && toOptionalText(existing.state) !== 'active') { + const report = buildBaseReport(action, cellId, leasePath, generatedAt, existing, { + status: STATUS.invalidState, + policy: { + operatorId: operatorProfile.operatorId, + currency: operatorProfile.currency, + laborRateUsdPerHour: operatorProfile.laborRateUsdPerHour + } + }); + report.summary.denialReasons.push('execution-cell-not-granted'); + await writeJsonAtomic(outputPath, report); + return report; + } + + let linkedDockerLane = null; + if (action === 'commit' && toOptionalText(options.dockerLaneReportPath)) { + linkedDockerLane = resolveDockerLaneLinkContext( + await loadOptionalJson(path.resolve(repoRoot, options.dockerLaneReportPath)) + ); + const linkReasons = validateDockerLaneLink(existing, linkedDockerLane); + if (linkReasons.length > 0) { + const report = buildBaseReport(action, cellId, leasePath, generatedAt, existing, { + status: STATUS.mismatch, + policy: { + operatorId: operatorProfile.operatorId, + currency: operatorProfile.currency, + laborRateUsdPerHour: operatorProfile.laborRateUsdPerHour + } + }); + report.summary.denialReasons.push(...linkReasons); + await writeJsonAtomic(outputPath, report); + return report; + } + } + + const lease = { + ...existing, + generatedAt, + state: 'active', + sequence: Number.isInteger(existing.sequence) ? existing.sequence + 1 : 2, + heartbeatAt: generatedAt, + host: hostContext.context || existing.host || null, + commit: { + committedAt: generatedAt, + harnessInstanceId: + toOptionalText(options.harnessInstanceId) || toOptionalText(existing?.commit?.harnessInstanceId), + dockerLaneId: + linkedDockerLane?.laneId || toOptionalText(existing?.commit?.dockerLaneId), + dockerLaneLeaseId: + linkedDockerLane?.leaseId || toOptionalText(existing?.commit?.dockerLaneLeaseId), + workingRoot: toOptionalText(options.workingRoot) || toOptionalText(existing?.request?.workingRoot), + artifactRoot: toOptionalText(options.artifactRoot) || toOptionalText(existing?.request?.artifactRoot) + } + }; + + await writeJsonAtomic(leasePath, lease); + const report = buildBaseReport(action, cellId, leasePath, generatedAt, lease, { + status: action === 'commit' ? STATUS.committed : STATUS.renewed, + policy: { + operatorId: operatorProfile.operatorId, + currency: operatorProfile.currency, + laborRateUsdPerHour: operatorProfile.laborRateUsdPerHour + } + }); + report.summary.observations.push(...hostContext.observations); + if (linkedDockerLane?.laneId) { + report.summary.observations.push('linked-docker-lane-commit'); + } + await writeJsonAtomic(outputPath, report); + return report; + } + + if (action === 'release') { + const leaseId = toOptionalText(options.leaseId); + if (leaseId && leaseId !== toOptionalText(existing?.grant?.leaseId)) { + const report = buildBaseReport(action, cellId, leasePath, generatedAt, existing, { + status: STATUS.mismatch, + policy: { + operatorId: operatorProfile.operatorId, + currency: operatorProfile.currency, + laborRateUsdPerHour: operatorProfile.laborRateUsdPerHour + } + }); + report.summary.denialReasons.push('lease-id-mismatch'); + await writeJsonAtomic(outputPath, report); + return report; + } + + const lease = { + ...existing, + generatedAt, + state: 'released', + sequence: Number.isInteger(existing.sequence) ? existing.sequence + 1 : 2, + heartbeatAt: generatedAt, + release: { + releasedAt: generatedAt, + artifactPaths: options.artifactPaths?.map((entry) => normalizeText(entry)).filter(Boolean) || [] + } + }; + await writeJsonAtomic(leasePath, lease); + const report = buildBaseReport(action, cellId, leasePath, generatedAt, lease, { + status: STATUS.released, + policy: { + operatorId: operatorProfile.operatorId, + currency: operatorProfile.currency, + laborRateUsdPerHour: operatorProfile.laborRateUsdPerHour + } + }); + await writeJsonAtomic(outputPath, report); + return report; + } + + if (action === 'inspect') { + const stale = isExecutionCellLeaseStale(existing, now.getTime()); + const report = buildBaseReport(action, cellId, leasePath, generatedAt, existing, { + status: stale ? STATUS.stale : toOptionalText(existing.state) === 'released' ? STATUS.released : STATUS.active, + policy: { + operatorId: operatorProfile.operatorId, + currency: operatorProfile.currency, + laborRateUsdPerHour: operatorProfile.laborRateUsdPerHour + } + }); + await writeJsonAtomic(outputPath, report); + return report; + } + + throw new Error(`Unsupported action '${action}'`); +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + const report = await runExecutionCellLease(options); + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +} + +const isEntrypoint = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +if (isEntrypoint) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + }); +} diff --git a/tools/priority/github-comment-budget-hook.mjs b/tools/priority/github-comment-budget-hook.mjs new file mode 100644 index 000000000..78cfe2735 --- /dev/null +++ b/tools/priority/github-comment-budget-hook.mjs @@ -0,0 +1,475 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { runMaterializeAgentCostRollup } from './materialize-agent-cost-rollup.mjs'; + +export const REPORT_SCHEMA = 'priority/github-comment-budget-hook@v1'; +export const POLICY_SCHEMA = 'priority/github-comment-budget-hook-policy@v1'; +export const DEFAULT_POLICY_PATH = path.join('tools', 'policy', 'github-comment-budget-hook.json'); +export const DEFAULT_OUTPUT_PATH = path.join('tests', 'results', '_agent', 'cost', 'github-comment-budget-hook.json'); +export const DEFAULT_MARKDOWN_OUTPUT_PATH = path.join('tests', 'results', '_agent', 'cost', 'github-comment-budget-hook.md'); +export const COMMENT_HOOK_START_MARKER = ''; +export const COMMENT_HOOK_END_MARKER = ''; +export const COMMENT_HOOK_JSON_PREFIX = '` + ]; + + if (report.summary.status === 'blocked') { + const blockerCodes = report.blockers.map((entry) => `\`${entry.code}\``).join(', '); + lines.push(`_Budget hook_: unavailable (${blockerCodes || '`unknown-blocker`'}). Receipt: \`${report.source.outputPath ?? 'none'}\`.`); + } else { + const billingWindow = report.funding.billingWindow; + const reservedFunding = report.funding.reservedFunding; + const operatorBudgetText = report.summary.operatorBudgetCapUsd == null + ? 'operator cap unknown' + : `operator ${formatUsd(report.summary.operatorLaborObservedUsd)} of ${formatUsd(report.summary.operatorBudgetCapUsd)} cap (remaining ${report.summary.operatorBudgetRemainingStatus === 'lower-bound' ? '>=' : ''}${formatUsd(report.summary.operatorBudgetRemainingLowerBoundUsd)})`; + const billingWindowText = billingWindow?.invoiceTurnId + ? `window \`${billingWindow.invoiceTurnId}\` spent ${formatUsd(billingWindow.tokenSpendUsd)} remaining ${formatUsd(billingWindow.remainingUsd)}` + : 'window unavailable'; + const reserveText = reservedFunding.count > 0 + ? `; calibration reserve ${formatUsd(reservedFunding.totalReservedUsd)} across ${reservedFunding.count} held window(s)` + : ''; + const timingText = report.summary.operatorLaborMissingTurnCount > 0 + ? `; ${report.summary.operatorLaborMissingTurnCount} turn(s) still pending labor timing` + : ''; + lines.push(`_Budget hook_: blended lower bound ${formatUsd(report.summary.observedBlendedLowerBoundUsd)}; ${operatorBudgetText}; ${billingWindowText}; turns ${report.turns.totalTurns} total (${report.turns.liveTurnCount} live, ${report.turns.backgroundTurnCount} background)${timingText}${reserveText}. Receipt: \`${report.source.outputPath}\`.`); + } + + lines.push(COMMENT_HOOK_END_MARKER, ''); + return `${lines.join('\n').trimEnd()}\n`; +} + +export function stripExistingBudgetHook(body) { + const normalizedBody = normalizeText(body); + if (!normalizedBody.includes(COMMENT_HOOK_START_MARKER)) { + return normalizedBody; + } + const startIndex = normalizedBody.indexOf(COMMENT_HOOK_START_MARKER); + const endIndex = normalizedBody.indexOf(COMMENT_HOOK_END_MARKER, startIndex); + if (startIndex < 0 || endIndex < 0) { + return normalizedBody; + } + const prefix = normalizedBody.slice(0, startIndex).trimEnd(); + const suffix = normalizedBody.slice(endIndex + COMMENT_HOOK_END_MARKER.length).trimStart(); + if (prefix && suffix) { + return `${prefix}\n\n${suffix}`.trimEnd(); + } + return (prefix || suffix || '').trimEnd(); +} + +export function appendBudgetHook(body, hookMarkdown) { + const cleanBody = stripExistingBudgetHook(body); + const hook = normalizeText(hookMarkdown); + if (!hook) { + return cleanBody; + } + if (!cleanBody) { + return `${hook}\n`; + } + return `${cleanBody}\n\n${hook}\n`; +} + +export function buildGitHubCommentBudgetHookReport({ rollup, repository, targetKind, targetNumber, operatorBudgetCapUsd, reservedFunding, billingWindow, source, blockers, now }) { + const metrics = rollup?.summary?.metrics ?? {}; + const tokenSpendUsd = toNonNegativeNumber(metrics.totalUsd) ?? 0; + const operatorLaborObservedUsd = toNonNegativeNumber(metrics.operatorLaborUsd) ?? 0; + const operatorLaborMissingTurnCount = Number(metrics.operatorLaborMissingTurnCount ?? 0) || 0; + const observedBlendedLowerBoundUsd = roundUsd(tokenSpendUsd + operatorLaborObservedUsd) ?? 0; + const knownBlendedUsd = toNonNegativeNumber(metrics.blendedTotalUsd); + const operatorBudgetRemainingLowerBoundUsd = + operatorBudgetCapUsd == null ? null : roundUsd(Math.max(0, operatorBudgetCapUsd - operatorLaborObservedUsd)); + const operatorBudgetRemainingStatus = + operatorBudgetCapUsd == null ? 'unknown' : operatorLaborMissingTurnCount > 0 ? 'lower-bound' : 'observed'; + + const blocking = Array.isArray(blockers) ? blockers.filter(Boolean) : []; + const status = blocking.length > 0 ? 'blocked' : operatorLaborMissingTurnCount > 0 ? 'warn' : 'pass'; + const recommendation = + status === 'blocked' + ? 'repair-comment-budget-hook-inputs' + : operatorLaborMissingTurnCount > 0 + ? 'continue-observing-labor-timing' + : 'comment-budget-hook-ready'; + + return { + schema: REPORT_SCHEMA, + generatedAt: now.toISOString(), + repository, + target: { + kind: asOptional(targetKind) || 'unknown', + number: targetNumber ?? null + }, + summary: { + status, + recommendation, + tokenSpendUsd, + operatorLaborObservedUsd, + operatorLaborMissingTurnCount, + observedBlendedLowerBoundUsd, + knownBlendedUsd, + operatorBudgetCapUsd, + operatorBudgetRemainingLowerBoundUsd, + operatorBudgetRemainingStatus + }, + turns: { + totalTurns: Number(metrics.totalTurns ?? 0) || 0, + liveTurnCount: Number(metrics.liveTurnCount ?? 0) || 0, + backgroundTurnCount: Number(metrics.backgroundTurnCount ?? 0) || 0 + }, + funding: { + billingWindow, + reservedFunding + }, + source, + blockers: blocking + }; +} + +export function runGitHubCommentBudgetHook( + options, + { + now = new Date(), + readJsonFn = readJson, + writeJsonFn = writeJson, + writeTextFn = writeText, + runMaterializeAgentCostRollupFn = runMaterializeAgentCostRollup + } = {} +) { + const repoRoot = path.resolve(options.repoRoot || process.cwd()); + const { resolvedPolicyPath, policy } = loadPolicy(repoRoot, options.policyPath || DEFAULT_POLICY_PATH); + let costRollupPath = path.resolve(repoRoot, options.costRollupPath || asOptional(policy.costRollupPath) || DEFAULT_OUTPUT_PATH.replace('github-comment-budget-hook', 'agent-cost-rollup')); + let costRollupMaterialized = false; + let costRollupMaterializationReportPath = null; + const blockers = []; + + const shouldMaterialize = options.materialize ?? Boolean(policy.materializeCostRollup); + if (shouldMaterialize) { + try { + const materializeResult = runMaterializeAgentCostRollupFn({ + repoRoot, + repo: options.repo, + policyPath: asOptional(policy.materializationPolicyPath) || undefined, + costRollupPath, + outputPath: asOptional(policy.materializationReportPath) || undefined + }); + costRollupMaterialized = true; + costRollupPath = path.resolve(materializeResult.costRollupPath || costRollupPath); + costRollupMaterializationReportPath = safeRelative(repoRoot, materializeResult.outputPath); + } catch (error) { + blockers.push(createBlocker('cost-rollup-materialization-failed', error?.message || String(error))); + } + } + + let rollup = null; + if (!fs.existsSync(costRollupPath)) { + blockers.push(createBlocker('cost-rollup-missing', 'Agent cost rollup is missing.', safeRelative(repoRoot, costRollupPath))); + } else { + try { + rollup = readJsonFn(costRollupPath); + if (normalizeText(rollup?.schema) !== 'priority/agent-cost-rollup@v1') { + blockers.push(createBlocker('cost-rollup-schema-mismatch', 'Agent cost rollup schema must remain priority/agent-cost-rollup@v1.', safeRelative(repoRoot, costRollupPath))); + } + } catch (error) { + blockers.push(createBlocker('cost-rollup-unreadable', error?.message || String(error), safeRelative(repoRoot, costRollupPath))); + } + } + + const billingWindow = summarizeBillingWindow(rollup); + const reservedFunding = summarizeReservedFundingWindows(rollup, policy, billingWindow?.invoiceTurnId ?? null); + const repository = chooseTargetRepository(options.repo, rollup); + const operatorBudgetCapUsd = toNonNegativeNumber(policy.operatorBudgetCapUsd); + const outputPath = path.resolve(repoRoot, options.outputPath || asOptional(policy.outputPath) || DEFAULT_OUTPUT_PATH); + const markdownOutputPath = path.resolve(repoRoot, options.markdownOutputPath || asOptional(policy.markdownOutputPath) || DEFAULT_MARKDOWN_OUTPUT_PATH); + + const report = buildGitHubCommentBudgetHookReport({ + rollup, + repository, + targetKind: options.targetKind, + targetNumber: options.targetNumber, + operatorBudgetCapUsd, + reservedFunding, + billingWindow, + source: { + policyPath: safeRelative(repoRoot, resolvedPolicyPath), + costRollupPath: safeRelative(repoRoot, costRollupPath), + costRollupMaterialized, + costRollupMaterializationReportPath, + operatorCostProfilePath: asOptional(rollup?.summary?.provenance?.operatorProfiles?.[0]?.operatorProfilePath) || 'tools/policy/operator-cost-profile.json', + outputPath: safeRelative(repoRoot, outputPath), + markdownOutputPath: safeRelative(repoRoot, markdownOutputPath) + }, + blockers, + now + }); + + const markdown = buildMarkdown(report); + writeJsonFn(outputPath, report); + writeTextFn(markdownOutputPath, markdown); + + return { + report, + markdown, + outputPath, + markdownOutputPath + }; +} + +function printUsage() { + [ + 'Usage: node tools/priority/github-comment-budget-hook.mjs [options]', + '', + 'Builds a durable spend/budget hook for automation-authored GitHub comments.', + '', + ` --policy Policy path (default: ${DEFAULT_POLICY_PATH}).`, + ' --repo-root Repository root (default: cwd).', + ' --repo Repository slug override.', + ' --target-kind Comment target kind.', + ' --target-number Comment target number.', + ' --cost-rollup Cost rollup path override.', + ` --output JSON report path (default: ${DEFAULT_OUTPUT_PATH}).`, + ` --markdown-output Markdown hook path (default: ${DEFAULT_MARKDOWN_OUTPUT_PATH}).`, + ' --materialize Force cost-rollup materialization before read.', + ' --no-materialize Skip cost-rollup materialization.', + ' -h, --help Show this message.' + ].forEach((line) => console.log(line)); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + try { + const options = parseArgs(process.argv); + if (options.help) { + printUsage(); + process.exit(0); + } + const result = runGitHubCommentBudgetHook(options); + console.log(`[github-comment-budget-hook] wrote ${result.outputPath}`); + console.log(`[github-comment-budget-hook] markdown ${result.markdownOutputPath}`); + } catch (error) { + console.error(`[github-comment-budget-hook] ${error.message}`); + process.exit(1); + } +} diff --git a/tools/priority/lib/remote-utils.mjs b/tools/priority/lib/remote-utils.mjs index 2c60340c6..6f398a939 100644 --- a/tools/priority/lib/remote-utils.mjs +++ b/tools/priority/lib/remote-utils.mjs @@ -174,7 +174,13 @@ export function buildGraphqlArgs(query, variables = {}) { if (value == null) { continue; } - args.push('-f', `${key}=${String(value)}`); + const flag = + typeof value === 'boolean' || + typeof value === 'number' || + typeof value === 'bigint' + ? '-F' + : '-f'; + args.push(flag, `${key}=${String(value)}`); } return args; } diff --git a/tools/priority/pr-spend-projection.mjs b/tools/priority/pr-spend-projection.mjs index 8438284bc..93c6cb6a2 100644 --- a/tools/priority/pr-spend-projection.mjs +++ b/tools/priority/pr-spend-projection.mjs @@ -6,6 +6,12 @@ import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { runMaterializeAgentCostRollup } from './materialize-agent-cost-rollup.mjs'; +import { + COMMENT_HOOK_END_MARKER, + COMMENT_HOOK_START_MARKER, + appendBudgetHook, + runGitHubCommentBudgetHook +} from './github-comment-budget-hook.mjs'; export const REPORT_SCHEMA = 'priority/pr-spend-projection@v1'; export const COST_ROLLUP_SCHEMA = 'priority/agent-cost-rollup@v1'; @@ -292,6 +298,16 @@ function buildMarkdown(report) { return `${lines.join('\n').trimEnd()}\n`; } +function buildBudgetHookFallback(error) { + const message = normalizeText(error?.message ?? error); + return [ + COMMENT_HOOK_START_MARKER, + `_Budget hook_: unavailable (\`comment-budget-hook-generation-failed\`): ${message || 'unknown error'}.`, + COMMENT_HOOK_END_MARKER, + '' + ].join('\n'); +} + function findExistingSpendComment(repo, prNumber, marker = COMMENT_MARKER) { const result = spawnSync( 'gh', @@ -726,7 +742,8 @@ export function runPrSpendProjection( upsertCommentFn = upsertPullRequestComment, lookupCurrentLoginFn = lookupCurrentLogin, resolvePullRequestContextFn = resolvePullRequestContext, - materializeAgentCostRollupFn = runMaterializeAgentCostRollup + materializeAgentCostRollupFn = runMaterializeAgentCostRollup, + runGitHubCommentBudgetHookFn = runGitHubCommentBudgetHook } = {} ) { const repo = resolveRepoSlugFn(options.repo); @@ -783,7 +800,20 @@ export function runPrSpendProjection( report.source.costRollupPath = safeRelative(resolvedCostRollupPath); report.source.costRollupMaterialized = costRollupMaterialized; report.source.costRollupMaterializationReportPath = materializationReportPath ? safeRelative(materializationReportPath) : null; - const markdown = buildMarkdown(report); + let budgetHookMarkdown = ''; + try { + budgetHookMarkdown = runGitHubCommentBudgetHookFn({ + repoRoot: process.cwd(), + repo, + costRollupPath: resolvedCostRollupPath, + materialize: false, + targetKind: 'pr', + targetNumber: options.prNumber ?? prContext?.number ?? null + })?.markdown ?? ''; + } catch (error) { + budgetHookMarkdown = buildBudgetHookFallback(error); + } + const markdown = appendBudgetHook(buildMarkdown(report), budgetHookMarkdown); const markdownPath = writeTextFn(options.markdownOutputPath || DEFAULT_MARKDOWN_OUTPUT_PATH, markdown); report.source.markdownPath = safeRelative(markdownPath); diff --git a/tools/priority/release-conductor.mjs b/tools/priority/release-conductor.mjs index 444f7f438..5dea1b441 100644 --- a/tools/priority/release-conductor.mjs +++ b/tools/priority/release-conductor.mjs @@ -13,6 +13,9 @@ export const DEFAULT_QUEUE_REPORT_PATH = path.join('tests', 'results', '_agent', export const DEFAULT_POLICY_SNAPSHOT_PATH = path.join('tests', 'results', '_agent', 'policy', 'policy-state-snapshot.json'); export const DEFAULT_DWELL_MINUTES = 60; export const DEFAULT_QUARANTINE_STALE_HOURS = 24; +export const RELEASE_PUBLICATION_WORKFLOW = 'release.yml'; +export const RELEASE_PUBLICATION_WORKFLOW_REF = 'develop'; +export const RELEASE_PUBLICATION_TAG_INPUT = 'release_tag'; const REQUIRED_DWELL_WORKFLOWS = Object.freeze([ { name: 'Validate', file: 'validate.yml' }, @@ -24,6 +27,7 @@ function printUsage() { console.log(''); console.log('Options:'); console.log(' --apply Apply release mutation (default is --dry-run).'); + console.log(' --repair-existing-tag Repair an existing authoritative tag by recreating it as a signed annotated tag.'); console.log(` --report Write report JSON (default: ${DEFAULT_REPORT_PATH}).`); console.log(` --queue-report Queue supervisor report path (default: ${DEFAULT_QUEUE_REPORT_PATH}).`); console.log(` --policy-snapshot Policy snapshot path (default: ${DEFAULT_POLICY_SNAPSHOT_PATH}).`); @@ -96,6 +100,7 @@ export function parseArgs(argv = process.argv) { const options = { apply: false, dryRun: true, + repairExistingTag: false, reportPath: DEFAULT_REPORT_PATH, queueReportPath: DEFAULT_QUEUE_REPORT_PATH, policySnapshotPath: DEFAULT_POLICY_SNAPSHOT_PATH, @@ -119,6 +124,10 @@ export function parseArgs(argv = process.argv) { options.dryRun = false; continue; } + if (token === '--repair-existing-tag') { + options.repairExistingTag = true; + continue; + } if (token === '--dry-run') { options.apply = false; options.dryRun = true; @@ -297,11 +306,46 @@ export function evaluateQueueHealthGate(queueReportEnvelope) { queueReport?.throughputController?.mode ?? queueReport?.adaptiveInflight?.mode ?? null; + const pausedReasons = Array.isArray(queueReport?.pausedReasons) ? queueReport.pausedReasons : []; + const runtimeTotals = + queueReport?.runtimeFleet && typeof queueReport.runtimeFleet === 'object' ? queueReport.runtimeFleet.totals : null; + const mergeQueueOccupancy = + queueReport?.queueInventory?.mergeQueueOccupancy ?? + queueReport?.summary?.mergeQueueOccupancy ?? + null; + const readyQueuedCount = + queueReport?.queueInventory?.readyQueuedCount ?? + queueReport?.summary?.readyQueuedCount ?? + null; + const quarantinedCount = queueReport?.summary?.quarantinedCount ?? null; + const idleSuccessRatePause = + paused && + controllerMode === 'stabilize' && + pausedReasons.length > 0 && + pausedReasons.every((reason) => reason === 'success-rate-below-threshold') && + Number(mergeQueueOccupancy ?? 0) === 0 && + Number(readyQueuedCount ?? 0) === 0 && + Number(runtimeTotals?.queued ?? 0) === 0 && + Number(runtimeTotals?.inProgress ?? 0) === 0 && + Number(runtimeTotals?.stalled ?? 0) === 0 && + Number(quarantinedCount ?? 0) === 0; const reasons = []; + if (idleSuccessRatePause) { + reasons.push('release-safe-idle-queue-pause'); + } if (paused) reasons.push('queue-paused'); if (controllerMode === 'stabilize') reasons.push('queue-stabilize-mode'); + if (idleSuccessRatePause) { + return { + status: 'pass', + reasons: ['release-safe-idle-queue-pause'], + paused, + controllerMode + }; + } + return { status: reasons.length === 0 ? 'pass' : 'fail', reasons, @@ -350,6 +394,7 @@ export function evaluateQuarantineGate({ return { status: 'fail', reasons: ['queue-report-unavailable'], + staleHours, staleCount: null, activeCount: null, staleEntries: [] @@ -443,17 +488,44 @@ function fetchWorkflowRunsByName({ runGhJsonFn, repository, branch, sampleSize, }; } -function detectSigningMaterial({ runCommandFn, repoRoot }) { +function detectSigningMaterial({ runCommandFn, repoRoot, environment = process.env }) { const keyResult = runCommandFn('git', ['config', '--get', 'user.signingkey'], { cwd: repoRoot, allowFailure: true }); const signingKey = asOptional(keyResult.stdout); + const formatResult = runCommandFn('git', ['config', '--get', 'gpg.format'], { + cwd: repoRoot, + allowFailure: true + }); + const nameResult = runCommandFn('git', ['config', '--get', 'user.name'], { + cwd: repoRoot, + allowFailure: true + }); + const emailResult = runCommandFn('git', ['config', '--get', 'user.email'], { + cwd: repoRoot, + allowFailure: true + }); + const configuredFormat = asOptional(formatResult.stdout); + const backend = asOptional(environment.RELEASE_TAG_SIGNING_BACKEND) ?? configuredFormat ?? 'openpgp'; + const source = signingKey ? asOptional(environment.RELEASE_TAG_SIGNING_SOURCE) ?? 'git-config' : 'missing'; + const identityName = asOptional(nameResult.stdout); + const identityEmail = asOptional(emailResult.stdout); + const identityAvailable = Boolean(identityName && identityEmail); return { available: Boolean(signingKey), signingKey, - source: signingKey ? 'git-config' : 'missing' + source, + backend, + identity: { + available: identityAvailable, + name: identityName, + email: identityEmail, + source: identityAvailable ? asOptional(environment.RELEASE_TAG_SIGNING_IDENTITY_SOURCE) ?? 'git-config' : 'missing', + login: asOptional(environment.RELEASE_TAG_SIGNING_IDENTITY_LOGIN), + accountId: asOptional(environment.RELEASE_TAG_SIGNING_IDENTITY_ID) + } }; } @@ -463,6 +535,119 @@ function resolveTargetTag(version) { return normalized.startsWith('v') ? normalized : `v${normalized}`; } +function inspectLocalTag({ repoRoot, tagRef, runCommandFn }) { + if (!tagRef) { + return { + present: false, + objectOid: null + }; + } + + const result = runCommandFn('git', ['rev-parse', '--verify', '--quiet', `refs/tags/${tagRef}`], { + cwd: repoRoot, + allowFailure: true + }); + return { + present: result.status === 0, + objectOid: asOptional(result.stdout) + }; +} + +function inspectRemoteTag({ repoRoot, remoteName, tagRef, runCommandFn }) { + if (!remoteName || !tagRef) { + return { + exists: false, + refName: tagRef ? `refs/tags/${tagRef}` : null, + objectOid: null, + targetCommitOid: null, + annotated: null, + lookupError: null + }; + } + + const refName = `refs/tags/${tagRef}`; + const result = runCommandFn('git', ['ls-remote', '--tags', remoteName, refName, `${refName}^{}`], { + cwd: repoRoot, + allowFailure: true + }); + if (result.status !== 0) { + return { + exists: false, + refName, + objectOid: null, + targetCommitOid: null, + annotated: null, + lookupError: asOptional(result.stderr) ?? asOptional(result.stdout) ?? `git ls-remote failed (${result.status})` + }; + } + + let objectOid = null; + let peeledOid = null; + for (const rawLine of String(result.stdout ?? '').split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line) { + continue; + } + const [oid, resolvedRef] = line.split(/\s+/, 2); + if (resolvedRef === refName) { + objectOid = oid; + } else if (resolvedRef === `${refName}^{}`) { + peeledOid = oid; + } + } + + const exists = Boolean(objectOid); + return { + exists, + refName, + objectOid, + targetCommitOid: peeledOid ?? objectOid, + annotated: exists ? Boolean(peeledOid) : null, + lookupError: null + }; +} + +function createRepairState({ requested, remoteTag = null, localTag = null }) { + return { + requested: Boolean(requested), + status: 'not-requested', + remoteTagRef: remoteTag?.refName ?? null, + remoteTagExists: Boolean(remoteTag?.exists), + remoteTagAnnotated: remoteTag?.annotated ?? null, + remoteTagObjectOid: remoteTag?.objectOid ?? null, + remoteTargetCommitOid: remoteTag?.targetCommitOid ?? null, + localTagPresent: Boolean(localTag?.present), + localTagDeleted: false, + tagRecreated: false, + pushLeaseExpectedOid: remoteTag?.objectOid ?? null, + lookupError: remoteTag?.lookupError ?? null + }; +} + +function resolveTagPushRemote({ repoRoot, repository, runCommandFn }) { + for (const remoteName of ['upstream', 'origin']) { + const remoteResult = runCommandFn('git', ['config', '--get', `remote.${remoteName}.url`], { + cwd: repoRoot, + allowFailure: true + }); + const remoteUrl = asOptional(remoteResult.stdout); + const remoteSlug = parseRemoteUrl(remoteUrl); + if (remoteSlug && remoteSlug === repository) { + return { + remoteName, + remoteSlug, + source: 'matching-remote-url' + }; + } + } + + return { + remoteName: null, + remoteSlug: null, + source: 'missing' + }; +} + async function writeReport(filePath, payload) { const resolved = path.resolve(filePath); await mkdir(path.dirname(resolved), { recursive: true }); @@ -567,11 +752,90 @@ export async function runReleaseConductor(options = {}) { }); } - const signingMaterial = detectSigningMaterial({ runCommandFn, repoRoot }); + const signingMaterial = detectSigningMaterial({ runCommandFn, repoRoot, environment }); const targetTag = resolveTargetTag(args.version); let tagCreated = false; + let tagPushed = false; let tagError = null; + let tagPushError = null; let proposalOnly = true; + const publicationReplay = { + requested: Boolean(args.repairExistingTag && applyRequested), + workflow: RELEASE_PUBLICATION_WORKFLOW, + ref: RELEASE_PUBLICATION_WORKFLOW_REF, + tagInputName: RELEASE_PUBLICATION_TAG_INPUT, + tagInputValue: targetTag, + dispatched: false, + status: args.repairExistingTag && applyRequested ? 'blocked' : 'not-requested', + error: null + }; + const tagPushRemote = resolveTagPushRemote({ repoRoot, repository, runCommandFn }); + const remoteTag = inspectRemoteTag({ + repoRoot, + remoteName: asOptional(tagPushRemote.remoteName), + tagRef: targetTag, + runCommandFn + }); + const localTag = inspectLocalTag({ + repoRoot, + tagRef: targetTag, + runCommandFn + }); + const repair = createRepairState({ + requested: args.repairExistingTag, + remoteTag, + localTag + }); + + if (repair.remoteTagExists && !repair.requested) { + repair.status = 'repair-available'; + pushUniqueDecisionEntry(advisories, { + code: 'existing-tag-repair-available', + message: `Authoritative tag ${targetTag} already exists; rerun release conductor with --repair-existing-tag to recreate it as a signed annotated tag.` + }); + } + if (repair.requested && !targetTag) { + repair.status = 'blocked'; + } else if (repair.requested && remoteTag.lookupError) { + repair.status = 'blocked'; + } else if (repair.requested && !asOptional(tagPushRemote.remoteName)) { + repair.status = 'blocked'; + } else if (repair.requested && !repair.remoteTagExists) { + repair.status = 'blocked'; + } else if (repair.requested && (!repair.remoteTargetCommitOid || !repair.pushLeaseExpectedOid)) { + repair.status = 'blocked'; + } else if (repair.requested && repair.remoteTagExists) { + repair.status = applyRequested ? 'blocked' : 'ready'; + } + + if (repair.requested) { + if (!targetTag) { + blockers.push({ + code: 'missing-version-for-tag', + message: 'Repair mode requires --version to identify the authoritative release tag.' + }); + } else if (!asOptional(tagPushRemote.remoteName)) { + blockers.push({ + code: 'tag-push-remote-missing', + message: `Repair mode could not resolve an authoritative push remote matching ${repository}.` + }); + } else if (remoteTag.lookupError) { + blockers.push({ + code: 'repair-remote-tag-lookup-failed', + message: `Unable to inspect authoritative tag ${targetTag}: ${remoteTag.lookupError}` + }); + } else if (!repair.remoteTagExists) { + blockers.push({ + code: 'repair-target-tag-missing', + message: `Repair mode requires existing authoritative tag ${targetTag}, but no authoritative tag ref was found on ${tagPushRemote.remoteName}.` + }); + } else if (!repair.remoteTargetCommitOid || !repair.pushLeaseExpectedOid) { + blockers.push({ + code: 'repair-target-unresolved', + message: `Repair mode could not resolve the authoritative object/commit for ${targetTag}.` + }); + } + } if (blockers.length === 0 && applyRequested && conductorEnabled) { if (!targetTag) { @@ -579,6 +843,112 @@ export async function runReleaseConductor(options = {}) { code: 'missing-version-for-tag', message: 'Apply mode requires --version to propose/create a release tag.' }); + } else if (!signingMaterial.available) { + blockers.push({ + code: 'tag-signing-material-missing', + message: 'Apply mode requires signed-tag readiness before tag push. Configure user.signingkey (or equivalent signing material) and retry.' + }); + } else if (args.repairExistingTag) { + if (repair.localTagPresent) { + const deleteResult = runCommandFn('git', ['tag', '-d', targetTag], { + cwd: repoRoot, + allowFailure: true + }); + if (deleteResult.status !== 0) { + tagError = asOptional(deleteResult.stderr) ?? asOptional(deleteResult.stdout) ?? 'local tag delete failed'; + blockers.push({ + code: 'repair-local-tag-delete-failed', + message: `Unable to remove existing local tag ${targetTag} before repair: ${tagError}` + }); + } else { + repair.localTagDeleted = true; + } + } + + if (blockers.length === 0) { + const tagResult = runCommandFn( + 'git', + ['tag', '-s', '-f', targetTag, repair.remoteTargetCommitOid, '-m', `Release ${targetTag}`], + { + cwd: repoRoot, + allowFailure: true + } + ); + if (tagResult.status === 0) { + tagCreated = true; + repair.tagRecreated = true; + const pushRemoteName = asOptional(tagPushRemote.remoteName); + if (!pushRemoteName) { + tagPushError = 'Unable to resolve an authoritative git remote for repair publication.'; + blockers.push({ + code: 'tag-push-remote-missing', + message: `Signed repair tag ${targetTag} was created locally but no authoritative push remote matched ${repository}.` + }); + } else { + const leaseArg = `--force-with-lease=refs/tags/${targetTag}:${repair.pushLeaseExpectedOid}`; + const pushResult = runCommandFn( + 'git', + ['push', leaseArg, pushRemoteName, `refs/tags/${targetTag}:refs/tags/${targetTag}`], + { + cwd: repoRoot, + allowFailure: true + } + ); + if (pushResult.status === 0) { + tagPushed = true; + proposalOnly = false; + repair.status = 'repaired'; + const dispatchResult = runCommandFn( + 'gh', + [ + 'workflow', + 'run', + RELEASE_PUBLICATION_WORKFLOW, + '--ref', + RELEASE_PUBLICATION_WORKFLOW_REF, + '-f', + `${RELEASE_PUBLICATION_TAG_INPUT}=${targetTag}` + ], + { + cwd: repoRoot, + allowFailure: true + } + ); + if (dispatchResult.status === 0) { + publicationReplay.dispatched = true; + publicationReplay.status = 'dispatched'; + } else { + publicationReplay.status = 'dispatch-failed'; + publicationReplay.error = + asOptional(dispatchResult.stderr) ?? + asOptional(dispatchResult.stdout) ?? + 'release workflow dispatch failed'; + blockers.push({ + code: 'release-replay-dispatch-failed', + message: `Release publication replay dispatch failed for ${targetTag}: ${publicationReplay.error}` + }); + } + } else { + tagPushError = asOptional(pushResult.stderr) ?? asOptional(pushResult.stdout) ?? 'repair tag push failed'; + blockers.push({ + code: 'repair-tag-push-failed', + message: `Signed repair publication failed for ${targetTag}: ${tagPushError}` + }); + } + } + } else { + tagError = asOptional(tagResult.stderr) ?? asOptional(tagResult.stdout) ?? 'repair tag creation failed'; + blockers.push({ + code: 'repair-tag-recreate-failed', + message: `Signed repair tag creation failed for ${targetTag}: ${tagError}` + }); + } + } + } else if (repair.remoteTagExists) { + blockers.push({ + code: 'existing-tag-requires-repair-mode', + message: `Authoritative tag ${targetTag} already exists. Rerun release conductor with --repair-existing-tag to recreate it as a signed annotated tag at ${repair.remoteTargetCommitOid}.` + }); } else if (signingMaterial.available) { const tagResult = runCommandFn('git', ['tag', '-s', targetTag, '-m', `Release ${targetTag}`], { cwd: repoRoot, @@ -586,7 +956,29 @@ export async function runReleaseConductor(options = {}) { }); if (tagResult.status === 0) { tagCreated = true; - proposalOnly = false; + const pushRemoteName = asOptional(tagPushRemote.remoteName); + if (!pushRemoteName) { + tagPushError = 'Unable to resolve an authoritative git remote for tag publication.'; + blockers.push({ + code: 'tag-push-remote-missing', + message: `Signed tag ${targetTag} was created locally but no authoritative push remote matched ${repository}.` + }); + } else { + const pushResult = runCommandFn('git', ['push', pushRemoteName, `refs/tags/${targetTag}`], { + cwd: repoRoot, + allowFailure: true + }); + if (pushResult.status === 0) { + tagPushed = true; + proposalOnly = false; + } else { + tagPushError = asOptional(pushResult.stderr) ?? asOptional(pushResult.stdout) ?? 'tag push failed'; + blockers.push({ + code: 'tag-push-failed', + message: `Signed tag publication failed for ${targetTag}: ${tagPushError}` + }); + } + } } else { tagError = asOptional(tagResult.stderr) ?? asOptional(tagResult.stdout) ?? 'tag creation failed'; blockers.push({ @@ -614,8 +1006,13 @@ export async function runReleaseConductor(options = {}) { targetTag, proposalOnly, tagCreated, + tagPushed, tagError, - signingMaterial + tagPushError, + tagPushRemote, + signingMaterial, + repair, + publicationReplay }, inputs: { reportPath: args.reportPath, diff --git a/tools/priority/release-published-bundle-observer.mjs b/tools/priority/release-published-bundle-observer.mjs new file mode 100644 index 000000000..b0a63ca14 --- /dev/null +++ b/tools/priority/release-published-bundle-observer.mjs @@ -0,0 +1,501 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +export const REPORT_SCHEMA = 'priority/release-published-bundle-observer-report@v1'; +export const DEFAULT_OUTPUT_PATH = path.join( + 'tests', + 'results', + '_agent', + 'release', + 'release-published-bundle-observer.json' +); +export const DEFAULT_RESULTS_DIR = path.join( + 'tests', + 'results', + '_agent', + 'release', + 'published-bundle-observer' +); +export const COMPAREVI_TOOLS_ASSET_PATTERN = /^CompareVI\.Tools-v.+\.zip$/i; + +function asOptional(value) { + if (value == null) { + return null; + } + const normalized = String(value).trim(); + return normalized.length > 0 ? normalized : null; +} + +function parseRemoteUrl(url) { + if (!url) { + return null; + } + const ssh = String(url).match(/:(?[^/]+\/[^/]+?)(?:\.git)?$/); + const https = String(url).match(/github\.com\/(?[^/]+\/[^/]+?)(?:\.git)?$/); + const repoPath = ssh?.groups?.repoPath ?? https?.groups?.repoPath; + if (!repoPath) { + return null; + } + const [owner, repoRaw] = repoPath.split('/'); + if (!owner || !repoRaw) { + return null; + } + const repo = repoRaw.endsWith('.git') ? repoRaw.slice(0, -4) : repoRaw; + return `${owner}/${repo}`; +} + +function resolveRepositorySlug(repoRoot, explicitRepo, environment = process.env) { + const explicit = asOptional(explicitRepo); + if (explicit && explicit.includes('/')) { + return explicit; + } + const envRepo = asOptional(environment.GITHUB_REPOSITORY); + if (envRepo && envRepo.includes('/')) { + return envRepo; + } + for (const remoteName of ['upstream', 'origin']) { + const result = spawnSync('git', ['config', '--get', `remote.${remoteName}.url`], { + cwd: repoRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'] + }); + if (result.status !== 0) { + continue; + } + const parsed = parseRemoteUrl(result.stdout.trim()); + if (parsed) { + return parsed; + } + } + throw new Error('Unable to resolve repository slug. Set GITHUB_REPOSITORY or pass --repo.'); +} + +function writeJson(filePath, payload) { + const resolved = path.resolve(filePath); + fs.mkdirSync(path.dirname(resolved), { recursive: true }); + fs.writeFileSync(resolved, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); + return resolved; +} + +function runGhJson(args, { cwd } = {}) { + const result = spawnSync('gh', args, { + cwd, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'] + }); + if (result.status !== 0) { + const message = result.stderr?.trim() || result.stdout?.trim() || `gh ${args.join(' ')} failed (${result.status})`; + throw new Error(message); + } + const text = String(result.stdout ?? '').trim(); + return text ? JSON.parse(text) : null; +} + +function runGh(args, { cwd } = {}) { + const result = spawnSync('gh', args, { + cwd, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'] + }); + if (result.status !== 0) { + const message = result.stderr?.trim() || result.stdout?.trim() || `gh ${args.join(' ')} failed (${result.status})`; + throw new Error(message); + } + return result; +} + +function expandArchive(archivePath, destinationPath) { + const archive = path.resolve(archivePath).replace(/'/g, "''"); + const destination = path.resolve(destinationPath).replace(/'/g, "''"); + const command = [ + "$ErrorActionPreference='Stop'", + `if (Test-Path -LiteralPath '${destination}') { Remove-Item -LiteralPath '${destination}' -Recurse -Force }`, + `Expand-Archive -LiteralPath '${archive}' -DestinationPath '${destination}' -Force` + ].join('; '); + const result = spawnSync('pwsh', ['-NoLogo', '-NoProfile', '-Command', command], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'] + }); + if (result.status !== 0) { + const message = + result.stderr?.trim() || result.stdout?.trim() || `Expand-Archive failed for ${path.basename(archivePath)} (${result.status})`; + throw new Error(message); + } + const directories = fs + .readdirSync(destinationPath, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(destinationPath, entry.name)); + if (directories.length === 1) { + return directories[0]; + } + return destinationPath; +} + +function getObjectPathValue(inputObject, objectPath) { + const segments = String(objectPath ?? '') + .split('.') + .map((segment) => segment.trim()) + .filter(Boolean); + let current = inputObject; + for (const segment of segments) { + if (current == null || typeof current !== 'object' || !(segment in current)) { + return null; + } + current = current[segment]; + } + return current; +} + +function findCompareVIToolsAsset(release) { + const assets = Array.isArray(release?.assets) ? release.assets : []; + return assets.find((asset) => COMPAREVI_TOOLS_ASSET_PATTERN.test(String(asset?.name ?? ''))) ?? null; +} + +function selectRelease(releases, requestedTag = null) { + const normalizedReleases = Array.isArray(releases) ? releases : []; + if (requestedTag) { + const release = normalizedReleases.find((entry) => asOptional(entry?.tag_name) === requestedTag) ?? null; + return { + status: release ? (findCompareVIToolsAsset(release) ? 'selected' : 'asset-missing') : 'release-not-found', + release, + asset: release ? findCompareVIToolsAsset(release) : null + }; + } + + for (const release of normalizedReleases) { + const asset = findCompareVIToolsAsset(release); + if (asset) { + return { + status: 'selected', + release, + asset + }; + } + } + + return { + status: normalizedReleases.length > 0 ? 'asset-missing' : 'release-unobserved', + release: normalizedReleases[0] ?? null, + asset: null + }; +} + +function evaluateBundleContract(bundleRoot) { + const metadataPath = path.join(bundleRoot, 'comparevi-tools-release.json'); + const result = { + status: 'metadata-missing', + metadataPath, + schema: null, + authoritativeConsumerPin: null, + authoritativeConsumerPinKind: null, + capabilityId: null, + distributionRole: null, + distributionModel: null, + bundleImportPath: null, + bundleImportPathExists: false, + releaseAssetPattern: null, + contractPathResolutions: [], + dockerCapabilityId: null, + dockerDistributionRole: null, + dockerDistributionModel: null, + authoritativeImageContractSource: null, + authoritativeImageContractSourceResolved: false, + dockerBundleImportPath: null, + dockerBundleImportPathExists: false, + dockerReleaseAssetPattern: null, + dockerImageContractSchema: null, + metadataPresent: false, + metadataSchemaMatches: false, + viHistoryCapabilityPresent: false, + viHistoryCapabilityProducerNative: false, + dockerProfileCapabilityPresent: false, + dockerProfileCapabilityProducerNative: false, + bundleContractPinResolved: false, + bundleContractPathsResolved: false + }; + + if (!fs.existsSync(metadataPath)) { + return result; + } + + const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8')); + result.metadataPresent = true; + result.schema = asOptional(metadata?.schema); + result.metadataSchemaMatches = result.schema === 'comparevi-tools-release-manifest@v1'; + result.authoritativeConsumerPin = asOptional(metadata?.versionContract?.authoritativeConsumerPin); + result.authoritativeConsumerPinKind = asOptional(metadata?.versionContract?.authoritativeConsumerPinKind); + result.bundleContractPinResolved = Boolean(result.authoritativeConsumerPin && result.authoritativeConsumerPinKind); + + const capability = metadata?.consumerContract?.capabilities?.viHistory ?? null; + result.capabilityId = asOptional(capability?.capabilityId); + result.distributionRole = asOptional(capability?.distributionRole); + result.distributionModel = asOptional(capability?.distributionModel); + result.bundleImportPath = asOptional(capability?.bundleImportPath); + result.releaseAssetPattern = asOptional(capability?.releaseAssetPattern); + result.viHistoryCapabilityPresent = + asOptional(capability?.schema) === 'comparevi-tools/vi-history-capability@v1' && result.capabilityId === 'vi-history'; + result.viHistoryCapabilityProducerNative = + result.distributionRole === 'upstream-producer' && result.distributionModel === 'release-bundle'; + + if (result.bundleImportPath) { + result.bundleImportPathExists = fs.existsSync(path.join(bundleRoot, result.bundleImportPath)); + } + + const contractPaths = capability && typeof capability.contractPaths === 'object' ? capability.contractPaths : {}; + result.contractPathResolutions = Object.entries(contractPaths).map(([name, contractPath]) => ({ + name, + path: String(contractPath), + resolved: getObjectPathValue(metadata, contractPath) != null + })); + result.bundleContractPathsResolved = result.contractPathResolutions.every((entry) => entry.resolved); + + const dockerCapability = metadata?.consumerContract?.capabilities?.dockerProfile ?? null; + result.dockerCapabilityId = asOptional(dockerCapability?.capabilityId); + result.dockerDistributionRole = asOptional(dockerCapability?.distributionRole); + result.dockerDistributionModel = asOptional(dockerCapability?.distributionModel); + result.authoritativeImageContractSource = asOptional(dockerCapability?.authoritativeImageContractSource); + result.dockerBundleImportPath = asOptional(dockerCapability?.bundleImportPath); + result.dockerReleaseAssetPattern = asOptional(dockerCapability?.releaseAssetPattern); + result.dockerProfileCapabilityPresent = + asOptional(dockerCapability?.schema) === 'comparevi-tools/docker-profile-capability@v1' && + result.dockerCapabilityId === 'docker-profile'; + result.dockerProfileCapabilityProducerNative = + result.dockerDistributionRole === 'upstream-producer' && result.dockerDistributionModel === 'release-bundle'; + result.authoritativeImageContractSourceResolved = + Boolean(result.authoritativeImageContractSource) && + getObjectPathValue(metadata, result.authoritativeImageContractSource) != null; + + if (result.dockerBundleImportPath) { + result.dockerBundleImportPathExists = fs.existsSync(path.join(bundleRoot, result.dockerBundleImportPath)); + } + + result.dockerImageContractSchema = asOptional( + getObjectPathValue(metadata, result.authoritativeImageContractSource ?? '')?.schema + ); + + result.status = + result.metadataSchemaMatches && + result.viHistoryCapabilityPresent && + result.viHistoryCapabilityProducerNative && + result.bundleContractPinResolved && + result.bundleImportPathExists && + result.bundleContractPathsResolved + ? 'producer-native-ready' + : 'producer-native-incomplete'; + + return result; +} + +function defaultDownloadAsset({ repoRoot, repository, releaseTag, assetName, destinationDirectory, runGhFn = runGh }) { + fs.mkdirSync(destinationDirectory, { recursive: true }); + runGhFn( + ['release', 'download', releaseTag, '--repo', repository, '--pattern', assetName, '--dir', destinationDirectory, '--clobber'], + { cwd: repoRoot } + ); + return path.join(destinationDirectory, assetName); +} + +export function parseArgs(argv = process.argv) { + const args = argv.slice(2); + const options = { + repoRoot: process.cwd(), + repo: null, + tag: null, + outputPath: DEFAULT_OUTPUT_PATH, + resultsDir: DEFAULT_RESULTS_DIR, + help: false + }; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === '-h' || token === '--help') { + options.help = true; + continue; + } + if (token === '--repo-root' || token === '--repo' || token === '--tag' || token === '--output' || token === '--results-dir') { + const next = args[index + 1]; + if (!next || next.startsWith('-')) { + throw new Error(`Missing value for ${token}.`); + } + if (token === '--repo-root') options.repoRoot = next; + if (token === '--repo') options.repo = next; + if (token === '--tag') options.tag = next; + if (token === '--output') options.outputPath = next; + if (token === '--results-dir') options.resultsDir = next; + index += 1; + continue; + } + throw new Error(`Unknown option: ${token}`); + } + + return options; +} + +function printHelp() { + [ + 'Usage: node tools/priority/release-published-bundle-observer.mjs [options]', + '', + 'Options:', + ' --repo-root Repository root override.', + ' --repo Repository slug override.', + ' --tag Observe a specific published release tag instead of the newest CompareVI.Tools asset release.', + ` --output Output JSON path (default: ${DEFAULT_OUTPUT_PATH}).`, + ` --results-dir Working directory for downloaded/extracted bundle files (default: ${DEFAULT_RESULTS_DIR}).`, + ' -h, --help Show help.' + ].forEach((line) => console.log(line)); +} + +export async function runReleasePublishedBundleObserver(options = {}, deps = {}) { + const repoRoot = path.resolve(options.repoRoot ?? process.cwd()); + const environment = deps.environment ?? process.env; + const repository = resolveRepositorySlug(repoRoot, options.repo, environment); + const outputPath = path.resolve(repoRoot, options.outputPath ?? DEFAULT_OUTPUT_PATH); + const resultsDir = path.resolve(repoRoot, options.resultsDir ?? DEFAULT_RESULTS_DIR); + const requestedTag = asOptional(options.tag); + const runGhJsonFn = deps.runGhJsonFn ?? runGhJson; + const downloadAssetFn = deps.downloadAssetFn ?? defaultDownloadAsset; + const extractArchiveFn = deps.extractArchiveFn ?? expandArchive; + const writeJsonFn = deps.writeJsonFn ?? writeJson; + const now = deps.now ?? new Date(); + + const releases = runGhJsonFn(['api', `repos/${repository}/releases?per_page=20`], { cwd: repoRoot }); + const selection = selectRelease(releases, requestedTag); + + const report = { + schema: REPORT_SCHEMA, + generatedAt: now.toISOString(), + repository, + inputs: { + requestedTag, + resultsDir: path.relative(repoRoot, resultsDir).replace(/\\/g, '/') + }, + selection: { + status: selection.status, + releaseTag: asOptional(selection.release?.tag_name), + publishedAt: asOptional(selection.release?.published_at), + releaseName: asOptional(selection.release?.name), + releaseId: selection.release?.id ?? null, + prerelease: selection.release?.prerelease ?? null, + draft: selection.release?.draft ?? null, + assetName: asOptional(selection.asset?.name), + assetId: selection.asset?.id ?? null + }, + bundle: { + status: 'not-downloaded', + archivePath: null, + extractionRoot: null, + downloadDirectory: path.relative(repoRoot, path.join(resultsDir, 'download')).replace(/\\/g, '/') + }, + bundleContract: { + status: selection.status === 'release-unobserved' || selection.status === 'release-not-found' ? 'release-unobserved' : 'unobserved', + metadataPath: null, + schema: null, + authoritativeConsumerPin: null, + authoritativeConsumerPinKind: null, + capabilityId: null, + distributionRole: null, + distributionModel: null, + bundleImportPath: null, + bundleImportPathExists: false, + releaseAssetPattern: null, + contractPathResolutions: [], + metadataPresent: false, + metadataSchemaMatches: false, + viHistoryCapabilityPresent: false, + viHistoryCapabilityProducerNative: false, + bundleContractPinResolved: false, + bundleContractPathsResolved: false + }, + summary: { + status: selection.status, + releaseTag: asOptional(selection.release?.tag_name), + assetName: asOptional(selection.asset?.name), + publishedAt: asOptional(selection.release?.published_at), + authoritativeConsumerPin: null + } + }; + + if (selection.status !== 'selected') { + const writtenPath = writeJsonFn(outputPath, report); + return { + report, + outputPath: writtenPath, + exitCode: 1 + }; + } + + const downloadDirectory = path.join(resultsDir, 'download'); + const extractDirectory = path.join(resultsDir, 'bundle'); + try { + const archivePath = path.resolve( + downloadAssetFn({ + repoRoot, + repository, + releaseTag: selection.release.tag_name, + assetName: selection.asset.name, + destinationDirectory: downloadDirectory, + runGhFn: deps.runGhFn ?? runGh + }) + ); + report.bundle.status = 'downloaded'; + report.bundle.archivePath = path.relative(repoRoot, archivePath).replace(/\\/g, '/'); + + const extractionRoot = path.resolve(extractArchiveFn(archivePath, extractDirectory)); + report.bundle.status = 'extracted'; + report.bundle.extractionRoot = path.relative(repoRoot, extractionRoot).replace(/\\/g, '/'); + + const bundleContract = evaluateBundleContract(extractionRoot); + report.bundleContract = { + ...bundleContract, + metadataPath: path.relative(repoRoot, bundleContract.metadataPath).replace(/\\/g, '/') + }; + report.summary.status = bundleContract.status; + report.summary.authoritativeConsumerPin = bundleContract.authoritativeConsumerPin; + } catch (error) { + const message = error?.message ?? String(error); + report.bundle.status = report.bundle.status === 'not-downloaded' ? 'download-failed' : 'extract-failed'; + report.bundle.error = message; + report.bundleContract.status = report.bundle.status; + report.summary.status = report.bundle.status; + } + + const writtenPath = writeJsonFn(outputPath, report); + return { + report, + outputPath: writtenPath, + exitCode: report.summary.status === 'producer-native-ready' ? 0 : 1 + }; +} + +export async function main(argv = process.argv) { + const options = parseArgs(argv); + if (options.help) { + printHelp(); + return 0; + } + const { report, outputPath, exitCode } = await runReleasePublishedBundleObserver(options); + console.log( + `[release-published-bundle-observer] wrote ${outputPath} status=${report.summary.status} tag=${report.summary.releaseTag ?? 'none'}` + ); + return exitCode; +} + +const isDirectExecution = + process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url)); + +if (isDirectExecution) { + main().then( + (code) => { + process.exitCode = code; + }, + (error) => { + console.error(`[release-published-bundle-observer] ${error.message}`); + process.exitCode = 1; + } + ); +} diff --git a/tools/priority/release-signing-readiness.mjs b/tools/priority/release-signing-readiness.mjs new file mode 100644 index 000000000..a4caeac28 --- /dev/null +++ b/tools/priority/release-signing-readiness.mjs @@ -0,0 +1,559 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +export const REPORT_SCHEMA = 'priority/release-signing-readiness-report@v1'; +export const DEFAULT_OUTPUT_PATH = path.join( + 'tests', + 'results', + '_agent', + 'release', + 'release-signing-readiness.json' +); +export const DEFAULT_RELEASE_CONDUCTOR_REPORT_PATH = path.join( + 'tests', + 'results', + '_agent', + 'release', + 'release-conductor-report.json' +); +export const DEFAULT_RELEASE_PUBLISHED_BUNDLE_OBSERVER_PATH = path.join( + 'tests', + 'results', + '_agent', + 'release', + 'release-published-bundle-observer.json' +); +export const REQUIRED_SIGNING_SECRET = 'RELEASE_TAG_SIGNING_PRIVATE_KEY'; +export const OPTIONAL_SIGNING_SECRET = 'RELEASE_TAG_SIGNING_PUBLIC_KEY'; +export const REQUIRED_SIGNING_SCOPE = 'admin:ssh_signing_key'; +export const RELEASE_CONDUCTOR_ENABLE_VARIABLE = 'RELEASE_CONDUCTOR_ENABLED'; + +function asOptional(value) { + if (value == null) { + return null; + } + const normalized = String(value).trim(); + return normalized.length > 0 ? normalized : null; +} + +function parseRemoteUrl(url) { + if (!url) return null; + const ssh = String(url).match(/:(?[^/]+\/[^/]+?)(?:\.git)?$/); + const https = String(url).match(/github\.com\/(?[^/]+\/[^/]+?)(?:\.git)?$/); + const repoPath = ssh?.groups?.repoPath ?? https?.groups?.repoPath; + if (!repoPath) return null; + const [owner, repoRaw] = repoPath.split('/'); + if (!owner || !repoRaw) return null; + const repo = repoRaw.endsWith('.git') ? repoRaw.slice(0, -4) : repoRaw; + return `${owner}/${repo}`; +} + +function resolveRepositorySlug(repoRoot, explicitRepo, environment = process.env) { + const explicit = asOptional(explicitRepo); + if (explicit && explicit.includes('/')) { + return explicit; + } + const envRepo = asOptional(environment.GITHUB_REPOSITORY); + if (envRepo && envRepo.includes('/')) { + return envRepo; + } + for (const remoteName of ['upstream', 'origin']) { + const result = spawnSync('git', ['config', '--get', `remote.${remoteName}.url`], { + cwd: repoRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'] + }); + if (result.status !== 0) { + continue; + } + const parsed = parseRemoteUrl(result.stdout.trim()); + if (parsed) { + return parsed; + } + } + throw new Error('Unable to resolve repository slug. Set GITHUB_REPOSITORY or pass --repo.'); +} + +function readOptionalJson(filePath) { + const resolved = path.resolve(filePath); + if (!fs.existsSync(resolved)) { + return null; + } + return JSON.parse(fs.readFileSync(resolved, 'utf8')); +} + +function writeJson(filePath, payload) { + const resolved = path.resolve(filePath); + fs.mkdirSync(path.dirname(resolved), { recursive: true }); + fs.writeFileSync(resolved, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); + return resolved; +} + +function runGhJson(args, { cwd } = {}) { + const result = spawnSync('gh', args, { + cwd, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'] + }); + if (result.status !== 0) { + const message = result.stderr?.trim() || result.stdout?.trim() || `gh ${args.join(' ')} failed (${result.status})`; + throw new Error(message); + } + const text = String(result.stdout ?? '').trim(); + return text ? JSON.parse(text) : null; +} + +export function parseArgs(argv = process.argv) { + const args = argv.slice(2); + const options = { + repoRoot: process.cwd(), + repo: null, + outputPath: DEFAULT_OUTPUT_PATH, + releaseConductorReportPath: DEFAULT_RELEASE_CONDUCTOR_REPORT_PATH, + releasePublishedBundleObserverPath: DEFAULT_RELEASE_PUBLISHED_BUNDLE_OBSERVER_PATH, + help: false + }; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === '-h' || token === '--help') { + options.help = true; + continue; + } + if ( + token === '--repo-root' || + token === '--repo' || + token === '--output' || + token === '--release-conductor-report' || + token === '--release-published-bundle-observer' + ) { + const next = args[index + 1]; + if (!next || next.startsWith('-')) { + throw new Error(`Missing value for ${token}.`); + } + if (token === '--repo-root') options.repoRoot = next; + if (token === '--repo') options.repo = next; + if (token === '--output') options.outputPath = next; + if (token === '--release-conductor-report') options.releaseConductorReportPath = next; + if (token === '--release-published-bundle-observer') options.releasePublishedBundleObserverPath = next; + index += 1; + continue; + } + throw new Error(`Unknown option: ${token}`); + } + + return options; +} + +function printHelp() { + [ + 'Usage: node tools/priority/release-signing-readiness.mjs [options]', + '', + 'Options:', + ' --repo-root Repository root override.', + ' --repo Repository slug override.', + ` --output Output JSON path (default: ${DEFAULT_OUTPUT_PATH}).`, + ` --release-conductor-report Release conductor report path (default: ${DEFAULT_RELEASE_CONDUCTOR_REPORT_PATH}).`, + ` --release-published-bundle-observer Published bundle observer path (default: ${DEFAULT_RELEASE_PUBLISHED_BUNDLE_OBSERVER_PATH}).`, + ' -h, --help Show help.' + ].forEach((line) => console.log(line)); +} + +function hasSigningWorkflowContract(repoRoot) { + const workflowPath = path.join(repoRoot, '.github', 'workflows', 'release-conductor.yml'); + if (!fs.existsSync(workflowPath)) { + return { + ready: false, + workflowPath, + reasons: ['release-conductor-workflow-missing'] + }; + } + const workflow = fs.readFileSync(workflowPath, 'utf8'); + const requiredSnippets = [ + 'Configure release tag signing material', + 'RELEASE_TAG_SIGNING_PRIVATE_KEY', + 'RELEASE_TAG_SIGNING_IDENTITY_NAME', + 'RELEASE_TAG_SIGNING_IDENTITY_EMAIL', + "gh api user --jq '.login'", + 'git config gpg.format ssh', + 'git config user.signingkey "$public_key_path"', + 'git config user.name "$signing_name"', + 'git config user.email "$signing_email"' + ]; + const missing = requiredSnippets.filter((snippet) => !workflow.includes(snippet)); + return { + ready: missing.length === 0, + workflowPath, + reasons: missing.map((snippet) => `missing-workflow-snippet:${snippet}`) + }; +} + +function normalizeSecretInventory(payload) { + const secrets = Array.isArray(payload?.secrets) ? payload.secrets : []; + const names = new Set(secrets.map((entry) => String(entry?.name ?? '').trim()).filter(Boolean)); + const requiredPresent = names.has(REQUIRED_SIGNING_SECRET); + const optionalPresent = names.has(OPTIONAL_SIGNING_SECRET); + return { + status: requiredPresent ? 'configured' : 'missing', + requiredSecretPresent: requiredPresent, + optionalPublicKeyPresent: optionalPresent, + listedSecretCount: secrets.length, + listedSecretNames: Array.from(names).sort() + }; +} + +function normalizeVariableInventory(payload) { + const variables = Array.isArray(payload?.variables) ? payload.variables : []; + const values = new Map( + variables + .map((entry) => [String(entry?.name ?? '').trim(), asOptional(entry?.value)]) + .filter(([name]) => Boolean(name)) + ); + const configuredValue = values.get(RELEASE_CONDUCTOR_ENABLE_VARIABLE) ?? null; + const enabled = configuredValue === '1'; + return { + status: enabled ? 'enabled' : 'disabled', + variablePresent: values.has(RELEASE_CONDUCTOR_ENABLE_VARIABLE), + enabled, + configuredValue, + listedVariableCount: variables.length, + listedVariableNames: Array.from(values.keys()).sort() + }; +} + +function normalizeSigningAuthority(payload) { + const keys = Array.isArray(payload) ? payload : Array.isArray(payload?.ssh_signing_keys) ? payload.ssh_signing_keys : []; + return { + status: keys.length > 0 ? 'ready' : 'keys-missing', + requiredScope: REQUIRED_SIGNING_SCOPE, + scopeAvailable: true, + listedKeyCount: keys.length + }; +} + +function hasMissingScopeError(message, scope) { + if (!message || !scope) { + return false; + } + return message.includes(scope) || message.includes(`"${scope}"`); +} + +function derivePublicationState(conductorReport) { + const release = conductorReport?.release ?? null; + if (!release) { + return { + status: 'unobserved', + tagCreated: false, + tagPushed: false, + targetTag: null + }; + } + const tagCreated = release.tagCreated === true; + const tagPushed = release.tagPushed === true; + return { + status: tagPushed ? 'authoritative-publication-successful' : tagCreated ? 'tag-created-not-pushed' : 'not-attempted', + tagCreated, + tagPushed, + targetTag: asOptional(release.targetTag) + }; +} + +function derivePublishedBundleObserverState(observerReport) { + if (!observerReport) { + return { + status: 'unobserved', + releaseTag: null, + assetName: null, + publishedAt: null, + authoritativeConsumerPin: null + }; + } + + return { + status: asOptional(observerReport?.summary?.status) || 'unobserved', + releaseTag: asOptional(observerReport?.summary?.releaseTag) || asOptional(observerReport?.selection?.releaseTag), + assetName: asOptional(observerReport?.summary?.assetName) || asOptional(observerReport?.selection?.assetName), + publishedAt: asOptional(observerReport?.summary?.publishedAt) || asOptional(observerReport?.selection?.publishedAt), + authoritativeConsumerPin: asOptional(observerReport?.summary?.authoritativeConsumerPin) + }; +} + +function createPublishedBundleBlocker(publishedBundleObserver) { + switch (publishedBundleObserver.status) { + case 'release-unobserved': + return { + code: 'published-bundle-release-unobserved', + message: 'No published CompareVI.Tools release could be observed yet for the producer-native vi-history distributor contract.' + }; + case 'release-not-found': + return { + code: 'published-bundle-release-not-found', + message: 'The requested CompareVI.Tools release tag was not found on GitHub, so producer-native vi-history publication is still unavailable.' + }; + case 'asset-missing': + return { + code: 'published-bundle-asset-missing', + message: 'The observed CompareVI.Tools release does not publish a CompareVI.Tools zip asset yet.' + }; + case 'download-failed': + return { + code: 'published-bundle-download-failed', + message: 'The published CompareVI.Tools asset could not be downloaded for producer-native vi-history verification.' + }; + case 'extract-failed': + return { + code: 'published-bundle-extract-failed', + message: 'The published CompareVI.Tools asset could not be extracted for producer-native vi-history verification.' + }; + case 'metadata-missing': + return { + code: 'published-bundle-metadata-missing', + message: 'The published CompareVI.Tools asset is missing comparevi-tools-release.json, so the producer-native vi-history contract is not published yet.' + }; + case 'producer-native-incomplete': + return { + code: 'published-bundle-producer-native-incomplete', + message: 'The published CompareVI.Tools asset exists, but it is still missing the producer-native vi-history consumer contract.' + }; + default: + return null; + } +} + +export async function runReleaseSigningReadiness(options = {}, deps = {}) { + const repoRoot = path.resolve(options.repoRoot ?? process.cwd()); + const environment = deps.environment ?? process.env; + const repository = resolveRepositorySlug(repoRoot, options.repo, environment); + const outputPath = path.resolve(repoRoot, options.outputPath ?? DEFAULT_OUTPUT_PATH); + const conductorReportPath = path.resolve( + repoRoot, + options.releaseConductorReportPath ?? DEFAULT_RELEASE_CONDUCTOR_REPORT_PATH + ); + const publishedBundleObserverPath = path.resolve( + repoRoot, + options.releasePublishedBundleObserverPath ?? DEFAULT_RELEASE_PUBLISHED_BUNDLE_OBSERVER_PATH + ); + const runGhJsonFn = deps.runGhJsonFn ?? runGhJson; + const readOptionalJsonFn = deps.readOptionalJsonFn ?? readOptionalJson; + const writeJsonFn = deps.writeJsonFn ?? writeJson; + const now = deps.now ?? new Date(); + + const workflowContract = hasSigningWorkflowContract(repoRoot); + const [owner, repo] = repository.split('/'); + let secretInventory; + try { + const payload = runGhJsonFn(['api', `repos/${owner}/${repo}/actions/secrets?per_page=100`], { cwd: repoRoot }); + secretInventory = { + ...normalizeSecretInventory(payload), + source: 'github-actions-secrets-api', + error: null + }; + } catch (error) { + secretInventory = { + status: 'unverifiable', + requiredSecretPresent: null, + optionalPublicKeyPresent: null, + listedSecretCount: null, + listedSecretNames: [], + source: 'github-actions-secrets-api', + error: error?.message ?? String(error) + }; + } + + let releaseConductorApply; + try { + const payload = runGhJsonFn(['api', `repos/${owner}/${repo}/actions/variables?per_page=100`], { cwd: repoRoot }); + releaseConductorApply = { + ...normalizeVariableInventory(payload), + source: 'github-actions-variables-api', + error: null + }; + } catch (error) { + releaseConductorApply = { + status: 'unverifiable', + variablePresent: null, + enabled: null, + configuredValue: null, + listedVariableCount: null, + listedVariableNames: [], + source: 'github-actions-variables-api', + error: error?.message ?? String(error) + }; + } + + let signingAuthority; + try { + const payload = runGhJsonFn(['api', 'user/ssh_signing_keys?per_page=100'], { cwd: repoRoot }); + signingAuthority = { + ...normalizeSigningAuthority(payload), + source: 'github-user-ssh-signing-keys-api', + error: null + }; + } catch (error) { + const message = error?.message ?? String(error); + const scopeMissing = hasMissingScopeError(message, REQUIRED_SIGNING_SCOPE); + signingAuthority = { + status: scopeMissing ? 'scope-missing' : 'unverifiable', + requiredScope: REQUIRED_SIGNING_SCOPE, + scopeAvailable: scopeMissing ? false : null, + listedKeyCount: null, + source: 'github-user-ssh-signing-keys-api', + error: message + }; + } + + const conductorReport = readOptionalJsonFn(conductorReportPath); + const publishedBundleObserverReport = readOptionalJsonFn(publishedBundleObserverPath); + if ( + publishedBundleObserverReport && + publishedBundleObserverReport.schema !== 'priority/release-published-bundle-observer-report@v1' + ) { + throw new Error( + `Expected priority/release-published-bundle-observer-report@v1 at ${publishedBundleObserverPath}.` + ); + } + const publication = derivePublicationState(conductorReport); + const publishedBundleObserver = derivePublishedBundleObserverState(publishedBundleObserverReport); + const blockers = []; + + if (!workflowContract.ready) { + blockers.push({ + code: 'workflow-signing-contract-missing', + message: 'Release conductor workflow does not yet expose the workflow-owned signing contract.' + }); + } + if (secretInventory.status === 'missing') { + blockers.push({ + code: 'workflow-signing-secret-missing', + message: `${REQUIRED_SIGNING_SECRET} is not configured for the repository Actions secrets surface.` + }); + } else if (secretInventory.status === 'unverifiable') { + blockers.push({ + code: 'workflow-signing-secret-unverifiable', + message: 'Unable to verify repository Actions secrets from the current automation identity.' + }); + } + if (releaseConductorApply.status === 'disabled') { + blockers.push({ + code: 'release-conductor-apply-disabled', + message: `${RELEASE_CONDUCTOR_ENABLE_VARIABLE} is not set to 1 for the repository Actions variable surface.` + }); + } else if (releaseConductorApply.status === 'unverifiable') { + blockers.push({ + code: 'release-conductor-apply-unverifiable', + message: 'Unable to verify release conductor apply gating from the current automation identity.' + }); + } + if (signingAuthority.status === 'keys-missing') { + blockers.push({ + code: 'workflow-signing-key-missing', + message: 'Authenticated identity can inspect SSH signing keys, but no SSH signing key is currently registered.' + }); + } else if (signingAuthority.status === 'scope-missing') { + blockers.push({ + code: 'workflow-signing-admin-scope-missing', + message: `${REQUIRED_SIGNING_SCOPE} is not available to the current automation identity, so SSH signing-key authority cannot be verified or managed.` + }); + } else if (signingAuthority.status === 'unverifiable') { + blockers.push({ + code: 'workflow-signing-authority-unverifiable', + message: 'Unable to verify SSH signing-key authority for the current automation identity.' + }); + } + + const publishedBundleBlocker = createPublishedBundleBlocker(publishedBundleObserver); + if (publishedBundleBlocker) { + blockers.push(publishedBundleBlocker); + } + + const externalBlockerPriority = [ + 'workflow-signing-secret-missing', + 'workflow-signing-secret-unverifiable', + 'workflow-signing-admin-scope-missing', + 'workflow-signing-key-missing', + 'workflow-signing-authority-unverifiable', + 'release-conductor-apply-disabled', + 'release-conductor-apply-unverifiable' + ]; + + const summary = { + status: blockers.length === 0 ? 'pass' : 'warn', + codePathState: workflowContract.ready ? 'ready' : 'missing-contract', + signingCapabilityState: + secretInventory.status === 'configured' + ? 'configured' + : secretInventory.status === 'missing' + ? 'missing' + : 'unverifiable', + signingAuthorityState: signingAuthority.status, + releaseConductorApplyState: releaseConductorApply.status, + publicationState: publication.status, + publishedBundleState: publishedBundleObserver.status, + publishedBundleReleaseTag: publishedBundleObserver.releaseTag, + publishedBundleAuthoritativeConsumerPin: publishedBundleObserver.authoritativeConsumerPin, + externalBlocker: externalBlockerPriority.find((code) => blockers.some((entry) => entry.code === code)) ?? null, + blockerCount: blockers.length + }; + + const report = { + schema: REPORT_SCHEMA, + generatedAt: now.toISOString(), + repository, + inputs: { + releaseConductorReportPath: path.relative(repoRoot, conductorReportPath).replace(/\\/g, '/'), + releasePublishedBundleObserverPath: path.relative(repoRoot, publishedBundleObserverPath).replace(/\\/g, '/') + }, + workflowContract: { + ready: workflowContract.ready, + workflowPath: path.relative(repoRoot, workflowContract.workflowPath).replace(/\\/g, '/'), + reasons: workflowContract.reasons + }, + secretInventory, + releaseConductorApply, + signingAuthority, + publication, + publishedBundleObserver, + summary, + blockers + }; + + const writtenPath = writeJsonFn(outputPath, report); + return { + report, + outputPath: writtenPath, + exitCode: summary.status === 'pass' ? 0 : 1 + }; +} + +export async function main(argv = process.argv) { + const options = parseArgs(argv); + if (options.help) { + printHelp(); + return 0; + } + const { report, outputPath, exitCode } = await runReleaseSigningReadiness(options); + console.log( + `[release-signing-readiness] wrote ${outputPath} status=${report.summary.status} externalBlocker=${report.summary.externalBlocker ?? 'none'}` + ); + return exitCode; +} + +const isDirectExecution = + process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url)); + +if (isDirectExecution) { + main().then( + (code) => { + process.exitCode = code; + }, + (error) => { + console.error(`[release-signing-readiness] ${error.message}`); + process.exitCode = 1; + } + ); +} diff --git a/tools/priority/release-trust-remediation.mjs b/tools/priority/release-trust-remediation.mjs new file mode 100644 index 000000000..8201afa52 --- /dev/null +++ b/tools/priority/release-trust-remediation.mjs @@ -0,0 +1,166 @@ +#!/usr/bin/env node + +import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +export const DEFAULT_TRUST_REPORT_PATH = path.join('tests', 'results', '_agent', 'supply-chain', 'release-trust-gate.json'); +export const DEFAULT_OUTPUT_PATH = path.join('tests', 'results', '_agent', 'release', 'release-trust-remediation.md'); +const REPAIR_FAILURE_CODES = new Set(['tag-not-annotated', 'tag-signature-unverified']); + +function normalizeOptional(value) { + if (value == null) return null; + const normalized = String(value).trim(); + return normalized || null; +} + +function normalizeVersionFromTag(tagRef) { + const normalized = normalizeOptional(tagRef); + if (!normalized) { + return null; + } + return normalized.startsWith('v') ? normalized.slice(1) : normalized; +} + +export function parseArgs(argv = process.argv) { + const args = argv.slice(2); + const options = { + trustReportPath: DEFAULT_TRUST_REPORT_PATH, + outputPath: DEFAULT_OUTPUT_PATH, + summaryPath: null, + tagRef: null + }; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === '--trust-report' || token === '--output' || token === '--summary' || token === '--tag-ref') { + const next = args[index + 1]; + if (!next || next.startsWith('-')) { + throw new Error(`Missing value for ${token}.`); + } + index += 1; + if (token === '--trust-report') options.trustReportPath = next; + if (token === '--output') options.outputPath = next; + if (token === '--summary') options.summaryPath = next; + if (token === '--tag-ref') options.tagRef = next; + continue; + } + if (token === '--help' || token === '-h') { + return { ...options, help: true }; + } + throw new Error(`Unknown option: ${token}`); + } + + return options; +} + +async function readJson(filePath) { + return JSON.parse(await readFile(path.resolve(filePath), 'utf8')); +} + +async function writeText(filePath, contents) { + const resolved = path.resolve(filePath); + await mkdir(path.dirname(resolved), { recursive: true }); + await writeFile(resolved, contents, 'utf8'); + return resolved; +} + +async function appendText(filePath, contents) { + const resolved = path.resolve(filePath); + await mkdir(path.dirname(resolved), { recursive: true }); + await appendFile(resolved, contents, 'utf8'); + return resolved; +} + +export function buildReleaseTrustRemediationMarkdown({ trustReport, tagRef }) { + const failures = Array.isArray(trustReport?.failures) + ? trustReport.failures + : Array.isArray(trustReport?.summary?.failures) + ? trustReport.summary.failures + : []; + const relevantFailures = failures.filter((failure) => REPAIR_FAILURE_CODES.has(String(failure?.code ?? '').trim())); + const normalizedTag = normalizeOptional(tagRef) ?? normalizeOptional(trustReport?.tagSignature?.refName); + const normalizedVersion = normalizeVersionFromTag(normalizedTag); + + const lines = ['## Release Trust Remediation', '']; + if (relevantFailures.length === 0) { + lines.push('- No repair-mode remediation is required for the current trust-gate result.'); + lines.push(''); + return lines.join('\n'); + } + + lines.push(`- Release trust gate reported repair-eligible tag failures for \`${normalizedTag ?? 'unknown-tag'}\`.`); + lines.push(`- Failure codes: ${relevantFailures.map((failure) => `\`${failure.code}\``).join(', ')}`); + lines.push('- Preserve tag identity and asset names. Do not rename the release tag to bypass trust verification.'); + lines.push('- Rerun `.github/workflows/release-conductor.yml` with:'); + lines.push(` - \`version = ${normalizedVersion ?? 'X.Y.Z'}\``); + lines.push(' - `apply = true`'); + lines.push(' - `repair_existing_tag = true`'); + lines.push('- Continue release publication only after `tests/results/_agent/release/release-conductor-report.json` shows:'); + lines.push(' - `release.repair.status = repaired`'); + lines.push(' - `release.tagPushed = true`'); + lines.push(''); + return lines.join('\n'); +} + +export async function runReleaseTrustRemediation(options = {}) { + const args = options.args ?? parseArgs(); + if (args.help) { + return { markdown: '', outputPath: null, summaryPath: null, wroteSummary: false }; + } + + const trustReport = await readJson(args.trustReportPath); + const markdown = buildReleaseTrustRemediationMarkdown({ + trustReport, + tagRef: args.tagRef + }); + const outputPath = await writeText(args.outputPath, `${markdown}\n`); + + let summaryPath = null; + let wroteSummary = false; + if (args.summaryPath) { + summaryPath = await appendText(args.summaryPath, `\n${markdown}\n`); + wroteSummary = true; + } + + return { + markdown, + outputPath, + summaryPath, + wroteSummary + }; +} + +async function main(argv = process.argv) { + const args = parseArgs(argv); + if (args.help) { + console.log('Usage: node tools/priority/release-trust-remediation.mjs [options]'); + console.log(''); + console.log(` --trust-report Trust-gate report path (default: ${DEFAULT_TRUST_REPORT_PATH}).`); + console.log(` --output Markdown output path (default: ${DEFAULT_OUTPUT_PATH}).`); + console.log(' --summary Optional workflow step summary path to overwrite.'); + console.log(' --tag-ref Optional release tag name override.'); + return 0; + } + + const result = await runReleaseTrustRemediation({ args }); + console.log(`[release-trust-remediation] wrote ${result.outputPath}`); + if (result.wroteSummary) { + console.log(`[release-trust-remediation] summary ${result.summaryPath}`); + } + return 0; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) { + main(process.argv) + .then((exitCode) => { + if (exitCode !== 0) { + process.exitCode = exitCode; + } + }) + .catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + }); +} diff --git a/tools/priority/runtime-supervisor.mjs b/tools/priority/runtime-supervisor.mjs index 5c90d5574..0294bb767 100644 --- a/tools/priority/runtime-supervisor.mjs +++ b/tools/priority/runtime-supervisor.mjs @@ -59,6 +59,7 @@ import { } from './sync-standing-priority.mjs'; import { buildWorkerProviderSelectionRequest, + buildExecutionTopologyRuntimeState, buildLocalReviewLoopRequest, buildCanonicalDeliveryDecision, selectWorkerProviderAssignment, @@ -416,6 +417,10 @@ async function resolveGovernorPortfolioHandoff({ repoRoot, repository, deps = {} nextAction: null, ownerDecisionSource: null, governorMode: null, + viHistoryDistributorDependencyStatus: null, + viHistoryDistributorDependencyTargetRepository: null, + viHistoryDistributorDependencyExternalBlocker: null, + viHistoryDistributorDependencyPublicationState: null, reason: `Unable to read governor portfolio summary: ${error?.message || String(error)}` }; } @@ -429,6 +434,10 @@ async function resolveGovernorPortfolioHandoff({ repoRoot, repository, deps = {} nextAction: null, ownerDecisionSource: null, governorMode: null, + viHistoryDistributorDependencyStatus: null, + viHistoryDistributorDependencyTargetRepository: null, + viHistoryDistributorDependencyExternalBlocker: null, + viHistoryDistributorDependencyPublicationState: null, reason: 'Governor portfolio summary is unavailable for queue-empty handoff.' }; } @@ -442,6 +451,10 @@ async function resolveGovernorPortfolioHandoff({ repoRoot, repository, deps = {} nextAction: null, ownerDecisionSource: null, governorMode: null, + viHistoryDistributorDependencyStatus: null, + viHistoryDistributorDependencyTargetRepository: null, + viHistoryDistributorDependencyExternalBlocker: null, + viHistoryDistributorDependencyPublicationState: null, reason: 'Governor portfolio summary does not match the expected schema.' }; } @@ -451,6 +464,14 @@ async function resolveGovernorPortfolioHandoff({ repoRoot, repository, deps = {} const nextAction = normalizeText(payload?.summary?.nextAction) || null; const ownerDecisionSource = normalizeText(payload?.summary?.ownerDecisionSource) || null; const governorMode = normalizeText(payload?.summary?.governorMode) || null; + const viHistoryDistributorDependencyStatus = + normalizeText(payload?.summary?.viHistoryDistributorDependencyStatus) || null; + const viHistoryDistributorDependencyTargetRepository = + normalizeText(payload?.summary?.viHistoryDistributorDependencyTargetRepository) || null; + const viHistoryDistributorDependencyExternalBlocker = + normalizeText(payload?.summary?.viHistoryDistributorDependencyExternalBlocker) || null; + const viHistoryDistributorDependencyPublicationState = + normalizeText(payload?.summary?.viHistoryDistributorDependencyPublicationState) || null; if (!currentOwnerRepository || !nextOwnerRepository || !nextAction || !ownerDecisionSource || !governorMode) { return { @@ -461,10 +482,34 @@ async function resolveGovernorPortfolioHandoff({ repoRoot, repository, deps = {} nextAction, ownerDecisionSource, governorMode, + viHistoryDistributorDependencyStatus, + viHistoryDistributorDependencyTargetRepository, + viHistoryDistributorDependencyExternalBlocker, + viHistoryDistributorDependencyPublicationState, reason: 'Governor portfolio summary is missing required owner handoff fields.' }; } + let reason = null; + if (currentOwnerRepository === repository) { + if (viHistoryDistributorDependencyStatus === 'blocked' && viHistoryDistributorDependencyTargetRepository) { + reason = + `Governor portfolio keeps current ownership in ${currentOwnerRepository} while the vi-history distributor ` + + `dependency for ${viHistoryDistributorDependencyTargetRepository} remains blocked` + + (viHistoryDistributorDependencyExternalBlocker + ? ` (${viHistoryDistributorDependencyExternalBlocker}).` + : '.'); + } else if (viHistoryDistributorDependencyStatus === 'unknown' && viHistoryDistributorDependencyTargetRepository) { + reason = + `Governor portfolio keeps current ownership in ${currentOwnerRepository} until the vi-history distributor ` + + `dependency for ${viHistoryDistributorDependencyTargetRepository} is refreshed.`; + } else { + reason = `Governor portfolio keeps current ownership in ${currentOwnerRepository}.`; + } + } else { + reason = `Governor portfolio assigns current ownership to ${currentOwnerRepository}.`; + } + return { summaryPath, status: currentOwnerRepository === repository ? 'owner-match' : 'external-owner', @@ -473,10 +518,11 @@ async function resolveGovernorPortfolioHandoff({ repoRoot, repository, deps = {} nextAction, ownerDecisionSource, governorMode, - reason: - currentOwnerRepository === repository - ? `Governor portfolio keeps current ownership in ${currentOwnerRepository}.` - : `Governor portfolio assigns current ownership to ${currentOwnerRepository}.` + viHistoryDistributorDependencyStatus, + viHistoryDistributorDependencyTargetRepository, + viHistoryDistributorDependencyExternalBlocker, + viHistoryDistributorDependencyPublicationState, + reason }; } @@ -508,8 +554,29 @@ async function resolveGovernorPortfolioPivotExecution({ const nextAction = normalizeText(governorPortfolioHandoff?.nextAction) || null; const ownerDecisionSource = normalizeText(governorPortfolioHandoff?.ownerDecisionSource) || null; const governorMode = normalizeText(governorPortfolioHandoff?.governorMode) || null; + const viHistoryDistributorDependencyStatus = + normalizeText(governorPortfolioHandoff?.viHistoryDistributorDependencyStatus) || null; + const viHistoryDistributorDependencyTargetRepository = + normalizeText(governorPortfolioHandoff?.viHistoryDistributorDependencyTargetRepository) || null; + const viHistoryDistributorDependencyExternalBlocker = + normalizeText(governorPortfolioHandoff?.viHistoryDistributorDependencyExternalBlocker) || null; + const viHistoryDistributorDependencyPublicationState = + normalizeText(governorPortfolioHandoff?.viHistoryDistributorDependencyPublicationState) || null; if (!nextOwnerRepository || nextOwnerRepository.toLowerCase() === currentRepository.toLowerCase()) { + let reason = `Governor portfolio keeps repo-context ownership in ${currentRepository}.`; + if (viHistoryDistributorDependencyStatus === 'blocked' && viHistoryDistributorDependencyTargetRepository) { + reason = + `Governor portfolio keeps repo-context ownership in ${currentRepository} while the vi-history distributor ` + + `dependency for ${viHistoryDistributorDependencyTargetRepository} remains blocked` + + (viHistoryDistributorDependencyExternalBlocker + ? ` (${viHistoryDistributorDependencyExternalBlocker}).` + : '.'); + } else if (viHistoryDistributorDependencyStatus === 'unknown' && viHistoryDistributorDependencyTargetRepository) { + reason = + `Governor portfolio keeps repo-context ownership in ${currentRepository} until the vi-history distributor ` + + `dependency for ${viHistoryDistributorDependencyTargetRepository} is refreshed.`; + } return { status: 'same-repository', registryPath: null, @@ -519,12 +586,16 @@ async function resolveGovernorPortfolioPivotExecution({ nextAction, ownerDecisionSource, governorMode, + viHistoryDistributorDependencyStatus, + viHistoryDistributorDependencyTargetRepository, + viHistoryDistributorDependencyExternalBlocker, + viHistoryDistributorDependencyPublicationState, targetEntrypointPath: null, targetHeadSha: null, targetCheckoutState: null, targetReceipts: null, targetCurrentState: null, - reason: `Governor portfolio keeps repo-context ownership in ${currentRepository}.` + reason }; } @@ -538,6 +609,10 @@ async function resolveGovernorPortfolioPivotExecution({ nextAction, ownerDecisionSource, governorMode, + viHistoryDistributorDependencyStatus, + viHistoryDistributorDependencyTargetRepository, + viHistoryDistributorDependencyExternalBlocker, + viHistoryDistributorDependencyPublicationState, targetEntrypointPath: null, targetHeadSha: null, targetCheckoutState: null, @@ -573,6 +648,10 @@ async function resolveGovernorPortfolioPivotExecution({ nextAction, ownerDecisionSource, governorMode, + viHistoryDistributorDependencyStatus, + viHistoryDistributorDependencyTargetRepository, + viHistoryDistributorDependencyExternalBlocker, + viHistoryDistributorDependencyPublicationState, targetEntrypointPath: null, targetHeadSha: null, targetCheckoutState: null, @@ -592,6 +671,10 @@ async function resolveGovernorPortfolioPivotExecution({ nextAction, ownerDecisionSource, governorMode, + viHistoryDistributorDependencyStatus, + viHistoryDistributorDependencyTargetRepository, + viHistoryDistributorDependencyExternalBlocker, + viHistoryDistributorDependencyPublicationState, targetEntrypointPath: null, targetHeadSha: null, targetCheckoutState: null, @@ -611,6 +694,10 @@ async function resolveGovernorPortfolioPivotExecution({ nextAction, ownerDecisionSource, governorMode, + viHistoryDistributorDependencyStatus, + viHistoryDistributorDependencyTargetRepository, + viHistoryDistributorDependencyExternalBlocker, + viHistoryDistributorDependencyPublicationState, targetEntrypointPath: null, targetHeadSha: null, targetCheckoutState: null, @@ -649,6 +736,10 @@ async function resolveGovernorPortfolioPivotExecution({ nextAction, ownerDecisionSource, governorMode, + viHistoryDistributorDependencyStatus, + viHistoryDistributorDependencyTargetRepository, + viHistoryDistributorDependencyExternalBlocker, + viHistoryDistributorDependencyPublicationState, targetEntrypointPath: normalizeText(entrypoint.path) || null, targetHeadSha: normalizeText(entrypoint.headSha) || null, targetCheckoutState: normalizeText(entrypoint.checkoutState) || null, @@ -733,6 +824,7 @@ function projectConcurrentLaneStatusReceipt(receiptPath, receipt) { const hostedRun = receipt.hostedRun && typeof receipt.hostedRun === 'object' ? receipt.hostedRun : {}; const pullRequest = receipt.pullRequest && typeof receipt.pullRequest === 'object' ? receipt.pullRequest : {}; const mergeQueue = pullRequest.mergeQueue && typeof pullRequest.mergeQueue === 'object' ? pullRequest.mergeQueue : {}; + const executionBundle = receipt.executionBundle && typeof receipt.executionBundle === 'object' ? receipt.executionBundle : {}; return { receiptPath, @@ -755,6 +847,28 @@ function projectConcurrentLaneStatusReceipt(receiptPath, receipt) { enqueuedAt: normalizeText(mergeQueue.enqueuedAt) || null } }, + executionBundle: { + path: normalizeText(executionBundle.path) || null, + schema: normalizeText(executionBundle.schema) || null, + status: normalizeText(executionBundle.status) || null, + cellId: normalizeText(executionBundle.cellId) || null, + laneId: normalizeText(executionBundle.laneId) || null, + cellClass: normalizeText(executionBundle.cellClass) || null, + suiteClass: normalizeText(executionBundle.suiteClass) || null, + executionCellLeaseId: normalizeText(executionBundle.executionCellLeaseId) || null, + dockerLaneLeaseId: normalizeText(executionBundle.dockerLaneLeaseId) || null, + harnessKind: normalizeText(executionBundle.harnessKind) || null, + harnessInstanceId: normalizeText(executionBundle.harnessInstanceId) || null, + planeBinding: normalizeText(executionBundle.planeBinding) || null, + premiumSaganMode: executionBundle.premiumSaganMode === true, + reciprocalLinkReady: executionBundle.reciprocalLinkReady === true, + effectiveBillableRateUsdPerHour: Number.isFinite(executionBundle.effectiveBillableRateUsdPerHour) + ? executionBundle.effectiveBillableRateUsdPerHour + : null, + operatorAuthorizationRef: normalizeText(executionBundle.operatorAuthorizationRef) || null, + isolatedLaneGroupId: normalizeText(executionBundle.isolatedLaneGroupId) || null, + fingerprintSha256: normalizeText(executionBundle.fingerprintSha256) || null + }, summary: { laneCount: coercePositiveInteger(summary.laneCount) ?? 0, activeLaneCount: coercePositiveInteger(summary.activeLaneCount) ?? 0, @@ -763,6 +877,9 @@ function projectConcurrentLaneStatusReceipt(receiptPath, receipt) { deferredLaneCount: coercePositiveInteger(summary.deferredLaneCount) ?? 0, manualLaneCount: coercePositiveInteger(summary.manualLaneCount) ?? 0, shadowLaneCount: coercePositiveInteger(summary.shadowLaneCount) ?? 0, + executionBundleStatus: normalizeText(summary.executionBundleStatus) || null, + executionBundleReciprocalLinkReady: summary.executionBundleReciprocalLinkReady === true, + executionBundlePremiumSaganMode: summary.executionBundlePremiumSaganMode === true, pullRequestStatus: normalizeText(summary.pullRequestStatus) || null, orchestratorDisposition: normalizeText(summary.orchestratorDisposition) || null } @@ -1074,7 +1191,16 @@ async function planCompareviRuntimeStepFromLiveStanding({ repoRoot, targetReposi let reason = classification.message; if (governorPortfolioHandoff.status === 'owner-match') { - if ( + if (normalizeText(governorPortfolioHandoff.viHistoryDistributorDependencyStatus) === 'blocked') { + const dependencyTarget = + normalizeText(governorPortfolioHandoff.viHistoryDistributorDependencyTargetRepository) || + 'the canonical template'; + const dependencyBlocker = normalizeText(governorPortfolioHandoff.viHistoryDistributorDependencyExternalBlocker); + reason = + `standing queue is empty; governor portfolio keeps ownership in ${governorPortfolioHandoff.currentOwnerRepository} ` + + `while the vi-history distributor dependency for ${dependencyTarget} remains blocked` + + (dependencyBlocker ? ` (${dependencyBlocker}).` : '.'); + } else if ( normalizeText(governorPortfolioHandoff.nextOwnerRepository) && normalizeText(governorPortfolioHandoff.nextOwnerRepository).toLowerCase() !== targetRepository.toLowerCase() && ['future-agent-may-pivot', 'reopen-template-monitoring-work'].includes( @@ -1296,6 +1422,15 @@ async function buildCompareviTaskPacket({ repoRoot, schedulerDecision, preparedW deps, selectedProviderId: workerProviderSelection.selectedProviderId }); + const executionTopology = buildExecutionTopologyRuntimeState({ + providerSelection: workerProviderSelection, + workerSlotId: + normalizeText(workerBranch?.slotId) || + normalizeText(workerReady?.slotId) || + normalizeText(preparedWorker?.slotId) || + null, + concurrentLaneStatus + }); return { source: 'comparevi-runtime', @@ -1386,6 +1521,7 @@ async function buildCompareviTaskPacket({ repoRoot, schedulerDecision, preparedW backlog: artifacts.backlogRepair ?? null, concurrentLaneApply, concurrentLaneStatus, + executionTopology, planeTransition, localReviewLoop, liveAgentModelSelection, diff --git a/tools/priority/sagan-context-concentrator.mjs b/tools/priority/sagan-context-concentrator.mjs new file mode 100644 index 000000000..99ff5781e --- /dev/null +++ b/tools/priority/sagan-context-concentrator.mjs @@ -0,0 +1,747 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)); +const DEFAULT_REPO_ROOT = path.resolve(MODULE_DIR, '..', '..'); + +export const REPORT_SCHEMA = 'priority/sagan-context-concentrator-report@v1'; +export const SUBAGENT_EPISODE_SCHEMA = 'priority/subagent-episode-report@v1'; +export const DEFAULT_PRIORITY_CACHE_PATH = '.agent_priority_cache.json'; +export const DEFAULT_GOVERNOR_SUMMARY_PATH = path.join( + 'tests', + 'results', + '_agent', + 'handoff', + 'autonomous-governor-summary.json' +); +export const DEFAULT_GOVERNOR_PORTFOLIO_SUMMARY_PATH = path.join( + 'tests', + 'results', + '_agent', + 'handoff', + 'autonomous-governor-portfolio-summary.json' +); +export const DEFAULT_MONITORING_MODE_PATH = path.join( + 'tests', + 'results', + '_agent', + 'handoff', + 'monitoring-mode.json' +); +export const DEFAULT_OPERATOR_STEERING_EVENT_PATH = path.join( + 'tests', + 'results', + '_agent', + 'handoff', + 'operator-steering-event.json' +); +export const DEFAULT_EPISODE_DIR = path.join( + 'tests', + 'results', + '_agent', + 'memory', + 'subagent-episodes' +); +export const DEFAULT_OUTPUT_PATH = path.join( + 'tests', + 'results', + '_agent', + 'handoff', + 'sagan-context-concentrator.json' +); + +function normalizeText(value) { + if (value == null) { + return null; + } + const normalized = String(value).trim(); + return normalized.length > 0 ? normalized : null; +} + +function normalizeFiniteNumber(value) { + if (value == null || value === '') { + return null; + } + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; +} + +function normalizeInteger(value) { + if (value == null || value === '') { + return null; + } + const parsed = Number(value); + return Number.isInteger(parsed) && parsed > 0 ? parsed : null; +} + +function toPortablePath(filePath) { + return String(filePath).replace(/\\/g, '/'); +} + +function toRelative(repoRoot, targetPath) { + return path.relative(repoRoot, path.resolve(targetPath)).replace(/\\/g, '/'); +} + +function toDisplayPath(repoRoot, targetPath) { + if (!targetPath) { + return null; + } + const relative = path.relative(path.resolve(repoRoot), path.resolve(targetPath)); + if (!relative.startsWith('..') && !path.isAbsolute(relative)) { + return toPortablePath(relative); + } + return toPortablePath(path.resolve(targetPath)); +} + +function writeJson(filePath, payload) { + const resolvedPath = path.resolve(filePath); + fs.mkdirSync(path.dirname(resolvedPath), { recursive: true }); + fs.writeFileSync(resolvedPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); + return resolvedPath; +} + +function readOptionalJson(filePath) { + const resolvedPath = path.resolve(filePath); + if (!fs.existsSync(resolvedPath)) { + return null; + } + return JSON.parse(fs.readFileSync(resolvedPath, 'utf8')); +} + +function ensureSchema(payload, filePath, schema) { + if (payload?.schema !== schema) { + throw new Error(`Expected ${schema} at ${filePath}.`); + } + return payload; +} + +function isEpisodeActive(episode) { + const status = normalizeText(episode?.summary?.status)?.toLowerCase(); + return !['completed', 'pass', 'success', 'closed', 'retired'].includes(status || ''); +} + +function buildEpisodeDigest(episode, repoRoot, filePath) { + return { + episodeId: normalizeText(episode?.episodeId), + generatedAt: normalizeText(episode?.generatedAt), + agentId: normalizeText(episode?.agent?.id), + agentName: normalizeText(episode?.agent?.name), + agentRole: normalizeText(episode?.agent?.role), + status: normalizeText(episode?.summary?.status), + taskSummary: normalizeText(episode?.task?.summary), + nextAction: normalizeText(episode?.summary?.nextAction), + blocker: normalizeText(episode?.summary?.blocker), + executionPlane: normalizeText(episode?.execution?.executionPlane), + dockerLaneId: normalizeText(episode?.execution?.dockerLaneId), + sourcePath: toDisplayPath(repoRoot, filePath) + }; +} + +function makeMemoryItem({ + id, + kind, + label, + status, + detail = null, + sourcePath = null, + updatedAt = null, + issueNumber = null, + repository = null, + agentName = null, + nextAction = null +}) { + return { + id: normalizeText(id), + kind: normalizeText(kind), + label: normalizeText(label), + status: normalizeText(status), + detail: normalizeText(detail), + sourcePath: normalizeText(sourcePath), + updatedAt: normalizeText(updatedAt), + issueNumber: normalizeInteger(issueNumber), + repository: normalizeText(repository), + agentName: normalizeText(agentName), + nextAction: normalizeText(nextAction) + }; +} + +function addUniqueMemoryItem(collection, item, seenIds) { + if (!item?.id || seenIds.has(item.id)) { + return false; + } + collection.push(item); + seenIds.add(item.id); + return true; +} + +function sortEpisodesDescending(entries) { + return [...entries].sort((left, right) => { + const leftTime = Date.parse(left.episode.generatedAt || 0); + const rightTime = Date.parse(right.episode.generatedAt || 0); + return rightTime - leftTime; + }); +} + +function listEpisodeFiles(directoryPath) { + if (!fs.existsSync(directoryPath) || !fs.statSync(directoryPath).isDirectory()) { + return []; + } + return fs + .readdirSync(directoryPath, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.json')) + .map((entry) => path.join(directoryPath, entry.name)) + .sort(); +} + +function readEpisodes(repoRoot, episodeDirPath, readJsonFn = readOptionalJson) { + const episodeFiles = listEpisodeFiles(episodeDirPath); + const validEpisodes = []; + const invalidEpisodes = []; + + for (const episodePath of episodeFiles) { + try { + const payload = readJsonFn(episodePath); + ensureSchema(payload, episodePath, SUBAGENT_EPISODE_SCHEMA); + validEpisodes.push({ path: episodePath, episode: payload }); + } catch (error) { + invalidEpisodes.push({ + path: toDisplayPath(repoRoot, episodePath), + error: error.message + }); + } + } + + return { + files: episodeFiles, + validEpisodes, + invalidEpisodes + }; +} + +function countByStatus(entries) { + const counts = new Map(); + for (const entry of entries) { + const status = normalizeText(entry.episode?.summary?.status) || 'unknown'; + counts.set(status, (counts.get(status) || 0) + 1); + } + return [...counts.entries()] + .map(([status, count]) => ({ status, count })) + .sort((left, right) => left.status.localeCompare(right.status)); +} + +function countByAgent(entries) { + const counts = new Map(); + for (const entry of entries) { + const agentId = normalizeText(entry.episode?.agent?.id) || 'unknown'; + const agentName = normalizeText(entry.episode?.agent?.name); + const key = `${agentId}::${agentName || ''}`; + const current = counts.get(key) || { + agentId, + agentName, + count: 0 + }; + current.count += 1; + counts.set(key, current); + } + return [...counts.values()].sort((left, right) => { + if (right.count !== left.count) { + return right.count - left.count; + } + return (left.agentName || left.agentId).localeCompare(right.agentName || right.agentId); + }); +} + +function sumEpisodeCost(entries, fieldName) { + return entries.reduce((sum, entry) => { + const value = normalizeFiniteNumber(entry.episode?.cost?.[fieldName]); + return sum + (value ?? 0); + }, 0); +} + +function deriveOwnerSummary(governorSummary, governorPortfolioSummary, monitoringMode) { + return { + currentOwnerRepository: + normalizeText(governorPortfolioSummary?.summary?.currentOwnerRepository) || + normalizeText(governorSummary?.summary?.currentOwnerRepository) || + normalizeText(monitoringMode?.policy?.compareRepository), + nextOwnerRepository: + normalizeText(governorPortfolioSummary?.summary?.nextOwnerRepository) || + normalizeText(governorSummary?.summary?.nextOwnerRepository), + nextAction: + normalizeText(governorPortfolioSummary?.summary?.nextAction) || + normalizeText(governorSummary?.summary?.nextAction), + governorMode: + normalizeText(governorSummary?.summary?.governorMode) || + normalizeText(governorPortfolioSummary?.summary?.governorMode), + monitoringStatus: + normalizeText(governorSummary?.summary?.monitoringStatus) || + normalizeText(monitoringMode?.summary?.status) + }; +} + +function deriveFocus(priorityCache, ownerSummary) { + const number = normalizeInteger(priorityCache?.number); + return { + activeIssue: number + ? { + number, + title: normalizeText(priorityCache?.title), + url: normalizeText(priorityCache?.url), + state: normalizeText(priorityCache?.state), + repository: normalizeText(priorityCache?.repository) + } + : null, + currentOwnerRepository: ownerSummary.currentOwnerRepository, + nextOwnerRepository: ownerSummary.nextOwnerRepository, + nextAction: ownerSummary.nextAction, + governorMode: ownerSummary.governorMode, + monitoringStatus: ownerSummary.monitoringStatus + }; +} + +function deriveSystemMemoryItems({ + repoRoot, + priorityCachePath, + governorSummaryPath, + governorSummary, + governorPortfolioSummaryPath, + governorPortfolioSummary, + focus +}) { + const items = []; + const seenIds = new Set(); + + if (focus.activeIssue) { + addUniqueMemoryItem( + items, + makeMemoryItem({ + id: `issue-${focus.activeIssue.number}`, + kind: 'active-issue', + label: `#${focus.activeIssue.number}: ${focus.activeIssue.title || 'standing priority'}`, + status: focus.activeIssue.state || 'open', + detail: 'Current standing-priority objective', + sourcePath: toDisplayPath(repoRoot, priorityCachePath), + updatedAt: normalizeText(priorityCachePath ? focus.activeIssue?.updatedAt : null), + issueNumber: focus.activeIssue.number, + repository: focus.activeIssue.repository, + nextAction: focus.nextAction + }), + seenIds + ); + } + + addUniqueMemoryItem( + items, + makeMemoryItem({ + id: 'governor-owner-decision', + kind: 'owner-decision', + label: focus.currentOwnerRepository + ? `Owner: ${focus.currentOwnerRepository}` + : 'Owner decision unavailable', + status: focus.governorMode || 'unknown', + detail: focus.nextOwnerRepository + ? `Next owner ${focus.nextOwnerRepository}` + : 'No next-owner decision recorded', + sourcePath: toDisplayPath(repoRoot, governorPortfolioSummaryPath || governorSummaryPath), + updatedAt: + normalizeText(governorPortfolioSummary?.generatedAt) || normalizeText(governorSummary?.generatedAt), + repository: focus.currentOwnerRepository, + nextAction: focus.nextAction + }), + seenIds + ); + + const releaseBlocker = normalizeText(governorSummary?.summary?.releaseSigningExternalBlocker); + const releasePublishedBundleState = normalizeText(governorSummary?.summary?.releasePublishedBundleState); + if (releaseBlocker || releasePublishedBundleState) { + addUniqueMemoryItem( + items, + makeMemoryItem({ + id: 'release-publication-blocker', + kind: 'blocker', + label: releaseBlocker || `Published bundle ${releasePublishedBundleState}`, + status: normalizeText(governorSummary?.summary?.releaseSigningStatus) || 'warn', + detail: normalizeText(governorSummary?.summary?.releasePublicationState), + sourcePath: toDisplayPath(repoRoot, governorSummaryPath), + updatedAt: normalizeText(governorSummary?.generatedAt), + issueNumber: focus.activeIssue?.number, + repository: focus.currentOwnerRepository, + nextAction: focus.nextAction + }), + seenIds + ); + } + + const dependencyStatus = normalizeText(governorPortfolioSummary?.summary?.viHistoryDistributorDependencyStatus); + if (dependencyStatus) { + addUniqueMemoryItem( + items, + makeMemoryItem({ + id: 'vi-history-distributor-dependency', + kind: 'dependency', + label: `vi-history dependency ${dependencyStatus}`, + status: dependencyStatus, + detail: + normalizeText(governorPortfolioSummary?.summary?.viHistoryDistributorDependencyExternalBlocker) || + normalizeText(governorPortfolioSummary?.summary?.viHistoryDistributorDependencyPublishedBundleState), + sourcePath: toDisplayPath(repoRoot, governorPortfolioSummaryPath), + updatedAt: normalizeText(governorPortfolioSummary?.generatedAt), + repository: + normalizeText(governorPortfolioSummary?.summary?.viHistoryDistributorDependencyTargetRepository), + nextAction: focus.nextAction + }), + seenIds + ); + } + + return { items, seenIds }; +} + +function deriveEpisodeMemoryItems(sortedEpisodes, repoRoot, seenIds) { + const hotEpisodes = []; + const warmEpisodes = []; + const usedEpisodeIds = new Set(); + + for (const entry of sortedEpisodes) { + const digest = buildEpisodeDigest(entry.episode, repoRoot, entry.path); + const item = makeMemoryItem({ + id: `episode-${digest.episodeId || digest.agentId || digest.generatedAt}`, + kind: 'subagent-episode', + label: `${digest.agentName || digest.agentId || 'subagent'}: ${digest.taskSummary || 'task'}`, + status: digest.status || 'reported', + detail: digest.blocker || digest.executionPlane, + sourcePath: digest.sourcePath, + updatedAt: digest.generatedAt, + issueNumber: normalizeInteger(entry.episode?.task?.issueNumber), + repository: normalizeText(entry.episode?.repository), + agentName: digest.agentName, + nextAction: digest.nextAction + }); + + if (usedEpisodeIds.has(item.id) || seenIds.has(item.id)) { + continue; + } + + if (isEpisodeActive(entry.episode) && hotEpisodes.length < 3) { + hotEpisodes.push(item); + usedEpisodeIds.add(item.id); + seenIds.add(item.id); + continue; + } + + if (warmEpisodes.length < 5) { + warmEpisodes.push(item); + usedEpisodeIds.add(item.id); + continue; + } + } + + const archiveCount = Math.max(sortedEpisodes.length - usedEpisodeIds.size, 0); + return { hotEpisodes, warmEpisodes, archiveCount }; +} + +function buildReport({ + repoRoot, + priorityCachePath, + priorityCache, + governorSummaryPath, + governorSummary, + governorPortfolioSummaryPath, + governorPortfolioSummary, + monitoringModePath, + monitoringMode, + operatorSteeringEventPath, + operatorSteeringEvent, + episodeDirPath, + episodes, + now +}) { + const ownerSummary = deriveOwnerSummary(governorSummary, governorPortfolioSummary, monitoringMode); + const focus = deriveFocus(priorityCache, ownerSummary); + const { items: systemItems, seenIds } = deriveSystemMemoryItems({ + repoRoot, + priorityCachePath, + governorSummaryPath, + governorSummary, + governorPortfolioSummaryPath, + governorPortfolioSummary, + focus + }); + const sortedEpisodes = sortEpisodesDescending(episodes.validEpisodes); + const { hotEpisodes, warmEpisodes, archiveCount } = deriveEpisodeMemoryItems(sortedEpisodes, repoRoot, seenIds); + const hotWorkingSet = [...systemItems, ...hotEpisodes]; + const byStatus = countByStatus(episodes.validEpisodes); + const byAgent = countByAgent(episodes.validEpisodes); + const cost = { + episodeCountWithCost: episodes.validEpisodes.filter( + (entry) => + normalizeFiniteNumber(entry.episode?.cost?.tokenUsd) != null || + normalizeFiniteNumber(entry.episode?.cost?.operatorLaborUsd) != null || + normalizeFiniteNumber(entry.episode?.cost?.blendedLowerBoundUsd) != null + ).length, + tokenUsd: Number(sumEpisodeCost(episodes.validEpisodes, 'tokenUsd').toFixed(6)), + operatorLaborUsd: Number(sumEpisodeCost(episodes.validEpisodes, 'operatorLaborUsd').toFixed(6)), + blendedLowerBoundUsd: Number(sumEpisodeCost(episodes.validEpisodes, 'blendedLowerBoundUsd').toFixed(6)), + observedDurationSeconds: Number(sumEpisodeCost(episodes.validEpisodes, 'observedDurationSeconds').toFixed(3)) + }; + const blockerCount = hotWorkingSet.filter((item) => + ['blocked', 'warn', 'fail', 'unknown', 'producer-native-incomplete'].includes((item.status || '').toLowerCase()) + ).length; + const concentrationStatus = + episodes.invalidEpisodes.length > 0 + ? 'warn' + : governorSummary || governorPortfolioSummary + ? 'pass' + : 'incomplete'; + + return { + schema: REPORT_SCHEMA, + generatedAt: now.toISOString(), + repository: + normalizeText(governorSummary?.repository) || + normalizeText(governorPortfolioSummary?.repository) || + normalizeText(priorityCache?.repository), + inputs: { + priorityCachePath: toDisplayPath(repoRoot, priorityCachePath), + governorSummaryPath: toDisplayPath(repoRoot, governorSummaryPath), + governorPortfolioSummaryPath: toDisplayPath(repoRoot, governorPortfolioSummaryPath), + monitoringModePath: toDisplayPath(repoRoot, monitoringModePath), + operatorSteeringEventPath: toDisplayPath(repoRoot, operatorSteeringEventPath), + episodeDirectoryPath: toDisplayPath(repoRoot, episodeDirPath) + }, + sources: { + priorityCache: { + path: toDisplayPath(repoRoot, priorityCachePath), + exists: Boolean(priorityCache) + }, + governorSummary: { + path: toDisplayPath(repoRoot, governorSummaryPath), + exists: Boolean(governorSummary) + }, + governorPortfolioSummary: { + path: toDisplayPath(repoRoot, governorPortfolioSummaryPath), + exists: Boolean(governorPortfolioSummary) + }, + monitoringMode: { + path: toDisplayPath(repoRoot, monitoringModePath), + exists: Boolean(monitoringMode) + }, + operatorSteeringEvent: { + path: toDisplayPath(repoRoot, operatorSteeringEventPath), + exists: Boolean(operatorSteeringEvent) + }, + episodeDirectory: { + path: toDisplayPath(repoRoot, episodeDirPath), + exists: fs.existsSync(episodeDirPath), + fileCount: episodes.files.length, + validEpisodeCount: episodes.validEpisodes.length, + invalidEpisodeCount: episodes.invalidEpisodes.length + } + }, + focus, + memory: { + hotWorkingSet, + warmMemory: warmEpisodes, + archiveCount + }, + episodes: { + totalCount: episodes.files.length, + validCount: episodes.validEpisodes.length, + invalidCount: episodes.invalidEpisodes.length, + invalidEpisodes: episodes.invalidEpisodes, + byStatus, + byAgent, + recent: sortedEpisodes.slice(0, 5).map((entry) => buildEpisodeDigest(entry.episode, repoRoot, entry.path)) + }, + cost, + summary: { + status: + normalizeText(governorSummary?.summary?.governorMode) === 'monitoring-active' ? 'monitoring' : 'active', + concentrationStatus, + currentOwnerRepository: focus.currentOwnerRepository, + nextOwnerRepository: focus.nextOwnerRepository, + nextAction: focus.nextAction, + activeIssueNumber: normalizeInteger(focus.activeIssue?.number), + hotWorkingSetCount: hotWorkingSet.length, + warmMemoryCount: warmEpisodes.length, + archiveCount, + blockerCount, + recentEpisodeCount: episodes.validEpisodes.length, + blendedLowerBoundUsd: cost.blendedLowerBoundUsd + } + }; +} + +export function parseArgs(argv = process.argv) { + const args = argv.slice(2); + const options = { + repoRoot: DEFAULT_REPO_ROOT, + priorityCachePath: DEFAULT_PRIORITY_CACHE_PATH, + governorSummaryPath: DEFAULT_GOVERNOR_SUMMARY_PATH, + governorPortfolioSummaryPath: DEFAULT_GOVERNOR_PORTFOLIO_SUMMARY_PATH, + monitoringModePath: DEFAULT_MONITORING_MODE_PATH, + operatorSteeringEventPath: DEFAULT_OPERATOR_STEERING_EVENT_PATH, + episodeDirectoryPath: DEFAULT_EPISODE_DIR, + outputPath: DEFAULT_OUTPUT_PATH, + help: false + }; + + const stringFlags = new Map([ + ['--repo-root', 'repoRoot'], + ['--priority-cache', 'priorityCachePath'], + ['--governor-summary', 'governorSummaryPath'], + ['--governor-portfolio-summary', 'governorPortfolioSummaryPath'], + ['--monitoring-mode', 'monitoringModePath'], + ['--operator-steering-event', 'operatorSteeringEventPath'], + ['--episode-directory', 'episodeDirectoryPath'], + ['--output', 'outputPath'] + ]); + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === '-h' || token === '--help') { + options.help = true; + continue; + } + + if (stringFlags.has(token)) { + const next = args[index + 1]; + if (!next || next.startsWith('-')) { + throw new Error(`Missing value for ${token}.`); + } + index += 1; + options[stringFlags.get(token)] = next; + continue; + } + + throw new Error(`Unknown option: ${token}`); + } + + return options; +} + +function printHelp() { + console.log('Usage: node tools/priority/sagan-context-concentrator.mjs [options]'); + console.log(''); + console.log('Options:'); + console.log(' --repo-root Repository root.'); + console.log(` --priority-cache Priority cache (default: ${DEFAULT_PRIORITY_CACHE_PATH}).`); + console.log(` --governor-summary Governor summary (default: ${DEFAULT_GOVERNOR_SUMMARY_PATH}).`); + console.log( + ` --governor-portfolio-summary Governor portfolio summary (default: ${DEFAULT_GOVERNOR_PORTFOLIO_SUMMARY_PATH}).` + ); + console.log(` --monitoring-mode Monitoring mode report (default: ${DEFAULT_MONITORING_MODE_PATH}).`); + console.log( + ` --operator-steering-event Operator steering event (default: ${DEFAULT_OPERATOR_STEERING_EVENT_PATH}).` + ); + console.log(` --episode-directory Subagent episode directory (default: ${DEFAULT_EPISODE_DIR}).`); + console.log(` --output Output path (default: ${DEFAULT_OUTPUT_PATH}).`); + console.log(' -h, --help Show this help text.'); +} + +export async function runSaganContextConcentrator(options = {}, deps = {}) { + const repoRoot = path.resolve(options.repoRoot || DEFAULT_REPO_ROOT); + const priorityCachePath = path.resolve(repoRoot, options.priorityCachePath || DEFAULT_PRIORITY_CACHE_PATH); + const governorSummaryPath = path.resolve(repoRoot, options.governorSummaryPath || DEFAULT_GOVERNOR_SUMMARY_PATH); + const governorPortfolioSummaryPath = path.resolve( + repoRoot, + options.governorPortfolioSummaryPath || DEFAULT_GOVERNOR_PORTFOLIO_SUMMARY_PATH + ); + const monitoringModePath = path.resolve(repoRoot, options.monitoringModePath || DEFAULT_MONITORING_MODE_PATH); + const operatorSteeringEventPath = path.resolve( + repoRoot, + options.operatorSteeringEventPath || DEFAULT_OPERATOR_STEERING_EVENT_PATH + ); + const episodeDirPath = path.resolve(repoRoot, options.episodeDirectoryPath || DEFAULT_EPISODE_DIR); + const outputPath = path.resolve(repoRoot, options.outputPath || DEFAULT_OUTPUT_PATH); + + const readOptionalJsonFn = deps.readOptionalJsonFn || readOptionalJson; + const writeJsonFn = deps.writeJsonFn || writeJson; + const now = deps.now || new Date(); + + const priorityCache = readOptionalJsonFn(priorityCachePath); + const governorSummary = readOptionalJsonFn(governorSummaryPath); + const governorPortfolioSummary = readOptionalJsonFn(governorPortfolioSummaryPath); + const monitoringMode = readOptionalJsonFn(monitoringModePath); + const operatorSteeringEvent = readOptionalJsonFn(operatorSteeringEventPath); + const episodes = readEpisodes(repoRoot, episodeDirPath, readOptionalJsonFn); + + if (governorSummary) { + ensureSchema(governorSummary, governorSummaryPath, 'priority/autonomous-governor-summary-report@v1'); + } + if (governorPortfolioSummary) { + ensureSchema( + governorPortfolioSummary, + governorPortfolioSummaryPath, + 'priority/autonomous-governor-portfolio-summary-report@v1' + ); + } + if (monitoringMode) { + ensureSchema(monitoringMode, monitoringModePath, 'agent-handoff/monitoring-mode-v1'); + } + + const report = buildReport({ + repoRoot, + priorityCachePath, + priorityCache, + governorSummaryPath, + governorSummary, + governorPortfolioSummaryPath, + governorPortfolioSummary, + monitoringModePath, + monitoringMode, + operatorSteeringEventPath, + operatorSteeringEvent, + episodeDirPath, + episodes, + now + }); + + const writtenPath = writeJsonFn(outputPath, report); + return { report, outputPath: writtenPath }; +} + +export async function main(argv = process.argv) { + let options; + try { + options = parseArgs(argv); + } catch (error) { + console.error(`[sagan-context-concentrator] ${error.message}`); + printHelp(); + return 1; + } + + if (options.help) { + printHelp(); + return 0; + } + + try { + const { report, outputPath } = await runSaganContextConcentrator(options); + console.log( + `[sagan-context-concentrator] wrote ${outputPath} (${report.summary.concentrationStatus}, hot=${report.summary.hotWorkingSetCount})` + ); + return 0; + } catch (error) { + console.error(`[sagan-context-concentrator] ${error.message}`); + return 1; + } +} + +const modulePath = path.resolve(fileURLToPath(import.meta.url)); +const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null; +if (invokedPath && invokedPath === modulePath) { + main(process.argv) + .then((code) => { + if (code !== 0) { + process.exitCode = code; + } + }) + .catch((error) => { + console.error(`[sagan-context-concentrator] ${error.message}`); + process.exitCode = 1; + }); +} diff --git a/tools/priority/subagent-episode.mjs b/tools/priority/subagent-episode.mjs new file mode 100644 index 000000000..4fb0bb9ba --- /dev/null +++ b/tools/priority/subagent-episode.mjs @@ -0,0 +1,292 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)); +const DEFAULT_REPO_ROOT = path.resolve(MODULE_DIR, '..', '..'); + +export const REPORT_SCHEMA = 'priority/subagent-episode-report@v1'; +export const DEFAULT_OUTPUT_DIR = path.join( + 'tests', + 'results', + '_agent', + 'memory', + 'subagent-episodes' +); + +function normalizeText(value) { + if (value == null) { + return null; + } + const normalized = String(value).trim(); + return normalized.length > 0 ? normalized : null; +} + +function sanitizeSegment(value, fallback = 'episode') { + return String(value || '') + .trim() + .replace(/[^A-Za-z0-9._-]+/g, '-') + .replace(/^-+|-+$/g, '') || fallback; +} + +function normalizeStringArray(value) { + if (!Array.isArray(value)) { + return []; + } + return value.map((entry) => normalizeText(entry)).filter(Boolean); +} + +function normalizeFiniteNumber(value) { + if (value == null || value === '') { + return null; + } + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; +} + +function normalizeInteger(value) { + if (value == null || value === '') { + return null; + } + const parsed = Number(value); + return Number.isInteger(parsed) && parsed > 0 ? parsed : null; +} + +function normalizeObject(value) { + return value && typeof value === 'object' && !Array.isArray(value) ? value : null; +} + +function toPortablePath(filePath) { + return String(filePath).replace(/\\/g, '/'); +} + +function toDisplayPath(repoRoot, filePath) { + if (!filePath) { + return null; + } + const resolvedRepoRoot = path.resolve(repoRoot); + const resolvedPath = path.resolve(filePath); + const relative = path.relative(resolvedRepoRoot, resolvedPath); + if (!relative.startsWith('..') && !path.isAbsolute(relative)) { + return toPortablePath(relative); + } + return toPortablePath(resolvedPath); +} + +function isValidDateTime(value) { + return typeof value === 'string' && !Number.isNaN(Date.parse(value)); +} + +function readJsonFile(filePath) { + const resolvedPath = path.resolve(filePath); + return JSON.parse(fs.readFileSync(resolvedPath, 'utf8')); +} + +function writeJsonFile(filePath, payload) { + const resolvedPath = path.resolve(filePath); + fs.mkdirSync(path.dirname(resolvedPath), { recursive: true }); + fs.writeFileSync(resolvedPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); + return resolvedPath; +} + +export function parseArgs(argv = process.argv) { + const args = argv.slice(2); + const options = { + repoRoot: DEFAULT_REPO_ROOT, + inputPath: null, + outputPath: null, + help: false + }; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === '-h' || token === '--help') { + options.help = true; + continue; + } + + if (token === '--repo-root' || token === '--input' || token === '--output') { + const next = args[index + 1]; + if (!next || next.startsWith('-')) { + throw new Error(`Missing value for ${token}.`); + } + index += 1; + if (token === '--repo-root') { + options.repoRoot = next; + } else if (token === '--input') { + options.inputPath = next; + } else if (token === '--output') { + options.outputPath = next; + } + continue; + } + + throw new Error(`Unknown option: ${token}`); + } + + if (!options.help && !normalizeText(options.inputPath)) { + throw new Error('--input is required.'); + } + + return options; +} + +function buildDefaultOutputPath(repoRoot, report) { + const timestamp = report.generatedAt.replace(/[:.]/g, '-'); + const agentSlug = sanitizeSegment(report.agent.name || report.agent.id || 'subagent', 'subagent'); + const issueSlug = Number.isInteger(report.task.issueNumber) ? `issue-${report.task.issueNumber}` : 'no-issue'; + return path.resolve( + repoRoot, + DEFAULT_OUTPUT_DIR, + `${timestamp}-${agentSlug}-${issueSlug}.json` + ); +} + +export function buildSubagentEpisodeReport(input, options = {}) { + const repoRoot = path.resolve(options.repoRoot || DEFAULT_REPO_ROOT); + const now = options.now || new Date(); + const source = normalizeObject(input) || {}; + const agent = normalizeObject(source.agent) || {}; + const task = normalizeObject(source.task) || {}; + const execution = normalizeObject(source.execution) || {}; + const summary = normalizeObject(source.summary) || {}; + const evidence = normalizeObject(source.evidence) || {}; + const cost = normalizeObject(source.cost) || {}; + const generatedAt = isValidDateTime(source.generatedAt) ? source.generatedAt : now.toISOString(); + const issueNumber = + normalizeInteger(task.issueNumber) ?? + normalizeInteger(source.issueNumber) ?? + null; + + const report = { + schema: REPORT_SCHEMA, + generatedAt, + repository: normalizeText(source.repository), + inputs: { + sourcePath: toDisplayPath(repoRoot, options.inputPath || null) + }, + episodeId: + normalizeText(source.episodeId) || + `${sanitizeSegment(agent.name || agent.id || 'subagent', 'subagent')}-${generatedAt.replace(/[:.]/g, '-')}`, + agent: { + id: normalizeText(agent.id), + name: normalizeText(agent.name), + role: normalizeText(agent.role), + model: normalizeText(agent.model) + }, + task: { + summary: normalizeText(task.summary) || '(unspecified task)', + class: normalizeText(task.class), + issueNumber, + issueUrl: normalizeText(task.issueUrl) || normalizeText(source.issueUrl) + }, + execution: { + status: normalizeText(execution.status) || 'completed', + lane: normalizeText(execution.lane), + branch: normalizeText(execution.branch), + executionPlane: normalizeText(execution.executionPlane), + dockerLaneId: normalizeText(execution.dockerLaneId), + hostCapabilityLeaseId: normalizeText(execution.hostCapabilityLeaseId) + }, + summary: { + status: normalizeText(summary.status) || normalizeText(source.status) || 'reported', + outcome: normalizeText(summary.outcome), + blocker: normalizeText(summary.blocker), + nextAction: normalizeText(summary.nextAction), + detail: normalizeText(summary.detail) + }, + evidence: { + filesTouched: normalizeStringArray(evidence.filesTouched), + receipts: normalizeStringArray(evidence.receipts), + commands: normalizeStringArray(evidence.commands), + notes: normalizeStringArray(evidence.notes) + }, + cost: { + observedDurationSeconds: + normalizeFiniteNumber(cost.observedDurationSeconds) ?? + normalizeFiniteNumber(cost.elapsedSeconds), + tokenUsd: normalizeFiniteNumber(cost.tokenUsd), + operatorLaborUsd: normalizeFiniteNumber(cost.operatorLaborUsd), + blendedLowerBoundUsd: + normalizeFiniteNumber(cost.blendedLowerBoundUsd) ?? + normalizeFiniteNumber(cost.blendedUsd) + } + }; + + return report; +} + +export async function runSubagentEpisode(options = {}, deps = {}) { + const repoRoot = path.resolve(options.repoRoot || DEFAULT_REPO_ROOT); + const inputPath = path.resolve(repoRoot, options.inputPath); + const readJsonFn = deps.readJsonFn || readJsonFile; + const writeJsonFn = deps.writeJsonFn || writeJsonFile; + const now = deps.now || new Date(); + + const input = readJsonFn(inputPath); + const report = buildSubagentEpisodeReport(input, { + repoRoot, + inputPath, + now + }); + const outputPath = path.resolve( + repoRoot, + options.outputPath || buildDefaultOutputPath(repoRoot, report) + ); + const writtenPath = writeJsonFn(outputPath, report); + return { report, outputPath: writtenPath }; +} + +function printHelp() { + console.log('Usage: node tools/priority/subagent-episode.mjs [options]'); + console.log(''); + console.log('Options:'); + console.log(' --repo-root Repository root (default: current repo).'); + console.log(' --input Required input JSON path describing a subagent episode.'); + console.log(` --output Optional output path (default under ${DEFAULT_OUTPUT_DIR}).`); + console.log(' -h, --help Show this help text.'); +} + +export async function main(argv = process.argv) { + let options; + try { + options = parseArgs(argv); + } catch (error) { + console.error(`[subagent-episode] ${error.message}`); + printHelp(); + return 1; + } + + if (options.help) { + printHelp(); + return 0; + } + + try { + const { report, outputPath } = await runSubagentEpisode(options); + console.log( + `[subagent-episode] wrote ${outputPath} (${report.agent.name || report.agent.id || 'subagent'} -> ${report.summary.status})` + ); + return 0; + } catch (error) { + console.error(`[subagent-episode] ${error.message}`); + return 1; + } +} + +const modulePath = path.resolve(fileURLToPath(import.meta.url)); +const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null; +if (invokedPath && invokedPath === modulePath) { + main(process.argv) + .then((code) => { + if (code !== 0) { + process.exitCode = code; + } + }) + .catch((error) => { + console.error(`[subagent-episode] ${error.message}`); + process.exitCode = 1; + }); +} diff --git a/tools/priority/supply-chain-trust-gate.mjs b/tools/priority/supply-chain-trust-gate.mjs index 9df964422..c844a46a6 100644 --- a/tools/priority/supply-chain-trust-gate.mjs +++ b/tools/priority/supply-chain-trust-gate.mjs @@ -32,8 +32,10 @@ const FAILURE_HINTS = { 'tag-ref-lookup-failed': 'Unable to resolve release tag via GitHub API. Confirm tag exists and token has repo read access.', 'tag-object-lookup-failed': 'Unable to resolve annotated tag object via GitHub API. Confirm tag object availability.', 'tag-signature-parse-failed': 'Tag signature payload could not be parsed. Re-run and inspect gh api output.', - 'tag-not-annotated': 'Release tag is lightweight/non-annotated. Create a signed annotated tag for release.', - 'tag-signature-unverified': 'Release tag signature is not verified. Sign the tag and re-run the release.', + 'tag-not-annotated': + 'Release tag is lightweight/non-annotated. Run priority:release:signing:readiness first; if readiness is ready, rerun .github/workflows/release-conductor.yml with repair_existing_tag = true to recreate the same release tag as a signed annotated tag.', + 'tag-signature-unverified': + 'Release tag signature is not verified. Run priority:release:signing:readiness first; if readiness is ready, rerun .github/workflows/release-conductor.yml with repair_existing_tag = true to repair the same release tag before rerunning release.', 'attestation-cli-unavailable': 'GitHub CLI is unavailable. Install/enable gh on runner before trust gate.', 'attestation-output-parse-failed': 'Attestation verification output was not valid JSON. Re-run verification and inspect gh logs.', 'attestation-unverified': 'Artifact attestation verification failed. Confirm attest-build-provenance step and signer workflow.', diff --git a/tools/schemas/definitions.ts b/tools/schemas/definitions.ts index 9514fcea0..15e30fc77 100644 --- a/tools/schemas/definitions.ts +++ b/tools/schemas/definitions.ts @@ -306,35 +306,145 @@ const compareCliSchema = cliInfoSchema; const comparePolicySchema = z.enum(['lv-first', 'cli-first', 'cli-only', 'lv-only']); +const testStandCompareOutcomeSchema = z + .object({ + exitCode: z.number(), + seconds: z.number().optional(), + command: z.string().optional(), + diff: z.boolean().optional(), + }) + .nullable(); + +const testStandCompareNodeSchema = z.object({ + events: z.string().min(1), + capture: z.union([z.string().min(1), z.null()]), + report: z.boolean(), + command: z.string().min(1).optional(), + cliPath: z.string().min(1).optional(), + cli: compareCliSchema.optional(), + staging: z + .object({ + enabled: z.boolean(), + root: z.union([z.string().min(1), z.null()]), + }) + .optional(), + allowSameLeaf: z.boolean().optional(), + policy: comparePolicySchema.optional(), + mode: z.string().min(1).optional(), + autoCli: z.boolean().optional(), + sameName: z.boolean().optional(), + timeoutSeconds: z.number().min(0).optional(), +}); + +const testStandExecutionCellSchema = z.object({ + cellId: z.string().min(1).nullable().optional(), + leaseId: z.string().min(1).nullable().optional(), + leasePath: z.string().min(1).nullable().optional(), + agentId: z.string().min(1).nullable().optional(), + agentClass: z.enum(['sagan', 'subagent', 'other']).nullable().optional(), + cellClass: z.enum(['worker', 'coordinator', 'kernel-coordinator']).nullable().optional(), + suiteClass: z.enum(['single-compare', 'dual-plane-parity']).nullable().optional(), + planeBinding: z.string().min(1).nullable().optional(), + runtimeSurface: z.literal('windows-native-teststand').nullable().optional(), + premiumSaganMode: z.boolean().optional(), + operatorAuthorizationRef: z.string().min(1).nullable().optional(), + workingRoot: z.string().min(1).nullable().optional(), + artifactRoot: z.string().min(1).nullable().optional(), + isolatedLaneGroupId: z.string().min(1).nullable().optional(), + hostOsFingerprintSha256: hexSha256.nullable().optional(), +}); + +const testStandProcessModelSchema = z.object({ + runtimeSurface: z.literal('windows-native-teststand'), + processModelClass: z.enum(['sequential-process-model', 'parallel-process-model']), + windowsOnly: z.literal(true), + rootHarnessInstanceId: z.string().min(1), + planeCount: z.number().int().min(1), +}); + +const testStandHarnessInstanceSchema = z.object({ + harnessKind: z.string().min(1), + instanceId: z.string().min(1), + role: z.enum(['single-plane', 'coordinator', 'plane-child']), + processModelClass: z.enum(['sequential-process-model', 'parallel-process-model']), + planeBinding: z.string().min(1).nullable().optional(), + parentInstanceId: z.string().min(1).nullable().optional(), +}); + +const testStandPlaneSessionSchema = z.object({ + plane: z.string().min(1), + architecture: z.enum(['32-bit', '64-bit']), + labviewExePath: z.union([z.string().min(1), z.null()]).optional(), + outputRoot: z.string().min(1), + warmup: z.object({ + mode: warmupModeSchema, + events: warmupEventsSchema, + }), + compare: testStandCompareNodeSchema, + outcome: testStandCompareOutcomeSchema, + error: z.union([z.string().min(1), z.null()]).optional(), + exitCode: z.number(), + executionCell: testStandExecutionCellSchema.nullable().optional(), + harnessInstance: testStandHarnessInstanceSchema.nullable().optional(), + processModel: testStandProcessModelSchema.optional(), +}); + +const testStandParitySummarySchema = z.object({ + status: z.enum(['match', 'mismatch', 'incomplete']), + comparedFields: z.array(z.string().min(1)), + exitCodeParity: z.boolean().nullable().optional(), + diffParity: z.boolean().nullable().optional(), + mismatchCount: z.number().int().min(0), + mismatches: z.array( + z.object({ + field: z.string().min(1), + x64: z.union([z.string().min(1), z.number(), z.boolean(), z.null()]).optional(), + x32: z.union([z.string().min(1), z.number(), z.boolean(), z.null()]).optional(), + }), + ), +}); + const testStandCompareSessionSchema = z.object({ - schema: z.literal('teststand-compare-session/v1'), + schema: z.enum(['teststand-compare-session/v1', 'teststand-compare-session/v2']), at: isoString, warmup: z.object({ mode: warmupModeSchema, events: warmupEventsSchema, }), - compare: z.object({ - events: z.string().min(1), - capture: z.union([z.string().min(1), z.null()]), - report: z.boolean(), - command: z.string().min(1).optional(), - cliPath: z.string().min(1).optional(), - cli: compareCliSchema.optional(), - policy: comparePolicySchema.optional(), - mode: z.string().min(1).optional(), - autoCli: z.boolean().optional(), - sameName: z.boolean().optional(), - timeoutSeconds: z.number().min(0).optional(), - }), - outcome: z + compare: testStandCompareNodeSchema, + outcome: testStandCompareOutcomeSchema, + error: z.union([z.string().min(1), z.null()]).optional(), + executionCell: testStandExecutionCellSchema.nullable().optional(), + harnessInstance: testStandHarnessInstanceSchema.nullable().optional(), + processModel: testStandProcessModelSchema.optional(), + suiteClass: z.enum(['single-compare', 'dual-plane-parity']).optional(), + primaryPlane: z.string().min(1).optional(), + requestedSimultaneous: z.boolean().optional(), + planes: z .object({ - exitCode: z.number(), - seconds: z.number().optional(), - command: z.string().optional(), - diff: z.boolean().optional(), + x64: testStandPlaneSessionSchema, + x32: testStandPlaneSessionSchema, }) - .nullable(), - error: z.union([z.string().min(1), z.null()]).optional(), + .optional(), + parity: testStandParitySummarySchema.optional(), +}).superRefine((value, ctx) => { + if (value.schema === 'teststand-compare-session/v2') { + if (!value.suiteClass) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'suiteClass is required for v2 sessions', path: ['suiteClass'] }); + } + if (!value.primaryPlane) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'primaryPlane is required for v2 sessions', path: ['primaryPlane'] }); + } + if (typeof value.requestedSimultaneous !== 'boolean') { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'requestedSimultaneous is required for v2 sessions', path: ['requestedSimultaneous'] }); + } + if (!value.planes) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'planes is required for v2 sessions', path: ['planes'] }); + } + if (!value.parity) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'parity is required for v2 sessions', path: ['parity'] }); + } + } }); const invokerEventSchema = z.object({