Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f5562b0
chore(loop): add Actions + R2 host for stage A1 (report-only)
Alex1990 Sep 10, 2026
bc39614
chore(loop): repair workflow contexts that broke parsing
Alex1990 Sep 10, 2026
5b91b9e
chore(loop): keep credential failures out of the human inbox, log out…
Alex1990 Sep 10, 2026
eb5c131
chore(loop): target the right gh subcommand, import PRs locally
Alex1990 Sep 10, 2026
ce3fd02
chore(loop): shrink R2 configuration to three secrets
Alex1990 Sep 10, 2026
0b820d5
chore(loop): derive one R2 bucket per repository
Alex1990 Sep 10, 2026
da8f781
chore(loop): include the owner in the derived bucket name
Alex1990 Sep 10, 2026
79c6241
chore(loop): let workflow_dispatch check out its own ref
Alex1990 Sep 10, 2026
37e37fd
chore(loop): correct the model id, validate it, add an infrastructure…
Alex1990 Sep 10, 2026
ef3aef7
chore(loop): render system-level runs correctly in SUMMARY
Alex1990 Sep 10, 2026
4783d46
chore(loop): no pager, bounded timeouts, and a readable smoke
Alex1990 Sep 10, 2026
6dc9426
chore(loop): give system-level runs their own outcome vocabulary
Alex1990 Sep 10, 2026
250ccb0
chore(loop): record A1 acceptance progress with run evidence
Alex1990 Sep 10, 2026
6a70891
chore(loop): verify the task-scoped path on Actions
Alex1990 Sep 10, 2026
ceecd91
chore(loop): check out the PR's code for same-repo PRs
Alex1990 Sep 10, 2026
efaf5a4
chore(loop): log every decision that produces no run
Alex1990 Sep 10, 2026
cd29dad
chore(norms): add the commit-message convention
Alex1990 Sep 10, 2026
5df0f97
chore(loop): translate the loop host to English
Alex1990 Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions .github/workflows/loop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
name: Loop

# Single workflow = scheduler + sandbox (05 §5.1): every event joins one serial
# domain, and the concurrency group provides the platform-level single-writer
# lock (R2 has no concurrent writes).
on:
issues:
types: [opened, reopened, edited]
issue_comment:
types: [created]
pull_request:
types: [opened, synchronize]
pull_request_review:
types: [submitted, dismissed]
pull_request_target:
types: [closed]
release:
types: [published]
schedule:
- cron: '17 3 * * *' # daily sweep; weekly retro cron: scripts/loop/lib/route.mjs WEEKLY_CRON
workflow_dispatch:
inputs:
task:
description: 'Task number (issue/PR). Empty = stage-only run.'
required: false
stage:
description: 'triage | bugfix | feature | pr-review | deps | security | sweep | release'
required: false
default: triage
execute_writes:
description: 'L2 rehearsal: set "true" to allow real GitHub writes (default report-only)'
required: false
default: 'false'
smoke:
description: 'Infrastructure only: exercise checkout/install/R2 pull+push, skip the agent'
required: false
default: 'false'

concurrency:
group: loop
cancel-in-progress: false

# Least privilege: GitHub reads use GITHUB_TOKEN; writes (only the
# execute_writes=true rehearsal) go through LOOP_GH_TOKEN and exist only in
# default-branch contexts — fork events carry no secrets (platform guarantee).
permissions:
contents: read
issues: read
pull-requests: read

jobs:
loop:
runs-on: ubuntu-24.04
timeout-minutes: 60
# Note: job-level env can only reference contexts like github / secrets / vars.
# `inputs` is unavailable for non-workflow_dispatch events, and the `runner`
# context exists only inside steps — neither belongs here (this once broke
# workflow parsing, with GitHub only reporting "workflow file issue").
env:
LOOP_EVENT_NAME: ${{ github.event_name }}
LOOP_EVENT_ACTION: ${{ github.event.action }}
LOOP_EVENT_JSON: ${{ toJSON(github.event) }}
LOOP_REPO: ${{ github.repository }}
# For non-dispatch events github.event.inputs is null → default to report (report-only)
LOOP_EXECUTE_WRITES: ${{ github.event.inputs.execute_writes || 'false' }}
LOOP_SMOKE: ${{ github.event.inputs.smoke || 'false' }}
LOOP_MODEL: deepseek/deepseek-v4-flash
LOOP_RUN_TIMEOUT_MS: '3300000' # 55min, leaving headroom for push (job timeout 60min)
GH_TOKEN: ${{ secrets.LOOP_GH_TOKEN || github.token }}
# Credentials: secrets only
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
# Non-sensitive config: prefer the repository variable, falling back to the
# same-named secret (both setups are supported). ACCOUNT_ID is part of the
# R2 endpoint (https://<id>.r2.cloudflarestorage.com), not a credential.
R2_ACCOUNT_ID: ${{ vars.R2_ACCOUNT_ID || secrets.R2_ACCOUNT_ID }}
# The bucket name needs no config: the code derives it from
# `loop-state-<owner>-<repo>` (one bucket per repo). Set it only to point at
# another bucket (migration/troubleshooting); unset expands to an empty
# string → derivation is used.
R2_BUCKET: ${{ vars.R2_BUCKET || secrets.R2_BUCKET }}

steps:
# What to check out — decided by "is this code trusted":
#
# workflow_dispatch → github.ref. The human-driven smoke/rehearsal entry;
# dispatch can only target refs in this repo, so fork
# branches are excluded and it stays safe.
# same-repo PR → the PR's head branch. The PRs loop itself opens
# (`loop/<n>-*`) fall here, and pr-review must get the
# PR code to review it — checking out only the default
# branch would leave pr-review blind to the content
# under review. Same-repo branches can only be created
# by OWNER/MEMBER/COLLABORATOR, so the code is trusted.
# everything else → the default branch. **Fork PRs land here**: never
# check out external code, use only trusted scripts
# from this repo's default branch, and read diffs with
# `gh pr diff`. Same for issues / schedule / release /
# pull_request_target.
- uses: actions/checkout@v4
with:
ref: >-
${{
github.event_name == 'workflow_dispatch' && github.ref
|| (github.event_name == 'pull_request'
&& github.event.pull_request.head.repo.full_name == github.repository
&& github.event.pull_request.head.ref)
|| github.event.repository.default_branch
}}

- uses: pnpm/action-setup@v4
with:
version: 11

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install engine (pi)
run: |
npm i -g @earendil-works/pi-coding-agent@0.85.1
pi --version
echo '--- available DeepSeek models ---'
available=$(pi --list-models deepseek)
echo "$available"
# pi **silently falls back to the default model** for an invalid
# --model rather than erroring — a misspelled model name would quietly
# run every run with the wrong model. Validate right after install.
want="${LOOP_MODEL#*/}"
if ! grep -qE "(^|[[:space:]])${want}([[:space:]]|$)" <<<"$available"; then
echo "::error::LOOP_MODEL=$LOOP_MODEL not in pi's DeepSeek catalog (want '$want')"
exit 1
fi
echo "model $want is available"

- name: Pull state from R2
run: node scripts/loop/r2-sync.mjs pull

- name: Run stage
env:
LOOP_SESSION_DIR: ${{ runner.temp }}/pi-sessions
run: node scripts/loop/entry.mjs

# Push the state layer back even on crash/timeout (the task stays in
# processing and is reclaimed by the next sweep via TTL)
- name: Push state to R2
if: always()
run: node scripts/loop/r2-sync.mjs push

- name: Upload session transcripts
if: always()
uses: actions/upload-artifact@v4
with:
name: loop-sessions-${{ github.run_id }}
path: ${{ runner.temp }}/pi-sessions
retention-days: 90
if-no-files-found: ignore
17 changes: 17 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ tiny-oss is a tiny object storage SDK focused on uploading, with a functional AP
- Integration specs (`test/*-integration.spec.ts`) need `npm run serve` first (Hono server on :8080, credentials from `.env`); signature/unit specs don't.
- Input data is `Blob | ArrayBuffer | Uint8Array | string`; mini programs pass `ArrayBuffer`.

## Commit messages

Conventional commits, and the type drives the user-facing `CHANGELOG.md`
(`changelogen`), so it must describe what a **package consumer** can observe:

- `feat` / `fix` / `perf` — reaches users; only for changes to the published
package's behaviour.
- `docs` — user-facing documentation (`README.md`, `UPGRADING.md`).
- `chore(<area>)` — repository maintenance no consumer sees: the loop host
(`scripts/loop/`), CI workflows, the norms layer (this file, `docs/agents/`,
`skills/`), tooling. Name the area: `chore(loop)`, `chore(ci)`,
`chore(norms)`.

Never use `fix` or `refactor` for maintenance work. Both are rendered in the
CHANGELOG — `refactor` additionally carries a patch semver — so internal
iteration would read to a user as a change to the package.

## References

- Usage and per-provider options: `README.md`; building a custom provider: README "Extension" section and `src/provider.ts`.
Expand Down
Loading
Loading