diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..f15441a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..b3be88b
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,17 @@
+# Normalize line endings: LF in the repository, LF on checkout.
+* text=auto eol=lf
+
+# Sources that must stay byte-stable (content hashes depend on them)
+*.md text eol=lf
+*.jsonl text eol=lf
+*.json text eol=lf
+*.toml text eol=lf
+*.yml text eol=lf
+
+# Generated artifacts: hide from diffs and language stats
+PHILOSOPHY.lock.json linguist-generated=true
+
+# Binary assets
+*.png binary
+*.jpg binary
+*.gif binary
diff --git a/.github/workflows/agent-e2e.yml b/.github/workflows/agent-e2e.yml
new file mode 100644
index 0000000..f1b5448
--- /dev/null
+++ b/.github/workflows/agent-e2e.yml
@@ -0,0 +1,141 @@
+name: Actual agent validation
+
+on:
+ workflow_dispatch:
+ inputs:
+ candidate_run_id:
+ description: Successful trusted candidate workflow run for this exact source
+ required: true
+ type: string
+ manifest_sha256:
+ description: Approved canonical release manifest digest
+ required: true
+ type: string
+ matrix:
+ description: Validation scope
+ required: true
+ default: smoke
+ type: choice
+ options: [smoke, full]
+ probe:
+ description: Authentication/process probe only (never release evidence)
+ required: true
+ default: true
+ type: boolean
+
+permissions:
+ contents: read
+ actions: read
+
+concurrency:
+ group: agent-validation-${{ github.ref }}-${{ inputs.matrix }}-${{ inputs.probe }}
+ cancel-in-progress: false
+
+jobs:
+ actual-agents:
+ if: github.repository == 'shendeguize/AgentOrganon' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release/1.0.0')
+ environment: agent-validation
+ timeout-minutes: 150
+ strategy:
+ fail-fast: false
+ matrix:
+ agent: [codex, claude, cursor, copilot, gemini, opencode]
+ os: ${{ fromJSON(inputs.matrix == 'full' && '["ubuntu-24.04"]' || '["ubuntu-24.04", "macos-15", "windows-2025"]') }}
+ runs-on: ${{ matrix.os }}
+ env:
+ RELEASE_MANIFEST_SHA256: ${{ inputs.manifest_sha256 }}
+ CANDIDATE_RUN_ID: ${{ inputs.candidate_run_id }}
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
+ with:
+ ref: ${{ github.sha }}
+ persist-credentials: false
+ submodules: false
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+ with:
+ node-version: '22.23.2'
+ - name: Verify trusted source workflow before downloading artifacts
+ id: candidate
+ shell: bash
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ run: node scripts/agents/verify-candidate.mjs --run-id "$CANDIDATE_RUN_ID"
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
+ with:
+ artifact-ids: ${{ steps.candidate.outputs.candidate_artifact_id }}
+ merge-multiple: true
+ run-id: ${{ inputs.candidate_run_id }}
+ github-token: ${{ github.token }}
+ path: candidate
+ - name: Verify approved manifest and clean source identity
+ shell: bash
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ SELECTED_ATTEMPT: ${{ steps.candidate.outputs.candidate_run_attempt }}
+ SELECTED_ARTIFACT: ${{ steps.candidate.outputs.candidate_artifact_id }}
+ run: node scripts/agents/verify-candidate.mjs --run-id "$CANDIDATE_RUN_ID" --attempt "$SELECTED_ATTEMPT" --artifact-id "$SELECTED_ARTIFACT" --manifest candidate/release-manifest.json
+ - name: Set temporary agent tools directory
+ shell: bash
+ run: node -e 'require("node:fs").appendFileSync(process.env.GITHUB_ENV,"ORGANON_AGENT_TOOLS="+require("node:path").join(process.env.RUNNER_TEMP,"organon-agent-tools")+"\n")'
+ - name: Install pinned CLI in temporary home without credentials
+ shell: bash
+ run: node scripts/agents/setup.mjs --agent "${{ matrix.agent }}"
+ - name: Actual codex invocation
+ if: matrix.agent == 'codex'
+ shell: bash
+ env:
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ CODEX_MODEL: ${{ vars.CODEX_MODEL }}
+ run: node scripts/agents/run.mjs --agent codex --matrix "${{ inputs.matrix }}" ${{ inputs.probe && '--probe' || '' }} --package-dir candidate --manifest candidate/release-manifest.json --out "candidate/evidence/codex-${{ runner.os }}-${{ inputs.matrix }}" || test "$?" -eq 3
+ - name: Actual claude invocation
+ if: matrix.agent == 'claude'
+ shell: bash
+ env:
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_MODEL: ${{ vars.ANTHROPIC_MODEL }}
+ run: node scripts/agents/run.mjs --agent claude --matrix "${{ inputs.matrix }}" ${{ inputs.probe && '--probe' || '' }} --package-dir candidate --manifest candidate/release-manifest.json --out "candidate/evidence/claude-${{ runner.os }}-${{ inputs.matrix }}" || test "$?" -eq 3
+ - name: Actual cursor invocation
+ if: matrix.agent == 'cursor'
+ shell: bash
+ env:
+ CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
+ CURSOR_MODEL: ${{ vars.CURSOR_MODEL }}
+ run: node scripts/agents/run.mjs --agent cursor --matrix "${{ inputs.matrix }}" ${{ inputs.probe && '--probe' || '' }} --package-dir candidate --manifest candidate/release-manifest.json --out "candidate/evidence/cursor-${{ runner.os }}-${{ inputs.matrix }}" || test "$?" -eq 3
+ - name: Actual copilot invocation
+ if: matrix.agent == 'copilot'
+ shell: bash
+ env:
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ COPILOT_MODEL: ${{ vars.COPILOT_MODEL }}
+ run: node scripts/agents/run.mjs --agent copilot --matrix "${{ inputs.matrix }}" ${{ inputs.probe && '--probe' || '' }} --package-dir candidate --manifest candidate/release-manifest.json --out "candidate/evidence/copilot-${{ runner.os }}-${{ inputs.matrix }}" || test "$?" -eq 3
+ - name: Actual gemini invocation
+ if: matrix.agent == 'gemini'
+ shell: bash
+ env:
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
+ GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
+ run: node scripts/agents/run.mjs --agent gemini --matrix "${{ inputs.matrix }}" ${{ inputs.probe && '--probe' || '' }} --package-dir candidate --manifest candidate/release-manifest.json --out "candidate/evidence/gemini-${{ runner.os }}-${{ inputs.matrix }}" || test "$?" -eq 3
+ - name: Actual opencode invocation
+ if: matrix.agent == 'opencode'
+ shell: bash
+ env:
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ OPENCODE_MODEL: ${{ vars.OPENCODE_MODEL }}
+ run: node scripts/agents/run.mjs --agent opencode --matrix "${{ inputs.matrix }}" ${{ inputs.probe && '--probe' || '' }} --package-dir candidate --manifest candidate/release-manifest.json --out "candidate/evidence/opencode-${{ runner.os }}-${{ inputs.matrix }}" || test "$?" -eq 3
+ # Exit 3 means completed invocation awaiting independent review, never release approval.
+ - name: Preserve raw captured evidence including failures and pending review
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ with:
+ name: agent-${{ matrix.agent }}-${{ runner.os }}-${{ inputs.matrix }}-${{ github.run_attempt }}
+ path: candidate/evidence/
+ if-no-files-found: warn
+ retention-days: 30
+ - name: Preserve CLI installation diagnostics
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ with:
+ name: agent-setup-${{ matrix.agent }}-${{ runner.os }}-${{ github.run_attempt }}
+ path: ${{ runner.temp }}/organon-agent-tools/${{ matrix.agent }}/installation.json
+ if-no-files-found: warn
+ retention-days: 30
diff --git a/.github/workflows/candidate.yml b/.github/workflows/candidate.yml
new file mode 100644
index 0000000..3f36ae3
--- /dev/null
+++ b/.github/workflows/candidate.yml
@@ -0,0 +1,107 @@
+name: Candidate validation
+on:
+ workflow_dispatch:
+ inputs:
+ approved_rc_version:
+ description: For stable preparation only, exact previously approved RC
+ required: false
+permissions:
+ contents: read
+ actions: read
+concurrency:
+ group: candidate-${{ github.sha }}
+ cancel-in-progress: false
+jobs:
+ build:
+ if: github.repository == 'shendeguize/AgentOrganon' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release/1.0.0')
+ runs-on: ubuntu-24.04
+ timeout-minutes: 45
+ outputs:
+ artifact_id: ${{ steps.packages.outputs.artifact-id }}
+ attempt: ${{ steps.identity.outputs.attempt }}
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
+ with:
+ ref: ${{ github.sha }}
+ submodules: recursive
+ persist-credentials: false
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+ with:
+ node-version: '22.23.2'
+ - run: npm ci --ignore-scripts
+ working-directory: OrganonCore/tools/site
+ - name: Install pinned Lean toolchain in temporary runner storage
+ env:
+ ELAN_HOME: ${{ runner.temp }}/organon-elan
+ run: |
+ mkdir -p "$RUNNER_TEMP/organon-elan-bootstrap"
+ curl --proto '=https' --tlsv1.2 --fail --location https://github.com/leanprover/elan/releases/download/v4.2.4/elan-x86_64-unknown-linux-gnu.tar.gz --output "$RUNNER_TEMP/organon-elan-bootstrap/elan.tar.gz"
+ echo "42b94d4244e8353142c456ec0e4ca6528fd898a6c604d4059f494e706e431f63 $RUNNER_TEMP/organon-elan-bootstrap/elan.tar.gz" | sha256sum --check --strict
+ tar -xzf "$RUNNER_TEMP/organon-elan-bootstrap/elan.tar.gz" -C "$RUNNER_TEMP/organon-elan-bootstrap"
+ "$RUNNER_TEMP/organon-elan-bootstrap/elan-init" -y --no-modify-path --default-toolchain leanprover/lean4:v4.33.1
+ "$ELAN_HOME/bin/elan" toolchain install leanprover/lean4:v4.33.1
+ echo "ELAN_HOME=$ELAN_HOME" >> "$GITHUB_ENV"
+ echo "$ELAN_HOME/bin" >> "$GITHUB_PATH"
+ - name: Source, declaration and reader gates
+ run: |
+ npm test
+ npm --prefix OrganonCore test
+ npm --prefix AdvisedOrganons test
+ node OrganonCore/skills/organon-core-leanify-prove/scripts/check.js OrganonCore/lean/philosophy --manuscript manuscript.json
+ node OrganonCore/skills/organon-core-leanify-prove/scripts/check.js AdvisedOrganons/SoftwareEngineering/lean/philosophy --manuscript manuscript.json
+ - name: Freeze identical channel packages and gate reports
+ env:
+ GH_TOKEN: ${{ github.token }}
+ GOVERNANCE_AUDIT_TOKEN: ${{ secrets.GOVERNANCE_AUDIT_TOKEN }}
+ APPROVED_RC_VERSION: ${{ inputs.approved_rc_version }}
+ APPROVED_RC_MANIFEST_SHA256: ${{ vars.APPROVED_RC_MANIFEST_SHA256 }}
+ run: node scripts/release/candidate.mjs --out candidate
+ - id: identity
+ run: node -e 'require("node:fs").appendFileSync(process.env.GITHUB_OUTPUT,"attempt="+process.env.GITHUB_RUN_ATTEMPT+"\n")'
+ - id: packages
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ with:
+ name: candidate-packages-${{ github.run_attempt }}
+ path: candidate/
+ if-no-files-found: error
+ retention-days: 30
+ - name: Display candidate identity for subsequent explicit validation
+ run: node --input-type=module -e 'import {digest,readJSON} from "./scripts/release/lib.mjs"; console.log(digest(readJSON("candidate/release-manifest.json")))'
+ install:
+ needs: build
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-24.04, macos-15, windows-2025]
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 30
+ steps:
+ - run: |
+ git config --global core.autocrlf false
+ git config --global core.symlinks true
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
+ with:
+ ref: ${{ github.sha }}
+ submodules: recursive
+ persist-credentials: false
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+ with:
+ node-version: '22.23.2'
+ - name: Require a complete rerun attempt
+ env:
+ BUILD_ATTEMPT: ${{ needs.build.outputs.attempt }}
+ run: node -e 'if(process.env.BUILD_ATTEMPT!==process.env.GITHUB_RUN_ATTEMPT) throw Error("Use Rerun all jobs; cannot borrow build artifacts from an older attempt")'
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
+ with:
+ artifact-ids: ${{ needs.build.outputs.artifact_id }}
+ merge-multiple: true
+ path: candidate
+ - name: Actual isolated install lifecycle
+ run: node scripts/release/install-matrix.mjs --manifest candidate/release-manifest.json --out candidate/reports/install-${{ runner.os }}.json
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ if: always()
+ with:
+ name: install-${{ runner.os }}-${{ github.run_attempt }}
+ path: candidate/reports/install-*
+ if-no-files-found: error
+ retention-days: 30
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..5b72869
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,83 @@
+name: Repository
+on:
+ pull_request:
+ push:
+ branches: [main, 'dev*', 'dev**/**', 'release*', 'release**/**', 'codex/**']
+ workflow_dispatch:
+permissions:
+ contents: read
+concurrency:
+ group: repository-${{ github.ref }}
+ cancel-in-progress: true
+jobs:
+ install-platforms:
+ name: Installer / ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-24.04, macos-15, windows-2025]
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 30
+ steps:
+ - run: |
+ git config --global core.autocrlf false
+ git config --global core.symlinks true
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
+ with:
+ submodules: recursive
+ persist-credentials: false
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+ with:
+ node-version: '22.23.2'
+ - name: Native isolated package and installer lifecycle
+ run: npm run test:install
+ verify:
+ needs: install-platforms
+ name: Repository / verify
+ runs-on: ubuntu-24.04
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
+ with:
+ path: product
+ persist-credentials: false
+ submodules: recursive
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+ with:
+ node-version: '22.23.2'
+ - name: Install pinned Lean toolchain in temporary runner storage
+ env:
+ ELAN_HOME: ${{ runner.temp }}/organon-elan
+ run: |
+ mkdir -p "$RUNNER_TEMP/organon-elan-bootstrap"
+ curl --proto '=https' --tlsv1.2 --fail --location https://github.com/leanprover/elan/releases/download/v4.2.4/elan-x86_64-unknown-linux-gnu.tar.gz --output "$RUNNER_TEMP/organon-elan-bootstrap/elan.tar.gz"
+ echo "42b94d4244e8353142c456ec0e4ca6528fd898a6c604d4059f494e706e431f63 $RUNNER_TEMP/organon-elan-bootstrap/elan.tar.gz" | sha256sum --check --strict
+ tar -xzf "$RUNNER_TEMP/organon-elan-bootstrap/elan.tar.gz" -C "$RUNNER_TEMP/organon-elan-bootstrap"
+ "$RUNNER_TEMP/organon-elan-bootstrap/elan-init" -y --no-modify-path --default-toolchain leanprover/lean4:v4.33.1
+ "$ELAN_HOME/bin/elan" toolchain install leanprover/lean4:v4.33.1
+ echo "ELAN_HOME=$ELAN_HOME" >> "$GITHUB_ENV"
+ echo "$ELAN_HOME/bin" >> "$GITHUB_PATH"
+ - name: Source tests and human-document links
+ working-directory: product
+ env:
+ ORGANON_WORKSPACE_ROOT: ${{ github.workspace }}/product
+ ORGANON_CORE_ROOT: ${{ github.workspace }}/product/OrganonCore
+ run: |
+ npm test
+ npm run check:content
+ - name: Install locked site build dependencies
+ working-directory: product/OrganonCore/tools/site
+ run: npm ci --ignore-scripts
+ - name: Build and validate released-site paths
+ working-directory: product
+ env:
+ ORGANON_CORE_ROOT: ${{ github.workspace }}/product/OrganonCore
+ run: npm run check:site
+ - name: Check current Lean evidence and four reader editions
+ run: node product/OrganonCore/skills/organon-core-leanify-prove/scripts/check.js product/OrganonCore/lean/philosophy --manuscript manuscript.json
+ - name: Keep development site preview
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ with:
+ name: development-site-AgentOrganon-${{ github.run_attempt }}
+ path: product/dist/site/
+ if-no-files-found: error
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..d35f59d
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,62 @@
+name: Verified release Pages
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: Published product version
+ required: true
+ type: string
+ manifest_sha256:
+ description: Canonical digest of the fully published release manifest
+ required: true
+ type: string
+permissions: {}
+concurrency:
+ group: site-data-${{ github.repository }}
+ cancel-in-progress: false
+jobs:
+ prepare:
+ if: github.repository == 'shendeguize/AgentOrganon' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release/1.0.0')
+ runs-on: ubuntu-24.04
+ timeout-minutes: 45
+ permissions:
+ contents: write
+ outputs:
+ deploy: ${{ steps.state.outputs.deploy }}
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
+ with:
+ persist-credentials: false
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+ with:
+ node-version: '22.23.2'
+ - name: Verify all channels and build fixed released sources
+ id: state
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ GH_TOKEN: ${{ github.token }}
+ GOVERNANCE_AUDIT_TOKEN: ${{ secrets.GOVERNANCE_AUDIT_TOKEN }}
+ RELEASE_VERSION: ${{ inputs.version }}
+ MANIFEST_SHA256: ${{ inputs.manifest_sha256 }}
+ run: node scripts/release/site-data.mjs pages --version "$RELEASE_VERSION" --manifest-sha256 "$MANIFEST_SHA256"
+ - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa
+ if: steps.state.outputs.deploy == 'true'
+ with:
+ name: github-pages-${{ github.run_attempt }}
+ path: ${{ steps.state.outputs.public_dir }}
+ deploy:
+ needs: prepare
+ if: needs.prepare.outputs.deploy == 'true'
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e
+ id: deployment
+ with:
+ artifact_name: github-pages-${{ github.run_attempt }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..e1e91e4
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,103 @@
+name: Publish verified product
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: Exact validated product version
+ required: true
+ manifest_sha256:
+ description: Canonical sealed manifest digest
+ required: true
+ operation:
+ description: Controlled publication step
+ required: true
+ type: choice
+ options: [bootstrap-bundle, publish, promote]
+ bootstrap_npm:
+ description: Use the short-lived first-publication credential
+ type: boolean
+ default: false
+ validated_run_id:
+ description: Successful seal.yml run from this exact source (initial bundle only)
+ required: false
+permissions:
+ contents: read
+concurrency:
+ group: publish-${{ inputs.version }}
+ cancel-in-progress: false
+jobs:
+ publish:
+ if: github.ref == 'refs/heads/release/1.0.0'
+ environment: ${{ startsWith(inputs.version, '1.0.0-rc.') && 'npm-rc' || 'npm-stable' }}
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: write
+ actions: read
+ id-token: write
+ env:
+ RELEASE_VERSION: ${{ inputs.version }}
+ RELEASE_MANIFEST_SHA256: ${{ inputs.manifest_sha256 }}
+ APPROVED_RC_MANIFEST_SHA256: ${{ vars.APPROVED_RC_MANIFEST_SHA256 }}
+ NPM_CHANNEL_STRATEGY: ${{ vars.NPM_CHANNEL_STRATEGY }}
+ RELEASE_OPERATION: ${{ inputs.operation }}
+ GH_TOKEN: ${{ github.token }}
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
+ with:
+ ref: ${{ github.sha }}
+ path: product
+ persist-credentials: false
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+ with:
+ node-version: '22.23.2'
+ registry-url: https://registry.npmjs.org
+ - name: Install exact npm with OIDC support in the temporary runner
+ run: npm install --global npm@12.0.2 --ignore-scripts
+ - name: Verify sealed artifact provenance
+ id: sealed
+ if: inputs.operation == 'bootstrap-bundle'
+ env:
+ VALIDATED_RUN: ${{ inputs.validated_run_id }}
+ run: |
+ node --input-type=module -e 'import fs from "node:fs"; import {resolveAttempt} from "./product/scripts/release/actions-artifacts.mjs"; const {run,artifacts}=resolveAttempt(process.env.VALIDATED_RUN,"seal.yml",process.env.GITHUB_SHA); fs.appendFileSync(process.env.GITHUB_OUTPUT,"artifact_id="+artifacts[0].id+"\nrun_attempt="+run.run_attempt+"\n");'
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
+ if: inputs.operation == 'bootstrap-bundle'
+ with:
+ artifact-ids: ${{ steps.sealed.outputs.artifact_id }}
+ merge-multiple: true
+ run-id: ${{ inputs.validated_run_id }}
+ github-token: ${{ github.token }}
+ path: candidate
+ - name: Bind downloaded inventory to selected seal attempt
+ if: inputs.operation == 'bootstrap-bundle'
+ env:
+ SELECTED_RUN: ${{ inputs.validated_run_id }}
+ SELECTED_ATTEMPT: ${{ steps.sealed.outputs.run_attempt }}
+ run: node --input-type=module -e 'import {readJSON} from "./product/scripts/release/lib.mjs"; import {readTrustedInventory} from "./product/scripts/release/evidence.mjs"; const {inventory}=readTrustedInventory(readJSON("candidate/release-manifest.json"),"candidate",{required:true}); if(inventory.collector.run_id!==process.env.SELECTED_RUN || inventory.collector.run_attempt!==process.env.SELECTED_ATTEMPT) throw Error("Sealed artifact belongs to another run attempt");'
+ - name: Fetch exact public validated bundle
+ if: inputs.operation != 'bootstrap-bundle'
+ run: node product/scripts/release/publish.mjs fetch --version "$RELEASE_VERSION" --manifest-sha256 "$RELEASE_MANIFEST_SHA256" --out candidate
+ - name: Verify identity and every gate before credentials are exposed
+ run: |
+ node product/scripts/release/manifest.mjs check --manifest candidate/release-manifest.json
+ node --input-type=module -e 'import {readJSON} from "./product/scripts/release/lib.mjs"; import {assertDispatch} from "./product/scripts/release/manifest.mjs"; assertDispatch(readJSON("candidate/release-manifest.json"),process.env,"agent-organon")'
+ - name: Verify approved RC to stable source transition
+ if: inputs.version == '1.0.0'
+ run: node product/scripts/release/stable.mjs --manifest candidate/release-manifest.json
+ - name: First npm publication with short-lived bootstrap credential
+ if: inputs.operation == 'publish' && inputs.bootstrap_npm
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_BOOTSTRAP_TOKEN }}
+ GOVERNANCE_AUDIT_TOKEN: ${{ secrets.GOVERNANCE_AUDIT_TOKEN }}
+ run: node product/scripts/release/publish.mjs publish --manifest candidate/release-manifest.json --product agent-organon
+ - name: OIDC publication or verified bundle operation
+ if: inputs.operation != 'promote' && !(inputs.operation == 'publish' && inputs.bootstrap_npm)
+ env:
+ GOVERNANCE_AUDIT_TOKEN: ${{ secrets.GOVERNANCE_AUDIT_TOKEN }}
+ run: node product/scripts/release/publish.mjs "$RELEASE_OPERATION" --manifest candidate/release-manifest.json --product agent-organon
+ - name: Complete channel recommendation only after all three products match
+ if: inputs.operation == 'promote'
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TAG_TOKEN }}
+ GOVERNANCE_AUDIT_TOKEN: ${{ secrets.GOVERNANCE_AUDIT_TOKEN }}
+ run: node product/scripts/release/publish.mjs promote --manifest candidate/release-manifest.json --product agent-organon
diff --git a/.github/workflows/seal.yml b/.github/workflows/seal.yml
new file mode 100644
index 0000000..5ee9cac
--- /dev/null
+++ b/.github/workflows/seal.yml
@@ -0,0 +1,58 @@
+name: Seal independently reviewed candidate
+on:
+ workflow_dispatch:
+ inputs:
+ candidate_run_id:
+ description: Successful candidate run from this exact source commit
+ required: true
+ agent_run_ids:
+ description: Comma-separated successful actual smoke and full run IDs
+ required: true
+ candidate_manifest_sha256:
+ description: Exact prepared manifest identity reviewed
+ required: true
+ review_commit:
+ description: Full evidence commit with reviewed/ containing only approved summaries, receipts, assessments and reviewer inputs
+ required: true
+permissions:
+ contents: read
+ actions: read
+jobs:
+ seal:
+ if: github.repository == 'shendeguize/AgentOrganon' && github.ref == 'refs/heads/release/1.0.0'
+ environment: release-validation
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
+ with:
+ persist-credentials: false
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+ with:
+ node-version: '22.23.2'
+ - name: Validate immutable review source
+ env:
+ REVIEW_COMMIT: ${{ inputs.review_commit }}
+ run: node -e 'if(!/^[a-f0-9]{40}$/.test(process.env.REVIEW_COMMIT)) throw Error("Full review commit required")'
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
+ with:
+ ref: ${{ inputs.review_commit }}
+ path: review-input
+ persist-credentials: false
+ sparse-checkout: reviewed
+ - name: Collect exact trusted runs and data-only independent reviews
+ env:
+ GH_TOKEN: ${{ github.token }}
+ CANDIDATE_RUN: ${{ inputs.candidate_run_id }}
+ AGENT_RUNS: ${{ inputs.agent_run_ids }}
+ MANIFEST_SHA: ${{ inputs.candidate_manifest_sha256 }}
+ run: node scripts/release/collect.mjs --candidate-run "$CANDIDATE_RUN" --agent-runs "$AGENT_RUNS" --manifest-sha256 "$MANIFEST_SHA" --reviews review-input/reviewed --out candidate
+ - name: Collect passed assessments and require every release gate
+ run: |
+ node scripts/release/assemble.mjs --manifest candidate/release-manifest.json --reports candidate --out candidate/assembled.json
+ node scripts/release/manifest.mjs seal --manifest candidate/assembled.json --out candidate/release-manifest.json
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ with:
+ name: validated-release-${{ github.run_attempt }}
+ path: candidate/
+ if-no-files-found: error
+ retention-days: 30
diff --git a/.github/workflows/stars.yml b/.github/workflows/stars.yml
new file mode 100644
index 0000000..6007f85
--- /dev/null
+++ b/.github/workflows/stars.yml
@@ -0,0 +1,52 @@
+name: Observed stars
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: '17 2 * * *'
+permissions: {}
+concurrency:
+ group: site-data-${{ github.repository }}
+ cancel-in-progress: false
+jobs:
+ prepare:
+ if: github.repository == 'shendeguize/AgentOrganon' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release/1.0.0')
+ runs-on: ubuntu-24.04
+ timeout-minutes: 45
+ permissions:
+ contents: write
+ outputs:
+ deploy: ${{ steps.state.outputs.deploy }}
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
+ with:
+ persist-credentials: false
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+ with:
+ node-version: '22.23.2'
+ - name: Sample actual total and retain released HTML
+ id: state
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ GH_TOKEN: ${{ github.token }}
+ run: node scripts/release/site-data.mjs stars
+ - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa
+ if: steps.state.outputs.deploy == 'true'
+ with:
+ name: github-pages-${{ github.run_attempt }}
+ path: ${{ steps.state.outputs.public_dir }}
+ deploy:
+ needs: prepare
+ if: needs.prepare.outputs.deploy == 'true'
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e
+ id: deployment
+ with:
+ artifact_name: github-pages-${{ github.run_attempt }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c8bfc39
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,34 @@
+# Private goal workspaces (never committed)
+.local/
+
+# Agent / tool state
+.codegraph/
+.claude/settings.local.json
+.cursor/
+.copilot/
+*.log
+
+# Build outputs
+target/
+dist/
+build/
+node_modules/
+__pycache__/
+*.pyc
+.venv/
+
+# Secrets / env
+.env
+.env.*
+!.env.example
+
+# OS / editor noise
+.DS_Store
+Thumbs.db
+*.swp
+*.swo
+*~
+.idea/
+.vscode/
+!.vscode/settings.json
+!.vscode/extensions.json
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..d67d6aa
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "OrganonCore"]
+ path = OrganonCore
+ url = https://github.com/shendeguize/OrganonCore
+[submodule "AdvisedOrganons"]
+ path = AdvisedOrganons
+ url = https://github.com/shendeguize/AdvisedOrganons.git
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..a07c549
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,15 @@
+# Working on AgentOrganon
+
+Read [OrganonCore/AGENTS.md](OrganonCore/AGENTS.md) together with this file, including its linked philosophy, review methods, and [iteration rules](OrganonCore/skills/references/iteration.md). Apply the shared maintenance, independent review, wording, authorization, record, and stopping rules to outer repository work. Core owns Lean implementations, checking tools and shared pure parsing; the outer repository supplies philosophy-management scripts and compatible Lean forwarding entrypoints. Product runtime tools in both repositories have no third-party runtime dependencies. Site building and validation may use locked third-party dependencies, which are excluded from installation packages.
+
+Keep the adopted workspace philosophy, a proposed replacement, and the Core method's constraints distinct. Use the [philosophy resolution contract](OrganonCore/skills/references/philosophy-resolution.md) and [format contract](OrganonCore/skills/references/structure.md). Outer skills pass the selected baseline through delegation. A difference from Core is not automatically a contradiction within an adopter's commitments. Compatibility alone does not warrant adoption, and conflict alone does not defeat revision reasons.
+
+The root `PHILOSOPHY.md` is a read-only symlink to the Core source, with no root derived lock. Resolve its relative references from the real source directory. Do not use philosophy management writes to replace it or reach the Core target by another path. Manage regular adopter copies; modify Core only through its applicable maintenance or absorption workflow and authorization.
+
+Scripts perform deterministic parsing, comparison, and file operations. Agents and users retain philosophical judgments, semantic version decisions, and specific adoption authorization. A prepared application plan binds inputs; it does not certify authorization or philosophical correctness. Run relevant mechanical tests and independent actual skill cases for behavior changes, preserving raw independent responses before comparison with expected boundaries.
+
+Keep changes within the authorized scope and preserve existing user work. Retain private evidence and the shared six-part records under ignored `.local/`; use `.local/iterations/merges/` for merge reports, candidates, decisions, plans, and records. Verify that the directory is ignored before writing records. English documentation is authoritative for its translations; follow [zh/AGENTS.md](zh/AGENTS.md) when maintaining the translation pair. Do not translate SKILL.md files as part of this implementation.
+
+For local records, start with `.local/ACTIVE.md` in the relevant repository. Keep feedback records active. Exclude `.local/archived/` from default recursive searches and do not open archive packages by default. When the task explicitly requires historical material, consult `.local/archived/INDEX.md` and retrieve only the relevant packages or members, following their recorded dependencies and restoration conditions. Archived decisions are historical evidence, not active obligations; later corrections belong in separate records linked to the originals.
+
+Remote publication follows [Core's authorization rules](OrganonCore/AGENTS.md#remote-publication-authorization). Reuse authorization for its stated action, destination, and scope. An implementation request does not itself authorize a push. Do not publish an outer revision that references an unavailable Core commit.
diff --git a/AdvisedOrganons b/AdvisedOrganons
new file mode 160000
index 0000000..6b32ede
--- /dev/null
+++ b/AdvisedOrganons
@@ -0,0 +1 @@
+Subproject commit 6b32edea1619e740e229864da757c25d36096bc1
diff --git a/OrganonCore b/OrganonCore
new file mode 160000
index 0000000..77c1ef0
--- /dev/null
+++ b/OrganonCore
@@ -0,0 +1 @@
+Subproject commit 77c1ef086cc2eb55d192059e97658abc5ca7f95f
diff --git a/PHILOSOPHY.md b/PHILOSOPHY.md
new file mode 120000
index 0000000..223baf6
--- /dev/null
+++ b/PHILOSOPHY.md
@@ -0,0 +1 @@
+OrganonCore/PHILOSOPHY.md
\ No newline at end of file
diff --git a/README.md b/README.md
index edfdc56..a2dff61 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,59 @@
-# AgentOrganon
\ No newline at end of file
+
+
+[English](README.md) · [简体中文](zh/README.md) · [Website](https://shendeguize.github.io/AgentOrganon/) · [Releases](https://github.com/shendeguize/AgentOrganon/releases)
+
+# AgentOrganon
+
+Workspace methods and philosophy management.
+
+## Design Aim · Ground agent judgments and improvements
+
+Make commitments, reasons and boundaries readable, assessable and revisable. Organon supplies philosophy and methods; it does not promise correct judgments or automatic improvement. Philosophical adoption retains an explicit human decision.
+
+## Start here
+
+```sh
+npx --yes @shendeguize/agent-organon@1.0.0-rc.1 install --product agent-organon --agent codex --scope project --project /path/to/workspace --version 1.0.0-rc.1
+```
+
+Release candidate **1.0.0-rc.1** awaits review. Run this command once the public package is available; before publication use a local candidate archive. Requires Node.js 22+. Project and global installation are available; the release matrix records actual validation for the six agent adapters.
+
+[Complete quick start: install → check → select philosophy → read-only assessment](https://shendeguize.github.io/AgentOrganon/quick-start)
+
+## Read and navigate
+
+| Entry | Content |
+| --- | --- |
+| [AgentOrganon](https://github.com/shendeguize/AgentOrganon) | Workspace skills and management of adopted copies. |
+| [OrganonCore](https://github.com/shendeguize/OrganonCore) | Core philosophy, review methods and Lean evidence. |
+| [AdvisedOrganons](https://github.com/shendeguize/AdvisedOrganons) | Domain philosophy collection; select a domain explicitly. |
+| [Docs](https://shendeguize.github.io/AgentOrganon/understand) | Concepts, operations and capability boundaries for readers. |
+| [Philosophy](https://shendeguize.github.io/AgentOrganon/philosophy) | Adopted commitments with their meanings and conditions. |
+| [Lean](https://shendeguize.github.io/AgentOrganon/lean) | Claim overviews and paired code with line explanations. |
+
+Release packages contain philosophy, non-Lean skills and required runtime files. Websites, tutorials, Lean projects and evidence remain in source. Rationale is separate from reader documentation and adds no philosophical obligations; maintenance protocols live in maintenance.
+
+
+## Choose a method
+
+| Method | Purpose |
+| --- | --- |
+| `organon-assess` | Assess an input under the selected philosophy. |
+| `organon-absorb` | Examine reasons for philosophical revision and carry out authorized adoption. |
+| `organon-principled-review` | Analyze an object under the shared method’s stated standards. |
+| `organon-wording-review` | Review wording without deciding philosophical adoption. |
+| `organon-philosophy` | Manage philosophy copies, versions and file operations. |
+
+## Star history
+
+
+
+Daily observations begin when collection is enabled. Zero totals, unstars and missing samples are retained; the chart reports its update time.
+
+Source-checkout operations: [workspace files and script reference](docs/workspace-reference.md).
+
+## Contribute
+
+Read the repository’s agent guidance and maintenance protocols before contributing. Mechanical checks, independent review and human adoption decisions have distinct responsibilities.
+
+MIT · [License](LICENSE)
diff --git a/assets/banner.svg b/assets/banner.svg
new file mode 100644
index 0000000..a08f3cd
--- /dev/null
+++ b/assets/banner.svg
@@ -0,0 +1 @@
+
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 0000000..703cb74
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,47 @@
+# Quick start
+
+Use Node.js 22 or later and your chosen agent tool. These commands target the published candidate package; before publication, use the local candidate archive route below.
+
+## 1. Install the methods
+
+```sh
+npx --yes @shendeguize/agent-organon@1.0.0-rc.1 install --product agent-organon --agent codex --scope project --project /path/to/workspace --version 1.0.0-rc.1
+```
+
+Replace `/path/to/workspace` with your project path. Choose `codex`, `claude`, `cursor`, `copilot`, `gemini` or `opencode` for `--agent`. Start with project installation; use `--scope global` for global installation. Installing methods does not adopt a philosophy.
+
+## 2. Check installation and discovery
+
+```sh
+npx --yes @shendeguize/agent-organon@1.0.0-rc.1 check --product agent-organon --agent codex --scope project --project /path/to/workspace
+```
+
+Open the agent in that project and confirm the method appears in its skill list or can be loaded explicitly. File checks establish installation integrity; actual tool support is recorded in the candidate’s validation matrix. Resolve reported project/global naming conflicts before continuing.
+
+## 3. Select a philosophy explicitly
+
+```sh
+npx --yes @shendeguize/agent-organon@1.0.0-rc.1 init --product agent-organon --agent codex --scope project --project /path/to/workspace --philosophy core
+```
+
+Read the preview, confirm the destination and philosophical text, then repeat the command with `--apply --plan-sha256 ` using the exact `planSha256` value in that preview. A changed philosophy or instruction file requires a new preview and decision. An existing philosophy must not be silently replaced; the collection root does not select a domain. Method installation and project adoption are managed separately.
+
+## 4. Complete a read-only assessment
+
+Give the agent the actual material you want assessed and ask:
+
+> Use `organon-assess` with this project’s explicitly selected `PHILOSOPHY.md` as the adopted baseline. Assess the material’s grounds, applicability and possible conflicts. Return a read-only report; do not edit files or adopt a proposal.
+
+Check that the report identifies its baseline, reasons and limits. An assessment does not authorize philosophical revision; missing independent review must remain explicitly incomplete.
+
+## Offline archives and lifecycle commands
+
+Download and extract the archive from the corresponding GitHub Release. Every product includes `installer/organon.mjs`, which Node can run directly; only the AgentOrganon npm package registers the `organon` command.
+
+```sh
+node /path/to/extracted/package/installer/organon.mjs install --product agent-organon --agent codex --scope project --project /path/to/workspace --from /path/to/shendeguize-agent-organon-1.0.0-rc.1.tgz
+```
+
+The same entrypoint provides `update`, `rollback` and `uninstall`. Check integrity before updating; user modifications or existing-file conflicts stop an update. Uninstall retains user modifications and the adopted project philosophy.
+
+[Understand the concepts and boundaries](https://shendeguize.github.io/AgentOrganon/understand) · [GitHub Releases](https://github.com/shendeguize/AgentOrganon/releases)
diff --git a/docs/understanding.md b/docs/understanding.md
new file mode 100644
index 0000000..cb4a8a1
--- /dev/null
+++ b/docs/understanding.md
@@ -0,0 +1,38 @@
+# Understand Organon
+
+## Start with three distinct objects
+
+**Philosophy** states adopted commitments, their meanings and conditions of application. **Skills** apply methods to tasks and remain revisable. **Tools** check and transform files; they do not decide philosophical correctness or reasons for adoption.
+
+## How an assessment starts
+
+Identify the actual input and adopted philosophy, then state the question to assess. Examine reasons, conditions and scope; distinguish contradiction from insufficient support. When considering a revision, keep the preceding baseline identifiable. Compatibility does not warrant adoption, and conflict does not by itself defeat reasons for revision.
+
+## When change is needed
+
+An ordinary assessment can end with a report. Absorption examines reasons for philosophical revision and implements it after explicit authorization. Wording review concerns expression; file management checks structure and identity. These methods have different responsibilities and do not replace one another’s judgments.
+
+## A map of the reading material
+
+- [Philosophy](https://shendeguize.github.io/AgentOrganon/philosophy): commitments, meanings and limits.
+- [Rationale](https://shendeguize.github.io/AgentOrganon/rationale): arguments, alternatives and objections; no additional philosophical obligations.
+- [Lean reader](https://shendeguize.github.io/AgentOrganon/lean): begin with claims, premises and boundaries, then inspect declarations and line explanations.
+- Maintenance protocols: review, validation and release procedures for maintainers, in the repository’s `maintenance/` directory.
+
+## What Lean can establish
+
+Lean checks formal conclusions under given definitions and assumptions. Target `accepted`, kernel `passed` and source fidelity are different judgments; `limited`, `incomplete` and `not_applicable` retain their recorded meanings. Defining a duty does not show its fulfillment; a finite model does not establish universal real-world correctness. Readers unfamiliar with Lean can start with the claim overview and consult the paired code and explanation pages one line at a time.
+
+## Installation and adoption are separate
+
+Project installation supplies method entrypoints for that project; global installation supplies user-level entrypoints. Neither adopts a philosophy automatically. A project selects its baseline through explicit initialization; updating, rolling back or uninstalling methods does not automatically change that decision. A domain collection requires an explicit domain choice.
+
+## Choose a method
+
+| Method | Purpose |
+| --- | --- |
+| `organon-assess` | Assess an input under the selected philosophy. |
+| `organon-absorb` | Examine reasons for philosophical revision and carry out authorized adoption. |
+| `organon-principled-review` | Analyze an object under the shared method’s stated standards. |
+| `organon-wording-review` | Review wording without deciding philosophical adoption. |
+| `organon-philosophy` | Manage philosophy copies, versions and file operations. |
diff --git a/docs/workspace-reference.md b/docs/workspace-reference.md
new file mode 100644
index 0000000..b903b08
--- /dev/null
+++ b/docs/workspace-reference.md
@@ -0,0 +1,34 @@
+# Workspace files and scripts reference
+
+## Files and versions
+
+An adopting workspace uses a regular `PHILOSOPHY.md` and adjacent `PHILOSOPHY.lock.json`, whose `document_filename` identifies the managed file in that directory. The [format contract](../OrganonCore/skills/references/structure.md) defines the four supported frontmatter fields and stable IDs for every heading with its direct body and for introductory text. It supports unindented ATX headings such as `## Title`; other ATX forms and Setext headings are rejected. The current three-chapter Core organization is an initialization template; adopters may reorganize it. Initialization adds an empty Extensions section, identified by stable ID `extensions` rather than its title or position.
+
+- `format_version` identifies the supported file format. Unsupported formats stop managed writes.
+- `philosophy_version` describes semantic revision, proposed by an agent and decided by the user: changed or withdrawn commitments, meanings, or applicability require major; additions preserving existing commitments require minor; meaning-preserving wording or structural maintenance requires patch.
+- `core_version` records the last fully reviewed Core source version. A version outside `package.json`'s `organon.coreVersion` range produces a source-version warning, not a philosophical verdict.
+- `derived_from` records export provenance. Neither provenance nor matching versions establishes a common ancestor.
+
+The lock stores hashes and structure for reviewed source checkpoints, plus remembered declines. It contains no old source text. Local differences from a checkpoint are expected. A declined source unit is not proposed again until its incoming state changes; the actual difference remains visible. Imports with no verifiable source checkpoint use two-way differences and never advance Core's reviewed version.
+
+This repository's root `PHILOSOPHY.md` is a relative symlink to `OrganonCore/PHILOSOPHY.md`, used only for reading; relative references resolve from the real source directory. There is no root derived lock. Management scripts reject writes through or over this symlink and writes to the Core source. Core changes use its explicit maintenance or absorption workflow.
+
+## Local use
+
+Use Node.js 22; there are no third-party runtime dependencies. Run these commands from this checkout, with the OrganonCore submodule present. Use absolute script paths when working from another directory. The target's parent directory must already exist. In a Git workspace, ignore `.local/` before initialization so recovery journals remain private; the scripts create the journal directory when needed.
+
+```sh
+node scripts/check.js --source OrganonCore/PHILOSOPHY.md
+node scripts/check.js --source PHILOSOPHY.md
+node scripts/init.js --target /path/to/workspace/PHILOSOPHY.md
+node scripts/check.js /path/to/workspace/PHILOSOPHY.md
+node scripts/export.js --source /path/to/workspace/PHILOSOPHY.md --out /path/to/export.md
+node scripts/export.js --source /path/to/workspace/PHILOSOPHY.md --out /path/to/core-part.md --core-only
+node --test tests/*.test.js
+```
+
+Initialization and export require absent output paths. `--core-only` removes the subtree identified by `extensions`; a missing marker stops export. The remaining text is still the adopter's text, not an official Core copy. Source identities are caller-supplied labels or stored document identities, not authenticated publisher identities. Managed operations, including reads for initialization, export, and classification, stop when an input has a pending transaction; use the reported recovery command before resuming.
+
+Use `organon-philosophy merge --dry-run` to request a read-only difference report. The skill invokes `classify.js`; there is no installed `organon-philosophy` executable. The skill documents candidate preparation, decisions, recovery, and application commands. Keep reports, candidates, plans, and six-part merge records in ignored `.local/iterations/merges/`. A prepared plan binds the reviewed inputs and candidate; stale inputs stop application. Preparation and successful file checks do not supply adoption authorization.
+
+[Maintenance and iteration protocol](../maintenance/self-iteration.md)
diff --git a/installer/lib/adapters.mjs b/installer/lib/adapters.mjs
new file mode 100644
index 0000000..01af3c9
--- /dev/null
+++ b/installer/lib/adapters.mjs
@@ -0,0 +1,13 @@
+import path from 'node:path';
+
+// Native discovery paths, checked against the linked vendor documentation.
+// Filesystem installation does not certify authenticated invocation/delegation.
+export const AGENTS = {
+ codex: { project: '.agents/skills', global: '.agents/skills', instructions: 'AGENTS.md', aliases: ['.codex/skills'], command: 'codex', source: 'https://developers.openai.com/codex/skills/' },
+ claude: { project: '.claude/skills', global: '.claude/skills', instructions: 'CLAUDE.md', aliases: [], command: 'claude', source: 'https://code.claude.com/docs/en/skills' },
+ cursor: { project: '.cursor/skills', global: '.cursor/skills', instructions: 'AGENTS.md', aliases: ['.agents/skills', '.claude/skills'], command: 'agent', source: 'https://cursor.com/docs/skills' },
+ copilot: { project: '.github/skills', global: '.copilot/skills', instructions: 'AGENTS.md', aliases: ['.agents/skills', '.claude/skills'], command: 'copilot', source: 'https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference' },
+ gemini: { project: '.gemini/skills', global: '.gemini/skills', instructions: 'GEMINI.md', aliases: ['.agents/skills'], command: 'gemini', source: 'https://geminicli.com/docs/cli/skills/' },
+ opencode: { project: '.opencode/skills', global: '.config/opencode/skills', instructions: 'AGENTS.md', aliases: ['.agents/skills', '.claude/skills'], command: 'opencode', source: 'https://opencode.ai/docs/skills/' },
+};
+export function discoveryRoot(agent, scope, root) { return path.join(root, AGENTS[agent][scope]); }
diff --git a/installer/lib/archive.mjs b/installer/lib/archive.mjs
new file mode 100644
index 0000000..29e0518
--- /dev/null
+++ b/installer/lib/archive.mjs
@@ -0,0 +1,70 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { gzipSync, gunzipSync } from 'node:zlib';
+import { listFiles, safeRelative, write } from './files.mjs';
+
+// A small regular-file-only ustar implementation. Archives never create links,
+// devices or executable lifecycle hooks. Reject extension records and duplicates.
+const text = (buffer, start, length) => buffer.subarray(start, start + length).toString('utf8').replace(/\0.*$/s, '');
+function octal(buffer, start, length) {
+ const value = text(buffer, start, length).trim();
+ if (!/^[0-7]*$/.test(value)) throw new Error('Invalid tar numeric field');
+ return parseInt(value || '0', 8);
+}
+export function extractTarball(file, destination) {
+ const archive = gunzipSync(fs.readFileSync(file), { maxOutputLength: 64 * 1024 * 1024 });
+ const entries = [];
+ const names = new Set();
+ let offset = 0;
+ for (; offset + 512 <= archive.length; ) {
+ const header = archive.subarray(offset, offset + 512);
+ if (header.every(byte => byte === 0)) break;
+ const expected = octal(header, 148, 8);
+ const sum = header.reduce((total, byte, index) => total + (index >= 148 && index < 156 ? 32 : byte), 0);
+ if (sum !== expected) throw new Error('Invalid tar checksum');
+ const prefix = text(header, 345, 155);
+ const rawName = `${prefix ? `${prefix}/` : ''}${text(header, 0, 100)}`;
+ const type = text(header, 156, 1);
+ if (!['', '0', '5'].includes(type)) throw new Error(`Unsupported tar entry type: ${type}`);
+ const name = safeRelative(rawName.replace(/\/$/, ''));
+ if (name !== 'package' && !name.startsWith('package/')) throw new Error('Archive must use a package/ root');
+ if (names.has(name)) throw new Error(`Duplicate tar entry: ${name}`);
+ names.add(name);
+ const size = octal(header, 124, 12);
+ offset += 512;
+ if (offset + size > archive.length) throw new Error('Truncated tar entry');
+ if (type === '5' && size !== 0) throw new Error('Tar directory has content');
+ if (type !== '5') entries.push([name, archive.subarray(offset, offset + size)]);
+ offset += Math.ceil(size / 512) * 512;
+ }
+ if (archive.subarray(offset).some(byte => byte !== 0)) throw new Error('Invalid tar trailer');
+ for (const [name, bytes] of entries) write(path.join(destination, name), bytes);
+ return path.join(destination, 'package');
+}
+export function createTarball(root, destination) {
+ const chunks = [];
+ for (const name of listFiles(root)) {
+ const full = `package/${name}`;
+ let basename = full, prefix = '';
+ if (Buffer.byteLength(full) > 100) {
+ const split = full.lastIndexOf('/');
+ prefix = full.slice(0, split); basename = full.slice(split + 1);
+ }
+ if (Buffer.byteLength(prefix) > 155 || Buffer.byteLength(basename) > 100) throw new Error(`Path exceeds ustar limits: ${full}`);
+ const bytes = fs.readFileSync(path.join(root, name));
+ const header = Buffer.alloc(512);
+ const field = (value, start, length) => header.write(value, start, length, 'ascii');
+ const number = (value, start, length) => field(`${value.toString(8).padStart(length - 1, '0')}\0`, start, length);
+ header.write(basename, 0, 100, 'utf8');
+ number(name === 'installer/organon.mjs' ? 0o755 : 0o644, 100, 8);
+ number(0, 108, 8); number(0, 116, 8); number(bytes.length, 124, 12); number(0, 136, 12);
+ field(' ', 148, 8); field('0', 156, 1); field('ustar\0', 257, 6); field('00', 263, 2);
+ header.write(prefix, 345, 155, 'utf8');
+ const sum = header.reduce((total, byte) => total + byte, 0);
+ field(`${sum.toString(8).padStart(6, '0')}\0 `, 148, 8);
+ chunks.push(header, bytes, Buffer.alloc((512 - bytes.length % 512) % 512));
+ }
+ chunks.push(Buffer.alloc(1024));
+ write(destination, gzipSync(Buffer.concat(chunks), { level: 9 }));
+ return destination;
+}
diff --git a/installer/lib/files.mjs b/installer/lib/files.mjs
new file mode 100644
index 0000000..05ce01a
--- /dev/null
+++ b/installer/lib/files.mjs
@@ -0,0 +1,40 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { createHash } from 'node:crypto';
+
+export const sha256 = value => createHash('sha256').update(value).digest('hex');
+export const json = file => JSON.parse(fs.readFileSync(file, 'utf8'));
+export const exists = file => { try { fs.lstatSync(file); return true; } catch (error) { if (error.code === 'ENOENT') return false; throw error; } };
+export function write(file, value) {
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ fs.writeFileSync(file, value);
+}
+export const serialize = value => `${JSON.stringify(value, null, 2)}\n`;
+export function safeRelative(value) {
+ if (typeof value !== 'string' || !value || value.includes('\\') || value.includes('\0') || value.split('/').some(part => !part || part === '.' || part === '..') || /^[A-Za-z]:/.test(value)) throw new Error(`Unsafe relative path: ${value}`);
+ return value;
+}
+export function within(root, file) {
+ const relative = path.relative(root, file);
+ if (relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative)) throw new Error(`Path escapes managed root: ${file}`);
+ return file;
+}
+export function noSymlinkAncestors(file) {
+ let cursor = path.resolve(file);
+ for (;;) {
+ if (exists(cursor) && fs.lstatSync(cursor).isSymbolicLink()) throw new Error(`Symbolic link destination is not writable: ${cursor}`);
+ const parent = path.dirname(cursor);
+ if (parent === cursor) return;
+ cursor = parent;
+ }
+}
+export function listFiles(root, prefix = '') {
+ const result = [];
+ for (const entry of fs.readdirSync(path.join(root, prefix), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
+ const name = prefix ? `${prefix}/${entry.name}` : entry.name;
+ if (entry.isDirectory()) result.push(...listFiles(root, name));
+ else if (entry.isFile()) result.push(name);
+ else throw new Error(`Unsupported file type: ${name}`);
+ }
+ return result.sort();
+}
diff --git a/installer/lib/lifecycle.mjs b/installer/lib/lifecycle.mjs
new file mode 100644
index 0000000..9c89e44
--- /dev/null
+++ b/installer/lib/lifecycle.mjs
@@ -0,0 +1,277 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+import { spawnSync } from 'node:child_process';
+import { pathToFileURL } from 'node:url';
+import { AGENTS, discoveryRoot } from './adapters.mjs';
+import { PRODUCTS, openPackage, verifyPackage } from './package.mjs';
+import { exists, json, serialize, sha256, write, within, noSymlinkAncestors, listFiles } from './files.mjs';
+import { locked, recover, transact, matches } from './transaction.mjs';
+
+function context(options) {
+ if (!Object.hasOwn(PRODUCTS, options.product)) throw new Error('--product agent-organon|core|advised is required');
+ if (!Object.hasOwn(AGENTS, options.agent)) throw new Error(`--agent ${Object.keys(AGENTS).join('|')} is required`);
+ if (!['project', 'global'].includes(options.scope)) throw new Error('--scope project|global is required');
+ if (options.scope === 'project' && !options.project) throw new Error('--project is required for project scope');
+ const requestedRoot = options.scope === 'global' ? os.homedir() : path.resolve(options.project);
+ if (!exists(requestedRoot) || !fs.statSync(requestedRoot).isDirectory()) throw new Error(`Installation root must exist: ${requestedRoot}`);
+ const root = fs.realpathSync(requestedRoot);
+ const store = path.join(root, '.organon');
+ noSymlinkAncestors(store);
+ return { ...options, root, store, stateFile: path.join(store, 'installations.json') };
+}
+function stateOf(ctx) {
+ noSymlinkAncestors(ctx.stateFile);
+ const state = exists(ctx.stateFile) ? json(ctx.stateFile) : { schema: 1, products: {} };
+ if (state.schema !== 1 || !state.products || typeof state.products !== 'object') throw new Error('Invalid installation state');
+ for (const [product, entry] of Object.entries(state.products)) {
+ if (!Object.hasOwn(PRODUCTS, product)) throw new Error('Unknown product in installation state');
+ for (const candidate of [entry, entry.previous].filter(Boolean)) {
+ within(path.join(ctx.store, 'packages', product), candidate.directory);
+ noSymlinkAncestors(candidate.directory);
+ if (!/^[a-f0-9]{64}$/.test(candidate.manifestHash)) throw new Error('Invalid installed manifest identity');
+ }
+ for (const [agent, discovery] of Object.entries(entry.discovery ?? {})) {
+ if (!Object.hasOwn(AGENTS, agent)) throw new Error('Unknown agent in installation state');
+ for (const item of discovery) {
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(item.skill) || item.file !== path.join(discoveryRoot(agent, ctx.scope, ctx.root), item.skill, 'SKILL.md')) throw new Error('Invalid installed discovery path');
+ }
+ }
+ }
+ return state;
+}
+function assertCurrent(entry) {
+ const manifest = verifyPackage(entry.directory);
+ if (manifest.manifestHash !== entry.manifestHash) throw new Error('Installed manifest was modified');
+ for (const discovery of Object.values(entry.discovery)) {
+ for (const item of discovery) if (!matches(item.file, item.sha256)) throw new Error(`Installed skill was modified or removed: ${item.file}`);
+ }
+}
+function renderSkill(source, destination, directory) {
+ const payload = path.join(directory, 'payload');
+ const text = fs.readFileSync(source, 'utf8');
+ const canonical = path.dirname(source);
+ let rendered = text.replace(/\]\(([^\s)]+)\)/g, (whole, href) => {
+ if (/^(?:[a-z]+:|#|\/)/i.test(href)) return whole;
+ const [relative, fragment] = href.split('#');
+ const target = path.resolve(canonical, decodeURIComponent(relative));
+ return `](${path.relative(path.dirname(destination), target).split(path.sep).map(part => encodeURIComponent(part)).join('/')}${fragment ? `#${fragment}` : ''})`;
+ });
+ // Keep the caller's cwd unchanged. Script roots are explicit, including the
+ // management skill's formerly repository-relative command examples.
+ rendered = rendered.replace(/node (?:\/)?scripts\/([\w/-]+\.js)/g, (_, script) => `node "${path.join(payload, 'scripts', script)}"`);
+ rendered = rendered.replaceAll('', payload);
+ const end = rendered.indexOf('\n---', 4);
+ const note = `\n\nInstalled runtime directory: \`${payload}\`. Canonical skill directory: \`${canonical}\`. Resolve any remaining repository-relative command or resource from those directories; preserve the caller's workspace for philosophical selection. This installation contains non-Lean methods only.\n`;
+ return rendered.slice(0, end + 4) + note + rendered.slice(end + 4);
+}
+function discoveryFor(ctx, directory, manifest, agents) {
+ const discovery = {};
+ const operations = [];
+ for (const agent of agents) {
+ discovery[agent] = [];
+ for (const skill of manifest.skills) {
+ const file = path.join(discoveryRoot(agent, ctx.scope, ctx.root), skill.name, 'SKILL.md');
+ const content = renderSkill(path.join(directory, skill.path), file, directory);
+ discovery[agent].push({ file, sha256: sha256(content), sourceSha256: manifest.files.find(entry => entry.path === skill.path).sha256, skill: skill.name });
+ operations.push({ file, content });
+ }
+ }
+ return { discovery, operations };
+}
+function collisionCheck(ctx, operations, existing) {
+ const owned = new Map(Object.values(existing?.discovery ?? {}).flat().map(entry => [entry.file, entry.sha256]));
+ for (const { file } of operations) {
+ noSymlinkAncestors(file);
+ if (exists(file) && (!owned.has(file) || !matches(file, owned.get(file)))) throw new Error(`Discovery path conflict: ${file}`);
+ }
+}
+function aliases(ctx, entry) {
+ const warnings = [];
+ for (const [agent, discovered] of Object.entries(entry.discovery)) {
+ for (const skill of discovered) {
+ for (const alias of AGENTS[agent].aliases) {
+ const file = path.join(ctx.root, alias, skill.skill, 'SKILL.md');
+ if (file !== skill.file && exists(file)) {
+ const ownedAlias = Object.values(entry.discovery).flat().find(item => item.file === file);
+ warnings.push({ type: 'alias-discovery', agent, skill: skill.skill, file, sameManagedPayload: Boolean(ownedAlias && matches(file, ownedAlias.sha256)), resolution: 'Inspect both discovery paths. Retain the desired integration and explicitly uninstall an unwanted managed integration; agent precedence is not assumed.' });
+ }
+ }
+ const otherRoot = ctx.scope === 'project' ? os.homedir() : ctx.project && path.resolve(ctx.project);
+ const otherScope = ctx.scope === 'project' ? 'global' : 'project';
+ if (otherRoot) {
+ const file = path.join(discoveryRoot(agent, otherScope, otherRoot), skill.skill, 'SKILL.md');
+ if (file !== skill.file && exists(file)) warnings.push({ type: 'cross-scope-discovery', agent, skill: skill.skill, file, resolution: 'Compare the project and global installations. Explicitly uninstall the unwanted scope; no scope is silently preferred.' });
+ }
+ }
+ }
+ return warnings;
+}
+
+export async function install(options, { update = false, bundled } = {}) {
+ const ctx = context(options);
+ if (ctx.product === 'advised' && ctx.domain !== 'SoftwareEngineering') throw new Error('AdvisedOrganons requires explicit --domain SoftwareEngineering');
+ const source = await openPackage({ ...ctx, bundled });
+ try {
+ return locked(ctx.store, () => {
+ const recovered = recover(ctx.store, ctx.root);
+ const state = stateOf(ctx);
+ const existing = state.products[ctx.product];
+ if (existing) assertCurrent(existing);
+ if (update && !existing) throw new Error('Product is not installed; use install first');
+ if (!update && existing && existing.manifestHash !== source.manifest.manifestHash) throw new Error('Different content is already installed; use update');
+ if (existing?.manifestHash === source.manifest.manifestHash && existing.discovery[ctx.agent]) return { action: 'unchanged', product: ctx.product, version: existing.version, directory: existing.directory, recovered, warnings: aliases(ctx, existing) };
+ const directory = path.join(ctx.store, 'packages', ctx.product, `${source.manifest.version}-${source.manifest.manifestHash.slice(0, 16)}`);
+ noSymlinkAncestors(directory);
+ if (!exists(directory)) {
+ const staging = `${directory}.staging-${process.pid}`;
+ fs.mkdirSync(path.dirname(staging), { recursive: true });
+ fs.cpSync(source.directory, staging, { recursive: true, dereference: false, errorOnExist: true, force: false });
+ verifyPackage(staging); fs.renameSync(staging, directory);
+ }
+ const manifest = verifyPackage(directory);
+ const agents = [...new Set([...Object.keys(existing?.discovery ?? {}), ctx.agent])].sort();
+ const { discovery, operations } = discoveryFor(ctx, directory, manifest, agents);
+ collisionCheck(ctx, operations, existing);
+ const nextFiles = new Set(operations.map(entry => entry.file));
+ for (const item of Object.values(existing?.discovery ?? {}).flat()) if (!nextFiles.has(item.file)) operations.push({ file: item.file, content: null });
+ const entry = { version: manifest.version, manifestHash: manifest.manifestHash, directory, discovery, domain: ctx.domain ?? existing?.domain ?? null, previous: existing && existing.manifestHash !== manifest.manifestHash ? { version: existing.version, manifestHash: existing.manifestHash, directory: existing.directory } : existing?.previous ?? null };
+ state.products[ctx.product] = entry;
+ operations.push({ file: ctx.stateFile, content: serialize(state) });
+ transact(ctx.store, ctx.root, operations);
+ const retained = [];
+ const obsolete = existing?.previous;
+ if (obsolete && obsolete.directory !== directory && obsolete.directory !== entry.previous?.directory) {
+ try {
+ if (verifyPackage(obsolete.directory).manifestHash !== obsolete.manifestHash) retained.push(obsolete.directory);
+ else fs.rmSync(obsolete.directory, { recursive: true });
+ } catch { retained.push(obsolete.directory); }
+ }
+ return { action: update ? 'updated' : 'installed', product: ctx.product, version: manifest.version, directory, discovery, recovered, retained, warnings: aliases(ctx, entry), capabilityValidation: 'Filesystem content verified; authenticated agent invocation, resource permissions and delegation require separate tests.' };
+ });
+ } finally { source.close(); }
+}
+
+export function check(options) {
+ const ctx = context(options);
+ if (exists(path.join(ctx.store, 'transaction.json'))) throw new Error('Interrupted installation; rerun its lifecycle command to recover before checking');
+ const entry = stateOf(ctx).products[ctx.product];
+ if (!entry?.discovery[ctx.agent]) throw new Error('Product is not installed for this agent and scope');
+ assertCurrent(entry);
+ const command = AGENTS[ctx.agent].command;
+ const result = spawnSync(command, ['--version'], { encoding: 'utf8', timeout: 10000, windowsHide: true });
+ return { passed: true, validationScope: 'package-integrity-and-discovery-files', capabilityValidationComplete: false, product: ctx.product, version: entry.version, directory: entry.directory, discovery: entry.discovery[ctx.agent], warnings: aliases(ctx, entry), agent: { command, executableAvailable: !result.error && result.status === 0, authentication: 'not-checked', invocation: 'not-checked', resourcePermissions: 'not-checked', delegation: 'not-checked' } };
+}
+
+export function rollback(options) {
+ const ctx = context(options);
+ return locked(ctx.store, () => {
+ const recovered = recover(ctx.store, ctx.root);
+ const state = stateOf(ctx); const entry = state.products[ctx.product];
+ if (!entry?.previous) throw new Error('No previous complete installation is available');
+ assertCurrent(entry);
+ const previous = verifyPackage(entry.previous.directory);
+ if (previous.manifestHash !== entry.previous.manifestHash) throw new Error('Previous installation was modified');
+ const { discovery, operations } = discoveryFor(ctx, entry.previous.directory, previous, Object.keys(entry.discovery));
+ collisionCheck(ctx, operations, entry);
+ const nextFiles = new Set(operations.map(item => item.file));
+ for (const item of Object.values(entry.discovery).flat()) if (!nextFiles.has(item.file)) operations.push({ file: item.file, content: null });
+ state.products[ctx.product] = { ...entry.previous, discovery, domain: entry.domain, previous: { version: entry.version, manifestHash: entry.manifestHash, directory: entry.directory } };
+ operations.push({ file: ctx.stateFile, content: serialize(state) });
+ transact(ctx.store, ctx.root, operations);
+ return { action: 'rolled-back', product: ctx.product, version: previous.version, recovered };
+ });
+}
+
+export function uninstall(options) {
+ const ctx = context(options);
+ return locked(ctx.store, () => {
+ const recovered = recover(ctx.store, ctx.root);
+ const state = stateOf(ctx); const entry = state.products[ctx.product];
+ if (!entry?.discovery[ctx.agent]) return { action: 'unchanged', recovered };
+ const retained = [];
+ const operations = [];
+ for (const item of entry.discovery[ctx.agent]) {
+ if (matches(item.file, item.sha256)) operations.push({ file: item.file, content: null });
+ else if (exists(item.file)) retained.push(item.file);
+ }
+ delete entry.discovery[ctx.agent];
+ const removeProduct = !Object.keys(entry.discovery).length;
+ if (removeProduct) delete state.products[ctx.product];
+ operations.push({ file: ctx.stateFile, content: serialize(state) });
+ transact(ctx.store, ctx.root, operations);
+ if (removeProduct) {
+ // Retain content whenever a discovery copy was changed, so its references
+ // do not become dangling. Payload edits also prevent recursive deletion.
+ for (const candidate of [entry, entry.previous].filter(Boolean)) {
+ try {
+ const manifest = verifyPackage(candidate.directory);
+ if (manifest.manifestHash !== candidate.manifestHash || retained.length) retained.push(candidate.directory);
+ else fs.rmSync(candidate.directory, { recursive: true });
+ } catch { retained.push(candidate.directory); }
+ }
+ }
+ return { action: 'uninstalled', product: ctx.product, agent: ctx.agent, retained, recovered, adoption: 'unchanged' };
+ });
+}
+
+export async function initialize(options) {
+ const ctx = context(options);
+ if (!options.project) throw new Error('--project is required for explicit project adoption');
+ const project = fs.realpathSync(options.project);
+ const entry = stateOf(ctx).products[ctx.product];
+ if (!entry?.discovery[ctx.agent]) throw new Error('Install this product for the selected agent/scope before init');
+ assertCurrent(entry);
+ if (!['core', 'SoftwareEngineering'].includes(options.philosophy)) throw new Error('--philosophy core|SoftwareEngineering is required; installation is not adoption');
+ if (options.philosophy === 'SoftwareEngineering' && (ctx.product !== 'advised' || options.domain !== 'SoftwareEngineering')) throw new Error('Select --product advised --domain SoftwareEngineering to adopt this domain');
+ const source = path.join(entry.directory, 'payload', options.philosophy === 'core' ? 'OrganonCore/PHILOSOPHY.md' : 'AdvisedOrganons/SoftwareEngineering/PHILOSOPHY.md');
+ const target = path.join(project, 'PHILOSOPHY.md');
+ const lock = path.join(project, 'PHILOSOPHY.lock.json');
+ const instructions = path.join(project, AGENTS[ctx.agent].instructions);
+ const adoption = path.join(project, '.organon/adoption.json');
+ for (const file of [target, lock, instructions, adoption]) noSymlinkAncestors(file);
+ if (exists(target) || exists(lock) || exists(adoption)) throw new Error('Existing philosophy/adoption is preserved; use the philosophical review workflow for revision');
+ const sourceId = options.philosophy === 'core' ? 'https://github.com/shendeguize/OrganonCore' : 'https://github.com/shendeguize/AdvisedOrganons/SoftwareEngineering';
+ const block = '\n\nRead [PHILOSOPHY.md](PHILOSOPHY.md) as this workspace\'s adopted philosophy. Preserve its path and real reference directory through Organon delegation. Installation changes do not authorize philosophical revision.\n\n';
+ const current = exists(instructions) ? fs.readFileSync(instructions, 'utf8') : '';
+ if (current.includes('\n`;
+ const metadata = { ...document.metadata, derived_from: {
+ source_id: sourceId, philosophy_version: document.metadata.philosophy_version, content_hash: hash(raw) } };
+ const candidate = renderFrontmatter(metadata, body);
+ parseDocument(candidate);
+ writePair(target, candidate, serialize(initialLock(document, sourceId, path.basename(target))), { create: true });
+ return check(target);
+}
+
+export function exportPhilosophy({ source, out, coreOnly = false, sourceId }) {
+ if (!source || !out) throw new Error('--source and --out are required');
+ assertWritable(out, { absent: true });
+ assertNoPending(fs.realpathSync(source));
+ const raw = read(source);
+ const document = parseDocument(raw);
+ const sidecar = lockPath(source);
+ if (stat(sidecar) && json(sidecar).document_filename === path.basename(fs.realpathSync(source))) {
+ const lock = validateLock(json(sidecar), document.metadata);
+ sourceId ??= lock.document_id;
+ } else if (fs.realpathSync(source) === fs.realpathSync(CORE_FILE)) sourceId ??= CORE_ID;
+ if (typeof sourceId !== 'string' || !sourceId.trim()) throw new Error('Provide --source-id for a source without a managed document identity');
+ const metadata = { ...document.metadata, derived_from: {
+ source_id: sourceId, philosophy_version: document.metadata.philosophy_version, content_hash: hash(raw) } };
+ const body = coreOnly ? withoutExtensions(document) : document.body;
+ const output = renderFrontmatter(metadata, body);
+ parseDocument(output);
+ writeNew(out, output);
+ return { out: path.resolve(out), source_id: sourceId, content_hash: hash(output), core_only: coreOnly };
+}
+
+export function statusOf(base, ours, theirs) {
+ if (ours === theirs) return ours === base ? 'unchanged' : 'converged';
+ if (ours === base) return 'theirs-changed';
+ if (theirs === base) return 'ours-changed';
+ return 'conflict';
+}
+
+const changeType = (oldHash, newHash) => oldHash === newHash ? 'unchanged'
+ : oldHash === null ? 'added' : newHash === null ? 'deleted' : 'modified';
+const sectionHashes = document => Object.fromEntries(document.sections.map(section => [section.id, section.hash]));
+
+export function classify({ ours, theirs, sourceId, kind, baseFile }) {
+ if (!ours || !theirs || !sourceId?.trim() || !['core', 'import'].includes(kind)) throw new Error('--ours, --theirs, --source-id and --kind core|import are required');
+ ours = path.resolve(ours); theirs = path.resolve(theirs);
+ assertNoPending(ours);
+ assertNoPending(fs.realpathSync(theirs));
+ const local = parseDocument(read(ours));
+ const incoming = parseDocument(read(theirs));
+ const sidecar = lockPath(ours);
+ const lock = validateLock(json(sidecar), local.metadata, path.basename(ours));
+ if ((sourceId === lock.core_source_id) !== (kind === 'core')) throw new Error('Source identity and kind disagree; imports cannot advance the Core checkpoint');
+ const base = Object.hasOwn(lock.sources, sourceId) ? lock.sources[sourceId] : null;
+ if (base && base.kind !== kind) throw new Error('Source kind changed');
+ let baseText = null;
+ if (baseFile) {
+ if (!base) throw new Error('No checkpoint against which to verify --base-file');
+ baseText = read(baseFile);
+ const verified = parseDocument(baseText);
+ if (verified.content_hash !== base.content_hash || verified.structure_hash !== base.structure_hash
+ || serialize(sectionHashes(verified)) !== serialize(base.sections)) throw new Error('Base file does not match the source checkpoint');
+ }
+ const o = sectionHashes(local), t = sectionHashes(incoming), b = base?.sections ?? {};
+ const ids = [...new Set([...Object.keys(o), ...Object.keys(t), ...Object.keys(b)])];
+ const sections = ids.map(id => {
+ const O = o[id] ?? null, T = t[id] ?? null, B = b[id] ?? null;
+ return { id, base: base ? B : undefined, ours: O, theirs: T,
+ status: base ? statusOf(B, O, T) : O === T ? 'equal' : 'different',
+ ours_change: base ? changeType(B, O) : null,
+ theirs_change: base ? changeType(B, T) : null,
+ difference: changeType(O, T),
+ suppressed: lock.declined.some(entry => entry.source_id === sourceId && entry.id === id && entry.incoming_hash === T) };
+ });
+ return { format_version: '0.1.0', mode: base ? 'checkpoint' : 'two-way', source_id: sourceId, kind,
+ baseTextAvailable: baseText !== null, ...(baseText !== null ? { base_text: baseText } : {}),
+ metadata: { ours: local.metadata, theirs: incoming.metadata },
+ bindings: { ours: { path: ours, hash: digestFile(ours) }, theirs: { path: theirs, hash: digestFile(theirs) },
+ lock: { path: sidecar, hash: digestFile(sidecar) } },
+ sections, structure: { base: base?.structure_hash ?? null, ours: local.structure_hash, theirs: incoming.structure_hash,
+ status: base ? statusOf(base.structure_hash, local.structure_hash, incoming.structure_hash)
+ : local.structure_hash === incoming.structure_hash ? 'equal' : 'different',
+ ours_layout: local.structure, theirs_layout: incoming.structure,
+ ...(base ? { base_layout: base.structure } : {}) },
+ warnings: coreWarnings(incoming.metadata.core_version) };
+}
+
+function verifyBindings(bindings) {
+ for (const [label, binding] of Object.entries(bindings)) {
+ if (!binding || typeof binding.path !== 'string' || digestFile(binding.path) !== binding.hash) throw new Error(`Stale ${label}: reclassify and review the changed inputs`);
+ }
+}
+
+function validateCandidate(report, candidateText, decisions) {
+ const candidate = parseDocument(candidateText);
+ if (!decisions || !decisions.sections || Array.isArray(decisions.sections)
+ || !['ours', 'theirs', 'manual', 'agent'].includes(decisions.structure)
+ || !SEMVER.test(decisions.philosophy_version ?? '')) throw new Error('Invalid decisions: provide sections, structure and philosophy_version');
+ const c = sectionHashes(candidate);
+ for (const [id, choice] of Object.entries(decisions.sections)) {
+ if (!['ours', 'theirs', 'decline', 'manual', 'agent'].includes(choice)) throw new Error(`Invalid section decision: ${id}`);
+ if (!report.sections.some(row => row.id === id) && !(Object.hasOwn(c, id) && ['manual', 'agent'].includes(choice))) throw new Error(`Decision references an unknown section: ${id}`);
+ }
+ for (const row of report.sections) {
+ const incomingChange = report.mode === 'two-way' || row.base !== row.theirs;
+ const needsDecision = row.ours !== row.theirs && incomingChange && !row.suppressed;
+ const choice = decisions.sections[row.id] ?? (needsDecision ? null : row.suppressed ? 'decline' : 'ours');
+ if (!choice) throw new Error(`Missing decision for ${row.id}`);
+ const expected = choice === 'theirs' ? row.theirs : ['ours', 'decline'].includes(choice) ? row.ours : undefined;
+ if (expected !== undefined && (c[row.id] ?? null) !== expected) throw new Error(`Candidate does not implement ${choice} for ${row.id}`);
+ }
+ for (const id of Object.keys(c)) {
+ if (!report.sections.some(row => row.id === id) && !['manual', 'agent'].includes(decisions.sections[id])) throw new Error(`Candidate adds ${id} without a manual/agent decision`);
+ }
+ if (['ours', 'theirs'].includes(decisions.structure) && candidate.structure_hash !== report.structure[decisions.structure]) throw new Error('Candidate does not implement the selected structure');
+ const expectedCore = report.kind === 'core' ? report.metadata.theirs.core_version : report.metadata.ours.core_version;
+ if (candidate.metadata.core_version !== expectedCore || candidate.metadata.philosophy_version !== decisions.philosophy_version
+ || candidate.metadata.format_version !== report.metadata.ours.format_version
+ || JSON.stringify(candidate.metadata.derived_from) !== JSON.stringify(report.metadata.ours.derived_from)) throw new Error('Candidate metadata does not implement the confirmed version/lineage policy');
+ return candidate;
+}
+
+export function prepare({ report: reportFile, candidate: candidateFile, decisions: decisionsFile, record }) {
+ if (!reportFile || !candidateFile || !decisionsFile || !record) throw new Error('--report, --candidate, --decisions and --record are required');
+ const supplied = json(reportFile);
+ verifyBindings(supplied.bindings);
+ const ours = supplied.bindings.ours.path;
+ assertWritable(ours);
+ assertWritable(lockPath(ours));
+ const report = classify({ ours, theirs: supplied.bindings.theirs.path, sourceId: supplied.source_id, kind: supplied.kind });
+ const decisions = json(decisionsFile);
+ const candidateText = read(candidateFile);
+ validateCandidate(report, candidateText, decisions);
+ record = assertPrivateRecord(ours, record);
+ return { format_version: '0.1.0', source_id: report.source_id, kind: report.kind,
+ bindings: { ...report.bindings, candidate: { path: path.resolve(candidateFile), hash: hash(candidateText) },
+ decisions: { path: path.resolve(decisionsFile), hash: digestFile(decisionsFile) }, record: { path: record, hash: digestFile(record) } } };
+}
+
+export function applyPlan(planFile) {
+ const plan = json(planFile);
+ if (plan.format_version !== '0.1.0') throw new Error('Unsupported apply plan');
+ if (Object.keys(plan.bindings ?? {}).sort().join(',') !== 'candidate,decisions,lock,ours,record,theirs') throw new Error('Incomplete apply plan bindings');
+ verifyBindings(plan.bindings);
+ const target = plan.bindings.ours.path;
+ assertWritable(target);
+ assertWritable(lockPath(target));
+ if (plan.bindings.lock.path !== lockPath(target)) throw new Error('Plan lock path does not match target');
+ assertPrivateRecord(target, plan.bindings.record.path);
+ const report = classify({ ours: target, theirs: plan.bindings.theirs.path, sourceId: plan.source_id, kind: plan.kind });
+ const decisions = json(plan.bindings.decisions.path);
+ const candidateText = read(plan.bindings.candidate.path);
+ const candidate = validateCandidate(report, candidateText, decisions);
+ const lock = json(lockPath(target));
+ const incoming = parseDocument(read(plan.bindings.theirs.path));
+ lock.sources = { ...lock.sources, [plan.source_id]: checkpoint(incoming, plan.kind) };
+ if (plan.kind === 'core') lock.core_version = incoming.metadata.core_version;
+ const currentSourceDeclines = [];
+ for (const row of report.sections) {
+ const choice = decisions.sections[row.id] ?? (row.suppressed ? 'decline' : 'ours');
+ const reviewable = report.mode === 'two-way' || row.base !== row.theirs;
+ if (row.ours !== row.theirs && (((reviewable || row.suppressed) && choice === 'ours') || choice === 'decline')) {
+ currentSourceDeclines.push({ source_id: plan.source_id, id: row.id, incoming_hash: row.theirs });
+ }
+ }
+ lock.declined = [...lock.declined.filter(entry => entry.source_id !== plan.source_id), ...currentSourceDeclines];
+ validateLock(lock, candidate.metadata);
+ verifyBindings(plan.bindings);
+ writePair(target, candidateText, serialize(lock), { verifyInputs: () => verifyBindings(plan.bindings) });
+ return { applied: target, core_version: lock.core_version, philosophy_version: candidate.metadata.philosophy_version,
+ record: plan.bindings.record.path };
+}
diff --git a/scripts/lib/sections.js b/scripts/lib/sections.js
new file mode 100644
index 0000000..df465ba
--- /dev/null
+++ b/scripts/lib/sections.js
@@ -0,0 +1 @@
+export * from '../../OrganonCore/scripts/lib/sections.js';
diff --git a/scripts/package/build.mjs b/scripts/package/build.mjs
new file mode 100644
index 0000000..60df895
--- /dev/null
+++ b/scripts/package/build.mjs
@@ -0,0 +1,123 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+import { fileURLToPath } from 'node:url';
+import { execFileSync } from 'node:child_process';
+import { createTarball } from '../../installer/lib/archive.mjs';
+import { PRODUCTS, EXACT_VERSION, verifyPackage } from '../../installer/lib/package.mjs';
+import { exists, write, serialize, sha256, listFiles } from '../../installer/lib/files.mjs';
+
+const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
+const VERSION = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version;
+const excluded = /(?:^|\/)(?:lean|docs|tests?|examples|\.local|node_modules|site|\.git)(?:\/|$)|organon(?:-core)?-lean/;
+const repositories = [
+ { root: path.join(ROOT, 'OrganonCore'), name: 'OrganonCore' },
+ { root: path.join(ROOT, 'AdvisedOrganons'), name: 'AdvisedOrganons' },
+ { root: ROOT, name: 'AgentOrganon' },
+];
+function repositoryOf(file) { return repositories.find(repo => file === repo.root || file.startsWith(`${repo.root}${path.sep}`)); }
+function revision(repo) { return execFileSync('git', ['-C', repo.root, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); }
+
+export function buildPackage({ product, out, version = VERSION }) {
+ if (!Object.hasOwn(PRODUCTS, product) || !EXACT_VERSION.test(version)) throw new Error('Provide --product agent-organon|core|advised and an exact release version');
+ if (!out) throw new Error('--out is required');
+ out = path.resolve(out); fs.mkdirSync(out, { recursive: true });
+ const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'organon-build-'));
+ const staging = path.join(temporary, 'package'); fs.mkdirSync(staging);
+ const sourceMap = new Map();
+ const provenance = new Map();
+ const snapshots = new Map(repositories.map(repo => [repo.name, revision(repo)]));
+ function add(source, target) {
+ if (!exists(source)) throw new Error(`Required release source is missing: ${source}`);
+ const real = fs.realpathSync(source);
+ if (fs.statSync(real).isDirectory()) {
+ for (const entry of fs.readdirSync(real).sort()) {
+ const next = `${target}/${entry}`;
+ if (!excluded.test(next)) add(path.join(real, entry), next);
+ }
+ return;
+ }
+ if (!fs.statSync(real).isFile()) throw new Error('Release source is not a regular file');
+ sourceMap.set(target, { source, real });
+ }
+ try {
+ // Explicit roots only: no directory-wide repository copies or npm prepare hooks.
+ for (const file of ['LICENSE', 'AGENTS.md', 'PHILOSOPHY.md']) add(path.join(ROOT, file), `payload/${file}`);
+ for (const file of ['init.js', 'check.js', 'resolve.js', 'export.js', 'classify.js', 'apply.js']) add(path.join(ROOT, 'scripts', file), `payload/scripts/${file}`);
+ add(path.join(ROOT, 'scripts/lib'), 'payload/scripts/lib');
+ add(path.join(ROOT, 'skills'), 'payload/skills');
+ for (const file of ['LICENSE', 'AGENTS.md', 'PHILOSOPHY.md', 'zh/PHILOSOPHY.md', 'zh/AGENTS.md']) add(path.join(ROOT, 'OrganonCore', file), `payload/OrganonCore/${file}`);
+ add(path.join(ROOT, 'OrganonCore/skills'), 'payload/OrganonCore/skills');
+ add(path.join(ROOT, 'OrganonCore/scripts/lib'), 'payload/OrganonCore/scripts/lib');
+ // The migration is a release prerequisite; never silently package the old docs path.
+ add(path.join(ROOT, 'OrganonCore/rationale'), 'payload/OrganonCore/rationale');
+ if (exists(path.join(ROOT, 'OrganonCore/zh/rationale'))) add(path.join(ROOT, 'OrganonCore/zh/rationale'), 'payload/OrganonCore/zh/rationale');
+ if (product === 'advised') {
+ for (const file of ['PHILOSOPHY.md', 'AGENTS.md', 'zh/PHILOSOPHY.md']) add(path.join(ROOT, 'AdvisedOrganons/SoftwareEngineering', file), `payload/AdvisedOrganons/SoftwareEngineering/${file}`);
+ add(path.join(ROOT, 'AdvisedOrganons/SoftwareEngineering/rationale'), 'payload/AdvisedOrganons/SoftwareEngineering/rationale');
+ if (exists(path.join(ROOT, 'AdvisedOrganons/SoftwareEngineering/zh/rationale'))) add(path.join(ROOT, 'AdvisedOrganons/SoftwareEngineering/zh/rationale'), 'payload/AdvisedOrganons/SoftwareEngineering/zh/rationale');
+ add(path.join(ROOT, 'AdvisedOrganons/skills'), 'payload/AdvisedOrganons/skills');
+ add(path.join(ROOT, 'AdvisedOrganons/LICENSE'), 'payload/AdvisedOrganons/LICENSE');
+ }
+ add(path.join(ROOT, 'installer'), 'installer');
+ const destinationByReal = new Map();
+ for (const [target, entry] of sourceMap) {
+ // Prefer the canonical Core copy over the materialized outer mirror.
+ if (!destinationByReal.has(entry.real) || target.startsWith('payload/OrganonCore/')) destinationByReal.set(entry.real, target);
+ }
+ const omittedLinks = [];
+ for (const [target, { source, real }] of sourceMap) {
+ const raw = fs.readFileSync(real);
+ let bytes = raw;
+ const transformations = [];
+ if (target.endsWith('.md')) {
+ const converted = raw.toString('utf8').replace(/\]\(([^\s)]+)(?:\s+"[^"]*")?\)/g, (whole, href) => {
+ if (/^(?:[a-z]+:|#|\/)/i.test(href)) return whole;
+ const [relative, fragment] = href.split('#');
+ const resolved = path.resolve(path.dirname(real), decodeURIComponent(relative));
+ const mapped = destinationByReal.get(exists(resolved) ? fs.realpathSync(resolved) : resolved);
+ if (mapped) {
+ const link = path.relative(path.dirname(target), mapped).split(path.sep).join('/');
+ return `](${link}${fragment ? `#${fragment}` : ''})`;
+ }
+ if (/\.local(?:\/|$)/.test(relative)) return whole;
+ const repo = repositoryOf(resolved);
+ if (!repo) return whole;
+ const relativeSource = path.relative(repo.root, resolved).split(path.sep).map(encodeURIComponent).join('/');
+ const url = `https://github.com/shendeguize/${repo.name}/blob/${snapshots.get(repo.name)}/${relativeSource}${fragment ? `#${fragment}` : ''}`;
+ omittedLinks.push({ file: target, source: href, destination: url });
+ return `](${url})`;
+ });
+ bytes = Buffer.from(converted);
+ if (!bytes.equals(raw)) transformations.push('rebase-reference-links');
+ }
+ if (source !== real) transformations.push('materialize-symbolic-link');
+ write(path.join(staging, target), bytes);
+ const repo = repositoryOf(real);
+ provenance.set(target, { repository: repo.name, path: path.relative(repo.root, real).split(path.sep).join('/'), revision: snapshots.get(repo.name), sourceSha256: sha256(raw), transformations });
+ }
+ const metadata = { name: PRODUCTS[product], version, description: 'Organon philosophy and non-Lean agent skills', type: 'module', license: 'MIT', engines: { node: '>=22' }, ...(product === 'agent-organon' ? { bin: { organon: 'installer/organon.mjs' } } : {}), repository: { type: 'git', url: `git+https://github.com/shendeguize/${product === 'core' ? 'OrganonCore' : product === 'advised' ? 'AdvisedOrganons' : 'AgentOrganon'}.git` } };
+ write(path.join(staging, 'package.json'), serialize(metadata));
+ write(path.join(staging, 'payload/package.json'), serialize({ type: 'module', organon: { coreVersion: '>=0.1.0 <0.2.0' } }));
+ write(path.join(staging, 'LICENSE'), fs.readFileSync(path.join(ROOT, 'LICENSE')));
+ write(path.join(staging, 'README.md'), `# ${PRODUCTS[product]}\n\nProduct ${version}. Includes philosophy, non-Lean skills and the shared offline installer.\n\n\`node installer/organon.mjs install --product ${product} --agent codex --scope project --project /path/to/project --from .${product === 'advised' ? ' --domain SoftwareEngineering' : ''}\`\n\nInstallation does not adopt a philosophy. Use \`init\` to preview an explicit project adoption; \`--apply --plan-sha256 HASH\` executes the plan identified by the preview's \`planSha256\`. Lean and website documentation are not installed.\n`);
+ const prefix = product === 'core' ? 'payload/OrganonCore/skills/' : product === 'advised' ? 'payload/AdvisedOrganons/skills/' : 'payload/skills/';
+ const skills = listFiles(staging).filter(file => file.startsWith(prefix) && file.endsWith('/SKILL.md')).map(file => ({ name: fs.readFileSync(path.join(staging, file), 'utf8').match(/^name:\s*(.+)$/m)[1].trim(), path: file }));
+ const files = listFiles(staging).map(file => ({ path: file, sha256: sha256(fs.readFileSync(path.join(staging, file))), ...(provenance.has(file) ? { source: provenance.get(file) } : { generated: true }) }));
+ const manifest = { schema: 1, product, version, installerVersion: version, nonLean: true, snapshots: Object.fromEntries(snapshots), skills, files, omittedLinks };
+ write(path.join(staging, 'organon-content.json'), serialize(manifest));
+ verifyPackage(staging);
+ const tarball = path.join(out, `${PRODUCTS[product].replace('@', '').replace('/', '-')}-${version}.tgz`);
+ createTarball(staging, tarball);
+ const result = { product, version, tarball, sha256: sha256(fs.readFileSync(tarball)), manifestHash: sha256(fs.readFileSync(path.join(staging, 'organon-content.json'))), files: files.length, skills: skills.map(skill => skill.name) };
+ write(`${tarball}.json`, serialize(result));
+ return result;
+ } finally { fs.rmSync(temporary, { recursive: true, force: true }); }
+}
+
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
+ try {
+ const args = Object.fromEntries(process.argv.slice(2).reduce((rows, value, index, all) => index % 2 ? rows : [...rows, [value.replace(/^--/, ''), all[index + 1]]], []));
+ console.log(serialize(buildPackage(args)));
+ } catch (error) { console.error(error.message); process.exitCode = 1; }
+}
diff --git a/scripts/package/check.mjs b/scripts/package/check.mjs
new file mode 100644
index 0000000..56c3047
--- /dev/null
+++ b/scripts/package/check.mjs
@@ -0,0 +1,47 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+import { fileURLToPath } from 'node:url';
+import { extractTarball } from '../../installer/lib/archive.mjs';
+import { verifyPackage } from '../../installer/lib/package.mjs';
+import { exists, listFiles } from '../../installer/lib/files.mjs';
+
+export function checkPackage(file) {
+ const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'organon-check-package-'));
+ try {
+ const root = fs.statSync(file).isDirectory() ? file : extractTarball(file, temporary);
+ const manifest = verifyPackage(root);
+ if (!manifest.snapshots || Object.keys(manifest.snapshots).sort().join(',') !== ['AdvisedOrganons', 'AgentOrganon', 'OrganonCore'].sort().join(',') || Object.values(manifest.snapshots).some(value => !/^[a-f0-9]{40}$/.test(value))) throw new Error('Package source snapshots must identify all three repositories');
+ for (const file of manifest.files) {
+ if (file.source && (!Object.hasOwn(manifest.snapshots, file.source.repository) || file.source.revision !== manifest.snapshots[file.source.repository] || !/^[a-f0-9]{64}$/.test(file.source.sourceSha256 || ''))) throw new Error('Package file provenance differs from its source snapshot');
+ }
+ const forbidden = manifest.files.filter(entry => /(?:^|\/)(?:lean|docs|tests?|examples|\.local|node_modules|site|\.git)(?:\/|$)|organon(?:-core)?-lean/.test(entry.path));
+ if (forbidden.length) throw new Error(`Forbidden release content: ${forbidden.map(entry => entry.path).join(', ')}`);
+ const missing = [];
+ for (const name of listFiles(path.join(root, 'payload'))) {
+ const full = path.join(root, 'payload', name);
+ const text = fs.readFileSync(full, 'utf8');
+ if (/\.(?:js|mjs)$/.test(name)) {
+ for (const match of text.matchAll(/(?:from\s+|import\s*)['"](\.[^'"]+)['"]/g)) {
+ if (!exists(path.resolve(path.dirname(full), match[1]))) missing.push(`${name}: ${match[1]}`);
+ }
+ }
+ if (name.endsWith('.md')) {
+ for (const match of text.matchAll(/\]\(([^\s)]+)\)/g)) {
+ const href = match[1].split('#')[0];
+ if (!href || /^(?:[a-z]+:|\/)/i.test(href) || href.includes('.local/')) continue;
+ if (!exists(path.resolve(path.dirname(full), decodeURIComponent(href)))) missing.push(`${name}: ${href}`);
+ }
+ }
+ }
+ if (missing.length) throw new Error(`Package closure is incomplete:\n${missing.join('\n')}`);
+ return { passed: true, product: manifest.product, version: manifest.version, files: manifest.files.length, skills: manifest.skills.length, manifestHash: manifest.manifestHash, snapshots: manifest.snapshots };
+ } finally { fs.rmSync(temporary, { recursive: true, force: true }); }
+}
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
+ try {
+ const index = process.argv.indexOf('--package');
+ if (index < 0 || !process.argv[index + 1]) throw new Error('--package TGZ_OR_DIRECTORY is required');
+ console.log(JSON.stringify(checkPackage(path.resolve(process.argv[index + 1])), null, 2));
+ } catch (error) { console.error(error.message); process.exitCode = 1; }
+}
diff --git a/scripts/release/actions-artifacts.mjs b/scripts/release/actions-artifacts.mjs
new file mode 100644
index 0000000..2d45a85
--- /dev/null
+++ b/scripts/release/actions-artifacts.mjs
@@ -0,0 +1,46 @@
+import { gh, AGENTS } from './lib.mjs';
+const REPOSITORY = 'shendeguize/AgentOrganon';
+const positive = value => /^[1-9]\d*$/.test(String(value ?? ''));
+const incomplete = () => new Error('Incomplete current workflow attempt; use Rerun all jobs or a new workflow dispatch. Older artifacts are preserved but never borrowed.');
+export function assertTrustedRun(run, workflow, sourceCommit) {
+ if (!positive(run.id) || !positive(run.run_attempt) || run.head_sha !== sourceCommit || run.repository?.full_name !== REPOSITORY || run.head_repository?.full_name !== REPOSITORY || run.path !== `.github/workflows/${workflow}` || run.event !== 'workflow_dispatch' || run.conclusion !== 'success') throw new Error('Untrusted or incomplete workflow run');
+ return run;
+}
+export function assertArtifactIdentity(artifact, run, expected) {
+ if (!positive(artifact.id) || artifact.expired !== false || artifact.workflow_run?.id !== run.id || artifact.workflow_run?.head_sha !== run.head_sha || !/^sha256:[a-f0-9]{64}$/.test(artifact.digest || '') || typeof artifact.name !== 'string') throw new Error('Invalid workflow artifact identity');
+ if (expected && (artifact.id !== expected.id || artifact.name !== expected.name || artifact.digest !== expected.digest)) throw new Error('Artifact identity changed during download');
+ return artifact;
+}
+export function selectAttemptArtifacts(run, artifacts, workflow) {
+ const attempt = String(run.run_attempt);
+ let names;
+ if (workflow === 'candidate.yml') names = [`candidate-packages-${attempt}`, ...['Linux', 'macOS', 'Windows'].map(os => `install-${os}-${attempt}`)];
+ else if (workflow === 'seal.yml') names = [`validated-release-${attempt}`];
+ else if (workflow === 'agent-e2e.yml') {
+ const full = AGENTS.map(agent => `agent-${agent}-Linux-full-${attempt}`);
+ const smoke = AGENTS.flatMap(agent => ['Linux', 'macOS', 'Windows'].map(os => `agent-${agent}-${os}-smoke-${attempt}`));
+ const haveFull = artifacts.some(item => full.includes(item.name)), haveSmoke = artifacts.some(item => smoke.includes(item.name));
+ if (haveFull === haveSmoke) throw incomplete();
+ names = haveFull ? full : smoke;
+ } else throw new Error('Unexpected artifact workflow');
+ return names.map(name => {
+ const matches = artifacts.filter(item => item.name === name);
+ if (matches.length !== 1) throw incomplete();
+ return assertArtifactIdentity(matches[0], run);
+ });
+}
+export function listRunArtifacts(runID, api = gh) {
+ const result = [];
+ for (let page = 1; page <= 100; page++) {
+ const body = api([`repos/${REPOSITORY}/actions/runs/${runID}/artifacts?per_page=100&page=${page}`]);
+ if (!Array.isArray(body.artifacts)) throw new Error('Invalid artifact listing');
+ result.push(...body.artifacts);
+ if (result.length >= body.total_count) return result;
+ }
+ throw new Error('Artifact inventory exceeds pagination limit');
+}
+export function resolveAttempt(runID, workflow, sourceCommit, api = gh) {
+ if (!positive(runID)) throw new Error('Expected workflow run ID');
+ const run = assertTrustedRun(api([`repos/${REPOSITORY}/actions/runs/${runID}`]), workflow, sourceCommit);
+ return { run, artifacts: selectAttemptArtifacts(run, listRunArtifacts(runID, api), workflow) };
+}
diff --git a/scripts/release/assemble.mjs b/scripts/release/assemble.mjs
new file mode 100644
index 0000000..71d993b
--- /dev/null
+++ b/scripts/release/assemble.mjs
@@ -0,0 +1,28 @@
+import fs from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import path from 'node:path';
+import { parseArgs, readJSON, writeJSON, requireValue, sha256, confined, digest, main } from './lib.mjs';
+import { INVENTORY_FILE, validateTrustedEvidence } from './evidence.mjs';
+import { assertManifest } from './manifest.mjs';
+
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(() => {
+ const args = parseArgs(); const manifestFile = path.resolve(requireValue(args, 'manifest')); const root = path.dirname(manifestFile);
+ const manifest = readJSON(manifestFile); assertManifest(manifest);
+ const directory = path.resolve(requireValue(args, 'reports'));
+ if (directory !== root && !directory.startsWith(`${root}${path.sep}`)) throw new Error('Reports must be inside the candidate directory');
+ function visit(dir) { return fs.readdirSync(dir, { withFileTypes: true }).flatMap(item => item.isDirectory() ? visit(path.join(dir, item.name)) : [path.join(dir, item.name)]); }
+ const added = [];
+ for (const file of visit(directory).filter(file => file.endsWith('.json'))) {
+ const relative = path.relative(root, file).split(path.sep).join('/'); confined(root, relative);
+ const report = readJSON(file);
+ if (!report.gate || report.status !== 'passed') continue;
+ if (report.source_digest !== manifest.source_digest || report.artifact_digest !== digest(manifest.artifacts)) throw new Error(`Report belongs to another source or package object: ${relative}`);
+ const previous = manifest.evidence.find(item => item.file === relative);
+ const checksum = sha256(fs.readFileSync(file));
+ if (previous && previous.sha256 !== checksum) throw new Error(`A previously collected report changed: ${relative}; preserve it under a new name`);
+ if (!previous) { manifest.evidence.push({ file: relative, sha256: checksum }); added.push(relative); }
+ }
+ const inventory = path.join(root, INVENTORY_FILE);
+ if (fs.existsSync(inventory)) { manifest.trusted_executions = { file: INVENTORY_FILE, sha256: sha256(fs.readFileSync(inventory)) }; validateTrustedEvidence(manifest, root); }
+ writeJSON(requireValue(args, 'out'), manifest); console.log(JSON.stringify({ status: 'collected', added }));
+});
diff --git a/scripts/release/candidate.mjs b/scripts/release/candidate.mjs
new file mode 100644
index 0000000..79c3c0e
--- /dev/null
+++ b/scripts/release/candidate.mjs
@@ -0,0 +1,59 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { spawnSync } from 'node:child_process';
+import { buildPackage } from '../package/build.mjs';
+import { checkContent } from './content.mjs';
+import * as governance from './governance.mjs';
+import { PRODUCTS, sha256, digest, readJSON, writeJSON, git, parseArgs, requireValue, main } from './lib.mjs';
+import { sourceIdentity, assertManifest, assertGithubReport } from './manifest.mjs';
+
+const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
+function run(command, args, cwd) {
+ const result = spawnSync(command, args, { cwd, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
+ if (result.error || result.status !== 0) throw new Error(`${command} failed in ${cwd}: ${result.error?.message || result.stderr || result.stdout}`);
+ return result.stdout;
+}
+export async function collectGithubReport(product, inspect = governance.inspectReleaseGovernance) {
+ if (!PRODUCTS[product]) throw new Error('Unknown release product');
+ const report = await inspect(`shendeguize/${PRODUCTS[product].repo}`);
+ assertGithubReport(report, product);
+ return { ...report, gate: 'github', product };
+}
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(async () => {
+ const options = parseArgs(); const out = path.resolve(requireValue(options, 'out')); const version = options.version || readJSON(path.join(ROOT, 'package.json')).version;
+ if (fs.existsSync(path.join(out, 'release-manifest.json'))) throw new Error('Candidate destination already contains a manifest; preserve it and use a new directory');
+ fs.mkdirSync(out, { recursive: true });
+ const sources = Object.fromEntries(Object.entries(PRODUCTS).map(([product, spec]) => {
+ const repo = path.join(ROOT, spec.directory); const dirty = Boolean(git(repo, 'status', '--porcelain=v1'));
+ if (dirty && !options['allow-dirty']) throw new Error(`Uncommitted source: ${product}`);
+ return [product, { repository: `shendeguize/${spec.repo}`, commit: git(repo, 'rev-parse', 'HEAD'), dirty }];
+ }));
+ const manifest = { schema_version: 1, version, channel: version.includes('-rc.') ? 'rc' : 'stable', state: 'prepared', sources, artifacts: {}, evidence: [] };
+ if (options['approved-rc']) manifest.approved_rc = readJSON(options['approved-rc']);
+ else if (manifest.channel === 'stable') manifest.approved_rc = { version: process.env.APPROVED_RC_VERSION, manifest_sha256: process.env.APPROVED_RC_MANIFEST_SHA256 };
+ manifest.source_digest = sourceIdentity(manifest); assertManifest(manifest);
+ const pendingReports = [];
+ const addReport = (name, report) => pendingReports.push({ name, report });
+ for (const [product, spec] of Object.entries(PRODUCTS)) {
+ const repo = path.join(ROOT, spec.directory);
+ const built = buildPackage({ product, out, version });
+ manifest.artifacts[product] = { file: path.basename(built.tarball), sha256: built.sha256 };
+ const packageResult = JSON.parse(run(process.execPath, ['scripts/package/check.mjs', '--package', built.tarball], ROOT));
+ addReport(`${product}-package`, { gate: 'package', product, status: packageResult.passed ? 'passed' : 'failed', ...packageResult });
+ const content = checkContent(repo); addReport(`${product}-content`, { ...content, gate: 'content', product });
+ if (!options['skip-site']) {
+ const output = run(process.execPath, [repo === ROOT ? 'scripts/release/site.mjs' : 'scripts/site.mjs', 'check'], repo);
+ addReport(`${product}-site`, { gate: 'site', product, status: 'passed', output });
+ }
+ if (!options['skip-github']) addReport(`${product}-github`, await collectGithubReport(product));
+ }
+ for (const { name, report } of pendingReports) {
+ const file = `reports/${name}.json`;
+ writeJSON(path.join(out, file), { ...report, source_digest: manifest.source_digest, artifact_digest: digest(manifest.artifacts) });
+ manifest.evidence.push({ file, sha256: sha256(fs.readFileSync(path.join(out, file))) });
+ }
+ writeJSON(path.join(out, 'release-manifest.json'), manifest);
+ console.log(JSON.stringify({ status: 'prepared', manifest_sha256: digest(manifest), source_digest: manifest.source_digest,
+ remaining: 'Three-platform installer reports, six-agent actual smoke/full reports with independent assessment, and every failed/missing gate must be completed before sealing.' }, null, 2));
+});
diff --git a/scripts/release/collect.mjs b/scripts/release/collect.mjs
new file mode 100644
index 0000000..d919df5
--- /dev/null
+++ b/scripts/release/collect.mjs
@@ -0,0 +1,104 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+import { fileURLToPath } from 'node:url';
+import { execFileSync } from 'node:child_process';
+import { parseArgs, requireValue, main, gh, confined, sha256, readJSON, digest, writeJSON } from './lib.mjs';
+import { INVENTORY_FILE, assertSealRole, verifyReference, validateApprovedAgentReport, requireTrustedReference } from './evidence.mjs';
+import { resolveAttempt, assertArtifactIdentity } from './actions-artifacts.mjs';
+
+export function mergeArtifacts(source, target, onFile = () => {}) {
+ function visit(directory, relative = '') {
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
+ const name = relative ? `${relative}/${entry.name}` : entry.name;
+ const input = path.join(directory, entry.name), output = confined(target, name);
+ if (entry.isSymbolicLink()) throw new Error('Symlinks are forbidden in review evidence');
+ if (entry.isDirectory()) visit(input, name);
+ else if (entry.isFile()) {
+ const bytes = fs.readFileSync(input);
+ onFile(name, sha256(bytes));
+ if (fs.existsSync(output) && sha256(fs.readFileSync(output)) !== sha256(bytes)) throw new Error(`Conflicting evidence file: ${name}`);
+ fs.mkdirSync(path.dirname(output), { recursive: true }); fs.writeFileSync(output, bytes);
+ } else throw new Error('Unsupported evidence object');
+ }
+ }
+ visit(source);
+}
+// Reviews are derived judgments only. Execution inputs and bytes come exclusively from downloaded Actions artifacts.
+export function mergeReviews(source, target, trusted) {
+ const files = new Map();
+ function scan(dir, prefix = '') {
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const name = prefix + entry.name;
+ if (entry.isDirectory()) scan(path.join(dir, entry.name), name + '/');
+ else if (entry.isFile()) { confined(source, name); files.set(name, path.join(dir, entry.name)); }
+ else throw new Error('Unsupported review evidence object');
+ }
+ }
+ scan(source);
+ const approved = [], allowed = new Set();
+ const allow = ref => { verifyReference(source, ref); allowed.add(ref.file); };
+ for (const [name, file] of files) {
+ if (!name.endsWith('.json')) continue;
+ const report = readJSON(file);
+ if (report.gate !== 'agents' || report.status !== 'passed') continue;
+ requireTrustedReference(target, trusted, report.unreviewed_report, 'agent-e2e.yml');
+ const receipt = readJSON(verifyReference(source, report.review_receipt));
+ allowed.add(name); allow(report.review_receipt); allow(receipt.assessment); allow(receipt.reviewer_input);
+ approved.push(report);
+ }
+ if (!approved.length) throw new Error('No derived independent agent reviews supplied');
+ for (const name of files.keys()) {
+ if (!allowed.has(name) || fs.existsSync(confined(target, name)) || trusted.entries.has(name) || name === INVENTORY_FILE || name === 'release-manifest.json') throw new Error('Review files may only add derived judgments without replacing execution evidence: ' + name);
+ }
+ mergeArtifacts(source, target);
+ for (const report of approved) validateApprovedAgentReport(target, report);
+ return approved.length;
+}
+export function inventoryCollector(environment = process.env) {
+ return { repository: environment.GITHUB_REPOSITORY, workflow: environment.GITHUB_WORKFLOW_REF,
+ source_commit: environment.GITHUB_SHA, run_id: environment.GITHUB_RUN_ID, run_attempt: environment.GITHUB_RUN_ATTEMPT };
+}
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(() => {
+ const args = parseArgs(), output = path.resolve(requireValue(args, 'out'));
+ const candidate = requireValue(args, 'candidate-run');
+ const runs = requireValue(args, 'agent-runs').split(',');
+ if (![candidate, ...runs].every(id => /^[1-9]\d*$/.test(id)) || new Set(runs).size !== runs.length || runs.length > 30) throw new Error('Expected distinct numeric workflow run IDs');
+ if (process.env.GITHUB_ACTIONS !== 'true' || process.env.GITHUB_REPOSITORY !== 'shendeguize/AgentOrganon' || process.env.GITHUB_EVENT_NAME !== 'workflow_dispatch') throw new Error('Evidence collection requires an explicit coordinator workflow');
+ if (fs.existsSync(output) && fs.readdirSync(output).length) throw new Error('Collection output must be empty');
+ const inventory = { schema_version: 1, collector: inventoryCollector(), candidate_run: candidate, runs: [], files: [] };
+ const entries = new Map();
+ const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'organon-evidence-'));
+ try {
+ for (const [id, workflow] of [[candidate, 'candidate.yml'], ...runs.map(id => [id, 'agent-e2e.yml'])]) {
+ const { run, artifacts } = resolveAttempt(id, workflow, process.env.GITHUB_SHA);
+ inventory.runs.push({ run_id: id, run_attempt: String(run.run_attempt), workflow, repository: run.repository.full_name, source_commit: run.head_sha, conclusion: run.conclusion });
+ for (const artifact of artifacts) {
+ const entry = artifact.name;
+ const prefix = entry.startsWith('candidate-packages-') ? '' : workflow === 'candidate.yml' ? 'reports/' : 'evidence/';
+ const destination = path.join(temporary, id, entry);
+ execFileSync('gh', ['run', 'download', id, '--repo', 'shendeguize/AgentOrganon', '--name', entry, '--dir', destination], { stdio: 'pipe' });
+ // A name cannot silently switch to a newly uploaded ID while it is downloaded.
+ assertArtifactIdentity(gh([`repos/shendeguize/AgentOrganon/actions/artifacts/${artifact.id}`]), run, artifact);
+ mergeArtifacts(destination, path.join(output, prefix), (name, hash) => {
+ const relative = prefix + name;
+ if (relative === INVENTORY_FILE) throw new Error('Downloaded artifacts cannot supply the trusted inventory');
+ if (relative === 'release-manifest.json') return; // The final manifest references this inventory.
+ const previous = entries.get(relative);
+ if (previous && (previous.sha256 !== hash || previous.run_id !== id || previous.workflow !== workflow || previous.artifact_id !== String(artifact.id))) throw new Error('Ambiguous artifact origin: ' + relative);
+ if (!previous) entries.set(relative, { path: relative, sha256: hash, run_id: id, workflow, artifact: entry, run_attempt: String(run.run_attempt), artifact_id: String(artifact.id), artifact_digest: artifact.digest });
+ });
+ }
+ }
+ const manifest = readJSON(path.join(output, 'release-manifest.json'));
+ if (digest(manifest) !== requireValue(args, 'manifest-sha256')) throw new Error('Candidate identity differs from reviewed input');
+ assertSealRole(manifest);
+ inventory.source_digest = manifest.source_digest; inventory.artifact_digest = digest(manifest.artifacts);
+ inventory.files = [...entries.values()].sort((a, b) => a.path.localeCompare(b.path));
+ writeJSON(path.join(output, INVENTORY_FILE), inventory);
+ const reviews = path.resolve(requireValue(args, 'reviews'));
+ mergeReviews(reviews, output, { inventory, entries });
+ if (digest(readJSON(path.join(output, 'release-manifest.json'))) !== digest(manifest)) throw new Error('Review files must not replace the candidate manifest');
+ console.log(JSON.stringify({ status: 'collected', runs, candidate }));
+ } finally { fs.rmSync(temporary, { recursive: true, force: true }); }
+});
diff --git a/scripts/release/content.mjs b/scripts/release/content.mjs
new file mode 100644
index 0000000..1e74f4f
--- /dev/null
+++ b/scripts/release/content.mjs
@@ -0,0 +1,45 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { execFileSync } from 'node:child_process';
+import { parseArgs, readJSON, writeJSON, main } from './lib.mjs';
+
+export function checkContent(repo) {
+ repo = path.resolve(repo);
+ const names = execFileSync('git', ['-C', repo, 'ls-files', '-z', '--cached', '--others', '--exclude-standard'], { encoding: 'utf8' }).split('\0');
+ const errors = []; let checked = 0;
+ for (const relative of new Set(names)) {
+ if (!relative.endsWith('.md') || /(^|\/)(\.local|node_modules|dist|site|snapshots|reviews)(\/|$)/.test(relative)) continue;
+ const file = path.join(repo, relative);
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) continue;
+ const actual = fs.realpathSync(file);
+ // Quoted source/code examples are data, not executable repository navigation.
+ const text = fs.readFileSync(actual, 'utf8').replace(/^(`{3,}|~{3,})[^\n]*\n[\s\S]*?^\1\s*$/gm, '');
+ checked++;
+ for (const match of text.matchAll(/!?\[[^\]\n]*\]\((?:<([^>]+)>|([^\s)]+))(?:\s+"[^"]*")?\)/g)) {
+ const link = match[1] || match[2];
+ if (/^(?:[a-z][a-z0-9+.-]*:|#|\/\/)/i.test(link)) continue;
+ const pathname = decodeURIComponent(link.split('#')[0].split('?')[0]);
+ if (!pathname || pathname.startsWith('/')) continue;
+ if (!fs.existsSync(path.resolve(path.dirname(actual), pathname))) errors.push(`${relative}: missing ${link}`);
+ }
+ }
+ for (const relative of ['docs/rationale.md', 'zh/docs/rationale.md', 'zh/rationale.md', 'docs/self-iteration.md', 'zh/docs/self-iteration.md']) {
+ if (fs.existsSync(path.join(repo, relative))) errors.push(`Retired document entry remains: ${relative}`);
+ }
+ const philosophies = ['PHILOSOPHY.md', 'SoftwareEngineering/PHILOSOPHY.md'];
+ for (const relative of philosophies) {
+ const en = path.join(repo, relative); if (!fs.existsSync(en) || fs.lstatSync(en).isSymbolicLink()) continue;
+ const zh = path.join(path.dirname(en), 'zh/PHILOSOPHY.md');
+ if (!fs.existsSync(zh)) { errors.push(`${relative}: missing Chinese counterpart`); continue; }
+ const ids = file => [...fs.readFileSync(file, 'utf8').matchAll(//g)].map(m => m[1]);
+ if (JSON.stringify(ids(en)) !== JSON.stringify(ids(zh))) errors.push(`${relative}: EN/ZH stable IDs differ`);
+ }
+ return { status: errors.length ? 'failed' : 'passed', checked, errors };
+}
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(() => {
+ const args = parseArgs(); const result = checkContent(args.repo || '.');
+ if (args.manifest) { result.source_digest = readJSON(args.manifest).source_digest; result.gate = 'content'; result.product = args.product; }
+ if (args.out) writeJSON(args.out, result);
+ console.log(JSON.stringify(result, null, 2)); if (result.status !== 'passed') process.exitCode = 1;
+});
diff --git a/scripts/release/evidence.mjs b/scripts/release/evidence.mjs
new file mode 100644
index 0000000..c79b1f7
--- /dev/null
+++ b/scripts/release/evidence.mjs
@@ -0,0 +1,137 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { isDeepStrictEqual } from 'node:util';
+import { readJSON, confined, sha256, digest } from './lib.mjs';
+import { validateReceipt } from '../agents/review.mjs';
+
+export function verifyReference(root, reference) {
+ if (!reference || typeof reference.file !== 'string' || !/^[a-f0-9]{64}$/.test(reference.sha256 || '')) throw new Error('Invalid evidence reference');
+ const file = confined(root, reference.file);
+ if (!fs.statSync(file).isFile() || sha256(fs.readFileSync(file)) !== reference.sha256) throw new Error('Evidence artifact checksum mismatch: ' + reference.file);
+ return file;
+}
+// Only explicit {file, sha256} objects form artifact edges. Textual output is never interpreted as an artifact directive.
+export function discoverArtifactReferences(value) {
+ const references = [];
+ const visit = current => {
+ if (!current || typeof current !== 'object') return;
+ if (!Array.isArray(current) && Object.hasOwn(current, 'file') && Object.hasOwn(current, 'sha256')) {
+ if (typeof current.file !== 'string' || !/^[a-f0-9]{64}$/.test(current.sha256 || '')) throw new Error('Malformed nested artifact reference');
+ references.push({ file: current.file, sha256: current.sha256 });
+ }
+ for (const child of Object.values(current)) visit(child);
+ };
+ visit(value); return references;
+}
+export function artifactChildren(root, reference) {
+ const file = verifyReference(root, reference);
+ return reference.file.endsWith('.json') ? discoverArtifactReferences(readJSON(file)) : [];
+}
+export function evidenceRoots(manifest) { return discoverArtifactReferences(manifest); }
+export function collectEvidenceClosure(manifest, root) {
+ const pending = evidenceRoots(manifest), visited = new Map();
+ while (pending.length) {
+ const ref = pending.shift(), previous = visited.get(ref.file);
+ if (previous) { if (previous.sha256 !== ref.sha256) throw new Error('Conflicting identities for evidence file: ' + ref.file); continue; }
+ if (visited.size >= 10000) throw new Error('Evidence closure exceeds file limit');
+ visited.set(ref.file, ref); pending.push(...artifactChildren(root, ref));
+ }
+ return [...visited.values()].sort((a, b) => a.file.localeCompare(b.file));
+}
+export async function fetchEvidenceClosure(manifest, root, fetchArtifact) {
+ const pending = evidenceRoots(manifest), visited = new Map();
+ while (pending.length) {
+ const ref = pending.shift(), previous = visited.get(ref.file);
+ if (previous) { if (previous.sha256 !== ref.sha256) throw new Error('Conflicting identities for evidence file: ' + ref.file); continue; }
+ if (visited.size >= 10000) throw new Error('Evidence closure exceeds file limit');
+ // Confinement is checked before a caller is allowed to download this path.
+ confined(root, ref.file);
+ await fetchArtifact(ref);
+ visited.set(ref.file, ref); pending.push(...artifactChildren(root, ref));
+ }
+ return [...visited.values()].sort((a, b) => a.file.localeCompare(b.file));
+}
+export function validateApprovedAgentReport(root, approved) {
+ const unreviewedFile = verifyReference(root, approved.unreviewed_report);
+ const receipt = readJSON(verifyReference(root, approved.review_receipt));
+ const expected = { ...validateReceipt(root, unreviewedFile, receipt), unreviewed_report: approved.unreviewed_report, review_receipt: approved.review_receipt };
+ if (!isDeepStrictEqual(approved, expected)) throw new Error('Approved agent summary differs from its independently reviewed exact original');
+ return expected;
+}
+
+export const INVENTORY_FILE = 'trusted-executions.json';
+const COORDINATOR = 'shendeguize/AgentOrganon';
+const SEAL_WORKFLOW = `${COORDINATOR}/.github/workflows/seal.yml@refs/heads/release/1.0.0`;
+export function assertSealRole(manifest, environment = process.env) {
+ if (environment.GITHUB_ACTIONS !== 'true' || environment.GITHUB_EVENT_NAME !== 'workflow_dispatch' ||
+ environment.GITHUB_REPOSITORY !== COORDINATOR || environment.GITHUB_REF !== 'refs/heads/release/1.0.0' ||
+ environment.GITHUB_WORKFLOW_REF !== SEAL_WORKFLOW || environment.RUNNER_ENVIRONMENT !== 'github-hosted' ||
+ environment.GITHUB_SHA !== manifest.sources['agent-organon'].commit || !/^[1-9]\d*$/.test(environment.GITHUB_RUN_ID || '')) {
+ throw new Error('Sealing requires the exact protected hosted coordinator workflow');
+ }
+}
+export function readTrustedInventory(manifest, root, { required = false, sealing = false, environment = process.env } = {}) {
+ if (!manifest.trusted_executions) {
+ if (required || sealing || manifest.state === 'validated') throw new Error('Missing trusted execution inventory');
+ return null;
+ }
+ if (manifest.trusted_executions.file !== INVENTORY_FILE) throw new Error('Unexpected trusted inventory path');
+ const inventory = readJSON(verifyReference(root, manifest.trusted_executions));
+ const collector = inventory.collector;
+ if (inventory.schema_version !== 1 || inventory.source_digest !== manifest.source_digest || inventory.artifact_digest !== digest(manifest.artifacts) ||
+ collector?.repository !== COORDINATOR || collector.workflow !== SEAL_WORKFLOW || collector.source_commit !== manifest.sources['agent-organon'].commit ||
+ !/^[1-9]\d*$/.test(collector.run_id || '') || !/^[1-9]\d*$/.test(inventory.candidate_run || '') ||
+ !Array.isArray(inventory.runs) || !Array.isArray(inventory.files)) throw new Error('Invalid trusted execution inventory identity');
+ if (sealing) {
+ assertSealRole(manifest, environment);
+ if (collector.run_id !== environment.GITHUB_RUN_ID || collector.run_attempt !== environment.GITHUB_RUN_ATTEMPT) throw new Error('Inventory belongs to a different seal execution');
+ }
+ const runs = new Map();
+ for (const run of inventory.runs) {
+ if (!/^[1-9]\d*$/.test(run.run_id || '') || !/^[1-9]\d*$/.test(run.run_attempt || '') || runs.has(run.run_id) || run.source_commit !== collector.source_commit ||
+ run.repository !== COORDINATOR || !['candidate.yml', 'agent-e2e.yml'].includes(run.workflow) || run.conclusion !== 'success') throw new Error('Invalid trusted workflow run');
+ runs.set(run.run_id, run);
+ }
+ if (runs.get(inventory.candidate_run)?.workflow !== 'candidate.yml' || [...runs.values()].filter(run => run.workflow === 'candidate.yml').length !== 1) throw new Error('Missing unique trusted candidate run');
+ const entries = new Map();
+ for (const item of inventory.files) {
+ confined(root, item.path);
+ const run = runs.get(item.run_id);
+ if (entries.has(item.path) || item.path === INVENTORY_FILE || item.path === 'release-manifest.json' || !/^[a-f0-9]{64}$/.test(item.sha256 || '') ||
+ !run || item.workflow !== run.workflow || item.run_attempt !== run.run_attempt || !/^[1-9]\d*$/.test(item.artifact_id || '') || !/^sha256:[a-f0-9]{64}$/.test(item.artifact_digest || '') || !item.artifact?.endsWith('-' + run.run_attempt) || typeof item.artifact !== 'string' || !item.artifact) throw new Error('Invalid trusted artifact inventory entry');
+ entries.set(item.path, item);
+ }
+ return { inventory, entries };
+}
+export function requireTrustedReference(root, trusted, reference, workflow, runID) {
+ verifyReference(root, reference);
+ const entry = trusted.entries.get(reference.file);
+ if (!entry || entry.sha256 !== reference.sha256 || entry.workflow !== workflow || (runID && entry.run_id !== String(runID))) throw new Error('Evidence is not from the required trusted execution: ' + reference.file);
+ return entry;
+}
+export function validateTrustedEvidence(manifest, root, options = {}) {
+ const trusted = readTrustedInventory(manifest, root, options);
+ if (!trusted) return null;
+ const candidate = trusted.inventory.candidate_run;
+ for (const ref of Object.values(manifest.artifacts || {})) requireTrustedReference(root, trusted, ref, 'candidate.yml', candidate);
+ const packages = new Map(Object.values(manifest.artifacts || {}).map(ref => [ref.file, ref.sha256]));
+ for (const ref of manifest.evidence || []) {
+ const report = readJSON(verifyReference(root, ref));
+ if (report.gate !== 'agents') {
+ if (!['content', 'site', 'package', 'github', 'install'].includes(report.gate)) throw new Error('Unexpected mechanical evidence gate');
+ for (const child of collectEvidenceClosure({ evidence: [ref] }, root)) requireTrustedReference(root, trusted, child, 'candidate.yml', candidate);
+ continue;
+ }
+ const original = readJSON(verifyReference(root, report.unreviewed_report));
+ if (original.ci?.source_commit !== manifest.sources['agent-organon'].commit || !original.ci?.run_id) throw new Error('Agent execution CI identity mismatch');
+ const run = String(original.ci.run_id);
+ if (trusted.inventory.runs.find(item => item.run_id === run)?.run_attempt !== String(original.ci.run_attempt)) throw new Error('Agent execution attempt differs from trusted artifact origin');
+ // Raw output, inputs, indices and every captured attachment must share the original run.
+ // Only the exact release packages referenced by inputs belong to the candidate run.
+ for (const child of collectEvidenceClosure({ evidence: [report.unreviewed_report] }, root)) {
+ const isPackage = packages.get(child.file) === child.sha256;
+ requireTrustedReference(root, trusted, child, isPackage ? 'candidate.yml' : 'agent-e2e.yml', isPackage ? candidate : run);
+ }
+ }
+ return trusted;
+}
diff --git a/scripts/release/governance.mjs b/scripts/release/governance.mjs
new file mode 100644
index 0000000..3971e35
--- /dev/null
+++ b/scripts/release/governance.mjs
@@ -0,0 +1,109 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { PRODUCTS, gh, digest, readJSON, writeJSON, parseArgs, requireValue, main } from './lib.mjs';
+import { inspectSecurity } from './security.mjs';
+
+// Keep the audit credential out of site builders, npm and other child processes.
+// Only the read-only GitHub API adapter below receives it.
+const auditToken = process.env.GOVERNANCE_AUDIT_TOKEN;
+delete process.env.GOVERNANCE_AUDIT_TOKEN;
+function auditAPI(args, body) {
+ if (process.env.GITHUB_ACTIONS === 'true' && !auditToken) throw new Error('GOVERNANCE_AUDIT_TOKEN is required to inspect complete repository protections');
+ return gh(args, body, auditToken ? { ...process.env, GH_TOKEN: auditToken } : process.env);
+}
+
+export const REQUIRED_CHECK = 'Repository / verify';
+export const branchRules = () => ({ name: 'Organon protected branches', target: 'branch', enforcement: 'active', bypass_actors: [],
+ conditions: { ref_name: { include: ['refs/heads/main', 'refs/heads/dev*', 'refs/heads/dev*/**/*', 'refs/heads/release*', 'refs/heads/release*/**/*'], exclude: [] } },
+ rules: [{ type: 'deletion' }, { type: 'non_fast_forward' }, { type: 'pull_request', parameters: {
+ required_approving_review_count: 0, dismiss_stale_reviews_on_push: true, require_code_owner_review: false,
+ require_last_push_approval: false, required_review_thread_resolution: true,
+ } }, { type: 'required_status_checks', parameters: {
+ strict_required_status_checks_policy: true, required_status_checks: [{ context: REQUIRED_CHECK, integration_id: 15368 }],
+ } }],
+});
+export const tagRules = () => ({ name: 'Organon immutable version tags', target: 'tag', enforcement: 'active', bypass_actors: [],
+ conditions: { ref_name: { include: ['refs/tags/v*'], exclude: [] } }, rules: [{ type: 'deletion' }, { type: 'update' }, { type: 'non_fast_forward' }],
+});
+function comparableRules(value) {
+ const conditions = structuredClone(value.conditions);
+ for (const key of ['include', 'exclude']) conditions?.ref_name?.[key]?.sort();
+ const rules = structuredClone(value.rules);
+ for (const rule of rules) {
+ const p = rule.parameters;
+ if (rule.type === 'pull_request' && p) {
+ if (Array.isArray(p.required_reviewers) && p.required_reviewers.length === 0) delete p.required_reviewers;
+ if (p.ignore_approvals_from_contributors === false) delete p.ignore_approvals_from_contributors;
+ // GitHub adds this true default when creating the reviewed zero-approval rule.
+ // Retain it; an explicitly disabled value is not equivalent.
+ if (p.require_extra_approval_for_unattributed_changes === undefined) p.require_extra_approval_for_unattributed_changes = true;
+ if (Array.isArray(p.allowed_merge_methods) && [...p.allowed_merge_methods].sort().join() === 'merge,rebase,squash') delete p.allowed_merge_methods;
+ }
+ if (rule.type === 'required_status_checks' && p) {
+ if (p.do_not_enforce_on_create === false) delete p.do_not_enforce_on_create;
+ p.required_status_checks?.sort((a, b) => digest(a).localeCompare(digest(b)));
+ }
+ }
+ rules.sort((a, b) => a.type.localeCompare(b.type));
+ return { target: value.target, enforcement: value.enforcement, bypass_actors: value.bypass_actors, conditions, rules };
+}
+export const sameRules = (actual, desired) => Array.isArray(actual.bypass_actors) && digest(comparableRules(actual)) === digest(comparableRules(desired));
+export function allowedRepository(repository) {
+ if (!Object.values(PRODUCTS).some(p => `shendeguize/${p.repo}` === repository)) throw new Error('Unexpected repository');
+ return repository;
+}
+export async function inspect(repository, api = gh) {
+ allowedRepository(repository);
+ const listed = api([`repos/${repository}/rulesets`]);
+ const actual = listed.map(rule => api([`repos/${repository}/rulesets/${rule.id}`]));
+ const bypassEvidence = [];
+ for (const rule of actual) {
+ if (Array.isArray(rule.bypass_actors)) {
+ bypassEvidence.push({ id: rule.id, source: 'rest', count: rule.bypass_actors.length });
+ continue;
+ }
+ // REST omits actor details for read-only callers. GraphQL exposes the total
+ // count even when individual actor nodes are hidden. Never count visible nodes.
+ if (typeof rule.node_id !== 'string' || !rule.node_id) continue;
+ const [owner, name] = repository.split('/');
+ const response = api(['graphql'], { query: 'query($owner: String!, $name: String!, $databaseId: Int!) { repository(owner: $owner, name: $name) { nameWithOwner ruleset(databaseId: $databaseId) { __typename id databaseId source { __typename ... on Repository { nameWithOwner } } bypassActors(first: 1) { totalCount } } } }', variables: { owner, name, databaseId: rule.id } });
+ const repo = response?.data?.repository, node = repo?.ruleset, count = node?.bypassActors?.totalCount;
+ if (response?.errors?.length || repo?.nameWithOwner !== repository || node?.__typename !== 'RepositoryRuleset' || node?.id !== rule.node_id || node?.databaseId !== rule.id || node?.source?.__typename !== 'Repository' || node?.source?.nameWithOwner !== repository || !Number.isSafeInteger(count) || count < 0) throw new Error('Cannot establish complete ruleset bypass count');
+ bypassEvidence.push({ id: rule.id, node_id: rule.node_id, source: 'graphql-total-count', count });
+ if (count === 0) rule.bypass_actors = [];
+ }
+ const expected = [branchRules(), tagRules()];
+ const missing = expected.filter(rule => !actual.some(item => item.name === rule.name && sameRules(item, rule))).map(rule => rule.name);
+ return { status: missing.length ? 'failed' : 'passed', repository, missing, bypass_evidence: bypassEvidence, rulesets: actual.map(item => ({ id: item.id, name: item.name, enforcement: item.enforcement })) };
+}
+export async function inspectReleaseGovernance(repository, api = auditAPI) {
+ const rules = await inspect(repository, api);
+ const security = inspectSecurity(repository, api);
+ return { ...rules, status: rules.status === 'passed' && security.status === 'passed' ? 'passed' : 'failed', security };
+}
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(async () => {
+ const options = parseArgs(); const mode = options._[0] || 'check';
+ const repository = allowedRepository(requireValue(options, 'repository'));
+ if (mode === 'plan') {
+ const result = { repository, rulesets: [branchRules(), tagRules()], required_check: REQUIRED_CHECK };
+ if (options.out) writeJSON(options.out, result); else console.log(JSON.stringify(result, null, 2));
+ } else if (mode === 'apply') {
+ const ref = requireValue(options, 'validated-ref');
+ if (!/^[0-9a-f]{40}$/.test(ref)) throw new Error('A full validated commit is required');
+ const checks = gh([`repos/${repository}/commits/${ref}/check-runs`]).check_runs;
+ if (!checks.some(check => check.name === REQUIRED_CHECK && check.conclusion === 'success' && check.app?.slug === 'github-actions')) throw new Error('Required check has not actually succeeded on the supplied revision');
+ const existing = gh([`repos/${repository}/rulesets`]);
+ for (const rule of [branchRules(), tagRules()]) {
+ const match = existing.find(item => item.name === rule.name);
+ gh([`repos/${repository}/rulesets${match ? `/${match.id}` : ''}`, '--method', match ? 'PUT' : 'POST'], rule);
+ }
+ const result = await inspect(repository);
+ console.log(JSON.stringify(result, null, 2)); if (result.status !== 'passed') process.exitCode = 1;
+ } else if (mode === 'check') {
+ const result = await inspectReleaseGovernance(repository);
+ if (options.manifest) { const manifest = readJSON(options.manifest); result.gate = 'github'; result.product = Object.keys(PRODUCTS).find(k => `shendeguize/${PRODUCTS[k].repo}` === repository); result.source_digest = manifest.source_digest; }
+ if (options.out) writeJSON(options.out, result);
+ console.log(JSON.stringify(result, null, 2)); if (result.status !== 'passed') process.exitCode = 1;
+ } else throw new Error(`Unknown governance command: ${mode}`);
+});
diff --git a/scripts/release/install-matrix.mjs b/scripts/release/install-matrix.mjs
new file mode 100644
index 0000000..80cfe09
--- /dev/null
+++ b/scripts/release/install-matrix.mjs
@@ -0,0 +1,26 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { spawnSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import { parseArgs, requireValue, readJSON, writeJSON, digest, sha256, main } from './lib.mjs';
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
+main(() => {
+ const args = parseArgs();
+ const files = fs.readdirSync(path.join(root, 'tests')).filter(name => /^(installer|package).*\.test\.mjs$/.test(name)).sort().map(name => path.join(root, 'tests', name));
+ const result = spawnSync(process.execPath, ['--test', ...files], { cwd: root, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024,
+ env: { ...process.env, ...(args.manifest ? { ORGANON_TEST_CANDIDATE_MANIFEST: path.resolve(args.manifest) } : {}) } });
+ if (args.manifest) {
+ const manifest = readJSON(args.manifest), out = path.resolve(requireValue(args, 'out'));
+ if (process.env.GITHUB_ACTIONS !== 'true' || process.env.RUNNER_ENVIRONMENT !== 'github-hosted' || process.env.GITHUB_SHA !== manifest.sources['agent-organon'].commit) throw new Error('Release lifecycle evidence requires the exact source on a hosted runner');
+ fs.mkdirSync(path.dirname(out), { recursive: true });
+ const raw = `${out}.tap`; fs.writeFileSync(raw, `${result.stdout || ''}\n${result.stderr || ''}`, { flag: 'wx' });
+ writeJSON(out, { gate: 'install', status: result.status === 0 ? 'passed' : 'failed', platform: process.platform, lifecycle: result.status === 0 ? 'complete' : 'incomplete',
+ source_digest: manifest.source_digest, artifact_digest: digest(manifest.artifacts), node: process.version,
+ ci: { run_id: process.env.GITHUB_RUN_ID, source_commit: process.env.GITHUB_SHA },
+ raw_response: { file: path.relative(path.dirname(path.resolve(args.manifest)), raw).split(path.sep).join('/'), sha256: sha256(fs.readFileSync(raw)) } });
+ }
+ process.stdout.write(result.stdout || ''); process.stderr.write(result.stderr || '');
+ if (result.error) throw result.error;
+ process.exitCode = result.status ?? 1;
+});
diff --git a/scripts/release/lib.mjs b/scripts/release/lib.mjs
new file mode 100644
index 0000000..011781f
--- /dev/null
+++ b/scripts/release/lib.mjs
@@ -0,0 +1,57 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import crypto from 'node:crypto';
+import { execFileSync } from 'node:child_process';
+
+export const PRODUCTS = Object.freeze({
+ core: { repo: 'OrganonCore', directory: 'OrganonCore', package: '@shendeguize/organon-core' },
+ advised: { repo: 'AdvisedOrganons', directory: 'AdvisedOrganons', package: '@shendeguize/advised-organons' },
+ 'agent-organon': { repo: 'AgentOrganon', directory: '.', package: '@shendeguize/agent-organon' },
+});
+export const AGENTS = ['codex', 'claude', 'cursor', 'copilot', 'gemini', 'opencode'];
+export const PLATFORMS = ['linux', 'darwin', 'win32'];
+export const sha256 = data => crypto.createHash('sha256').update(data).digest('hex');
+export function canonical(value) {
+ if (Array.isArray(value)) return value.map(canonical);
+ if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(k => [k, canonical(value[k])]));
+ return value;
+}
+export const digest = value => sha256(JSON.stringify(canonical(value)));
+export const readJSON = file => JSON.parse(fs.readFileSync(file, 'utf8'));
+export function writeJSON(file, data) {
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
+}
+export function parseArgs(argv = process.argv.slice(2)) {
+ const options = { _: [] };
+ for (let i = 0; i < argv.length; i++) {
+ const token = argv[i];
+ if (!token.startsWith('--')) { options._.push(token); continue; }
+ const key = token.slice(2);
+ if (key in options) throw new Error(`Duplicate option: ${token}`);
+ options[key] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
+ }
+ return options;
+}
+export function requireValue(options, key) {
+ if (typeof options[key] !== 'string' || !options[key]) throw new Error(`Required: --${key} VALUE`);
+ return options[key];
+}
+export function confined(root, relative) {
+ if (typeof relative !== 'string' || !relative || path.isAbsolute(relative) || /^[A-Za-z]:/.test(relative) || relative.includes('\\')) throw new Error('Expected relative artifact path');
+ const target = path.resolve(root, relative);
+ if (!target.startsWith(`${path.resolve(root)}${path.sep}`)) throw new Error('Artifact escapes its root');
+ let cursor = target;
+ while (cursor !== path.resolve(root)) {
+ if (fs.existsSync(cursor) && fs.lstatSync(cursor).isSymbolicLink()) throw new Error(`Symlink artifact path: ${relative}`);
+ cursor = path.dirname(cursor);
+ }
+ return target;
+}
+export function git(repo, ...args) { return execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); }
+export function gh(args, body, env = process.env) {
+ return JSON.parse(execFileSync('gh', ['api', ...args, ...(body ? ['--input', '-'] : [])], {
+ input: body ? JSON.stringify(body) : undefined, env, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'],
+ }) || 'null');
+}
+export function main(fn) { Promise.resolve().then(fn).catch(error => { console.error(error.message); process.exitCode = 1; }); }
diff --git a/scripts/release/manifest.mjs b/scripts/release/manifest.mjs
new file mode 100644
index 0000000..48fcdf9
--- /dev/null
+++ b/scripts/release/manifest.mjs
@@ -0,0 +1,117 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { PRODUCTS, AGENTS, PLATFORMS, sha256, digest, readJSON, writeJSON, confined, git, parseArgs, requireValue, main } from './lib.mjs';
+
+import { checkPackage } from '../package/check.mjs';
+import { collectEvidenceClosure, validateApprovedAgentReport, validateTrustedEvidence } from './evidence.mjs';
+
+const HEX = /^[a-f0-9]{64}$/;
+const COMMIT = /^[a-f0-9]{40}$/;
+export const sourceIdentity = manifest => digest({ version: manifest.version, sources: manifest.sources });
+export function assertManifest(manifest, { publish = false } = {}) {
+ if (manifest.schema_version !== 1 || !/^1\.0\.0(?:-rc\.[1-9]\d*)?$/.test(manifest.version)) throw new Error('Invalid release manifest/version');
+ const rc = manifest.version.includes('-rc.');
+ if (manifest.channel !== (rc ? 'rc' : 'stable')) throw new Error('Release channel does not match version');
+ if (Object.keys(manifest.sources || {}).sort().join() !== Object.keys(PRODUCTS).sort().join()) throw new Error('All three source identities are required');
+ for (const [product, definition] of Object.entries(PRODUCTS)) {
+ const source = manifest.sources[product];
+ if (source.repository !== `shendeguize/${definition.repo}` || !COMMIT.test(source.commit)) throw new Error(`Invalid source identity: ${product}`);
+ if (publish && source.dirty) throw new Error(`Uncommitted source cannot be published: ${product}`);
+ }
+ if (manifest.source_digest !== sourceIdentity(manifest)) throw new Error('Source digest mismatch');
+ if (publish && manifest.state !== 'validated') throw new Error('Candidate has not completed all validation gates');
+ if (!rc && (!manifest.approved_rc || !/^1\.0\.0-rc\.[1-9]\d*$/.test(manifest.approved_rc.version) || !HEX.test(manifest.approved_rc.manifest_sha256))) throw new Error('Stable publication needs an exact approved RC manifest');
+ return manifest;
+}
+export function assertArtifact(root, artifact) {
+ if (!artifact || !HEX.test(artifact.sha256)) throw new Error('Missing artifact checksum');
+ const file = confined(root, artifact.file);
+ if (!fs.statSync(file).isFile() || sha256(fs.readFileSync(file)) !== artifact.sha256) throw new Error(`Artifact checksum mismatch: ${artifact.file}`);
+ return file;
+}
+export function assertGithubReport(report, product) {
+ const repository = PRODUCTS[product] && `shendeguize/${PRODUCTS[product].repo}`;
+ if (!repository || report?.status !== 'passed' || report.repository !== repository || report.security?.status !== 'passed' || report.security.repository !== repository) throw new Error(`Missing, failed or unrelated GitHub governance/security inspection: ${product}`);
+ return report;
+}
+export function validateEvidence(manifest, root, options = {}) {
+ assertManifest(manifest);
+ validateTrustedEvidence(manifest, root, options);
+ const reports = (manifest.evidence || []).map(item => {
+ const report = readJSON(assertArtifact(root, item));
+ if (report.status !== 'passed' || report.source_digest !== manifest.source_digest || report.artifact_digest !== digest(manifest.artifacts)) throw new Error(`Unsuccessful or unrelated evidence: ${item.file}`);
+ if (report.gate === 'agents') validateApprovedAgentReport(root, report);
+ if (report.gate === 'github') assertGithubReport(report, report.product);
+ return report;
+ });
+ for (const gate of ['content', 'site', 'package', 'github']) {
+ for (const product of Object.keys(PRODUCTS)) {
+ if (!reports.some(r => r.gate === gate && r.product === product)) throw new Error(`Missing ${gate} evidence: ${product}`);
+ }
+ }
+ for (const platform of PLATFORMS) {
+ if (!reports.some(r => r.gate === 'install' && r.platform === platform && r.lifecycle === 'complete')) throw new Error(`Missing installer lifecycle: ${platform}`);
+ for (const agent of AGENTS) {
+ const report = reports.find(r => r.gate === 'agents' && r.agent === agent && r.platform === platform && r.matrix === 'smoke');
+ if (!report || !report.actual_call || !report.independent_reviewer || !report.raw_response || !report.tool_version || !report.model) throw new Error(`Missing actual agent smoke: ${agent}/${platform}`);
+ assertArtifact(root, report.raw_response);
+ }
+ }
+ for (const agent of AGENTS) {
+ const report = reports.find(r => r.gate === 'agents' && r.agent === agent && r.platform === 'linux' && r.matrix === 'full');
+ if (!report || !report.actual_call || !report.independent_reviewer || !report.raw_response || !report.all_non_lean_skills || !report.independent_assessment) throw new Error(`Missing complete actual methods: ${agent}`);
+ assertArtifact(root, report.raw_response);
+ assertArtifact(root, report.independent_assessment);
+ }
+ validateReleasePackages(manifest, root);
+ collectEvidenceClosure(manifest, root);
+ return reports;
+}
+export function validateReleasePackages(manifest, root) {
+ assertManifest(manifest);
+ for (const [product] of Object.entries(PRODUCTS)) {
+ const checked = checkPackage(assertArtifact(root, manifest.artifacts?.[product]));
+ if (checked.product !== product || checked.version !== manifest.version) throw new Error('Package product/version does not match release: ' + product);
+ for (const [sourceProduct, source] of Object.entries(manifest.sources)) {
+ if (checked.snapshots[PRODUCTS[sourceProduct].repo] !== source.commit) throw new Error('Package source snapshots differ from release: ' + product + '/' + sourceProduct);
+ }
+ }
+ return true;
+}
+export function assertDispatch(manifest, environment, product = 'agent-organon') {
+ assertManifest(manifest, { publish: true });
+ if (environment.RELEASE_VERSION && environment.RELEASE_VERSION !== manifest.version) throw new Error('Dispatch version differs from approved manifest');
+ if (environment.GITHUB_ACTIONS !== 'true' || environment.GITHUB_EVENT_NAME !== 'workflow_dispatch') throw new Error('Publication runs only through explicitly dispatched GitHub Actions');
+ if (!PRODUCTS[product] || environment.GITHUB_REPOSITORY !== `shendeguize/${PRODUCTS[product].repo}`) throw new Error('Wrong publication repository');
+ if (environment.RELEASE_MANIFEST_SHA256 !== digest(manifest)) throw new Error('Approved manifest identity mismatch');
+ if (environment.GITHUB_SHA !== manifest.sources[product].commit) throw new Error('Workflow commit does not match release source');
+ if (environment.GITHUB_REF !== 'refs/heads/release/1.0.0') throw new Error('Publication must run from release/1.0.0');
+ if (manifest.channel === 'stable' && environment.APPROVED_RC_MANIFEST_SHA256 !== manifest.approved_rc.manifest_sha256) throw new Error('Stable RC approval identity mismatch');
+}
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(() => {
+ const options = parseArgs();
+ const mode = options._[0] || 'check';
+ if (mode === 'prepare') {
+ const workspace = path.resolve(options.workspace || '.');
+ const version = requireValue(options, 'version');
+ const sources = Object.fromEntries(Object.entries(PRODUCTS).map(([product, spec]) => {
+ const repo = path.join(workspace, spec.directory);
+ const dirty = Boolean(git(repo, 'status', '--porcelain=v1', '--untracked-files=normal'));
+ if (dirty && !options['allow-dirty']) throw new Error(`Source has uncommitted work: ${product}`);
+ return [product, { repository: `shendeguize/${spec.repo}`, commit: git(repo, 'rev-parse', 'HEAD'), dirty }];
+ }));
+ const manifest = { schema_version: 1, version, channel: version.includes('-rc.') ? 'rc' : 'stable', state: 'prepared', sources, artifacts: {}, evidence: [] };
+ if (options['approved-rc']) manifest.approved_rc = readJSON(options['approved-rc']);
+ manifest.source_digest = sourceIdentity(manifest);
+ assertManifest(manifest);
+ writeJSON(requireValue(options, 'out'), manifest);
+ console.log(JSON.stringify({ status: 'prepared', source_digest: manifest.source_digest }));
+ } else if (mode === 'check' || mode === 'seal') {
+ const file = path.resolve(requireValue(options, 'manifest'));
+ const manifest = readJSON(file);
+ validateEvidence(manifest, path.dirname(file), { sealing: mode === 'seal' });
+ if (mode === 'seal') { manifest.state = 'validated'; assertManifest(manifest, { publish: true }); writeJSON(requireValue(options, 'out'), manifest); }
+ console.log(JSON.stringify({ status: 'passed', manifest_sha256: digest(manifest), source_digest: manifest.source_digest }));
+ } else throw new Error(`Unknown manifest command: ${mode}`);
+});
diff --git a/scripts/release/publish.mjs b/scripts/release/publish.mjs
new file mode 100644
index 0000000..f8427d7
--- /dev/null
+++ b/scripts/release/publish.mjs
@@ -0,0 +1,172 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+import crypto from 'node:crypto';
+import { execFileSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import { PRODUCTS, sha256, digest, readJSON, writeJSON, confined, gh, parseArgs, requireValue, main } from './lib.mjs';
+import { assertManifest, assertArtifact, assertDispatch, validateEvidence, assertGithubReport } from './manifest.mjs';
+import { collectEvidenceClosure, fetchEvidenceClosure } from './evidence.mjs';
+import { validateStable } from './stable.mjs';
+import * as governance from './governance.mjs';
+
+export const assetName = item => item.file.endsWith('.tgz') ? path.basename(item.file) : `${item.sha256}-${path.basename(item.file)}`;
+const execute = (cmd, args, options = {}) => execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], ...options });
+const integrity = file => `sha512-${crypto.createHash('sha512').update(fs.readFileSync(file)).digest('base64')}`;
+export function releaseFiles(manifest, root) {
+ const items = collectEvidenceClosure(manifest, root), names = new Map();
+ for (const item of items) {
+ const name = assetName(item);
+ if (names.has(name) && names.get(name) !== item.sha256) throw new Error(`Conflicting release asset name: ${name}`);
+ names.set(name, item.sha256);
+ }
+ return [...new Map(items.map(item => [assetName(item), item])).values()];
+}
+export async function registryVersion(product, version) {
+ const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(PRODUCTS[product].package)}/${version}`);
+ if (response.status === 404) return null;
+ if (!response.ok) throw new Error(`npm registry lookup failed: HTTP ${response.status}`);
+ return response.json();
+}
+async function publishNpm(manifest, product, file) {
+ const previous = await registryVersion(product, manifest.version);
+ const expected = integrity(file);
+ if (previous) {
+ if (previous.dist?.integrity !== expected) throw new Error(`Published version has different bytes: ${product}`);
+ return 'already-matching';
+ }
+ const strategy = process.env.NPM_CHANNEL_STRATEGY;
+ if (!['direct', 'deferred'].includes(strategy)) throw new Error('Explicit approved NPM_CHANNEL_STRATEGY is required');
+ const temporaryTag = strategy === 'direct' ? (manifest.channel === 'rc' ? 'rc' : 'latest') : `candidate-${manifest.version.replace(/\./g, '-')}`;
+ execute('npm', ['publish', file, '--access', 'public', '--provenance', '--tag', temporaryTag], { stdio: 'inherit' });
+ const current = await registryVersion(product, manifest.version);
+ if (current?.dist?.integrity !== expected) throw new Error(`npm post-publication integrity mismatch: ${product}`);
+ return 'published';
+}
+function getRelease(repository, tag) {
+ try { return gh([`repos/${repository}/releases/tags/${tag}`]); }
+ catch (error) { if (String(error.stderr || error.message).includes('404')) return null; throw error; }
+}
+function assertTag(repository, tag, commit) {
+ let object = gh([`repos/${repository}/git/ref/tags/${tag}`]).object;
+ for (let depth = 0; object.type === 'tag' && depth < 5; depth++) object = gh([`repos/${repository}/git/tags/${object.sha}`]).object;
+ if (object.type !== 'commit' || object.sha !== commit) throw new Error(`Release tag has different source: ${repository}/${tag}`);
+}
+function ensureGithub(manifest, product, root, { coordinator = false } = {}) {
+ const repository = manifest.sources[product].repository;
+ const tag = `v${manifest.version}`;
+ let release = getRelease(repository, tag);
+ if (!release) {
+ release = gh([`repos/${repository}/releases`, '--method', 'POST'], { tag_name: tag, target_commitish: manifest.sources[product].commit,
+ name: `${PRODUCTS[product].repo} ${manifest.version}`, draft: true, prerelease: manifest.channel === 'rc', make_latest: 'false',
+ body: `Product ${manifest.version}. Manifest identity: ${digest(manifest)}. Philosophy and product versions are separate. Install and validation details are in the attached manifest.`, });
+ }
+ if (release.draft && release.target_commitish !== manifest.sources[product].commit) throw new Error('Existing draft targets a different source commit');
+ if (!release.draft) assertTag(repository, tag, manifest.sources[product].commit);
+ else {
+ try { gh([`repos/${repository}/git/ref/tags/${tag}`]); assertTag(repository, tag, manifest.sources[product].commit); }
+ catch (error) { if (!String(error.stderr || error.message).includes('404')) throw error; }
+ }
+ const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'organon-publish-'));
+ try {
+ const manifestFile = path.join(temporary, 'release-manifest.json'); writeJSON(manifestFile, manifest);
+ const items = coordinator ? releaseFiles(manifest, root) : [manifest.artifacts[product]];
+ const upload = [{ file: manifestFile, name: 'release-manifest.json' }, ...items.map(item => ({ file: assertArtifact(root, item), name: assetName(item) }))];
+ for (const item of upload) {
+ const existing = release.assets.find(asset => asset.name === item.name);
+ if (existing) {
+ const bytes = execFileSync('gh', ['api', `repos/${repository}/releases/assets/${existing.id}`, '-H', 'Accept: application/octet-stream'], { maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
+ if (sha256(bytes) !== sha256(fs.readFileSync(item.file))) throw new Error(`Existing release asset differs: ${item.name}`);
+ } else {
+ if (!release.draft) throw new Error(`Published release is incomplete; use a new RC: ${item.name}`);
+ const copied = path.join(temporary, item.name); if (copied !== item.file) fs.copyFileSync(item.file, copied);
+ execute('gh', ['release', 'upload', tag, copied, '--repo', repository]);
+ }
+ }
+ if (release.draft) gh([`repos/${repository}/releases/${release.id}`, '--method', 'PATCH'], { draft: false, prerelease: manifest.channel === 'rc', make_latest: 'false' });
+ assertTag(repository, tag, manifest.sources[product].commit);
+ } finally { fs.rmSync(temporary, { recursive: true, force: true }); }
+}
+export async function recheckReleaseGovernance(manifest, inspect = governance.inspectReleaseGovernance) {
+ assertManifest(manifest);
+ const reports = [];
+ for (const product of Object.keys(PRODUCTS)) {
+ const report = await inspect(manifest.sources[product].repository);
+ assertGithubReport(report, product);
+ reports.push({ ...report, product });
+ }
+ return reports;
+}
+export async function withReleaseGovernance(manifest, operation, inspect = governance.inspectReleaseGovernance) {
+ const reports = await recheckReleaseGovernance(manifest, inspect);
+ return operation(reports);
+}
+export async function verifyPublished(manifest, root, products = Object.keys(PRODUCTS), { inspect = governance.inspectReleaseGovernance } = {}) {
+ return withReleaseGovernance(manifest, async () => {
+ for (const product of products) {
+ const current = await registryVersion(product, manifest.version);
+ if (current?.dist?.integrity !== integrity(assertArtifact(root, manifest.artifacts[product]))) throw new Error(`Not all npm products are published with matching bytes: ${product}`);
+ const repository = manifest.sources[product].repository;
+ const release = getRelease(repository, `v${manifest.version}`);
+ if (!release || release.draft || release.prerelease !== (manifest.channel === 'rc')) throw new Error(`GitHub product not published: ${product}`);
+ assertTag(repository, `v${manifest.version}`, manifest.sources[product].commit);
+ const checks = [{ name: 'release-manifest.json', expected: sha256(`${JSON.stringify(manifest, null, 2)}\n`) },
+ ...((product === 'agent-organon' ? releaseFiles(manifest, root) : [manifest.artifacts[product]])).map(item => ({ name: assetName(item), expected: item.sha256 }))];
+ for (const item of checks) {
+ const asset = release.assets.find(value => value.name === item.name);
+ if (!asset) throw new Error(`GitHub artifact missing: ${product}/${item.name}`);
+ const bytes = execFileSync('gh', ['api', `repos/${repository}/releases/assets/${asset.id}`, '-H', 'Accept: application/octet-stream'], { maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
+ if (sha256(bytes) !== item.expected) throw new Error(`Downloaded GitHub bytes differ: ${product}/${item.name}`);
+ }
+ if (!current.dist?.tarball?.startsWith('https://registry.npmjs.org/')) throw new Error('Unexpected npm tarball origin');
+ const download = await fetch(current.dist.tarball);
+ if (!download.ok) throw new Error(`npm tarball download failed: ${product}`);
+ if (sha256(Buffer.from(await download.arrayBuffer())) !== manifest.artifacts[product].sha256) throw new Error(`Downloaded npm bytes differ: ${product}`);
+ }
+ }, inspect);
+}
+async function fetchAsset(version, name, expected, destination) {
+ const response = await fetch(`https://github.com/shendeguize/AgentOrganon/releases/download/v${version}/${encodeURIComponent(name)}`);
+ if (!response.ok) throw new Error(`Validated release bundle unavailable: ${name}, HTTP ${response.status}`);
+ const bytes = Buffer.from(await response.arrayBuffer());
+ if (bytes.length > 64 * 1024 * 1024 || (expected && sha256(bytes) !== expected)) throw new Error(`Invalid bundle asset: ${name}`);
+ fs.mkdirSync(path.dirname(destination), { recursive: true }); fs.writeFileSync(destination, bytes);
+}
+export async function fetchBundle(version, expectedManifest, root) {
+ if (!/^1\.0\.0(?:-rc\.[1-9]\d*)?$/.test(version) || !/^[a-f0-9]{64}$/.test(expectedManifest)) throw new Error('Invalid bundle identity');
+ const file = path.join(root, 'release-manifest.json');
+ await fetchAsset(version, 'release-manifest.json', null, file);
+ const manifest = readJSON(file); assertManifest(manifest, { publish: true });
+ if (manifest.version !== version || digest(manifest) !== expectedManifest) throw new Error('Downloaded manifest is not the approved object');
+ const fetchItem = item => fetchAsset(version, assetName(item), item.sha256, confined(root, item.file));
+ await fetchEvidenceClosure(manifest, root, fetchItem);
+ validateEvidence(manifest, root); return manifest;
+}
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(async () => {
+ const args = parseArgs(); const operation = args._[0];
+ if (operation === 'fetch') { const manifest = await fetchBundle(requireValue(args, 'version'), requireValue(args, 'manifest-sha256'), path.resolve(requireValue(args, 'out'))); console.log(JSON.stringify({ status: 'passed', manifest_sha256: digest(manifest) })); return; }
+ const file = path.resolve(requireValue(args, 'manifest')); const root = path.dirname(file); const manifest = readJSON(file);
+ validateEvidence(manifest, root);
+ if (operation === 'verify') { await verifyPublished(manifest, root); console.log(JSON.stringify({ status: 'passed' })); return; }
+ const product = requireValue(args, 'product'); assertDispatch(manifest, process.env, product);
+ if (manifest.channel === 'stable') await validateStable(manifest, root);
+ await withReleaseGovernance(manifest, async () => {
+ if (operation === 'bootstrap-bundle') {
+ if (product !== 'agent-organon') throw new Error('Only the coordinator publishes the validated bundle');
+ ensureGithub(manifest, product, root, { coordinator: true });
+ } else if (operation === 'publish') {
+ if (process.env.NPM_CHANNEL_STRATEGY === 'direct') await verifyPublished(manifest, root, Object.keys(PRODUCTS).slice(0, Object.keys(PRODUCTS).indexOf(product)));
+ await publishNpm(manifest, product, assertArtifact(root, manifest.artifacts[product]));
+ ensureGithub(manifest, product, root, { coordinator: product === 'agent-organon' });
+ } else if (operation === 'promote') {
+ await verifyPublished(manifest, root);
+ if (process.env.NPM_CHANNEL_STRATEGY === 'deferred') {
+ if (!process.env.NODE_AUTH_TOKEN) throw new Error('Deferred npm channel promotion requires a separately authorized tag-management credential; OIDC does not support dist-tag');
+ execute('npm', ['dist-tag', 'add', `${PRODUCTS[product].package}@${manifest.version}`, manifest.channel === 'rc' ? 'rc' : 'latest'], { stdio: 'inherit' });
+ } else if (process.env.NPM_CHANNEL_STRATEGY !== 'direct') throw new Error('Explicit approved NPM_CHANNEL_STRATEGY is required');
+ const release = getRelease(manifest.sources[product].repository, `v${manifest.version}`);
+ if (manifest.channel === 'stable') gh([`repos/${manifest.sources[product].repository}/releases/${release.id}`, '--method', 'PATCH'], { make_latest: 'true' });
+ } else throw new Error('Expected fetch, bootstrap-bundle, publish, promote or verify');
+ });
+ console.log(JSON.stringify({ status: 'passed', operation, product, version: manifest.version, manifest_sha256: digest(manifest) }));
+});
diff --git a/scripts/release/security.mjs b/scripts/release/security.mjs
new file mode 100644
index 0000000..bef0b96
--- /dev/null
+++ b/scripts/release/security.mjs
@@ -0,0 +1,58 @@
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { gh, parseArgs, requireValue, main, writeJSON } from './lib.mjs';
+import { allowedRepository } from './governance.mjs';
+
+export const environmentPlan = repository => [
+ { name: 'npm-rc', branches: ['release/1.0.0'], review: false },
+ { name: 'npm-stable', branches: ['release/1.0.0'], review: true },
+ { name: 'github-pages', branches: ['main', 'release/1.0.0'], review: false },
+ ...(repository.endsWith('/AgentOrganon') ? [
+ { name: 'agent-validation', branches: ['main', 'release/1.0.0'], review: true },
+ { name: 'release-validation', branches: ['release/1.0.0'], review: true },
+ ] : []),
+];
+export function inspectSecurity(repository, api = gh) {
+ allowedRepository(repository);
+ const actions = api([`repos/${repository}/actions/permissions`]);
+ const workflow = api([`repos/${repository}/actions/permissions/workflow`]);
+ const missing = [];
+ if (!actions.enabled || actions.sha_pinning_required !== true) missing.push('full Action revision pinning');
+ if (workflow.default_workflow_permissions !== 'read' || workflow.can_approve_pull_request_reviews !== false) missing.push('read-only default token without PR approval');
+ const environments = [];
+ for (const wanted of environmentPlan(repository)) {
+ let actual;
+ try { actual = api([`repos/${repository}/environments/${wanted.name}`]); }
+ catch (error) { if (!String(error.stderr || error.message).includes('404')) throw error; missing.push(`environment ${wanted.name}`); continue; }
+ if (actual.deployment_branch_policy?.protected_branches !== false || actual.deployment_branch_policy?.custom_branch_policies !== true) missing.push(`${wanted.name}: exact deployment branches`);
+ const branches = api([`repos/${repository}/environments/${wanted.name}/deployment-branch-policies`]).branch_policies;
+ if (branches.some(item => item.type !== 'branch') || branches.map(item => item.name).sort().join() !== [...wanted.branches].sort().join()) missing.push(`${wanted.name}: deployment branch list`);
+ const reviewer = actual.protection_rules.find(item => item.type === 'required_reviewers');
+ if (wanted.review && (!reviewer || reviewer.prevent_self_review !== false || reviewer.reviewers.length !== 1 || reviewer.reviewers[0].reviewer.login !== 'shendeguize' || actual.can_admins_bypass !== false)) missing.push(`${wanted.name}: owner approval without administrator bypass`);
+ environments.push({ name: wanted.name, can_admins_bypass: actual.can_admins_bypass, branches: branches.map(item => item.name), owner_approval: wanted.review });
+ }
+ return { status: missing.length ? 'failed' : 'passed', repository, missing, actions, workflow, environments };
+}
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(() => {
+ const args = parseArgs(), repository = allowedRepository(requireValue(args, 'repository'));
+ const operation = args._[0] || 'check';
+ if (operation === 'plan') { console.log(JSON.stringify({ repository, environments: environmentPlan(repository), actions: { enabled: true, sha_pinning_required: true }, workflow: { default_workflow_permissions: 'read', can_approve_pull_request_reviews: false } }, null, 2)); return; }
+ if (operation === 'apply') {
+ const current = gh([`repos/${repository}/actions/permissions`]);
+ gh([`repos/${repository}/actions/permissions`, '--method', 'PUT'], { enabled: true, allowed_actions: current.allowed_actions, sha_pinning_required: true });
+ gh([`repos/${repository}/actions/permissions/workflow`, '--method', 'PUT'], { default_workflow_permissions: 'read', can_approve_pull_request_reviews: false });
+ const owner = gh(['users/shendeguize']);
+ for (const environment of environmentPlan(repository)) {
+ gh([`repos/${repository}/environments/${environment.name}`, '--method', 'PUT'], { wait_timer: 0, prevent_self_review: false,
+ reviewers: environment.review ? [{ type: 'User', id: owner.id }] : [], deployment_branch_policy: { protected_branches: false, custom_branch_policies: true } });
+ const existing = gh([`repos/${repository}/environments/${environment.name}/deployment-branch-policies`]).branch_policies;
+ for (const item of existing) if (item.type !== 'branch' || !environment.branches.includes(item.name)) gh([`repos/${repository}/environments/${environment.name}/deployment-branch-policies/${item.id}`, '--method', 'DELETE']);
+ for (const name of environment.branches) if (!existing.some(item => item.type === 'branch' && item.name === name)) gh([`repos/${repository}/environments/${environment.name}/deployment-branch-policies`, '--method', 'POST'], { name, type: 'branch' });
+ }
+ // The documented REST update does not expose administrator bypass. Inspect it
+ // and require the actual GitHub UI setting rather than inventing an API field.
+ } else if (operation !== 'check') throw new Error('Expected plan, apply or check');
+ const result = inspectSecurity(repository);
+ if (args.out) writeJSON(args.out, result);
+ console.log(JSON.stringify(result, null, 2)); if (result.status !== 'passed') process.exitCode = 1;
+});
diff --git a/scripts/release/site-data.mjs b/scripts/release/site-data.mjs
new file mode 100644
index 0000000..2f202bc
--- /dev/null
+++ b/scripts/release/site-data.mjs
@@ -0,0 +1,163 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { execFileSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import { PRODUCTS, digest, sha256, readJSON, writeJSON, parseArgs, requireValue, main } from './lib.mjs';
+import { assertManifest } from './manifest.mjs';
+import { validateHistory, observe, renderSVG } from './stars.mjs';
+import { fetchBundle, verifyPublished } from './publish.mjs';
+
+const MUTABLE = new Set(['assets/stars.json', 'assets/stars.svg']);
+export function productFor(repository) {
+ const product = Object.keys(PRODUCTS).find(key => repository === `shendeguize/${PRODUCTS[key].repo}`);
+ if (!product) throw new Error('Unexpected site repository');
+ return product;
+}
+export function assertSiteRunner(env, operation) {
+ productFor(env.GITHUB_REPOSITORY);
+ if (!['stars', 'pages'].includes(operation) || env.GITHUB_ACTIONS !== 'true' || env.RUNNER_ENVIRONMENT !== 'github-hosted' || !env.RUNNER_TEMP || !env.GITHUB_TOKEN || !['refs/heads/main', 'refs/heads/release/1.0.0'].includes(env.GITHUB_REF) || !(operation === 'stars' ? ['schedule', 'workflow_dispatch'] : ['workflow_dispatch']).includes(env.GITHUB_EVENT_NAME)) throw new Error('Site writes require a reviewed GitHub-hosted workflow');
+}
+export function inventory(root) {
+ if (fs.lstatSync(root).isSymbolicLink() || !fs.statSync(root).isDirectory()) throw new Error('Invalid public site directory');
+ const files = {};
+ function visit(directory, prefix = '') {
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a,b) => a.name.localeCompare(b.name))) {
+ const relative = `${prefix}${entry.name}`;
+ if (entry.isSymbolicLink()) throw new Error('Site tree contains a symlink');
+ if (entry.isDirectory()) visit(path.join(directory, entry.name), `${relative}/`);
+ else if (entry.isFile()) { if (!MUTABLE.has(relative)) files[relative] = sha256(fs.readFileSync(path.join(directory, entry.name))); }
+ else throw new Error('Site tree contains a special file');
+ }
+ }
+ visit(root);
+ if (!files['index.html'] || !files['zh/index.html']) throw new Error('Missing bilingual site entrypoints');
+ return files;
+}
+export function validateState(root, repository) {
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
+ if (entry.name === '.git' && entry.isDirectory()) continue;
+ if (!['stars.json', 'stars.svg', 'public', 'release-manifest.json', 'recommended-release.json'].includes(entry.name) || entry.isSymbolicLink() || (entry.name === 'public' ? !entry.isDirectory() : !entry.isFile())) throw new Error('Unexpected or unsafe site-data entry');
+ }
+ if (fs.existsSync(path.join(root, 'stars.json'))) validateHistory(readJSON(path.join(root, 'stars.json')), repository);
+ const recommendation = path.join(root, 'recommended-release.json');
+ if (!fs.existsSync(recommendation)) {
+ if (fs.existsSync(path.join(root, 'public')) || fs.existsSync(path.join(root, 'release-manifest.json'))) throw new Error('Orphan public site without a release receipt');
+ return null;
+ }
+ const saved = readJSON(recommendation), manifest = readJSON(path.join(root, 'release-manifest.json'));
+ assertManifest(manifest, { publish: true });
+ const product = productFor(repository);
+ if (saved.schema_version !== 1 || saved.repository !== repository || saved.version !== manifest.version || saved.manifest_sha256 !== digest(manifest) || saved.source_commit !== manifest.sources[product].commit || saved.core_commit !== manifest.sources.core.commit || saved.verification !== 'all-three-npm-and-github' || !saved.verified_at || Number.isNaN(Date.parse(saved.verified_at)) || digest(saved.site_files) !== digest(inventory(path.join(root, 'public')))) throw new Error('Stored public site identity differs from verified release');
+ return saved;
+}
+export async function sampleStars(root, repository, fetcher = fetch, now = new Date(), token) {
+ const previous = fs.existsSync(path.join(root, 'stars.json')) ? validateHistory(readJSON(path.join(root, 'stars.json')), repository) : { schema_version: 1, repository, observations: [] };
+ const saved = validateState(root, repository);
+ const response = await fetcher(`https://api.github.com/repos/${repository}`, { headers: { Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', ...(token ? { Authorization: `Bearer ${token}` } : {}) } });
+ if (!response.ok) throw new Error(`Star observation failed: HTTP ${response.status}; state unchanged`);
+ const history = observe(previous, (await response.json()).stargazers_count, now);
+ writeJSON(path.join(root, 'stars.json'), history);
+ fs.writeFileSync(path.join(root, 'stars.svg'), renderSVG(history));
+ if (saved) copyStars(root);
+ return Boolean(saved);
+}
+function copyStars(root) {
+ const history = readJSON(path.join(root, 'stars.json'));
+ validateHistory(history, history.repository);
+ const assets = path.join(root, 'public/assets'); fs.mkdirSync(assets, { recursive: true });
+ writeJSON(path.join(assets, 'stars.json'), history);
+ fs.writeFileSync(path.join(assets, 'stars.svg'), renderSVG(history));
+}
+export function installPublic(root, repository, manifest, built, verifiedAt) {
+ assertManifest(manifest, { publish: true });
+ const previous = validateState(root, repository), product = productFor(repository);
+ const rank = version => version === '1.0.0' ? Infinity : Number(version.split('-rc.')[1]);
+ if (previous && (rank(previous.version) > rank(manifest.version) || (previous.version === manifest.version && previous.manifest_sha256 !== digest(manifest)))) throw new Error('Cannot replace a recommendation with older or different same-version content');
+ const files = inventory(built);
+ if (!verifiedAt || Number.isNaN(Date.parse(verifiedAt))) throw new Error('Missing completed publication verification time');
+ fs.rmSync(path.join(root, 'public'), { recursive: true, force: true });
+ fs.cpSync(built, path.join(root, 'public'), { recursive: true });
+ writeJSON(path.join(root, 'release-manifest.json'), manifest);
+ writeJSON(path.join(root, 'recommended-release.json'), { schema_version: 1, repository, version: manifest.version, manifest_sha256: digest(manifest), source_commit: manifest.sources[product].commit, core_commit: manifest.sources.core.commit, verification: 'all-three-npm-and-github', verified_at: verifiedAt, site_files: files });
+ if (fs.existsSync(path.join(root, 'stars.json'))) copyStars(root);
+ validateState(root, repository);
+}
+export function buildIdentity(repo, core, manifest, product) {
+ const site = readJSON(path.join(repo, 'site/site.json'));
+ const theme = readJSON(path.join(core, 'tools/site/package.json'));
+ if (site.repository !== PRODUCTS[product].repo || site.product !== product || site.version !== manifest.version || site.themeVersion !== theme.version || theme.version !== manifest.version) throw new Error('Site/product/theme version differs from fixed release');
+}
+function cleanEnv(scratch) {
+ const env = Object.fromEntries(['PATH', 'SystemRoot', 'WINDIR', 'LANG', 'LC_ALL'].filter(key => process.env[key]).map(key => [key, process.env[key]]));
+ const home = path.join(scratch, 'home'), temp = path.join(scratch, 'tmp'); fs.mkdirSync(home, { recursive: true }); fs.mkdirSync(temp, { recursive: true });
+ return { ...env, HOME: home, USERPROFILE: home, XDG_CONFIG_HOME: path.join(home, '.config'), TMPDIR: temp, TMP: temp, TEMP: temp, npm_config_cache: path.join(scratch, 'npm-cache'), CI: 'true' };
+}
+const execute = (command, args, options = {}) => execFileSync(command, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 32 * 1024 * 1024, ...options }).trim();
+export function checkoutState(root, repository, env, executor = execute) {
+ fs.mkdirSync(root, { recursive: true });
+ const git = (...args) => executor('git', ['-C', root, ...args], { env });
+ git('init'); git('remote', 'add', 'origin', `https://github.com/${repository}.git`);
+ let exists = true;
+ try { git('ls-remote', '--exit-code', '--heads', 'origin', 'refs/heads/site-data'); }
+ catch (error) { if (error.status !== 2) throw error; exists = false; }
+ if (exists) { git('fetch', '--depth=1', 'origin', 'refs/heads/site-data'); git('checkout', '-B', 'site-data', 'FETCH_HEAD'); }
+ else git('checkout', '--orphan', 'site-data');
+ return git;
+}
+function checkoutSource(scratch, source, env) {
+ const directory = path.join(scratch, source.repository.split('/')[1]);
+ fs.mkdirSync(directory, { recursive: true });
+ const git = (...args) => execute('git', ['-C', directory, ...args], { env });
+ git('init'); git('remote', 'add', 'origin', `https://github.com/${source.repository}.git`); git('fetch', '--depth=1', 'origin', source.commit); git('checkout', '--detach', 'FETCH_HEAD');
+ if (git('rev-parse', 'HEAD') !== source.commit) throw new Error('Source checkout identity mismatch');
+ return directory;
+}
+export async function verifyForSite(manifest, bundle, verifier = verifyPublished) {
+ await verifier(manifest, bundle);
+ return new Date().toISOString();
+}
+async function buildReleased(scratch, repository, version, hash, env) {
+ const bundle = path.join(scratch, 'bundle');
+ const manifest = await fetchBundle(version, hash, bundle);
+ const verifiedAt = await verifyForSite(manifest, bundle), product = productFor(repository);
+ const core = checkoutSource(scratch, manifest.sources.core, env);
+ const repo = product === 'core' ? core : checkoutSource(scratch, manifest.sources[product], env);
+ if (product === 'agent-organon') {
+ for (const key of ['core', 'advised']) {
+ const relative = PRODUCTS[key].directory;
+ const link = execute('git', ['-C', repo, 'ls-tree', 'HEAD', relative], { env });
+ if (!link.startsWith(`160000 commit ${manifest.sources[key].commit}\t`)) throw new Error('Released workspace gitlink mismatch');
+ }
+ fs.rmSync(path.join(repo, 'OrganonCore'), { recursive: true, force: true }); fs.cpSync(core, path.join(repo, 'OrganonCore'), { recursive: true });
+ const advised = checkoutSource(scratch, manifest.sources.advised, env);
+ fs.rmSync(path.join(repo, 'AdvisedOrganons'), { recursive: true, force: true }); fs.cpSync(advised, path.join(repo, 'AdvisedOrganons'), { recursive: true });
+ }
+ if (product === 'advised') {
+ const tooling = readJSON(path.join(repo, 'release/tooling.json'));
+ if (tooling.core?.repository !== manifest.sources.core.repository || tooling.core?.commit !== manifest.sources.core.commit) throw new Error('Released domain theme pin differs from manifest');
+ }
+ buildIdentity(repo, core, manifest, product);
+ execute('npm', ['ci', '--ignore-scripts', '--no-audit', '--no-fund'], { cwd: path.join(core, 'tools/site'), env });
+ for (const operation of ['build', 'check']) execute(process.execPath, [path.join(core, `tools/site/${operation}.mjs`), '--repo', repo], { env });
+ return { manifest, built: path.join(repo, 'dist/site'), verifiedAt };
+}
+export async function runSiteData(args, env = process.env) {
+ const operation = args._[0]; assertSiteRunner(env, operation);
+ const scratch = fs.mkdtempSync(path.join(env.RUNNER_TEMP, 'organon-site-'));
+ const localEnv = cleanEnv(scratch), state = path.join(scratch, 'state');
+ const git = checkoutState(state, env.GITHUB_REPOSITORY, localEnv);
+ let deploy;
+ if (operation === 'stars') deploy = await sampleStars(state, env.GITHUB_REPOSITORY, fetch, new Date(), env.GITHUB_TOKEN);
+ else {
+ const release = await buildReleased(scratch, env.GITHUB_REPOSITORY, requireValue(args, 'version'), requireValue(args, 'manifest-sha256'), localEnv);
+ installPublic(state, env.GITHUB_REPOSITORY, release.manifest, release.built, release.verifiedAt); deploy = true;
+ }
+ // Git configuration is process-local; the token never enters a URL, argv or a persisted config.
+ const pushEnv = { ...localEnv, GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'http.https://github.com/.extraheader', GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${env.GITHUB_TOKEN}`).toString('base64')}` };
+ git('config', 'user.name', 'github-actions[bot]'); git('config', 'user.email', '41898282+github-actions[bot]@users.noreply.github.com');
+ git('add', '--all');
+ if (git('status', '--porcelain')) { git('commit', '-m', operation === 'stars' ? 'Record observed GitHub stars' : 'Publish verified release site'); execute('git', ['-C', state, 'push', 'origin', 'HEAD:refs/heads/site-data'], { env: pushEnv }); }
+ if (env.GITHUB_OUTPUT) fs.appendFileSync(env.GITHUB_OUTPUT, `deploy=${deploy}\npublic_dir=${path.join(state, 'public')}\n`);
+ return { status: 'passed', operation, deploy, repository: env.GITHUB_REPOSITORY };
+}
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(async () => console.log(JSON.stringify(await runSiteData(parseArgs()))));
diff --git a/scripts/release/site.mjs b/scripts/release/site.mjs
new file mode 100644
index 0000000..0032ba5
--- /dev/null
+++ b/scripts/release/site.mjs
@@ -0,0 +1,14 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { spawnSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
+const core = process.env.ORGANON_CORE_ROOT || path.join(root, 'OrganonCore');
+const mode = process.argv[2];
+if (!['build', 'check'].includes(mode)) throw new Error('Expected build or check');
+const script = path.join(core, 'tools/site', `${mode}.mjs`);
+if (!fs.existsSync(script)) throw new Error('Missing fixed Core site tooling; initialize OrganonCore or set ORGANON_CORE_ROOT');
+const result = spawnSync(process.execPath, [script, '--repo', root, ...process.argv.slice(3)], { stdio: 'inherit' });
+if (result.error) throw result.error;
+process.exitCode = result.status ?? 1;
diff --git a/scripts/release/stable.mjs b/scripts/release/stable.mjs
new file mode 100644
index 0000000..d677c3d
--- /dev/null
+++ b/scripts/release/stable.mjs
@@ -0,0 +1,191 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import crypto from 'node:crypto';
+import { fileURLToPath } from 'node:url';
+import { PRODUCTS, digest, parseArgs, requireValue, readJSON, main } from './lib.mjs';
+import { assertManifest } from './manifest.mjs';
+
+const SHA = /^[a-f0-9]{40}$/;
+const HASH = /^[a-f0-9]{64}$/;
+const REPOSITORIES = new Set(Object.values(PRODUCTS).map(product => `shendeguize/${product.repo}`));
+const INTERNAL_PACKAGES = new Set([...Object.values(PRODUCTS).map(product => product.package), '@shendeguize/organon-site-tools']);
+const gitBlobHash = bytes => crypto.createHash('sha1').update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
+const utf8 = bytes => {
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
+ if (text.includes('\0')) throw new Error('Binary content cannot receive a release text transformation');
+ return text;
+};
+function insist(condition, message) { if (!condition) throw new Error(message); }
+function checkedRepository(repository) { insist(REPOSITORIES.has(repository), 'Unexpected source repository'); return repository; }
+function checkedSHA(sha) { insist(SHA.test(sha), 'Expected an immutable Git object SHA'); return sha; }
+async function responseBytes(response, limit) {
+ insist(response.ok, `Immutable release source unavailable: HTTP ${response.status}`);
+ const parts = []; let size = 0;
+ for await (const part of response.body) {
+ size += part.length; insist(size <= limit, 'Immutable release source exceeds read limit'); parts.push(part);
+ }
+ return Buffer.concat(parts);
+}
+
+/** Read-only transport. The test backend supplies the same object-shaped methods. */
+export function githubBackend({ token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN } = {}) {
+ const request = async (repository, suffix) => {
+ const headers = { Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' };
+ if (token) headers.Authorization = `Bearer ${token}`;
+ const response = await fetch(`https://api.github.com/repos/${checkedRepository(repository)}/${suffix}`, { headers, signal: AbortSignal.timeout(30000) });
+ return JSON.parse((await responseBytes(response, 32 * 1024 * 1024)).toString('utf8'));
+ };
+ return {
+ async rcManifest(version) {
+ insist(/^1\.0\.0-rc\.[1-9]\d*$/.test(version), 'Expected an exact RC version');
+ const response = await fetch(`https://github.com/shendeguize/AgentOrganon/releases/download/v${version}/release-manifest.json`, { signal: AbortSignal.timeout(30000) });
+ return JSON.parse((await responseBytes(response, 4 * 1024 * 1024)).toString('utf8'));
+ },
+ commit: (repository, sha) => request(repository, `git/commits/${checkedSHA(sha)}`),
+ tree: (repository, sha) => request(repository, `git/trees/${checkedSHA(sha)}?recursive=1`),
+ async blob(repository, sha) {
+ const response = await request(repository, `git/blobs/${checkedSHA(sha)}`);
+ insist(response.sha === sha && response.encoding === 'base64', 'GitHub returned an unrelated blob');
+ const bytes = Buffer.from(response.content, 'base64');
+ insist(response.size === bytes.length, 'GitHub blob size mismatch');
+ return bytes;
+ },
+ };
+}
+
+function treeEntries(tree, sha) {
+ insist(tree.sha === sha && tree.truncated === false && Array.isArray(tree.tree), 'Incomplete or unrelated Git tree');
+ const result = new Map();
+ for (const entry of tree.tree) {
+ insist(typeof entry.path === 'string' && entry.path && !entry.path.includes('\\') && !entry.path.split('/').some(part => !part || part === '.' || part === '..') && SHA.test(entry.sha), 'Invalid Git tree entry');
+ insist(!result.has(entry.path), 'Duplicate Git tree entry');
+ const types = { '040000': 'tree', '100644': 'blob', '100755': 'blob', '120000': 'blob', '160000': 'commit' };
+ insist(types[entry.mode] === entry.type, 'Unsupported Git tree mode/type');
+ result.set(entry.path, entry);
+ }
+ return result;
+}
+const protectedPath = file => /(?:^|\/)(?:PHILOSOPHY(?:\.lock)?\.json|PHILOSOPHY\.md|lean(?:\/|\.|$)|rationale(?:\/|\.|$)|philosophy(?:\/|\.|$))/i.test(file);
+function replaceVersion(text, version) {
+ const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ return text.replace(new RegExp(`(? {
+ if (!blobs.has(entry.sha)) {
+ const bytes = Buffer.from(await backend.blob(repository, entry.sha));
+ insist(gitBlobHash(bytes) === entry.sha, 'Git blob does not match its immutable identity');
+ blobs.set(entry.sha, bytes);
+ }
+ return blobs.get(entry.sha);
+ };
+ const oldPackage = oldEntries.get('package.json'); const newPackage = newEntries.get('package.json');
+ insist(oldPackage?.type === 'blob' && newPackage?.type === 'blob', 'Product package metadata is missing');
+ const oldMetadata = JSON.parse(utf8(await read(oldPackage))); const newMetadata = JSON.parse(utf8(await read(newPackage)));
+ insist(oldMetadata.name === spec.package && newMetadata.name === spec.package && oldMetadata.version === rc.version && newMetadata.version === '1.0.0', `Product metadata does not implement the approved version transition: ${product}`);
+ for (const [file, oldEntry] of oldEntries) {
+ const newEntry = newEntries.get(file);
+ insist(oldEntry.type === newEntry.type && oldEntry.mode === newEntry.mode, `Stable transition changes a file type/mode: ${product}/${file}`);
+ if (oldEntry.type === 'tree') continue;
+ if (product === 'agent-organon' && ['OrganonCore', 'AdvisedOrganons'].includes(file)) {
+ const peer = file === 'OrganonCore' ? 'core' : 'advised';
+ insist(oldEntry.type === 'commit' && oldEntry.sha === rc.sources[peer].commit && newEntry.sha === manifest.sources[peer].commit, `Peer gitlink does not match the frozen source identity: ${file}`);
+ if (oldEntry.sha !== newEntry.sha) changes.push({ product, path: file, before: oldEntry.sha, after: newEntry.sha, reason: 'peer-gitlink' });
+ continue;
+ }
+ if (oldEntry.sha === newEntry.sha) continue;
+ insist(oldEntry.type === 'blob' && oldEntry.mode !== '120000', `Unapproved symbolic-link or gitlink change: ${product}/${file}`);
+ const reason = permittedBlob(file, utf8(await read(oldEntry)), utf8(await read(newEntry)), rc, manifest, product);
+ changes.push({ product, path: file, before: oldEntry.sha, after: newEntry.sha, reason });
+ }
+ if (product === 'agent-organon') for (const peer of ['OrganonCore', 'AdvisedOrganons']) insist(oldEntries.has(peer), `Required peer gitlink is missing: ${peer}`);
+ repositories[product] = { repository, rc_commit: oldCommit, stable_commit: newCommit, rc_tree: oldObject.tree.sha, stable_tree: newObject.tree.sha };
+ }
+ return { schema_version: 1, gate: 'stable-transition', status: 'passed', approved_rc_manifest_sha256: approvalDigest, stable_manifest_sha256: digest(manifest), source_digest: manifest.source_digest, repositories, changes, approval_binding: 'separate-configured-RC-digest', scope: 'Immutable source differences only; rebuilt packages, fresh validation gates and user approval remain separate requirements.' };
+}
+
+if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) main(async () => {
+ const args = parseArgs(); const file = path.resolve(requireValue(args, 'manifest'));
+ console.log(JSON.stringify(await validateStable(readJSON(file), path.dirname(file)), null, 2));
+});
diff --git a/scripts/release/stars.mjs b/scripts/release/stars.mjs
new file mode 100644
index 0000000..64ea7d6
--- /dev/null
+++ b/scripts/release/stars.mjs
@@ -0,0 +1,61 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { parseArgs, readJSON, writeJSON, main, PRODUCTS } from './lib.mjs';
+
+export function validateHistory(history, repository) {
+ if (history.schema_version !== 1 || history.repository !== repository || !Array.isArray(history.observations)) throw new Error('Invalid star history identity');
+ let previous = '';
+ for (const point of history.observations) {
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(point.observed_at) || Number.isNaN(Date.parse(point.observed_at)) || point.observed_at <= previous || !Number.isSafeInteger(point.total) || point.total < 0) throw new Error('Invalid or unordered star observation');
+ previous = point.observed_at;
+ }
+ return history;
+}
+export function observe(history, total, now = new Date()) {
+ validateHistory(history, history.repository);
+ if (!Number.isSafeInteger(total) || total < 0) throw new Error('Invalid GitHub star count');
+ const timestamp = now.toISOString();
+ const last = history.observations.at(-1);
+ if (last && timestamp <= last.observed_at) throw new Error('Observation must advance time');
+ // At most one actual observation per UTC date; retries cannot rewrite history.
+ if (last?.observed_at.slice(0, 10) === timestamp.slice(0, 10)) return history;
+ return { ...history, observations: [...history.observations, { observed_at: timestamp, total }] };
+}
+const escape = text => String(text).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
+export function renderSVG(history) {
+ validateHistory(history, history.repository);
+ const points = history.observations;
+ const last = points.at(-1);
+ const firstTime = points.length ? Date.parse(points[0].observed_at) : 0;
+ const span = last ? Math.max(86400000, Date.parse(last.observed_at) - firstTime) : 86400000;
+ const ceiling = Math.max(1, ...points.map(p => p.total));
+ const xy = p => [56 + (Date.parse(p.observed_at) - firstTime) / span * 700, 176 - p.total / ceiling * 105];
+ const segments = [];
+ for (let i = 1; i < points.length; i++) {
+ if (Date.parse(points[i].observed_at.slice(0, 10)) - Date.parse(points[i - 1].observed_at.slice(0, 10)) === 86400000) {
+ const [a, b] = [xy(points[i - 1]), xy(points[i])];
+ segments.push(``);
+ }
+ }
+ return `\n`;
+}
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(async () => {
+ const args = parseArgs();
+ const repository = args.repository;
+ if (!Object.values(PRODUCTS).some(p => `shendeguize/${p.repo}` === repository)) throw new Error('Expected one of the three Organon repositories');
+ const file = path.resolve(args.history || 'site/public/assets/stars.json');
+ let history = fs.existsSync(file) ? validateHistory(readJSON(file), repository) : { schema_version: 1, repository, observations: [] };
+ if (!args.render) {
+ const headers = { Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' };
+ if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
+ const response = await fetch(`https://api.github.com/repos/${repository}`, { headers });
+ if (!response.ok) throw new Error(`GitHub star observation failed: HTTP ${response.status}; previous history preserved`);
+ history = observe(history, (await response.json()).stargazers_count);
+ writeJSON(file, history);
+ }
+ const svg = path.resolve(args.svg || path.join(path.dirname(file), 'stars.svg'));
+ fs.mkdirSync(path.dirname(svg), { recursive: true });
+ fs.writeFileSync(svg, renderSVG(history));
+ console.log(JSON.stringify({ status: 'passed', observations: history.observations.length, latest: history.observations.at(-1) || null }));
+});
diff --git a/scripts/release/tooling.mjs b/scripts/release/tooling.mjs
new file mode 100644
index 0000000..58a16e2
--- /dev/null
+++ b/scripts/release/tooling.mjs
@@ -0,0 +1,18 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { readJSON } from './lib.mjs';
+
+export function readTooling(root) {
+ const value = readJSON(path.join(root, 'release/tooling.json'));
+ if (value.schema_version !== 1) throw new Error('Unsupported tooling manifest');
+ for (const [key, repository] of [['workspace', 'shendeguize/AgentOrganon'], ['core', 'shendeguize/OrganonCore']]) {
+ if (!value[key] && key === 'core') continue;
+ if (value[key]?.repository !== repository || !/^[a-f0-9]{40}$/.test(value[key]?.commit)) throw new Error(`Invalid fixed tooling source: ${key}`);
+ }
+ return value;
+}
+if (process.argv[1] && path.resolve(process.argv[1]) === new URL(import.meta.url).pathname) {
+ const value = readTooling(process.argv[2] || '.');
+ if (!process.env.GITHUB_OUTPUT) throw new Error('Tooling outputs require GitHub Actions');
+ for (const key of ['workspace', 'core']) if (value[key]) fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value[key].commit}\n`);
+}
diff --git a/scripts/resolve.js b/scripts/resolve.js
new file mode 100644
index 0000000..db0bf58
--- /dev/null
+++ b/scripts/resolve.js
@@ -0,0 +1,7 @@
+import { argumentsFor, run } from './lib/cli.js';
+import { resolvePhilosophy } from './lib/operations.js';
+run(() => {
+ const args = argumentsFor(['cwd', 'philosophy']);
+ if (args.positional.length) throw new Error('Unexpected positional arguments');
+ return resolvePhilosophy(args);
+});
diff --git a/scripts/test.mjs b/scripts/test.mjs
new file mode 100644
index 0000000..043dd46
--- /dev/null
+++ b/scripts/test.mjs
@@ -0,0 +1,12 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { spawnSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const directory = path.join(root, 'tests');
+const files = fs.existsSync(directory) ? fs.readdirSync(directory).filter(name => /\.test\.(?:mjs|js)$/.test(name)).sort().map(name => path.join(directory, name)) : [];
+if (!files.length) throw new Error('No owned tests found');
+const result = spawnSync(process.execPath, ['--test', '--test-concurrency=2', ...process.argv.slice(2), ...files], { stdio: 'inherit', cwd: root });
+if (result.error) throw result.error;
+process.exitCode = result.status ?? 1;
diff --git a/site/index.md b/site/index.md
new file mode 100644
index 0000000..6888f46
--- /dev/null
+++ b/site/index.md
@@ -0,0 +1,35 @@
+---
+layout: home
+hero:
+ name: "AgentOrganon"
+ text: "Ground agent judgments and improvements."
+ tagline: "Workspace methods and philosophy management · Make commitments explicit. Examine grounds. Revise for reasons."
+ actions:
+ - theme: brand
+ text: "Quick start"
+ link: /quick-start
+ - theme: alt
+ text: "Read the philosophy"
+ link: /philosophy
+features:
+ - title: "Make the baseline explicit"
+ details: "Distinguish adopted commitments, a proposed change and the method’s own constraints."
+ - title: "Examine reasons and limits"
+ details: "Connect a claim to its assumptions, grounds and scope of application."
+ - title: "Revise for stated reasons"
+ details: "Examine reasons for change; retain an explicit human decision on philosophical adoption."
+---
+
+## Why philosophy as a design foundation
+
+Agents make judgments and propose changes. A philosophy makes their underlying commitments readable, open to criticism and revisable. Organon supplies philosophical text and review methods that ask for reasons and limits. It does not guarantee correct judgments or treat a count of methods as evidence of capability.
+
+## Three repositories, distinct responsibilities
+
+| Repository | Responsibility |
+| --- | --- |
+| [AgentOrganon](https://shendeguize.github.io/AgentOrganon/) | Workspace skills and management of adopted philosophy copies. |
+| [OrganonCore](https://shendeguize.github.io/OrganonCore/) | The core philosophy, review methods and bounded Lean evidence. |
+| [AdvisedOrganons](https://shendeguize.github.io/AdvisedOrganons/) | A collection of domain philosophies; select a domain explicitly. |
+
+Installation packages supply non-Lean capabilities; the website retains the philosophy and Lean reader views.
diff --git a/site/lean.md b/site/lean.md
new file mode 100644
index 0000000..193323f
--- /dev/null
+++ b/site/lean.md
@@ -0,0 +1,10 @@
+# Lean reader directory
+
+Begin with claims, assumptions and limits, then inspect proof declarations and paired code with line explanations. These pages belong to each philosophy’s source repository; AgentOrganon does not maintain a separate formalization.
+
+| Source | Content |
+| --- | --- |
+| [OrganonCore](https://shendeguize.github.io/OrganonCore/lean) | Core targets, statuses, source tracing and paired line explanations. |
+| [SoftwareEngineering](https://shendeguize.github.io/AdvisedOrganons/lean) | Domain philosophy with its retained Core baseline; no automatic adoption of a newer Core. |
+
+Lean `passed`, target `accepted` and source fidelity are reported separately. Bounded results do not establish universal real-world correctness; defining a duty does not prove its fulfillment. Installation packages exclude Lean capabilities; full projects, evidence and checking instructions remain in the source repositories.
diff --git a/site/philosophy.md b/site/philosophy.md
new file mode 100644
index 0000000..991972c
--- /dev/null
+++ b/site/philosophy.md
@@ -0,0 +1,9 @@
+# Sources of the philosophy
+
+AgentOrganon uses the Core philosophy for its own maintenance. Workspace assessment uses your explicitly selected adopted philosophy; the bundled Core cannot silently replace it.
+
+- [Read the Core philosophy](https://shendeguize.github.io/OrganonCore/philosophy)
+- [Select a domain philosophy](https://shendeguize.github.io/AdvisedOrganons/philosophy)
+- [Understand baselines, proposals and method constraints](understand.md)
+
+The root `PHILOSOPHY.md` is a read-only source link; workspace adoption uses a separate regular file. Installation updates do not automatically change an adopted philosophy.
diff --git a/site/public/assets/stars.json b/site/public/assets/stars.json
new file mode 100644
index 0000000..928d329
--- /dev/null
+++ b/site/public/assets/stars.json
@@ -0,0 +1,10 @@
+{
+ "schema_version": 1,
+ "repository": "shendeguize/AgentOrganon",
+ "observations": [
+ {
+ "observed_at": "2026-09-15T04:41:11.016Z",
+ "total": 0
+ }
+ ]
+}
diff --git a/site/public/assets/stars.svg b/site/public/assets/stars.svg
new file mode 100644
index 0000000..98c16fe
--- /dev/null
+++ b/site/public/assets/stars.svg
@@ -0,0 +1 @@
+
diff --git a/site/rationale.md b/site/rationale.md
new file mode 100644
index 0000000..87591a2
--- /dev/null
+++ b/site/rationale.md
@@ -0,0 +1,6 @@
+# Sources of the rationale
+
+Rationale supplies reasons and objections without adding philosophical obligations. Read it in the repository that owns the text.
+
+- [Core rationale](https://shendeguize.github.io/OrganonCore/rationale)
+- [SoftwareEngineering rationale](https://shendeguize.github.io/AdvisedOrganons/rationale)
diff --git a/site/site.json b/site/site.json
new file mode 100644
index 0000000..c719cde
--- /dev/null
+++ b/site/site.json
@@ -0,0 +1,7 @@
+{
+ "repository": "AgentOrganon",
+ "product": "agent-organon",
+ "version": "1.0.0-rc.1",
+ "themeVersion": "1.0.0-rc.1",
+ "philosophyRoot": null
+}
diff --git a/site/zh/index.md b/site/zh/index.md
new file mode 100644
index 0000000..53c7bd5
--- /dev/null
+++ b/site/zh/index.md
@@ -0,0 +1,35 @@
+---
+layout: home
+hero:
+ name: "AgentOrganon"
+ text: "让 agent 的判断与改进有依据。"
+ tagline: "工作区方法与哲学管理 · 明确承诺,审查根据,有理由地修订。"
+ actions:
+ - theme: brand
+ text: "快速开始"
+ link: /zh/quick-start
+ - theme: alt
+ text: "阅读哲学"
+ link: /zh/philosophy
+features:
+ - title: "明确判断基准"
+ details: "区分已采纳的承诺、待审查的提议与方法自身的约束。"
+ - title: "审查理由与限度"
+ details: "将主张与前提、根据及适用范围联系起来。"
+ - title: "有理由地修订"
+ details: "审查改变的理由;哲学采纳保留明确的人类决定。"
+---
+
+## 为什么采用哲学设计
+
+Agent 会进行判断,也会提出改变。哲学让这些判断所依据的承诺可读、可质疑、可修订。Organon 提供哲学文本与审查方法,要求说明理由和边界;它不保证每次判断正确,也不把方法数量当作能力证明。
+
+## 三个仓库,各有职责
+
+| 仓库 | 职责 |
+| --- | --- |
+| [AgentOrganon](https://shendeguize.github.io/AgentOrganon/zh/) | 工作区技能与已采纳哲学副本的管理。 |
+| [OrganonCore](https://shendeguize.github.io/OrganonCore/zh/) | 核心哲学、审查方法,以及有边界的 Lean 证据。 |
+| [AdvisedOrganons](https://shendeguize.github.io/AdvisedOrganons/zh/) | 领域哲学集合;使用时显式选择领域。 |
+
+安装包只提供非 Lean 能力;网站保留完整哲学及 Lean 读者视图。
diff --git a/site/zh/lean.md b/site/zh/lean.md
new file mode 100644
index 0000000..016e3c0
--- /dev/null
+++ b/site/zh/lean.md
@@ -0,0 +1,10 @@
+# Lean 读者入口
+
+从主张、前提和限制读起,再查看证明声明及逐行代码解释。以下页面来自各哲学所属仓库,AgentOrganon 不维护另一份形式化结果。
+
+| 本源 | 内容 |
+| --- | --- |
+| [OrganonCore](https://shendeguize.github.io/OrganonCore/zh/lean) | 核心哲学的目标、状态、来源追踪与逐行对照。 |
+| [SoftwareEngineering](https://shendeguize.github.io/AdvisedOrganons/zh/lean) | 领域哲学及其保留的 Core 基准,不自动采纳新 Core。 |
+
+Lean `passed`、目标 `accepted` 和来源保真状态分别报告。有限结果不证明现实中的普遍正确性;定义义务不证明义务已履行。安装包不包含 Lean 能力,完整工程、证据和检查方式保留在源仓库。
diff --git a/site/zh/philosophy.md b/site/zh/philosophy.md
new file mode 100644
index 0000000..62fb656
--- /dev/null
+++ b/site/zh/philosophy.md
@@ -0,0 +1,9 @@
+# 哲学的来源
+
+AgentOrganon 使用 Core 哲学作为自身维护基准。工作区评估使用你显式选择的已采纳哲学,不能自动用捆绑的 Core 替代。
+
+- [阅读 Core 哲学](https://shendeguize.github.io/OrganonCore/zh/philosophy)
+- [选择领域哲学](https://shendeguize.github.io/AdvisedOrganons/zh/philosophy)
+- [理解基准、提议和方法约束](understand.md)
+
+根目录 `PHILOSOPHY.md` 是只读源链接;工作区采纳使用独立的普通文件。安装更新不自动改变已采纳哲学。
diff --git a/site/zh/rationale.md b/site/zh/rationale.md
new file mode 100644
index 0000000..a11fff8
--- /dev/null
+++ b/site/zh/rationale.md
@@ -0,0 +1,6 @@
+# 论证的来源
+
+Rationale 提供理由和反对意见,不向哲学核心增加义务。请在文本所属仓库中阅读。
+
+- [Core 论证](https://shendeguize.github.io/OrganonCore/zh/rationale)
+- [SoftwareEngineering 论证](https://shendeguize.github.io/AdvisedOrganons/zh/rationale)
diff --git a/skills/organon-absorb/SKILL.md b/skills/organon-absorb/SKILL.md
new file mode 100644
index 0000000..d106a5f
--- /dev/null
+++ b/skills/organon-absorb/SKILL.md
@@ -0,0 +1,11 @@
+---
+name: organon-absorb
+description: Examine reasons to revise the calling workspace's adopted philosophy and delegate reviewed, authorized adoption to Organon Core. General documentation maintenance is separate.
+---
+
+# Organon Absorb
+
+1. Resolve the philosophy using [the resolution contract](../../OrganonCore/skills/references/philosophy-resolution.md). Run `node /scripts/resolve.js --cwd ` with `--philosophy ` when explicitly supplied. Locate `` from this skill's directory; preserve the caller's directory for resolution.
+2. If resolution fails, stop. For missing default candidates, suggest `organon-philosophy init`; an invalid explicit path must be corrected, not replaced by a default or Core.
+3. Read the selected `core_version` and compare it with `organon.coreVersion` in [package.json](../../package.json). Surface the resolver's source-version warning when outside the range. If metadata cannot be read, report the version as unknown and retain the selected text as the assessment baseline; managed writes still require valid supported metadata. These warnings are not philosophical validity judgments.
+4. Delegate the request to [organon-core-absorb](../../OrganonCore/skills/organon-core-absorb/SKILL.md), passing the resolved philosophy path as the fixed adopted baseline, with its real source directory. Identify any intended adoption target from the request and caller context; pass any established target and the allowed actions without substituting the baseline for a different target. Resolve relative target paths from the caller's directory. A request to revise the caller's own adopted text may use the same path for both roles. For an advisory request without an established target, keep the target unspecified and continue the authorized review; resolution alone does not select a write destination. Keep a proposed replacement and Core method constraints distinct. Apply Core's required independent review and concrete user decision before adoption. Reading a Core symlink as the baseline does not select Core as the adoption target; an actual request to revise protected Core text requires its explicit maintenance or absorption workflow, not a management write or symlink bypass.
diff --git a/skills/organon-assess/SKILL.md b/skills/organon-assess/SKILL.md
new file mode 100644
index 0000000..07485d9
--- /dev/null
+++ b/skills/organon-assess/SKILL.md
@@ -0,0 +1,11 @@
+---
+name: organon-assess
+description: Assess a claim, design, method, or artifact against the calling workspace's adopted philosophy by delegating to Organon Core. Adoption is a separate decision.
+---
+
+# Organon Assess
+
+1. Resolve the philosophy using [the resolution contract](../../OrganonCore/skills/references/philosophy-resolution.md). Run `node /scripts/resolve.js --cwd ` with `--philosophy ` when explicitly supplied. Locate `` from this skill's directory; preserve the caller's directory for resolution.
+2. If resolution fails, stop. For missing default candidates, suggest `organon-philosophy init`; an invalid explicit path must be corrected, not replaced by a default or Core.
+3. Read the selected `core_version` and compare it with `organon.coreVersion` in [package.json](../../package.json). Surface the resolver's source-version warning when outside the range. If metadata cannot be read, report the version as unknown and retain the selected text as the assessment baseline; managed writes still require valid supported metadata. These warnings are not philosophical validity judgments.
+4. Delegate the request to [organon-core-assess](../../OrganonCore/skills/organon-core-assess/SKILL.md), passing the resolved philosophy path and its real source directory explicitly. Preserve that adopted baseline throughout downstream calls; identify a candidate replacement and Core method constraints separately.
diff --git a/skills/organon-lean-natural-language/SKILL.md b/skills/organon-lean-natural-language/SKILL.md
new file mode 100644
index 0000000..4a41645
--- /dev/null
+++ b/skills/organon-lean-natural-language/SKILL.md
@@ -0,0 +1,10 @@
+---
+name: organon-lean-natural-language
+description: Forward Lean explanation, blind backtranslation and bilingual reader requests to the maintained Core skill.
+---
+
+# organon-lean-natural-language
+
+Use [organon-core-lean-natural-language](../../OrganonCore/skills/organon-core-lean-natural-language/SKILL.md) as the sole maintained method. Preserve the supplied source, output directory, language and explanation options, and existing authorization. This entrypoint does not maintain a separate formalization or translation method.
+
+Code-only explanation does not require a philosophy. Retain `--explain-lines` and the selected output language; default to Chinese as specified by Core. If the invocation is an independent blind review, pass only the permitted code and technical context and retain the initial output before disclosing source prose.
diff --git a/skills/organon-leanify-prove/SKILL.md b/skills/organon-leanify-prove/SKILL.md
new file mode 100644
index 0000000..8162f3e
--- /dev/null
+++ b/skills/organon-leanify-prove/SKILL.md
@@ -0,0 +1,12 @@
+---
+name: organon-leanify-prove
+description: Forward philosophical Lean formalization to Core while retaining the caller-selected baseline.
+---
+
+# organon-leanify-prove
+
+Use [organon-core-leanify-prove](../../OrganonCore/skills/organon-core-leanify-prove/SKILL.md) as the sole maintained method. Preserve the supplied source, output directory, language and explanation options, and existing authorization. This entrypoint does not maintain a separate formalization or translation method.
+
+When a philosophical baseline is needed, resolve the caller workspace with `node /scripts/resolve.js --cwd [--philosophy ]`, then pass the selected path and its real source directory to Core. Explicit or workspace lookup failure stops the dependent operation; do not invoke the Core default as fallback. Plain input text does not automatically become the adopted baseline.
+
+The existing `node /skills/organon-leanify-prove/scripts/check.js [--manuscript ]` command forwards to Core and preserves its CLI and programmatic interface. Run paths belong to the caller; current bundled runs are listed in [Core Lean delivery](../../OrganonCore/lean/README.md).
diff --git a/skills/organon-leanify-prove/references/manuscript-format.md b/skills/organon-leanify-prove/references/manuscript-format.md
new file mode 100644
index 0000000..4f54bbc
--- /dev/null
+++ b/skills/organon-leanify-prove/references/manuscript-format.md
@@ -0,0 +1,3 @@
+# Core-owned reference
+
+The maintained contract is [manuscript-format](../../../OrganonCore/skills/organon-core-leanify-prove/references/manuscript-format.md). This old path is retained for callers; use the Core contract rather than a duplicate specification.
diff --git a/skills/organon-leanify-prove/references/run-format.md b/skills/organon-leanify-prove/references/run-format.md
new file mode 100644
index 0000000..9726f66
--- /dev/null
+++ b/skills/organon-leanify-prove/references/run-format.md
@@ -0,0 +1,3 @@
+# Core-owned reference
+
+The maintained contract is [run-format](../../../OrganonCore/skills/organon-core-leanify-prove/references/run-format.md). This old path is retained for callers; use the Core contract rather than a duplicate specification.
diff --git a/skills/organon-leanify-prove/scripts/check.js b/skills/organon-leanify-prove/scripts/check.js
new file mode 100644
index 0000000..516c585
--- /dev/null
+++ b/skills/organon-leanify-prove/scripts/check.js
@@ -0,0 +1,10 @@
+#!/usr/bin/env node
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { runCli } from '../../../OrganonCore/skills/organon-core-leanify-prove/scripts/check.js';
+
+export * from '../../../OrganonCore/skills/organon-core-leanify-prove/scripts/check.js';
+
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
+ runCli('Usage: node skills/organon-leanify-prove/scripts/check.js [--manuscript ]');
+}
diff --git a/skills/organon-leanify-prove/scripts/manuscript.js b/skills/organon-leanify-prove/scripts/manuscript.js
new file mode 100644
index 0000000..d31ffde
--- /dev/null
+++ b/skills/organon-leanify-prove/scripts/manuscript.js
@@ -0,0 +1 @@
+export * from '../../../OrganonCore/skills/organon-core-leanify-prove/scripts/manuscript.js';
diff --git a/skills/organon-philosophy/SKILL.md b/skills/organon-philosophy/SKILL.md
new file mode 100644
index 0000000..f2280f8
--- /dev/null
+++ b/skills/organon-philosophy/SKILL.md
@@ -0,0 +1,92 @@
+---
+name: organon-philosophy
+description: Manage a workspace philosophy with init, check, export, import, and merge. Scripts handle deterministic files and differences; agents and users assess meaning and decide adoption.
+---
+
+# Organon Philosophy
+
+Read the [format contract](../../OrganonCore/skills/references/structure.md) and the [shared iteration rules](../../OrganonCore/skills/references/iteration.md). Resolve existing philosophies under the [resolution contract](../../OrganonCore/skills/references/philosophy-resolution.md). Locate scripts relative to this skill's AgentOrganon checkout and execute them with absolute paths when outside it. The commands below assume that checkout is the current directory; `<...>` denotes a supplied path or value, not literal syntax. These are skill operations, not an installed CLI.
+
+Keep the adopted philosophy, the proposed whole, and Core method constraints distinct. Adopters may reorganize, revise, or withdraw commitments. Mechanical validity, matching hashes, version compatibility, and agent agreement do not establish philosophical correctness or a reason to adopt. Scripts neither choose philosophical outcomes nor authorize them.
+
+## Init and check
+
+For `init`, take the target path and optionally a source file and source identity. The default source is bundled Core, identified as `https://github.com/shendeguize/OrganonCore`. For a custom source, explicitly pass its identity with `--source-id`; the script does not authenticate the publisher. A request to initialize the specified absent target authorizes creation of its philosophy and adjacent lock; reuse that authorization. Initialization copies the source and adds an empty section with stable ID `extensions` if it is missing. Existing targets or locks are not overwritten. The target's parent directory must already exist. In a Git workspace, ensure `.local/` is ignored before initialization; the script creates its private recovery-journal directory as needed.
+
+```sh
+node scripts/init.js --target [--source ] [--source-id ]
+node scripts/check.js
+node scripts/check.js --source
+```
+
+`check` is read-only. Default mode checks a managed workspace and its lock, including whether `document_filename` matches the target; `--source` checks a document without requiring a lock. Report format, identity, and state findings with their limits. An out-of-range `core_version` is a source-version warning. Unsupported `format_version` stops managed writes. Root `PHILOSOPHY.md` in AgentOrganon is a read-only Core symlink with no derived lock; use source checking there and regular copies for the management workflow. Managed operations, including reads for init, export, and classification, stop when an input has a pending transaction. Follow the reported recovery command before continuing.
+
+## Export
+
+Take the source, absent output path, and optional `--core-only` and source identity. The user's export request authorizes the stated local output, not remote publication. Check the source before writing.
+
+```sh
+node scripts/export.js --source --out [--core-only] [--source-id ]
+```
+
+Export includes all content by default. `--core-only` removes the subtree identified by stable ID `extensions`, even when renamed or moved; if missing, stop and ask the user to define the intended scope. The result remains the adopter's text, not official Core text. `derived_from` identifies the full input's source, philosophy version, and SHA-256 before filtering; it is provenance, not proof of ancestry. The managed document's lock identity supplies the default source ID; provide one explicitly when needed. Export writes a new file and does not modify the source or lock.
+
+## Import and merge: compare first
+
+Take the local philosophy, incoming source, stable source identity, and operation kind. Use `core` only for the source designated by the lock's `core_source_id`; unrelated imports use `import`. Establish the intended source with the caller and use its existing checkpoint identity consistently. The script checks identity/kind correspondence but does not authenticate the incoming file's publisher. Matching versions or an unverified `derived_from` claim do not establish common history.
+
+```sh
+node scripts/classify.js --ours --theirs --source-id --kind core|import [--base-file ]
+```
+
+For `merge --dry-run`, return this read-only JSON difference report and its limitations without adopting changes. Import uses the same comparison and proposal workflow for every adopted change, including when the resulting candidate replaces the whole document. Without a verifiable checkpoint for this source, report two-way differences and the missing baseline. `--base-file` may supply old text for a known checkpoint only when its hashes verify; never invent historical text from hashes or provenance. Preserve genuine structural changes without inventing an earlier movement when evidence is missing.
+
+For a verified checkpoint, `B` is the reviewed source, `O` the current local unit, and `T` the incoming unit. Known absence differs from an unknown baseline. Classification is ordered: `O = T = B` is `unchanged`; `O = T != B` is `converged`; `O = B != T` is `theirs-changed`; `T = B != O` is `ours-changed`; all remaining cases are `conflict`. Addition, deletion, and modification are separate attributes. Structural differences are reported separately from body hashes.
+
+A remembered decline binds the source, stable unit ID, and incoming hash or deletion marker. Unchanged incoming state stays suppressed even if local content changes; changed incoming state is proposed again. Suppression does not conceal the actual difference. The lock keeps source hashes and structure rather than old source text or the locally merged result.
+
+## Form a candidate and decide
+
+Save the report and working artifacts in the target workspace's ignored `.local/iterations/merges/`. Check that this directory is ignored before writing. Use a record named `YYYY-MM-DD-.md`, with a disambiguating suffix when a record already exists. Do not overwrite earlier evidence.
+
+For each proposed change, present retaining ours, taking theirs, manual editing, and an agent draft as meaningful candidate options. Choosing `ours` for an incoming difference retains the local unit and remembers rejection of that incoming state; `decline` explicitly requests the same behavior. Explain that consequence when obtaining the choice. These choices form candidate text; they do not by themselves justify its adoption. Review the entire proposed philosophy, including interactions between individually conflict-free units and structural reorganization. Do not let scripts guess where reorganized material belongs.
+
+Delegate substantive adoption to [organon-core-absorb](../../OrganonCore/skills/organon-core-absorb/SKILL.md), passing the local philosophy as the fixed baseline and exact adoption target, and the complete candidate separately. Use assess to identify current compatibility and support limits; examine reasons to change or withdraw commitments independently of that result. Follow absorb's independent-first review, applicable principled review, wording, and translation checks. Without required independent review, retain a proposal. Reuse valid prior review and explicit authorization for the same concrete change; do not request it again. A new or materially changed philosophical decision remains with the user.
+
+Propose `philosophy_version` from semantic impact and record the user's decision: major changes or withdraws commitments, meanings, or applicability; minor adds content while preserving existing commitments; patch preserves meaning in wording or structural maintenance. Retain the version for a no-change outcome where appropriate. Scripts validate the supplied version, not its semantic justification.
+
+Prepare a complete final candidate and a decisions JSON object:
+
+```json
+{
+ "sections": { "local.unit": "theirs", "local.another": "decline" },
+ "structure": "manual",
+ "philosophy_version": "0.1.1"
+}
+```
+
+Section choices are `theirs`, `ours`, `decline`, `manual`, or `agent`. Every unsuppressed incoming change that differs from the local unit requires an explicit choice; local-only changes and converged units default to `ours`, and unchanged remembered declines default to `decline`. Candidate-only IDs require `manual` or `agent`. `theirs`, `ours`, and `decline` must match the respective source or local body, including absence; `manual` and `agent` designate candidate text. Always specify structure as `ours`, `theirs`, `manual`, or `agent`; the first two must match that document's structure.
+
+Candidate metadata retains local `format_version` and `derived_from`. Its `philosophy_version` matches the confirmed decision. For a completed Core review, candidate `core_version` matches the incoming Core version; an external import retains the local `core_version`. Finish decisions for all proposals, including explicit refusals, before advancing the reviewed source checkpoint. Completion records what was reviewed, not adoption of every upstream change.
+
+## Apply and record
+
+Keep a six-part record under the shared iteration rules:
+
+1. **Object and baseline:** local and source identities, versions, input snapshots or hashes, and known or missing common baseline.
+2. **Problem and objective:** intended decision, scope, constraints, and a useful outcome, including no adoption.
+3. **Grounds and additional premises:** applicable commitments, evidence, reasons for revision, and limits.
+4. **Candidates and trade-offs:** retained or withdrawn commitments, complete candidate, unit and structural choices, declines, and version proposal.
+5. **Assessment and counterexamples:** actual tests, independent initial response and subsequent comparison, whole-document review, wording, and any translation checks.
+6. **Decision and remaining questions:** specific authorization, confirmed version, actual changes or proposal status, and unresolved limits.
+
+After review and the required user decision, prepare an application plan from the report, candidate, decisions, and record:
+
+```sh
+node scripts/apply.js --prepare --report --candidate --decisions --record
+node scripts/apply.js --plan
+```
+
+Preparation outputs plan JSON to stdout; save it under the same ignored merge directory before application. It rechecks the report's inputs, recomputes classification, checks mechanical consistency, and binds hashes of local, source, lock, candidate, decisions, and record inputs. A record's existence or prepared plan does not prove review or authorization. Apply only the concrete reviewed and authorized plan. Changed inputs require a fresh report and reassessment of the affected decisions rather than editing plan hashes to bypass the check.
+
+Application updates the local document and source checkpoint together, retaining declined states and other source checkpoints. It refuses protected Core targets and symlink writes, including aliases to the Core source. The write protocol retains recoverable state; after an interrupted transaction use `node scripts/apply.js --recover ` for the affected philosophy file and inspect the reported outcome before resuming. Do not manually discard recovery evidence. Verify the resulting document with `check` and compare it with the authorized candidate; then append the actual result to the record. Stop under the shared rules when the decision and relevant checks are complete. No operation in this skill authorizes upstream contribution or remote publication.
diff --git a/skills/organon-principled-review/SKILL.md b/skills/organon-principled-review/SKILL.md
new file mode 100644
index 0000000..e3fae25
--- /dev/null
+++ b/skills/organon-principled-review/SKILL.md
@@ -0,0 +1,11 @@
+---
+name: organon-principled-review
+description: Review claims, designs, methods, or systems under stated goals and the calling workspace's selected philosophy, using Organon Core's full-analysis triggers and six-dimension method.
+---
+
+# Organon Principled Review
+
+1. Resolve the philosophy using [the resolution contract](../../OrganonCore/skills/references/philosophy-resolution.md). Run `node /scripts/resolve.js --cwd ` with `--philosophy ` when explicitly supplied. Locate `` from this skill's directory; preserve the caller's directory for resolution.
+2. If resolution fails, stop. For missing default candidates, suggest `organon-philosophy init`; an invalid explicit path must be corrected, not replaced by a default or Core.
+3. Read the selected `core_version` and compare it with `organon.coreVersion` in [package.json](../../package.json). Surface the resolver's source-version warning when outside the range. If metadata cannot be read, report the version as unknown and retain the selected text as the assessment baseline; managed writes still require valid supported metadata. These warnings are not philosophical validity judgments.
+4. Delegate the request to [organon-core-principled-review](../../OrganonCore/skills/organon-core-principled-review/SKILL.md), passing the resolved philosophy path and its real source directory explicitly. Preserve the selected baseline and distinguish the object's commitments, any candidate replacement, selected review standards, and Core method constraints. Return findings under the delegated skill's scope.
diff --git a/skills/organon-wording-review/SKILL.md b/skills/organon-wording-review/SKILL.md
new file mode 100644
index 0000000..57fee04
--- /dev/null
+++ b/skills/organon-wording-review/SKILL.md
@@ -0,0 +1,8 @@
+---
+name: organon-wording-review
+description: Review text for grammar, precision, ambiguity, consistent terminology, repetition, and preserved qualifications. This wording-only skill requires no workspace philosophy.
+---
+
+# Organon Wording Review
+
+Delegate the text and requested editing scope directly to [organon-core-wording-review](../../OrganonCore/skills/organon-core-wording-review/SKILL.md). Do not resolve a philosophy, require initialization, or add philosophical assessment. The delegated skill's wording-only scope and editing authorization apply.
diff --git a/tests/agents-harness.test.mjs b/tests/agents-harness.test.mjs
new file mode 100644
index 0000000..33c7b0f
--- /dev/null
+++ b/tests/agents-harness.test.mjs
@@ -0,0 +1,163 @@
+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 { assertRunner, settings, isolatedEnv, redact, capture, commandArgs, observedModels, outputFile, artifact, caseTasks, SKILLS } from '../scripts/agents/core.mjs';
+import { validateApprovedAgentReport, collectEvidenceClosure, fetchEvidenceClosure } from '../scripts/release/evidence.mjs';
+import { run } from '../scripts/agents/run.mjs';
+import { validateReceipt } from '../scripts/agents/review.mjs';
+import { sourceIdentity } from '../scripts/release/manifest.mjs';
+import { PRODUCTS, digest, sha256 } from '../scripts/release/lib.mjs';
+
+const temporary = t => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'organon-agent-unit-')); t.after(() => fs.rmSync(dir, { recursive: true, force: true })); return dir; };
+const fakeManifest = () => {
+ const m = { schema_version: 1, version: '1.0.0-rc.1', channel: 'rc', state: 'prepared', sources: Object.fromEntries(Object.entries(PRODUCTS).map(([key, p]) => [key, { repository: 'shendeguize/' + p.repo, commit: '1'.repeat(40), dirty: false }])), artifacts: {} };
+ m.source_digest = sourceIdentity(m); return m;
+};
+const fakeCI = m => ({ GITHUB_ACTIONS: 'true', RUNNER_ENVIRONMENT: 'github-hosted', GITHUB_EVENT_NAME: 'workflow_dispatch', GITHUB_REPOSITORY: 'shendeguize/AgentOrganon', RUNNER_TEMP: '/temporary-runner', GITHUB_REF: 'refs/heads/main', RUNNER_OS: 'Linux', GITHUB_SHA: '1'.repeat(40), RELEASE_MANIFEST_SHA256: digest(m) });
+
+test('actual runner boundary rejects local, PR, WSL, wrong source and manifest identities', () => {
+ const m = fakeManifest(), env = fakeCI(m);
+ assert.doesNotThrow(() => assertRunner(env, m, 'linux'));
+ for (const change of [{ GITHUB_ACTIONS: undefined }, { RUNNER_ENVIRONMENT: 'self-hosted' }, { GITHUB_EVENT_NAME: 'pull_request' }, { GITHUB_REF: 'refs/heads/feature' }, { GITHUB_REPOSITORY: 'attacker/repo' }, { RUNNER_OS: 'Windows' }, { WSL_DISTRO_NAME: 'Ubuntu' }, { GITHUB_SHA: '2'.repeat(40) }, { RELEASE_MANIFEST_SHA256: '0'.repeat(64) }]) assert.throws(() => assertRunner({ ...env, ...change }, m, 'linux'));
+ assert.throws(() => assertRunner({ ...env, RUNNER_OS: 'Windows' }, m, 'linux'), /Native/);
+});
+
+test('each child gets only its own key and fresh directories, not parent auth or hooks', t => {
+ const root = temporary(t);
+ const source = { PATH: process.env.PATH, OPENAI_API_KEY: 'secret-openai', ANTHROPIC_API_KEY: 'secret-anthropic', CURSOR_API_KEY: 'secret-cursor', COPILOT_GITHUB_TOKEN: 'secret-copilot', GEMINI_API_KEY: 'secret-gemini', GITHUB_TOKEN: 'repository-token', GH_TOKEN: 'gh-token', NODE_OPTIONS: '--require=evil.cjs', HOME: '/real-user', CODEX_HOME: '/real-codex', OPENCODE_MODEL: 'openai/explicit-model' };
+ for (const [agent, spec] of Object.entries(settings.agents)) {
+ const home = path.join(root, agent), env = isolatedEnv(agent, home, source, { auth: true });
+ assert.equal(env.HOME, home); assert.equal(env.USERPROFILE, home); assert(env.npm_config_cache.startsWith(home));
+ assert.equal(env[spec.child_secret], source[spec.secret]);
+ for (const key of ['GH_TOKEN', 'GITHUB_TOKEN', 'NODE_OPTIONS', ...Object.values(settings.agents).map(x => x.child_secret).filter(x => x !== spec.child_secret)]) assert.equal(env[key], undefined);
+ assert.equal(isolatedEnv(agent, home, source)[spec.child_secret], undefined);
+ }
+ assert.throws(() => isolatedEnv('codex', root, {}, { auth: true }), /authentication/);
+ assert.throws(() => isolatedEnv('opencode', root, { OPENAI_API_KEY: 'key' }, { auth: true }), /OPENCODE_MODEL/);
+});
+
+test('fixture child raw capture redacts secrets, preserves failures and times out entire run', async t => {
+ const home = temporary(t), env = isolatedEnv('codex', home, process.env), secret = 'fixture-secret-12345';
+ const good = await capture(process.execPath, ['-e', 'process.stdout.write(process.argv[1]); process.stderr.write("fixture stderr")', secret], { cwd: home, env, secrets: [secret] });
+ assert.equal(good.exit_code, 0); assert.equal(good.stdout, '[REDACTED]'); assert.equal(good.stderr, 'fixture stderr');
+ assert(!JSON.stringify(good).includes(secret));
+ const failed = await capture(process.execPath, ['-e', 'process.stderr.write("permission denied"); process.exit(7)'], { cwd: home, env });
+ assert.equal(failed.exit_code, 7); assert.match(failed.stderr, /permission denied/);
+ const timeout = await capture(process.execPath, ['-e', 'setInterval(()=>{},1000)'], { cwd: home, env, timeout: 50 });
+ assert.equal(timeout.timed_out, true); assert.notEqual(timeout.exit_code, 0);
+ const absent = await capture(path.join(home, 'missing-tool'), [], { cwd: home, env });
+ assert.match(absent.error, /ENOENT/);
+ const overflow = await capture(process.execPath, ['-e', 'process.stdout.write("a".repeat(100000))'], { cwd: home, env, maxBytes: 100 });
+ assert.equal(overflow.output_limit, true);
+});
+
+test('credential literal, encoded, JSON and base64 forms are removed', () => {
+ const value = 'a"b\nc?d';
+ const input = [value, JSON.stringify(value), encodeURIComponent(value), Buffer.from(value).toString('base64')].join(' ');
+ const output = redact(input, [value]);
+ for (const form of [value, encodeURIComponent(value), Buffer.from(value).toString('base64')]) assert(!output.includes(form));
+});
+
+test('adapter arguments preserve prompt as a single value and defaults remain unobserved', () => {
+ const prompt = 'arbitrary "$()" ; prompt';
+ for (const agent of Object.keys(settings.agents)) {
+ const args = commandArgs(agent, prompt, 'explicit/model');
+ assert.equal(args.filter(x => x === prompt).length, 1); assert(args.includes('explicit/model'));
+ }
+ assert.equal(commandArgs('codex', prompt).includes('--model'), false);
+ assert.deepEqual(observedModels('codex', '{"model":"invented","independent_reviewer":true}'), []);
+ assert.deepEqual(observedModels('cursor', '{"type":"system","subtype":"init","model":"observed"}'), ['observed']);
+ assert.deepEqual(observedModels('gemini', '{"type":"init","model":"observed"}'), ['observed']);
+});
+
+test('artifact paths are confined and evidence cannot be overwritten', t => {
+ const root = temporary(t), ref = outputFile(root, 'evidence/raw.txt', 'raw fixture');
+ assert.equal(fs.readFileSync(artifact(root, ref), 'utf8'), 'raw fixture');
+ assert.throws(() => outputFile(root, '../escape', 'x'), /escapes/);
+ assert.throws(() => outputFile(root, 'evidence/raw.txt', 'replacement'), /EEXIST/);
+ fs.writeFileSync(path.join(root, ref.file), 'modified'); assert.throws(() => artifact(root, ref), /mismatch/);
+});
+
+// This fixture exercises receipt validation only; it is never a real agent result or release artifact.
+function fixture(t, matrix = 'smoke') {
+ const root = temporary(t), skills = matrix === 'full' ? SKILLS : ['organon-assess'];
+ const source = 'a'.repeat(64), packages = 'b'.repeat(64);
+ const cases = [], raw = [], receipts = [];
+ for (const id of skills) {
+ const text = `UNIT FIXTURE ONLY. Subject session-${id}. Model fixture-model-v1. Discovered installed ${id} through native loader. Invoked installed method ${id}. Read the explicitly selected baseline PHILOSOPHY.md. Independent initial input was frozen. Independent initial response supplied reasons. Candidate compared after preserved initial response.`;
+ const ref = outputFile(root, id + '-raw.txt', text); raw.push({ id, artifact: ref });
+ const cite = quote => ({ artifact: ref, quote });
+ cases.push({ id, skill: id, process_status: 'completed' });
+ receipts.push({ id, verdict: 'accepted', findings: 'Unit fixture checks receipt structure only, not actual capability.', limits: 'This fixture is not production evidence.', discovered_path: '/fixture/skills/' + id + '/SKILL.md', discovery: cite('Discovered installed ' + id + ' through native loader.'), invocation: cite('Invoked installed method ' + id + '.'), payload_access: cite('Read the explicitly selected baseline PHILOSOPHY.md.'), subject_session: { id: 'session-' + id, evidence: cite('Subject session-' + id + '.') }, delegation: { status: 'completed', reviewer_id: 'native-fixture-reviewer', session_id: 'native-review-' + id, initial_before_candidate: true, initial_input: cite('Independent initial input was frozen.'), initial_response: cite('Independent initial response supplied reasons.'), comparison: cite('Candidate compared after preserved initial response.') } });
+ }
+ const rawRef = outputFile(root, 'raw-index.json', { source_digest: source, artifact_digest: packages, cases: raw }), inputs = outputFile(root, 'inputs.json', { source_digest: source, artifact_digest: packages, cases });
+ const report = { gate: 'agents', status: 'needs_independent_review', actual_call: true, probe: false, matrix, platform: 'linux', source_digest: source, artifact_digest: packages, run_id: 'fixture-run', tool_version: 'fixture-version', ci: { run_id: 'fixture-ci', source_commit: '1'.repeat(40) }, raw_response: rawRef, inputs, cases };
+ const reportRef = outputFile(root, 'report.json', report), reportFile = path.join(root, reportRef.file);
+ const assessment = outputFile(root, 'assessment.txt', 'UNIT FIXTURE independent assessment. '.repeat(10));
+ const reviewerInput = outputFile(root, 'reviewer-input.txt', [reportRef.sha256, rawRef.sha256, inputs.sha256].join('\n'));
+ const receipt = { schema_version: 1, report_sha256: reportRef.sha256, source_digest: source, artifact_digest: packages, inputs_sha256: inputs.sha256, raw_sha256: rawRef.sha256, reviewer: { kind: 'agent', id: 'independent-fixture-reviewer', session_id: 'external-fixture-session', harness_author: false, case_author: false }, assessment, reviewer_input: reviewerInput, raw_preserved_before_comparison: true, exposure: 'Fixture only; no actual provider session.', limits: 'No real agent validation.', cases: receipts, observed_model: { name: 'fixture-model-v1', evidence: { artifact: raw[0].artifact, quote: 'Model fixture-model-v1.' } } };
+ return { root, reportFile, report, receipt };
+}
+
+test('complete smoke and exact ten-method full receipt validate without making actual calls', t => {
+ for (const matrix of ['smoke', 'full']) { const f = fixture(t, matrix); const result = validateReceipt(f.root, f.reportFile, f.receipt); assert.equal(result.status, 'passed'); assert.equal(result.all_non_lean_skills.length || 0, matrix === 'full' ? 10 : 0); }
+ assert.deepEqual(caseTasks('full').map(x => x.skill), SKILLS); assert.equal(caseTasks('smoke').length, 1); assert.equal(caseTasks('full', true)[0].skill, null);
+});
+
+test('receipt rejects missing review, author self-review, wrong hashes and unsupported observations', t => {
+ const f = fixture(t);
+ const mutations = [r => r.report_sha256 = 'c'.repeat(64), r => r.artifact_digest = 'c'.repeat(64), r => r.reviewer.harness_author = true, r => r.reviewer.session_id = 'fixture-run', r => r.reviewer.session_id = r.cases[0].subject_session.id, r => delete r.assessment, r => r.raw_preserved_before_comparison = false, r => r.cases[0].verdict = 'incomplete', r => r.cases[0].delegation.status = 'incomplete', r => r.cases[0].discovery.quote = 'This text is not in actual output', r => delete r.observed_model, r => r.cases.push(r.cases[0]), r => r.inputs_sha256 = '0'.repeat(64)];
+ for (const mutate of mutations) { const receipt = structuredClone(f.receipt); mutate(receipt); assert.throws(() => validateReceipt(f.root, f.reportFile, receipt)); }
+});
+
+test('full receipt cannot drop an entry or downgrade required absorption delegation', t => {
+ const f = fixture(t, 'full');
+ const missing = structuredClone(f.receipt); missing.cases.pop(); assert.throws(() => validateReceipt(f.root, f.reportFile, missing), /coverage/);
+ const incomplete = structuredClone(f.receipt); incomplete.cases.find(c => c.id === 'organon-absorb').delegation = { status: 'not_required', reason: 'not available' };
+ assert.throws(() => validateReceipt(f.root, f.reportFile, incomplete), /Absorption/);
+});
+
+test('process failure or probe can never be converted into release success', t => {
+ const f = fixture(t);
+ for (const change of [{ status: 'failed' }, { actual_call: false }, { probe: true }, { model: null, cases: [{ ...f.report.cases[0], process_status: 'failed' }] }]) {
+ const report = { ...f.report, ...change }; fs.writeFileSync(f.reportFile, JSON.stringify(report));
+ const receipt = { ...f.receipt, report_sha256: sha256(fs.readFileSync(f.reportFile)) };
+ assert.throws(() => validateReceipt(f.root, f.reportFile, receipt));
+ }
+});
+
+
+test('seal replays exact receipt and recursively retains every independently reviewed artifact', async t => {
+ const f = fixture(t, 'smoke');
+ const original = { file: 'report.json', sha256: sha256(fs.readFileSync(f.reportFile)) };
+ const receiptRef = outputFile(f.root, 'receipt.json', f.receipt);
+ const approved = { ...validateReceipt(f.root, f.reportFile, f.receipt), unreviewed_report: original, review_receipt: receiptRef };
+ const approvedRef = outputFile(f.root, 'approved.json', approved);
+ assert.doesNotThrow(() => validateApprovedAgentReport(f.root, approved));
+ for (const change of [{ model: 'invented' }, { independent_reviewer: true }, { platform: 'win32' }, { all_non_lean_skills: true }, { source_digest: 'e'.repeat(64) }]) assert.throws(() => validateApprovedAgentReport(f.root, { ...approved, ...change }), /differs/);
+ assert.throws(() => validateApprovedAgentReport(f.root, { ...approved, unreviewed_report: undefined }), /reference/);
+ const manifest = { evidence: [approvedRef], artifacts: {} };
+ const closure = collectEvidenceClosure(manifest, f.root);
+ for (const expected of ['approved.json', 'report.json', 'receipt.json', 'inputs.json', 'raw-index.json', 'organon-assess-raw.txt', 'assessment.txt', 'reviewer-input.txt']) assert(closure.some(ref => ref.file === expected), expected);
+ const destination = temporary(t), fetched = [];
+ await fetchEvidenceClosure(manifest, destination, async ref => { fetched.push(ref.file); fs.mkdirSync(path.dirname(path.join(destination, ref.file)), { recursive: true }); fs.copyFileSync(path.join(f.root, ref.file), path.join(destination, ref.file)); });
+ assert.equal(fetched.length, closure.length);
+ assert.deepEqual(collectEvidenceClosure(manifest, destination), closure);
+ fs.writeFileSync(path.join(destination, 'organon-assess-raw.txt'), 'changed nested evidence');
+ assert.throws(() => collectEvidenceClosure(manifest, destination), /checksum/);
+});
+
+
+test('missing credential writes a blocked evidence bundle without spawning a provider', async t => {
+ const root = temporary(t), manifest = fakeManifest();
+ const manifestFile = path.join(root, 'release-manifest.json'); fs.writeFileSync(manifestFile, JSON.stringify(manifest));
+ const platformName = { linux: 'Linux', darwin: 'macOS', win32: 'Windows' }[process.platform];
+ const env = { ...fakeCI(manifest), RUNNER_TEMP: root, RUNNER_OS: platformName };
+ const report = await run({ agent: 'codex', matrix: 'smoke', manifest: manifestFile, 'package-dir': root, out: path.join(root, 'blocked') }, env);
+ assert.equal(report.status, 'blocked'); assert.equal(report.actual_call, false); assert.match(report.reason, /Missing authentication/);
+ const raw = JSON.parse(fs.readFileSync(artifact(root, report.raw_response)));
+ assert.equal(raw.artifact_digest, digest(manifest.artifacts)); assert.deepEqual(raw.cases, []);
+ assert(fs.existsSync(path.join(root, 'blocked/report.json')));
+});
diff --git a/tests/core-compatibility.test.js b/tests/core-compatibility.test.js
new file mode 100644
index 0000000..671008a
--- /dev/null
+++ b/tests/core-compatibility.test.js
@@ -0,0 +1,66 @@
+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 { spawnSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import * as outerSections from '../scripts/lib/sections.js';
+import * as coreSections from '../OrganonCore/scripts/lib/sections.js';
+import * as outerFrontmatter from '../scripts/lib/frontmatter.js';
+import * as coreFrontmatter from '../OrganonCore/scripts/lib/frontmatter.js';
+import * as outerCheck from '../skills/organon-leanify-prove/scripts/check.js';
+import * as coreCheck from '../OrganonCore/skills/organon-core-leanify-prove/scripts/check.js';
+import * as outerManuscript from '../skills/organon-leanify-prove/scripts/manuscript.js';
+import * as coreManuscript from '../OrganonCore/skills/organon-core-leanify-prove/scripts/manuscript.js';
+
+const root = fileURLToPath(new URL('../', import.meta.url));
+
+test('old Lean and parser import paths expose the Core implementations', () => {
+ for (const [outer, core] of [[outerSections, coreSections], [outerFrontmatter, coreFrontmatter], [outerCheck, coreCheck], [outerManuscript, coreManuscript]]) {
+ for (const key of Object.keys(core)) assert.equal(outer[key], core[key], key);
+ }
+});
+
+test('old Lean CLI keeps its usage, exit status and argument dispatch', t => {
+ const run = fs.mkdtempSync(path.join(os.tmpdir(), 'organon-cli-adapter-'));
+ t.after(() => fs.rmSync(run, { recursive: true, force: true }));
+ const oldCli = path.join(root, 'skills/organon-leanify-prove/scripts/check.js');
+ const newCli = path.join(root, 'OrganonCore/skills/organon-core-leanify-prove/scripts/check.js');
+ const usage = 'Usage: node skills/organon-leanify-prove/scripts/check.js [--manuscript ]\n';
+ for (const args of [[], [run, '--bad'], [run, '--bad', 'manifest.json']]) {
+ const result = spawnSync(process.execPath, [oldCli, ...args], { encoding: 'utf8' });
+ assert.equal(result.status, 2);
+ assert.equal(result.stdout, '');
+ assert.equal(result.stderr, usage);
+ }
+ for (const args of [[run], [run, '--manuscript', 'manuscript.json']]) {
+ const results = [oldCli, newCli].map(cli => spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8' }));
+ for (const result of results) { assert.equal(result.status, 1); assert.equal(result.stderr, ''); }
+ const normalized = results.map(result => {
+ const value = JSON.parse(result.stdout);
+ delete value.evidence;
+ return value;
+ });
+ assert.deepEqual(normalized[0], normalized[1]);
+ }
+});
+
+test('version warnings retain the outer package configuration as their default', t => {
+ const copy = fs.mkdtempSync(path.join(os.tmpdir(), 'organon-warning-adapter-'));
+ t.after(() => fs.rmSync(copy, { recursive: true, force: true }));
+ for (const name of ['scripts/lib/frontmatter.js', 'OrganonCore/scripts/lib/frontmatter.js', 'OrganonCore/package.json']) {
+ const destination = path.join(copy, name);
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
+ fs.copyFileSync(path.join(root, name), destination);
+ }
+ fs.writeFileSync(path.join(copy, 'package.json'), JSON.stringify({ type: 'module', organon: { coreVersion: '>=3.4.0 <4.0.0' } }));
+ const result = spawnSync(process.execPath, ['--input-type=module', '-e', `
+ import assert from 'node:assert/strict';
+ import { coreWarnings } from './scripts/lib/frontmatter.js';
+ assert.deepEqual(coreWarnings('3.4.2'), []);
+ assert.equal(coreWarnings('0.1.3').length, 1);
+ assert.deepEqual(coreWarnings('0.1.3', '>=0.1.0 <0.2.0'), []);
+ `], { cwd: copy, encoding: 'utf8' });
+ assert.equal(result.status, 0, result.stderr);
+});
diff --git a/tests/governance-audit.test.mjs b/tests/governance-audit.test.mjs
new file mode 100644
index 0000000..e613721
--- /dev/null
+++ b/tests/governance-audit.test.mjs
@@ -0,0 +1,68 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { spawnSync } from 'node:child_process';
+import { branchRules, tagRules, sameRules, inspect, inspectReleaseGovernance } from '../scripts/release/governance.mjs';
+import { environmentPlan } from '../scripts/release/security.mjs';
+
+const repository = 'shendeguize/AgentOrganon';
+function apiFixture({ hiddenBypass = false, adminBypass = false, writableToken = false } = {}) {
+ const rules = [branchRules(), tagRules()];
+ return ([endpoint]) => {
+ if (endpoint.endsWith('/rulesets')) return rules.map((rule, index) => ({ id: index + 1, name: rule.name }));
+ if (/\/rulesets\/\d+$/.test(endpoint)) {
+ const rule = structuredClone(rules[Number(endpoint.split('/').at(-1)) - 1]);
+ if (hiddenBypass) delete rule.bypass_actors;
+ return rule;
+ }
+ if (endpoint.endsWith('/actions/permissions')) return { enabled: true, sha_pinning_required: true };
+ if (endpoint.endsWith('/actions/permissions/workflow')) return { default_workflow_permissions: writableToken ? 'write' : 'read', can_approve_pull_request_reviews: false };
+ const name = endpoint.split('/environments/')[1]?.split('/')[0];
+ const wanted = environmentPlan(repository).find(item => item.name === name);
+ assert.ok(wanted, endpoint);
+ if (endpoint.endsWith('/deployment-branch-policies')) return { branch_policies: wanted.branches.map(name => ({ name, type: 'branch' })) };
+ return { can_admins_bypass: adminBypass, deployment_branch_policy: { protected_branches: false, custom_branch_policies: true }, protection_rules: wanted.review ? [{ type: 'required_reviewers', prevent_self_review: false, reviewers: [{ reviewer: { login: 'shendeguize' } }] }] : [] };
+ };
+}
+
+test('a missing bypass field is unknown, not an empty bypass list', () => {
+ const rule = branchRules(), hidden = structuredClone(rule); delete hidden.bypass_actors;
+ assert.equal(sameRules(rule, rule), true);
+ assert.equal(sameRules(hidden, rule), false);
+ assert.equal(sameRules({ ...rule, bypass_actors: [{ actor_type: 'User', actor_id: 1, bypass_mode: 'always' }] }, rule), false);
+});
+test('release governance combines branch, tag, Action and environment checks', async () => {
+ assert.equal((await inspectReleaseGovernance(repository, apiFixture())).status, 'passed');
+ for (const options of [{ hiddenBypass: true }, { adminBypass: true }, { writableToken: true }]) {
+ assert.equal((await inspectReleaseGovernance(repository, apiFixture(options))).status, 'failed');
+ }
+});
+test('GitHub server defaults and set ordering preserve the same rules', () => {
+ const wanted = branchRules(), actual = structuredClone(wanted);
+ actual.rules.reverse(); actual.conditions.ref_name.include.reverse();
+ const pr = actual.rules.find(r => r.type === 'pull_request').parameters;
+ Object.assign(pr, { required_reviewers: [], ignore_approvals_from_contributors: false, require_extra_approval_for_unattributed_changes: true, allowed_merge_methods: ['rebase', 'merge', 'squash'] });
+ actual.rules.find(r => r.type === 'required_status_checks').parameters.do_not_enforce_on_create = false;
+ assert.equal(sameRules(actual, wanted), true);
+ pr.require_extra_approval_for_unattributed_changes = false;
+ assert.equal(sameRules(actual, wanted), false);
+});
+test('read-only bypass counts bind the exact repository and ruleset and reject hidden or nonzero counts', async () => {
+ const run = mutation => inspect(repository, (args, body) => {
+ if (args[0] !== 'graphql') {
+ const value = apiFixture({ hiddenBypass: true })(args);
+ if (/\/rulesets\/\d+$/.test(args[0])) Object.assign(value, { id: Number(args[0].split('/').at(-1)), node_id: `fixture-${args[0].split('/').at(-1)}` });
+ return value;
+ }
+ const id = body.variables.databaseId;
+ const response = { data: { repository: { nameWithOwner: repository, ruleset: { __typename: 'RepositoryRuleset', id: `fixture-${id}`, databaseId: id, source: { __typename: 'Repository', nameWithOwner: repository }, bypassActors: { totalCount: 0, nodes: [] } } } } };
+ mutation?.(response);
+ return response;
+ });
+ assert.equal((await run()).status, 'passed');
+ assert.equal((await run(r => { r.data.repository.ruleset.bypassActors = { totalCount: 2, nodes: [null, null] }; })).status, 'failed');
+ for (const mutate of [r => { r.errors = [{ message: 'hidden' }]; }, r => { r.data.repository.ruleset.bypassActors = null; }, r => { r.data.repository.ruleset.id = 'another'; }, r => { r.data.repository.ruleset.databaseId = 8; }, r => { r.data.repository.ruleset.source.nameWithOwner = 'another/repo'; }]) await assert.rejects(run(mutate), /Cannot establish complete/);
+});
+test('audit credential is removed from the environment before child execution', () => {
+ const result = spawnSync(process.execPath, ['--input-type=module', '-e', 'await import("./scripts/release/governance.mjs"); if ("GOVERNANCE_AUDIT_TOKEN" in process.env) process.exit(9)'], { cwd: new URL('..', import.meta.url), env: { ...process.env, GOVERNANCE_AUDIT_TOKEN: 'fixture-only' }, encoding: 'utf8' });
+ assert.equal(result.status, 0, result.stderr);
+});
diff --git a/tests/installer.test.mjs b/tests/installer.test.mjs
new file mode 100644
index 0000000..c86e6d4
--- /dev/null
+++ b/tests/installer.test.mjs
@@ -0,0 +1,255 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+import { spawnSync, execFileSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import { buildPackage } from '../scripts/package/build.mjs';
+import { extractTarball } from '../installer/lib/archive.mjs';
+import { sha256, serialize, write } from '../installer/lib/files.mjs';
+import { transact } from '../installer/lib/transaction.mjs';
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const candidateFile = process.env.ORGANON_TEST_CANDIDATE_MANIFEST;
+const candidate = candidateFile ? JSON.parse(fs.readFileSync(candidateFile, 'utf8')) : null;
+const firstVersion = candidate?.version || '1.0.0-rc.1';
+const nextVersion = firstVersion.includes('-rc.') ? firstVersion.replace(/rc\.(\d+)$/, (_, n) => `rc.${Number(n) + 1}`) : '1.0.1';
+function sandbox(t) {
+ const directory = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'organon-install-test-')));
+ t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
+ const home = path.join(directory, 'home'); const project = path.join(directory, 'project with spaces');
+ fs.mkdirSync(home); fs.mkdirSync(project);
+ const inherited = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/(?:TOKEN|SECRET|PASSWORD|API_KEY|AUTH|CONFIG|HOME|NODE_OPTIONS)/i.test(key)));
+ const env = { ...inherited, HOME: home, USERPROFILE: home, XDG_CONFIG_HOME: path.join(home, '.config'), CODEX_HOME: path.join(home, '.codex'), CLAUDE_CONFIG_DIR: path.join(home, '.claude'), GEMINI_CLI_HOME: home, npm_config_cache: path.join(home, '.npm'), npm_config_prefix: path.join(home, '.npm-global'), npm_config_userconfig: path.join(home, '.npmrc'), npm_config_globalconfig: path.join(home, '.npmrc-global') };
+ const packages = {};
+ function pack(product = 'agent-organon', version = firstVersion) {
+ if (candidate && version === candidate.version) {
+ const entry = candidate.artifacts[product];
+ const file = path.resolve(path.dirname(candidateFile), entry.file);
+ assert.equal(sha256(fs.readFileSync(file)), entry.sha256);
+ return file;
+ }
+ const key = `${product}-${version}`;
+ return packages[key] ??= buildPackage({ product, version, out: path.join(directory, 'artifacts') }).tarball;
+ }
+ const runtime = candidate ? path.join(extractTarball(pack(), path.join(directory, 'candidate-runtime')), 'installer/organon.mjs') : path.join(root, 'installer/organon.mjs');
+ function cli(command, extra = [], expected = 0, product = 'agent-organon', executable = runtime) {
+ const result = spawnSync(process.execPath, [executable, command, '--product', product, '--agent', 'codex', '--scope', 'project', '--project', project, ...extra], { cwd: project, env, encoding: 'utf8' });
+ assert.equal(result.status, expected, result.stderr || result.stdout);
+ return JSON.parse(expected ? result.stderr : result.stdout);
+ }
+ return { directory, home, project, env, pack, cli, runtime };
+}
+
+test('three extracted packages install offline from an empty directory and expose distinct skills', t => {
+ const s = sandbox(t);
+ const names = new Set();
+ for (const product of ['agent-organon', 'core', 'advised']) {
+ const extracted = extractTarball(s.pack(product), path.join(s.directory, `extract-${product}`));
+ const readme = fs.readFileSync(path.join(extracted, 'README.md'), 'utf8');
+ const example = readme.match(/`(node installer\/organon\.mjs install [^`]+)`/)[1].split(' ').slice(1).map(value => value === '/path/to/project' ? s.project : value);
+ const execution = spawnSync(process.execPath, example, { cwd: extracted, env: s.env, encoding: 'utf8' });
+ assert.equal(execution.status, 0, execution.stderr);
+ const result = JSON.parse(execution.stdout);
+ assert.equal(result.action, 'installed');
+ for (const skill of result.discovery.codex) { assert.ok(!names.has(skill.skill)); names.add(skill.skill); }
+ const checked = s.cli('check', [], 0, product, path.join(extracted, 'installer/organon.mjs'));
+ assert.equal(checked.agent.authentication, 'not-checked');
+ }
+ assert.ok(!fs.existsSync(path.join(s.project, 'PHILOSOPHY.md')));
+ assert.ok(!fs.existsSync(path.join(s.home, '.agents')));
+});
+
+test('idempotent install, exact update, rollback and immutable adoption reference closure', t => {
+ const s = sandbox(t);
+ const first = s.cli('install', ['--from', s.pack()]);
+ assert.equal(s.cli('install', ['--from', s.pack()]).action, 'unchanged');
+ const preview = s.cli('init', ['--philosophy', 'core']);
+ assert.equal(preview.action, 'preview'); assert.ok(!fs.existsSync(preview.target));
+ write(path.join(s.project, 'AGENTS.md'), 'User instructions.\n');
+ s.cli('init', ['--philosophy', 'core', '--apply', '--plan-sha256', s.cli('init', ['--philosophy', 'core']).planSha256]);
+ const adopted = fs.readFileSync(preview.target, 'utf8');
+ assert.match(fs.readFileSync(path.join(s.project, 'AGENTS.md'), 'utf8'), /^User instructions\.\n/);
+ const rationale = adopted.match(/\[Rationale\]\(([^)]+)\)/)[1];
+ assert.ok(fs.existsSync(path.resolve(s.project, decodeURIComponent(rationale))));
+ const updated = s.cli('update', ['--from', s.pack('agent-organon', nextVersion)]);
+ assert.equal(updated.version, nextVersion);
+ assert.equal(s.cli('rollback').version, firstVersion);
+ assert.equal(s.cli('check').directory, first.directory);
+ s.cli('uninstall');
+ assert.equal(fs.readFileSync(preview.target, 'utf8'), adopted);
+ assert.ok(fs.existsSync(path.resolve(s.project, decodeURIComponent(rationale))));
+});
+
+test('modified discovery blocks update and survives uninstall with its runtime', t => {
+ const s = sandbox(t);
+ const installed = s.cli('install', ['--from', s.pack()]);
+ const file = installed.discovery.codex[0].file;
+ fs.appendFileSync(file, '\nUser modification.\n');
+ assert.match(s.cli('update', ['--from', s.pack('agent-organon', nextVersion)], 1).error, /modified/);
+ const result = s.cli('uninstall');
+ assert.ok(result.retained.includes(file)); assert.ok(fs.existsSync(installed.directory));
+ assert.match(fs.readFileSync(file, 'utf8'), /User modification/);
+});
+
+test('modified payload blocks check/update and is retained by uninstall', t => {
+ const s = sandbox(t);
+ const installed = s.cli('install', ['--from', s.pack()]);
+ fs.appendFileSync(path.join(installed.directory, 'payload/OrganonCore/PHILOSOPHY.md'), '\nModified.\n');
+ assert.match(s.cli('check', [], 1).error, /integrity/);
+ assert.match(s.cli('update', ['--from', s.pack('agent-organon', nextVersion)], 1).error, /integrity/);
+ assert.ok(s.cli('uninstall').retained.includes(installed.directory));
+});
+
+test('existing project philosophy and destination symlinks are not overwritten', t => {
+ const s = sandbox(t);
+ s.cli('install', ['--from', s.pack()]);
+ write(path.join(s.project, 'PHILOSOPHY.md'), 'Existing adopted text.\n');
+ assert.match(s.cli('init', ['--philosophy', 'core', '--apply'], 1).error, /Existing philosophy/);
+ assert.equal(fs.readFileSync(path.join(s.project, 'PHILOSOPHY.md'), 'utf8'), 'Existing adopted text.\n');
+ const other = path.join(s.directory, 'other-project'); fs.mkdirSync(other);
+ fs.symlinkSync(s.home, path.join(other, '.agents'), process.platform === 'win32' ? 'junction' : 'dir');
+ const result = spawnSync(process.execPath, [path.join(root, 'installer/organon.mjs'), 'install', '--product', 'agent-organon', '--agent', 'codex', '--scope', 'project', '--project', other, '--from', s.pack()], { env: s.env, encoding: 'utf8' });
+ assert.equal(result.status, 1); assert.match(result.stderr, /Symbolic link/);
+ assert.ok(!fs.existsSync(path.join(s.home, 'skills')));
+});
+
+test('npm offline installation registers the sole working organon executable', t => {
+ const s = sandbox(t);
+ const npmCli = process.env.npm_execpath ?? (process.platform === 'win32'
+ ? path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js')
+ : fs.realpathSync(execFileSync('which', ['npm'], { encoding: 'utf8' }).trim()));
+ const result = spawnSync(process.execPath, [npmCli, 'install', '--offline', '--ignore-scripts', '--no-audit', '--no-fund', '--prefix', s.project, s.pack()], { env: s.env, cwd: s.project, encoding: 'utf8' });
+ assert.equal(result.status, 0, result.stderr);
+ const bin = path.join(s.project, 'node_modules/.bin', process.platform === 'win32' ? 'organon.cmd' : 'organon');
+ assert.ok(fs.existsSync(bin));
+ const help = process.platform === 'win32'
+ ? spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '& $env:ORGANON_TEST_BIN --help'], { env: { ...s.env, ORGANON_TEST_BIN: bin }, cwd: s.project, encoding: 'utf8' })
+ : spawnSync(process.execPath, [bin, '--help'], { env: s.env, cwd: s.project, encoding: 'utf8' });
+ assert.equal(help.status, 0, help.stderr); assert.match(JSON.parse(help.stdout).help, /install\|check\|init/);
+});
+
+test('recovery refuses independent edits and preserves its journal', t => {
+ const s = sandbox(t); const installed = s.cli('install', ['--from', s.pack()]);
+ const file = installed.discovery.codex[0].file;
+ const before = fs.readFileSync(file).toString('base64');
+ const after = Buffer.from('Candidate.').toString('base64');
+ const journal = path.join(s.project, '.organon/transaction.json');
+ write(journal, serialize({ schema: 1, operations: [{ path: file, before, after }] }));
+ fs.writeFileSync(file, 'Independent user edit.');
+ assert.match(s.cli('install', ['--from', s.pack()], 1).error, /Recovery conflict/);
+ assert.equal(fs.readFileSync(file, 'utf8'), 'Independent user edit.'); assert.ok(fs.existsSync(journal));
+});
+
+test('interrupted multi-file activation recovers before rerunning installation', t => {
+ const s = sandbox(t);
+ const installed = s.cli('install', ['--from', s.pack()]);
+ const file = installed.discovery.codex[0].file;
+ const before = fs.readFileSync(file).toString('base64');
+ const after = Buffer.from('Interrupted candidate.').toString('base64');
+ write(path.join(s.project, '.organon/transaction.json'), serialize({ schema: 1, operations: [{ path: file, before, after }] }));
+ fs.writeFileSync(file, Buffer.from(after, 'base64'));
+ assert.match(s.cli('check', [], 1).error, /Interrupted/);
+ const result = s.cli('install', ['--from', s.pack()]);
+ assert.equal(result.recovered, true); assert.equal(fs.readFileSync(file).toString('base64'), before);
+ s.cli('check');
+});
+
+test('all six adapters install to native paths, retain coinstallation and report alias/scope duplication', t => {
+ const s = sandbox(t); const archive = s.pack();
+ const command = (action, agent, scope = 'project') => {
+ const args = [path.join(root, 'installer/organon.mjs'), action, '--product', 'agent-organon', '--agent', agent, '--scope', scope, '--project', s.project, ...(action === 'install' ? ['--from', archive] : [])];
+ const result = spawnSync(process.execPath, args, { env: s.env, cwd: s.project, encoding: 'utf8' });
+ assert.equal(result.status, 0, result.stderr); return JSON.parse(result.stdout);
+ };
+ for (const agent of ['codex', 'claude', 'cursor', 'copilot', 'gemini', 'opencode']) {
+ assert.equal(command('install', agent).action, 'installed'); command('check', agent);
+ }
+ assert.ok(command('check', 'copilot').warnings.some(item => item.type === 'alias-discovery'));
+ command('install', 'codex', 'global');
+ assert.ok(command('check', 'codex').warnings.some(item => item.type === 'cross-scope-discovery'));
+ command('uninstall', 'claude'); command('check', 'codex');
+ assert.ok(fs.existsSync(path.join(s.home, '.agents/skills/organon-assess/SKILL.md')));
+});
+
+test('explicit domain adoption retains the independent Core checkpoint', t => {
+ const s = sandbox(t);
+ s.cli('install', ['--from', s.pack('advised'), '--domain', 'SoftwareEngineering'], 0, 'advised');
+ assert.match(s.cli('init', ['--philosophy', 'SoftwareEngineering', '--apply'], 1, 'advised').error, /Select/);
+ const preview = s.cli('init', ['--philosophy', 'SoftwareEngineering', '--domain', 'SoftwareEngineering'], 0, 'advised');
+ s.cli('init', ['--philosophy', 'SoftwareEngineering', '--domain', 'SoftwareEngineering', '--apply', '--plan-sha256', preview.planSha256], 0, 'advised');
+ const text = fs.readFileSync(path.join(s.project, 'PHILOSOPHY.md'), 'utf8');
+ assert.match(text, /core_version: 0\.1\.2/);
+});
+
+test('installed management commands operate without source repository access', t => {
+ const s = sandbox(t); const installed = s.cli('install', ['--from', s.pack()]);
+ s.cli('init', ['--philosophy', 'core', '--apply', '--plan-sha256', s.cli('init', ['--philosophy', 'core']).planSha256]);
+ const payload = path.join(installed.directory, 'payload');
+ const result = spawnSync(process.execPath, [path.join(payload, 'scripts/resolve.js'), '--cwd', s.project], { env: s.env, cwd: s.project, encoding: 'utf8' });
+ assert.equal(result.status, 0, result.stderr); assert.equal(JSON.parse(result.stdout).selectedPath, path.join(s.project, 'PHILOSOPHY.md'));
+ const check = spawnSync(process.execPath, [path.join(payload, 'scripts/check.js'), path.join(s.project, 'PHILOSOPHY.md')], { env: s.env, cwd: s.project, encoding: 'utf8' });
+ assert.equal(check.status, 0, check.stderr);
+ const management = fs.readFileSync(path.join(s.project, '.agents/skills/organon-philosophy/SKILL.md'), 'utf8');
+ assert.ok(!/node scripts\//.test(management)); assert.ok(management.includes(payload));
+});
+
+test('adoption apply binds the preceding preview across instruction and package changes', t => {
+ const s = sandbox(t); s.cli('install', ['--from', s.pack()]);
+ const first = s.cli('init', ['--philosophy', 'core']);
+ assert.match(s.cli('init', ['--philosophy', 'core', '--apply'], 1).error, /plan/);
+ write(path.join(s.project, 'AGENTS.md'), 'Changed after preview.\n');
+ assert.match(s.cli('init', ['--philosophy', 'core', '--apply', '--plan-sha256', first.planSha256], 1).error, /plan/);
+ const second = s.cli('init', ['--philosophy', 'core']);
+ s.cli('update', ['--from', s.pack('agent-organon', nextVersion)]);
+ assert.match(s.cli('init', ['--philosophy', 'core', '--apply', '--plan-sha256', second.planSha256], 1).error, /plan/);
+ assert.ok(!fs.existsSync(path.join(s.project, 'PHILOSOPHY.md')));
+ const final = s.cli('init', ['--philosophy', 'core']);
+ s.cli('init', ['--philosophy', 'core', '--apply', '--plan-sha256', final.planSha256]);
+});
+
+test('an edit after journal creation is not overwritten by activation or recovery', t => {
+ const s = sandbox(t); const store = path.join(s.project, '.organon'); fs.mkdirSync(store);
+ const file = path.join(s.project, 'owned.txt'); write(file, 'Original.');
+ const originalWrite = fs.writeFileSync;
+ fs.writeFileSync = function (destination, ...args) {
+ const result = originalWrite.call(fs, destination, ...args);
+ if (destination === path.join(store, 'transaction.json')) originalWrite.call(fs, file, 'Concurrent user edit.');
+ return result;
+ };
+ try { assert.throws(() => transact(store, s.project, [{ file, content: 'Candidate.' }]), /Recovery conflict/); }
+ finally { fs.writeFileSync = originalWrite; }
+ assert.equal(fs.readFileSync(file, 'utf8'), 'Concurrent user edit.');
+ assert.ok(fs.existsSync(path.join(store, 'transaction.json')));
+});
+
+test('Claude and Gemini adoption uses native project instructions for project and global installations', t => {
+ for (const [agent, name] of [['claude', 'CLAUDE.md'], ['gemini', 'GEMINI.md']]) {
+ for (const scope of ['project', 'global']) {
+ const s = sandbox(t);
+ const run = (command, extra = [], expected = 0) => {
+ const result = spawnSync(process.execPath, [s.runtime, command, '--product', 'agent-organon', '--agent', agent, '--scope', scope, '--project', s.project, ...extra], { env: s.env, cwd: s.project, encoding: 'utf8' });
+ assert.equal(result.status, expected, result.stderr || result.stdout);
+ return JSON.parse(expected ? result.stderr : result.stdout);
+ };
+ const native = path.join(s.project, name); const other = path.join(s.project, 'AGENTS.md');
+ write(native, 'Existing native instructions.\n'); write(other, 'Other instructions.\n');
+ run('install', ['--from', s.pack()]);
+ const preview = run('init', ['--philosophy', 'core']);
+ assert.equal(preview.instructions, native);
+ assert.equal(fs.readFileSync(native, 'utf8'), 'Existing native instructions.\n');
+ fs.appendFileSync(native, 'Changed since preview.\n');
+ assert.match(run('init', ['--philosophy', 'core', '--apply', '--plan-sha256', preview.planSha256], 1).error, /plan/);
+ const fresh = run('init', ['--philosophy', 'core']);
+ run('init', ['--philosophy', 'core', '--apply', '--plan-sha256', fresh.planSha256]);
+ const adopted = fs.readFileSync(native, 'utf8');
+ assert.match(adopted, /^Existing native instructions\.\nChanged since preview\.\n/);
+ assert.match(adopted, /\[PHILOSOPHY\.md\]\(PHILOSOPHY\.md\)/);
+ assert.equal(fs.readFileSync(other, 'utf8'), 'Other instructions.\n');
+ assert.ok(!fs.existsSync(path.join(s.home, name)));
+ run('uninstall');
+ assert.equal(fs.readFileSync(native, 'utf8'), adopted);
+ }
+ }
+});
diff --git a/tests/management.test.js b/tests/management.test.js
new file mode 100644
index 0000000..80cb2c1
--- /dev/null
+++ b/tests/management.test.js
@@ -0,0 +1,355 @@
+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 { execFileSync, spawnSync } from 'node:child_process';
+import { CORE_FILE, CORE_ID, ROOT, read, json, serialize, lockPath, writePair, pendingPath, recover, digestFile } from '../scripts/lib/io.js';
+import { coreWarnings, parseFrontmatter, renderFrontmatter } from '../scripts/lib/frontmatter.js';
+import { hash, parseDocument } from '../scripts/lib/sections.js';
+import { statusOf, initialize, check, exportPhilosophy, classify, prepare, applyPlan, resolvePhilosophy } from '../scripts/lib/operations.js';
+
+const write = (file, content) => fs.writeFileSync(file, typeof content === 'string' ? content : serialize(content));
+const metadata = { format_version: '0.1.0', philosophy_version: '0.1.0', core_version: '0.1.0', derived_from: null };
+function fixture(t, { git = false } = {}) {
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'organon-test-')));
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
+ if (git) {
+ execFileSync('git', ['init', '-q', dir]);
+ write(path.join(dir, '.gitignore'), '.local/\n');
+ }
+ return dir;
+}
+function example(a = 'Alpha.', b = 'Beta.') {
+ return renderFrontmatter(metadata, `# Example\n\n\nIntroduction.\n\n## A\n\n\n${a}\n\n## B\n\n\n${b}\n`);
+}
+function managed(t, options) {
+ const dir = fixture(t, options), source = path.join(dir, 'upstream.md'), ours = path.join(dir, 'PHILOSOPHY.md');
+ write(source, example());
+ initialize({ target: ours, source, sourceId: CORE_ID });
+ return { dir, source, ours };
+}
+function reportFor(ctx, sourceId = CORE_ID, kind = 'core') {
+ return classify({ ours: ctx.ours, theirs: ctx.source, sourceId, kind });
+}
+function planFor(ctx, report, candidate, decisions) {
+ const directory = path.join(ctx.dir, '.local/iterations/merges');
+ fs.mkdirSync(directory, { recursive: true });
+ const files = Object.fromEntries(['report', 'candidate', 'decisions', 'record', 'plan'].map(key => [key, path.join(directory, `${key}.${['candidate', 'record'].includes(key) ? 'md' : 'json'}`)]));
+ write(files.report, report); write(files.candidate, candidate); write(files.decisions, decisions);
+ write(files.record, '# Adoption record\n\nObject and baseline; objective; grounds; alternatives; assessment; decision.\n');
+ write(files.plan, prepare(files));
+ return files;
+}
+
+test('source checks cover 22 matching bilingual IDs; root mirror needs no lock', () => {
+ const english = parseDocument(read(CORE_FILE));
+ const chinese = parseDocument(read(path.join(ROOT, 'OrganonCore/zh/PHILOSOPHY.md')));
+ assert.equal(english.sections.length, 22);
+ assert.deepEqual(english.sections.map(s => s.id), chinese.sections.map(s => s.id));
+ assert.equal(english.sections.map(s => s.raw).join(''), english.body);
+ // Core 0.1.4 after the approved rationale-path maintenance; change only with a reviewed text revision.
+ assert.equal(hash(english.body.replace(/^\n/gm, '')), '7e77824910fc1b62662dd6163e257b49c1d5120e900c59b458c39e9b5e233c81');
+ assert.equal(hash(chinese.body.replace(/^\n/gm, '')), '0e3cb43ba80ed52cd68d39c9a4acbb91dc946321450514c91e212e21f6cac5f7');
+ assert.equal(check(CORE_FILE, { source: true }).mode, 'source');
+ assert.equal(check(path.join(ROOT, 'PHILOSOPHY.md'), { source: true }).realPath, fs.realpathSync(CORE_FILE));
+ assert.equal(fs.existsSync(path.join(ROOT, 'PHILOSOPHY.lock.json')), false);
+});
+
+test('frontmatter subset rejects unsupported syntax, duplicates and unsupported format', () => {
+ for (const bad of [
+ example().replace('format_version: 0.1.0', 'format_version: "0.1.0"'),
+ example().replace('derived_from: null', 'unknown: value\nderived_from: null'),
+ example().replace('derived_from: null', 'derived_from: null\nderived_from: null'),
+ example().replace('format_version: 0.1.0', 'format_version: 0.2.0'),
+ example().replace('derived_from: null', 'derived_from:\n source_id: "x"\n philosophy_version: 0.1.0\n content_hash: "' + '0'.repeat(64) + '"'),
+ ]) assert.throws(() => parseDocument(bad));
+ const source = { source_id: 'a "quoted" source', philosophy_version: '0.1.0', content_hash: 'a'.repeat(64) };
+ assert.deepEqual(parseFrontmatter(renderFrontmatter({ ...metadata, derived_from: source }, '# text')).metadata.derived_from, source);
+});
+
+test('heading units cover parent provisions and ignore fenced examples', () => {
+ const body = example().replace('Alpha.', 'Parent commitment.\n\n### Child\n\n\n```md\n## Unmanaged example\n```');
+ const document = parseDocument(body);
+ assert.equal(document.sections.length, 4);
+ assert.match(document.sections[1].body, /Parent commitment/);
+ assert.equal(document.sections[2].parent, 'example.a');
+ assert.throws(() => parseDocument(example().replace('example.b', 'example.a')), /Duplicate/);
+ assert.throws(() => parseDocument(example().replace('organon.preamble', 'example.intro')), /preamble/);
+ assert.throws(() => parseDocument(example().replace('## A\n', 'A\n---')), /setext/);
+ assert.throws(() => parseDocument(example() + '\n```bad`\n## Hidden\n'), /fence/);
+});
+
+test('hashes normalize line endings only; title and body have separate identities', () => {
+ const a = parseDocument(example()), crlf = parseDocument(example().replaceAll('\n', '\r\n'));
+ assert.deepEqual(a.sections.map(s => s.hash), crlf.sections.map(s => s.hash));
+ assert.notEqual(a.content_hash, crlf.content_hash);
+ const space = parseDocument(example().replace('Alpha.', 'Alpha. '));
+ assert.notEqual(a.sections[1].hash, space.sections[1].hash);
+ const renamed = parseDocument(example().replace('## A', '## Renamed'));
+ assert.equal(a.sections[1].hash, renamed.sections[1].hash);
+ assert.notEqual(a.structure_hash, renamed.structure_hash);
+});
+
+test('five mutually exclusive statuses cover presence and deletion combinations', () => {
+ const cases = [
+ ['a', 'a', 'a', 'unchanged'], ['a', 'b', 'b', 'converged'],
+ ['a', 'a', 'b', 'theirs-changed'], ['a', 'b', 'a', 'ours-changed'], ['a', 'b', 'c', 'conflict'],
+ [null, null, 'b', 'theirs-changed'], [null, 'a', 'b', 'conflict'],
+ ['a', null, 'a', 'ours-changed'], ['a', 'b', null, 'conflict'], ['a', null, null, 'converged'], [null, 'a', null, 'ours-changed'],
+ ];
+ for (const [b, o, t, expected] of cases) assert.equal(statusOf(b, o, t), expected);
+});
+
+test('init creates a regular managed copy, lineage and empty Extensions; never overwrites', t => {
+ const ctx = managed(t, { git: true });
+ const document = parseDocument(read(ctx.ours)), lock = json(lockPath(ctx.ours));
+ assert.equal(document.sections.at(-1).id, 'extensions');
+ for (const section of parseDocument(read(ctx.source)).sections) {
+ assert.equal(document.sections.find(s => s.id === section.id).hash, section.hash);
+ }
+ assert.equal(document.metadata.derived_from.content_hash, digestFile(ctx.source));
+ assert.equal(lock.sources[CORE_ID].sections['extensions'], undefined);
+ assert.equal(check(ctx.ours).mode, 'managed');
+ assert.throws(() => initialize({ target: ctx.ours, source: ctx.source }), /already exists/);
+ write(ctx.ours, read(ctx.ours).replace('Alpha.', 'Locally revised.'));
+ assert.equal(check(ctx.ours).mode, 'managed');
+});
+
+test('resolution uses repository root then cwd and never falls back after explicit failure', t => {
+ const ctx = managed(t, { git: true });
+ const child = path.join(ctx.dir, 'nested'); fs.mkdirSync(child);
+ write(path.join(child, 'PHILOSOPHY.md'), example('Child only.'));
+ assert.equal(resolvePhilosophy({ cwd: child }).realPath, fs.realpathSync(ctx.ours));
+ assert.throws(() => resolvePhilosophy({ cwd: child, philosophy: 'missing.md' }), /no Core fallback/);
+ fs.unlinkSync(ctx.ours);
+ assert.equal(resolvePhilosophy({ cwd: child }).realPath, fs.realpathSync(path.join(child, 'PHILOSOPHY.md')));
+ fs.unlinkSync(path.join(child, 'PHILOSOPHY.md'));
+ assert.throws(() => resolvePhilosophy({ cwd: child }), /init/);
+});
+
+test('core merge rejects stale candidate and lock while a valid plan applies exact choices', t => {
+ const ctx = managed(t);
+ write(ctx.source, example('Incoming.').replace('core_version: 0.1.0', 'core_version: 0.1.1'));
+ const report = reportFor(ctx);
+ assert.equal(report.sections.find(s => s.id === 'example.a').status, 'theirs-changed');
+ const candidate = read(ctx.ours).replace('Alpha.', 'Incoming.').replace('core_version: 0.1.0', 'core_version: 0.1.1').replace('philosophy_version: 0.1.0', 'philosophy_version: 1.0.0');
+ const decisions = { sections: { 'example.a': 'theirs' }, structure: 'ours', philosophy_version: '1.0.0' };
+ const files = planFor(ctx, report, candidate, decisions);
+ const oldLock = read(lockPath(ctx.ours));
+ write(lockPath(ctx.ours), `${oldLock}\n`);
+ assert.throws(() => applyPlan(files.plan), /Stale lock/);
+ write(lockPath(ctx.ours), oldLock);
+ write(files.candidate, candidate + '\n');
+ assert.throws(() => applyPlan(files.plan), /Stale candidate/);
+ write(files.candidate, candidate);
+ applyPlan(files.plan);
+ assert.equal(read(ctx.ours), candidate);
+ assert.equal(json(lockPath(ctx.ours)).core_version, '0.1.1');
+ assert.equal(reportFor(ctx).sections.find(s => s.id === 'example.a').status, 'unchanged');
+ assert.throws(() => applyPlan(files.plan), /Stale/);
+});
+
+test('declines survive source checkpoint advance and local edits; changed source reopens proposal', t => {
+ const ctx = managed(t);
+ write(ctx.source, example('First incoming.'));
+ const decisions = { sections: { 'example.a': 'decline' }, structure: 'ours', philosophy_version: '0.1.0' };
+ const files = planFor(ctx, reportFor(ctx), read(ctx.ours), decisions);
+ applyPlan(files.plan);
+ let row = reportFor(ctx).sections.find(s => s.id === 'example.a');
+ assert.equal(row.status, 'ours-changed'); assert.equal(row.suppressed, true);
+ const again = planFor(ctx, reportFor(ctx), read(ctx.ours), { ...decisions, sections: { 'example.a': 'ours' } });
+ applyPlan(again.plan);
+ assert.equal(reportFor(ctx).sections.find(s => s.id === 'example.a').suppressed, true);
+ assert.equal(json(lockPath(ctx.ours)).sources[CORE_ID].sections['example.a'], parseDocument(read(ctx.source)).sections[1].hash);
+ write(ctx.ours, read(ctx.ours).replace('Alpha.', 'A local edit.'));
+ assert.equal(reportFor(ctx).sections.find(s => s.id === 'example.a').suppressed, true);
+ write(ctx.source, example('Second incoming.'));
+ row = reportFor(ctx).sections.find(s => s.id === 'example.a');
+ assert.equal(row.status, 'conflict'); assert.equal(row.suppressed, false);
+});
+
+test('delete/modify is conflict and declined deletion uses explicit absent state', t => {
+ const ctx = managed(t);
+ write(ctx.ours, read(ctx.ours).replace('Beta.', 'Local revision.'));
+ const upstream = parseDocument(read(ctx.source));
+ write(ctx.source, renderFrontmatter(upstream.metadata, upstream.sections.filter(s => s.id !== 'example.b').map(s => s.raw).join('')));
+ const report = reportFor(ctx), row = report.sections.find(s => s.id === 'example.b');
+ assert.equal(row.status, 'conflict'); assert.equal(row.theirs_change, 'deleted');
+ applyPlan(planFor(ctx, report, read(ctx.ours), { sections: { 'example.b': 'decline' }, structure: 'ours', philosophy_version: '0.1.0' }).plan);
+ assert.deepEqual(json(lockPath(ctx.ours)).declined, [{ source_id: CORE_ID, id: 'example.b', incoming_hash: null }]);
+});
+
+test('foreign import is two-way, scopes decline memory and does not advance Core', t => {
+ const ctx = managed(t);
+ write(ctx.source, example('Foreign input.').replace('core_version: 0.1.0', 'core_version: 0.9.0'));
+ const before = json(lockPath(ctx.ours)).sources[CORE_ID];
+ const report = reportFor(ctx, 'urn:example:foreign', 'import');
+ assert.equal(report.mode, 'two-way'); assert.equal(report.sections.find(s => s.id === 'example.a').status, 'different');
+ const decisions = { sections: { 'example.a': 'decline', extensions: 'ours' }, structure: 'ours', philosophy_version: '0.1.0' };
+ applyPlan(planFor(ctx, report, read(ctx.ours), decisions).plan);
+ const lock = json(lockPath(ctx.ours));
+ assert.equal(lock.core_version, '0.1.0'); assert.deepEqual(lock.sources[CORE_ID], before);
+ assert.equal(reportFor(ctx, 'urn:example:foreign', 'import').mode, 'checkpoint');
+ assert.equal(reportFor(ctx, 'urn:example:different', 'import').sections.find(s => s.id === 'example.a').suppressed, false);
+ assert.throws(() => reportFor(ctx, CORE_ID, 'import'), /kind disagree/);
+});
+
+test('verified old source is optional; provenance alone cannot supply base text', t => {
+ const ctx = managed(t), old = path.join(ctx.dir, 'old.md'); write(old, read(ctx.source));
+ write(ctx.source, example('Changed.'));
+ assert.equal(reportFor(ctx).baseTextAvailable, false);
+ assert.equal(classify({ ours: ctx.ours, theirs: ctx.source, sourceId: CORE_ID, kind: 'core', baseFile: old }).baseTextAvailable, true);
+ write(old, read(old) + '\n');
+ assert.throws(() => classify({ ours: ctx.ours, theirs: ctx.source, sourceId: CORE_ID, kind: 'core', baseFile: old }), /does not match/);
+});
+
+test('heading rename, parent and order changes are visible without invented body conflicts', t => {
+ const ctx = managed(t);
+ for (const transformed of [example().replace('## A', '## Renamed'), example().replace('## B', '### B'),
+ renderFrontmatter(metadata, [0, 2, 1].map(i => parseDocument(example()).sections[i].raw).join(''))]) {
+ write(ctx.source, transformed);
+ const report = reportFor(ctx);
+ assert.notEqual(report.structure.ours, report.structure.theirs);
+ assert.equal(report.structure.status, 'conflict'); // ours added Extensions; theirs changed the source structure.
+ assert.equal(report.sections.find(s => s.id === 'example.a').status, 'unchanged');
+ }
+});
+
+test('export excludes marked subtree after renaming/moving and preserves source bytes', t => {
+ const ctx = managed(t);
+ write(ctx.ours, read(ctx.ours).replace('## 4. Extensions', '## Local material') + '\n### Notes\n\n\nPrivate example.\n');
+ const original = read(ctx.ours), full = path.join(ctx.dir, 'full.md'), subset = path.join(ctx.dir, 'subset.md');
+ exportPhilosophy({ source: ctx.ours, out: full });
+ exportPhilosophy({ source: ctx.ours, out: subset, coreOnly: true });
+ assert.match(read(full), /Private example/); assert.doesNotMatch(read(subset), /Private example|id extensions/);
+ assert.equal(parseDocument(read(full)).metadata.derived_from.source_id, json(lockPath(ctx.ours)).document_id);
+ assert.equal(parseDocument(read(full)).metadata.derived_from.content_hash, hash(original));
+ assert.equal(read(ctx.ours), original);
+ assert.throws(() => exportPhilosophy({ source: ctx.source, out: path.join(ctx.dir, 'bad.md'), coreOnly: true, sourceId: 'test' }), /No Extensions/);
+ assert.throws(() => exportPhilosophy({ source: ctx.ours, out: full }), /already exists/);
+});
+
+test('incorrect or incomplete decisions cannot apply a different candidate', t => {
+ const ctx = managed(t); write(ctx.source, example('Incoming.'));
+ const report = reportFor(ctx);
+ assert.throws(() => planFor(ctx, report, read(ctx.ours), { sections: {}, structure: 'ours', philosophy_version: '0.1.0' }), /Missing decision/);
+ assert.throws(() => planFor(ctx, report, read(ctx.ours), { sections: { 'example.a': 'theirs' }, structure: 'ours', philosophy_version: '0.1.0' }), /does not implement theirs/);
+});
+
+test('pair failure restores original file and lock; interrupted writes can be recovered', t => {
+ const ctx = managed(t), original = read(ctx.ours), originalLock = read(lockPath(ctx.ours));
+ const candidate = original.replace('Alpha.', 'Candidate.');
+ assert.throws(() => writePair(ctx.ours, candidate, originalLock, { failAfterFirst: true }), /Injected/);
+ assert.equal(read(ctx.ours), original); assert.equal(read(lockPath(ctx.ours)), originalLock);
+ const pending = pendingPath(ctx.ours);
+ write(pending, { target: ctx.ours, lock: lockPath(ctx.ours), oldDocument: original, oldLock: originalLock, newDocument: candidate, newLock: originalLock });
+ write(ctx.ours, candidate);
+ assert.throws(() => check(ctx.ours), /Interrupted transaction/);
+ recover(ctx.ours);
+ assert.equal(read(ctx.ours), original); assert.equal(fs.existsSync(pending), false);
+});
+
+test('recovery refuses an unrelated concurrent edit and keeps its journal', t => {
+ const ctx = managed(t), original = read(ctx.ours), oldLock = read(lockPath(ctx.ours)), pending = pendingPath(ctx.ours);
+ write(pending, { target: ctx.ours, lock: lockPath(ctx.ours), oldDocument: original, oldLock, newDocument: original + '\n', newLock: oldLock });
+ write(ctx.ours, original.replace('Alpha.', 'Concurrent work.'));
+ assert.throws(() => recover(ctx.ours), /changed outside/);
+ assert.match(read(ctx.ours), /Concurrent work/); assert.equal(fs.existsSync(pending), true);
+});
+
+test('all write operations reject symlinks and protected Core targets without changing either', t => {
+ const ctx = managed(t), mirror = path.join(ctx.dir, 'mirror.md'), before = digestFile(CORE_FILE);
+ fs.symlinkSync(CORE_FILE, mirror);
+ for (const target of [mirror, path.join(ROOT, 'PHILOSOPHY.md'), CORE_FILE]) {
+ assert.throws(() => initialize({ target, source: ctx.source }), /symlink|protected source/);
+ assert.throws(() => exportPhilosophy({ source: ctx.ours, out: target }), /symlink|protected source/);
+ assert.throws(() => writePair(target, example(), '{}'), /symlink|protected source/);
+ }
+ const report = reportFor(ctx);
+ const files = planFor(ctx, report, read(ctx.ours), { sections: {}, structure: 'ours', philosophy_version: '0.1.0' });
+ fs.unlinkSync(ctx.ours); fs.symlinkSync(CORE_FILE, ctx.ours);
+ assert.throws(() => applyPlan(files.plan), /Stale|symlink/);
+ assert.equal(digestFile(CORE_FILE), before);
+ assert.equal(fs.lstatSync(mirror).isSymbolicLink(), true);
+ assert.equal(fs.lstatSync(path.join(ROOT, 'PHILOSOPHY.md')).isSymbolicLink(), true);
+});
+
+test('CLI errors return nonzero JSON and source check is executable', () => {
+ const success = spawnSync(process.execPath, ['scripts/check.js', '--source', 'PHILOSOPHY.md'], { cwd: ROOT, encoding: 'utf8' });
+ assert.equal(success.status, 0, success.stderr); assert.equal(JSON.parse(success.stdout).sections, 22);
+ const bad = spawnSync(process.execPath, ['scripts/classify.js', '--unknown'], { cwd: ROOT, encoding: 'utf8' });
+ assert.equal(bad.status, 1); assert.match(JSON.parse(bad.stderr).error, /Unknown/);
+});
+
+test('unsupported ATX spellings cannot silently hide unnumbered sections', () => {
+ for (const heading of ['##', '##\tHidden', ' ## Indented']) {
+ assert.throws(() => parseDocument(example() + `\n${heading}\nBody.\n`), /ATX/);
+ }
+});
+
+test('pending transaction detection uses the physical target across parent aliases', t => {
+ const ctx = managed(t), alias = path.join(ctx.dir, 'alias');
+ fs.symlinkSync(ctx.dir, alias);
+ const aliasFile = path.join(alias, 'PHILOSOPHY.md');
+ assert.equal(pendingPath(aliasFile), pendingPath(ctx.ours));
+ const old = read(ctx.ours), oldLock = read(lockPath(ctx.ours)), pending = pendingPath(ctx.ours);
+ const report = reportFor(ctx);
+ const files = planFor(ctx, report, old, { sections: {}, structure: 'ours', philosophy_version: '0.1.0' });
+ write(pending, { target: ctx.ours, lock: lockPath(ctx.ours), oldDocument: old, oldLock, newDocument: old, newLock: oldLock });
+ assert.throws(() => check(aliasFile), /Interrupted/);
+ assert.throws(() => classify({ ours: aliasFile, theirs: ctx.source, sourceId: CORE_ID, kind: 'core' }), /Interrupted/);
+ assert.throws(() => applyPlan(files.plan), /Interrupted/);
+ recover(aliasFile);
+ assert.equal(read(ctx.ours), old);
+});
+
+test('interrupted sources cannot enter export, init or another merge', t => {
+ const ctx = managed(t), otherDir = fixture(t), other = path.join(otherDir, 'PHILOSOPHY.md');
+ initialize({ target: other, source: ctx.source, sourceId: CORE_ID });
+ const old = read(ctx.ours), oldLock = read(lockPath(ctx.ours));
+ write(pendingPath(ctx.ours), { target: ctx.ours, lock: lockPath(ctx.ours), oldDocument: old, oldLock, newDocument: old, newLock: oldLock });
+ assert.throws(() => exportPhilosophy({ source: ctx.ours, out: path.join(ctx.dir, 'export.md') }), /Interrupted/);
+ assert.throws(() => initialize({ target: path.join(fixture(t), 'new.md'), source: ctx.ours, sourceId: 'custom' }), /Interrupted/);
+ assert.throws(() => classify({ ours: other, theirs: ctx.ours, sourceId: 'foreign', kind: 'import' }), /Interrupted/);
+});
+
+test('a sibling document cannot borrow the managed identity from an unrelated sidecar', t => {
+ const ctx = managed(t), out = path.join(ctx.dir, 'export.md');
+ assert.throws(() => exportPhilosophy({ source: ctx.source, out }), /Provide --source-id/);
+ assert.throws(() => check(ctx.source), /different document/);
+ assert.throws(() => initialize({ source: ctx.source, target: path.join(fixture(t), 'PHILOSOPHY.md') }), /Provide --source-id/);
+});
+
+test('input validation runs under the exclusive transaction before any document write', t => {
+ const ctx = managed(t), old = read(ctx.ours), oldLock = read(lockPath(ctx.ours));
+ assert.throws(() => writePair(ctx.ours, old + '\n', oldLock, {
+ verifyInputs() {
+ assert.equal(fs.existsSync(pendingPath(ctx.ours)), true);
+ write(ctx.ours, old.replace('Alpha.', 'Independent update.'));
+ throw new Error('Stale input after acquiring transaction');
+ },
+ }), /Stale input/);
+ assert.match(read(ctx.ours), /Independent update/);
+ assert.equal(read(lockPath(ctx.ours)), oldLock);
+ assert.equal(fs.existsSync(pendingPath(ctx.ours)), false);
+});
+
+test('version warnings respect the manifest range without becoming format judgments', () => {
+ assert.deepEqual(coreWarnings('0.1.99'), []);
+ assert.equal(coreWarnings('0.2.0').length, 1);
+ assert.deepEqual(coreWarnings('3.4.2', '>=3.4.0 <4.0.0'), []);
+ assert.equal(coreWarnings('3.3.99', '>=3.4.0 <4.0.0').length, 1);
+ assert.throws(() => coreWarnings('0.1.0', '^0.1.0'), /Unsupported/);
+});
+
+test('each reviewed input is bound, including source, current text, decisions and record', t => {
+ const ctx = managed(t);
+ const report = reportFor(ctx);
+ const files = planFor(ctx, report, read(ctx.ours), { sections: {}, structure: 'ours', philosophy_version: '0.1.0' });
+ for (const [label, file] of [['ours', ctx.ours], ['theirs', ctx.source], ['decisions', files.decisions], ['record', files.record]]) {
+ const original = read(file); write(file, original + '\n');
+ assert.throws(() => applyPlan(files.plan), new RegExp(`Stale ${label}`));
+ write(file, original);
+ }
+});
diff --git a/tests/package.test.mjs b/tests/package.test.mjs
new file mode 100644
index 0000000..5d10e61
--- /dev/null
+++ b/tests/package.test.mjs
@@ -0,0 +1,69 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+import { gzipSync, gunzipSync } from 'node:zlib';
+import { buildPackage } from '../scripts/package/build.mjs';
+import { checkPackage } from '../scripts/package/check.mjs';
+import { extractTarball } from '../installer/lib/archive.mjs';
+import { verifyPackage, openPackage } from '../installer/lib/package.mjs';
+
+test('release payloads are reproducible, symlink-free, complete and omit Lean/docs/tests', t => {
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'organon-package-test-'));
+ t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
+ const installerHashes = [];
+ for (const product of ['agent-organon', 'core', 'advised']) {
+ const first = buildPackage({ product, out: directory });
+ const second = buildPackage({ product, out: path.join(directory, 'second') });
+ assert.equal(first.sha256, second.sha256);
+ assert.equal(checkPackage(first.tarball).passed, true);
+ const extracted = extractTarball(first.tarball, path.join(directory, product));
+ const manifest = verifyPackage(extracted);
+ assert.ok(manifest.files.every(entry => !entry.path.includes('/lean/') && !entry.path.includes('/docs/')));
+ const mirror = manifest.files.find(entry => entry.path === 'payload/PHILOSOPHY.md');
+ assert.ok(mirror.source.transformations.includes('materialize-symbolic-link'));
+ const metadata = JSON.parse(fs.readFileSync(path.join(extracted, 'package.json')));
+ assert.equal(Boolean(metadata.bin), product === 'agent-organon');
+ installerHashes.push(manifest.files.filter(entry => entry.path.startsWith('installer/')).map(entry => [entry.path, entry.sha256]));
+ fs.appendFileSync(path.join(extracted, 'payload/PHILOSOPHY.md'), '\nTampered\n');
+ assert.throws(() => verifyPackage(extracted), /integrity/);
+ }
+ assert.deepEqual(installerHashes[0], installerHashes[1]); assert.deepEqual(installerHashes[1], installerHashes[2]);
+});
+
+test('a bundled installer never fetches another product implicitly', async t => {
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'organon-offline-test-'));
+ t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
+ const archive = buildPackage({ product: 'core', out: directory }).tarball;
+ const bundled = extractTarball(archive, path.join(directory, 'extracted'));
+ const originalFetch = globalThis.fetch;
+ let called = false;
+ globalThis.fetch = () => { called = true; throw new Error('Unexpected network request'); };
+ try {
+ const local = await openPackage({ product: 'core', bundled });
+ assert.equal(local.manifest.product, 'core'); local.close();
+ await assert.rejects(openPackage({ product: 'agent-organon', bundled }), /exact --version/);
+ assert.equal(called, false);
+ } finally { globalThis.fetch = originalFetch; }
+});
+
+test('archive extraction rejects traversal, links and checksum tampering before writing', t => {
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'organon-archive-test-'));
+ t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
+ const archive = buildPackage({ product: 'core', out: directory }).tarball;
+ for (const mutation of ['traversal', 'link', 'checksum']) {
+ const bytes = gunzipSync(fs.readFileSync(archive));
+ if (mutation === 'traversal') { bytes.fill(0, 0, 100); bytes.write('package/../../escape', 0); }
+ if (mutation === 'link') bytes.write('2', 156);
+ if (mutation !== 'checksum') {
+ bytes.fill(32, 148, 156);
+ const sum = bytes.subarray(0, 512).reduce((total, byte) => total + byte, 0);
+ bytes.write(`${sum.toString(8).padStart(6, '0')}\0 `, 148);
+ } else bytes[10] ^= 1;
+ const bad = path.join(directory, `${mutation}.tgz`); fs.writeFileSync(bad, gzipSync(bytes));
+ const destination = path.join(directory, mutation);
+ assert.throws(() => extractTarball(bad, destination), /Unsafe|Unsupported|checksum/);
+ assert.ok(!fs.existsSync(destination));
+ }
+});
diff --git a/tests/release-attempts.test.mjs b/tests/release-attempts.test.mjs
new file mode 100644
index 0000000..f9e6555
--- /dev/null
+++ b/tests/release-attempts.test.mjs
@@ -0,0 +1,41 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { assertTrustedRun, assertArtifactIdentity, selectAttemptArtifacts, resolveAttempt } from '../scripts/release/actions-artifacts.mjs';
+import { AGENTS } from '../scripts/release/lib.mjs';
+const run = (workflow = 'candidate.yml', attempt = 2) => ({ id: 10, run_attempt: attempt, head_sha: 'a'.repeat(40), repository: { full_name: 'shendeguize/AgentOrganon' }, head_repository: { full_name: 'shendeguize/AgentOrganon' }, path: '.github/workflows/' + workflow, event: 'workflow_dispatch', conclusion: 'success' });
+const artifact = (name, id = 100) => ({ id, name, expired: false, digest: 'sha256:' + 'b'.repeat(64), workflow_run: { id: 10, head_sha: 'a'.repeat(40) } });
+const candidate = attempt => [`candidate-packages-${attempt}`, ...['Linux', 'macOS', 'Windows'].map(os => `install-${os}-${attempt}`)].map((name, i) => artifact(name, attempt * 100 + i));
+
+test('complete latest candidate attempt selects exact IDs and never borrows old successful assets', () => {
+ const current = candidate(2), all = [...candidate(1), ...current];
+ assert.deepEqual(selectAttemptArtifacts(run(), all, 'candidate.yml'), current);
+ for (const missing of current) assert.throws(() => selectAttemptArtifacts(run(), all.filter(a => a !== missing), 'candidate.yml'), /Rerun all jobs/);
+ assert.throws(() => selectAttemptArtifacts(run(), candidate(1), 'candidate.yml'), /Rerun all jobs/);
+});
+test('a full or smoke agent attempt must be complete even when earlier attempts succeeded', () => {
+ for (const matrix of ['smoke', 'full']) {
+ const names = (attempt) => AGENTS.flatMap(agent => (matrix === 'full' ? ['Linux'] : ['Linux', 'macOS', 'Windows']).map(os => `agent-${agent}-${os}-${matrix}-${attempt}`));
+ const current = names(2).map((name, i) => artifact(name, 100 + i));
+ const old = names(1).map((name, i) => artifact(name, 200 + i));
+ assert.deepEqual(selectAttemptArtifacts(run('agent-e2e.yml'), [...old, ...current], 'agent-e2e.yml'), current);
+ assert.throws(() => selectAttemptArtifacts(run('agent-e2e.yml'), [...old, ...current.slice(1)], 'agent-e2e.yml'), /Rerun all jobs/);
+ }
+});
+test('seal artifacts are selected by current attempt and original IDs stay distinct', () => {
+ const old = artifact('validated-release-1', 10), current = artifact('validated-release-2', 20);
+ assert.deepEqual(selectAttemptArtifacts(run('seal.yml'), [old, current], 'seal.yml'), [current]);
+ assert.throws(() => selectAttemptArtifacts(run('seal.yml'), [old], 'seal.yml'), /Rerun all jobs/);
+});
+test('expired, duplicate, unrelated and changed artifact identities are rejected', () => {
+ const r = run(), current = candidate(2);
+ for (const change of [{ expired: true }, { id: null }, { digest: null }, { workflow_run: { id: 11, head_sha: r.head_sha } }]) assert.throws(() => assertArtifactIdentity({ ...current[0], ...change }, r));
+ for (const change of [{ id: 999 }, { name: 'changed' }, { digest: 'sha256:' + 'c'.repeat(64) }]) assert.throws(() => assertArtifactIdentity({ ...current[0], ...change }, r, current[0]), /changed/);
+ assert.throws(() => selectAttemptArtifacts(r, [...current, current[0]], 'candidate.yml'), /Rerun all jobs/);
+});
+test('resolve verifies current run before selecting paginated immutable artifacts', () => {
+ const calls = [], current = candidate(2), r = run();
+ const api = ([route]) => { calls.push(route); if (!route.includes('artifacts?')) return r; return route.endsWith('page=1') ? { total_count: 8, artifacts: candidate(1) } : { total_count: 8, artifacts: current }; };
+ assert.deepEqual(resolveAttempt('10', 'candidate.yml', r.head_sha, api).artifacts, current);
+ assert.equal(calls.length, 3);
+ for (const change of [{ run_attempt: undefined }, { conclusion: 'failure' }, { path: '.github/workflows/ci.yml' }, { head_sha: 'c'.repeat(40) }]) assert.throws(() => assertTrustedRun({ ...r, ...change }, 'candidate.yml', r.head_sha));
+});
diff --git a/tests/release-evidence.test.mjs b/tests/release-evidence.test.mjs
new file mode 100644
index 0000000..9c6b192
--- /dev/null
+++ b/tests/release-evidence.test.mjs
@@ -0,0 +1,58 @@
+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 { buildPackage } from '../scripts/package/build.mjs';
+import { checkPackage } from '../scripts/package/check.mjs';
+import { extractTarball, createTarball } from '../installer/lib/archive.mjs';
+import { validateReleasePackages, sourceIdentity, validateEvidence } from '../scripts/release/manifest.mjs';
+import { PRODUCTS, sha256, digest } from '../scripts/release/lib.mjs';
+import { outputFile } from '../scripts/agents/core.mjs';
+import { collectEvidenceClosure } from '../scripts/release/evidence.mjs';
+
+function temp(t) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'organon-evidence-unit-')); t.after(() => fs.rmSync(root, { force: true, recursive: true })); return root; }
+
+test('release verifies real archive product, version and all three source snapshots', t => {
+ const root = temp(t), artifacts = {}, snapshots = {}, fixtureVersion = '1.0.0-rc.1';
+ for (const product of Object.keys(PRODUCTS)) {
+ const built = buildPackage({ product, out: root, version: fixtureVersion });
+ artifacts[product] = { file: path.basename(built.tarball), sha256: built.sha256 };
+ Object.assign(snapshots, checkPackage(built.tarball).snapshots);
+ }
+ const m = { schema_version: 1, version: fixtureVersion, channel: 'rc', state: 'prepared', sources: Object.fromEntries(Object.entries(PRODUCTS).map(([product, p]) => [product, { repository: 'shendeguize/' + p.repo, commit: snapshots[p.repo], dirty: false }])), artifacts, evidence: [] };
+ m.source_digest = sourceIdentity(m);
+ assert.equal(validateReleasePackages(m, root), true);
+ const swapped = structuredClone(m); swapped.artifacts.core = swapped.artifacts.advised;
+ assert.throws(() => validateReleasePackages(swapped, root), /product\/version/);
+ const wrongVersion = structuredClone(m); wrongVersion.version = '1.0.0-rc.2'; wrongVersion.source_digest = sourceIdentity(wrongVersion);
+ assert.throws(() => validateReleasePackages(wrongVersion, root), /product\/version/);
+ const wrongSource = structuredClone(m); wrongSource.sources.core.commit = '0'.repeat(40); wrongSource.source_digest = sourceIdentity(wrongSource);
+ assert.throws(() => validateReleasePackages(wrongSource, root), /snapshots differ/);
+ const text = outputFile(root, 'not-an-archive.tgz', 'Checksummed text is not a release package.');
+ const invalid = structuredClone(m); invalid.artifacts.core = text;
+ assert.throws(() => validateReleasePackages(invalid, root));
+ const unpacked = extractTarball(path.join(root, artifacts.core.file), path.join(root, 'unpacked'));
+ const manifestFile = path.join(unpacked, 'organon-content.json'), content = JSON.parse(fs.readFileSync(manifestFile));
+ content.files.find(file => file.source).source.revision = 'f'.repeat(40);
+ fs.writeFileSync(manifestFile, JSON.stringify(content));
+ assert.throws(() => checkPackage(unpacked), /provenance differs/);
+});
+
+test('opaque passed summaries cannot replace independently bound actual evidence', t => {
+ const root = temp(t);
+ const m = { schema_version: 1, version: '1.0.0-rc.1', channel: 'rc', state: 'prepared', sources: Object.fromEntries(Object.entries(PRODUCTS).map(([product, p]) => [product, { repository: 'shendeguize/' + p.repo, commit: '1'.repeat(40), dirty: false }])), artifacts: {}, evidence: [] };
+ m.source_digest = sourceIdentity(m);
+ const fake = outputFile(root, 'fabricated.json', { gate: 'agents', status: 'passed', source_digest: m.source_digest, artifact_digest: digest(m.artifacts), agent: 'codex', platform: 'linux', matrix: 'smoke', actual_call: true, independent_reviewer: true, model: 'unknown', tool_version: 'unknown' });
+ m.evidence = [fake]; assert.throws(() => validateEvidence(m, root), /Invalid evidence reference/);
+});
+
+test('closure rejects changed nested objects and conflicting same-path identities', t => {
+ const root = temp(t), raw = outputFile(root, 'raw.txt', 'frozen');
+ const report = outputFile(root, 'report.json', { nested: { raw } });
+ assert.equal(collectEvidenceClosure({ evidence: [report] }, root).length, 2);
+ const conflict = outputFile(root, 'conflict.json', { a: raw, b: { file: raw.file, sha256: '0'.repeat(64) } });
+ assert.throws(() => collectEvidenceClosure({ evidence: [conflict] }, root), /Conflicting/);
+ fs.rmSync(path.join(root, 'raw.txt'));
+ assert.throws(() => collectEvidenceClosure({ evidence: [report] }, root));
+});
diff --git a/tests/release-github-gate.test.mjs b/tests/release-github-gate.test.mjs
new file mode 100644
index 0000000..0414863
--- /dev/null
+++ b/tests/release-github-gate.test.mjs
@@ -0,0 +1,62 @@
+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 { PRODUCTS, digest, sha256 } from '../scripts/release/lib.mjs';
+import { sourceIdentity, assertGithubReport, validateEvidence } from '../scripts/release/manifest.mjs';
+import { collectGithubReport } from '../scripts/release/candidate.mjs';
+import { recheckReleaseGovernance, withReleaseGovernance, verifyPublished } from '../scripts/release/publish.mjs';
+const candidate = () => {
+ const manifest = { schema_version: 1, version: '1.0.0-rc.1', channel: 'rc', state: 'prepared', artifacts: {}, evidence: [], sources: Object.fromEntries(Object.entries(PRODUCTS).map(([key, spec]) => [key, { repository: `shendeguize/${spec.repo}`, commit: 'a'.repeat(40), dirty: false }])) };
+ manifest.source_digest = sourceIdentity(manifest); return manifest;
+};
+const passing = repository => ({ status: 'passed', repository, rulesets: [{ id: 12, name: 'Unit fixture', bypass_actors: [] }], missing: [], security: { status: 'passed', repository, environments: [{ name: 'npm-stable', owner_approval: true }], actions: { sha_pinning_required: true }, workflow: { default_workflow_permissions: 'read' } } });
+
+test('candidate report preserves full governance and security observations for the exact product', async () => {
+ const original = passing('shendeguize/OrganonCore');
+ const result = await collectGithubReport('core', async repository => { assert.equal(repository, original.repository); return original; });
+ assert.deepEqual(result, { ...original, gate: 'github', product: 'core' });
+ assert.deepEqual(result.security.environments, original.security.environments);
+});
+test('missing, failed and cross-repository security cannot become a passed GitHub gate', async () => {
+ const valid = passing('shendeguize/OrganonCore');
+ const changes = [{ security: undefined }, { security: { ...valid.security, status: 'failed' } }, { security: { ...valid.security, repository: 'shendeguize/AdvisedOrganons' } }, { status: 'failed' }, { repository: 'shendeguize/AdvisedOrganons' }];
+ for (const change of changes) {
+ const report = { ...valid, ...change };
+ assert.throws(() => assertGithubReport(report, 'core'), /governance\/security/);
+ await assert.rejects(() => collectGithubReport('core', async () => report), /governance\/security/);
+ }
+ await assert.rejects(() => collectGithubReport('core', async () => { throw new Error('Missing audit credential'); }), /credential/);
+});
+test('manifest validation rejects historical ruleset-only reports despite a valid checksum', t => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'organon-github-gate-')); t.after(() => fs.rmSync(root, { recursive: true, force: true }));
+ for (const security of [undefined, { status: 'failed', repository: 'shendeguize/OrganonCore' }, { status: 'passed', repository: 'other/repo' }]) {
+ const manifest = candidate(), report = { ...passing('shendeguize/OrganonCore'), security, gate: 'github', product: 'core', source_digest: manifest.source_digest, artifact_digest: digest(manifest.artifacts) };
+ const file = path.join(root, 'github.json'); fs.writeFileSync(file, JSON.stringify(report)); manifest.evidence = [{ file: 'github.json', sha256: sha256(fs.readFileSync(file)) }];
+ assert.throws(() => validateEvidence(manifest, root), /governance\/security/);
+ }
+});
+test('every operation freshly audits all three repositories before its callback', async () => {
+ const manifest = candidate(), order = [];
+ const inspect = async repository => { order.push(repository); return passing(repository); };
+ await withReleaseGovernance(manifest, reports => { assert.equal(reports.length, 3); order.push('mutation'); }, inspect);
+ assert.deepEqual(order, [...Object.values(manifest.sources).map(source => source.repository), 'mutation']);
+ order.length = 0;
+ await withReleaseGovernance(manifest, () => order.push('mutation'), inspect);
+ assert.equal(order.length, 4);
+});
+test('later governance drift or credential failure blocks publication instead of reusing old success', async () => {
+ const manifest = candidate(); let changed = false, mutations = 0;
+ const inspect = async repository => { const report = passing(repository); if (changed && repository.endsWith('/AdvisedOrganons')) report.security.status = 'failed'; return report; };
+ await recheckReleaseGovernance(manifest, inspect); changed = true;
+ await assert.rejects(() => withReleaseGovernance(manifest, () => mutations++, inspect), /governance\/security/);
+ await assert.rejects(() => withReleaseGovernance(manifest, () => mutations++, async () => { throw new Error('Audit API denied'); }), /denied/);
+ assert.equal(mutations, 0);
+});
+test('dual-channel verification checks all three governance states before artifact or registry access', async () => {
+ const manifest = candidate(), visited = [];
+ // Missing local directory/packages would fail later. Audit failure must win before any network/package access.
+ await assert.rejects(() => verifyPublished(manifest, '/missing-unit-fixture', ['core'], { inspect: async repository => { visited.push(repository); const report = passing(repository); if (repository.endsWith('/AgentOrganon')) report.security.status = 'failed'; return report; } }), /governance\/security/);
+ assert.deepEqual(visited, Object.values(manifest.sources).map(source => source.repository));
+});
diff --git a/tests/release-site-data.test.mjs b/tests/release-site-data.test.mjs
new file mode 100644
index 0000000..23712b9
--- /dev/null
+++ b/tests/release-site-data.test.mjs
@@ -0,0 +1,85 @@
+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 { PRODUCTS, digest, writeJSON } from '../scripts/release/lib.mjs';
+import { sourceIdentity } from '../scripts/release/manifest.mjs';
+import { assertSiteRunner, inventory, validateState, sampleStars, installPublic, buildIdentity, verifyForSite, checkoutState } from '../scripts/release/site-data.mjs';
+const repository = 'shendeguize/AgentOrganon';
+function temporary(t) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'organon-site-state-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); return root; }
+function manifest(version = '1.0.0-rc.1') {
+ const value = { schema_version: 1, version, channel: 'rc', state: 'validated', sources: Object.fromEntries(Object.entries(PRODUCTS).map(([key, p]) => [key, { repository: `shendeguize/${p.repo}`, commit: 'a'.repeat(40), dirty: false }])), evidence: [], artifacts: {} };
+ value.source_digest = sourceIdentity(value); return value;
+}
+function built(t) { const root = temporary(t); fs.mkdirSync(path.join(root, 'zh')); fs.writeFileSync(path.join(root, 'index.html'), 'English released HTML'); fs.writeFileSync(path.join(root, 'zh/index.html'), '中文发行页面'); return root; }
+const api = total => async () => ({ ok: true, json: async () => ({ stargazers_count: total }) });
+test('site writes reject local, foreign, PR and self-hosted contexts', () => {
+ const env = { GITHUB_ACTIONS: 'true', RUNNER_ENVIRONMENT: 'github-hosted', GITHUB_REPOSITORY: repository, GITHUB_REF: 'refs/heads/main', GITHUB_EVENT_NAME: 'workflow_dispatch', RUNNER_TEMP: '/tmp', GITHUB_TOKEN: 'fixture' };
+ assert.doesNotThrow(() => assertSiteRunner(env, 'pages'));
+ for (const key of Object.keys(env)) assert.throws(() => assertSiteRunner({ ...env, [key]: '' }, 'pages'));
+ assert.throws(() => assertSiteRunner({ ...env, GITHUB_EVENT_NAME: 'schedule' }, 'pages'));
+ assert.doesNotThrow(() => assertSiteRunner({ ...env, GITHUB_EVENT_NAME: 'schedule' }, 'stars'));
+});
+test('first actual sample does not create or deploy an unreleased site', async t => {
+ const root = temporary(t);
+ assert.equal(await sampleStars(root, repository, api(0), new Date('2026-09-15T00:00:00Z')), false);
+ assert.equal(validateState(root, repository), null); assert.equal(fs.existsSync(path.join(root, 'public')), false);
+ const before = fs.readFileSync(path.join(root, 'stars.json'));
+ await assert.rejects(sampleStars(root, repository, async () => ({ ok: false, status: 403 }), new Date('2026-09-16T00:00:00Z')), /state unchanged/);
+ assert.deepEqual(fs.readFileSync(path.join(root, 'stars.json')), before);
+ await assert.rejects(sampleStars(root, repository, api(undefined), new Date('2026-09-16T00:00:00Z')), /Invalid GitHub/);
+ assert.deepEqual(fs.readFileSync(path.join(root, 'stars.json')), before);
+});
+test('daily sampling preserves exact released HTML while retaining decreases and gaps', async t => {
+ const root = temporary(t), source = built(t), release = manifest();
+ installPublic(root, repository, release, source, '2026-09-14T00:00:00Z');
+ const before = inventory(path.join(root, 'public')), receipt = fs.readFileSync(path.join(root, 'recommended-release.json'));
+ for (const [day, count] of [[15, 3], [16, 0], [19, 1]]) assert.equal(await sampleStars(root, repository, api(count), new Date(`2026-09-${day}T00:00:00Z`)), true);
+ assert.deepEqual(inventory(path.join(root, 'public')), before); assert.deepEqual(fs.readFileSync(path.join(root, 'recommended-release.json')), receipt);
+ const points = JSON.parse(fs.readFileSync(path.join(root, 'public/assets/stars.json'))).observations;
+ assert.deepEqual(points.map(p => p.total), [3,0,1]);
+ assert.equal((fs.readFileSync(path.join(root, 'public/assets/stars.svg'), 'utf8').match(/ observe(history, -1));
+ assert.throws(() => observe(history, 0, new Date('2026-09-18T00:00:00Z')));
+});
+test('star gaps follow UTC calendar dates rather than elapsed hours', () => {
+ for (const [dates, segments] of [
+ [['2026-09-14T23:30:00Z', '2026-09-16T00:30:00Z'], 0],
+ [['2026-09-14T00:01:00Z', '2026-09-15T23:59:00Z'], 1],
+ ]) {
+ const observations = dates.map((date, total) => ({ observed_at: new Date(date).toISOString(), total }));
+ const svg = renderSVG({ schema_version: 1, repository: 'shendeguize/AgentOrganon', observations });
+ assert.equal((svg.match(/