Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions .agents/skills/review-venfork/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ Split the diff by seam. Start one reviewer per touched seam, each in its own git

## Seams

1. Leak to public/upstream. Files: `src/shared/stage-gate.ts`, `src/shared/deny-list.ts`, `src/shared/redaction.ts`, `src/commands/stage.ts`, `src/shared/preserve-entries.ts`. Invariant: nothing mirror-only reaches public or upstream. Try: stage a branch with a commit that adds a preserved path; put `venfork` or the mirror owner in an author, committer, message or PR title; use mixed-case or unmatched `venfork:internal` markers in a body.
1. Leak to public/upstream. Files: `src/shared/stage-gate.ts`, `src/shared/deny-list.ts`, `src/shared/redaction.ts`, `src/commands/stage.ts`, `src/shared/preserve-entries.ts`, `src/shared/ai-attribution.ts`, `src/shared/approval.ts`. Invariant: nothing mirror-only reaches public or upstream. Try: stage a branch with a commit that adds a preserved path; put `venfork` or the mirror owner in an author, committer, message or PR title; use mixed-case or unmatched `venfork:internal` markers in a body.
2. Data loss on sync. Files: `src/commands/sync.ts`, `src/shared/divergence.ts`, `src/shared/mirror-commit.ts`, `src/shared/managed-commit.ts`. Invariant: sync never drops a non-managed commit, and the managed commit SHA depends only on the upstream tip and tree. Try: add a user commit under a legacy managed subject; move origin between lease read and push; run sync twice and compare SHAs.
3. Config-branch concurrency. Files: `src/config.ts` (`updateVenforkConfig`, lease on `venfork-config`), `src/shared/config-change.ts`, `tests/integration/preserve-concurrency.test.ts`. Invariant: concurrent config writes never lose a patch. Try: copy the test-local `mock.module` wrappers in that test (`beforeConfigWrite`, `beforeConfigChange` around `updateVenforkConfig` and `applyConfigChange`; the product has no such hook) to land a second write mid-update; patch with `null` against a stale read; exhaust the retry loop.
4. Token and secret scope. Files: `src/workflow.ts`, `src/shared/push-token.ts`, `src/commands/doctor.ts` (`token` check), `src/commands/schedule.ts`. Invariant: the generated workflow holds the least privilege and never prints a token. Try: read the YAML for `permissions`, `secrets.*` in logs and fork-triggered events; run doctor with no token and with a read-only token; check that advice text does not suggest a wider scope than needed.
4. Token and secret scope. Files: `src/workflow.ts`, `src/shared/push-token.ts`, `src/commands/doctor.ts` (`token` check), `src/commands/schedule.ts`. Invariant: the generated workflow holds the least privilege and never prints a token. Try: read the YAML for `permissions`, `secrets.*` in logs and fork-triggered events; run doctor with no token and with a read-only token; check that advice text does not suggest a wider scope than needed. The approval gate also writes to the mirror: the `--force` bypass comment (`gh api --method POST repos/<mirror>/issues/<n>/comments`, `recordBypassOnMirrorPr` in `src/shared/approval.ts`) runs with the user's gh auth; check that no advice asks for more than comment access to the mirror.
5. Version pin and migration. Files: `src/shared/semver.ts`, `src/shared/mirror-commit.ts`, `src/errors.ts` (`PinDowngradeError`, `UnmigratedMirrorError`), `isUnpinnedWorkflow`. Invariant: a mirror is never silently downgraded or left on an unpinned workflow. Try: run an older CLI against a newer pin; sync a mirror with a pre-pin workflow; set `VENFORK_INSTALL_SPEC` to an odd value.
6. Network safety. Files: `src/shared/net.ts` (`netExec`, `runNetOp`, `netFetch`), `src/shared/constants.ts`. Invariant: every network git/gh call has no stdin, a timeout and `BatchMode=yes`. Try: `grep` for raw `$` or `execa` calls on `clone`, `fetch`, `push`, `gh api`; set `VENFORK_GIT_TIMEOUT=1`; point a remote at a host that asks for credentials.
7. CLI contract. Files: `src/index.ts`, `src/dispatch.ts`, `src/*-args.ts`, `src/commands/help.ts`, `tests/unit/docs-drift.test.ts`. Invariant: parsers reject bad input before any side effect, help and the user docs (README.md, docs/reference) match the flags, and cancel exits 130. Try: unknown flags, missing values, `--` and repeated flags; compare `--help` with docs/reference/commands.md; cancel a prompt and check the exit code.
8. Approval gate. Files: `src/shared/approval.ts` (mirror PR lookup, reviews, permissions, checks, the `--force` bypass comment), `src/shared/approval-guard.ts` (clone memory in `<git common dir>/venfork/approval.json`), `src/shared/ai-attribution.ts`, `src/commands/approval.ts`, the gate wiring in `src/commands/stage.ts`, `tests/harness/gh-mirror.ts`. Invariant: a gated stage ships exactly the approved mirror PR head; `--yes`, `--json` or a missing TTY never lowers the level or bypasses the gate; the only bypass record is the comment on the mirror PR; nothing about approval or a bypass reaches public or upstream, and venfork-config stores only the level. Try: move the branch between the gate read and the rebuild; approve from the stage runner under `two-person`; pass `--internal-pr` for another branch's PR; corrupt `.git/venfork/approval.json`; `stage approval off` then stage with `--json`; grep the published commits and PR body for the approver, the level or the bypass text.

## Report format

Expand Down
5 changes: 4 additions & 1 deletion .agents/skills/verify-venfork/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ T=$(mktemp -d); bun scripts/verify-fixture.ts --shell > "$T/vf.env"
- Helpers: `venfork`, `vf_tip <repo> [ref]`, `vf_pushes <repo> [ref]` (reflog count = accepted pushes),
`vf_yes <args>` (answers one confirm through a pseudo-TTY), `vf_upstream_commit <path> <text> [msg]`,
`vf_origin_commit <path> <text> [msg]` (a teammate push to origin).
`vf_yes` answers one confirm only. A lowered approval level or a `--force` bypass adds a gate confirm before the push confirm, and `--yes` never answers a gate confirm. Then use `vf_yes ... --yes`: `vf_yes` answers the gate confirm and `--yes` the push confirm. Without `--yes`, the push confirm hits end of input and exits 130.
- The fixture's venfork-config has no `approval` (off), unlike a new `setup` mirror (`two-person`). Stage and `stage approval` remember the level in the clone memory `$VF_WORK/.git/venfork/approval.json`. After an unconfirmed lowering (`stage approval off` without a TTY), every later non-TTY stage in this fixture exits 1 with `approval` reason `gate-weakened`. Reset with `rm "$VF_WORK/.git/venfork/approval.json"`, or use a fresh fixture.
- Each tool call is a new shell. Source `"$T/vf.env"` again in each one.
- When a sandbox refuses to source `vf.env` or to set `GIT_*` variables inline, write the drive to a script file that exports the JSON `env` (read it with `jq`) and calls `node "$CLI"`, then run it with `sh <file>`.

## Doctor

Expand Down Expand Up @@ -68,7 +71,7 @@ Evidence is git state, read from the bare repos:
- "Sync complete!" prints after a no-op too. Read the spinner lines (`already up to date` vs `Updated`) and the push count.
- `stage` rebuilds every commit, so the public SHA changes on every stage. Compare trees and subjects, not SHAs.
- The fixture remotes are local paths. The deny-list then holds the origin path and `venfork`, not `owner/name`.
- gh-backed paths (`setup`, `clone`, `pull`, `stage` without `--no-pr`, issues) stop at the stub. Their success paths are proven only by e2e.
- gh-backed paths (`setup`, `clone`, `pull`, `stage` without `--no-pr`, any `stage` with approval `reviewed` or `two-person`, issues) stop at the stub. A gated stage stops even earlier on the fixture, at `precondition`, because origin is not a GitHub repository. Their success paths are proven only by e2e.

## Clean up

Expand Down
6 changes: 3 additions & 3 deletions .agents/skills/verify-venfork/references/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ Cross-cutting: `leak-invariants.md` (what must never reach public or upstream),
- Exit codes: 0 success (and "no" at a confirm at a terminal), 1 a runtime failure (the message prints once, as `error: ...` or, on a TTY, after a red `■`, with no spinner `done:` line and no failed outro; `reportFailure` in `src/shared/ui.ts`), a failing `doctor` check (doctor prints no `error:` line; its summary outro counts the failed checks, e.g. `❌ 2 failed, 5 passed, 4 skipped`) or a partial success (`partialFailureExitCode` in `src/dispatch.ts`: stage `prAction: failed`, a failed `pull pr` push to origin; the success envelope still holds the result), 2 a usage error (`UsageError`: unknown command or option, missing or unexpected argument, malformed flag value, flags that cannot be combined, bare `venfork`) or missing input or consent (`MissingInputError`), 130 a cancel or SIGINT, 143 a SIGTERM. Input valid in form that fails against the repos (a missing branch, divergence, a flag that contradicts `venfork-config`) is exit 1.
- Exit 130 has three causes: a prompt that is cancelled (Ctrl-C, Esc) or whose input ends (`Cancelled: input ended at a prompt`); a terminal that closes during the run (EIO on stdin, stdout or stderr, `src/shared/closed-output.ts`); a SIGINT (without `--json`, Node's default handler ends the run and the shell reports 130). Under `--json`, `src/index.ts` prints the `cancelled` envelope on a SIGINT (exit 130) or a SIGTERM (exit 143). Without `--json`, a SIGTERM is Node's default handler too and the shell reports 143. `setup` handles both signals itself: it removes its temp dir, then exits 130 for SIGINT or 143 for SIGTERM (`tests/integration/cli.test.ts` pins 143 for `sync --json` and `setup`). A reader that closes the pipe early (`venfork help | head -1`, EPIPE) keeps the exit code so far and prints no stack.
- Prompts: venfork prompts only when stdin and stderr are both a TTY (prompts render on stderr), `CI` is unset (or empty, `false`, `0`), `VENFORK_NONINTERACTIVE` is not `1` and there is no `--no-input` or `--json` (`canPrompt` in `src/shared/prompt.ts`). At a terminal, `venfork stage feat --no-pr 2>/dev/null` exits 2 naming `--yes`; it does not wait on a hidden prompt (`tests/integration/cli.test.ts`, "stage with stderr redirected at a TTY"). Every command accepts `--no-input`, `--yes` and `-y`; where nothing prompts or confirms they are no-ops (`isPromptFlag` in `src/shared/args.ts`).
- Outside a git repository, `sync`, `stage`, `pull`, `schedule`, `workflows` and `keep` stop before the command runs with `NotInRepositoryError` (code `not-in-repository`, exit 1; `assertInRepository` in `src/dispatch.ts`, called by `src/index.ts` after the arguments parse and gh auth). stderr holds only `error: Not in a git repository. ...`: no intro, spinner or command error. A usage error still exits 2, because every usage check runs in the parse step (the cron check in `parseScheduleCliArgs`, the `preserve add` path check in `src/index.ts`). `doctor` reports it as its `repo` check, and `setup` and `clone` run outside a repo.
- Outside a git repository, `sync`, `stage`, `pull`, `schedule`, `workflows` and `keep` stop before the command runs with `NotInRepositoryError` (code `not-in-repository`, exit 1; `assertInRepository` in `src/dispatch.ts`, called by `src/index.ts` after the arguments parse and gh auth). stderr holds only `error: Not in a git repository. ...`: no intro, spinner or command error. A usage error still exits 2, because every usage check runs in the parse step (the cron check in `parseScheduleCliArgs`, the `keep add` path check, `normalizeKeepArgs`, called from `src/index.ts`). `doctor` reports it as its `repo` check, and `setup` and `clone` run outside a repo.
- Typed errors live in `src/errors.ts`. Without `--json` the CLI prints only `.message`. With `--json`, match on `error.code`; it is stable, the message is not. The codes (`ErrorCode` in `src/errors.ts`; `describeError` in `src/shared/json-output.ts` maps a failed `git` or `gh` subprocess to `git` or `github`, else `internal`):
- exit 2: `usage`, `missing-input` (with `error.flag`).
- exit 130 or 143: `cancelled`. A declined confirm is `CommandExitError(0)`, also coded `cancelled`, but no envelope carries it: `--json` implies `--no-input`, so a JSON run never shows a confirm.
- exit 1: `auth`, `git`, `github`, `precondition`, `remote-not-found`, `not-in-repository`, `branch-not-found`, `sync-divergence`, `config`, `redaction`, `mirror-reference`, `stage-leak`, `pin-downgrade`, `unmigrated-mirror`, `runtime`, `internal`. No envelope reports `command-failed`: a `CommandExitError` without a cause is `runtime` (exit 1), `usage` (exit 2) or `cancelled`.
- `error.hint` (the next step) and `error.details` (files, paths, branch, flag) are present for the codes README "JSON output" lists; without `--json` the hint follows the message on the one `error:` line.
- exit 1: `auth`, `git`, `github`, `precondition`, `remote-not-found`, `not-in-repository`, `branch-not-found`, `sync-divergence`, `config`, `redaction`, `mirror-reference`, `stage-leak`, `approval` (with `details.reason`, see `stage.md`), `pin-downgrade`, `unmigrated-mirror`, `runtime`, `internal`. No envelope reports `command-failed`: a `CommandExitError` without a cause is `runtime` (exit 1), `usage` (exit 2) or `cancelled`.
- `error.hint` (the next step) and `error.details` (files, paths, branch, flag) are present for the codes `docs/reference/json.md` lists; without `--json` the hint follows the message on the one `error:` line.
- Network ops: every git fetch, push and ls-remote runs through `netExec` (`src/shared/net.ts`): no stdin, a hard timeout, ssh `BatchMode`, and gh as the only credential helper for `https://github.com` (see `sync.md`).
- "Concurrent lease" drives use a stale copy of origin: fetch from the copy, push to the real origin.

Expand Down
23 changes: 19 additions & 4 deletions .agents/skills/verify-venfork/references/features/approval.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
---
command: approval
entry: src/commands/approval.ts
shared: [json-output]
shared: [approval-guard, prompt, json-output]
config:
read: [approval]
written: [approval]
tests:
unit: [tests/unit/stage-args.test.ts, tests/unit/config.test.ts]
integration: [tests/integration/approval.test.ts]
unit: [tests/unit/stage-args.test.ts, tests/unit/config.test.ts, tests/unit/approval.test.ts]
integration: [tests/integration/approval.test.ts, tests/integration/approval-guard.test.ts]
e2e: []
---

Expand All @@ -33,12 +33,27 @@ venfork stage approval on </dev/null 2>&1 | grep -o "Approval set to two-person"
git -C "$VF_ORIGIN" show venfork-config:.venfork/config.json | jq -r .approval # two-person
venfork stage approval off </dev/null 2>&1 | grep -o "The approval gate is off"
venfork stage approval maybe </dev/null; echo $? # 2
rm "$VF_WORK/.git/venfork/approval.json" # reset the clone memory (see What usually lies)
```

What the level does to a stage on the fixture (the last two lines reset the memory):

```bash
git checkout -q -b feat upstream/main && echo f > src/feat.txt && git add . && git commit -qm "feat: add feat"
venfork stage approval on </dev/null >/dev/null 2>&1
venfork stage feat --no-pr --yes --json </dev/null 2>/dev/null | jq -r .error.code # precondition: the fixture origin is not GitHub
venfork stage approval off </dev/null >/dev/null 2>&1 # the hint's next step, without a TTY
venfork stage feat --no-pr --yes --json </dev/null 2>/dev/null | jq -c '[.error.code, .error.details.reason]' # ["approval","gate-weakened"]
rm "$VF_WORK/.git/venfork/approval.json"
venfork stage feat --no-pr --yes </dev/null >/dev/null 2>&1; echo $? # 0, and the memory now holds off
```

## What proves it

- `git -C "$VF_ORIGIN" show venfork-config:.venfork/config.json` has `"approval"` after `on`/`reviewed`, and none after `off`.
- `cat "$VF_WORK/.git/venfork/approval.json"` holds `{ "level" }`: the new level after a raise or a confirmed lowering, the earlier level after a lowering that no one confirmed.

## What usually lies

- The setting alone proves nothing about stage. Check the stage feature file for what stage does with the level.
- The setting alone proves nothing about stage. Check the stage feature file for what stage does with the level. With `reviewed` or `two-person`, every fixture stage (also with `--no-pr`) stops at `precondition` "origin is not a GitHub repository". Its hint `venfork stage approval off` leads to the next point.
- `stage approval on` then `stage approval off` without a TTY leaves this clone's memory (`<git common dir>/venfork/approval.json`, in the fixture `$VF_WORK/.git/venfork/approval.json`) at `two-person` while venfork-config says `off`. After that, every non-TTY `venfork stage` in this fixture exits 1 with `approval` reason `gate-weakened`, so the `stage.md` and `leak-invariants.md` drives do not give their documented results (`tests/integration/approval.test.ts` pins this). Reset with `rm "$VF_WORK/.git/venfork/approval.json"` (the next stage records `off`), or run this file last on a fresh fixture. At a TTY, `vf_yes stage feat --no-pr --yes` answers the lowering confirm once and `--yes` answers the push confirm.
2 changes: 1 addition & 1 deletion .agents/skills/verify-venfork/references/features/clone.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
command: clone
entry: src/commands/clone.ts
shared: [json-output, prompt]
shared: [json-output, net, prompt]
config:
read: [mode, upstreamUrl, publicForkUrl]
written: []
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
command: doctor
entry: src/commands/doctor.ts
shared: [constants, cron, divergence, json-output, managed-commit, mirror-commit, net, push-token, semver]
shared: [constants, cron, divergence, json-output, managed-commit, mirror-commit, mirror-origin, net, push-token, semver]
config:
read: [mode, upstreamUrl, publicForkUrl, schedule, scheduleAuth, preserve, invalidPreserve, shippedBranches, pulledPrs, shippedIssues, pulledIssues]
written: []
Expand Down
Loading
Loading