diff --git a/.github/workflows/upstream-watch.yml b/.github/workflows/upstream-watch.yml new file mode 100644 index 00000000..e41e4362 --- /dev/null +++ b/.github/workflows/upstream-watch.yml @@ -0,0 +1,120 @@ +name: upstream-watch + +# The upstream watch (docs/UPSTREAM-WATCH.md, decision 14). The script decides +# everything: which ledger comments count, where the check starts, and the +# comment's text. This workflow only posts that text on the ledger issue and +# labels the issue when a released fix is ready to dispatch, which fires the +# dispatch routine. No model runs here. + +on: + schedule: + - cron: '0 14 * * *' + workflow_dispatch: + inputs: + post: + description: Post the ledger comment (off = preview in the job summary only) + type: boolean + default: true + pull_request: + paths: + - scripts/upstream-watch.mjs + - scripts/upstream-watch/** + - src/lib/hook-audit/** + - .github/workflows/upstream-watch.yml + +permissions: + contents: read + +env: + REGISTRY: src/lib/hook-audit/agentic-dependency-constraints.json + DISPATCH_LABEL: upstream-dispatch + +jobs: + # Read-only proof on a pull request that the workflow token reads the + # upstream threads and the ledger. Never posts. + preview: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: '24' + - name: Check (read-only) + env: + GH_TOKEN: ${{ github.token }} + run: | + set +e + node scripts/upstream-watch.mjs comment --json > watch.json 2> errors.txt + code=$? + set -e + { + echo "## Upstream watch preview (exit $code)" + jq -r '"events \(.events | length), could not check \(.fetchErrors | length), blind \(.blind), would post \(.post), dispatch \(.dispatch | join(", "))"' watch.json + echo; echo '```text'; cat errors.txt; echo '```' + } >> "$GITHUB_STEP_SUMMARY" + exit "$code" + + watch: + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + issues: write + concurrency: + group: upstream-watch + cancel-in-progress: false + env: + GH_TOKEN: ${{ github.token }} + POST: ${{ github.event_name == 'schedule' || inputs.post }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: '24' + - name: Check + id: check + run: | + set +e + node scripts/upstream-watch.mjs comment --json > watch.json 2> errors.txt + code=$? + set -e + { + echo "## Upstream watch (exit $code)" + jq -r '"since \(.since) (\(.sinceSource)), events \(.events | length), could not check \(.fetchErrors | length), blind \(.blind), post \(.post), dispatch \(.dispatch | join(", "))"' watch.json + echo; echo '```text'; cat errors.txt; echo '```' + jq -r '.body' watch.json + } >> "$GITHUB_STEP_SUMMARY" + # 3 = blind: gh unusable, the ledger unreadable, or no thread read. + exit "$code" + - name: Post the ledger comment + if: env.POST == 'true' + run: | + [ "$(jq -r '.post' watch.json)" = true ] || { echo 'Nothing to post.'; exit 0; } + issue=$(jq -r '.watchPolicy.ledger.issue' "$REGISTRY") + repo=$(jq -r '.watchPolicy.ledger.repo' "$REGISTRY") + jq -r '.body' watch.json > body.md + test -s body.md + [ "$(head -n 1 body.md)" = '```text' ] + grep -q '^checked-at ' body.md + url=$(gh issue comment "$issue" --repo "$repo" --body-file body.md) + echo "Posted $url" + id=${url##*issuecomment-} + length=$(gh api "repos/$repo/issues/comments/$id" --jq '.body | length') + echo "Posted length $length (file $(wc -c < body.md))" + [ "$length" -gt 0 ] + - name: Signal the dispatch routine + if: env.POST == 'true' + run: | + [ "$(jq -r '.dispatch | length' watch.json)" -gt 0 ] || { echo 'Nothing to dispatch.'; exit 0; } + issue=$(jq -r '.watchPolicy.ledger.issue' "$REGISTRY") + repo=$(jq -r '.watchPolicy.ledger.repo' "$REGISTRY") + gh label create "$DISPATCH_LABEL" --repo "$repo" --color 5319e7 \ + --description 'The upstream watch has a released fix to dispatch' --force + # Remove, then add: a label already present would fire no new event. + gh issue edit "$issue" --repo "$repo" --remove-label "$DISPATCH_LABEL" || true + gh issue edit "$issue" --repo "$repo" --add-label "$DISPATCH_LABEL" diff --git a/AGENTS.md b/AGENTS.md index 5acbbef2..51e89cc0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -294,7 +294,8 @@ Code's own `~/.claude.json`) are listed as "concurrent writers" and do not fail (or `AK_TRIPWIRE_STRICT=1`) fails on them too. Every command also runs with `TMPDIR`/`TEMP`/`TMP` pointed at a fresh `ak-suite-*` folder: anything left in it afterwards fails the run and is listed, and the runner refuses to start when that folder sits inside a git repository (point -`TMPDIR` elsewhere). Tests make temporary folders with `tempDir()` from +`TMPDIR` elsewhere). The runner also drops `FORCE_COLOR` (Claude Code shells set it), because +tests read plain text from pipes. Tests make temporary folders with `tempDir()` from `tests/kit/helpers/temp-dir.mjs`, and spawned children get their environment from `spawnEnv()` in `tests/kit/helpers/home-sandbox.mjs`. UI tests launch Chrome with `launchChrome()` from `tests/ui/helpers/launch-chrome.mjs`, which gives the browser its own temp folder and removes it on diff --git a/MAINTAINER.md b/MAINTAINER.md index 7b933a66..f928b30a 100644 --- a/MAINTAINER.md +++ b/MAINTAINER.md @@ -483,9 +483,12 @@ gh run list --workflow=nightly.yml --limit 3 ```bash node scripts/upstream-watch.mjs report # counts, then action items with links node scripts/upstream-watch.mjs check --since 2026-09-26 --ledger ledger.md # new ledger lines only +node scripts/upstream-watch.mjs comment # the ledger comment the daily workflow would post +gh workflow run upstream-watch.yml -f post=false # run the daily workflow now, summary only ``` -Read-only against GitHub and npm. The registry, lifecycle, ledger and daily routine are in +The script is read-only against GitHub and npm; the `upstream-watch` workflow posts its comment +on the ledger issue daily. The registry, lifecycle, ledger, workflow and dispatch routine are in [UPSTREAM-WATCH.md](docs/UPSTREAM-WATCH.md). ### Pull requests diff --git a/docs/UPSTREAM-WATCH.md b/docs/UPSTREAM-WATCH.md index 5920e813..50b7dc2d 100644 --- a/docs/UPSTREAM-WATCH.md +++ b/docs/UPSTREAM-WATCH.md @@ -13,8 +13,9 @@ is the only upstream registry. It ships with ak because the hook audit reads it. the evidence needed, and the **removal proof** a workaround must pass before it goes. - `constraints`: version-bound workarounds with a retest date and a sunset condition ([ADR-0041 §7](adr/0041-host-neutral-hook-configuration-assurance.md#7-upstream-constraints-are-lifecycle-data)). -- `watchPolicy`: our GitHub logins, the stale limit (90 days), automated-reply patterns, the - ledger issue and its sentinel, and the dispatch rules. +- `watchPolicy`: our GitHub logins (`ours`: whose upstream comment is our last word), the stale + limit (90 days), automated-reply patterns, the ledger issue, its sentinel and the logins that + write it (`ledger.authors`), and the dispatch rules. - `watch`: every upstream issue or pull request ak filed, commented on, or cites in `src/`, `bin/`, `claude/`, `tests/`, `README.md` or a guide in `docs/` (dated audits, proposals and research references are exempt by name in `scripts/upstream-watch/citations.mjs`), plus ak's @@ -71,24 +72,37 @@ A constraint whose `nextRetestAt` has passed shows as stale evidence in the hook `scripts/upstream-watch.mjs` is maintainer tooling; it is not published. It reads GitHub with `gh api` and releases with `npm view` or GitHub releases, at most four calls at a time, and -writes nothing. It runs on macOS and Linux (the routine runs on Linux). On Windows, npm is a +writes nothing. It runs on macOS and Linux (the scheduled workflow runs on Linux). On Windows, npm is a `.cmd` file, which Node refuses to start without a shell ([Spawning `.bat` and `.cmd` files on Windows](https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows)), so every npm-gated release would read "Could not check"; the script passes version ranges such as -`^3.33.0` that `cmd.exe` would misread, so it does not add one. If `gh` is missing or signed out it says so and reports only what the registry -records. It exits 0 unless the command line is wrong. `check` prints only ledger lines on stdout; -each thread or release it could not check goes to stderr as `Could not check : `, and -`check --json` lists them in `fetchErrors`. +`^3.33.0` that `cmd.exe` would misread, so it does not add one. It needs a `gh` that can call the +GitHub API (it probes with `gh api rate_limit`, which any token passes, including the Actions +token); if `gh` is missing or cannot reach GitHub it says so and reports only what the registry +records. `check` prints only ledger lines on stdout; each thread or release it could not check +goes to stderr as `Could not check : `, and `check --json` lists them in `fetchErrors` +and says `blind` when not one watched thread could be read. It prints "No new upstream events." +only when every read succeeded; otherwise it names the threads it could not check. `report` and +`check` exit 0 unless the command line is wrong. + +`comment` is what the scheduled workflow runs. It reads the ledger issue's comments by +`watchPolicy.ledger.authors` only, starts the check from the newest `checked-at` in them (seven +days ago when there is none), drops lines already in them, and prints the comment to post (see +[The ledger](#the-ledger)), or nothing when there is no new event. `comment --json` also gives the +start, the new `checked-at`, the dispatch branches, the events and the fetch errors. It exits 3 +when blind: `gh` cannot reach GitHub, the ledger cannot be read, or no watched thread could be. ```bash node scripts/upstream-watch.mjs report [--json] node scripts/upstream-watch.mjs check --since [--ledger ] [--json] +node scripts/upstream-watch.mjs comment [--json] ``` The Ruflo support window (the newest six minors, never fewer than those released in the last 30 days; `supportWindow` on the Ruflo dependency policy, ADR-0041 §7) comes from the npm release -dates the check reads. When they cannot be read, or `gh` is signed out, nothing is held for the -window. +dates the check reads. When they cannot be read, every Ruflo-carried fix (Ruflo's own and +AgentDB's) waits for the support window, so its `released` line carries no `branch=` and nothing +is dispatched until a check reads the window again. When `gh` is signed out nothing is checked. `report` gives counts, then these groups (a thread can be in more than one): @@ -114,9 +128,9 @@ window. The ledger is [pacphi/agentic-kit#243](https://github.com/pacphi/agentic-kit/issues/243), titled "Upstream watch", pinned and locked (`gh issue lock`, so only collaborators can comment); -`watchPolicy.ledger.issue` records it. The repository is public, so the routine reads only its own comments and -those of the logins in `watchPolicy.ours`; anyone else's comment is ignored. Each event is a -line: +`watchPolicy.ledger.issue` records it. The repository is public, so only comments by the logins +in `watchPolicy.ledger.authors` (the maintainer and `github-actions[bot]`, the workflow's login) +count; anyone else's comment is ignored. Each event is a line: ```text UPSTREAM-WATCH [key=value ...] @@ -130,13 +144,19 @@ watch). `check --since` limits replies, acknowledgements, closures and merges to `--since`. The other events repeat while their condition holds, dated by the upstream fact, so the same fact always gives the same line. A `released` line for a fix held for the support window has no `branch=` field; the line with one appears once the window's floor contains the fix. `--ledger ` drops any line already in that file, -so an exact line the routine recorded is never acted on twice. The file holds only the -routine's own comments: a line someone else posted would suppress a real event. Each posted -comment ends with `checked-at