diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 421d618e..30e11a8b 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -9,7 +9,7 @@ "source": { "source": "url", "url": "https://github.com/avksp/agent-lifecycle-kit.git", - "ref": "v1.51.0" + "ref": "v1.52.0" }, "policy": { "installation": "AVAILABLE", diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a8b7bf5a..3c2c2772 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,10 +9,10 @@ "source": { "source": "github", "repo": "avksp/agent-lifecycle-kit", - "ref": "v1.51.0" + "ref": "v1.52.0" }, "description": "Reviewed SDD planning, budgeted execution, adapter conformance, implementation audit, and final proof.", - "version": "1.51.0", + "version": "1.52.0", "author": { "name": "Agent Lifecycle Kit contributors" }, diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 3aa34cc4..881acbe9 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-lifecycle-kit", - "version": "1.51.0", + "version": "1.52.0", "description": "Provider-neutral lifecycle kit for reviewed SDD planning, budgeted execution, adapter conformance, independent audits, and final proof.", "author": { "name": "Agent Lifecycle Kit contributors" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index f8a590d6..dea48a4d 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-lifecycle-kit", - "version": "1.51.0", + "version": "1.52.0", "description": "Provider-neutral lifecycle kit for reviewed SDD planning, budgeted execution, adapter conformance, independent audits, and final proof.", "author": { "name": "Agent Lifecycle Kit contributors", diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index b66d8bb0..89c053fc 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -5,14 +5,14 @@ }, "metadata": { "description": "Provider-neutral lifecycle kit for reviewed SDD planning, budgeted execution, adapter conformance, independent audits, and final proof.", - "version": "1.51.0" + "version": "1.52.0" }, "plugins": [ { "name": "agent-lifecycle-kit", "source": ".", "description": "Reviewed SDD planning, budgeted execution, adapter conformance, implementation audit, and final proof.", - "version": "1.51.0", + "version": "1.52.0", "author": { "name": "Agent Lifecycle Kit contributors" }, diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index bf987ede..5ac141f8 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-lifecycle-kit", - "version": "1.51.0", + "version": "1.52.0", "description": "Provider-neutral lifecycle kit for reviewed SDD planning, budgeted execution, adapter conformance, independent audits, and final proof.", "author": { "name": "Agent Lifecycle Kit contributors" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eab2453..1e6d4d24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ - No changes yet. +## 1.52.0 - 2026-08-09 + +- Added `agent-lifecycle start` as one beginner-facing command for task text, + Markdown, explicit frozen-run delegation and stored ALK session resume. +- Added `auto`, `research`, `plan`, `review` and explicit `implement` modes with + fail-closed execution boundaries and verified adapter/session lineage. +- Added `agent-lifecycle-start-receipt.v1`, path-safe delegate summaries, + focused non-execution tests and English/Russian operator guidance. +- Updated package, plugin, marketplace and quickstart publication metadata to + `1.52.0`. + ## 1.51.0 - 2026-08-09 - Added Python 3.14 to package metadata and the tested CI, release, publication diff --git a/README.md b/README.md index 76efdca1..e6f58556 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,11 @@ From a source checkout: python -m pip install -e . agent-lifecycle version agent-lifecycle diagnose --no-install-plans -agent-lifecycle adapter validate --descriptor adapters/codex/adapter.descriptor.json +agent-lifecycle start --adapter codex --text "Draft a reviewed implementation plan" ``` The official [PyPI package](https://pypi.org/project/agent-lifecycle-kit/) supports Python 3.11-3.14. -Install the exact release with `python -m pip install agent-lifecycle-kit==1.51.0`. +Install the exact release with `python -m pip install agent-lifecycle-kit==1.52.0`. For a short walkthrough, use [Quickstart](docs/guides/quickstart.md) and the [Lifecycle cookbook](docs/guides/lifecycle-cookbook.md). For structure and positioning, see [System architecture](docs/architecture/system-architecture.md) and [Project comparison](docs/reference/project-comparison.md). Russian documentation starts at [Документация на русском](docs/ru/README.md). @@ -46,9 +46,9 @@ For a short walkthrough, use [Quickstart](docs/guides/quickstart.md) and the [Li - Usage visibility: tokens, time, and resource counters are native; monetary cost is optional and only used when a metered host reports it. - Read-only progress and managed sessions: host UIs can show lifecycle state, - attested tokens and Git-style change counters; `adapter task start` accepts - task text, Markdown or frozen run requests while preserving review/freeze - boundaries. + attested tokens and Git-style change counters; `start` accepts task text, + Markdown, frozen run requests or stored ALK sessions while preserving + review/freeze boundaries. ## Feature areas diff --git a/adapters/claude/.claude-plugin/plugin.json b/adapters/claude/.claude-plugin/plugin.json index e720cc57..3a0871be 100644 --- a/adapters/claude/.claude-plugin/plugin.json +++ b/adapters/claude/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-lifecycle-kit", - "version": "1.51.0", + "version": "1.52.0", "description": "Lifecycle skills and adapter metadata for reviewed planning, budgeted execution, adapter conformance, audit, and final proof.", "author": { "name": "Agent Lifecycle Kit contributors" diff --git a/adapters/codex/.codex-plugin/plugin.json b/adapters/codex/.codex-plugin/plugin.json index 581ff397..dd197a5f 100644 --- a/adapters/codex/.codex-plugin/plugin.json +++ b/adapters/codex/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-lifecycle-kit", - "version": "1.51.0", + "version": "1.52.0", "description": "Provider-neutral lifecycle kit and adapter metadata for reviewed planning, budgeted execution, adapter conformance, audit, and final proof.", "author": { "name": "Agent Lifecycle Kit contributors" diff --git a/adapters/cursor/.cursor-plugin/plugin.json b/adapters/cursor/.cursor-plugin/plugin.json index 7965a74f..65b7b172 100644 --- a/adapters/cursor/.cursor-plugin/plugin.json +++ b/adapters/cursor/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-lifecycle-kit", - "version": "1.51.0", + "version": "1.52.0", "description": "Agent lifecycle planning, budgeted execution, adapter conformance, audit, and final proof for Cursor.", "author": { "name": "Agent Lifecycle Kit contributors" diff --git a/docs/README.md b/docs/README.md index 692899cb..10add2ff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,8 +14,9 @@ project. ## Quick start - Official PyPI package for Python 3.11-3.14: - `python -m pip install agent-lifecycle-kit==1.51.0` from + `python -m pip install agent-lifecycle-kit==1.52.0` from [agent-lifecycle-kit](https://pypi.org/project/agent-lifecycle-kit/). +- One safe entrypoint: `agent-lifecycle start --adapter codex --file task.md`. - [Quickstart](guides/quickstart.md) - [Lifecycle cookbook](guides/lifecycle-cookbook.md) - [System architecture](architecture/system-architecture.md) diff --git a/docs/architecture/system-architecture.md b/docs/architecture/system-architecture.md index 328bd272..c6906aae 100644 --- a/docs/architecture/system-architecture.md +++ b/docs/architecture/system-architecture.md @@ -247,10 +247,43 @@ sequenceDiagram Main-->>User: stable JSON stdout ``` -Pattern: command dispatcher plus functional core. `cli/dispatch.py` only selects -one of five command-group handlers: adapters/readiness, contracts/evidence, -lifecycle, observability or planning. CLI modules should stay thin; domain -services own behavior and tests. +Pattern: command dispatcher plus functional core. `cli/dispatch.py` selects the +unified start facade or one of five command-group handlers: +adapters/readiness, contracts/evidence, lifecycle, observability or planning. +CLI modules stay thin; domain services own behavior and tests. + +### Unified lifecycle start + +```mermaid +sequenceDiagram + participant User + participant StartCLI as cli/start.py + participant Start as adapter_sessions/unified_start.py + participant Intake as adapter_sessions/task_intake.py + participant Resume as adapter_sessions/workflow_bridge.py + participant Store as adapter_sessions/session_store.py + + User->>StartCLI: start --adapter --file|--text|--resume + StartCLI->>Start: start_lifecycle() + alt raw task in auto/research/plan/review + Start->>Intake: start_adapter_task() + Intake-->>Start: reviewed draft receipt + else frozen input with explicit implement + Start->>Intake: existing frozen delegation + Intake-->>Start: managed-run receipt + else persisted ALK session + Start->>Store: load_session() + Start->>Resume: resume_adapter_session() + Resume-->>Start: lineage result + end + Start-->>User: agent-lifecycle-start-receipt.v1 +``` + +The facade selects an existing primitive; it does not own workflow transitions. +Raw task modes cannot call managed-run or host-launch code. Frozen delegation +requires explicit `implement` and complete bindings. Resume accepts only a +stored ALK session, validates adapter and state lineage, and does not interpret +native host conversation identifiers. ### Raw task or Markdown intake diff --git a/docs/guides/lifecycle-cookbook.md b/docs/guides/lifecycle-cookbook.md index 2f1c6bc2..a4890baa 100644 --- a/docs/guides/lifecycle-cookbook.md +++ b/docs/guides/lifecycle-cookbook.md @@ -36,13 +36,14 @@ Return: - validation commands that would be needed later. EOF -agent-lifecycle adapter task start \ +agent-lifecycle start \ --adapter codex \ + --mode research \ --file work/tasks/research.md \ - --out work/tasks/research-intake.json + --out work/tasks/research-start.json agent-lifecycle review-mesh recommend \ - --intake work/tasks/research-intake.json \ + --file work/tasks/research.md \ --out work/tasks/research-recommendation.json ``` @@ -54,13 +55,14 @@ into a normal ALK plan package and freeze it. Use this for a task, proposal or plan saved in one file: ```bash -agent-lifecycle adapter task start \ +agent-lifecycle start \ --adapter claude \ + --mode review \ --file tasks/proposal.md \ - --out work/review/proposal-intake.json + --out work/review/proposal-start.json agent-lifecycle review-mesh recommend \ - --intake work/review/proposal-intake.json \ + --file tasks/proposal.md \ --out work/review/proposal-recommendation.json ``` @@ -92,10 +94,11 @@ Check requirements, acceptance criteria, evidence routes, write ownership, security gates and release claims. EOF -agent-lifecycle adapter task start \ +agent-lifecycle start \ --adapter codex \ + --mode review \ --file work/review/plan-review-task.md \ - --out work/review/plan-review-intake.json + --out work/review/plan-review-start.json ``` ## Review code changes @@ -109,10 +112,11 @@ git diff origin/main...HEAD > work/code-review/current/diff.patch ``` ```bash -agent-lifecycle adapter task start \ +agent-lifecycle start \ --adapter codex \ + --mode review \ --file work/code-review/current/review-task.md \ - --out work/code-review/current/intake.json + --out work/code-review/current/start.json ``` When architecture is documented, list the architecture documents in the task @@ -161,10 +165,11 @@ For defects, regressions, flaky failures, incidents and security bugs, start with task intake and let ALK add advisory-only Bug Forensics markers: ```bash -agent-lifecycle adapter task start \ +agent-lifecycle start \ --adapter codex \ + --mode plan \ --file work/bugs/checkout-regression.md \ - --out work/bugs/checkout-intake.json + --out work/bugs/checkout-start.json ``` The advisory does not activate the workflow gate. A reviewed frozen plan must @@ -192,3 +197,7 @@ OpenCode; any configured model can be used by changing the host command. - Plugin installation alone is not proof that the ALK lifecycle ran. - Raw text, Markdown and imported plans never authorize implementation by themselves. + +Use `agent-lifecycle start` for the common path. Advanced scripts may use the +atomic `adapter task start`, `adapter run` and `adapter session resume` +commands when they need direct control over one lifecycle primitive. diff --git a/docs/guides/quickstart.md b/docs/guides/quickstart.md index af2dd421..919240c7 100644 --- a/docs/guides/quickstart.md +++ b/docs/guides/quickstart.md @@ -28,7 +28,7 @@ supports Python 3.11-3.14. When the package is available for the requested version, install the exact semantic version: ```bash -python -m pip install agent-lifecycle-kit==1.51.0 +python -m pip install agent-lifecycle-kit==1.52.0 agent-lifecycle version ``` @@ -103,18 +103,50 @@ The same command supports `--dialect bmad` and `--dialect spec-kitty`. Imported material remains a draft candidate. It cannot start implementation or replace a frozen ALK plan until it is reviewed and frozen. -## Start adapter task intake +## Start with one command For a task file or short text: ```bash -agent-lifecycle adapter task start --adapter codex --file task.md -agent-lifecycle adapter task start --adapter codex --text "Fix the failing test" +agent-lifecycle start --adapter codex --file task.md +agent-lifecycle start --adapter codex --text "Fix the failing test" ``` -This does not start implementation for raw input. It returns a review-gated -draft receipt. Managed execution requires a frozen run request or a frozen plan -with workflow binding. +The default mode is `auto`. Raw input never starts implementation: it returns +`agent-lifecycle-start-receipt.v1` with a review-gated draft result. Use an +explicit non-executing mode when the requested outcome is narrower: + +```bash +agent-lifecycle start --adapter codex --mode research --file research.md +agent-lifecycle start --adapter codex --mode plan --file feature.md +agent-lifecycle start --adapter codex --mode review --file proposed-plan.md +``` + +Only `--mode implement` can delegate to the existing managed-run path, and it +requires a structured frozen run request with complete state, manifest, lock, +task, operation and revision bindings: + +```bash +agent-lifecycle start \ + --adapter codex \ + --mode implement \ + --file work/run/adapter-run-request.json +``` + +To resume a session recorded by ALK: + +```bash +agent-lifecycle start \ + --adapter codex \ + --resume \ + --session-root .alk/adapter-sessions +``` + +Resume verifies the stored adapter and workflow lineage. It does not interpret +the value as a native Codex, Claude, OpenCode or other host conversation id. +This release does not launch an external host CLI. Advanced automation can use +the lower-level `adapter task start`, `adapter run` and `adapter session resume` +commands documented in the [CLI reference](../reference/cli.md). ## Review code changes @@ -129,13 +161,14 @@ git diff origin/main...HEAD > work/code-review/current/diff.patch Then pass the task to ALK without starting implementation: ```bash -agent-lifecycle adapter task start \ +agent-lifecycle start \ --adapter codex \ + --mode review \ --file work/code-review/current/review-task.md \ - --out work/code-review/current/intake.json + --out work/code-review/current/start.json agent-lifecycle review-mesh recommend \ - --intake work/code-review/current/intake.json \ + --file work/code-review/current/review-task.md \ --out work/code-review/current/recommendation.json ``` @@ -155,6 +188,11 @@ agent-lifecycle review-mesh recommend --file task.md To prepare local reviewer packets from a task intake receipt: ```bash +agent-lifecycle adapter task start \ + --adapter codex \ + --file work/code-review/current/review-task.md \ + --out work/code-review/current/intake.json + agent-lifecycle review-mesh prepare \ --intake work/code-review/current/intake.json \ --template leader-draft-review \ diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 2169ccfc..f861aa05 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -10,7 +10,7 @@ Python 3.11-3.14 is supported. Install the exact release from the official [PyPI project](https://pypi.org/project/agent-lifecycle-kit/): ```bash -python -m pip install agent-lifecycle-kit==1.51.0 +python -m pip install agent-lifecycle-kit==1.52.0 ``` ## Foundation @@ -45,6 +45,17 @@ python -m pip install agent-lifecycle-kit==1.51.0 ## Execution +- `agent-lifecycle start --adapter (--file task.md | --text "..." | + --resume )`: beginner-facing facade over task intake, frozen + managed-run delegation and stored ALK session resume. Task-source aliases are + `--task-file` and `--task-text`; exactly one action is required. + `--mode auto|research|plan|review|implement` defaults to `auto`. Raw input and + every mode except explicit `implement` remain non-executing. `implement` + requires a structured frozen request with complete state, manifest, lock, + task, operation and revision bindings. The command returns + `agent-lifecycle-start-receipt.v1`, does not launch a host CLI and never + treats `--resume` as a native host conversation identifier. + - `agent-lifecycle workflow run`: verify the frozen plan/state binding and return the next host-owned action without mutating state or starting model work. Add `--progress-hook stderr` for opt-in terminal progress on stderr, or @@ -133,6 +144,9 @@ python -m pip install agent-lifecycle-kit==1.51.0 ## Adapters +Use `agent-lifecycle start` for the simple path. The commands below remain the +atomic interface for scripts and advanced operators. + - `agent-lifecycle adapter validate`: check a descriptor against the baseline. - `agent-lifecycle adapter inspect`: inspect source projection and safe host command surfaces. diff --git a/docs/reference/managed-adapter-sessions.md b/docs/reference/managed-adapter-sessions.md index 5363f4c4..a6e1d42e 100644 --- a/docs/reference/managed-adapter-sessions.md +++ b/docs/reference/managed-adapter-sessions.md @@ -3,6 +3,10 @@ Managed adapter sessions give operators one ALK entrypoint for adapter-backed work without making ALK a second coding-agent runtime. +The simplest entrypoint is `agent-lifecycle start`. It selects task intake, +explicit frozen-run delegation or stored-session resume while preserving the +same lower-level contracts. + There are three operator-facing modes: - interactive session: `adapter session start --adapter ` records a @@ -24,6 +28,11 @@ with the requested adapter, workflow state and task id. A mismatch returns ## Commands ```bash +agent-lifecycle start --adapter codex --file task.md +agent-lifecycle start --adapter codex --mode research --text "Inspect the current design" +agent-lifecycle start --adapter codex --mode implement --file adapter-run-request.json +agent-lifecycle start --adapter codex --resume + agent-lifecycle adapter session start --adapter codex agent-lifecycle adapter session start --adapter codex --launch agent-lifecycle adapter session status --session @@ -46,6 +55,13 @@ agent-lifecycle adapter run \ --task ``` +`start` emits `agent-lifecycle-start-receipt.v1`. Its nested delegate summary +contains only stable status, advisory and receipt-digest fields; it excludes +raw task text and local absolute paths. `auto`, `research`, `plan` and `review` +are non-executing. `implement` delegates only a complete structured frozen +request. Resume accepts only a session stored by ALK, checks adapter and state +lineage, and never attaches to a native host conversation. + `adapter task start` emits `agent-adapter-task-start-receipt.v1`. For raw text or Markdown, the receipt stores only source label, digest and byte count, not the raw task text. The `reviewMeshRecommendation` field is advisory and cannot @@ -93,6 +109,7 @@ Managed adapter sessions are fail-closed and display-safe: The stable receipts are `agent-adapter-session-receipt.v1`, `agent-managed-adapter-launch-receipt.v1`, `agent-adapter-session-resume-receipt.v1`, -`agent-adapter-task-start-receipt.v1` and -`agent-adapter-task-run-request.v1`. Review Mesh recommendations use +`agent-adapter-task-start-receipt.v1`, +`agent-adapter-task-run-request.v1` and `agent-lifecycle-start-receipt.v1`. +Review Mesh recommendations use `agent-review-mesh-recommendation.v1`. diff --git a/docs/ru/README.md b/docs/ru/README.md index 411fe28e..10814140 100644 --- a/docs/ru/README.md +++ b/docs/ru/README.md @@ -16,17 +16,17 @@ OpenInterpreter, Pi, Grok Build и других. Ядро не зависит о Главная цель проекта - закрывать задачу полностью, с максимально возможным качеством для выбранной модели, без оверинжиринга и с контролем расхода токенов. -**Лицензия:** Apache-2.0 · **Версия:** 1.51.0 · **Python:** 3.11-3.14 +**Лицензия:** Apache-2.0 · **Версия:** 1.52.0 · **Python:** 3.11-3.14 ## Быстрый старт -Официальный [пакет в PyPI](https://pypi.org/project/agent-lifecycle-kit/) для Python 3.11-3.14: `python -m pip install agent-lifecycle-kit==1.51.0`. +Официальный [пакет в PyPI](https://pypi.org/project/agent-lifecycle-kit/) для Python 3.11-3.14: `python -m pip install agent-lifecycle-kit==1.52.0`. Из исходного дерева: ```bash python -m pip install -e . agent-lifecycle version agent-lifecycle diagnose --no-install-plans -agent-lifecycle adapter validate --descriptor adapters/codex/adapter.descriptor.json +agent-lifecycle start --adapter codex --text "Составь проверяемый план реализации" ``` Пошаговый пример: [Быстрый старт](quickstart.md). Устройство проекта описывает [архитектура системы](architecture/system-architecture.md), а отличие от похожих инструментов показывает [сравнение проекта](reference/project-comparison.md). @@ -44,10 +44,9 @@ agent-lifecycle adapter validate --descriptor adapters/codex/adapter.descriptor. или уровень риска. - Учёт расхода показывает токены, время и ресурсы; денежная стоимость необязательна и используется только если её сообщает платный хост. -- Прогресс и управляемые сессии работают без вызова модели: можно показать - состояние, токены и счётчик изменений, а `adapter run` связывает работу с - зафиксированным состоянием рабочего цикла без заявления прямого запуска - хоста. +- Прогресс и управляемые сессии работают без вызова модели: команда `start` + принимает текст, файл задачи, зафиксированный запрос или сохранённую сессию, + не заявляя прямой запуск внешнего инструмента. ## Области возможностей diff --git a/docs/ru/architecture/system-architecture.md b/docs/ru/architecture/system-architecture.md index e082da62..eef20c48 100644 --- a/docs/ru/architecture/system-architecture.md +++ b/docs/ru/architecture/system-architecture.md @@ -251,11 +251,45 @@ sequenceDiagram Main-->>User: стабильный JSON в stdout ``` -Паттерн: диспетчер команд и функциональное ядро. `cli/dispatch.py` только -выбирает один из пяти обработчиков групп команд: адаптеры и готовность, -контракты и подтверждения, жизненный цикл, наблюдаемость или планирование. -CLI-модули должны оставаться тонкими; поведение и тесты живут в доменных -сервисах. +Паттерн: диспетчер команд и функциональное ядро. `cli/dispatch.py` выбирает +единую команду запуска или один из пяти обработчиков групп: адаптеры и +готовность, контракты и подтверждения, жизненный цикл, наблюдаемость или +планирование. Модули командной строки остаются тонкими; поведение и тесты живут +в доменных сервисах. + +### Единая команда запуска жизненного цикла + +```mermaid +sequenceDiagram + participant User as Пользователь + participant StartCLI as cli/start.py + participant Start as adapter_sessions/unified_start.py + participant Intake as adapter_sessions/task_intake.py + participant Resume as adapter_sessions/workflow_bridge.py + participant Store as adapter_sessions/session_store.py + + User->>StartCLI: start --adapter --file|--text|--resume + StartCLI->>Start: start_lifecycle() + alt обычная задача в auto/research/plan/review + Start->>Intake: start_adapter_task() + Intake-->>Start: проверяемый черновик + else зафиксированный ввод и явный implement + Start->>Intake: существующая передача управляемому шагу + Intake-->>Start: подтверждение управляемого шага + else сохранённая сессия ALK + Start->>Store: load_session() + Start->>Resume: resume_adapter_session() + Resume-->>Start: результат проверки происхождения + end + Start-->>User: agent-lifecycle-start-receipt.v1 +``` + +Фасад выбирает существующий примитив и не владеет переходами рабочего цикла. +Обычные режимы задачи не могут вызвать управляемое выполнение или запуск +внешнего инструмента. Передача зафиксированного входа требует явного режима +`implement` и полной привязки. Возобновление принимает только сохранённую +сессию ALK, проверяет адаптер и происхождение состояния и не трактует значение +как идентификатор диалога внешнего инструмента. ### Приём обычной задачи или Markdown diff --git a/docs/ru/lifecycle-cookbook.md b/docs/ru/lifecycle-cookbook.md index fca56be0..bcc09cb4 100644 --- a/docs/ru/lifecycle-cookbook.md +++ b/docs/ru/lifecycle-cookbook.md @@ -37,13 +37,14 @@ cat > work/tasks/research.md <<'EOF' - команды проверки, которые понадобятся позже. EOF -agent-lifecycle adapter task start \ +agent-lifecycle start \ --adapter codex \ + --mode research \ --file work/tasks/research.md \ - --out work/tasks/research-intake.json + --out work/tasks/research-start.json agent-lifecycle review-mesh recommend \ - --intake work/tasks/research-intake.json \ + --file work/tasks/research.md \ --out work/tasks/research-recommendation.json ``` @@ -55,13 +56,14 @@ agent-lifecycle review-mesh recommend \ Подходит для задачи, предложения или плана в одном файле: ```bash -agent-lifecycle adapter task start \ +agent-lifecycle start \ --adapter claude \ + --mode review \ --file tasks/proposal.md \ - --out work/review/proposal-intake.json + --out work/review/proposal-start.json agent-lifecycle review-mesh recommend \ - --intake work/review/proposal-intake.json \ + --file tasks/proposal.md \ --out work/review/proposal-recommendation.json ``` @@ -93,10 +95,11 @@ cat > work/review/plan-review-task.md <<'EOF' файлами, проверки безопасности и релизные заявления. EOF -agent-lifecycle adapter task start \ +agent-lifecycle start \ --adapter codex \ + --mode review \ --file work/review/plan-review-task.md \ - --out work/review/plan-review-intake.json + --out work/review/plan-review-start.json ``` ## Проверка изменений кода @@ -110,10 +113,11 @@ git diff origin/main...HEAD > work/code-review/current/diff.patch ``` ```bash -agent-lifecycle adapter task start \ +agent-lifecycle start \ --adapter codex \ + --mode review \ --file work/code-review/current/review-task.md \ - --out work/code-review/current/intake.json + --out work/code-review/current/start.json ``` Если архитектура описана, перечислите документы архитектуры в файле задачи. @@ -164,10 +168,11 @@ agent-lifecycle goal view \ начните с приёма задачи. ALK добавит только рекомендательные признаки профиля: ```bash -agent-lifecycle adapter task start \ +agent-lifecycle start \ --adapter codex \ + --mode plan \ --file work/bugs/checkout-regression.md \ - --out work/bugs/checkout-intake.json + --out work/bugs/checkout-start.json ``` Рекомендация не активирует проверку рабочего цикла. Проверенный @@ -198,3 +203,8 @@ agent-lifecycle adapter task start \ выполнен. - Обычный текст, Markdown и импортированные планы сами по себе не разрешают реализацию. + +Для обычной работы используйте `agent-lifecycle start`. В сценариях +автоматизации можно применять отдельные команды `adapter task start`, `adapter +run` и `adapter session resume`, когда нужен прямой контроль одного шага +жизненного цикла. diff --git a/docs/ru/quickstart.md b/docs/ru/quickstart.md index febed9fa..1fc319e2 100644 --- a/docs/ru/quickstart.md +++ b/docs/ru/quickstart.md @@ -27,7 +27,7 @@ PYTHONPATH=src python -m agent_lifecycle version устанавливайте точную семантическую версию: ```bash -python -m pip install agent-lifecycle-kit==1.51.0 +python -m pip install agent-lifecycle-kit==1.52.0 agent-lifecycle version ``` @@ -104,19 +104,50 @@ agent-lifecycle import plan \ реализацию и не заменяет зафиксированный план ALK, пока не пройдёт проверку и заморозку. -## Приём задачи для адаптера +## Единая команда запуска Для файла задачи или короткого текста: ```bash -agent-lifecycle adapter task start --adapter codex --file task.md -agent-lifecycle adapter task start --adapter codex --text "Исправь падающий тест" +agent-lifecycle start --adapter codex --file task.md +agent-lifecycle start --adapter codex --text "Исправь падающий тест" ``` -Обычный текст не запускает реализацию. Команда возвращает черновое -подтверждение, которое должно пройти проверку. Управляемое выполнение требует -зафиксированного запроса запуска или зафиксированного плана с привязкой к -рабочему циклу. +По умолчанию используется режим `auto`. Обычный текст не запускает реализацию: +команда возвращает `agent-lifecycle-start-receipt.v1` с черновым результатом, +который должен пройти проверку. Для узкой цели укажите неисполняющий режим: + +```bash +agent-lifecycle start --adapter codex --mode research --file research.md +agent-lifecycle start --adapter codex --mode plan --file feature.md +agent-lifecycle start --adapter codex --mode review --file proposed-plan.md +``` + +Только явный режим `implement` может передать управление существующему +управляемому шагу. Для него нужен структурированный зафиксированный запрос с +полной привязкой состояния, манифеста, lock-файла, задачи, операции и ревизий: + +```bash +agent-lifecycle start \ + --adapter codex \ + --mode implement \ + --file work/run/adapter-run-request.json +``` + +Чтобы возобновить сессию, ранее записанную ALK: + +```bash +agent-lifecycle start \ + --adapter codex \ + --resume \ + --session-root .alk/adapter-sessions +``` + +Команда проверяет сохранённый адаптер и происхождение состояния. Значение +`--resume` не трактуется как идентификатор диалога Codex, Claude, OpenCode или +другого внешнего инструмента. Эта версия не запускает внешний CLI. Для +автоматизации остаются отдельные команды `adapter task start`, `adapter run` и +`adapter session resume` из [справочника команд](reference/cli.md). ## Проверка изменений @@ -131,13 +162,14 @@ git diff origin/main...HEAD > work/code-review/current/diff.patch Затем передайте задачу в ALK без запуска реализации: ```bash -agent-lifecycle adapter task start \ +agent-lifecycle start \ --adapter codex \ + --mode review \ --file work/code-review/current/review-task.md \ - --out work/code-review/current/intake.json + --out work/code-review/current/start.json agent-lifecycle review-mesh recommend \ - --intake work/code-review/current/intake.json \ + --file work/code-review/current/review-task.md \ --out work/code-review/current/recommendation.json ``` @@ -158,6 +190,11 @@ agent-lifecycle review-mesh recommend --file task.md Чтобы подготовить локальные пакеты проверяющих из артефакта приёма задачи: ```bash +agent-lifecycle adapter task start \ + --adapter codex \ + --file work/code-review/current/review-task.md \ + --out work/code-review/current/intake.json + agent-lifecycle review-mesh prepare \ --intake work/code-review/current/intake.json \ --template leader-draft-review \ diff --git a/docs/ru/reference/cli.md b/docs/ru/reference/cli.md index 171c3125..7a06dc32 100644 --- a/docs/ru/reference/cli.md +++ b/docs/ru/reference/cli.md @@ -9,7 +9,7 @@ JSON, чтобы результат можно было проверять ав [проекта в PyPI](https://pypi.org/project/agent-lifecycle-kit/): ```bash -python -m pip install agent-lifecycle-kit==1.51.0 +python -m pip install agent-lifecycle-kit==1.52.0 ``` ## Основа @@ -44,6 +44,17 @@ python -m pip install agent-lifecycle-kit==1.51.0 ## Выполнение +- `agent-lifecycle start --adapter (--file task.md | --text "..." | + --resume )`: единая команда над приёмом задачи, передачей + зафиксированного запроса управляемому шагу и возобновлением сохранённой сессии + ALK. Псевдонимы источников: `--task-file` и `--task-text`; требуется ровно + одно действие. Режим `--mode auto|research|plan|review|implement` по умолчанию + равен `auto`. Обычный ввод и все режимы, кроме явного `implement`, ничего не + выполняют. Для `implement` нужен структурированный зафиксированный запрос с + полной привязкой состояния, манифеста, lock-файла, задачи, операции и ревизий. + Команда возвращает `agent-lifecycle-start-receipt.v1`, не запускает внешний + CLI и не принимает `--resume` за идентификатор диалога внешнего инструмента. + - `agent-lifecycle workflow run`: проверяет связь зафиксированного плана и сохранённого состояния, затем возвращает следующий шаг для хоста без записи в состояние и без запуска модели. Добавьте `--progress-hook stderr`, чтобы @@ -114,6 +125,10 @@ python -m pip install agent-lifecycle-kit==1.51.0 ## Адаптеры +Для обычного запуска используйте `agent-lifecycle start`. Следующие команды +остаются отдельным интерфейсом для сценариев автоматизации и опытных +пользователей. + - `agent-lifecycle adapter validate`: проверка дескриптора. - `agent-lifecycle adapter inspect`: безопасный осмотр адаптера. - `agent-lifecycle adapter install-plan`: пробный план установки без записи. diff --git a/docs/ru/reference/managed-adapter-sessions.md b/docs/ru/reference/managed-adapter-sessions.md index 898cc999..0c876889 100644 --- a/docs/ru/reference/managed-adapter-sessions.md +++ b/docs/ru/reference/managed-adapter-sessions.md @@ -3,6 +3,10 @@ Управляемые сессии адаптеров дают оператору одну точку входа ALK для работы через адаптеры, но не превращают ALK во вторую среду выполнения кодового агента. +Самая простая точка входа - `agent-lifecycle start`. Она выбирает приём задачи, +явную передачу зафиксированного запроса управляемому шагу или возобновление +сохранённой сессии, сохраняя прежние низкоуровневые контракты. + Есть три режима: - интерактивная сессия: `adapter session start --adapter ` записывает @@ -27,6 +31,11 @@ ## Команды ```bash +agent-lifecycle start --adapter codex --file task.md +agent-lifecycle start --adapter codex --mode research --text "Исследуй текущую архитектуру" +agent-lifecycle start --adapter codex --mode implement --file adapter-run-request.json +agent-lifecycle start --adapter codex --resume + agent-lifecycle adapter session start --adapter codex agent-lifecycle adapter session start --adapter codex --launch agent-lifecycle adapter session status --session @@ -49,6 +58,14 @@ agent-lifecycle adapter run \ --task ``` +`start` возвращает `agent-lifecycle-start-receipt.v1`. Сводка вложенного +результата содержит только устойчивые статусы, рекомендации и отпечатки +подтверждений; исходный текст задачи и локальные абсолютные пути исключены. +Режимы `auto`, `research`, `plan` и `review` ничего не выполняют. Режим +`implement` передаёт только полный структурированный зафиксированный запрос. +Возобновление принимает только сессию, записанную ALK, проверяет адаптер и +происхождение состояния и не подключается к диалогу внешнего инструмента. + `adapter task start` возвращает `agent-adapter-task-start-receipt.v1`. Для обычного текста и Markdown подтверждение хранит только метку источника, отпечаток и размер в байтах, но не исходный текст задачи. Поле @@ -100,6 +117,7 @@ start --launch` и прямой общий запуск через дескри Стабильные подтверждения: `agent-adapter-session-receipt.v1`, `agent-managed-adapter-launch-receipt.v1`, `agent-adapter-session-resume-receipt.v1`, -`agent-adapter-task-start-receipt.v1` и -`agent-adapter-task-run-request.v1`. Рекомендации групповой проверки используют +`agent-adapter-task-start-receipt.v1`, +`agent-adapter-task-run-request.v1` и `agent-lifecycle-start-receipt.v1`. +Рекомендации групповой проверки используют `agent-review-mesh-recommendation.v1`. diff --git a/pyproject.toml b/pyproject.toml index 013a6da9..7d1746e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agent-lifecycle-kit" -version = "1.51.0" +version = "1.52.0" description = "Provider-neutral lifecycle control layer that helps coding agents plan, execute, validate, and prove software tasks to completion." readme = "README.md" requires-python = ">=3.11,<3.15" diff --git a/skills/agent-workflow-orchestrator/SKILL.md b/skills/agent-workflow-orchestrator/SKILL.md index 527d5e0d..a10a103a 100644 --- a/skills/agent-workflow-orchestrator/SKILL.md +++ b/skills/agent-workflow-orchestrator/SKILL.md @@ -38,18 +38,19 @@ write `agent-progress-hook-receipt.v1` with `--progress-hook receipt --progress-receipt `. Use this only as a display/proof-of-managed-command layer; installing a plugin or skill is not proof that the lifecycle ran. -For adapter-backed work, prefer `agent-lifecycle adapter task start` as the -operator-facing entrypoint. Raw `--file`/`--task-file` and `--text`/`--task-text` -inputs create reviewed draft intake only; they do not claim lifecycle coverage -or start implementation. If the receipt includes -`reviewMeshRecommendation`, surface it as advice and ask for operator or -reviewed-plan confirmation before treating Review Mesh as required evidence. -When the operator already has a frozen workflow state and task id, `adapter -task start` can consume a frozen run request or frozen manifest binding and -delegate to the managed run path. Use `adapter run` for the lower-level managed -command, and use `adapter session start` only to record an interactive -`WAITING_FOR_TASK` session. Native host launching remains descriptor-driven and -host-owned. +For adapter-backed work, prefer `agent-lifecycle start --adapter ` as the +operator-facing entrypoint. It requires exactly one task source or `--resume +`. Raw `--file`/`--task-file` and `--text`/`--task-text` inputs in +`auto`, `research`, `plan` or `review` mode create reviewed draft intake only; +they do not claim lifecycle coverage or start implementation. Surface the +receipt's review recommendation as advice and require operator or reviewed-plan +confirmation before treating multi-review as mandatory evidence. Only explicit +`--mode implement` may consume a fully bound frozen request and delegate to the +existing managed run path. `--resume` accepts only stored ALK session identity; +it must never guess a native host conversation id. Use `adapter task start`, +`adapter run` and `adapter session resume` as lower-level commands, and use +`adapter session start` only to record an interactive `WAITING_FOR_TASK` +session. Native host launching remains descriptor-driven and host-owned. For common operator requests, route through the cookbook before dropping to atomic commands: `docs/guides/lifecycle-cookbook.md` covers research-only, diff --git a/src/agent_lifecycle/_version.py b/src/agent_lifecycle/_version.py index 2fa726c7..beaab314 100644 --- a/src/agent_lifecycle/_version.py +++ b/src/agent_lifecycle/_version.py @@ -1,3 +1,3 @@ """Package version.""" -__version__ = "1.51.0" +__version__ = "1.52.0" diff --git a/src/agent_lifecycle/adapter_sessions/__init__.py b/src/agent_lifecycle/adapter_sessions/__init__.py index b97d1ed2..c334e4b5 100644 --- a/src/agent_lifecycle/adapter_sessions/__init__.py +++ b/src/agent_lifecycle/adapter_sessions/__init__.py @@ -1,7 +1,9 @@ """Managed adapter session helpers.""" from agent_lifecycle.adapter_sessions.contracts import ( + LIFECYCLE_START_RECEIPT_SCHEMA, build_adapter_session_receipt, + build_lifecycle_start_receipt, build_launch_receipt, build_resume_receipt, ) @@ -17,6 +19,7 @@ ADAPTER_TASK_START_RECEIPT_SCHEMA, start_adapter_task, ) +from agent_lifecycle.adapter_sessions.unified_start import START_MODES, start_lifecycle from agent_lifecycle.adapter_sessions.workflow_bridge import ( managed_adapter_run, promote_session_to_workflow, @@ -25,11 +28,14 @@ __all__ = [ "build_adapter_session_receipt", + "build_lifecycle_start_receipt", "build_launch_receipt", "build_resume_receipt", "create_session", "ADAPTER_TASK_RUN_REQUEST_SCHEMA", "ADAPTER_TASK_START_RECEIPT_SCHEMA", + "LIFECYCLE_START_RECEIPT_SCHEMA", + "START_MODES", "launch_from_descriptor", "load_session", "managed_adapter_run", @@ -37,5 +43,6 @@ "resume_adapter_session", "session_path", "start_adapter_task", + "start_lifecycle", "update_session", ] diff --git a/src/agent_lifecycle/adapter_sessions/contracts.py b/src/agent_lifecycle/adapter_sessions/contracts.py index 5fe16fbb..9c8019bf 100644 --- a/src/agent_lifecycle/adapter_sessions/contracts.py +++ b/src/agent_lifecycle/adapter_sessions/contracts.py @@ -10,6 +10,7 @@ ADAPTER_SESSION_RECEIPT_SCHEMA = "agent-adapter-session-receipt.v1" MANAGED_ADAPTER_LAUNCH_RECEIPT_SCHEMA = "agent-managed-adapter-launch-receipt.v1" ADAPTER_SESSION_RESUME_RECEIPT_SCHEMA = "agent-adapter-session-resume-receipt.v1" +LIFECYCLE_START_RECEIPT_SCHEMA = "agent-lifecycle-start-receipt.v1" def build_adapter_session_receipt( @@ -127,6 +128,44 @@ def build_resume_receipt( return {**body, "receiptDigest": canonical_digest(body)} +def build_lifecycle_start_receipt( + *, + status: str, + adapter_id: str, + requested_mode: str, + action: str, + input_summary: dict[str, Any], + delegate_summary: dict[str, Any] | None = None, + execution_started: bool = False, + lifecycle_coverage_claimed: bool = False, + requires_review: bool = False, + blockers: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """Build the public, path-safe receipt for the unified start facade.""" + + body = { + "schemaVersion": LIFECYCLE_START_RECEIPT_SCHEMA, + "status": status, + "adapterId": adapter_id, + "requestedMode": requested_mode, + "action": action, + "input": input_summary, + "delegate": delegate_summary, + "executionStarted": execution_started, + "lifecycleCoverageClaimed": lifecycle_coverage_claimed, + "requiresReview": requires_review, + "modelCallsStarted": False, + "hostLaunchStarted": False, + "nativeSessionAttached": False, + "rawTaskTextStored": False, + "secretsWritten": False, + "nativeConfigWritten": False, + "blockers": blockers or [], + "productionPromotionClaimed": False, + } + return {**body, "receiptDigest": canonical_digest(body)} + + def _profile_summary(profile: dict[str, Any]) -> dict[str, Any]: return { "status": profile.get("status"), diff --git a/src/agent_lifecycle/adapter_sessions/unified_start.py b/src/agent_lifecycle/adapter_sessions/unified_start.py new file mode 100644 index 00000000..28df68e1 --- /dev/null +++ b/src/agent_lifecycle/adapter_sessions/unified_start.py @@ -0,0 +1,370 @@ +"""Fail-closed facade over task intake, managed run and session resume.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from agent_lifecycle.adapter_sessions.contracts import build_lifecycle_start_receipt +from agent_lifecycle.adapter_sessions.session_store import load_session +from agent_lifecycle.adapter_sessions.task_intake import ( + ADAPTER_TASK_RUN_REQUEST_SCHEMA, + start_adapter_task, +) +from agent_lifecycle.adapter_sessions.workflow_bridge import resume_adapter_session +from agent_lifecycle.contracts import LifecycleError, load_json_object, sha256_hex + +START_MODES = ("auto", "research", "plan", "review", "implement") +_NON_EXECUTING_MODES = frozenset({"auto", "research", "plan", "review"}) +_SESSION_STATE_SCHEMA = "agent-adapter-session-state.v1" +_MANAGED_PROOF_KIND = "alk-managed-adapter-session" +_LINEAGE_STRING_FIELDS = ("runId", "packageId", "planDigest", "sourceRevision", "phase") +_LINEAGE_INTEGER_FIELDS = ("planRevision", "stateRevision") + + +def start_lifecycle( + *, + adapter_id: str, + mode: str = "auto", + task_file: Path | None = None, + task_text: str | None = None, + resume_session_id: str | None = None, + candidate_out: Path | None = None, + descriptor_path: Path | None = None, + session_root: Path | None = None, + state_path: Path | None = None, + lock_path: Path | None = None, + task_id: str | None = None, + operation_id: str | None = None, + expected_revision: int | None = None, + source_revision: str | None = None, + max_input_bytes: int = 32768, + target_tokens: int = 4096, + package_id: str = "unified-start", +) -> dict[str, Any]: + """Select one existing lifecycle action without creating new authority.""" + + if not adapter_id: + return _blocked(adapter_id="", mode=mode, input_summary=_empty_input(), code="start-adapter-required") + if mode not in START_MODES: + return _blocked(adapter_id=adapter_id, mode="auto", input_summary=_empty_input(), code="start-mode-invalid") + + has_task_source = task_file is not None or task_text is not None + if resume_session_id is not None: + if has_task_source: + return _blocked(adapter_id=adapter_id, mode=mode, input_summary=_session_input(resume_session_id), code="start-action-conflict") + if mode != "auto": + return _blocked(adapter_id=adapter_id, mode=mode, input_summary=_session_input(resume_session_id), code="start-resume-mode-invalid") + return _resume(adapter_id=adapter_id, session_id=resume_session_id, session_root=session_root) + if not has_task_source or (task_file is not None and task_text is not None): + return _blocked(adapter_id=adapter_id, mode=mode, input_summary=_empty_input(), code="start-task-source-invalid") + + input_summary, payload = _inspect_task_source(task_file=task_file, task_text=task_text) + structured_frozen = _is_frozen_input(payload) + if mode == "implement" and not structured_frozen: + return _blocked(adapter_id=adapter_id, mode=mode, input_summary=input_summary, code="start-implement-frozen-input-required") + if mode == "implement": + missing = _missing_frozen_bindings( + payload, + state_path=state_path, + lock_path=lock_path, + task_id=task_id, + operation_id=operation_id, + expected_revision=expected_revision, + source_revision=source_revision, + ) + if missing: + return build_lifecycle_start_receipt( + status="BLOCKED", + adapter_id=adapter_id, + requested_mode=mode, + action="BLOCKED", + input_summary=input_summary, + blockers=[{"code": "start-frozen-binding-missing", "fields": missing}], + ) + if mode in _NON_EXECUTING_MODES and structured_frozen: + return _blocked(adapter_id=adapter_id, mode=mode, input_summary=input_summary, code="start-mode-implement-required") + + receipt = start_adapter_task( + adapter_id=adapter_id, + task_file=task_file, + task_text=task_text, + candidate_out=candidate_out, + descriptor_path=descriptor_path, + session_root=session_root, + state_path=state_path, + lock_path=lock_path, + task_id=task_id, + operation_id=operation_id, + expected_revision=expected_revision, + source_revision=source_revision, + max_input_bytes=max_input_bytes, + target_tokens=target_tokens, + package_id=package_id, + ) + if mode in _NON_EXECUTING_MODES and _claims_execution(receipt): + return _blocked(adapter_id=adapter_id, mode=mode, input_summary=input_summary, code="start-non-implement-execution-claim") + return _from_task_receipt(adapter_id=adapter_id, mode=mode, receipt=receipt) + + +def _resume(*, adapter_id: str, session_id: str, session_root: Path | None) -> dict[str, Any]: + input_summary = _session_input(session_id) + try: + session = load_session(session_id, session_root=session_root) + except (LifecycleError, OSError): + return _blocked(adapter_id=adapter_id, mode="auto", input_summary=input_summary, code="start-resume-session-missing") + + blockers = _session_blockers(session, session_id=session_id, adapter_id=adapter_id) + if blockers: + return build_lifecycle_start_receipt( + status="BLOCKED", + adapter_id=adapter_id, + requested_mode="auto", + action="BLOCKED", + input_summary=input_summary, + blockers=blockers, + ) + try: + receipt = resume_adapter_session(session_id=session_id, session_root=session_root, adapter_id=adapter_id) + except (LifecycleError, OSError): + return _blocked(adapter_id=adapter_id, mode="auto", input_summary=input_summary, code="start-resume-session-invalid") + status = str(receipt.get("status", "BLOCKED")) + if status not in {"PASS", "UNMANAGED"}: + return build_lifecycle_start_receipt( + status="BLOCKED", + adapter_id=adapter_id, + requested_mode="auto", + action="BLOCKED", + input_summary=input_summary, + delegate_summary=_resume_summary(receipt), + blockers=_blocker_summaries(receipt.get("blockers")), + ) + return build_lifecycle_start_receipt( + status=status, + adapter_id=adapter_id, + requested_mode="auto", + action="RESUME", + input_summary=input_summary, + delegate_summary=_resume_summary(receipt), + lifecycle_coverage_claimed=status == "PASS" and bool(receipt.get("lifecycleCoverageClaimed")), + ) + + +def _session_blockers(session: dict[str, Any], *, session_id: str, adapter_id: str) -> list[dict[str, Any]]: + blockers: list[dict[str, Any]] = [] + if session.get("schemaVersion") != _SESSION_STATE_SCHEMA: + blockers.append({"code": "start-resume-session-schema-invalid"}) + if session.get("sessionId") != session_id: + blockers.append({"code": "start-resume-session-id-mismatch"}) + if session.get("adapterId") != adapter_id: + blockers.append({"code": "start-resume-adapter-mismatch"}) + identity = session.get("stateIdentity") + if identity is None: + return blockers + if not isinstance(identity, dict): + blockers.append({"code": "start-resume-lineage-invalid"}) + return blockers + missing = [field for field in _LINEAGE_STRING_FIELDS if not isinstance(identity.get(field), str) or not identity.get(field)] + missing.extend( + field + for field in _LINEAGE_INTEGER_FIELDS + if not isinstance(identity.get(field), int) or isinstance(identity.get(field), bool) or identity.get(field) < 1 + ) + if missing: + blockers.append({"code": "start-resume-lineage-invalid", "fields": sorted(missing)}) + return blockers + proof = session.get("managedWorkflowProof") + if not isinstance(proof, dict): + blockers.append({"code": "start-resume-proof-missing"}) + return blockers + if ( + proof.get("kind") != _MANAGED_PROOF_KIND + or proof.get("status") != "PASS" + or proof.get("adapterId") != adapter_id + or proof.get("stateIdentity") != identity + ): + blockers.append({"code": "start-resume-proof-mismatch"}) + return blockers + + +def _inspect_task_source(*, task_file: Path | None, task_text: str | None) -> tuple[dict[str, Any], dict[str, Any] | None]: + if task_text is not None: + data = task_text.encode("utf-8") + return _source_input("TEXT", "inline-task", data), _parse_payload(data) + if task_file is None or not task_file.is_file(): + label = task_file.name if task_file is not None else None + return {**_empty_input(), "label": label}, None + try: + data = task_file.read_bytes() + except OSError: + return {**_empty_input(), "label": task_file.name}, None + return _source_input("FILE", task_file.name, data), _parse_payload(data) + + +def _parse_payload(data: bytes) -> dict[str, Any] | None: + try: + return load_json_object(data, label="lifecycle start input") + except LifecycleError: + return None + + +def _is_frozen_input(payload: dict[str, Any] | None) -> bool: + if not isinstance(payload, dict): + return False + if payload.get("schemaVersion") == ADAPTER_TASK_RUN_REQUEST_SCHEMA: + return True + return payload.get("schemaVersion") == "agent-plan-manifest.v1" and payload.get("status") == "FROZEN" + + +def _missing_frozen_bindings( + payload: dict[str, Any] | None, + *, + state_path: Path | None, + lock_path: Path | None, + task_id: str | None, + operation_id: str | None, + expected_revision: int | None, + source_revision: str | None, +) -> list[str]: + if not isinstance(payload, dict): + return ["frozenInput"] + if payload.get("schemaVersion") == ADAPTER_TASK_RUN_REQUEST_SCHEMA: + values = { + "state": payload.get("state"), + "manifest": payload.get("manifest"), + "lock": payload.get("lock"), + "task": payload.get("task"), + "operationId": payload.get("operationId"), + "expectedRevision": payload.get("expectedRevision"), + "sourceRevision": payload.get("sourceRevision"), + } + else: + values = { + "state": state_path, + "manifest": "provided-input", + "lock": lock_path, + "task": task_id, + "operationId": operation_id, + "expectedRevision": expected_revision, + "sourceRevision": source_revision, + } + missing = [field for field, value in values.items() if value in {None, ""}] + revision = values.get("expectedRevision") + if revision is not None and (not isinstance(revision, int) or isinstance(revision, bool) or revision < 1): + missing.append("expectedRevision") + return sorted(set(missing)) + + +def _from_task_receipt(*, adapter_id: str, mode: str, receipt: dict[str, Any]) -> dict[str, Any]: + status = str(receipt.get("status", "BLOCKED")) + action = str(receipt.get("action", "BLOCKED")) + if status not in {"REVIEW_REQUIRED", "READY", "BLOCKED"}: + status = "BLOCKED" + action = "BLOCKED" + return build_lifecycle_start_receipt( + status=status, + adapter_id=adapter_id, + requested_mode=mode, + action=action, + input_summary=_safe_input(receipt.get("input")), + delegate_summary=_task_summary(receipt), + execution_started=mode == "implement" and bool(receipt.get("executionStarted")), + lifecycle_coverage_claimed=mode == "implement" and bool(receipt.get("lifecycleCoverageClaimed")), + requires_review=bool(receipt.get("requiresReview")), + blockers=_blocker_summaries(receipt.get("reviewBlockers")), + ) + + +def _task_summary(receipt: dict[str, Any]) -> dict[str, Any]: + planning = receipt.get("planningImport") if isinstance(receipt.get("planningImport"), dict) else {} + session = receipt.get("adapterSessionReceipt") if isinstance(receipt.get("adapterSessionReceipt"), dict) else {} + recommendation = ( + receipt.get("reviewMeshRecommendation") + if isinstance(receipt.get("reviewMeshRecommendation"), dict) + else {} + ) + return { + "schemaVersion": receipt.get("schemaVersion"), + "status": receipt.get("status"), + "action": receipt.get("action"), + "detectedTaskShape": receipt.get("detectedTaskShape"), + "recommendedQualityProfiles": list(receipt.get("recommendedQualityProfiles", [])), + "planningImportDigest": planning.get("importDigest"), + "sessionReceiptDigest": session.get("receiptDigest"), + "reviewRecommendation": { + "recommendedMode": recommendation.get("recommendedMode"), + "phaseCoverage": list(recommendation.get("phaseCoverage", [])), + "requiredReviewers": recommendation.get("requiredReviewers"), + "advisoryOnly": recommendation.get("advisoryOnly"), + "recommendationDigest": recommendation.get("recommendationDigest"), + } + if recommendation + else None, + "receiptDigest": receipt.get("receiptDigest"), + } + + +def _resume_summary(receipt: dict[str, Any]) -> dict[str, Any]: + return { + "schemaVersion": receipt.get("schemaVersion"), + "status": receipt.get("status"), + "lineageStatus": receipt.get("lineageStatus"), + "managedWorkflow": bool(receipt.get("managedWorkflow")), + "lifecycleCoverageClaimed": bool(receipt.get("lifecycleCoverageClaimed")), + "receiptDigest": receipt.get("receiptDigest"), + } + + +def _claims_execution(receipt: dict[str, Any]) -> bool: + return any(bool(receipt.get(field)) for field in ("executionStarted", "hostLaunchStarted", "lifecycleCoverageClaimed")) + + +def _safe_input(value: Any) -> dict[str, Any]: + if not isinstance(value, dict): + return _empty_input() + return { + "type": value.get("type", "NONE"), + "label": value.get("label"), + "digest": value.get("digest", sha256_hex(b"")), + "byteCount": value.get("byteCount", 0), + "rawTextStored": False, + } + + +def _source_input(kind: str, label: str, data: bytes) -> dict[str, Any]: + return {"type": kind, "label": label, "digest": sha256_hex(data), "byteCount": len(data), "rawTextStored": False} + + +def _session_input(session_id: str) -> dict[str, Any]: + encoded = session_id.encode("utf-8") + return {"type": "SESSION", "label": session_id, "digest": sha256_hex(encoded), "byteCount": len(encoded), "rawTextStored": False} + + +def _empty_input() -> dict[str, Any]: + return {"type": "NONE", "label": None, "digest": sha256_hex(b""), "byteCount": 0, "rawTextStored": False} + + +def _blocker_summaries(value: Any) -> list[dict[str, Any]]: + if not isinstance(value, list): + return [] + summaries: list[dict[str, Any]] = [] + for item in value: + if isinstance(item, dict): + summary = {"code": str(item.get("code", "start-delegate-blocked"))} + if isinstance(item.get("field"), str): + summary["field"] = item["field"] + if isinstance(item.get("fields"), list): + summary["fields"] = [str(field) for field in item["fields"]] + summaries.append(summary) + return summaries + + +def _blocked(*, adapter_id: str, mode: str, input_summary: dict[str, Any], code: str) -> dict[str, Any]: + return build_lifecycle_start_receipt( + status="BLOCKED", + adapter_id=adapter_id, + requested_mode=mode, + action="BLOCKED", + input_summary=input_summary, + blockers=[{"code": code}], + ) diff --git a/src/agent_lifecycle/cli/dispatch.py b/src/agent_lifecycle/cli/dispatch.py index 4183e7dc..8df571c5 100644 --- a/src/agent_lifecycle/cli/dispatch.py +++ b/src/agent_lifecycle/cli/dispatch.py @@ -12,12 +12,15 @@ from agent_lifecycle.cli.dispatch_planning import dispatch_planning from agent_lifecycle.cli.followup import dispatch_followup from agent_lifecycle.cli.policy import dispatch_policy +from agent_lifecycle.cli.start import dispatch_start from agent_lifecycle.cli.worktree import dispatch_worktree from agent_lifecycle.contracts import LifecycleError def dispatch(args: argparse.Namespace, remainder: list[str]) -> dict[str, Any] | str | None: """Route a parsed command without owning domain behavior or CLI output.""" + if args.command == "start": + return dispatch_start(args, remainder) del remainder if args.command in {"diagnose", "diagnostics", "adapter"}: return dispatch_adapters(args) diff --git a/src/agent_lifecycle/cli/parsers.py b/src/agent_lifecycle/cli/parsers.py index 70f3e413..879d7c84 100644 --- a/src/agent_lifecycle/cli/parsers.py +++ b/src/agent_lifecycle/cli/parsers.py @@ -9,6 +9,7 @@ from agent_lifecycle.cli.metrics_parser import add_metrics_parser from agent_lifecycle.cli.policy import add_policy_parser from agent_lifecycle.cli.progress_hooks import add_progress_hook_args +from agent_lifecycle.adapter_sessions import START_MODES from agent_lifecycle.cli.worktree import add_worktree_parser from agent_lifecycle.contracts.review_mesh_schemas import REVIEW_MESH_MODE_IDS from agent_lifecycle.review_mesh.operator_templates import REVIEW_MESH_OPERATOR_TEMPLATE_IDS @@ -18,6 +19,7 @@ def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="agent-lifecycle") subparsers = parser.add_subparsers(dest="command", required=True) subparsers.add_parser("version", help="print package version as compact JSON") + _add_start_parser(subparsers) _add_diagnose_parser(subparsers) _add_diagnostics_parser(subparsers) _add_schema_parser(subparsers) @@ -47,6 +49,29 @@ def build_parser() -> argparse.ArgumentParser: return parser +def _add_start_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None: + start = subparsers.add_parser("start", help="start or resume an ALK lifecycle action") + start.add_argument("--adapter", required=True) + action = start.add_mutually_exclusive_group(required=True) + action.add_argument("--file", "--task-file", dest="task_file") + action.add_argument("--text", "--task-text", dest="task_text") + action.add_argument("--resume", dest="resume_session_id") + start.add_argument("--mode", choices=list(START_MODES), default="auto") + start.add_argument("--descriptor") + start.add_argument("--session-root") + start.add_argument("--state") + start.add_argument("--lock") + start.add_argument("--task") + start.add_argument("--operation-id") + start.add_argument("--expected-revision", type=int) + start.add_argument("--source-revision") + start.add_argument("--candidate-out") + start.add_argument("--package-id", default="unified-start") + start.add_argument("--max-input-bytes", type=int, default=32768) + start.add_argument("--target-tokens", type=int, default=4096) + start.add_argument("--out") + + def _add_diagnose_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None: diagnose = subparsers.add_parser("diagnose", help="build a redacted readiness report") diagnose.add_argument("--project-root", default=".") diff --git a/src/agent_lifecycle/cli/start.py b/src/agent_lifecycle/cli/start.py new file mode 100644 index 00000000..995d4579 --- /dev/null +++ b/src/agent_lifecycle/cli/start.py @@ -0,0 +1,39 @@ +"""Root unified lifecycle start command.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Any + +from agent_lifecycle.adapter_sessions import start_lifecycle +from agent_lifecycle.contracts import LifecycleError, write_json_create + + +def dispatch_start(args: argparse.Namespace, remainder: list[str]) -> dict[str, Any]: + """Delegate the public start command to adapter-session composition.""" + + if remainder: + raise LifecycleError("start-argument-unknown", f"unknown start arguments: {' '.join(remainder)}") + payload = start_lifecycle( + adapter_id=args.adapter, + mode=args.mode, + task_file=Path(args.task_file) if args.task_file else None, + task_text=args.task_text, + resume_session_id=args.resume_session_id, + candidate_out=Path(args.candidate_out) if args.candidate_out else None, + descriptor_path=Path(args.descriptor) if args.descriptor else None, + session_root=Path(args.session_root) if args.session_root else None, + state_path=Path(args.state) if args.state else None, + lock_path=Path(args.lock) if args.lock else None, + task_id=args.task, + operation_id=args.operation_id, + expected_revision=args.expected_revision, + source_revision=args.source_revision, + max_input_bytes=args.max_input_bytes, + target_tokens=args.target_tokens, + package_id=args.package_id, + ) + if args.out: + write_json_create(Path(args.out), payload) + return payload diff --git a/src/agent_lifecycle/contracts/compatibility.py b/src/agent_lifecycle/contracts/compatibility.py index 547db575..24a46f45 100644 --- a/src/agent_lifecycle/contracts/compatibility.py +++ b/src/agent_lifecycle/contracts/compatibility.py @@ -68,6 +68,7 @@ {"command": "adapter session resume", "schemaVersion": "agent-adapter-session-resume-receipt.v1", "compatibility": "stable-json"}, {"command": "adapter run", "schemaVersion": "agent-adapter-session-receipt.v1", "compatibility": "stable-json"}, {"command": "adapter task start", "schemaVersion": "agent-adapter-task-start-receipt.v1", "compatibility": "stable-json"}, + {"command": "start", "schemaVersion": "agent-lifecycle-start-receipt.v1", "compatibility": "stable-json"}, {"command": "review-mesh profile", "schemaVersion": "agent-review-mesh-profile.v1", "compatibility": "stable-json"}, {"command": "review-mesh recommend", "schemaVersion": "agent-review-mesh-recommendation.v1", "compatibility": "stable-json"}, {"command": "specification completion-gate", "schemaVersion": "agent-completion-gate-receipt.v1", "compatibility": "stable-json"}, diff --git a/src/agent_lifecycle/contracts/schemas.py b/src/agent_lifecycle/contracts/schemas.py index 7f59a6a4..7d207150 100644 --- a/src/agent_lifecycle/contracts/schemas.py +++ b/src/agent_lifecycle/contracts/schemas.py @@ -33,9 +33,57 @@ from agent_lifecycle.contracts.status_goal_schemas import STATUS_GOAL_SCHEMAS from agent_lifecycle.contracts.task_template_schemas import TASK_TEMPLATE_SCHEMAS from agent_lifecycle.contracts.usage_export_schemas import USAGE_EXPORT_SCHEMAS +from agent_lifecycle.contracts.schema_builders import open_object_schema SCHEMA_INDEX_VERSION = "agent-lifecycle-schema-index.v1" +LIFECYCLE_START_SCHEMAS: dict[str, dict[str, Any]] = { + "agent-lifecycle-start-receipt.v1": open_object_schema( + "agent-lifecycle-start-receipt.v1", + required=[ + "schemaVersion", + "status", + "adapterId", + "requestedMode", + "action", + "input", + "delegate", + "executionStarted", + "lifecycleCoverageClaimed", + "requiresReview", + "modelCallsStarted", + "hostLaunchStarted", + "nativeSessionAttached", + "rawTaskTextStored", + "secretsWritten", + "nativeConfigWritten", + "blockers", + "productionPromotionClaimed", + "receiptDigest", + ], + properties={ + "status": {"enum": ["REVIEW_REQUIRED", "READY", "PASS", "UNMANAGED", "BLOCKED"]}, + "adapterId": {"type": "string", "minLength": 1}, + "requestedMode": {"enum": ["auto", "research", "plan", "review", "implement"]}, + "action": {"enum": ["DRAFT_INTAKE", "DRAFT_PLAN_REVIEW", "MANAGED_RUN", "RESUME", "BLOCKED"]}, + "input": {"type": "object"}, + "delegate": {"type": ["object", "null"]}, + "executionStarted": {"type": "boolean"}, + "lifecycleCoverageClaimed": {"type": "boolean"}, + "requiresReview": {"type": "boolean"}, + "modelCallsStarted": {"const": False}, + "hostLaunchStarted": {"const": False}, + "nativeSessionAttached": {"const": False}, + "rawTaskTextStored": {"const": False}, + "secretsWritten": {"const": False}, + "nativeConfigWritten": {"const": False}, + "blockers": {"type": "array", "items": {"type": "object"}}, + "productionPromotionClaimed": {"const": False}, + "receiptDigest": {"type": "string", "minLength": 64, "maxLength": 64}, + }, + ) +} + _SCHEMA_GROUPS = ( CORE_SCHEMAS, AUDIT_SCHEMAS, @@ -65,6 +113,7 @@ PLAN_CONTRACT_SCHEMAS, METRIC_SCHEMAS, POLICY_SCHEMAS, + LIFECYCLE_START_SCHEMAS, ) _SCHEMAS: dict[str, dict[str, Any]] = {} diff --git a/tests/adapter_sessions/test_unified_start_contracts.py b/tests/adapter_sessions/test_unified_start_contracts.py new file mode 100644 index 00000000..a25ed00e --- /dev/null +++ b/tests/adapter_sessions/test_unified_start_contracts.py @@ -0,0 +1,262 @@ +from __future__ import annotations + +import json +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from agent_lifecycle.adapter_sessions.session_store import create_session, session_path +from agent_lifecycle.adapter_sessions.unified_start import start_lifecycle +from agent_lifecycle.contracts.compatibility import build_contract_policy, validate_contract_policy +from agent_lifecycle.contracts.schemas import get_schema, list_schemas + + +class UnifiedStartContractTests(unittest.TestCase): + def test_raw_text_is_review_gated_and_never_starts_execution(self) -> None: + raw = "Investigate the checkout failure and prepare a plan" + + with ( + patch("agent_lifecycle.adapter_sessions.task_intake.managed_adapter_run") as managed_run, + patch("agent_lifecycle.adapter_sessions.launcher.launch_from_descriptor") as host_launch, + patch("agent_lifecycle.adapter_sessions.process.run_process") as process_run, + ): + receipt = start_lifecycle(adapter_id="codex", task_text=raw) + + self.assertEqual(receipt["schemaVersion"], "agent-lifecycle-start-receipt.v1") + self.assertEqual(receipt["status"], "REVIEW_REQUIRED") + self.assertEqual(receipt["action"], "DRAFT_INTAKE") + self.assertFalse(receipt["executionStarted"]) + self.assertFalse(receipt["modelCallsStarted"]) + self.assertFalse(receipt["hostLaunchStarted"]) + self.assertFalse(receipt["lifecycleCoverageClaimed"]) + self.assertTrue(receipt["requiresReview"]) + self.assertNotIn(raw, json.dumps(receipt, ensure_ascii=False)) + managed_run.assert_not_called() + host_launch.assert_not_called() + process_run.assert_not_called() + + def test_non_implement_modes_reject_complete_frozen_input(self) -> None: + request = _run_request() + for mode in ("auto", "research", "plan", "review"): + with self.subTest(mode=mode), patch("agent_lifecycle.adapter_sessions.task_intake.managed_adapter_run") as managed_run: + receipt = start_lifecycle(adapter_id="codex", mode=mode, task_text=json.dumps(request)) + + self.assertEqual(receipt["status"], "BLOCKED") + self.assertEqual(receipt["blockers"][0]["code"], "start-mode-implement-required") + self.assertFalse(receipt["executionStarted"]) + self.assertFalse(receipt["hostLaunchStarted"]) + managed_run.assert_not_called() + + def test_implement_rejects_raw_input_and_incomplete_bindings(self) -> None: + raw = start_lifecycle(adapter_id="codex", mode="implement", task_text="Implement this now") + incomplete = _run_request() + incomplete.pop("lock") + missing = start_lifecycle(adapter_id="codex", mode="implement", task_text=json.dumps(incomplete)) + + self.assertEqual(raw["blockers"][0]["code"], "start-implement-frozen-input-required") + self.assertEqual(missing["blockers"][0]["code"], "start-frozen-binding-missing") + self.assertEqual(missing["blockers"][0]["fields"], ["lock"]) + + def test_implement_delegates_complete_run_request_to_existing_managed_path(self) -> None: + managed_receipt = { + "schemaVersion": "agent-adapter-session-receipt.v1", + "status": "READY", + "blockers": [], + "lifecycleCoverageClaimed": True, + "hostLaunchStarted": False, + "receiptDigest": "a" * 64, + } + with patch( + "agent_lifecycle.adapter_sessions.task_intake.managed_adapter_run", + return_value=managed_receipt, + ) as managed_run: + receipt = start_lifecycle( + adapter_id="codex", + mode="implement", + task_text=json.dumps(_run_request()), + ) + + self.assertEqual(receipt["status"], "READY") + self.assertEqual(receipt["action"], "MANAGED_RUN") + self.assertTrue(receipt["executionStarted"]) + self.assertTrue(receipt["lifecycleCoverageClaimed"]) + self.assertFalse(receipt["hostLaunchStarted"]) + managed_run.assert_called_once() + + def test_implement_delegates_frozen_manifest_with_complete_bindings(self) -> None: + managed_receipt = { + "schemaVersion": "agent-adapter-session-receipt.v1", + "status": "READY", + "blockers": [], + "lifecycleCoverageClaimed": True, + "hostLaunchStarted": False, + "receiptDigest": "a" * 64, + } + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + manifest_path = root / "plan.manifest.json" + manifest_path.write_text( + json.dumps({"schemaVersion": "agent-plan-manifest.v1", "status": "FROZEN"}), + encoding="utf-8", + ) + with patch( + "agent_lifecycle.adapter_sessions.task_intake.managed_adapter_run", + return_value=managed_receipt, + ) as managed_run: + receipt = start_lifecycle( + adapter_id="codex", + mode="implement", + task_file=manifest_path, + state_path=root / "state.json", + lock_path=root / "plan.lock.json", + task_id="WS-01", + operation_id="start-run", + expected_revision=1, + source_revision="source", + ) + + self.assertEqual(receipt["status"], "READY") + self.assertEqual(receipt["action"], "MANAGED_RUN") + self.assertTrue(receipt["executionStarted"]) + managed_run.assert_called_once() + self.assertEqual(managed_run.call_args.kwargs["manifest_path"], manifest_path) + + def test_resume_blocks_missing_mismatched_and_corrupt_sessions(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + missing = start_lifecycle(adapter_id="codex", resume_session_id="missing", session_root=root) + session = create_session( + adapter_id="codex", + mode="INTERACTIVE", + status="WAITING_FOR_TASK", + launch_profile={"status": "WRAPPER_ONLY"}, + session_root=root, + ) + mismatch = start_lifecycle( + adapter_id="claude", + resume_session_id=session["sessionId"], + session_root=root, + ) + stored = json.loads(session_path(session["sessionId"], session_root=root).read_text(encoding="utf-8")) + stored["stateIdentity"] = {"runId": "run"} + session_path(session["sessionId"], session_root=root).write_text(json.dumps(stored), encoding="utf-8") + corrupt = start_lifecycle( + adapter_id="codex", + resume_session_id=session["sessionId"], + session_root=root, + ) + + self.assertEqual(missing["blockers"][0]["code"], "start-resume-session-missing") + self.assertEqual(mismatch["blockers"][0]["code"], "start-resume-adapter-mismatch") + self.assertEqual(corrupt["blockers"][0]["code"], "start-resume-lineage-invalid") + + def test_resume_rejects_every_explicit_non_auto_mode(self) -> None: + for mode in ("research", "plan", "review", "implement"): + with self.subTest(mode=mode): + receipt = start_lifecycle( + adapter_id="codex", + resume_session_id="session", + mode=mode, + ) + + self.assertEqual(receipt["status"], "BLOCKED") + self.assertEqual(receipt["blockers"][0]["code"], "start-resume-mode-invalid") + self.assertFalse(receipt["executionStarted"]) + self.assertFalse(receipt["nativeSessionAttached"]) + + def test_resume_accepts_persisted_managed_lineage_without_exposing_paths(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + identity = { + "statePath": (root / "private-state.json").as_posix(), + "runId": "run", + "packageId": "package", + "planRevision": 1, + "planDigest": "b" * 64, + "sourceRevision": "source", + "stateRevision": 2, + "phase": "RUNNING", + "taskId": "WS-01", + } + proof = { + "kind": "alk-managed-adapter-session", + "status": "PASS", + "command": "adapter run", + "adapterId": "codex", + "taskId": "WS-01", + "stateIdentity": identity, + } + session = create_session( + adapter_id="codex", + mode="MANAGED_TASK", + status="READY", + launch_profile={"status": "WRAPPER_ONLY"}, + session_root=root / "sessions", + state_identity=identity, + managed_workflow_proof=proof, + ) + + receipt = start_lifecycle( + adapter_id="codex", + resume_session_id=session["sessionId"], + session_root=root / "sessions", + ) + + self.assertEqual(receipt["status"], "PASS") + self.assertEqual(receipt["action"], "RESUME") + self.assertTrue(receipt["lifecycleCoverageClaimed"]) + self.assertFalse(receipt["nativeSessionAttached"]) + self.assertNotIn(root.as_posix(), json.dumps(receipt)) + + def test_resume_unbound_alk_session_is_truthfully_unmanaged(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + session = create_session( + adapter_id="codex", + mode="INTERACTIVE", + status="WAITING_FOR_TASK", + launch_profile={"status": "WRAPPER_ONLY"}, + session_root=root, + ) + receipt = start_lifecycle( + adapter_id="codex", + resume_session_id=session["sessionId"], + session_root=root, + ) + + self.assertEqual(receipt["status"], "UNMANAGED") + self.assertFalse(receipt["lifecycleCoverageClaimed"]) + + def test_public_schema_and_compatibility_row_are_registered(self) -> None: + ids = {item["id"] for item in list_schemas()["schemas"]} + schema = get_schema("agent-lifecycle-start-receipt.v1") + policy = build_contract_policy() + + self.assertIn("agent-lifecycle-start-receipt.v1", ids) + self.assertEqual(schema["properties"]["hostLaunchStarted"], {"const": False}) + self.assertEqual(schema["properties"]["nativeSessionAttached"], {"const": False}) + self.assertEqual(validate_contract_policy(policy)["status"], "PASS") + self.assertIn( + ("start", "agent-lifecycle-start-receipt.v1"), + {(item["command"], item["schemaVersion"]) for item in policy["cliOutputs"]}, + ) + + +def _run_request() -> dict[str, object]: + return { + "schemaVersion": "agent-adapter-task-run-request.v1", + "adapterId": "codex", + "state": "state.json", + "manifest": "tasks/release/plan.manifest.json", + "lock": "tasks/release/plan.lock.json", + "task": "WS-01", + "operationId": "start-run", + "expectedRevision": 1, + "sourceRevision": "source", + "productionPromotionClaimed": False, + } + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cli/test_foundation.py b/tests/cli/test_foundation.py index 58fd5005..31457df8 100644 --- a/tests/cli/test_foundation.py +++ b/tests/cli/test_foundation.py @@ -20,6 +20,7 @@ def test_root_dispatch_delegates_are_importable(self) -> None: "agent_lifecycle.cli.dispatch_lifecycle", "agent_lifecycle.cli.dispatch_observability", "agent_lifecycle.cli.dispatch_planning", + "agent_lifecycle.cli.start", ): with self.subTest(module=module_name): self.assertIsNotNone(import_module(module_name)) diff --git a/tests/cli/test_start_commands.py b/tests/cli/test_start_commands.py new file mode 100644 index 00000000..52163eb6 --- /dev/null +++ b/tests/cli/test_start_commands.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +import contextlib +import json +import tempfile +import unittest +from io import StringIO +from pathlib import Path +from unittest.mock import patch + +from agent_lifecycle.adapter_sessions.session_store import create_session +from agent_lifecycle.cli import main +from agent_lifecycle.cli.parsers import build_parser + + +class StartCommandTests(unittest.TestCase): + def test_text_defaults_to_auto_and_returns_json_receipt(self) -> None: + code, payload, stderr = _run_cli(["start", "--adapter", "codex", "--text", "Research the cache design"]) + + self.assertEqual(code, 0) + self.assertEqual(stderr, "") + self.assertEqual(payload["schemaVersion"], "agent-lifecycle-start-receipt.v1") + self.assertEqual(payload["requestedMode"], "auto") + self.assertEqual(payload["status"], "REVIEW_REQUIRED") + self.assertFalse(payload["executionStarted"]) + + def test_canonical_and_alias_task_flags_share_the_same_destinations(self) -> None: + parser = build_parser() + canonical_file = parser.parse_args(["start", "--adapter", "codex", "--file", "task.md"]) + alias_file = parser.parse_args(["start", "--adapter", "codex", "--task-file", "task.md"]) + canonical_text = parser.parse_args(["start", "--adapter", "codex", "--text", "task"]) + alias_text = parser.parse_args(["start", "--adapter", "codex", "--task-text", "task"]) + + self.assertEqual(canonical_file.task_file, alias_file.task_file) + self.assertEqual(canonical_text.task_text, alias_text.task_text) + + def test_parser_requires_adapter_and_exactly_one_action(self) -> None: + parser = build_parser() + cases = ( + ["start", "--text", "task"], + ["start", "--adapter", "codex"], + ["start", "--adapter", "codex", "--text", "task", "--resume", "session"], + ["start", "--adapter", "codex", "--file", "task.md", "--text", "task"], + ) + for argv in cases: + with self.subTest(argv=argv), contextlib.redirect_stderr(StringIO()), self.assertRaises(SystemExit): + parser.parse_args(argv) + + def test_research_mode_is_non_executing(self) -> None: + code, payload, _stderr = _run_cli( + ["start", "--adapter", "codex", "--mode", "research", "--task-text", "Inspect this module"] + ) + + self.assertEqual(code, 0) + self.assertEqual(payload["requestedMode"], "research") + self.assertEqual(payload["status"], "REVIEW_REQUIRED") + self.assertFalse(payload["executionStarted"]) + + def test_implement_mode_delegates_only_complete_structured_request(self) -> None: + managed_receipt = { + "schemaVersion": "agent-adapter-session-receipt.v1", + "status": "READY", + "blockers": [], + "lifecycleCoverageClaimed": True, + "hostLaunchStarted": False, + "receiptDigest": "a" * 64, + } + with patch( + "agent_lifecycle.adapter_sessions.task_intake.managed_adapter_run", + return_value=managed_receipt, + ) as managed_run: + code, payload, stderr = _run_cli( + [ + "start", + "--adapter", + "codex", + "--mode", + "implement", + "--text", + json.dumps(_run_request()), + ] + ) + + self.assertEqual(code, 0) + self.assertEqual(stderr, "") + self.assertEqual(payload["action"], "MANAGED_RUN") + self.assertTrue(payload["executionStarted"]) + managed_run.assert_called_once() + + def test_resume_missing_session_returns_blocked_receipt(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + code, payload, _stderr = _run_cli( + [ + "start", + "--adapter", + "codex", + "--resume", + "missing", + "--session-root", + tmp, + ] + ) + + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "BLOCKED") + self.assertEqual(payload["blockers"][0]["code"], "start-resume-session-missing") + + def test_resume_with_implement_mode_is_rejected_by_the_domain_guard(self) -> None: + code, payload, _stderr = _run_cli( + [ + "start", + "--adapter", + "codex", + "--resume", + "session", + "--mode", + "implement", + ] + ) + + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "BLOCKED") + self.assertEqual(payload["blockers"][0]["code"], "start-resume-mode-invalid") + self.assertFalse(payload["executionStarted"]) + + def test_resume_persisted_unbound_session_is_unmanaged(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + session = create_session( + adapter_id="codex", + mode="INTERACTIVE", + status="WAITING_FOR_TASK", + launch_profile={"status": "WRAPPER_ONLY"}, + session_root=root, + ) + code, payload, _stderr = _run_cli( + [ + "start", + "--adapter", + "codex", + "--resume", + session["sessionId"], + "--session-root", + str(root), + ] + ) + + self.assertEqual(code, 0) + self.assertEqual(payload["status"], "UNMANAGED") + self.assertFalse(payload["lifecycleCoverageClaimed"]) + + def test_out_writes_the_same_receipt_and_unknown_args_fail(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + out = Path(tmp) / "start.json" + code, payload, _stderr = _run_cli( + ["start", "--adapter", "codex", "--text", "Draft a plan", "--out", str(out)] + ) + written = json.loads(out.read_text(encoding="utf-8")) + unknown_code, unknown, _stderr = _run_cli( + ["start", "--adapter", "codex", "--text", "Draft a plan", "--unknown"] + ) + + self.assertEqual(code, 0) + self.assertEqual(written, payload) + self.assertEqual(unknown_code, 2) + self.assertEqual(unknown["code"], "start-argument-unknown") + + +def _run_cli(args: list[str]) -> tuple[int, dict, str]: + stdout = StringIO() + stderr = StringIO() + with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + code = main(args) + return code, json.loads(stdout.getvalue()), stderr.getvalue() + + +def _run_request() -> dict[str, object]: + return { + "schemaVersion": "agent-adapter-task-run-request.v1", + "adapterId": "codex", + "state": "state.json", + "manifest": "tasks/release/plan.manifest.json", + "lock": "tasks/release/plan.lock.json", + "task": "WS-01", + "operationId": "start-run", + "expectedRevision": 1, + "sourceRevision": "source", + "productionPromotionClaimed": False, + } + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/package/test_foundation.py b/tests/package/test_foundation.py index bb0291e3..8d1a5253 100644 --- a/tests/package/test_foundation.py +++ b/tests/package/test_foundation.py @@ -18,7 +18,7 @@ def test_package_metadata_is_stable(self) -> None: pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) project = pyproject["project"] self.assertEqual(project["name"], "agent-lifecycle-kit") - self.assertEqual(project["version"], "1.51.0") + self.assertEqual(project["version"], "1.52.0") self.assertEqual(project["requires-python"], ">=3.11,<3.15") self.assertEqual(project["license"]["text"], "Apache-2.0") self.assertEqual(project["dependencies"], []) @@ -62,7 +62,7 @@ def test_uv_lock_declares_runtime_graph(self) -> None: self.assertEqual(lock["requires-python"], ">=3.11, <3.15") packages = {package["name"]: package for package in lock["package"]} self.assertIn("agent-lifecycle-kit", packages) - self.assertEqual(packages["agent-lifecycle-kit"]["version"], "1.51.0") + self.assertEqual(packages["agent-lifecycle-kit"]["version"], "1.52.0") def test_foundation_ci_uses_stdlib_unittest(self) -> None: pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) diff --git a/tests/release/test_docs_gates.py b/tests/release/test_docs_gates.py index cc65594f..6a742022 100644 --- a/tests/release/test_docs_gates.py +++ b/tests/release/test_docs_gates.py @@ -126,6 +126,10 @@ def test_quickstart_and_adapter_docs_cover_bounded_commands(self) -> None: self.assertIn("agent-lifecycle plan check", text) self.assertIn("agent-lifecycle import plan", text) self.assertIn("agent-lifecycle context check", text) + self.assertIn("agent-lifecycle start --adapter codex", text) + self.assertIn("--mode research", text) + self.assertIn("--mode implement", text) + self.assertIn("--resume ", text) self.assertIn("lifecycle-cookbook.md", quickstart) self.assertIn("lifecycle-cookbook.md", quickstart_ru) for command in ( @@ -336,11 +340,11 @@ def _assert_links_resolve(path: Path) -> None: def _write_min_docs(root: Path, *, unsupported_verified_row: bool) -> None: _write_text( root / "README.md", - "`VERIFIED` for Codex CLI 0.145.0. `VERIFIED` for Claude Code 2.1.220. `VERIFIED` for OpenCode CLI 1.18.9. `VERIFIED` for Hermes Agent v0.19.0. `VERIFIED` for Qwen Code 0.21.0. `EXPERIMENTAL` means bounded live host conformance and usage/resource calibration are required. Public contracts live in docs/reference/public-contracts.md. https://pypi.org/project/agent-lifecycle-kit/ supports Python 3.11-3.14. `completionCheck` requires `agent-completion-check-receipt.v1`. `agent-goal-record.v1` produces `agent-objective-snapshot.v1`. `agent-runner-state.v1` produces `agent-runner-snapshot.v1`. `agent-follow-up-register.v1` produces `agent-follow-up-summary.v1`. `agent-worktree-isolation-policy.v1` validates `agent-worktree-attempt-receipt.v1`. `agent-adapter-event-stream-receipt.v1` validates `agent-adapter-event-capture-validation.v1`. `agent-review-verdict.v1` produces `agent-review-routing-summary.v1`. `agent-optional-quality-pack.v1`. `agent-behavior-check-run.v1`. `agent-diagnostic-bundle.v1`. `agent-readonly-status-view.v1`. `agent-workflow-event-feed.v1`. `agent-lifecycle-progress-view.v1`.\n", + "`VERIFIED` for Codex CLI 0.145.0. `VERIFIED` for Claude Code 2.1.220. `VERIFIED` for OpenCode CLI 1.18.9. `VERIFIED` for Hermes Agent v0.19.0. `VERIFIED` for Qwen Code 0.21.0. `EXPERIMENTAL` means bounded live host conformance and usage/resource calibration are required. Public contracts live in docs/reference/public-contracts.md. https://pypi.org/project/agent-lifecycle-kit/ supports Python 3.11-3.14. agent-lifecycle start --adapter codex. `completionCheck` requires `agent-completion-check-receipt.v1`. `agent-goal-record.v1` produces `agent-objective-snapshot.v1`. `agent-runner-state.v1` produces `agent-runner-snapshot.v1`. `agent-follow-up-register.v1` produces `agent-follow-up-summary.v1`. `agent-worktree-isolation-policy.v1` validates `agent-worktree-attempt-receipt.v1`. `agent-adapter-event-stream-receipt.v1` validates `agent-adapter-event-capture-validation.v1`. `agent-review-verdict.v1` produces `agent-review-routing-summary.v1`. `agent-optional-quality-pack.v1`. `agent-behavior-check-run.v1`. `agent-diagnostic-bundle.v1`. `agent-readonly-status-view.v1`. `agent-workflow-event-feed.v1`. `agent-lifecycle-progress-view.v1`.\n", ) _write_text( root / "docs/ru/README.md", - "`VERIFIED` для Codex CLI 0.145.0. `VERIFIED` для Claude Code 2.1.220. `VERIFIED` для OpenCode CLI 1.18.9. `VERIFIED` для Hermes Agent v0.19.0. `VERIFIED` для Qwen Code 0.21.0. `EXPERIMENTAL` означает, что без калибровки расхода продвижение запрещено. Список в Публичных контрактах: reference/public-contracts.md. https://pypi.org/project/agent-lifecycle-kit/ поддерживает Python 3.11-3.14. `completionCheck` требует `agent-completion-check-receipt.v1`. `agent-goal-record.v1` создаёт `agent-objective-snapshot.v1`. `agent-runner-state.v1` создаёт `agent-runner-snapshot.v1`. `agent-follow-up-register.v1` создаёт `agent-follow-up-summary.v1`. `agent-worktree-isolation-policy.v1` проверяет `agent-worktree-attempt-receipt.v1`. `agent-adapter-event-stream-receipt.v1` проверяет `agent-adapter-event-capture-validation.v1`. `agent-review-verdict.v1` создаёт `agent-review-routing-summary.v1`. `agent-optional-quality-pack.v1`. `agent-behavior-check-run.v1`. `agent-diagnostic-bundle.v1`. `agent-readonly-status-view.v1`. `agent-workflow-event-feed.v1`. `agent-lifecycle-progress-view.v1`.\n", + "`VERIFIED` для Codex CLI 0.145.0. `VERIFIED` для Claude Code 2.1.220. `VERIFIED` для OpenCode CLI 1.18.9. `VERIFIED` для Hermes Agent v0.19.0. `VERIFIED` для Qwen Code 0.21.0. `EXPERIMENTAL` означает, что без калибровки расхода продвижение запрещено. Список в Публичных контрактах: reference/public-contracts.md. https://pypi.org/project/agent-lifecycle-kit/ поддерживает Python 3.11-3.14. agent-lifecycle start --adapter codex. `completionCheck` требует `agent-completion-check-receipt.v1`. `agent-goal-record.v1` создаёт `agent-objective-snapshot.v1`. `agent-runner-state.v1` создаёт `agent-runner-snapshot.v1`. `agent-follow-up-register.v1` создаёт `agent-follow-up-summary.v1`. `agent-worktree-isolation-policy.v1` проверяет `agent-worktree-attempt-receipt.v1`. `agent-adapter-event-stream-receipt.v1` проверяет `agent-adapter-event-capture-validation.v1`. `agent-review-verdict.v1` создаёт `agent-review-routing-summary.v1`. `agent-optional-quality-pack.v1`. `agent-behavior-check-run.v1`. `agent-diagnostic-bundle.v1`. `agent-readonly-status-view.v1`. `agent-workflow-event-feed.v1`. `agent-lifecycle-progress-view.v1`.\n", ) public_contracts = ( "`completionCheck`.\n" @@ -420,6 +424,8 @@ def _write_min_docs(root: Path, *, unsupported_verified_row: bool) -> None: "docs/ru/lifecycle-cookbook.md.\n" "adapter session start/status/resume/promote.\n" "adapter run.\n" + "agent-lifecycle start --adapter .\n" + "`agent-lifecycle-start-receipt.v1`.\n" "`WAITING_FOR_TASK`.\n" "`agent-adapter-session-receipt.v1`.\n" "https://pypi.org/project/agent-lifecycle-kit/.\n" @@ -438,7 +444,7 @@ def _write_min_docs(root: Path, *, unsupported_verified_row: bool) -> None: "Проверка изменений кода.\n" "Аудит подтверждений реализации.\n" "Согласованная перепроверка.\n" - "agent-lifecycle adapter task start.\n" + "agent-lifecycle start.\n" "agent-lifecycle import plan.\n" "review-mesh recommend.\n" "review-mesh prepare.\n" @@ -781,6 +787,8 @@ def _write_min_docs(root: Path, *, unsupported_verified_row: bool) -> None: "adapter session start.\n" "adapter session resume.\n" "adapter run.\n" + "agent-lifecycle start.\n" + "`agent-lifecycle-start-receipt.v1`.\n" "`WRAPPER_ONLY`.\n" "shell: false.\n" "adapter-generic-launch-disabled.\n" @@ -795,6 +803,8 @@ def _write_min_docs(root: Path, *, unsupported_verified_row: bool) -> None: "adapter session start.\n" "adapter session resume.\n" "adapter run.\n" + "agent-lifecycle start.\n" + "`agent-lifecycle-start-receipt.v1`.\n" "`WRAPPER_ONLY`.\n" "shell: false.\n" "adapter-generic-launch-disabled.\n" diff --git a/tools/release/validate_docs_compat.py b/tools/release/validate_docs_compat.py index 6517ace6..aa10faad 100644 --- a/tools/release/validate_docs_compat.py +++ b/tools/release/validate_docs_compat.py @@ -25,6 +25,7 @@ "docs/reference/public-contracts.md", "https://pypi.org/project/agent-lifecycle-kit/", "Python 3.11-3.14", + "agent-lifecycle start --adapter codex", ), ), ( @@ -41,6 +42,7 @@ "reference/public-contracts.md", "https://pypi.org/project/agent-lifecycle-kit/", "Python 3.11-3.14", + "agent-lifecycle start --adapter codex", ), ), ( @@ -560,6 +562,8 @@ "docs/guides/lifecycle-cookbook.md", "adapter session start/status/resume/promote", "adapter run", + "agent-lifecycle start --adapter ", + "`agent-lifecycle-start-receipt.v1`", "`WAITING_FOR_TASK`", "`agent-adapter-session-receipt.v1`", "https://pypi.org/project/agent-lifecycle-kit/", @@ -574,6 +578,8 @@ "docs/ru/lifecycle-cookbook.md", "adapter session start/status/resume/promote", "adapter run", + "agent-lifecycle start --adapter ", + "`agent-lifecycle-start-receipt.v1`", "`WAITING_FOR_TASK`", "`agent-adapter-session-receipt.v1`", "https://pypi.org/project/agent-lifecycle-kit/", @@ -588,7 +594,7 @@ "Review code changes", "Audit implementation evidence", "Coordinate cross-review", - "agent-lifecycle adapter task start", + "agent-lifecycle start", "agent-lifecycle import plan", "review-mesh recommend", ), @@ -601,7 +607,7 @@ "Проверка изменений кода", "Аудит подтверждений реализации", "Согласованная перепроверка", - "agent-lifecycle adapter task start", + "agent-lifecycle start", "agent-lifecycle import plan", "review-mesh recommend", ), @@ -687,6 +693,8 @@ "adapter session start", "adapter session resume", "adapter run", + "agent-lifecycle start", + "`agent-lifecycle-start-receipt.v1`", "`WRAPPER_ONLY`", "shell: false", "adapter-generic-launch-disabled", @@ -739,6 +747,8 @@ "adapter session start", "adapter session resume", "adapter run", + "agent-lifecycle start", + "`agent-lifecycle-start-receipt.v1`", "`WRAPPER_ONLY`", "shell: false", "adapter-generic-launch-disabled", diff --git a/uv.lock b/uv.lock index 25ffc016..fbf7f869 100644 --- a/uv.lock +++ b/uv.lock @@ -4,5 +4,5 @@ requires-python = ">=3.11, <3.15" [[package]] name = "agent-lifecycle-kit" -version = "1.51.0" +version = "1.52.0" source = { editable = "." }