diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index c9fba33..fded20e 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -9,21 +9,29 @@ on: types: [opened, synchronize, reopened, edited] push: branches: [main] + workflow_call: + inputs: + ref: + description: Commit to gate. upstream-sync passes its sync commit and merges only after this run passes. + type: string + required: true jobs: gate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} - uses: oven-sh/setup-bun@v2 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Shell syntax # One bash -n per file, in a loop. `bash -n a.sh b.sh` parses a.sh and passes b.sh through as # a positional parameter, so the single-command form checks exactly one file and reports - # success for the rest, which is worse than no step: it looks like four scripts are covered. + # success for the rest, which is worse than no step: it looks like every script is covered. run: | - for f in omp-port/*.sh; do + for f in $(git ls-files '*.sh'); do echo "::group::$f" bash -n "$f" echo "::endgroup::" @@ -41,26 +49,29 @@ jobs: run: bun run typecheck - name: Extension tests working-directory: plugins/pstack - run: bun test ./extensions/potetomode/index.test.js ./extensions/pstackpolicy/index.test.js + run: bun test ./extensions smoke: - # A real omp loads plugins/pstack: every skill directory must register as a command, and the - # pin extension must record and reload its state. omp-port/smoke-plugin.mjs names each check. + # A real omp loads plugins/pstack through -e, `omp plugin link`, and a local marketplace install, + # and runs scripted turns: every skill registers, the pinned prompt names poteto-mode, and + # pstackpolicy refuses a merge before gh runs. omp-port/smoke-plugin.mjs names each check. runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} - uses: oven-sh/setup-bun@v2 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Install pinned omp - # can1357/oh-my-pi release v18.12.6, asset omp-linux-x64, sha256 - # 85d9daf3a05fc76e8cd425ae1904eb576af8d21cc85fcf4690742626e07ccfa4. That is the digest - # `gh api repos/can1357/oh-my-pi/releases/latest` reports for the asset and the sha256 of the - # downloaded file, checked 2026-10-22. A bump changes the tag and the digest together. + # can1357/oh-my-pi release v18.14.1, asset omp-linux-x64, sha256 + # 02d76a0cfeee473ffa387f50cf0378b0de1c44684c83e67ca27ba4d717cfb0d0. That is the digest + # `gh release view v18.14.1 -R can1357/oh-my-pi --json assets` reports for the asset and the + # sha256 of the downloaded file, checked 2026-10-31. A bump changes the tag and the digest together. env: - OMP_TAG: v18.12.6 - OMP_SHA256: 85d9daf3a05fc76e8cd425ae1904eb576af8d21cc85fcf4690742626e07ccfa4 + OMP_TAG: v18.14.1 + OMP_SHA256: 02d76a0cfeee473ffa387f50cf0378b0de1c44684c83e67ca27ba4d717cfb0d0 run: | dir="$RUNNER_TEMP/omp" mkdir -p "$dir" @@ -73,3 +84,22 @@ jobs: run: OMP_SMOKE_REQUIRED=1 bun omp-port/smoke-plugin.mjs - name: Worktree audit against live omp run: OMP_AUDIT_LIVE_REQUIRED=1 bash omp-port/test-worktree-audit.sh + + statechart: + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + working-directory: plugins/pstack/skills/statechart/scripts + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + - uses: actions/setup-node@v7 + with: + node-version: 22 + - name: Install test dependencies + run: npm install --no-package-lock + - name: Statechart tests + # The cases that need TLC skip, because the runner sets no TLA2TOOLS. + run: npm test diff --git a/.github/workflows/upstream-sync.yml b/.github/workflows/upstream-sync.yml index 4dc7646..910d858 100644 --- a/.github/workflows/upstream-sync.yml +++ b/.github/workflows/upstream-sync.yml @@ -20,10 +20,13 @@ permissions: jobs: sync: runs-on: ubuntu-latest + outputs: + pr: ${{ steps.build.outputs.pr }} + sha: ${{ steps.build.outputs.sha }} env: GH_TOKEN: ${{ github.token }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 @@ -70,7 +73,8 @@ jobs: TARGET: ${{ steps.plan.outputs.target }} run: bash omp-port/check-port.sh "" "$TARGET" - - if: steps.plan.outputs.go == 'yes' + - id: build + if: steps.plan.outputs.go == 'yes' env: TARGET: ${{ steps.plan.outputs.target }} SHORT: ${{ steps.plan.outputs.short }} @@ -106,8 +110,29 @@ jobs: pr=$(gh pr create --base main --head "sync/upstream-$SHORT" \ --title "sync: upstream pstack $SHORT ($version)" --body-file "$RUNNER_TEMP/body.md" | tail -n1) if [ "$code" -eq 0 ] && [ "$gate" -eq 0 ]; then - gh pr merge "$pr" --squash --delete-branch + printf 'pr=%s\nsha=%s\n' "$pr" "$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" exit 0 fi echo "left $pr open for a human: sync exit $code, gate exit $gate" exit 1 + + # A merge made with github.token starts no push run on main, so this job is the only full gate a + # sync gets. A red gate skips the merge and leaves the PR open for a human. + gate: + needs: sync + if: needs.sync.outputs.sha != '' + permissions: + contents: read + uses: ./.github/workflows/gate.yml + with: + ref: ${{ needs.sync.outputs.sha }} + + merge: + needs: [sync, gate] + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ needs.sync.outputs.pr }} + SHA: ${{ needs.sync.outputs.sha }} + steps: + - run: gh pr merge "$PR" --repo "$GITHUB_REPOSITORY" --squash --delete-branch --match-head-commit "$SHA" diff --git a/PORTING.md b/PORTING.md index 34283e0..1fc53d5 100644 --- a/PORTING.md +++ b/PORTING.md @@ -141,9 +141,13 @@ question locally: bash omp-port/check-port.sh "" "$(gh api 'repos/cursor/plugins/commits?path=pstack&sha=main&per_page=1' --jq '.[0].sha')" ``` -The gate runs as its own workflow, `gate.yml`, on every pull request and on every push to `main`. A -PR opened with `github.token` does not trigger `pull_request` workflows, so the sync PR carries the -gate output in its body and the `push` to `main` run after the merge is the backstop. +The gate runs as its own workflow, `gate.yml`, on every pull request and on every push to `main`. +Neither trigger gated the two syncs the bot merged, #19 and #32. Each PR got a `pull_request` run +that ended with no jobs a second after the merge, and the merge, made with `github.token`, started +no `push` run. So `upstream-sync` calls `gate.yml` itself, as a reusable workflow on the sync +commit, and its `merge` job runs only after that call passes. A sync that fails the build, +`check-port.sh`, or `gate.yml` stays open for a human, and later runs stop at the open-PR guard +until someone resolves it. Never substitute the `backnotprop/pstack` mirror for upstream. @@ -367,7 +371,7 @@ The injected reminder is a pointer, not the playbook. It tells the agent to read tool blocks until the next settled job or peer message. These are the sanctioned primitives when workers must coordinate instead of running independent. There is no `hub` tool. Probe it rather than trusting this line: `grep -a -A40 'src/tools/builtin-names.ts' "$(readlink -f "$(command -v omp)")"` - prints the binary's builtin tool list, which on 18.12.6 carries `wait` and no `hub`; the binary's + prints the binary's builtin tool list, which on 18.14.1 carries `wait` and no `hub`; the binary's only other `hub` is the `/hub` slash command that opens Agent Hub. An earlier tree read a `hub` tool with list/send/inbox/wait ops off a stale pi-coding-agent 17.x package, and the gate now fails any shipped sentence that names one. Cross-session messaging does not exist locally and @@ -439,7 +443,7 @@ thinking id for judgment work. ### What omp can actually stop The playbooks say "stop where the human's call begins", and until recently that was advice with -nothing behind it. Three facts from `omp://approval-mode.md`, on omp 18.4.10, decide what an +nothing behind it. Three facts from `omp://approval-mode.md`, on omp 18.14.1, decide what an instruction is worth here: - `yolo` auto-approves the `read`, `write`, **and `exec`** tiers. A `gh pr merge` inside a bash