diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a1abd6..77676a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ This project follows Semantic Versioning. ## [Unreleased] +### Added + +- Added `--batch` and `--batch-help` as the primary names for autonomous Codex + execution while keeping `--human-gate` and `--human-gate-help` as compatible + aliases. +- Added explicit `restricted` and `full-delivery` batch permissions through + `--batch-permissions` and `START_ISSUE_BATCH_PERMISSIONS`. +- Added English and Russian batch-mode guides with preflight, examples, human + handoff semantics, and troubleshooting. + ## [2.0.2] - 2026-08-04 ### Changed diff --git a/Makefile b/Makefile index 06e8916..fef8b33 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build install uninstall test e2e-sandbox e2e-human-gate print-version bump-patch bump-minor bump-major release-patch release-minor release-major +.PHONY: build install uninstall test e2e-sandbox e2e-batch e2e-human-gate print-version bump-patch bump-minor bump-major release-patch release-minor release-major PREFIX ?= $(HOME)/.local BINDIR ?= $(PREFIX)/bin @@ -31,8 +31,10 @@ test: python3 scripts/check_memory_bank_index.py --max-depth 4 git diff --check -e2e-human-gate: build - @START_ISSUE_E2E_BINARY="$(abspath $(BUILD_OUTPUT))" bash test/e2e/human-gate.sh +e2e-batch: build + @START_ISSUE_E2E_BINARY="$(abspath $(BUILD_OUTPUT))" bash test/e2e/batch.sh + +e2e-human-gate: e2e-batch e2e-sandbox: build @START_ISSUE_SANDBOX_BINARY="$(abspath $(BUILD_OUTPUT))" bash test/e2e/sandbox.sh diff --git a/README.md b/README.md index 573c702..1854ab2 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ start-issue https://github.com/owner/repo/issues/123 start-issue 123 --repo owner/repo --base develop start-issue 123 --agent codex start-issue 123 --agent codex --model gpt-5.2 -start-issue 123 --agent codex --human-gate +start-issue 123 --batch start-issue 123 --agent kimi --prompt-file .start-issue/prompt.md start-issue 123 --no-agent start-issue 123 --dry-run @@ -76,7 +76,7 @@ start-issue update start-issue --update start-issue install start-issue --install -start-issue --human-gate-help +start-issue --batch-help ``` Running `start-issue` without an issue prints the normal help plus the currently @@ -120,7 +120,7 @@ explicit external process boundaries. - Repository/worktree helpers fetch issue metadata, plan reuse safely, and run the optional `init.sh` hook found in a prepared worktree. - Agent helpers validate adapters, build launch commands, generate AI branch - names, and run Codex human-gate mode. + names, and run Codex batch mode. - Release helpers select platform assets, verify checksums and staged `--version` output, and atomically install updates. @@ -158,8 +158,11 @@ second runtime implementation. | `--prompt TEXT` | Inline prompt template for the selected agent. With `init`, the prompt template to write. Mutually exclusive with `--prompt-file`. | | `--prompt-file PATH` | Prompt template file for the selected agent. With `init`, the file content to write. Mutually exclusive with `--prompt`. | | `--improve-prompt` | Ask the selected agent to generate a reviewable improved prompt template proposal, then exit before creating a worktree. | -| `--human-gate` | Codex-only batch mode for issue work. Runs `codex exec`, exits on `STATUS: DONE`, and resumes the same session on `STATUS: HUMAN_GATE`. | -| `--human-gate-help` | Show dedicated help for the Codex human-gate workflow, including prompt contract, exit codes, and state files. | +| `--batch` | Codex-only batch mode for issue work. Runs `codex exec`, exits on `STATUS: DONE`, and resumes the same session on `STATUS: HUMAN_GATE`. | +| `--human-gate` | Compatibility alias for `--batch`. | +| `--batch-permissions restricted\|full-delivery` | Select the batch capability contract. Requires `--batch` or its legacy alias; CLI overrides `START_ISSUE_BATCH_PERMISSIONS`; default is `restricted`. | +| `--batch-help` | Show dedicated help for Codex batch mode, including the `HUMAN_GATE` handoff, exit codes, and state files. | +| `--human-gate-help` | Compatibility alias for `--batch-help`. | | `--prompt-output-file PATH` | Proposal output path for `--improve-prompt`. | | `--no-init` | Do not run `init.sh` even if it exists in the created worktree. | | `--command COMMAND`, `-c COMMAND` | Claude command prefix used by the default Claude prompt. Default: `/task-router:route-task`. | @@ -190,6 +193,7 @@ Related Claude Code marketplace workflows: | `START_ISSUE_PROMPT` | Inline prompt template used when no CLI prompt is provided. It overrides project and user prompt files. Mutually exclusive with `START_ISSUE_PROMPT_FILE` when no CLI prompt is provided. | | `START_ISSUE_PROMPT_FILE` | Prompt template file used when no CLI prompt is provided. It overrides project and user prompt files. Mutually exclusive with `START_ISSUE_PROMPT` when no CLI prompt is provided. | | `START_ISSUE_WORKTREE_DIR` | Default parent directory for created worktrees when `--worktree-dir` is not provided. Built-in default: `~/worktrees`. | +| `START_ISSUE_BATCH_PERMISSIONS` | Batch capability contract when the CLI option is absent: `restricted` or `full-delivery`. Built-in default: `restricted`. | | `START_ISSUE_DUMP_PROMPT` | When set to `1`, dry-run output includes the full rendered prompt instead of only summary information. | ## Configuration Files @@ -225,9 +229,9 @@ The update workflow works outside a git repository and requires only `gh`. The Go binary parses release metadata, downloads assets, and verifies checksums internally. -## Codex Human-Gate +## Codex Batch Mode -`start-issue 123 --agent codex --human-gate` keeps the normal issue-start workflow through worktree creation, optional `init.sh`, and prompt rendering, but replaces the final interactive Codex launch with a resumable batch run. +`start-issue 123 --batch` keeps the normal issue-start workflow through worktree creation, optional `init.sh`, and prompt rendering, but replaces the final interactive Codex launch with a resumable batch run. The batch flow: @@ -236,20 +240,54 @@ The batch flow: 3. exits `0` on `STATUS: DONE`; 4. opens `codex resume --include-non-interactive ` on `STATUS: HUMAN_GATE`. -This mode is intentionally Codex-only. `--human-gate` with any other agent fails clearly instead of being ignored. +This mode is intentionally Codex-only. `--batch` with any other resolved agent +fails clearly instead of being ignored. The released `--human-gate` flag remains +available as a compatibility alias for `--batch`. + +Batch permissions are explicit: + +- `restricted` is the default. It uses `--sandbox workspace-write` and supports + working-tree edits, but network access, Git metadata writes, push, and PR + delivery are not guaranteed. +- `full-delivery` is an explicit opt-in. It runs Codex with + `--dangerously-bypass-approvals-and-sandbox`, allowing the normal issue + workflow to read GitHub context, edit, test, commit, push, and create or + update a PR when the current `gh` session and repository permissions allow + it. This is unsandboxed execution. + +Select the mode with the CLI (highest precedence), the environment, or the +safe built-in default: + +```bash +start-issue 123 --batch \ + --batch-permissions full-delivery + +START_ISSUE_BATCH_PERMISSIONS=full-delivery \ + start-issue 123 --batch +``` + +Full delivery changes launcher capability only. It does not authorize +destructive Git operations, production/security changes, or product decisions; +the prompt must still return `STATUS: HUMAN_GATE` for those. Before using it, +verify `gh auth status`, the selected account, the remote, and repository write +access. A restricted capability failure should be handled by manual delivery or +an explicit full-delivery rerun, not reported as a task-level product decision. When the workflow is about to block for a branch/worktree decision, it prints `Waiting for input: ...`. Before handing control to an interactive agent or Codex batch run, it prints `Handing off to in `. A non-zero -exit from `codex exec` is reported as a failed human-gate run with exit code 1; +exit from `codex exec` is reported as a failed batch run with exit code 1; the captured events and thread id remain available for diagnosis. Dedicated help: ```bash -start-issue --human-gate-help +start-issue --batch-help ``` +For a copy-ready preflight and end-to-end example, see +[Codex batch mode: autonomous work with a human gate](doc/batch-mode.md). + Prompt contract: - The final message must contain exactly one terminal status line: `STATUS: DONE` or `STATUS: HUMAN_GATE`. @@ -275,7 +313,7 @@ The normal automated test suite uses a fake Codex CLI. To exercise the real loca CLI, run this opt-in test from a `start-issue` checkout: ```bash -START_ISSUE_E2E=1 make e2e-human-gate +START_ISSUE_E2E=1 make e2e-batch ``` The script uses the private `dapi/start-issue-e2e-fixture` repository and its @@ -286,23 +324,34 @@ fixture worktree change other than its `.start-issue` state. To test interactive ```bash START_ISSUE_E2E=1 \ -test/e2e/human-gate.sh --scenario human-gate +test/e2e/batch.sh --scenario human-gate ``` Exit the resumed Codex session to let the script verify the artifacts. +To validate actual commit, push, and PR creation in the private fixture, use +the separately authorized unsandboxed scenario. It creates and retains a unique +remote branch, PR, and local diagnostic fixture as evidence: + +```bash +START_ISSUE_E2E=1 START_ISSUE_E2E_FULL_DELIVERY=1 \ + test/e2e/batch.sh --scenario full-delivery +``` + #### Scenarios and checks | Scenario | Command | What it verifies | | --- | --- | --- | -| `done` | `START_ISSUE_E2E=1 make e2e-human-gate` | A real Codex batch run emits `thread.started`, saves `thread-id`, `events.jsonl`, and `last-message.txt`, ends with `STATUS: DONE`, and leaves no fixture change other than `.start-issue` state. | -| `human-gate` | `START_ISSUE_E2E=1 test/e2e/human-gate.sh --scenario human-gate` | The same artifact and clean-worktree checks, plus the reported explicit `codex resume --include-non-interactive ` handoff. The operator exits the resumed interactive session before the script can finish. | +| `done` | `START_ISSUE_E2E=1 make e2e-batch` | A real Codex batch run emits `thread.started`, saves `thread-id`, `events.jsonl`, and `last-message.txt`, ends with `STATUS: DONE`, and leaves no fixture change other than `.start-issue` state. | +| `human-gate` | `START_ISSUE_E2E=1 test/e2e/batch.sh --scenario human-gate` | The same artifact and clean-worktree checks, plus the reported explicit `codex resume --include-non-interactive ` handoff. The operator exits the resumed interactive session before the script can finish. | +| `full-delivery` | `START_ISSUE_E2E=1 START_ISSUE_E2E_FULL_DELIVERY=1 test/e2e/batch.sh --scenario full-delivery` | The current Codex accepts the global full-delivery option and completes a unique fixture commit, push, and PR; the runner prints the retained PR URL and local artifact path. | -Both scenarios verify authenticated `gh`, a real rather than fake Codex binary, +All scenarios verify authenticated `gh`, a real rather than fake Codex binary, and the required `codex exec` help interface (`--output-last-message`, without the obsolete `--ask-for-approval` flag). The selected Codex executable is -printed in the test output. They do not prove application behavior beyond this -human-gate protocol and are intentionally excluded from CI. +printed in the test output. The `done` and `human-gate` scenarios do not prove +application behavior beyond this protocol; `full-delivery` additionally proves +the explicitly authorized fixture delivery path. All are excluded from CI. ### CI sandbox E2E diff --git a/README.ru.md b/README.ru.md index 93e158a..020af56 100644 --- a/README.ru.md +++ b/README.ru.md @@ -68,9 +68,9 @@ Workflow обновления определяет последний GitHub Rel релиз, обновляет тот же путь executable. Если установленная версия уже актуальна, команда успешно завершается и печатает понятный no-op статус. -## Codex Human-Gate +## Codex Batch Mode -`start-issue 123 --agent codex --human-gate` сохраняет обычный issue-start workflow до создания worktree, опционального `init.sh` и рендера prompt, но заменяет финальный интерактивный запуск Codex на resumable batch run. +`start-issue 123 --batch` сохраняет обычный issue-start workflow до создания worktree, опционального `init.sh` и рендера prompt, но заменяет финальный интерактивный запуск Codex на resumable batch run. Batch flow: @@ -79,20 +79,50 @@ Batch flow: 3. завершает команду с кодом `0` на `STATUS: DONE`; 4. открывает `codex resume --include-non-interactive ` на `STATUS: HUMAN_GATE`. -Режим намеренно поддерживается только для Codex. `--human-gate` с любым другим agent завершается явной ошибкой. +Режим намеренно поддерживается только для Codex. `--batch` с любым другим +выбранным agent завершается явной ошибкой. Ранее выпущенный `--human-gate` +продолжает работать как совместимый alias для `--batch`. + +Права batch mode задаются явно: + +- `restricted` — безопасный default. Он использует `--sandbox workspace-write` + и разрешает редактирование worktree, но не гарантирует network, запись Git + metadata, push и доставку PR. +- `full-delivery` — явный opt-in. Codex запускается с + `--dangerously-bypass-approvals-and-sandbox`, поэтому при корректной `gh` + session и правах репозитория workflow может прочитать GitHub context, + изменить и проверить код, сделать commit/push и создать или обновить PR. + Это unsandboxed execution. + +```bash +start-issue 123 --batch \ + --batch-permissions full-delivery + +START_ISSUE_BATCH_PERMISSIONS=full-delivery \ + start-issue 123 --batch +``` + +Приоритет: CLI, environment, затем `restricted`. Full delivery расширяет только +технические возможности launcher и не разрешает destructive Git operations, +production/security changes или product decisions: для них prompt по-прежнему +обязан вернуть `STATUS: HUMAN_GATE`. Перед запуском проверьте `gh auth status`, +выбранный account, remote и write access к репозиторию. Перед ожиданием решения по конфликту branch/worktree команда печатает `Waiting for input: ...`, а перед передачей управления agent — `Handing off to in `. Ненулевой код `codex exec` считается -ошибкой human-gate и возвращается как код 1; сохраненные events и thread id +ошибкой batch run и возвращается как код 1; сохраненные events и thread id остаются доступными для диагностики. Отдельная справка: ```bash -start-issue --human-gate-help +start-issue --batch-help ``` +Пошаговая проверка и готовый end-to-end пример находятся в +[инструкции по Codex batch mode](doc/batch-mode.ru.md). + Контракт prompt: - Финальное сообщение должно содержать ровно одну terminal status line: `STATUS: DONE` или `STATUS: HUMAN_GATE`. @@ -118,7 +148,7 @@ State files: Codex из checkout `start-issue` выполните opt-in команду: ```bash -START_ISSUE_E2E=1 make e2e-human-gate +START_ISSUE_E2E=1 make e2e-batch ``` Скрипт использует приватный репозиторий `dapi/start-issue-e2e-fixture` и его @@ -129,23 +159,34 @@ control issue, требует авторизованный `gh`, не допус ```bash START_ISSUE_E2E=1 \ -test/e2e/human-gate.sh --scenario human-gate +test/e2e/batch.sh --scenario human-gate ``` Выйдите из возобновлённой Codex-сессии, после чего скрипт проверит артефакты. +Для реальной проверки commit, push и создания PR в private fixture используется +отдельно подтверждаемый unsandboxed scenario. Он сохраняет уникальные remote +branch, PR и локальный diagnostic fixture как evidence: + +```bash +START_ISSUE_E2E=1 START_ISSUE_E2E_FULL_DELIVERY=1 \ + test/e2e/batch.sh --scenario full-delivery +``` + #### Сценарии и проверки | Сценарий | Команда | Что проверяется | | --- | --- | --- | -| `done` | `START_ISSUE_E2E=1 make e2e-human-gate` | Реальный Codex batch run выдаёт `thread.started`, сохраняет `thread-id`, `events.jsonl` и `last-message.txt`, заканчивается `STATUS: DONE` и не меняет fixture worktree за пределами `.start-issue` state. | -| `human-gate` | `START_ISSUE_E2E=1 test/e2e/human-gate.sh --scenario human-gate` | Те же проверки артефактов и чистоты worktree, а также явный handoff `codex resume --include-non-interactive `. Перед завершением скрипта оператор выходит из возобновлённой interactive session. | +| `done` | `START_ISSUE_E2E=1 make e2e-batch` | Реальный Codex batch run выдаёт `thread.started`, сохраняет `thread-id`, `events.jsonl` и `last-message.txt`, заканчивается `STATUS: DONE` и не меняет fixture worktree за пределами `.start-issue` state. | +| `human-gate` | `START_ISSUE_E2E=1 test/e2e/batch.sh --scenario human-gate` | Те же проверки артефактов и чистоты worktree, а также явный handoff `codex resume --include-non-interactive `. Перед завершением скрипта оператор выходит из возобновлённой interactive session. | +| `full-delivery` | `START_ISSUE_E2E=1 START_ISSUE_E2E_FULL_DELIVERY=1 test/e2e/batch.sh --scenario full-delivery` | Текущий Codex принимает global full-delivery option и выполняет уникальные fixture commit, push и PR; runner печатает сохранённые PR URL и local artifact path. | -Оба сценария проверяют авторизованный `gh`, реальный, а не fake Codex binary, и +Все сценарии проверяют авторизованный `gh`, реальный, а не fake Codex binary, и обязательный интерфейс справки `codex exec` (`--output-last-message`, без устаревшего флага `--ask-for-approval`). Выбранный Codex executable печатается -в test output. Они не доказывают поведение приложения за пределами human-gate -protocol и намеренно не входят в CI. +в test output. `done` и `human-gate` не доказывают поведение приложения за +пределами protocol; `full-delivery` дополнительно проверяет явно разрешённую +доставку в fixture. Все сценарии намеренно не входят в CI. ### CI sandbox E2E @@ -168,7 +209,7 @@ start-issue https://github.com/owner/repo/issues/123 start-issue 123 --repo owner/repo --base develop start-issue 123 --agent codex start-issue 123 --agent codex --model gpt-5.2 -start-issue 123 --agent codex --human-gate +start-issue 123 --batch start-issue 123 --agent kimi --prompt-file .start-issue/prompt.md start-issue 123 --no-agent start-issue 123 --dry-run @@ -180,7 +221,7 @@ start-issue update start-issue --update start-issue install start-issue --install -start-issue --human-gate-help +start-issue --batch-help ``` Запуск `start-issue` без issue печатает обычную справку, а также текущий @@ -221,7 +262,7 @@ CLI entrypoint — `cmd/start-issue`; runtime, build и тесты реализ - Helpers repository/worktree получают metadata issue, безопасно планируют reuse и запускают optional hook `init.sh` внутри подготовленной worktree. - Helpers agent adapter валидируют agent, строят launch commands, генерируют AI - branch names и выполняют Codex human-gate mode. + branch names и выполняют Codex batch mode. - Helpers release выбирают platform assets, проверяют checksum и staged `--version`, затем атомарно устанавливают update. @@ -259,8 +300,11 @@ boundaries. Новые возможности должны сохранять э | `--prompt TEXT` | Inline prompt template для выбранного агента. С `init` - prompt template, который нужно записать. Нельзя использовать вместе с `--prompt-file`. | | `--prompt-file PATH` | Файл prompt template для выбранного агента. С `init` - содержимое файла, которое нужно записать. Нельзя использовать вместе с `--prompt`. | | `--improve-prompt` | Попросить выбранного агента сгенерировать reviewable proposal улучшенного prompt template и выйти до создания worktree. | -| `--human-gate` | Codex-only batch mode для issue workflow. Запускает `codex exec`, выходит на `STATUS: DONE` и резюмирует ту же сессию на `STATUS: HUMAN_GATE`. | -| `--human-gate-help` | Показать отдельную справку по Codex human-gate workflow: prompt contract, exit codes и state files. | +| `--batch` | Codex-only batch mode для issue workflow. Запускает `codex exec`, выходит на `STATUS: DONE` и резюмирует ту же сессию на `STATUS: HUMAN_GATE`. | +| `--human-gate` | Совместимый alias для `--batch`. | +| `--batch-permissions restricted\|full-delivery` | Выбрать capability contract batch mode. Требует `--batch` или его legacy alias; CLI имеет приоритет над `START_ISSUE_BATCH_PERMISSIONS`; default — `restricted`. | +| `--batch-help` | Показать отдельную справку по Codex batch mode: `HUMAN_GATE`, exit codes и state files. | +| `--human-gate-help` | Совместимый alias для `--batch-help`. | | `--prompt-output-file PATH` | Путь для proposal-файла в режиме `--improve-prompt`. | | `--no-init` | Не запускать `init.sh`, даже если он есть в созданном worktree. | | `--command COMMAND`, `-c COMMAND` | Префикс Claude command для стандартного Claude prompt. Значение по умолчанию: `/task-router:route-task`. | @@ -291,6 +335,7 @@ boundaries. Новые возможности должны сохранять э | `START_ISSUE_PROMPT` | Inline prompt template, который используется, если prompt не задан через CLI. Перебивает project и user prompt files. Нельзя использовать вместе с `START_ISSUE_PROMPT_FILE`, когда prompt не задан через CLI. | | `START_ISSUE_PROMPT_FILE` | Файл prompt template, который используется, если prompt не задан через CLI. Перебивает project и user prompt files. Нельзя использовать вместе с `START_ISSUE_PROMPT`, когда prompt не задан через CLI. | | `START_ISSUE_WORKTREE_DIR` | Родительская директория по умолчанию для создаваемых worktree, если `--worktree-dir` не передан. Встроенное значение по умолчанию: `~/worktrees`. | +| `START_ISSUE_BATCH_PERMISSIONS` | Capability contract batch mode при отсутствии CLI option: `restricted` или `full-delivery`. Built-in default: `restricted`. | | `START_ISSUE_DUMP_PROMPT` | Если задана в `1`, dry-run выводит полный rendered prompt вместо краткой информации. | ## Файлы конфигурации diff --git a/cmd/start-issue/main.go b/cmd/start-issue/main.go index eb0d6bd..ab9257c 100644 --- a/cmd/start-issue/main.go +++ b/cmd/start-issue/main.go @@ -57,11 +57,12 @@ func versionFromBuildInfo(info *debug.BuildInfo, fallback ...string) string { } type options struct { - repo, base, worktreeDir, agent, model, promptFile, prompt, command string - promptOutput, worktreeDirSource string - issue string - dryRun, noInit, flat, ai, improvePrompt, humanGate, project, user, force bool - mode string + repo, base, worktreeDir, agent, model, promptFile, prompt, command string + promptOutput, worktreeDirSource string + batchPermissions, batchPermissionsSource string + issue string + dryRun, noInit, flat, ai, improvePrompt, batch, project, user, force bool + mode string } type issue struct { @@ -138,10 +139,18 @@ func main() { } func parse(args []string) (options, error) { - o := options{worktreeDir: os.Getenv("START_ISSUE_WORKTREE_DIR")} + o := options{ + worktreeDir: os.Getenv("START_ISSUE_WORKTREE_DIR"), + batchPermissions: "restricted", + batchPermissionsSource: "built-in default", + } if o.worktreeDir != "" { o.worktreeDirSource = "START_ISSUE_WORKTREE_DIR" } + if permissions := os.Getenv("START_ISSUE_BATCH_PERMISSIONS"); permissions != "" { + o.batchPermissions = permissions + o.batchPermissionsSource = "START_ISSUE_BATCH_PERMISSIONS" + } var err error for len(args) > 0 { a := args[0] @@ -197,8 +206,13 @@ func parse(args []string) (options, error) { o.improvePrompt = true case "--prompt-output-file": o.promptOutput, err = value() - case "--human-gate": - o.humanGate = true + case "--batch", "--human-gate": + o.batch = true + case "--batch-permissions": + o.batchPermissions, err = value() + if err == nil { + o.batchPermissionsSource = "CLI" + } case "--project": o.project = true case "--user": @@ -211,8 +225,8 @@ func parse(args []string) (options, error) { return o, fmt.Errorf("Use only one command mode; got %s and %s.", o.mode, mode) } o.mode = mode - case "--human-gate-help": - humanGateHelp() + case "--batch-help", "--human-gate-help": + batchHelp() os.Exit(0) default: if strings.HasPrefix(a, "-") { @@ -239,6 +253,12 @@ func parse(args []string) (options, error) { if o.mode != "" && o.issue != "" { return o, fmt.Errorf("Use either %s or , not both.", o.mode) } + if o.batchPermissionsSource == "CLI" && !o.batch { + return o, errors.New("--batch-permissions requires --batch.") + } + if !validBatchPermissions(o.batchPermissions) { + return o, fmt.Errorf("Invalid batch permissions %q. Use restricted or full-delivery.", o.batchPermissions) + } if o.worktreeDir == "" && o.mode == "" { home, err := userHomeDir() if err != nil { @@ -250,6 +270,10 @@ func parse(args []string) (options, error) { return o, nil } +func validBatchPermissions(value string) bool { + return value == "restricted" || value == "full-delivery" +} + func userHomeDir() (string, error) { home, err := os.UserHomeDir() if err != nil { @@ -282,8 +306,8 @@ func runWithReader(o options, reader *bufio.Reader) error { if err != nil { return err } - if o.humanGate && agent != "codex" { - return fmt.Errorf("--human-gate requires agent 'codex'. Current agent: %s.", agent) + if o.batch && agent != "codex" { + return fmt.Errorf("--batch requires agent 'codex'. Current agent: %s.", agent) } if o.improvePrompt && agent == "none" { return errors.New("--improve-prompt requires an agent. Use --agent claude, codex, kimi, or pi.") @@ -424,8 +448,8 @@ func runWithReader(o options, reader *bufio.Reader) error { rendered := renderIssuePrompt(prompt, issueURL, number, in, labels, repo, branch, worktree, o.base) if o.dryRun { fmt.Printf(" [DRY-RUN] Would run: git worktree add -b %s %s %s\n", branch, worktree, o.base) - if o.humanGate { - return humanGate(model, worktree, rendered, true) + if o.batch { + return runBatch(model, worktree, rendered, o.batchPermissions, o.batchPermissionsSource, true) } return launchSelected(options{dryRun: true}, agent, model, worktree, rendered) } @@ -1742,8 +1766,8 @@ func canonicalPath(path string) string { } func launchSelected(o options, agent, model, worktree, prompt string) error { if o.dryRun { - if o.humanGate { - return humanGate(model, worktree, prompt, true) + if o.batch { + return runBatch(model, worktree, prompt, o.batchPermissions, o.batchPermissionsSource, true) } if agent == "none" { printManualNextSteps(model, worktree) @@ -1752,11 +1776,11 @@ func launchSelected(o options, agent, model, worktree, prompt string) error { printLaunch(agent, model, worktree, prompt) return nil } - if o.humanGate { + if o.batch { if !o.dryRun { printAgentHandoff(agent, worktree) } - return humanGate(model, worktree, prompt, false) + return runBatch(model, worktree, prompt, o.batchPermissions, o.batchPermissionsSource, false) } if !o.dryRun && agent != "none" { printAgentHandoff(agent, worktree) @@ -1824,16 +1848,21 @@ func normalizePromptProposal(result string) string { } return strings.TrimSpace(strings.Join(lines, "\n")) } -func humanGate(model, worktree, prompt string, dryRun bool) error { +func runBatch(model, worktree, prompt, permissions, permissionsSource string, dryRun bool) error { runID := os.Getenv("START_ISSUE_RUN_ID") if runID == "" { runID = time.Now().Format("20060102-150405") } dir := filepath.Join(worktree, ".start-issue", "runs", runID) events, last := filepath.Join(dir, "events.jsonl"), filepath.Join(dir, "last-message.txt") - args := []string{"exec", "--cd", worktree, "--sandbox", "workspace-write", "--json", "--output-last-message", last, "-"} - if model != "" { - args = append([]string{"exec", "--model", model}, args[1:]...) + args := batchArgs(model, worktree, last, permissions) + fmt.Printf(" State dir: %s\n", dir) + fmt.Printf(" Batch permissions: %s (%s)\n", permissions, permissionsSource) + if permissions == "full-delivery" { + fmt.Println(" WARNING: Codex will run without approvals or sandboxing for GitHub and Git delivery.") + fmt.Println(" Requires authenticated GitHub access and repository write permission; destructive or production actions still require HUMAN_GATE.") + } else { + fmt.Println(" Restricted mode: working-tree edits only; network, Git metadata writes, push, and PR delivery are not guaranteed.") } if dryRun { threadID := filepath.Join(dir, "thread-id") @@ -1888,6 +1917,21 @@ func humanGate(model, worktree, prompt string, dryRun bool) error { return fmt.Errorf("No recognized final status found. Inspect: %s", last) } +func batchArgs(model, worktree, lastMessage, permissions string) []string { + args := []string{} + if model != "" { + args = append(args, "--model", model) + } + if permissions == "full-delivery" { + args = append(args, "--dangerously-bypass-approvals-and-sandbox") + } + args = append(args, "exec", "--cd", worktree) + if permissions == "restricted" { + args = append(args, "--sandbox", "workspace-write") + } + return append(args, "--json", "--output-last-message", lastMessage, "-") +} + func captureThreadID(events string) (string, error) { eventsBody, err := os.ReadFile(events) if err != nil { @@ -1902,7 +1946,7 @@ func captureThreadID(events string) (string, error) { return event.ThreadID, nil } } - return "", fmt.Errorf("Codex human-gate run did not capture thread_id. Inspect: %s", events) + return "", fmt.Errorf("Codex batch run did not capture thread_id. Inspect: %s", events) } func finalStatus(body string) string { @@ -2158,8 +2202,13 @@ Options: --prompt-file Prompt template file for the launched agent --improve-prompt Ask the selected agent to improve the selected prompt template and write a reviewable proposal - --human-gate Codex-only batch mode that resumes on HUMAN_GATE - --human-gate-help Show detailed help for the human-gate mode + --batch Run Codex autonomously until DONE or HUMAN_GATE + --human-gate Compatibility alias for --batch + --batch-permissions + Requires --batch; capability contract for the run + Default: START_ISSUE_BATCH_PERMISSIONS or restricted + --batch-help Show detailed help for Codex batch mode + --human-gate-help Compatibility alias for --batch-help --prompt-output-file Output path for --improve-prompt proposal --no-init Skip init.sh execution @@ -2215,6 +2264,7 @@ Environment variables: START_ISSUE_PROMPT START_ISSUE_PROMPT_FILE START_ISSUE_WORKTREE_DIR + START_ISSUE_BATCH_PERMISSIONS START_ISSUE_DUMP_PROMPT Examples: @@ -2223,7 +2273,8 @@ Examples: start-issue 123 --repo owner/repo --base develop start-issue 123 --agent codex start-issue 123 --agent codex --model gpt-5.2 - start-issue 123 --agent codex --human-gate + start-issue 123 --batch + start-issue 123 --batch --batch-permissions full-delivery start-issue 123 --agent claude --model sonnet start-issue 123 --agent kimi --prompt-file .start-issue/prompt.md start-issue 123 --no-agent # Only create worktree @@ -2240,7 +2291,7 @@ Examples: start-issue --update start-issue install start-issue --install - start-issue --human-gate-help + start-issue --batch-help `, runningVersion()) } @@ -2248,18 +2299,53 @@ func printBanner() { fmt.Printf("start-issue v%s\n\n", runningVersion()) } -func humanGateHelp() { +func batchHelp() { printBanner() - fmt.Println(`Codex human-gate mode + fmt.Println(`Codex batch mode + +What it is: + --batch runs Codex autonomously through codex exec instead of opening the + interactive UI immediately. Codex works until its final message reports + STATUS: DONE or STATUS: HUMAN_GATE. HUMAN_GATE resumes the exact saved + thread so the operator can make the required decision. + + Batch mode requires the resolved agent to be codex. The examples below + assume codex is already selected in project or user configuration. Usage: - start-issue --agent codex --human-gate - start-issue --human-gate-help + start-issue --batch + start-issue --batch \ + --batch-permissions full-delivery + start-issue --batch-help + +Compatibility aliases: + --human-gate Same behavior as --batch. + --human-gate-help Same behavior as --batch-help. + +Permission modes: + restricted (default) + Uses Codex workspace-write sandboxing. Working-tree edits are supported, + but network access, Git metadata writes, push, and PR delivery are not + guaranteed. Select with START_ISSUE_BATCH_PERMISSIONS=restricted or + --batch-permissions restricted. + + full-delivery (explicit opt-in) + Runs Codex with --dangerously-bypass-approvals-and-sandbox so a normal + issue workflow can read GitHub context, edit, test, commit, push, and + create or update a PR. This is unsandboxed execution. It requires an + authenticated gh session and repository write permission. It does not + authorize destructive, production, security, or product decisions; those + still require STATUS: HUMAN_GATE. + +Precedence: + --batch-permissions, START_ISSUE_BATCH_PERMISSIONS, restricted. + --batch-permissions requires --batch or its --human-gate alias. Flow: The normal issue workflow creates or reuses the worktree, renders the - prompt, and runs Codex in batch mode. The final message must contain one - terminal status line: STATUS: DONE or STATUS: HUMAN_GATE. + prompt, and runs Codex in batch mode. start-issue saves the event stream, + last message, and thread id. The final message must contain one terminal + status line: STATUS: DONE or STATUS: HUMAN_GATE. Exit codes: 0 Codex returned STATUS: DONE. @@ -2280,6 +2366,10 @@ Final status examples: Troubleshooting: Inspect events.jsonl and last-message.txt when batch parsing fails. The explicit thread id is saved before status handling when available. + If restricted mode cannot read GitHub or write Git metadata, either finish + delivery manually or explicitly select full-delivery after reviewing its risk. + If full delivery cannot push or create a PR, verify gh auth status, the + selected GitHub account, remote URL, and repository permissions. If automatic resume fails, run: codex resume --include-non-interactive `) } diff --git a/cmd/start-issue/main_test.go b/cmd/start-issue/main_test.go index cace48e..de4f9ba 100644 --- a/cmd/start-issue/main_test.go +++ b/cmd/start-issue/main_test.go @@ -58,6 +58,52 @@ func TestParseTracksWorktreeDirectorySource(t *testing.T) { } } +func TestParseBatchPermissionsPrecedenceAndValidation(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + t.Setenv("START_ISSUE_BATCH_PERMISSIONS", "") + + o, err := parse([]string{"1"}) + if err != nil { + t.Fatal(err) + } + if o.batchPermissions != "restricted" || o.batchPermissionsSource != "built-in default" { + t.Fatalf("default permissions = %q (%s)", o.batchPermissions, o.batchPermissionsSource) + } + + t.Setenv("START_ISSUE_BATCH_PERMISSIONS", "full-delivery") + o, err = parse([]string{"1"}) + if err != nil { + t.Fatal(err) + } + if o.batchPermissions != "full-delivery" || o.batchPermissionsSource != "START_ISSUE_BATCH_PERMISSIONS" { + t.Fatalf("environment permissions = %q (%s)", o.batchPermissions, o.batchPermissionsSource) + } + + o, err = parse([]string{"1", "--batch", "--batch-permissions", "restricted"}) + if err != nil { + t.Fatal(err) + } + if !o.batch || o.batchPermissions != "restricted" || o.batchPermissionsSource != "CLI" { + t.Fatalf("CLI permissions = %q (%s)", o.batchPermissions, o.batchPermissionsSource) + } + + legacy, err := parse([]string{"1", "--human-gate"}) + if err != nil || !legacy.batch { + t.Fatalf("legacy --human-gate alias = %+v, %v", legacy, err) + } + + if _, err := parse([]string{"1", "--batch-permissions", "restricted"}); err == nil || !strings.Contains(err.Error(), "requires --batch") { + t.Fatalf("permission flag without batch error = %v", err) + } + if _, err := parse([]string{"1", "--batch", "--batch-permissions", "unlimited"}); err == nil || !strings.Contains(err.Error(), "Use restricted or full-delivery") { + t.Fatalf("invalid CLI permissions error = %v", err) + } + t.Setenv("START_ISSUE_BATCH_PERMISSIONS", "unlimited") + if _, err := parse([]string{"1"}); err == nil || !strings.Contains(err.Error(), "Use restricted or full-delivery") { + t.Fatalf("invalid environment permissions error = %v", err) + } +} + func TestUserHomeDirRejectsUnavailableOrRelativeHome(t *testing.T) { t.Setenv("HOME", "") if runtime.GOOS != "windows" { @@ -1116,7 +1162,12 @@ func TestUsageListsCompatibilityEntryPoints(t *testing.T) { "--setup", "--update", "--install", + "--batch", + "--human-gate Compatibility alias for --batch", + "--batch-help", "--human-gate-help", + "--batch-permissions ", + "START_ISSUE_BATCH_PERMISSIONS", "Agent selection precedence:", ".start-issue/agent in the git root", "Prompt template precedence:", @@ -1129,6 +1180,26 @@ func TestUsageListsCompatibilityEntryPoints(t *testing.T) { } } +func TestBatchHelpExplainsPermissionContract(t *testing.T) { + output := captureStdout(t, batchHelp) + for _, want := range []string{ + "restricted (default)", + "full-delivery (explicit opt-in)", + "--dangerously-bypass-approvals-and-sandbox", + "authenticated gh session", + "repository write permission", + "destructive, production, security, or product decisions", + "START_ISSUE_BATCH_PERMISSIONS", + "--human-gate Same behavior as --batch", + "STATUS: DONE or STATUS: HUMAN_GATE", + "gh auth status", + } { + if !strings.Contains(output, want) { + t.Fatalf("batch help missing %q:\n%s", want, output) + } + } +} + func TestAIBranchPromptPreservesTransliterationAndTagConstraints(t *testing.T) { bin, log := t.TempDir(), filepath.Join(t.TempDir(), "prompt") writeExecutable(t, filepath.Join(bin, "pi"), "#!/bin/sh\nlast=''\nfor arg do last=$arg; done\nprintf '%s' \"$last\" > '"+log+"'\nprintf '%s\\n' feature/issue-34-ispravit-tsap\n") @@ -1763,7 +1834,7 @@ func TestHelperArgsAreNonInteractive(t *testing.T) { } } -func TestHumanGateSavesThreadIDBeforeDone(t *testing.T) { +func TestBatchSavesThreadIDBeforeDone(t *testing.T) { worktree, bin := t.TempDir(), t.TempDir() writeFakeCodex(t, bin) t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) @@ -1772,7 +1843,7 @@ func TestHumanGateSavesThreadIDBeforeDone(t *testing.T) { t.Setenv("CODEX_LAST", "STATUS: DONE") t.Setenv("START_ISSUE_FAKE_CODEX_REJECT_ASK_FOR_APPROVAL", "1") - if err := humanGate("", worktree, "prompt", false); err != nil { + if err := runBatch("", worktree, "prompt", "restricted", "built-in default", false); err != nil { t.Fatal(err) } threadID, err := os.ReadFile(filepath.Join(worktree, ".start-issue", "runs", "done", "thread-id")) @@ -1781,7 +1852,7 @@ func TestHumanGateSavesThreadIDBeforeDone(t *testing.T) { } } -func TestHumanGateSavesThreadIDWhenFinalMessageIsMissing(t *testing.T) { +func TestBatchSavesThreadIDWhenFinalMessageIsMissing(t *testing.T) { worktree, bin := t.TempDir(), t.TempDir() writeFakeCodex(t, bin) t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) @@ -1789,9 +1860,9 @@ func TestHumanGateSavesThreadIDWhenFinalMessageIsMissing(t *testing.T) { t.Setenv("CODEX_EVENTS", `{"type":"thread.started","thread_id":"thread-recovery"}`) t.Setenv("CODEX_SKIP_LAST", "1") - err := humanGate("", worktree, "prompt", false) + err := runBatch("", worktree, "prompt", "restricted", "built-in default", false) if err == nil || !strings.Contains(err.Error(), "No recognized final status found") { - t.Fatalf("humanGate error = %v, want missing final-status error", err) + t.Fatalf("runBatch error = %v, want missing final-status error", err) } threadID, readErr := os.ReadFile(filepath.Join(worktree, ".start-issue", "runs", "missing-last-message", "thread-id")) if readErr != nil || string(threadID) != "thread-recovery\n" { @@ -1799,7 +1870,7 @@ func TestHumanGateSavesThreadIDWhenFinalMessageIsMissing(t *testing.T) { } } -func TestHumanGateExecFailureReturnsExitCodeOne(t *testing.T) { +func TestBatchExecFailureReturnsExitCodeOne(t *testing.T) { worktree, bin := t.TempDir(), t.TempDir() writeFakeCodex(t, bin) t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) @@ -1808,10 +1879,10 @@ func TestHumanGateExecFailureReturnsExitCodeOne(t *testing.T) { t.Setenv("CODEX_LAST", "STATUS: DONE") t.Setenv("CODEX_EXEC_EXIT", "42") - err := humanGate("", worktree, "prompt", false) + err := runBatch("", worktree, "prompt", "restricted", "built-in default", false) var exit exitError if !errors.As(err, &exit) || exit.code != 1 { - t.Fatalf("got %T %v, want human-gate exit code 1", err, err) + t.Fatalf("got %T %v, want batch exit code 1", err, err) } if !strings.Contains(err.Error(), "Codex batch run failed") { t.Fatalf("error = %v, want batch failure diagnostic", err) @@ -1831,16 +1902,18 @@ func TestRunChecksForGitBeforeRepositoryValidation(t *testing.T) { } } -func TestHumanGateDryRunShowsAllStateArtifacts(t *testing.T) { +func TestBatchDryRunShowsAllStateArtifacts(t *testing.T) { worktree := t.TempDir() t.Setenv("START_ISSUE_RUN_ID", "plan") dir := filepath.Join(worktree, ".start-issue", "runs", "plan") output := captureStdout(t, func() { - if err := humanGate("", worktree, "prompt", true); err != nil { + if err := runBatch("", worktree, "prompt", "restricted", "built-in default", true); err != nil { t.Fatal(err) } }) for _, want := range []string{ + "Batch permissions: restricted (built-in default)", + "Restricted mode: working-tree edits only", "--output-last-message " + filepath.Join(dir, "last-message.txt"), "> " + filepath.Join(dir, "events.jsonl"), "Would write captured thread ID: " + filepath.Join(dir, "thread-id"), @@ -1853,11 +1926,78 @@ func TestHumanGateDryRunShowsAllStateArtifacts(t *testing.T) { t.Fatalf("dry-run includes obsolete --ask-for-approval argument:\n%s", output) } if _, err := os.Stat(dir); !os.IsNotExist(err) { - t.Fatalf("human-gate dry-run created state directory: %v", err) + t.Fatalf("batch dry-run created state directory: %v", err) } } -func TestHumanGatePreservesCallerWorkingDirectory(t *testing.T) { +func TestBatchArgsMapPermissionModesInSupportedOrder(t *testing.T) { + worktree := "/tmp/worktree" + last := "/tmp/last-message.txt" + restricted := batchArgs("gpt-test", worktree, last, "restricted") + if got, want := fmt.Sprint(restricted), "[--model gpt-test exec --cd /tmp/worktree --sandbox workspace-write --json --output-last-message /tmp/last-message.txt -]"; got != want { + t.Fatalf("restricted args = %s, want %s", got, want) + } + fullDelivery := batchArgs("gpt-test", worktree, last, "full-delivery") + if got, want := fmt.Sprint(fullDelivery), "[--model gpt-test --dangerously-bypass-approvals-and-sandbox exec --cd /tmp/worktree --json --output-last-message /tmp/last-message.txt -]"; got != want { + t.Fatalf("full-delivery args = %s, want %s", got, want) + } +} + +func TestBatchFullDeliveryDryRunShowsResolvedModeAndCommand(t *testing.T) { + worktree := t.TempDir() + t.Setenv("START_ISSUE_RUN_ID", "full-delivery-plan") + output := captureStdout(t, func() { + err := launchSelected(options{ + dryRun: true, + batch: true, + batchPermissions: "full-delivery", + batchPermissionsSource: "CLI", + }, "codex", "gpt-test", worktree, "prompt") + if err != nil { + t.Fatal(err) + } + }) + wantCommand := "codex --model gpt-test --dangerously-bypass-approvals-and-sandbox exec --cd " + for _, want := range []string{ + "Batch permissions: full-delivery (CLI)", + wantCommand, + "WARNING: Codex will run without approvals or sandboxing", + } { + if !strings.Contains(output, want) { + t.Fatalf("full-delivery dry-run missing %q:\n%s", want, output) + } + } + if strings.Contains(output, "--sandbox workspace-write") { + t.Fatalf("full-delivery dry-run retained restricted sandbox:\n%s", output) + } +} + +func TestBatchFullDeliveryReportsWarningAndCompletes(t *testing.T) { + worktree, bin := t.TempDir(), t.TempDir() + writeFakeCodex(t, bin) + t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("START_ISSUE_RUN_ID", "full-delivery") + t.Setenv("CODEX_EVENTS", `{"type":"thread.started","thread_id":"thread-full-delivery"}`) + t.Setenv("CODEX_LAST", "STATUS: DONE") + + output := captureStdout(t, func() { + if err := runBatch("gpt-test", worktree, "prompt", "full-delivery", "CLI", false); err != nil { + t.Fatal(err) + } + }) + for _, want := range []string{ + "Batch permissions: full-delivery (CLI)", + "WARNING: Codex will run without approvals or sandboxing", + "destructive or production actions still require HUMAN_GATE", + "STATUS: DONE", + } { + if !strings.Contains(output, want) { + t.Fatalf("full-delivery output missing %q:\n%s", want, output) + } + } +} + +func TestBatchPreservesCallerWorkingDirectory(t *testing.T) { worktree, bin, log := t.TempDir(), t.TempDir(), filepath.Join(t.TempDir(), "cwd") writeFakeCodex(t, bin) t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) @@ -1869,7 +2009,7 @@ func TestHumanGatePreservesCallerWorkingDirectory(t *testing.T) { if err != nil { t.Fatal(err) } - if err := humanGate("", worktree, "prompt", false); err != nil { + if err := runBatch("", worktree, "prompt", "restricted", "built-in default", false); err != nil { t.Fatal(err) } got, err := os.ReadFile(log) @@ -1881,7 +2021,7 @@ func TestHumanGatePreservesCallerWorkingDirectory(t *testing.T) { } } -func TestHumanGateRejectsDoneWithoutThreadID(t *testing.T) { +func TestBatchRejectsDoneWithoutThreadID(t *testing.T) { worktree, bin := t.TempDir(), t.TempDir() writeFakeCodex(t, bin) t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) @@ -1889,13 +2029,13 @@ func TestHumanGateRejectsDoneWithoutThreadID(t *testing.T) { t.Setenv("CODEX_EVENTS", `{"type":"item.completed"}`) t.Setenv("CODEX_LAST", "STATUS: DONE") - err := humanGate("", worktree, "prompt", false) + err := runBatch("", worktree, "prompt", "restricted", "built-in default", false) if err == nil || !strings.Contains(err.Error(), "did not capture thread_id") { t.Fatalf("got %v", err) } } -func TestHumanGateResumeFailureReturnsExitCodeTwo(t *testing.T) { +func TestBatchResumeFailureReturnsExitCodeTwo(t *testing.T) { worktree, bin := t.TempDir(), t.TempDir() writeFakeCodex(t, bin) t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) @@ -1904,7 +2044,7 @@ func TestHumanGateResumeFailureReturnsExitCodeTwo(t *testing.T) { t.Setenv("CODEX_LAST", "STATUS: HUMAN_GATE") t.Setenv("CODEX_RESUME_EXIT", "1") - err := humanGate("", worktree, "prompt", false) + err := runBatch("", worktree, "prompt", "restricted", "built-in default", false) var exit exitError if !errors.As(err, &exit) || exit.code != 2 { t.Fatalf("got %T %v", err, err) @@ -1928,9 +2068,28 @@ fi if [ -n "$START_ISSUE_CWD_LOG" ]; then pwd > "$START_ISSUE_CWD_LOG" fi +while [ "$#" -gt 0 ] && [ "$1" != "exec" ] && [ "$1" != "resume" ]; do + case "$1" in + --model) + shift 2 + ;; + --dangerously-bypass-approvals-and-sandbox) + shift + ;; + *) + printf '%s\n' "unexpected global option: $1" >&2 + exit 1 + ;; + esac +done if [ "$1" = "exec" ]; then + shift last="" while [ "$#" -gt 0 ]; do + if [ "$1" = "--dangerously-bypass-approvals-and-sandbox" ]; then + printf '%s\n' "full-delivery option must precede exec" >&2 + exit 1 + fi if [ "$1" = "--output-last-message" ]; then last="$2" shift 2 diff --git a/doc/batch-mode.md b/doc/batch-mode.md new file mode 100644 index 0000000..5ee3daf --- /dev/null +++ b/doc/batch-mode.md @@ -0,0 +1,195 @@ +# Codex batch mode: autonomous work with a human gate + +This guide is for someone seeing `start-issue --batch` for the first time. It +explains what batch mode does, why a run may stop at `HUMAN_GATE`, how the +operator continues the same Codex session, and when the agent needs restricted +or full-delivery permissions. For the complete CLI contract, see +[spec.md](spec.md). + +The examples assume that `codex` is already selected by project or user +configuration. The released `--human-gate` flag remains available as a +compatibility alias for `--batch`. + +## What human-gate is + +Human-gate is a checkpoint where autonomous Codex work is either declared +complete or handed to a person for a decision. + +A normal `start-issue` run opens an interactive Codex session. With `--batch`, +Codex runs in batch mode and works through the issue without the +operator remaining in the interactive UI. Its final response must contain one +explicit result: + +- `STATUS: DONE` — the task is complete and no human action is needed; +- `STATUS: HUMAN_GATE` — continuing requires a human decision. + +Human-gate is not an approval prompt before every command and it is not a +sandbox mode. It defines how an autonomous run ends and when control returns to +the operator. + +## Why use it + +Without this protocol, an exit code alone cannot reliably distinguish finished +work from a run that stopped because of a question, missing access, or an +unresolved failure. Human-gate lets Codex implement and test unattended, report +unambiguous completion, stop before decisions it must not make, and resume the +same saved thread instead of starting over with a new agent. + +## How it works + +1. `start-issue` resolves the issue, creates or reuses the worktree, and renders + the prompt as usual. +2. It runs `codex exec` in batch mode instead of opening an interactive session. +3. It saves the event stream, final message, and `thread_id`. +4. Codex performs the task and ends with exactly one status line: + `STATUS: DONE` or `STATUS: HUMAN_GATE`. +5. `DONE` exits successfully. `HUMAN_GATE` opens the saved session with + `codex resume --include-non-interactive `, preserving all context + for the operator. +6. A Codex error or missing status produces an error while preserving the + diagnostic files. + +Codex decides whether to stop according to the prompt rules; `start-issue` +does not inspect every command. It reads the final status and performs the +corresponding handoff. + +## When Codex should stop + +`STATUS: HUMAN_GATE` is appropriate for destructive operations, missing +credentials, incompatible product choices, unresolved failures that cannot be +safely fixed within the issue, and production or security decisions. Routine +technical choices and recoverable errors should be handled by Codex without +stopping the run. + +## How permissions relate to human-gate + +These are separate controls: + +- `--batch` enables the batch run and `DONE` / `HUMAN_GATE` handoff; +- `--batch-permissions` controls which technical operations Codex can + perform during that run. + +Editing a worktree and delivering a PR cross different trust boundaries. A +sandboxed run can usually change and test code but may not be able to access +GitHub, write Git metadata, push, or create a PR. The safe boundary therefore +remains the default, while end-to-end delivery requires a visible opt-in. + +## Choose a mode + +Use `restricted` for normal working-tree implementation: + +```bash +start-issue 123 --batch +``` + +`restricted` is the default. Codex runs with the `workspace-write` sandbox. It +can edit and test files in the prepared worktree, but network access, Git +metadata writes, push, and pull-request delivery are not guaranteed. + +Use `full-delivery` only when the run must also read GitHub context, commit, +push, and create or update a pull request: + +```bash +start-issue 123 --batch \ + --batch-permissions full-delivery +``` + +This is an explicit opt-in to unsandboxed Codex execution. It grants technical +capability, not permission for destructive Git operations, production or +security changes, or unresolved product decisions. Codex must still return +`STATUS: HUMAN_GATE` when one of those decisions requires the operator. + +## Full-delivery preflight + +Before using `full-delivery`, check the selected GitHub account, remote, and +repository access: + +```bash +gh auth status +git remote get-url origin +gh repo view --json nameWithOwner,viewerPermission +``` + +Run a dry-run to verify the resolved mode and launch command without creating +a worktree or starting Codex: + +```bash +start-issue 123 --batch \ + --batch-permissions full-delivery \ + --dry-run +``` + +The output should report: + +```text +Batch permissions: full-delivery (CLI) +``` + +## Example: implement an issue and deliver its PR + +Assume issue `123` is in the current repository and the authenticated GitHub +account has write access. + +1. Inspect the planned run: + + ```bash + start-issue 123 --batch \ + --batch-permissions full-delivery \ + --dry-run + ``` + +2. Start the real run after reviewing the unsandboxed-execution warning: + + ```bash + start-issue 123 --batch \ + --batch-permissions full-delivery + ``` + +3. Codex can now implement and test the issue, commit the result, push the + issue branch, and create or update its pull request when the task and + repository state allow it. + +4. A final `STATUS: DONE` exits successfully. A final + `STATUS: HUMAN_GATE` opens the exact saved Codex thread for the operator. + +The CLI keeps diagnostic state under: + +```text +/.start-issue/runs//events.jsonl +/.start-issue/runs//last-message.txt +/.start-issue/runs//thread-id +``` + +## One-command environment override + +The environment variable is useful for one command or a controlled automation +wrapper: + +```bash +START_ISSUE_BATCH_PERMISSIONS=full-delivery \ + start-issue 123 --batch +``` + +The CLI option has higher precedence than the environment variable. Avoid +putting `full-delivery` in a global shell profile: keeping the opt-in visible at +the command site makes the unsandboxed boundary easier to review. + +## Troubleshooting + +- If `restricted` cannot access GitHub or write Git metadata, finish delivery + manually or rerun with an explicitly reviewed `full-delivery` selection. +- If full delivery cannot push or create a PR, recheck `gh auth status`, the + selected account, `origin`, and `viewerPermission`. +- If status parsing fails, inspect `events.jsonl` and `last-message.txt` in the + printed state directory. +- If automatic resume fails, use the saved thread id: + + ```bash + codex resume --include-non-interactive + ``` + +For the built-in reference, run: + +```bash +start-issue --batch-help +``` diff --git a/doc/batch-mode.ru.md b/doc/batch-mode.ru.md new file mode 100644 index 0000000..52cf0cc --- /dev/null +++ b/doc/batch-mode.ru.md @@ -0,0 +1,149 @@ +# Batch-режим Codex + +Флаг `--batch` запускает Codex без интерактивного интерфейса. `start-issue` +готовит рабочее дерево, формирует задание, запускает `codex exec` и ждёт +результат. + +Такой запуск удобен, когда не нужно наблюдать за каждым шагом агента. Codex +может сам изменить код и запустить тесты, а человек подключается после +завершения работы или когда без его решения продолжать нельзя. + +Команды ниже предполагают, что `codex` уже выбран в конфигурации проекта или +пользователя. Старый флаг `--human-gate` продолжает работать как alias для +`--batch`. + +## Как проходит запуск + +1. `start-issue` получает issue, создаёт или переиспользует рабочее дерево и + формирует задание. +2. Вместо обычной интерактивной сессии запускается `codex exec`. +3. Codex работает над задачей и в конце возвращает один из двух статусов: + `STATUS: DONE` или `STATUS: HUMAN_GATE`. +4. `start-issue` сохраняет журнал событий, последнее сообщение и `thread_id`. +5. При `DONE` команда завершается с кодом `0`. +6. При `HUMAN_GATE` открывается та же сессия через + `codex resume --include-non-interactive `. + +Если Codex завершился с ошибкой или не вернул статус, `start-issue` сообщает об +ошибке и сохраняет диагностические файлы. + +## Что означает HUMAN_GATE + +`HUMAN_GATE` означает, что агент дошёл до решения, которое нельзя безопасно +принять самостоятельно. Например: + +- нужно удалить данные или переписать историю Git; +- не хватает учётных данных или доступа; +- есть несколько несовместимых вариантов поведения продукта; +- действие затрагивает продакшен или безопасность; +- тест или конфликт нельзя исправить в рамках текущей задачи. + +Обычные технические вопросы и исправимые ошибки не должны приводить к +`HUMAN_GATE`: Codex должен разобраться с ними сам. + +Финальное сообщение Codex должно содержать ровно одну строку со статусом. При +`HUMAN_GATE` в сообщении также должен быть конкретный вопрос к человеку. + +`start-issue` не анализирует действия агента и сам не решает, когда нужна +остановка. Он читает статус из последнего сообщения Codex и либо завершает +команду, либо открывает сохранённую сессию. + +## Права batch-режима + +`--batch` определяет способ запуска Codex. `--batch-permissions` определяет, +что Codex может делать во время этого запуска. + +### restricted + +Режим по умолчанию: + +```bash +start-issue 123 --batch +``` + +Codex запускается в песочнице `workspace-write`. Он может изменять файлы +рабочего дерева и запускать тесты. Доступ к GitHub, запись метаданных Git, push +и создание PR не гарантируются. + +Этот режим подходит, если Codex должен подготовить изменения, а commit, push и +PR будут сделаны вручную. + +### full-delivery + +```bash +start-issue 123 --batch \ + --batch-permissions full-delivery +``` + +Codex запускается без песочницы и подтверждений. Он может получить контекст из +GitHub, изменить и проверить код, сделать commit и push, создать или обновить +pull request. + +Для этого нужны авторизованный `gh`, правильный `origin` и право записи в +репозиторий: + +```bash +gh auth status +git remote get-url origin +gh repo view --json nameWithOwner,viewerPermission +``` + +`full-delivery` даёт Codex технический доступ, но не разрешает удаление данных, +изменения продакшена, решения, связанные с безопасностью, и другие опасные +действия. Для них агент всё равно должен вернуть `STATUS: HUMAN_GATE`. + +## Проверка перед запуском + +Через `--dry-run` можно проверить выбранный режим и команду Codex без создания +рабочего дерева и запуска агента: + +```bash +start-issue 123 --batch \ + --batch-permissions full-delivery \ + --dry-run +``` + +В выводе должно быть: + +```text +Batch permissions: full-delivery (CLI) +``` + +После проверки запустите ту же команду без `--dry-run`. + +## Переключение через переменную окружения + +```bash +START_ISSUE_BATCH_PERMISSIONS=full-delivery \ + start-issue 123 --batch +``` + +Параметр командной строки имеет приоритет над переменной окружения. Не стоит +добавлять `full-delivery` в глобальный профиль оболочки: безопаснее включать +его явно для конкретного запуска. + +## Диагностика + +Файлы запуска сохраняются в рабочем дереве: + +```text +/.start-issue/runs//events.jsonl +/.start-issue/runs//last-message.txt +/.start-issue/runs//thread-id +``` + +- `events.jsonl` содержит события `codex exec`; +- `last-message.txt` содержит сообщение со статусом; +- `thread-id` содержит идентификатор сессии для продолжения работы. + +Если автоматическое продолжение сессии не сработало: + +```bash +codex resume --include-non-interactive +``` + +Полная встроенная справка: + +```bash +start-issue --batch-help +``` diff --git a/doc/spec.md b/doc/spec.md index 20ad2d7..55ceb34 100644 --- a/doc/spec.md +++ b/doc/spec.md @@ -71,8 +71,11 @@ Agent-specific behavior должен быть централизован за е | `--prompt` | Inline prompt template | См. приоритет prompt | | `--prompt-file` | Файл prompt template | См. приоритет prompt | | `--improve-prompt` | Сгенерировать reviewable proposal улучшенного prompt template и выйти до создания worktree | false | -| `--human-gate` | Codex-only batch mode для issue workflow с resume на `STATUS: HUMAN_GATE` | false | -| `--human-gate-help` | Показать отдельную справку по human-gate mode | false | +| `--batch` | Codex-only batch mode для issue workflow с resume на `STATUS: HUMAN_GATE` | false | +| `--human-gate` | Совместимый alias для `--batch` | false | +| `--batch-permissions ` | Capability contract для batch mode; требует `--batch` или его legacy alias; CLI имеет приоритет над `START_ISSUE_BATCH_PERMISSIONS` | `restricted` | +| `--batch-help` | Показать отдельную справку по batch mode и `HUMAN_GATE` handoff | false | +| `--human-gate-help` | Совместимый alias для `--batch-help` | false | | `--prompt-output-file` | Путь proposal-файла для `--improve-prompt` | Для `.md`: рядом с source как `*.improved.md`; для остальных файлов: `.improved`; иначе `.start-issue/prompt.improved.md` | | `--no-init` | Пропустить запуск `init.sh` | false | | `--command` / `-c` | Совместимый Claude command для дефолтного Claude prompt | `/task-router:route-task` | @@ -194,19 +197,24 @@ git rev-parse --show-toplevel - `gh` CLI с авторизованной GitHub session -## Codex human-gate mode +## Codex batch mode -`--human-gate` вводит отдельный Codex-only launch path для issue workflow. +`--batch` вводит отдельный Codex-only launch path для issue workflow. +Ранее выпущенный `--human-gate` является совместимым alias и включает тот же +внутренний mode без отдельной ветки поведения. Контракт режима: 1. Режим валиден только для `agent=codex`; для остальных agent он завершается явной ошибкой. 2. До agent launch workflow остается обычным: parse input, resolve config, fetch issue, plan branch, create/reuse worktree, run optional `init.sh`, render prompt. -3. Вместо интерактивного Codex launch выполняется: +3. Permission mode разрешается в порядке CLI + `--batch-permissions`, `START_ISSUE_BATCH_PERMISSIONS`, built-in + `restricted`. Другие значения отклоняются до issue fetch и worktree mutation. +4. В restricted mode вместо интерактивного Codex launch выполняется: ```bash -codex exec \ - [--model "$MODEL"] \ +codex [--model "$MODEL"] \ + exec \ --cd "$WORKTREE_PATH" \ --sandbox workspace-write \ --json \ @@ -214,28 +222,50 @@ codex exec \ - ``` -4. Rendered prompt передается в `codex exec` через stdin. -5. Из JSONL event stream извлекается `thread_id` из события `thread.started`. -6. Saved `last-message.txt` является единственным источником final status. -7. Поддерживаются только два terminal status: +5. В explicit full-delivery mode выполняется: + +```bash +codex [--model "$MODEL"] \ + --dangerously-bypass-approvals-and-sandbox \ + exec \ + --cd "$WORKTREE_PATH" \ + --json \ + --output-last-message "$STATE_DIR/last-message.txt" \ + - +``` + +6. Full delivery требует authenticated `gh`, корректный remote и repository + write permission. Это unsandboxed execution, но оно не авторизует destructive, + production/security или product decisions: они остаются `HUMAN_GATE`. +7. Rendered prompt передается в `codex exec` через stdin. +8. Из JSONL event stream извлекается `thread_id` из события `thread.started`. +9. Saved `last-message.txt` является единственным источником final status. +10. Поддерживаются только два terminal status: - `STATUS: DONE` - `STATUS: HUMAN_GATE` -8. На `STATUS: DONE` команда завершается с кодом `0`, не открывая Codex TUI. -9. На `STATUS: HUMAN_GATE` выполняется: +11. На `STATUS: DONE` команда завершается с кодом `0`, не открывая Codex TUI. +12. На `STATUS: HUMAN_GATE` выполняется: ```bash codex resume --include-non-interactive "$thread_id" ``` -10. `codex resume --last` не используется как primary mechanism. -11. `codex exec --ephemeral` не используется, потому что session должна быть resumable. +13. `codex resume --last` не используется как primary mechanism. +14. `codex exec --ephemeral` не используется, потому что session должна быть resumable. Dedicated help доступен через: ```bash -start-issue --human-gate-help +start-issue --batch-help ``` +`start-issue --human-gate-help` сохраняется как совместимый alias. + +Практические инструкции и пример полного запуска: + +- [English](batch-mode.md) +- [Русский](batch-mode.ru.md) + Там документируются: - полный flow; @@ -366,7 +396,7 @@ Templating правила: 11. Если включен `--improve-prompt`, сгенерировать proposal улучшенного prompt template и завершить workflow до worktree/agent launch. 12. Если это ordinary non-setup launch и `~/.config/start-issue` отсутствует, выполнить first-run onboarding gate перед оставшимся workflow. -Если включен `--human-gate`: +Если включен `--batch`: 1. После `render_prompt_template` проверить, что resolved agent равен `codex`. 2. Создать `STATE_DIR=/.start-issue/runs/`. @@ -567,7 +597,7 @@ start-issue 123 --repo owner/repo start-issue 123 --base develop start-issue 123 --agent codex start-issue 123 --agent codex --model gpt-5.2 -start-issue 123 --agent codex --human-gate +start-issue 123 --batch start-issue 123 --agent claude --model sonnet start-issue 123 --agent kimi --prompt-file .start-issue/prompt.md start-issue 123 --agent codex --improve-prompt @@ -584,7 +614,7 @@ start-issue init --user --force START_ISSUE_AGENT=codex start-issue 123 START_ISSUE_MODEL=sonnet start-issue 123 START_ISSUE_WORKTREE_DIR=~/projects/worktrees start-issue 123 -start-issue --human-gate-help +start-issue --batch-help ``` ## Зависимости @@ -606,7 +636,8 @@ start-issue --human-gate-help - [x] `start-issue 123` по умолчанию выбирает `claude`. - [x] `start-issue 123 --agent codex` создает worktree и запускает Codex в этом worktree. -- [x] `start-issue 123 --agent codex --human-gate` запускает Codex через `codex exec`, а не через обычный интерактивный launch. +- [x] `start-issue 123 --batch` запускает Codex через `codex exec`, а не через обычный интерактивный launch. +- [x] `start-issue 123 --human-gate` включает тот же batch path как совместимый alias. - [x] `start-issue 123 --agent kimi` запускает Kimi в этом worktree. - [x] `start-issue 123 --agent pi` запускает Pi в этом worktree. - [x] `start-issue 123 --no-agent` только готовит worktree и печатает следующие шаги. @@ -624,7 +655,8 @@ start-issue --human-gate-help - [x] `STATUS: DONE` завершает workflow без открытия Codex TUI. - [x] `STATUS: HUMAN_GATE` резюмирует ту же Codex session по explicit `thread_id`. - [x] Missing final status или missing `thread_id` завершаются явной ошибкой с указанием diagnostic artifact. -- [x] `start-issue --human-gate-help` документирует flow, prompt contract, exit codes и state files. +- [x] `start-issue --batch-help` документирует flow, prompt contract, exit codes и state files. +- [x] `start-issue --human-gate-help` сохраняет совместимость как alias для `--batch-help`. - [x] `--improve-prompt` создает reviewable proposal улучшенного prompt template и не перезаписывает active prompt. - [x] Claude-specific aliases сохранены, help text описывает agent-neutral поведение. - [x] `--dry-run` печатает selected agent, selected model, prompt source и launch command. diff --git a/memory-bank/README.md b/memory-bank/README.md index 6d4b3ff..ead8fc1 100644 --- a/memory-bank/README.md +++ b/memory-bank/README.md @@ -75,4 +75,4 @@ flows and templates remain upstream-derived process references. - [features/FT-016/README.md](features/FT-016/README.md) Opt-in real Codex human-gate E2E suite. - [features/FT-017/README.md](features/FT-017/README.md) - Codex human-gate delivery permission modes. + Codex batch delivery permission modes. diff --git a/memory-bank/domain/README.md b/memory-bank/domain/README.md index 5634e9c..d103ec2 100644 --- a/memory-bank/domain/README.md +++ b/memory-bank/domain/README.md @@ -14,7 +14,7 @@ audience: humans_and_agents `memory-bank/domain/` defines the stable language and workflow model for `start-issue`: issue input, repository context, configuration, prompt templates, -worktrees, agent adapters, release assets, human-gate runs, and feature +worktrees, agent adapters, release assets, batch runs, human gates, and feature packages. Domain documents do not own product positioning, shell implementation sequence, diff --git a/memory-bank/domain/context-map.md b/memory-bank/domain/context-map.md index 9f15025..ce74397 100644 --- a/memory-bank/domain/context-map.md +++ b/memory-bank/domain/context-map.md @@ -61,4 +61,4 @@ canonical_for: - `OQ-01` How future lifecycle commands should be decomposed into focused Go helpers without weakening the existing ownership boundaries. - `OQ-02` Whether non-Codex agents will expose enough resumable batch semantics - to generalize human-gate mode. + to generalize batch mode. diff --git a/memory-bank/domain/events.md b/memory-bank/domain/events.md index 392d385..b28fd2e 100644 --- a/memory-bank/domain/events.md +++ b/memory-bank/domain/events.md @@ -25,7 +25,7 @@ canonical_for: | `DE-03` | `WorktreeReady` | It is safe to run init/prompt/agent inside the target worktree | Worktree lifecycle | Pipeline, agent launch | branch name and path | | `DE-04` | `PromptProposalWritten` | A reviewable improved prompt proposal exists | Prompt improvement workflow | User/maintainer | output path and source prompt | | `DE-05` | `ReleaseUpdateInstalled` | Running executable has been replaced by verified release asset | Update workflow | CLI user | old version, new version, executable path | -| `DE-06` | `HumanGateReached` | Codex batch run requires a human decision | Human-gate workflow | CLI user, resume command | thread id, state directory, final message | +| `DE-06` | `HumanGateReached` | Codex batch run requires a human decision | Batch workflow | CLI user, resume command | thread id, state directory, final message | | `DE-07` | `MemoryBankAuditPassed` | Governed docs are reachable and links are valid | `check_memory_bank_index.py` | Maintainer/agent | scope, entrypoint, max depth | ## Event Rules @@ -39,5 +39,5 @@ canonical_for: This project does not currently publish runtime domain events. The events above are conceptual workflow facts used for documentation, tests, and feature design. -For Codex human-gate, JSONL `thread.started` is an external technical event +For Codex batch mode, JSONL `thread.started` is an external technical event observed by the workflow, while `HumanGateReached` is the local workflow verdict. diff --git a/memory-bank/domain/glossary.md b/memory-bank/domain/glossary.md index f7dae8c..e946754 100644 --- a/memory-bank/domain/glossary.md +++ b/memory-bank/domain/glossary.md @@ -39,7 +39,8 @@ canonical_for: | `prompt proposal` | Reviewable improved prompt file written by `--improve-prompt` | Prompt improvement | Active prompt template | | `release asset` | Built single-file `start-issue` binary uploaded to GitHub Releases | Install and update | Source modules | | `running executable` | The exact `start-issue` path invoked by the user | Self-update | Repository source script | -| `human-gate run` | Codex batch execution with persisted state and final status parsing | `--human-gate` | Normal interactive Codex launch | +| `batch run` | Autonomous Codex execution with persisted state and final status parsing | `--batch`; legacy alias `--human-gate` | Normal interactive Codex launch | +| `human gate` | Terminal batch outcome that resumes the saved Codex thread for a human decision | `STATUS: HUMAN_GATE` | The batch mode itself or a permission mode | | `memory-bank` | Governed project documentation and process layer adapted from `dapi/memory-bank` | Agent/project context | Runtime state under `.start-issue` | ## Naming Rules @@ -50,8 +51,8 @@ canonical_for: config initialization. - Use `worktree` for git worktrees only; use `workspace` only in prose when the branch/worktree/agent setup is meant together. -- Use `human-gate` only for the Codex batch/resume workflow currently grounded - in `codex exec` and `codex resume`. +- Use `batch` for the Codex execution mode grounded in `codex exec`; use + `human gate` only for the `STATUS: HUMAN_GATE` outcome and saved-thread resume. ## Ambiguous Terms diff --git a/memory-bank/domain/model.md b/memory-bank/domain/model.md index 34f5109..1f138fa 100644 --- a/memory-bank/domain/model.md +++ b/memory-bank/domain/model.md @@ -30,7 +30,7 @@ canonical_for: | `AgentAdapter` | policy/boundary | Agent-specific validation and command construction | Uses `Configuration`, `RenderedPrompt`, worktree path | Owns Codex/Claude/Kimi/Pi differences | | `ReleaseMetadata` | value object | Latest release tag and asset URLs | Used by self-update | Comes from GitHub Releases | | `InstalledExecutable` | entity | Running binary path and current version | Updated in place by self-update | Not necessarily repo checkout | -| `HumanGateRun` | entity | Codex batch state directory, events, last message, thread id | Belongs to one worktree run | Final status comes from `last-message.txt` | +| `BatchRun` | entity | Codex batch state directory, events, last message, thread id | Belongs to one worktree run | Final status comes from `last-message.txt`; `HUMAN_GATE` is one outcome | | `FeaturePackage` | documentation aggregate | Governed docs for one delivery unit | Uses memory-bank flows and stable IDs | Existing packages may use legacy layout | ## Relationship Map diff --git a/memory-bank/domain/rules.md b/memory-bank/domain/rules.md index dda387d..5006ddd 100644 --- a/memory-bank/domain/rules.md +++ b/memory-bank/domain/rules.md @@ -26,7 +26,7 @@ canonical_for: | `DR-04` | Existing worktree reuse requires exact branch/path validation before side effects | `WorktreePlan` | Avoid corrupting unrelated local work | FT-008 | | `DR-05` | `setup` writes only user config, while project config belongs to `init --project` | `setup`, `init` | Keep onboarding safe outside git repos | FT-014 | | `DR-06` | `update` uses latest GitHub Release and updates the running executable path | `InstalledExecutable` | Avoid updating the wrong binary | FT-013 | -| `DR-07` | `--human-gate` is Codex-only until another adapter has an equivalent grounded resume contract | `HumanGateRun` | Avoid false support claims | FT-015 | +| `DR-07` | `--batch` is Codex-only until another adapter has an equivalent grounded resume contract; `--human-gate` is a compatibility alias | `BatchRun` | Avoid false support claims without breaking released commands | FT-015, FT-017 | | `DR-08` | Agent-specific behavior must stay behind the adapter boundary | `AgentAdapter` | Prevent scattered per-agent branching | FT-009 / spec | ## Policies diff --git a/memory-bank/domain/states.md b/memory-bank/domain/states.md index f454b46..c44c2a4 100644 --- a/memory-bank/domain/states.md +++ b/memory-bank/domain/states.md @@ -24,7 +24,7 @@ canonical_for: | `SM-02` | Worktree lifecycle | `worktree.sh` | Plan before side effects | | `SM-03` | Configuration setup | `init.sh` / `config.sh` | `setup` and `init` have distinct scopes | | `SM-04` | Self-update workflow | `update.sh` | Independent from git repo and issue workflow | -| `SM-05` | Codex human-gate run | `agent.sh` / `pipeline.sh` | Uses persisted run state | +| `SM-05` | Codex batch run | Go launcher | Uses persisted run state and may end at a human gate | ## States @@ -50,7 +50,7 @@ canonical_for: | `TR-02` | `config_resolved` | `issue_fetched` | Issue workflow continues | Issue input is present and dependencies are available | Mode is `setup`, `init`, or `update` | | `TR-03` | `issue_fetched` | `worktree_planned` | Worktree planner runs | Branch name can be generated | Branch/path state is unreadable | | `TR-04` | `worktree_planned` | `worktree_ready` | User or planner selects create/reuse/delete path | Path validation passes | Existing path belongs to another branch | -| `TR-05` | `worktree_ready` | `agent_launched` | Agent launch | Agent is supported and not `none` | `--human-gate` with non-Codex | +| `TR-05` | `worktree_ready` | `agent_launched` | Agent launch | Agent is supported and not `none` | `--batch` with non-Codex | | `TR-06` | `worktree_ready` | `manual_next_steps` | No-agent mode | Agent is `none` | none | | `TR-07` | `config_resolved` | `update_noop` / `update_installed` | Update mode | Release metadata and running executable version known | Checksum/download/install fails | | `TR-08` | `worktree_ready` | `human_gate_done` / `human_gate_resume` | Codex batch final status parsed | Agent is Codex and thread id/status are valid | Missing or unknown final status | @@ -60,7 +60,7 @@ canonical_for: - `SI-01` Side effects that assume a valid worktree cannot run before `worktree_ready`. - `SI-02` Update states do not require git repository context. -- `SI-03` Human-gate terminal verdict comes only from saved `last-message.txt`. +- `SI-03` Batch terminal verdict comes only from saved `last-message.txt`. ## Implementation Notes diff --git a/memory-bank/engineering/architecture.md b/memory-bank/engineering/architecture.md index 11a0740..197f76a 100644 --- a/memory-bank/engineering/architecture.md +++ b/memory-bank/engineering/architecture.md @@ -36,7 +36,7 @@ Agent-specific behavior must stay centralized in Go adapter helpers: - model argument handling; - AI branch-name helper commands; - prompt-improvement helper commands; -- Codex-specific human-gate batch/resume command construction. +- Codex-specific batch/resume command construction. Do not add ad hoc `case "$AGENT"` branches in unrelated modules unless the change is only routing to the adapter boundary. @@ -58,10 +58,10 @@ The owner is the Go configuration layer; user-facing descriptions must stay alig ## Failure Handling - Fail fast for invalid user intent: unknown agents, prompt source conflicts, - unsupported human-gate agent, missing required update dependencies. + unsupported batch agent, missing required update dependencies. - Treat optional integrations as warnings when documented as optional: `zellij-tab-status` and non-zero `init.sh` do not abort the normal workflow. -- Preserve diagnostic artifacts for Codex human-gate failures under the run state +- Preserve diagnostic artifacts for Codex batch failures under the run state directory. - Never continue after a worktree safety validation failure. diff --git a/memory-bank/engineering/autonomy-boundaries.md b/memory-bank/engineering/autonomy-boundaries.md index 01dee16..28f979b 100644 --- a/memory-bank/engineering/autonomy-boundaries.md +++ b/memory-bank/engineering/autonomy-boundaries.md @@ -46,7 +46,7 @@ Stop and ask before: - deleting user worktrees or local branches outside an explicit tested workflow; - changing release tags, publishing releases, or pushing to `master`; - adding a new runtime language/core rewrite; -- generalizing Codex human-gate behavior to other agents without evidence; +- generalizing Codex batch behavior to other agents without evidence; - changing security/sandbox approval defaults for agent launch; - resolving contradictory product requirements by guessing. diff --git a/memory-bank/engineering/frontend.md b/memory-bank/engineering/frontend.md index 019adec..4128dcb 100644 --- a/memory-bank/engineering/frontend.md +++ b/memory-bank/engineering/frontend.md @@ -21,7 +21,7 @@ the CLI: flags, prompts, help text, dry-run output, and agent launch commands. - `--dry-run` output. - `setup` and first-run onboarding prompts. - `init` prompts and planned writes. -- `--human-gate-help`. +- `--batch-help` (`--human-gate-help` remains a compatibility alias). - Release/update status messages. ## CLI UX Rules @@ -29,7 +29,7 @@ the CLI: flags, prompts, help text, dry-run output, and agent launch commands. - Keep output explicit about effective config and sources. - Do not hide destructive choices behind defaults. - Keep normal help concise; use dedicated help for complex modes such as - `--human-gate-help`. + `--batch-help`. - When adding interactive prompts, support non-interactive test coverage through Go test input simulation. diff --git a/memory-bank/features/FT-017/README.md b/memory-bank/features/FT-017/README.md index 682ccb9..24f904b 100644 --- a/memory-bank/features/FT-017/README.md +++ b/memory-bank/features/FT-017/README.md @@ -1,8 +1,8 @@ --- -title: "FT-017: Codex human-gate delivery permissions" +title: "FT-017: Codex batch delivery permissions" doc_kind: feature doc_function: index -purpose: "Navigation for the Codex human-gate permission and full-delivery contract feature package." +purpose: "Navigation for the Codex batch permission and full-delivery contract feature package." derived_from: - ../../dna/governance.md - brief.md @@ -12,7 +12,7 @@ status: active audience: humans_and_agents --- -# FT-017: Codex human-gate delivery permissions +# FT-017: Codex batch delivery permissions ## About @@ -33,5 +33,4 @@ Git delivery. for live GitHub-writing verification. - [decision-log.md](decision-log.md) - Historical release-distribution decisions retained from the earlier FT-017 - migration package. + FPF decisions, evidence provenance, and the remaining live-verification gate. diff --git a/memory-bank/features/FT-017/brief.md b/memory-bank/features/FT-017/brief.md index 42c9353..4e3a43c 100644 --- a/memory-bank/features/FT-017/brief.md +++ b/memory-bank/features/FT-017/brief.md @@ -1,8 +1,8 @@ --- -title: "FT-017: Codex human-gate delivery permissions" +title: "FT-017: Codex batch delivery permissions" doc_kind: feature doc_function: canonical -purpose: "Canonical brief for making Codex human-gate capabilities explicit and supporting opt-in end-to-end Git delivery." +purpose: "Canonical brief for making Codex batch capabilities explicit and supporting opt-in end-to-end Git delivery." derived_from: - ../../flows/feature-flow.md - ../../product/context.md @@ -10,14 +10,14 @@ derived_from: - ../FT-015/feature.md - https://github.com/dapi/start-issue/issues/37 status: active -delivery_status: planned +delivery_status: in_progress audience: humans_and_agents must_not_define: - implementation_sequence - solution_space --- -# FT-017: Codex human-gate delivery permissions +# FT-017: Codex batch delivery permissions ## What @@ -41,19 +41,19 @@ supported Codex CLI while closing the remaining capability-contract gap. | Metric ID | Metric | Baseline | Target | Measurement method | | --- | --- | --- | --- | --- | -| `MET-01` | Human-gate capability contract visibility | One implicit `workspace-write` command | Every run reports either restricted or full-delivery permissions | Dry-run/help assertions | +| `MET-01` | Batch capability contract visibility | One implicit `workspace-write` command | Every run reports either restricted or full-delivery permissions | Dry-run/help assertions | | `MET-02` | Full Git delivery reachability | GitHub/network/Git writes are not guaranteed | An explicitly authorized mode can edit, test, commit, push, and create/update a PR | Deterministic command tests plus opt-in live E2E evidence | -| `MET-03` | Supported Codex command compatibility | Compatibility can drift with CLI option placement | Generated commands are accepted by the supported Codex CLI contract | Bats command-shape coverage and real-Codex smoke validation | +| `MET-03` | Supported Codex command compatibility | Compatibility can drift with CLI option placement | Generated commands are accepted by the supported Codex CLI contract | Go command-shape tests and real-Codex smoke validation | ### Scope -- `REQ-01` Preserve a restricted human-gate mode for working-tree-only +- `REQ-01` Preserve a restricted batch mode for working-tree-only automation and make that restricted completion boundary explicit in output and help. - `REQ-02` Add an explicit opt-in full-delivery mode whose declared contract includes GitHub reads, network operations, Git metadata writes, push, and pull-request creation/update. -- `REQ-03` Resolve the human-gate permission mode from a documented CLI option, +- `REQ-03` Resolve the batch permission mode from a documented CLI option, environment variable, and safe built-in default, and show the winning value in dry-run output. - `REQ-04` Generate a Codex command compatible with the supported CLI syntax, @@ -64,16 +64,19 @@ supported Codex CLI while closing the remaining capability-contract gap. occurs before the batch run. - `REQ-06` Explain authentication, network access, Git write access, the risk of full-delivery mode, restricted-mode limitations, and troubleshooting in - dedicated human-gate help and project documentation. + dedicated batch help and project documentation. - `REQ-07` Add deterministic automated coverage for permission precedence, validation, command construction, dry-run visibility, default compatibility, and existing `DONE`/`HUMAN_GATE` behavior. - `REQ-08` Provide an explicit opt-in real-Codex E2E procedure for validating full delivery without adding live GitHub writes to `make test` or CI. +- `REQ-09` Present `--batch` and `--batch-help` as the primary public names + while preserving the released `--human-gate` and `--human-gate-help` forms + as behaviorally equivalent compatibility aliases. ### Non-Scope -- `NS-01` Do not generalize human-gate mode to Claude, Kimi, Pi, or `agent=none`. +- `NS-01` Do not generalize batch mode to Claude, Kimi, Pi, or `agent=none`. - `NS-02` Do not provision GitHub credentials, modify Codex user configuration, or store secrets in project files. - `NS-03` Do not authorize production changes, destructive Git operations, or @@ -86,8 +89,10 @@ supported Codex CLI while closing the remaining capability-contract gap. ### Constraints / Assumptions -- `ASM-01` The supported reference environment is Codex CLI `0.145.0`, whose - global permission options are accepted before the `exec` subcommand. +- `ASM-01` Issue #37 reproduces the rejected argument order with Codex CLI + `0.144.6`. Local parser validation confirms both selected command forms on + Codex CLI `0.145.0`; live full-delivery behavior still requires the explicit + `CHK-03` approval gate. The repository does not pin an installed version. - `ASM-02` Full delivery requires independently configured GitHub authentication and repository authorization; `start-issue` can select a launcher policy but cannot grant those external capabilities. @@ -122,6 +127,8 @@ supported Codex CLI while closing the remaining capability-contract gap. security risk, mode selection, and troubleshooting consistently. - `EC-06` An explicitly approved live E2E can demonstrate end-to-end Git delivery and preserve auditable artifacts without becoming a CI dependency. +- `EC-07` New users can discover the workflow as batch mode, while existing + `--human-gate` invocations continue to work without behavior changes. ### Traceability matrix @@ -135,13 +142,14 @@ supported Codex CLI while closing the remaining capability-contract gap. | `REQ-06` | `ASM-02`, `CON-03` | `EC-05`, `SC-05` | `CHK-02` | `EVID-02` | | `REQ-07` | `CON-01` | `EC-01` - `EC-04`, `SC-01` - `SC-04`, `NEG-01` | `CHK-01` | `EVID-01` | | `REQ-08` | `CON-04` | `EC-06`, `SC-06` | `CHK-03` | `EVID-03` | +| `REQ-09` | `PCON-01` | `EC-07`, `SC-07` | `CHK-01`, `CHK-02` | `EVID-01`, `EVID-02` | ### Acceptance Scenarios - `SC-01` Given no permission override, when a user inspects or runs - human-gate mode, then restricted mode is selected and its working-tree-only + batch mode, then restricted mode is selected and its working-tree-only completion boundary is visible. -- `SC-02` Given explicit full-delivery selection, when the human-gate command is +- `SC-02` Given explicit full-delivery selection, when the batch command is built, then the mode is visibly reported and the command permits the declared GitHub/network/Git delivery workflow. - `SC-03` Given the supported Codex CLI, when either permission mode builds the @@ -156,6 +164,9 @@ supported Codex CLI while closing the remaining capability-contract gap. - `SC-06` Given explicit operator authorization, isolated fixture resources, and valid credentials, when the full-delivery E2E runs, then it records successful commit/push/PR delivery and retained diagnostic artifacts. +- `SC-07` Given either primary `--batch` or legacy `--human-gate`, when the + same issue workflow runs, then command construction, state, status, resume, + and exit behavior are identical; help presents the legacy form as an alias. ### Negative / Edge Scenarios @@ -168,13 +179,16 @@ supported Codex CLI while closing the remaining capability-contract gap. - `NEG-03` Given full-delivery mode without valid GitHub credentials or remote authorization, when the live workflow reaches delivery, then it fails with a capability diagnostic and does not relabel the failure as a product decision. +- `NEG-04` Given `--batch-permissions` without either batch entrypoint, when + arguments are parsed, then the CLI rejects the option before issue fetch or + worktree mutation and names `--batch` as the required primary mode. ### Checks | Check ID | Covers | How to check | Expected result | Evidence path | | --- | --- | --- | --- | --- | -| `CHK-01` | `EC-01` - `EC-04`, `SC-01` - `SC-04`, `NEG-01` | `make test` | Syntax, shellcheck, memory-bank audit, and deterministic Bats coverage pass for both modes and FT-015 regressions. | Local terminal/CI test output | -| `CHK-02` | `EC-01`, `EC-05`, `SC-01`, `SC-05`, `NEG-02` | Review `--help`, `--human-gate-help`, README files, and spec alongside output assertions | All surfaces state the same default, opt-in, capability, risk, and troubleshooting contract. | Review diff and Bats output | +| `CHK-01` | `EC-01` - `EC-04`, `EC-07`, `SC-01` - `SC-04`, `SC-07`, `NEG-01`, `NEG-04` | `make test` | Go formatting/vet/tests, memory-bank audit, and deterministic batch coverage pass for both permission modes, primary and legacy entrypoints, and FT-015 regressions. | Local terminal/CI test output | +| `CHK-02` | `EC-01`, `EC-05`, `EC-07`, `SC-01`, `SC-05`, `SC-07`, `NEG-02` | Review `--help`, `--batch-help`, README files, practical guides, and spec alongside Go output assertions | All surfaces present batch as primary, label legacy aliases, and state the same default, opt-in, capability, risk, and troubleshooting contract. | Review diff and Go test output | | `CHK-03` | `EC-02`, `EC-03`, `EC-06`, `SC-02`, `SC-03`, `SC-06`, `NEG-03` | With explicit approval, run the real-Codex full-delivery E2E procedure from FT-017's plan | Supported Codex accepts the command and the isolated fixture records commit, push, PR, terminal status, and retained artifacts. | Retained E2E artifact directory and fixture PR URL | ### Test matrix @@ -198,5 +212,5 @@ supported Codex CLI while closing the remaining capability-contract gap. | Evidence ID | Artifact | Producer | Path contract | Reused by checks | | --- | --- | --- | --- | --- | | `EVID-01` | Local and CI test output | implementer / CI | Terminal output and GitHub Actions job | `CHK-01` | -| `EVID-02` | Documentation diff plus help assertions | implementer / reviewer | Changed docs and Bats output | `CHK-02` | +| `EVID-02` | Documentation diff plus help assertions | implementer / reviewer | Changed docs and Go test output | `CHK-02` | | `EVID-03` | Live-E2E log, state files, commit/PR identifiers | approved operator | Retained E2E artifact path printed by runner | `CHK-03` | diff --git a/memory-bank/features/FT-017/decision-log.md b/memory-bank/features/FT-017/decision-log.md index af7ca3f..08e3a86 100644 --- a/memory-bank/features/FT-017/decision-log.md +++ b/memory-bank/features/FT-017/decision-log.md @@ -5,15 +5,15 @@ doc_function: reference purpose: "Records FPF analysis and accepted local decisions for FT-017. It does not own feature scope, selected design, acceptance criteria, or execution sequence." derived_from: - brief.md - - ../../../.github/workflows/ci.yml - - ../../../.github/workflows/release.yml - - ../../../install.sh + - design.md + - implementation-plan.md + - https://github.com/openai/codex/blob/main/codex-rs/exec/src/cli.rs status: active audience: humans_and_agents must_not_define: - - ft_016_scope - - ft_016_selected_design - - ft_016_acceptance_criteria + - ft_017_scope + - ft_017_selected_design + - ft_017_acceptance_criteria - implementation_sequence --- @@ -21,88 +21,83 @@ must_not_define: ## Purpose and Ownership -This log records why `DEC-01` remains open. The canonical owner of the blocker and the verify contract is [brief.md](brief.md). A selected solution belongs in a future `design.md`, not here. +This log records FPF decisions for FT-017. `brief.md` owns problem space and +acceptance; `design.md` owns the selected solution; `implementation-plan.md` +owns execution sequencing. This file records rationale and provenance only. -## DL-01 — Multi-platform Go release distribution contract +## DL-01 — Permission boundary and default -**Status:** accepted on 2026-07-22 by feature requester. +**Status:** accepted by FPF review on 2026-08-04. -### FPF framing +### Facts -- **Bounded context:** distribution is separate from CLI-semantic parity. It owns the relationship among a compiled artifact, release assets, installer/update selection, and the platform on which a user executes the artifact. -- **Evidence boundary:** facts below come only from the current repository and issue #34. The issue requests a Go binary but defines neither supported OS/architecture targets nor asset-selection rules. -- **Decision criterion:** provide Go releases for the requester-selected operating systems with explicit platform assets, verifiable integrity, and no inferred reduction of platform support. - -### Available facts - -1. `install.sh` downloads one fixed asset named `start-issue` and one fixed checksum file named `start-issue.sha256`. -2. `.github/workflows/release.yml` builds the current sole release asset on `ubuntu-latest`. -3. `.github/workflows/ci.yml` verifies installation on both `ubuntu-latest` and `macos-latest`. -4. A Bash release artifact is portable across those CI operating systems; a Go executable is platform-specific. -5. Issue #34 requires Go to become the primary distribution artifact and requires installation/release workflows to publish it successfully, but does not state the intended OS/architecture matrix or compatibility policy. +- Issue #37 states that `workspace-write` does not provide the network and Git + metadata writes needed for normal GitHub delivery. +- Existing FT-015 behavior uses `workspace-write` and must remain compatible. +- Full delivery can create external GitHub state and must therefore be opt-in. ### Decision -| Area | Accepted contract | -| --- | --- | -| Target matrix | `linux/amd64`, `linux/arm64`, `darwin/amd64`, `darwin/arm64`, and `windows/amd64`. The operating systems are requester-selected; the architecture set follows the explicit `dapi/port-selector` release pattern. | -| Build/release | Use GoReleaser v2 with `CGO_ENABLED=0`, one statically built executable per target, `start-issue--` asset names, and a SHA-256 `checksums.txt` manifest. During the v1-to-v2 cutover, also upload a `start-issue` bridge and its `start-issue.sha256` checksum for the v1 updater. | -| POSIX install | Adapt the referenced install-script strategy: detect `uname -s`/`uname -m`, select the matching asset, download it, verify its checksum from `checksums.txt`, and install it under the public name `start-issue`. | -| Windows delivery | Publish `start-issue-windows-amd64.exe` as a first-class release asset and document manual download/PATH installation. The existing POSIX shell installer is not a Windows installer. | -| Cutover | No separate human release-approval gate. The normal tag-triggered release proceeds only after `CHK-01` through `CHK-03` are green. | - -### Resolution rationale +Keep two semantic modes: `restricted` and `full-delivery`. `restricted` is the +built-in default. Full delivery requires an explicit CLI/environment selection +and a visible warning before Codex starts. -The requester directly chose macOS, Linux, and Windows and delegated release-strategy selection to this feature. The selected GoReleaser layout and target architecture set are grounded in the referenced `dapi/port-selector` repository: its `.goreleaser.yml` uses the exact five targets, `CGO_ENABLED=0`, binary-format archives, and `checksums.txt`; its installer performs POSIX OS/architecture detection. The decision preserves explicit asset integrity while avoiding a false claim that the POSIX installer supports Windows. +### FPF rationale -### Rejected alternatives +The launcher capability boundary is a separate bounded context from task-level +approval decisions. Least privilege is the selection criterion: absent an +explicit user choice, preserve the existing restricted behavior. The semantic +names keep the public contract independent from Codex's low-level flags. -- A single cross-platform `start-issue` Go asset is rejected: compiled Go executables are platform-specific. -- A narrower target matrix is rejected: the requester selected all three operating systems and the referenced strategy supplies the matching explicit matrix. -- A release approval gate is rejected: the requester explicitly said it is unnecessary; automated evidence gates remain mandatory. +## DL-02 — Full-delivery command mapping -## DL-02 — Go toolchain and Windows update boundary +**Status:** accepted by FPF review on 2026-08-04, pending live verification. -**Status:** accepted on 2026-07-22 by feature owner under delegated release-strategy choice. +### Evidence -### FPF framing and facts - -- The toolchain is an execution-environment contract, not a user-facing CLI capability; it must be deterministic in local, CI, and release paths. -- The referenced `dapi/port-selector` release pattern pins `go 1.21` in `go.mod` and GitHub Actions. This repository currently has no Go toolchain contract. -- A POSIX process can replace its executable through the existing install/update style; Windows generally locks a running executable. The referenced release strategy documents a Windows binary download rather than a shell installer. +- The official Codex `exec` CLI source marks + `dangerously_bypass_approvals_and_sandbox` as a global option for `exec`. +- The same source marks `model`, `json`, and `output-last-message` as global + options compatible with the `exec` command. +- The local Codex CLI `0.145.0` accepts both selected command forms through its + parser/help path. No agent session or external GitHub write was performed by + this parser validation. ### Decision -1. Pin Go `1.24` in `go.mod`, `mise.toml`, and CI/release setup for this migration. -2. The initial Windows contract is binary release plus manual installation and manual update: `start-issue update` on Windows must not try to overwrite its running `.exe`; it returns a clear instruction naming the matching release asset. POSIX retains verified automatic install/update behavior. +Map `full-delivery` to: + +```text +codex [--model MODEL] --dangerously-bypass-approvals-and-sandbox exec \ + --cd WORKTREE --json --output-last-message PATH - +``` -### Rationale and risk control +Keep restricted mode on the existing `codex exec --cd WORKTREE +--sandbox workspace-write --json --output-last-message PATH -` path. Do not +use `--ask-for-approval` in the generated command because issue #37 identifies +that spelling/placement as the compatibility failure under investigation. -Go 1.24 is the explicit baseline because its linker emits a Mach-O `LC_UUID`, which current macOS releases require. The Windows manual-update behavior avoids an unsafe or undeclared helper-process design. It is a documented platform-specific delivery difference, not a hidden parity exception, because the Bash baseline has no Windows runtime contract. +### Rationale and limits -## DL-03 — ID-01 dry-run worktree-path conflict handling +The bypass mapping is the only documented current CLI mechanism found that +explicitly covers both approvals and sandboxing. It is intentionally treated +as a high-risk capability switch, not as authorization for destructive or +production actions. `CHK-03` and `AG-01` remain mandatory before acceptance. -**Status:** accepted on 2026-07-24 by the feature requester. +## DL-03 — Current implementation grounding -### Case and approved expectation +**Status:** accepted by FPF review on 2026-08-04. -- **Stable case ID:** `ID-01` -- **Parity case:** `worktree-path-conflict-dry-run` in - `cmd/start-issue/parity_integration_test.go` -- **Bash baseline expectation:** accepts the supplied conflict choice during - `--dry-run` and reports `Worktree path already exists` before continuing - down the selected reuse path. -- **Go expectation:** reports `Worktree path exists; would prompt for reuse or - delete/recreate` without consuming a choice or selecting a reuse/delete - outcome. +The feature package must target the current Go implementation under +`cmd/start-issue/`, its Go tests, `test/e2e/batch.sh`, `Makefile`, and the +README/spec documentation. The earlier references to `scripts/lib/start_issue` +and Bats were stale artifacts from the pre-Go implementation and have been +removed from the execution plan. -### User-visible rationale and acceptance +## Open evidence gate -When a worktree path conflicts, the Go dry-run tells the user that a choice is -still required. This avoids presenting one stdin-supplied choice as the -determined outcome of a non-executing command and, importantly, avoids the -legacy path in which the delete/recreate selection can reach mutation logic -before Bash's later dry-run check. The different dry-run diagnostic is -user-visible and is intentionally accepted for `ID-01`; all other observable -records, fake-command logs, and filesystem state remain subject to `CHK-01` -parity. +The exact future release/version matrix is not asserted locally. Before +`delivery_status: done`, a retained approved E2E artifact must prove the +declared full-delivery behavior on the selected Codex executable. If that +verification fails, reject `full-delivery` and keep the restricted path as the +safe fallback. diff --git a/memory-bank/features/FT-017/design.md b/memory-bank/features/FT-017/design.md index 73fba04..d156ee1 100644 --- a/memory-bank/features/FT-017/design.md +++ b/memory-bank/features/FT-017/design.md @@ -2,7 +2,7 @@ title: "FT-017: Design" doc_kind: feature doc_function: canonical -purpose: "Solution-space document for explicit restricted and full-delivery Codex human-gate permission modes." +purpose: "Solution-space document for explicit restricted and full-delivery Codex batch permission modes." derived_from: - brief.md - ../FT-015/solution.md @@ -31,10 +31,12 @@ capability boundary around that lifecycle. The solution must keep restricted behavior safe by default while giving an operator one deliberate, visible way to authorize end-to-end Git delivery. -The local reference CLI is Codex `0.145.0`. Its approval and sandbox flags are -global options, while JSONL and last-message outputs are `exec` options. The -design therefore needs a stable semantic contract owned by `start-issue`, not -an unchecked string of arbitrary Codex arguments. +Issue #37 reproduces the obsolete `--ask-for-approval` placement with Codex +`0.144.6`. The current upstream Codex `exec` source exposes +`--dangerously-bypass-approvals-and-sandbox` as a global option and keeps the +JSONL/last-message contract on `exec`. The design therefore owns a semantic +contract rather than passing arbitrary Codex arguments; the exact approved +release remains a live-verification concern. ## C4 Applicability @@ -63,13 +65,12 @@ enforces credentials and repository authorization independently. ## Selected Solution -- `SOL-01` Add one semantic configuration axis named human-gate permissions +- `SOL-01` Add one semantic configuration axis named batch permissions with exactly two values: `restricted` and `full-delivery`. Resolve it as CLI option → environment variable → built-in `restricted`. -- `SOL-02` Map `restricted` to Codex global options - `--ask-for-approval never --sandbox workspace-write` and map explicit - `full-delivery` to - `--ask-for-approval never --sandbox danger-full-access`. +- `SOL-02` Keep `restricted` on the existing `--sandbox workspace-write` + command and map explicit `full-delivery` to the global + `--dangerously-bypass-approvals-and-sandbox` option. - `SOL-03` Build the command in supported grammar order: `codex`, global model and permission options, `exec`, then worktree and batch-output options. - `SOL-04` Print the resolved semantic mode and a concise capability statement @@ -81,6 +82,9 @@ enforces credentials and repository authorization independently. - `SOL-06` Extend the opt-in real-Codex E2E runner with a separately authorized full-delivery scenario that uses isolated fixture resources and retains delivery evidence. +- `SOL-07` Expose the existing workflow primarily as `--batch` and + `--batch-help`; keep the released `--human-gate` and `--human-gate-help` + spellings as compatibility aliases for the same internal mode. ## Alternatives Considered @@ -96,16 +100,16 @@ enforces credentials and repository authorization independently. | Trade-off ID | Decision | Benefit | Cost / Risk | | --- | --- | --- | --- | | `TRD-01` | Expose two semantic modes instead of raw Codex controls | Small, testable public contract with stable operator meaning | Advanced Codex policies are not expressible through this feature. | -| `TRD-02` | Use `danger-full-access` for explicit full delivery | Provides network and Git metadata writes required by the delivery contract | Batch commands are unsandboxed and must be treated as high risk. | -| `TRD-03` | Keep `never` approval for batch execution | Preserves unattended human-gate semantics | Capability errors cannot escalate mid-run and must be diagnosed clearly. | +| `TRD-02` | Use the explicit Codex bypass option for full delivery | Covers the approvals and sandbox boundaries implicated by issue #37 | Batch commands are unsandboxed and must be treated as high risk. | +| `TRD-03` | Use the explicit bypass mode for unattended batch execution | Preserves unattended human-gate semantics for the selected full-delivery mode | Capability errors cannot escalate mid-run and must be diagnosed clearly. | ## Accepted Local Decisions - `SD-01` Name the public values by user outcome (`restricted`, `full-delivery`) rather than Codex implementation names so help and future adapters can describe capability without leaking every low-level flag. -- `SD-02` Use `--human-gate-permissions VALUE` and - `START_ISSUE_HUMAN_GATE_PERMISSIONS` as the two explicit inputs. Project/user +- `SD-02` Use `--batch-permissions VALUE` and + `START_ISSUE_BATCH_PERMISSIONS` as the two explicit inputs. Project/user persistence is deferred; the dangerous mode must not become an unnoticed repository default in this feature. - `SD-03` A full-delivery selection is itself explicit authorization to launch @@ -113,16 +117,20 @@ enforces credentials and repository authorization independently. production actions excluded by `NS-03`. - `SD-04` Live full-delivery verification remains a manual approval gate and is never folded into `make test` or CI. +- `SD-05` Name the execution mode after what the user starts (`batch`), while + keeping `HUMAN_GATE` as one terminal outcome of that mode rather than the + mode's primary public name. ## Contracts | Contract ID | Input / Output | Producer / Consumer | Semantics / Constraints | | --- | --- | --- | --- | -| `CTR-01` | `--human-gate-permissions restricted\|full-delivery` | CLI parser / config resolver | CLI value wins over environment; invalid or empty explicit values fail before issue fetch. | -| `CTR-02` | `START_ISSUE_HUMAN_GATE_PERMISSIONS` | shell environment / config resolver | Used only when CLI input is absent; unset resolves to `restricted`. | -| `CTR-03` | Restricted Codex command | launcher / Codex | `codex [--model MODEL] --ask-for-approval never --sandbox workspace-write exec --cd WORKTREE --json --output-last-message PATH -`. | -| `CTR-04` | Full-delivery Codex command | launcher / Codex | Same shape as `CTR-03`, with `--sandbox danger-full-access`; selected only by explicit `full-delivery`. | +| `CTR-01` | `--batch-permissions restricted\|full-delivery` | CLI parser / config resolver | CLI value wins over environment; it requires batch mode through `--batch` or its legacy alias; invalid or empty explicit values fail before issue fetch. | +| `CTR-02` | `START_ISSUE_BATCH_PERMISSIONS` | shell environment / config resolver | Used only when CLI input is absent; unset resolves to `restricted`. | +| `CTR-03` | Restricted Codex command | launcher / Codex | `codex [--model MODEL] exec --cd WORKTREE --sandbox workspace-write --json --output-last-message PATH -`. | +| `CTR-04` | Full-delivery Codex command | launcher / Codex | `codex [--model MODEL] --dangerously-bypass-approvals-and-sandbox exec --cd WORKTREE --json --output-last-message PATH -`; selected only by explicit `full-delivery`. | | `CTR-05` | Permission status output | launcher / operator | Reports semantic mode and capability boundary before execution and in dry-run; full delivery includes an unsandboxed-execution warning. | +| `CTR-06` | `--batch`, `--batch-help`; legacy `--human-gate`, `--human-gate-help` | CLI parser / operator | Batch names are primary in help/docs; each legacy form maps to exactly the same runtime or help behavior without a second implementation path. | ## Invariants @@ -133,6 +141,8 @@ enforces credentials and repository authorization independently. - `INV-04` Batch commands remain arrays and are never evaluated through `eval`. - `INV-05` FT-015's exact thread-id resume and state-artifact paths remain stable across permission modes. +- `INV-06` Primary and legacy batch entrypoints resolve to one internal boolean + mode and cannot diverge in command, state, resume, or exit semantics. ## Failure Modes @@ -147,6 +157,8 @@ enforces credentials and repository authorization independently. - `FM-04` A full-delivery prompt proposes destructive or production work: prompt policy still requires `STATUS: HUMAN_GATE`; permission mode does not broaden product authorization. +- `FM-05` A legacy alias diverges from `--batch`: shared parser-state tests fail + and the release is blocked until both entrypoints use the same internal path. ## Rollout / Backout @@ -174,3 +186,4 @@ contract changes. | `REQ-06` | `SOL-04`, `TRD-02`, `SD-03` | `CTR-05`, `INV-03` | `FM-02`, `FM-04`, `RB-02` | | `REQ-07` | `SOL-01` - `SOL-05` | `CTR-01` - `CTR-05`, `INV-01` - `INV-05` | `FM-01` - `FM-04`, `RB-01`, `RB-02` | | `REQ-08` | `SOL-06`, `SD-04` | `INV-03`, `INV-05` | `FM-02`, `RB-03` | +| `REQ-09` | `SOL-07`, `SD-05` | `CTR-06`, `INV-06` | `FM-05`, `RB-01` | diff --git a/memory-bank/features/FT-017/implementation-plan.md b/memory-bank/features/FT-017/implementation-plan.md index 757d928..34717ec 100644 --- a/memory-bank/features/FT-017/implementation-plan.md +++ b/memory-bank/features/FT-017/implementation-plan.md @@ -2,7 +2,7 @@ title: "FT-017: Implementation Plan" doc_kind: feature doc_function: derived -purpose: "Execution plan for explicit restricted and full-delivery Codex human-gate permission modes." +purpose: "Execution plan for explicit restricted and full-delivery Codex batch permission modes." derived_from: - brief.md - design.md @@ -22,6 +22,11 @@ must_not_define: Implement the accepted FT-017 permission-mode contract while preserving the existing FT-015 batch, state, status, and resume behavior. +Deterministic implementation, documentation, command-shape coverage, and local +Codex CLI `0.145.0` parser validation are complete. `STEP-06` remains pending +because the real full-delivery run requires explicit `AG-01` authorization and +creates retained fixture GitHub state. + ## Grounding / Support References | Document | Role in this plan | Facts reused | Conflict action | @@ -35,15 +40,11 @@ existing FT-015 batch, state, status, and resume behavior. | Path / module | Current role | Why relevant | Reuse / mirror | | --- | --- | --- | --- | -| `scripts/start-issue` | Initializes shared CLI state and sources modules | New resolved permission state needs a safe default | Follow existing agent/model state initialization | -| `scripts/lib/start_issue/cli.sh` | Parses public options and validates mode combinations | Owns the new CLI input and early rejection path | Follow `--human-gate`/`--model` value parsing patterns | -| `scripts/lib/start_issue/config.sh` | Resolves config values and sources | Owns CLI/environment/default precedence | Mirror model resolution without adding project/user persistence | -| `scripts/lib/start_issue/agent.sh` | Builds and runs Codex human-gate commands | Main permission mapping and supported grammar change surface | Keep array-based command construction and FT-015 state helpers | -| `scripts/lib/start_issue/output.sh` | Renders help, dry-run, and runtime status | Must expose mode, capabilities, and warning consistently | Extend current human-gate help and dry-run output | -| `test/helpers/fake-bin/codex` | Deterministic Codex command double | Must validate global option order and both sandbox mappings | Extend argument capture/rejection behavior | -| `test/start_issue.bats` | Public CLI and human-gate regression suite | Existing tests cover restricted command, DONE, HUMAN_GATE, and errors | Add precedence, invalid value, full-delivery, and order assertions | -| `test/e2e/human-gate.sh` | Opt-in real-Codex smoke runner | Closest existing live verification surface | Add a separately guarded full-delivery scenario only after approval | -| `README.md`, `README.ru.md`, `doc/spec.md` | Public and canonical behavior docs | Must match help and command behavior | Update together with output assertions | +| `cmd/start-issue/main.go` | Go CLI parser, config resolution, launcher, help, and batch state | Owns the primary names, compatibility aliases, permission mapping, and diagnostics | Extend existing options and array-based `exec.Cmd` construction | +| `cmd/start-issue/main_test.go` | Deterministic Go regression suite | Existing tests cover batch command, DONE, HUMAN_GATE, and errors | Add precedence, invalid value, full-delivery, and order assertions | +| `cmd/start-issue/parity_integration_test.go` | Go/Bash observable parity coverage | Protects unaffected legacy behavior during the Go implementation | Keep non-human-gate parity cases green | +| `test/e2e/batch.sh` | Opt-in real-Codex smoke runner | Closest existing live verification surface | Add a separately guarded full-delivery scenario only after approval | +| `README.md`, `README.ru.md`, `doc/batch-mode*.md`, `doc/spec.md` | Public guides and canonical behavior docs | Must match help and command behavior | Update together with output assertions | ## Test Strategy @@ -52,22 +53,22 @@ existing FT-015 batch, state, status, and resume behavior. | Permission resolution and validation | `REQ-03`, `SC-01`, `SC-02`, `NEG-01`, `CTR-01`, `CTR-02` | None | CLI beats env; env beats default; invalid value fails before fetch/mutation | `make test` | Existing test job | none | none | | Codex command construction | `REQ-01`, `REQ-02`, `REQ-04`, `SC-03`, `CTR-03`, `CTR-04`, `INV-02`, `INV-04` | Restricted dry-run and fake Codex execution | Assert semantic mode mapping, global-before-`exec` order, model coexistence, and no raw interpolation | `make test` | Existing test job | Real installed-Codex parser behavior is external | `AG-01` for live run | | FT-015 state/resume regression | `REQ-05`, `SC-04`, `INV-05` | DONE, HUMAN_GATE, missing status/thread | Run existing scenarios under default restricted and one full-delivery fake path | `make test` | Existing test job | none | none | -| Help/docs contract | `REQ-01`, `REQ-06`, `SC-05`, `NEG-02`, `CTR-05` | Dedicated human-gate help assertions | Assert default, full-delivery warning, prerequisites, limitations, and troubleshooting | `make test`; documentation review | Existing test job | Prose consistency review is manual | reviewer approval in PR | +| Help/docs contract | `REQ-01`, `REQ-06`, `REQ-09`, `SC-05`, `SC-07`, `NEG-02`, `CTR-05`, `CTR-06` | Dedicated batch help assertions | Assert primary and legacy names, default, full-delivery warning, prerequisites, limitations, and troubleshooting | `make test`; documentation review | Existing test job | Prose consistency review is manual | reviewer approval in PR | | End-to-end Git delivery | `REQ-02`, `REQ-08`, `SC-06`, `NEG-03`, `SOL-06`, `RB-03` | Real Codex terminal-state smoke only | Keep syntax/static coverage automated; add guarded scenario entrypoint | `make test` plus explicitly approved E2E | Excluded from CI | Requires real Codex, credentials, network, Git writes, push, and PR creation | `AG-01` | ## Open Questions / Ambiguities | Open Question ID | Question | Why unresolved | Blocks | Default action / escalation owner | | --- | --- | --- | --- | --- | -| `OQ-01` | Which future Codex versions remain compatible after `0.145.0`? | The external CLI has no repository-owned stability guarantee. | Does not block implementation; affects future maintenance | Treat `0.145.0` as the tested baseline and update adapter/docs together on command-shape failure. | +| `OQ-01` | Which future Codex versions remain compatible after the issue baseline? | The external CLI has no repository-owned stability guarantee; local parser validation covers `0.145.0` only. | Does not block deterministic implementation; blocks claims about future versions | Treat the approved live executable as the acceptance baseline and update adapter/docs together on command-shape failure. | | `OQ-02` | Which isolated fixture repository/issue should receive the live full-delivery PR? | Live target selection is operator-owned and may change. | `STEP-06` only | Require explicit target and approval through `AG-01`; never infer from global focus or an unrelated repo. | ## Environment Contract | Area | Contract | Used by | Failure symptom | | --- | --- | --- | --- | -| setup | Bash, Git, jq, shellcheck, Bats, fake agent binaries, and the current modular source tree | `STEP-01` - `STEP-05` | `make test` dependency or fixture failure | -| supported Codex | Local reference is `codex-cli 0.145.0`; approval/sandbox options must be accepted before `exec` | `STEP-03`, `STEP-06` | Real CLI rejects command before emitting `thread.started` | +| setup | Go, Bash, Git, and the current Go source tree; deterministic tests use fakes | `STEP-01` - `STEP-05` | `make test` dependency or fixture failure | +| supported Codex | Issue failure baseline is `0.144.6`; local parser validation covers `0.145.0`; the approved live executable must complete the recorded full-delivery flow | `STEP-03`, `STEP-06` | Parser rejection or no `thread.started` event | | deterministic test | `make test` is canonical and must not use network or real agent binaries | `CHK-01`, `STEP-02` - `STEP-05` | External side effects or nondeterministic test failures | | live access | Explicit opt-in, authenticated `gh`, real Codex, authorized fixture repo/issue, network, and permission to push/create a PR | `STEP-06` | Missing auth, push rejection, absent PR, or no terminal status | | secrets | Credentials remain in existing authenticated tools/environment and never enter tracked files or command output | all steps | Token-like data appears in diff, logs, or state artifacts | @@ -86,23 +87,23 @@ existing FT-015 batch, state, status, and resume behavior. | --- | --- | --- | --- | --- | | `WS-1` | `REQ-03`, `SOL-01`, `SD-02`, `CTR-01`, `CTR-02` | Validated permission resolution and early errors | agent | `PRE-01` | | `WS-2` | `REQ-01`, `REQ-02`, `REQ-04`, `SOL-02` - `SOL-05`, `CTR-03` - `CTR-05` | Correct commands and visible capability status | agent | `WS-1`, `PRE-02` | -| `WS-3` | `REQ-06`, `REQ-07`, `SC-01` - `SC-05` | Automated regression coverage and aligned docs | agent | `WS-1`, `WS-2` | +| `WS-3` | `REQ-06`, `REQ-07`, `REQ-09`, `SC-01` - `SC-05`, `SC-07` | Automated regression coverage, compatibility aliases, and aligned docs | agent | `WS-1`, `WS-2` | | `WS-4` | `REQ-08`, `SOL-06`, `SC-06`, `RB-03` | Guarded full-delivery E2E evidence | human + agent | `WS-2`, `WS-3`, `PRE-03`, `AG-01` | ## Approval Gates | Approval Gate ID | Trigger | Applies to | Why approval is required | Approver / evidence | | --- | --- | --- | --- | --- | -| `AG-01` | Running a real full-delivery session that can commit, push, and create/update a PR | `STEP-06`, `WS-4`, `CHK-03` | The run is unsandboxed and creates external GitHub state | User names/approves the fixture target; retained log and PR URL record approval context | +| `AG-01` | Running a real full-delivery session that can commit, push, and create/update a PR | `STEP-06`, `WS-4`, `CHK-03` | The run is unsandboxed and creates external GitHub state | User names/approves the fixture target; retained fixture directory, log, state artifacts, and PR URL record approval context | ## Work Order | Step ID | Actor | Implements | Goal | Touchpoints | Artifact | Verifies | Evidence IDs | Check command / procedure | Blocked by | Needs approval | Escalate if | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `STEP-01` | agent | `REQ-03`, `SOL-01`, `SD-02`, `CTR-01`, `CTR-02`, `FM-03` | Add CLI/environment/default resolution and fail-fast validation | `scripts/start-issue`, `cli.sh`, `config.sh` | Resolved mode and source | `CHK-01`, `NEG-01` | `EVID-01` | Focused Bats tests, then `make test` | `PRE-01` | none | Validation occurs after fetch or mutation | -| `STEP-02` | agent | `REQ-07`, `INV-01`, `INV-02` | Extend fake Codex and tests before changing launcher behavior | Fake Codex, Bats suite | Red/green command contract tests | `CHK-01`, `SC-01` - `SC-04` | `EVID-01` | `bats test/start_issue.bats` | `STEP-01` | none | Fake cannot distinguish global and exec arguments | -| `STEP-03` | agent | `REQ-01`, `REQ-02`, `REQ-04`, `SOL-02`, `SOL-03`, `CTR-03`, `CTR-04`, `INV-04` | Build validated restricted/full-delivery commands in supported order | `agent.sh` | Array-based Codex command mapping | `CHK-01`, `SC-02`, `SC-03` | `EVID-01` | Focused Bats tests; inspect `--dry-run` command | `STEP-02`, `PRE-02` | none | Supported Codex rejects generated grammar | -| `STEP-04` | agent | `REQ-01`, `REQ-06`, `SOL-04`, `CTR-05`, `FM-02`, `FM-04` | Add capability output, warning, dedicated help, and public docs | `output.sh`, README files, spec | Consistent operator contract | `CHK-02`, `SC-05`, `NEG-02` | `EVID-02` | Help assertions and documentation review | `STEP-03` | none | Docs imply permission equals credentials or product authorization | +| `STEP-01` | agent | `REQ-03`, `REQ-09`, `SOL-01`, `SOL-07`, `SD-02`, `SD-05`, `CTR-01`, `CTR-02`, `CTR-06`, `FM-03`, `FM-05` | Add primary batch names, compatibility aliases, permission resolution, and fail-fast validation | `cmd/start-issue/main.go`, `main_test.go` | One internal batch mode with resolved permissions and source | `CHK-01`, `SC-07`, `NEG-01`, `NEG-04` | `EVID-01` | Focused Go tests, then `make test` | `PRE-01` | none | Alias behavior diverges or validation occurs after fetch/mutation | +| `STEP-02` | agent | `REQ-07`, `INV-01`, `INV-02` | Extend the fake Codex process and Go tests before changing launcher behavior | `cmd/start-issue/main_test.go` | Red/green command contract tests | `CHK-01`, `SC-01` - `SC-04` | `EVID-01` | `go test ./cmd/start-issue` | `STEP-01` | none | Fake cannot distinguish global and exec arguments | +| `STEP-03` | agent | `REQ-01`, `REQ-02`, `REQ-04`, `SOL-02`, `SOL-03`, `CTR-03`, `CTR-04`, `INV-04` | Build validated restricted/full-delivery commands in supported order | `cmd/start-issue/main.go` | Array-based Codex command mapping | `CHK-01`, `SC-02`, `SC-03` | `EVID-01` | Focused Go tests; inspect `--dry-run` command | `STEP-02`, `PRE-02` | none | Supported Codex rejects generated grammar | +| `STEP-04` | agent | `REQ-01`, `REQ-06`, `SOL-04`, `CTR-05`, `FM-02`, `FM-04` | Add capability output, warning, dedicated help, and public docs | `cmd/start-issue/main.go`, README files, practical guides, spec | Consistent operator contract | `CHK-02`, `SC-05`, `NEG-02` | `EVID-02` | Help assertions and documentation review | `STEP-03` | none | Docs imply permission equals credentials or product authorization | | `STEP-05` | agent | `REQ-05`, `REQ-07`, `SOL-05`, `INV-05`, `RB-01`, `RB-02` | Run full deterministic regression and simplify review | All changed runtime/tests/docs | Green local suite and complexity verdict | `CHK-01`, `CHK-02`, `SC-04` | `EVID-01`, `EVID-02`, `EVID-09` | `make test`; inspect diff for unnecessary branches/abstractions | `STEP-01` - `STEP-04` | none | FT-015 state/resume behavior changes | | `STEP-06` | human + agent | `REQ-08`, `SOL-06`, `SD-04`, `SC-06`, `NEG-03`, `RB-03` | Extend/run isolated live full-delivery verification and retain evidence | E2E runner and approved fixture repo/issue | E2E log, state artifacts, commit and PR URL | `CHK-03`, `EC-06` | `EVID-03` | Follow canonical cmux caller-tab procedure and poll to terminal PASS/failure | `STEP-05`, `PRE-03`, `OQ-02` | `AG-01` | Target/auth/caller context is missing, or any unexpected external scope appears | diff --git a/memory-bank/features/FT-019/implementation-plan.md b/memory-bank/features/FT-019/implementation-plan.md index 615e16d..3d3e0ed 100644 --- a/memory-bank/features/FT-019/implementation-plan.md +++ b/memory-bank/features/FT-019/implementation-plan.md @@ -21,7 +21,7 @@ must_not_define: | Path / module | Current role | Why relevant | Reuse / mirror | | --- | --- | --- | --- | -| `test/e2e/human-gate.sh` | Existing real-agent E2E | Establishes E2E script conventions and opt-in boundary | Keep separate because it needs secrets/interactive Codex | +| `test/e2e/batch.sh` | Existing real-agent E2E | Establishes E2E script conventions and opt-in boundary | Keep separate because it needs secrets/interactive Codex | | `Makefile` | Build/test entrypoint | Owns the local and CI target | Add sandbox target beside human-gate target | | `.github/workflows/ci.yml` | CI checks | Runs Go build and tests | Add network-free sandbox job | | `cmd/start-issue/main.go` | Product executable | Must be exercised as a built subprocess | Do not add test-only product hooks | diff --git a/memory-bank/features/README.md b/memory-bank/features/README.md index 340f0d0..d8ee66e 100644 --- a/memory-bank/features/README.md +++ b/memory-bank/features/README.md @@ -39,9 +39,9 @@ audience: humans_and_agents - [FT-016: Real Codex human-gate E2E suite](FT-016/README.md) Opt-in local smoke suite that validates the real Codex human-gate path. -- [FT-017: Codex human-gate delivery permissions](FT-017/README.md) +- [FT-017: Codex batch delivery permissions](FT-017/README.md) Explicit restricted/default and opt-in full-delivery capability contract for - Codex human-gate runs. + Codex batch runs. - [FT-018: Agent CLI launch compatibility](FT-018/README.md) Issue #34 follow-up package for current Kimi Code CLI command and cwd compatibility. diff --git a/memory-bank/ops/development.md b/memory-bank/ops/development.md index 650c75e..46608bc 100644 --- a/memory-bank/ops/development.md +++ b/memory-bank/ops/development.md @@ -39,11 +39,11 @@ Install locally from source: make install ``` -Run the opt-in real-Codex human-gate E2E smoke test with a usable issue in the +Run the opt-in real-Codex batch E2E smoke test with a usable issue in the current repository: ```bash -START_ISSUE_E2E=1 make e2e-human-gate +START_ISSUE_E2E=1 make e2e-batch ``` Run the deterministic, network-free built-binary E2E used in CI: diff --git a/memory-bank/ops/stages.md b/memory-bank/ops/stages.md index 2cd1add..7f76229 100644 --- a/memory-bank/ops/stages.md +++ b/memory-bank/ops/stages.md @@ -55,7 +55,7 @@ There is no centralized runtime observability. Diagnostics come from: - command output; - Go test logs; - GitHub Actions logs; -- human-gate state files under +- batch state files under `/.start-issue/runs//`. ## Test Data And Smoke Targets diff --git a/memory-bank/product/context.md b/memory-bank/product/context.md index 22a8055..f9fc9a1 100644 --- a/memory-bank/product/context.md +++ b/memory-bank/product/context.md @@ -58,7 +58,7 @@ tracker, or the developer's review and merge process. prompt silently. - `WF-04` Update the installed CLI: compare the running executable version with the latest GitHub Release, verify checksum, and install into the same path. -- `WF-05` Run Codex human-gate mode: execute Codex in batch mode, persist run +- `WF-05` Run Codex batch mode: execute Codex autonomously, persist run state, exit on `STATUS: DONE`, and resume interactively on `STATUS: HUMAN_GATE`. diff --git a/memory-bank/product/roadmap.md b/memory-bank/product/roadmap.md index 04ce786..5ec5828 100644 --- a/memory-bank/product/roadmap.md +++ b/memory-bank/product/roadmap.md @@ -42,7 +42,7 @@ into feature packages. ## Open Bets -- `BET-01` Whether Codex human-gate patterns should remain Codex-only or become +- `BET-01` Whether Codex batch/human-gate patterns should remain Codex-only or become a generic agent capability after other CLIs expose equivalent contracts. - `BET-02` Whether future lifecycle complexity warrants extracting Go helper packages from the current command package. diff --git a/memory-bank/product/vision.md b/memory-bank/product/vision.md index 6e398e7..7640add 100644 --- a/memory-bank/product/vision.md +++ b/memory-bank/product/vision.md @@ -32,7 +32,7 @@ behavior behind implicit magic. | --- | --- | --- | --- | --- | | `BET-01` | Agent-neutral start workflow | Developers switch between Claude, Codex, Kimi, Pi, and manual mode | Current adapter support in README/spec | Revisit when adding an agent | | `BET-02` | Reviewable prompt/config evolution | Prompt templates materially affect agent output | `--improve-prompt`, setup/init flows | Revisit when prompt placeholders change | -| `BET-03` | Resumable batch work for Codex | Some issue work can run unattended until a real human gate | `--human-gate` feature | Revisit when other agents expose equivalent resume contracts | +| `BET-03` | Resumable batch work for Codex | Some issue work can run unattended until a real human gate | `--batch` feature | Revisit when other agents expose equivalent resume contracts | ## Experience Principles diff --git a/test/e2e/batch.sh b/test/e2e/batch.sh new file mode 100755 index 0000000..408af9c --- /dev/null +++ b/test/e2e/batch.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash + +# Opt-in smoke test for a real local Codex batch session. +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +fixture_repo="dapi/start-issue-e2e-fixture" +fixture_issue="1" +scenario="done" + +usage() { + cat <<'EOF' +Usage: START_ISSUE_E2E=1 test/e2e/batch.sh [--scenario done|human-gate|full-delivery] + +Runs start-issue against a real Codex CLI using the private fixture repository +dapi/start-issue-e2e-fixture and its control issue #1. It deletes the temporary +clone after a successful run; set START_ISSUE_E2E_KEEP=1 to retain it. The +HUMAN_GATE scenario opens Codex resume interactively; exit it to continue. +FULL_DELIVERY also requires START_ISSUE_E2E_FULL_DELIVERY=1. It authorizes an +unsandboxed Codex run that creates a unique fixture commit, remote branch, and +pull request. Its temporary fixture and diagnostic artifacts are retained as +evidence automatically. +EOF +} + +fail() { + printf 'E2E batch: %s\n' "$*" >&2 + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --scenario) + scenario="${2:-}" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + fail "unknown argument: $1" + ;; + esac +done + +[[ "$scenario" == "done" || "$scenario" == "human-gate" || "$scenario" == "full-delivery" ]] || \ + fail "scenario must be done, human-gate, or full-delivery" +[[ "${START_ISSUE_E2E:-}" == "1" ]] || fail "set START_ISSUE_E2E=1 to authorize a real Codex session" +if [[ "$scenario" == "full-delivery" ]]; then + [[ "${START_ISSUE_E2E_FULL_DELIVERY:-}" == "1" ]] || \ + fail "set START_ISSUE_E2E_FULL_DELIVERY=1 to authorize unsandboxed GitHub delivery" +fi +start_issue_bin="${START_ISSUE_E2E_BINARY:-$repo_root/.build/start-issue}" + +[[ -x "$start_issue_bin" ]] || fail "start-issue executable not found: $start_issue_bin" + +codex_path="$(command -v codex || true)" +[[ -n "$codex_path" ]] || fail "codex is not on PATH" +[[ "$codex_path" != "$repo_root/test/helpers/fake-bin/codex" ]] || fail "PATH resolves codex to the test fake" +gh auth status >/dev/null || fail "gh is not authenticated" + +codex_exec_help="$(codex exec --help 2>&1)" || fail "codex exec --help failed" +printf '%s' "$codex_exec_help" | grep -Fq 'Run Codex non-interactively' || \ + fail "resolved codex does not expose the real Codex exec interface" +printf '%s' "$codex_exec_help" | grep -Fq -- '--output-last-message' || \ + fail "resolved codex does not support --output-last-message" +if printf '%s' "$codex_exec_help" | grep -q -- '--ask-for-approval'; then + fail "installed codex exec still advertises --ask-for-approval; use a current Codex CLI" +fi +if [[ "$scenario" == "full-delivery" ]]; then + codex_help="$(codex --help 2>&1)" || fail "codex --help failed" + printf '%s' "$codex_help" | grep -Fq -- '--dangerously-bypass-approvals-and-sandbox' || \ + fail "resolved codex does not support the full-delivery permission option" +fi + +fixture_root="$(mktemp -d "${TMPDIR:-/tmp}/start-issue-batch.XXXXXX")" +fixture_dir="$fixture_root/fixture" +worktree_parent="$fixture_root/worktrees" +log_path="$fixture_root/e2e.log" +expected_status="DONE" +permission_args=() +if [[ "$scenario" == "human-gate" ]]; then + expected_status="HUMAN_GATE" +fi + +if [[ "$scenario" == "full-delivery" ]]; then + permission_args=(--batch-permissions full-delivery) + delivery_id="$(date -u +%Y%m%d%H%M%S)-$$" + delivery_branch="e2e/batch-full-delivery-$delivery_id" + delivery_file="full-delivery-$delivery_id.txt" + fixture_base="$(gh repo view "$fixture_repo" --json defaultBranchRef --jq '.defaultBranchRef.name')" + [[ -n "$fixture_base" ]] || fail "could not resolve fixture default branch" +fi + +if [[ "$scenario" == "full-delivery" ]]; then + prompt=$(cat <&1 | tee "$log_path" +command_status=${PIPESTATUS[0]} +set -e + +[[ $command_status -eq 0 ]] || fail "start-issue exited $command_status; inspect $log_path" + +state_dir="$(awk '/^[[:space:]]*State dir: / { sub(/^[[:space:]]*State dir: /, ""); print; exit }' "$log_path")" +[[ -n "$state_dir" ]] || fail "state directory was not reported; inspect $log_path" +last_message_path="$state_dir/last-message.txt" +worktree_path="$(dirname "$(dirname "$(dirname "$state_dir")")")" + +[[ -f "$state_dir/events.jsonl" ]] || fail "events.jsonl is missing: $state_dir/events.jsonl" +[[ -f "$last_message_path" ]] || fail "last-message.txt is missing: $last_message_path" +[[ -f "$state_dir/thread-id" ]] || fail "thread-id is missing: $state_dir/thread-id" +jq -e 'select(.type == "thread.started") | .thread_id | strings' "$state_dir/events.jsonl" >/dev/null || \ + fail "thread.started event is missing from $state_dir/events.jsonl" +grep -Fx "STATUS: $expected_status" "$last_message_path" >/dev/null || \ + fail "expected STATUS: $expected_status in $last_message_path" + +if [[ "$scenario" == "human-gate" ]]; then + grep -F 'Resume command: codex resume --include-non-interactive ' "$log_path" >/dev/null || \ + fail "resume command was not reported; inspect $log_path" +fi + +if [[ "$scenario" == "full-delivery" ]]; then + current_branch="$(git -C "$worktree_path" branch --show-current)" + [[ "$current_branch" == "$delivery_branch" ]] || \ + fail "full-delivery branch is $current_branch, want $delivery_branch" + git -C "$worktree_path" show "HEAD:$delivery_file" | grep -Fx "full-delivery $delivery_id" >/dev/null || \ + fail "delivery commit does not contain the expected marker" + git -C "$worktree_path" ls-remote --exit-code --heads origin "$delivery_branch" >/dev/null || \ + fail "remote delivery branch is missing: $delivery_branch" + pr_url="$(gh pr list --repo "$fixture_repo" --state open --head "$delivery_branch" --json url --jq '.[0].url // empty')" + [[ -n "$pr_url" ]] || fail "full-delivery pull request is missing for $delivery_branch" + printf 'Full-delivery PR: %s\n' "$pr_url" +fi + +unexpected_changes="$(git -C "$worktree_path" status --porcelain | awk '$0 !~ /^\?\? \.start-issue\// { print }')" +[[ -z "$unexpected_changes" ]] || fail "fixture worktree has unexpected changes: $unexpected_changes" + +printf 'PASS: real Codex batch %s scenario. State: %s\n' "$scenario" "$state_dir" +if [[ "$scenario" == "full-delivery" || "${START_ISSUE_E2E_KEEP:-}" == "1" ]]; then + printf 'The temporary fixture is preserved at: %s\n' "$fixture_root" +else + git -C "$fixture_dir" worktree remove --force "$worktree_path" + rm -rf -- "$fixture_root" + printf 'Temporary fixture clone and worktree removed. Set START_ISSUE_E2E_KEEP=1 to preserve them.\n' +fi diff --git a/test/e2e/human-gate.sh b/test/e2e/human-gate.sh index 9f1efa2..f9ddb4c 100755 --- a/test/e2e/human-gate.sh +++ b/test/e2e/human-gate.sh @@ -1,133 +1,6 @@ #!/usr/bin/env bash - -# Opt-in smoke test for a real local Codex human-gate session. set -euo pipefail -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -fixture_repo="dapi/start-issue-e2e-fixture" -fixture_issue="1" -scenario="done" - -usage() { - cat <<'EOF' -Usage: START_ISSUE_E2E=1 test/e2e/human-gate.sh [--scenario done|human-gate] - -Runs start-issue against a real Codex CLI using the private fixture repository -dapi/start-issue-e2e-fixture and its control issue #1. It deletes the temporary -clone after a successful run; set START_ISSUE_E2E_KEEP=1 to retain it. The -HUMAN_GATE scenario opens Codex resume interactively; exit it to continue. -EOF -} - -fail() { - printf 'E2E human-gate: %s\n' "$*" >&2 - exit 1 -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --scenario) - scenario="${2:-}" - shift 2 - ;; - --help|-h) - usage - exit 0 - ;; - *) - fail "unknown argument: $1" - ;; - esac -done - -[[ "$scenario" == "done" || "$scenario" == "human-gate" ]] || fail "scenario must be done or human-gate" -[[ "${START_ISSUE_E2E:-}" == "1" ]] || fail "set START_ISSUE_E2E=1 to authorize a real Codex session" -start_issue_bin="${START_ISSUE_E2E_BINARY:-$repo_root/.build/start-issue}" - -[[ -x "$start_issue_bin" ]] || fail "start-issue executable not found: $start_issue_bin" - -codex_path="$(command -v codex || true)" -[[ -n "$codex_path" ]] || fail "codex is not on PATH" -[[ "$codex_path" != "$repo_root/test/helpers/fake-bin/codex" ]] || fail "PATH resolves codex to the test fake" -gh auth status >/dev/null || fail "gh is not authenticated" - -codex_exec_help="$(codex exec --help 2>&1)" || fail "codex exec --help failed" -printf '%s' "$codex_exec_help" | grep -Fq 'Run Codex non-interactively' || \ - fail "resolved codex does not expose the real Codex exec interface" -printf '%s' "$codex_exec_help" | grep -Fq -- '--output-last-message' || \ - fail "resolved codex does not support --output-last-message" -if printf '%s' "$codex_exec_help" | grep -q -- '--ask-for-approval'; then - fail "installed codex exec still advertises --ask-for-approval; use a current Codex CLI" -fi - -fixture_root="$(mktemp -d "${TMPDIR:-/tmp}/start-issue-human-gate.XXXXXX")" -fixture_dir="$fixture_root/fixture" -worktree_parent="$fixture_root/worktrees" -log_path="$fixture_root/e2e.log" -expected_status="DONE" -if [[ "$scenario" == "human-gate" ]]; then - expected_status="HUMAN_GATE" -fi - -prompt=$(cat <&1 | tee "$log_path" -command_status=${PIPESTATUS[0]} -set -e - -[[ $command_status -eq 0 ]] || fail "start-issue exited $command_status; inspect $log_path" - -state_dir="$(awk '/^[[:space:]]*State dir: / { sub(/^[[:space:]]*State dir: /, ""); print; exit }' "$log_path")" -[[ -n "$state_dir" ]] || fail "state directory was not reported; inspect $log_path" -last_message_path="$state_dir/last-message.txt" -worktree_path="$(dirname "$(dirname "$(dirname "$state_dir")")")" - -[[ -f "$state_dir/events.jsonl" ]] || fail "events.jsonl is missing: $state_dir/events.jsonl" -[[ -f "$last_message_path" ]] || fail "last-message.txt is missing: $last_message_path" -[[ -f "$state_dir/thread-id" ]] || fail "thread-id is missing: $state_dir/thread-id" -jq -e 'select(.type == "thread.started") | .thread_id | strings' "$state_dir/events.jsonl" >/dev/null || \ - fail "thread.started event is missing from $state_dir/events.jsonl" -grep -Fx "STATUS: $expected_status" "$last_message_path" >/dev/null || \ - fail "expected STATUS: $expected_status in $last_message_path" - -if [[ "$scenario" == "human-gate" ]]; then - grep -F 'Resume command: codex resume --include-non-interactive ' "$log_path" >/dev/null || \ - fail "resume command was not reported; inspect $log_path" -fi - -unexpected_changes="$(git -C "$worktree_path" status --porcelain | awk '$0 !~ /^\?\? \.start-issue\// { print }')" -[[ -z "$unexpected_changes" ]] || fail "fixture worktree has unexpected changes: $unexpected_changes" - -printf 'PASS: real Codex human-gate %s scenario. State: %s\n' "$scenario" "$state_dir" -if [[ "${START_ISSUE_E2E_KEEP:-}" == "1" ]]; then - printf 'The temporary fixture is preserved at: %s\n' "$fixture_root" -else - git -C "$fixture_dir" worktree remove --force "$worktree_path" - rm -rf -- "$fixture_root" - printf 'Temporary fixture clone and worktree removed. Set START_ISSUE_E2E_KEEP=1 to preserve them.\n' -fi +# Compatibility wrapper. New documentation and automation should use batch.sh. +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$script_dir/batch.sh" "$@"