diff --git a/.agents/NOW.md b/.agents/NOW.md index b23e75c6a..819d60864 100644 --- a/.agents/NOW.md +++ b/.agents/NOW.md @@ -63,11 +63,11 @@ both gate models, reproduced 2–3x on an idle box. See the `parity-ledger.md` LINE anchors (re-anchor by ROW ID). **Operator/helper protocol** -([spec](workflow.md)): roles DECLARED then MATERIALIZED -into a lock or worktree+PR; operator merges PRs first and does features only via -sub-agents; helpers use worktrees on `row/` and open a DRAFT PR at the -START, which IS the claim. **W0-W5 LANDED**; role discipline ENFORCING, -`--require-role` is the DEFAULT. Queue: 10 rows; backfill 79 rows, 30 anchored. +([spec](workflow.md)): roles are a lock or worktree+PR; operator delegates, +helpers claim `row/` with a start-time DRAFT PR. **W0-W5 LANDED**; +role/entrypoint gates ENFORCE `agent-start.py` → claim → preflight. Review FAIL +loops through fresh implementer, both gates, and fresh review until PASS; budgets +cannot stop it. Queue: 10 rows; backfill 79 rows, 30 anchored. **Upstream inventory** ([spec](specs/upstream-derived-inventory-2026-08-05.md), drift-gated, arch parity BOTH ways): SM060/061/070 below vLLM's floor = OUT-OF-SCOPE; COMP-*/DISTRIBUTED-* are REAL unported work; **all 362 archs have diff --git a/.agents/policy-cutover b/.agents/policy-cutover index 72058001c..40f087d32 100644 --- a/.agents/policy-cutover +++ b/.agents/policy-cutover @@ -1 +1 @@ -031410e8f01be61ab57aa228550156a365292351 +9432a7adff82a4a46c870a5d5f27e07efa2648e6 diff --git a/.agents/policy.csv b/.agents/policy.csv index 87ffaefd7..ab222839e 100644 --- a/.agents/policy.csv +++ b/.agents/policy.csv @@ -1,6 +1,7 @@ rule_id,scope,trigger,requirement,enforcement,waiver_class,procedure POL-AUTH-REGISTRY,repository,any policy decision,Use policy.csv as the sole repository-policy authority.,scripts/check-policy.py,never,.agents/workflow.md POL-AUTH-PRECEDENCE,repository,repository instructions conflict,Apply AGENTS then the selected policy rule then its procedure then task evidence.,scripts/check-policy.py,never,.agents/workflow.md +POL-BOOT-ENTRYPOINT,session,at session start,Run agent-start before role declaration preflight or task state.,scripts/check-protocol-consistency.py; scripts/check-policy.py,never,.agents/workflow.md POL-BOOT-ROLE,session,before task work,Resolve or declare the worktree role before reading task state or editing.,scripts/check-role-discipline.py; scripts/check-policy.py,never,.agents/workflow.md POL-BOOT-NOW,session,after role resolution,Read NOW.md before deeper task records.,scripts/check-now-current.py; scripts/check-policy.py,never,.agents/workflow.md POL-BOOT-TASK,session,after NOW,Read only the claimed task and its linked procedures and evidence.,scripts/check-policy.py,expiring,.agents/workflow.md @@ -15,7 +16,7 @@ POL-REMOTE-UNKNOWN,remote gate,remote state cannot be queried,Report REMOTE_UNVE POL-OPERATOR-BOUNDARY,operator,feature work,Delegate implementation and keep main integration and GPU ownership in the operator role.,scripts/check-role-discipline.py; scripts/check-policy.py,expiring,.agents/workflow.md POL-OPERATOR-VERIFY,operator,implementation returns,Run the claimed gate instead of trusting an implementer report.,scripts/check-protocol-consistency.py; scripts/check-policy.py,never,.agents/workflow.md POL-REVIEW-FRESH,review,implementation is ready for review,Use a fresh reviewer that performs static review and targeted scratch mutation.,scripts/check-protocol-consistency.py; scripts/check-policy.py,never,.agents/workflow.md -POL-REVIEW-NO-REPAIR,review,a finding is found,Return findings to a fresh implementer and do not repair them in the coordinating session.,scripts/check-protocol-consistency.py; scripts/check-policy.py,never,.agents/workflow.md +POL-REVIEW-NO-REPAIR,review,a finding is found,Return actionable in-scope findings to a fresh implementer without repair in the coordinating session and repeat focused and full gates plus fresh scoped review until PASS; attempt budgets never terminate correctable findings and only explicit developer direction or a precise external authority or resource blocker may stop the loop.,scripts/check-protocol-consistency.py; scripts/check-policy.py,never,.agents/workflow.md POL-PR-DISPOSITION,operator,a PR is verified or obsolete,Merge a verified PR in-session or close an obsolete PR with the reason recorded.,scripts/check-policy.py,expiring,.agents/workflow.md POL-MIRROR-VLLM,feature port,vLLM defines behavior,Mirror every applicable vLLM mode instead of inventing product behavior.,scripts/check-agent-record.py; scripts/check-policy.py,never,.agents/porting.md POL-GROUND-CHAIN,parity investigation,comparing implementation,Ground conclusions in vLLM and the complete runtime dependency and generated-kernel chain.,scripts/check-policy.py,never,.agents/verification.md diff --git a/.agents/prompts/operator.md b/.agents/prompts/operator.md index eab58fb11..92b029188 100644 --- a/.agents/prompts/operator.md +++ b/.agents/prompts/operator.md @@ -14,6 +14,7 @@ policy-rules: POL-PROMPT-ENVELOPE POL-PROMPT-BOUNDARIES POL-OPERATOR-BOUNDARY PO ## Method - `OP-DELEGATE` | required | Delegate implementation and repairs to fresh implementers. +- `OP-CONTINUE` | required | For every actionable in-scope reviewer FAIL dispatch a fresh implementer, run focused and full gates, and dispatch a fresh scoped reviewer; repeat until PASS because attempt or retry budgets are scheduling controls and never terminal blockers. - `OP-VERIFY` | required | Run claimed verification on the returned commit without trusting the implementer report. - `OP-REVIEW` | required | Dispatch a fresh reviewer for independent static review and targeted scratch mutation. - `OP-DISPOSITION` | required | Merge a verified PR in-session or close an obsolete PR with its recorded reason. @@ -28,6 +29,7 @@ policy-rules: POL-PROMPT-ENVELOPE POL-PROMPT-BOUNDARIES POL-OPERATOR-BOUNDARY PO - remaining_concern: EVIDENCE | NONE ## Stop conditions -- `STOP-AUTHORITY` | BLOCKED | A required action exceeds Authority. +- `STOP-AUTHORITY` | BLOCKED | A required external action exceeds Authority and the precise missing authority is named. +- `STOP-RESOURCE` | BLOCKED | A required external resource is unavailable and the precise resource is named. +- `STOP-DEVELOPER` | BLOCKED | The developer explicitly directs the review loop to stop before PASS. - `STOP-REMOTE` | REMOTE_UNVERIFIED | Required remote state cannot be queried. -- `STOP-BLOCKER` | BLOCKED | Non-terminal work cannot name its blocker on the PR. diff --git a/.agents/specs/review-failure-continuation.md b/.agents/specs/review-failure-continuation.md new file mode 100644 index 000000000..b70230901 --- /dev/null +++ b/.agents/specs/review-failure-continuation.md @@ -0,0 +1,87 @@ +# Review-failure continuation + +User-approved 2026-08-08. Governance task: strengthen the existing +`POL-REVIEW-NO-REPAIR` contract so an actionable review failure cannot be +converted into a terminal result by an orchestration retry budget. + +## Verified gap + +The policy registry and workflow already require reviewer findings to return +to a fresh implementer, followed by focused and full gates and a fresh scoped +reviewer. They do not state that this cycle continues until `PASS`, and the +operator prompt still permits a generic `BLOCKED` result. A generic attempt or +retry budget can therefore stop work while an actionable, in-scope finding is +still correctable. + +The branch also inherits a cutover-record defect from the policy-history +squash. `.agents/policy-cutover` names commit +`00927ed611f4c5b720ceb158f6174be1e5470b03`, which exists locally but is not an +ancestor of the current head. The fail-closed trailer checker correctly rejects +that unreachable anchor. This spec commit is intentionally strict and becomes +the replacement reachable cutover anchor; a following strict commit updates +only the marker. + +## Binding design + +Strengthen `POL-REVIEW-NO-REPAIR` rather than introduce an overlapping rule. +Every actionable, in-scope reviewer `FAIL` starts this cycle: + +1. send the bounded finding and evidence to a fresh implementer; +2. rerun the focused and full gates on the resulting immutable head; and +3. send that head to a fresh scoped reviewer. + +Repeat the cycle until the reviewer returns `PASS`. Attempt and retry budgets +are scheduling controls, never terminal blockers for correctable findings. +The cycle stops short of `PASS` only on explicit developer direction or a +precise external authority or resource blocker. The coordinating/operator +session continues to coordinate and independently verify; it does not repair +the finding itself. + +## Affected surfaces + +- `.agents/policy.csv`: strengthen the authoritative + `POL-REVIEW-NO-REPAIR` requirement. +- `.agents/workflow.md`: bind the exact continuation procedure. +- `AGENTS.md`: refresh the generated compact T0 projection. +- `.agents/prompts/operator.md`: close the operator output/stop grammar so a + correctable finding cannot be reported as terminal `BLOCKED`. +- `scripts/check-protocol-consistency.py` and + `tests/scripts/test_check_protocol_consistency.py`: require and mutation-test + the continuation semantics. +- `tests/scripts/test_policy_contract.py` and + `tests/scripts/test_check_prompt_contract.py`: cover the synchronized policy + projection and closed prompt contract where applicable. +- `.agents/state.md`, `.agents/NOW.md`, `docs/STATUS.md`, and + `docs/BENCHMARKS.md`: record the governance checkpoint without changing a + feature/model/backend/quantization surface. +- `.agents/policy-cutover`: replace the unreachable SHA with this spec commit's + full SHA in a separate strict commit, without weakening any checker. + +## Verification and evidence + +The cutover repair must prove the replacement anchor is reachable and all +commits at or after it use strict Git-parsed trailers: + +```sh +cutover=$(tr -d '\n' < .agents/policy-cutover) +git merge-base --is-ancestor "$cutover" HEAD +python3 scripts/check-commit-trailers.py \ + --range origin/main..HEAD --cutover "$cutover" +``` + +The policy change uses red-before mutations for the continuation phrases and +the operator prompt row, then runs: + +```sh +python3 scripts/check-policy.py +python3 scripts/check-prompt-contract.py +python3 scripts/check-protocol-consistency.py +python3 -m unittest \ + tests.scripts.test_policy_contract \ + tests.scripts.test_check_prompt_contract \ + tests.scripts.test_check_protocol_consistency +scripts/agent-preflight.sh +``` + +No checker is relaxed, no attempt budget can terminate an actionable review +loop, and no remote operation is part of this task. diff --git a/.agents/specs/session-onboarding.md b/.agents/specs/session-onboarding.md index c299a863a..f69148842 100644 --- a/.agents/specs/session-onboarding.md +++ b/.agents/specs/session-onboarding.md @@ -1,5 +1,13 @@ # Session onboarding — ask, don't assume + +Every session first runs `scripts/agent-start.py`. Explicit work supplies +`--intent operator|helper|read-only` and a known helper row, suppressing the +first-time welcome. Follow the printed claim instruction, rerun the entrypoint +from materialized state, and then run `scripts/agent-preflight.sh`. The router +is non-interactive and owns no role, lock, worktree, environment value, or gate. + + User-directed 2026-08-06. Status: **IMPLEMENTED 2026-08-06**, except the one piece § Enforcement marks DEFERRED (refusal on write paths other than `preflight --staged`). This document is the contract; § Work breakdown records diff --git a/.agents/state.md b/.agents/state.md index a7ae2d214..ea702f8b4 100644 --- a/.agents/state.md +++ b/.agents/state.md @@ -42614,6 +42614,25 @@ tier manifests, forced-tier mutation tests, and execution on feature-poor and feature-rich hosts/emulation are release gates. W1 is now the CUDA per-source gencode prerequisite; multi-SM AOT and CPU ISA audits precede bundle work. +## 2026-08-08 — Universal, source-owned agent session entrypoint (`SESSION-ONBOARDING-UX`) + + +`scripts/agent-start.py` is now the mandatory, non-interactive first command for +every session. It consumes `agent-onboard.py`'s real probe and renders stable +welcome/action blocks; only an undeclared session without explicit intent gets +the compact ASCII welcome. Declared and explicit-intent routes suppress it, +print exact non-mutating next actions, respect operator lock conflicts, helper +row/queue state, declared mode, and environment status without values. + +The onboarding probe now projects `branch` and the real per-worktree git-dir +identity from `agent-role.py`; real CLI/worktree tests prevent renderer fixtures +from masking a missing projection. Preflight keeps all flags and role gates but +points undeclared sessions to the canonical entrypoint. The post-cutover policy +registry, generated bootstrap, procedure, and protocol-consistency mutations +bind start-before-preflight without copying banner text into Markdown. This +governance checkpoint changes no engine, kernel, model, correctness, or +performance path; the benchmark disposition is NOT APPLICABLE. + ## 2026-08-08 — ARCH-ONE-SURFACE ROW 2: MiniMax-H3 video+audio generation folded onto the ONE surface (PR #123) @@ -42982,8 +43001,6 @@ removing only `; #129: SPIKE∅`; the compact clause consumes the existing 279150-character ratchet exactly. `ENG-RELEASE-BINARIES` remains `SPIKE`: there is no archive, runtime, correctness or performance evidence. - - ## 2026-08-08 — Tensor-parallelism end-to-end spike lands at the current pin (task #287) @@ -43053,7 +43070,6 @@ F79-4 remain open on the landed tree; the review's merge-and-fix map is the binding description. Pi concurrency, BF16 GEMM/speed closure (W6) stay open as the lane's own next steps. - ## 2026-08-08 — ROCm approach-(b): unified memory true by construction on integrated APUs @@ -43247,7 +43263,6 @@ Row moved INVENTORIED→READY (roadmap_v1 + feature-matrix §2). M1-M4 are the i rows this unblocks; M1/M2/M4-CPU are CPU-completable, M3 (profile run) is dgx-gated. Records-only spike (no code). - ## 2026-08-08 — ROAD-V1-MEM M1+M2 LANDED: absolute --kv-cache-memory knob + group-aware bytes-per-block (CPU brick) @@ -43319,4 +43334,22 @@ Gates: `check-device-leakage` RED→GREEN (`kcuda=0`, DSR 32 == baseline 32); -Werror CPU build. SEPARATE pre-existing main breakage recorded, not fixed here: `check-doc-checkpoint` is RED on `1a021b1b` itself (that policy commit touched 70+ `.agents/` files without docs/BENCHMARKS.md + docs/FEATURES.md); it validates -HEAD, so it self-heals as compliant commits land. \ No newline at end of file +HEAD, so it self-heals as compliant commits land. + +## 2026-08-09 — Review failures are an enforced until-PASS continuation loop + + +Governance task `review-failure-continuation` closes the gap found by the fresh +review of the universal session entrypoint. `POL-REVIEW-NO-REPAIR` now requires +every actionable in-scope `FAIL` to return to a fresh implementer, run focused +and full gates, and reach a fresh scoped reviewer; that cycle repeats until +`PASS`. Attempt and retry budgets are scheduling controls, never terminal +blockers for a correctable finding. Only explicit developer direction or a +precise external authority/resource blocker may stop the cycle short of +`PASS`, and the coordinating/operator session still never repairs findings. + +The closed operator grammar adds `OP-CONTINUE` and removes the generic +`STOP-BLOCKER` escape in favor of exact authority, resource, developer, and +remote-state stops. Policy, T0, workflow, prompt, prompt checker, protocol +checker, and mutation suites move together; no product or benchmark code +changes, so the public benchmark result remains NOT APPLICABLE. diff --git a/.agents/workflow.md b/.agents/workflow.md index 04876bd38..feba71061 100644 --- a/.agents/workflow.md +++ b/.agents/workflow.md @@ -11,8 +11,12 @@ Each controlled paragraph names exactly the rule it implements. [POL-AUTH-PRECEDENCE] Resolve conflicts in this order: repository `AGENTS.md`, the applicable registry row, that row's procedure, then task evidence. Report an unresolved contradiction before changing state. + +[POL-BOOT-ENTRYPOINT] Start every session with `scripts/agent-start.py`. Supply `--intent operator|helper|read-only` and a known helper `--row` when the opening request is explicit; otherwise relay its source-owned welcome verbatim and ask what work the contributor intends. Follow its printed claim action, rerun it from materialized state, and only then run `scripts/agent-preflight.sh`. The command is non-interactive and never claims, locks, creates a worktree, or mutates a gate. + + -[POL-BOOT-ROLE] Begin with `scripts/agent-role.py show`. If no valid role exists, ask what work the developer intends and run `scripts/agent-role.py claim operator` for a multi-step integration campaign, `scripts/agent-role.py claim helper --row ` for one scoped task, or `scripts/agent-role.py claim read-only` for inspection. Add `--headless` only when the developer explicitly declares an unattended run; never infer it. +[POL-BOOT-ROLE] Use the entrypoint's reported worktree role. If none exists, ask what work the developer intends and run `scripts/agent-role.py claim operator` for a multi-step integration campaign, `scripts/agent-role.py claim helper --row ` for one scoped task, or `scripts/agent-role.py claim read-only` for inspection. Add `--headless` only when the developer explicitly declares an unattended run; never infer it. [POL-BOOT-NOW] After role resolution, read `.agents/NOW.md` as the one-read live snapshot; consult the append-only state tail only when the task needs deeper history. @@ -92,7 +96,7 @@ gates. [POL-REVIEW-FRESH] After focused and full gates pass on an immutable head from a fresh [implementer](prompts/implementer.md), dispatch a fresh [reviewer](prompts/reviewer.md), never the agent that wrote the code, to perform both static inspection and targeted scratch mutations of the claimed guarantees—mutate, not read. Review output identifies commands, mutations, findings, and the reviewed SHA. -[POL-REVIEW-NO-REPAIR] A coordinating/operator session never repairs a reviewer finding: never fix findings yourself. Send the bounded finding and evidence to a fresh implementer, rerun focused and full gates, then use a fresh scoped reviewer. +[POL-REVIEW-NO-REPAIR] Never fix findings yourself in a coordinating session. Each actionable in-scope reviewer FAIL and its evidence return to a fresh implementer for focused and full gates then a fresh scoped reviewer. Repeat this cycle until PASS. Attempt and retry budgets are scheduling controls and never terminal blockers for correctable findings. Stop only on explicit developer direction or a precise external authority or resource blocker. [POL-OPERATOR-VERIFY] The operator independently checks the immutable head: run the row's gate yourself. Implementer or reviewer summaries are evidence inputs, not gate results. diff --git a/AGENTS.md b/AGENTS.md index 3f4bdcf59..c8e1bd75e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ and evidence. If prose conflicts with the registry, the registry wins. ## Boot order -1. Resolve the worktree role with `python3 scripts/agent-role.py show`; claim the developer-selected role only when no valid role exists. +1. Run `scripts/agent-start.py`; pass explicit intent when known, follow its role action, and rerun it after declaration. 2. Resolve `.env` and `.agents/developer-preferences.md` from the shared checkout, requesting only values required by the current task. 3. Read `.agents/NOW.md` for the live snapshot. 4. Read `.agents/policy.csv`, then the procedure named by each applicable rule. @@ -28,6 +28,7 @@ byte-for-byte. Do not edit it independently. - `POL-AUTH-REGISTRY` — Use policy.csv as the sole repository-policy authority. +- `POL-BOOT-ENTRYPOINT` — Run agent-start before role declaration preflight or task state. - `POL-ROLE-DECLARED` — Declare operator helper or read-only and keep the role state consistent with the worktree. - `POL-SPIKE-FIRST` — Commit a complete spike spec before a row enters READY or ACTIVE. - `POL-MIRROR-VLLM` — Mirror every applicable vLLM mode instead of inventing product behavior. @@ -35,7 +36,7 @@ byte-for-byte. Do not edit it independently. - `POL-CORRECTNESS-GATE` — Pass the declared token-exact or ratified distributional correctness gate before performance acceptance. - `POL-PREFLIGHT` — Run the applicable preflight and prevent a failed gate from being followed by a push. - `POL-REVIEW-FRESH` — Use a fresh reviewer that performs static review and targeted scratch mutation. -- `POL-REVIEW-NO-REPAIR` — Return findings to a fresh implementer and do not repair them in the coordinating session. +- `POL-REVIEW-NO-REPAIR` — Return actionable in-scope findings to a fresh implementer without repair in the coordinating session and repeat focused and full gates plus fresh scoped review until PASS; attempt budgets never terminate correctable findings and only explicit developer direction or a precise external authority or resource blocker may stop the loop. - `POL-OPERATOR-VERIFY` — Run the claimed gate instead of trusting an implementer report. - `POL-ONE-SURFACE` — Expose every shipped capability through include/vllm.h and keep examples as thin clients of the same library surface. - `POL-EVIDENCE-PRESERVE` — Move evidence without deleting it. @@ -92,6 +93,7 @@ combination. Historical policy and evidence are archived under ## Essential commands ```sh +scripts/agent-start.py python3 scripts/agent-role.py show scripts/agent-preflight.sh scripts/agent-preflight.sh --staged diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index d16579665..085d118a9 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -293,11 +293,12 @@ Correctness re-validated bit-identical across the advance, zero golden drift. discarded. Workload equivalence between arms is audited, not assumed: batch cap, token budget, context, corpus bytes, KV and SSM dtypes, kernel family, and graphed decode all match, and the audit is -[recorded](../.agents/specs/benchmark-equivalence-audit-2026-07-15.md). The 2026-08-04/06 records work (agent-record substrate, triage, -compaction, CI concurrency, anchor backfill, the operator/helper protocol W0-W5 -with role discipline now enforcing, the upstream/device inventory, and session -onboarding through probe 5/5) touched -no engine code and moved no number: NOT APPLICABLE, nothing to reproduce. +[recorded](../.agents/specs/benchmark-equivalence-audit-2026-07-15.md). The +2026-08-04/08 governance checkpoints (record/CI substrate, anchor backfill, +operator/helper W0-W5, upstream/device inventory, onboarding probe, the +review-hardened `agent-start.py` entrypoint, and CI-bound review-until-PASS +policy) touched no engine code and moved no number: **NOT APPLICABLE**, nothing +to reproduce. The PR #28 sanitizer repair is also NOT APPLICABLE to performance: both full 333-test CPU detector lanes pass after merging upstream `main`, while the diff --git a/docs/STATUS.md b/docs/STATUS.md index 7c15c6315..6fbbd7503 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -36,8 +36,9 @@ Startup-latency axis (2026-08-07): `MEASURED / provisional`. Cold launch to firs 6 legs contended, uncontended repeat died with a host reboot. [Detail](../.agents/specs/startup-latency-axis.md). -Protocol (2026-08-07): PR disposition — verified-good PRs MERGE in-session, superseded -CLOSE with a reason; prompt pair tracked, 25 gate rows exact-pinned. +Protocol (2026-08-08): `agent-start.py` → claim → preflight; review FAIL → +fresh implementer → both gates → fresh review until PASS. Budgets cannot stop +it; registry/prompt mutation gates bind it. Verified PRs merge; obsolete close. Supported-model registry guard (2026-08-06): the public per-architecture list in [FEATURES](FEATURES.md) is CI-bound to the C++ registry by diff --git a/docs/USAGE.md b/docs/USAGE.md index 509ab4bf7..32c3240f3 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -6,6 +6,15 @@ the reference behind it. Per-capability lifecycle state is [docs/STATUS.md](STATUS.md); measured numbers are [docs/BENCHMARKS.md](BENCHMARKS.md). +## Starting an agent-assisted contribution + +Run `scripts/agent-start.py` first. It reports an inherited worktree role or, +for a new contributor with no declared role or explicit intent, prints the +welcome that the agent should relay. An explicit request can use +`--intent operator|helper|read-only` and a helper `--row ID`. Follow its printed +claim action, rerun it after declaration, then run `scripts/agent-preflight.sh`. +The entrypoint is non-interactive and does not mutate the checkout. + ## Running inference (CLI) `vllm-cli` runs a one-shot completion through the C ABI. Source: @@ -435,4 +444,3 @@ Useful for measurement: `--denoise-only` times the DiT loop without loading the Served over HTTP too: pass `--video-dit` (plus the VAEs and configs) to `examples/server` and `POST /v1/videos`, `POST /v1/videos/sync` and `GET /v1/videos/{id}` register. Without it the routes stay unregistered. - diff --git a/docs/superpowers/specs/2026-08-08-agent-start-entrypoint-design.md b/docs/superpowers/specs/2026-08-08-agent-start-entrypoint-design.md new file mode 100644 index 000000000..6653d5661 --- /dev/null +++ b/docs/superpowers/specs/2026-08-08-agent-start-entrypoint-design.md @@ -0,0 +1,241 @@ +# Agent session entrypoint design + +**Date:** 2026-08-08 +**Status:** Approved for implementation +**Scope:** Contributor onboarding and session routing only + +## Problem + +The repository already has the right low-level pieces for session onboarding: +`agent-role.py` declares roles, `agent-onboard.py` probes state, and +`agent-preflight.sh` runs the gates. The first minute still feels procedural. +An agent must assemble those pieces from Markdown, a new contributor meets role +vocabulary before learning why it exists, and the undeclared-role failure +duplicates instructions instead of presenting one intentional entrypoint. + +The contributor experience should be welcoming without permanently spending +context on decoration. A first-time contributor should see a compact project +welcome and choose the shape of the work. An experienced contributor who has +already stated the intended role should go directly to the exact next command. +Every session, including a session whose worktree already carries a role, +should receive one state-aware route into preflight and the resume record. + +## Goals + +- Establish one mandatory first command for every agent session. +- Show a compact ASCII welcome only when the worktree is undeclared and the + user has not already made their intent explicit. +- Explain helper, operator, and read-only in terms of contributor intent. +- Keep the welcome out of `AGENTS.md`, workflow prose, and other documents + loaded into the agent's immediate context. +- Give the agent exact, state-aware next commands without performing hidden + writes or role decisions. +- Preserve the existing ownership boundaries between role declaration, state + probing, and preflight. + +## Non-goals + +- Replacing `agent-role.py`, `agent-onboard.py`, or `agent-preflight.sh`. +- Adding an interactive stdin prompt. +- Guessing a role, row, environment value, or headless mode. +- Changing helper worktree, pull-request, operator-lock, or gate semantics. +- Adding a harness-specific startup hook. + +## Approaches considered + +### 1. A state-aware standalone entrypoint — selected + +Add `scripts/agent-start.py`. It consumes the existing onboarding probe, +renders the welcome when appropriate, and prints exact next actions. The agent +relays a delimited welcome block verbatim when directed. + +This keeps presentation in executable source, produces consistent behavior +across agent harnesses, and leaves every state transition with its current +owner. + +### 2. Treat terminal or tool output as the welcome + +This avoids the relay instruction, but tool output is collapsed, styled, or +hidden differently by each harness. A contributor may never see it as the +agent's welcome. + +### 3. Put the banner in agent Markdown + +This makes the banner easy to reproduce but loads decoration into context on +every session, including sessions that do not need onboarding. It also invites +the prose and executable behavior to drift. + +## Command contract + +The canonical first command is: + +```console +scripts/agent-start.py [--intent operator|helper|read-only] [--row ROW-ID] [--headless] +``` + +The agent runs it before preflight on every session. An explicit intent is +passed only when the user's opening request already makes the work shape clear. +`--headless` is passed only when the user explicitly declared an unattended +run. The command never infers either value. + +`--row` is valid only with `--intent helper`; supplying it for operator or +read-only is an argument error. `--headless` without an explicit intent is also +an argument error because there is no role claim to receive the mode. When a +declared worktree is inspected, mode comes exclusively from its materialized +marker rather than from command-line overrides. + +The entrypoint is non-interactive and instruction-only. It does not claim a +role, create a worktree, acquire a lock, edit `.env`, or run preflight. A +successful routing result exits zero even when more action is required. +Invalid arguments and failures that prevent truthful routing exit nonzero. + +## State machine + +### Declared worktree + +The banner is suppressed. The output reports the inherited role, row when +applicable, mode, and worktree. If an explicit intent conflicts with the +inherited declaration, the entrypoint calls out the mismatch and gives the +exact re-declaration or escalation instruction; it never silently changes the +role. + +With no conflict, the next-action block instructs the agent to: + +1. confirm the inherited role fits the current request; +2. run `scripts/agent-preflight.sh`; +3. use the `NOW.md` surface printed by preflight; +4. read developer preferences when present; and +5. resume from the declared row's coordination and state anchors. + +### Undeclared worktree with explicit intent + +The banner is suppressed. The entrypoint emits the exact claim command: + +- operator: `scripts/agent-role.py claim operator`; +- read-only: `scripts/agent-role.py claim read-only`; +- helper with a row: `scripts/agent-role.py claim helper --row ROW-ID`; +- helper without a row: identify or create the scoped row before claiming; the + READY helper queue may be shown, but no row is selected automatically. + +The action block then instructs the agent to rerun `agent-start.py` so routing +is re-derived from materialized state, followed by preflight. + +An operator lock held by another worktree is reported as a blocker rather than +converted into a different role. The agent reports it and obtains direction. + +### Undeclared worktree without explicit intent + +The entrypoint emits the selected compact-frame welcome. The source constant, +not a Markdown copy, is the canonical rendering. It is ASCII-only, contains no +ANSI color, fits within 72 columns, and gives the three paths this wording: + +- **Helper:** contribute one focused task that helps move the project forward. +- **Operator:** maintain the project or coordinate a long, multi-agent + campaign. +- **Looking:** read, review, or ask questions without claiming project work. + +The welcome is wrapped in stable `WELCOME: RELAY VERBATIM` delimiters. The +following action block instructs the agent to relay only that block verbatim, +ask what the contributor is here to do, invoke the matching role claimer, +rerun `agent-start.py`, and then run preflight. + +## Output contract + +Human-readable output has stable sections: + +```text +--- WELCOME: RELAY VERBATIM --- # present only for first-time routing + +--- END WELCOME --- + +--- AGENT NEXT ACTIONS --- # always present + +--- END ACTIONS --- +``` + +The action block is written for the agent, while the welcome block is written +for the contributor. The separation prevents an agent from relaying internal +commands or paraphrasing the visual welcome. + +Only status is printed for environment configuration. Values, paths, tokens, +and other machine-specific content are never echoed. Missing environment +values retain the existing just-in-time policy: they do not block startup and +are asked only when a later gate needs them. + +## Component boundaries + +| Component | Responsibility after this change | +|---|---| +| `scripts/agent-start.py` | First-command presentation and state-aware next-action routing | +| `scripts/agent-onboard.py` | Read-only role/environment/helper-queue state probe | +| `scripts/agent-role.py` | Role marker, helper row declaration, mode, and operator lock | +| `scripts/agent-preflight.sh` | Repository correctness gates and undeclared-role backstop | + +`agent-start.py` consumes the probe's computation rather than reparsing record +files or duplicating role resolution. Preflight's undeclared-role failure stops +duplicating the interview and points to the canonical entrypoint. + +## Failure handling + +- An unreadable environment file or unavailable helper queue is reported + honestly and does not become an invented empty state. +- A missing helper row produces a specific next action rather than a malformed + claim command. +- A held operator lock reports the holder conflict and does not recommend an + unauthorized fallback. +- A declared-role/explicit-intent mismatch is visible and never mutates state. +- Headless mode is propagated into the exact claim command only when explicitly + supplied. +- Unexpected probe failures produce a concise error and a nonzero exit. + +## Documentation and enforcement + +`AGENTS.md` and `.agents/workflow.md` will name `agent-start.py` as the first +command of every session and describe the intent flag at a high level. They +will not contain the banner. The existing session-onboarding and +operator/helper protocol specs will be updated in the same change so prose and +behavior remain aligned. + +`scripts/check-protocol-consistency.py` will assert that the canonical +entrypoint and ordering agree across the binding protocol surfaces. It will +not duplicate the banner text. + +`agent-preflight.sh` remains the backstop: when a session is undeclared, it +fails with the exact instruction to run `scripts/agent-start.py`. CI and +scripted callers retain the existing explicit preflight escape behavior. + +## Test strategy + +Add `tests/scripts/test_agent_start.py` and register it with preflight. Tests +cover: + +- declared operator, helper, and read-only routing; +- declared interactive and headless modes; +- undeclared routing with no intent, each explicit intent, and helper with and + without a row; +- suppression of the welcome for every declared or explicit-intent path; +- the compact banner's ASCII-only and maximum-width guarantees; +- stable welcome/action delimiters and exact role-claim commands; +- inherited-role/explicit-intent conflicts; +- a live operator lock owned by another worktree; +- unavailable helper queue and unreadable/incomplete environment states; +- absence of environment values and secrets in rendered output; +- nonzero exits only for invalid input or an inability to route truthfully; +- preflight pointing to `agent-start.py`; and +- protocol-consistency mutations proving that stale entrypoint prose fails. + +## Acceptance criteria + +1. Every binding session-start surface directs agents to `agent-start.py` + before preflight. +2. A genuinely first-time, non-explicit session receives the compact welcome + and a verbatim-relay instruction. +3. Declared sessions and sessions with explicit intent never render the + welcome. +4. Every valid state receives exact, non-mutating next actions. +5. Existing role, lock, environment, helper queue, and preflight semantics are + unchanged. +6. The banner exists only in executable/test source, not in immediately loaded + agent Markdown; executable source remains the one canonical rendering and + tests assert properties or reference that constant rather than copying it. +7. The focused unit, mutation, protocol-consistency, and preflight suites pass. diff --git a/scripts/agent-onboard.py b/scripts/agent-onboard.py index db915bcdd..87f3bfcaa 100755 --- a/scripts/agent-onboard.py +++ b/scripts/agent-onboard.py @@ -132,6 +132,12 @@ def probe() -> dict: # operator` when that will fail. "blocked_by_other_operator": bool(state.get("operator_held_by_other")), "reason": state.get("reason"), + # The role tool owns both facts. Keep the branch from resolve() and + # expose the same per-worktree identity used by role markers/locks so + # downstream routers report real materialized state rather than a + # renderer fixture's invented fields. + "branch": state.get("branch"), + "worktree": role_mod.worktree_id(), # resolve() now carries this (step 2). Still read with .get and still # rendered as a DEFAULT when absent: headless is never inferred, so a # state that carries no mode must not read as a declaration either. @@ -155,6 +161,8 @@ def render_probe(state: dict) -> str: ) lines = [ f"role: {role}{row} mode: {mode}", + f"branch: {state.get('branch') or 'unavailable'} " + f"worktree: {state.get('worktree') or 'unavailable'}", f".env: {state['env']}" + (f" (unset: {', '.join(state['env_missing'])})" if state["env_missing"] else ""), queue_line, diff --git a/scripts/agent-preflight.sh b/scripts/agent-preflight.sh index 31a8bf481..297994d66 100755 --- a/scripts/agent-preflight.sh +++ b/scripts/agent-preflight.sh @@ -83,6 +83,7 @@ SUITES=( test_check_release_binary_contract test_agent_role test_agent_onboard + test_agent_start test_claim_view test_upstream_inventory test_doc_checkpoint @@ -121,11 +122,8 @@ if role_line=$(python3 scripts/agent-role.py show 2>&1); then printf ' \033[32mok\033[0m %s\n' "$role_line" else printf ' \033[33m--\033[0m %s\n' "$(printf '%s' "$role_line" | head -1)" - printf ' This session has not declared a role. Ask what the work is:\n' - printf ' a long or multi-step campaign -> scripts/agent-role.py claim operator\n' - printf ' one scoped change -> scripts/agent-role.py claim helper --row \n' - printf ' just reading or answering -> scripts/agent-role.py claim read-only\n' - printf ' Add --headless to an unattended run. See .agents/workflow.md.\n' + printf ' This session has not declared a role. Run scripts/agent-start.py\n' + printf ' for the canonical role interview and exact next actions.\n' if [ "$REQUIRE_ROLE" -eq 1 ]; then failed+=("role-undeclared") fi diff --git a/scripts/agent-start.py b/scripts/agent-start.py new file mode 100755 index 000000000..bee841028 --- /dev/null +++ b/scripts/agent-start.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Route every agent session into role declaration, preflight, and resume. + +This command is deliberately read-only and non-interactive. The onboarding +probe owns state discovery, agent-role.py owns declarations, and preflight owns +gates. This module only renders truthful next actions. +""" + +from __future__ import annotations + +import argparse +import importlib.util +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def _load_onboard(): + path = ROOT / "scripts/agent-onboard.py" + spec = importlib.util.spec_from_file_location("agent_onboard_for_start", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules["agent_onboard_for_start"] = module + spec.loader.exec_module(module) + return module + + +onboard = _load_onboard() + + +WELCOME = """+------------------------------------------------------------------+ +| Welcome to vllm.cpp | ++------------------------------------------------------------------+ +| Helper One focused contribution that moves the project forward.| +| Operator Maintain the project or run a long multi-agent campaign.| +| Looking Read, review, or ask questions without claiming work. | ++------------------------------------------------------------------+""" + +WELCOME_BEGIN = "--- WELCOME: RELAY VERBATIM ---" +WELCOME_END = "--- END WELCOME ---" +ACTIONS_BEGIN = "--- AGENT NEXT ACTIONS ---" +ACTIONS_END = "--- END ACTIONS ---" + + +def _status_lines(state: dict) -> list[str]: + """Render status labels only; never echo environment values or keys.""" + return [ + f"environment: {state.get('env') or 'unavailable'}", + f"branch: {state.get('branch') or 'unavailable'}", + f"worktree: {state.get('worktree') or 'unavailable'}", + ] + + +def _claim_command(intent: str, row: str | None, headless: bool) -> str: + parts = ["scripts/agent-role.py", "claim", intent] + if intent == "helper" and row: + parts.extend(("--row", row)) + if headless: + parts.append("--headless") + return " ".join(parts) + + +def _declared_actions( + state: dict, intent: str | None, headless: bool +) -> list[str]: + role = state["role"] + lines = [f"role: {role}"] + if state.get("row"): + lines.append(f"row: {state['row']}") + lines.extend( + [ + f"mode: {state.get('mode') or 'unavailable'}", + *_status_lines(state), + ] + ) + + if intent is not None and intent != role: + lines.extend( + [ + f"MISMATCH: intent {intent} conflicts with declared role {role}.", + "1. Stop and obtain direction before changing materialized state.", + "2. Resolve current lock/worktree ownership, then re-declare", + " through scripts/agent-role.py and rerun this entrypoint.", + "3. No worktree or PR was created by this command.", + ] + ) + return lines + + lines.extend( + [ + "1. Confirm this inherited role fits the current request.", + "2. Run scripts/agent-preflight.sh.", + "3. Use the printed .agents/NOW.md as the live snapshot.", + "4. Read .agents/developer-preferences.md when it exists.", + "5. Resume the row from .agents/coordination.md and", + " .agents/state.md anchors that apply to this claim.", + ] + ) + if headless: + lines.append("NOTE: declared marker mode wins; --headless changed nothing.") + return lines + + +def _helper_without_row(state: dict) -> list[str]: + lines = [ + "1. Identify or create one scoped row before claiming helper.", + ] + if state.get("queue_error"): + lines.append(f" READY queue unavailable: {state['queue_error']}") + elif state.get("queue"): + lines.append(" READY queue:") + lines.extend(f" - {item}" for item in state["queue"]) + else: + lines.append(" READY queue is empty; create and spike a scoped row.") + lines.extend( + [ + "2. Rerun scripts/agent-start.py with --intent helper and --row", + " set to the selected row's actual ID.", + "3. Run the printed claim command, then rerun agent-start.", + "4. Run scripts/agent-preflight.sh after the role is declared.", + ] + ) + return lines + + +def _undeclared_actions( + state: dict, intent: str | None, row: str | None, headless: bool +) -> list[str]: + lines = _status_lines(state) + + if intent is None: + if state.get("blocked_by_other_operator"): + reason = state.get("reason") or "reason unavailable" + lines.extend( + [ + "BLOCKED OPTION: the operator lock is held by another live worktree.", + f"Reason: {reason}", + "1. Relay only the welcome block above verbatim.", + "2. Then ask what the contributor is here to do.", + "3. If the contributor chooses operator, report the conflict;", + " do not run a known-failing claim or select another role.", + "4. For helper or read-only, use the matching claim command.", + "5. After claiming, rerun scripts/agent-start.py.", + "6. Then run scripts/agent-preflight.sh.", + ] + ) + return lines + lines.extend( + [ + "1. Relay only the welcome block above verbatim.", + "2. Then ask what the contributor is here to do.", + "3. Use the matching scripts/agent-role.py claim command.", + "4. After claiming, rerun scripts/agent-start.py.", + "5. Then run scripts/agent-preflight.sh.", + ] + ) + return lines + + if intent == "operator" and state.get("blocked_by_other_operator"): + reason = state.get("reason") or "reason unavailable" + lines.extend( + [ + "BLOCKED: the operator lock is held by another live worktree.", + f"Reason: {reason}", + "Do not run a known-failing claim or select another role.", + "Report the conflict and obtain direction.", + ] + ) + return lines + + if intent == "helper" and not row: + lines.extend(_helper_without_row(state)) + return lines + + command = _claim_command(intent, row, headless) + lines.extend( + [ + f"1. Run: {command}", + "2. After claiming, rerun scripts/agent-start.py.", + "3. Then run scripts/agent-preflight.sh.", + ] + ) + return lines + + +def render_route( + state: dict, intent: str | None, row: str | None, headless: bool +) -> str: + """Pure rendering of a probed state and caller-supplied intent.""" + sections: list[str] = [] + if state.get("role") is None and intent is None: + sections.append(f"{WELCOME_BEGIN}\n{WELCOME}\n{WELCOME_END}") + + if state.get("role") is not None: + actions = _declared_actions(state, intent, headless) + else: + actions = _undeclared_actions(state, intent, row, headless) + sections.append(f"{ACTIONS_BEGIN}\n" + "\n".join(actions) + f"\n{ACTIONS_END}") + return "\n\n".join(sections) + + +def _argument_error(message: str) -> int: + print(f"ERROR: {message}", file=sys.stderr) + return 2 + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Print the truthful next actions for this agent session." + ) + parser.add_argument("--intent", choices=("operator", "helper", "read-only")) + parser.add_argument("--row", metavar="ROW-ID") + parser.add_argument("--headless", action="store_true") + try: + args = parser.parse_args(argv) + except SystemExit as error: + return int(error.code) + + if args.row and args.intent != "helper": + return _argument_error("--row is valid only with --intent helper") + if args.headless and args.intent is None: + return _argument_error("--headless requires an explicit --intent") + + try: + state = onboard.probe() + rendered = render_route(state, args.intent, args.row, args.headless) + except Exception as error: + print(f"ERROR: unable to route session: {error}", file=sys.stderr) + return 1 + print(rendered) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check-prompt-contract.py b/scripts/check-prompt-contract.py index 5452744f3..b386b5bf9 100644 --- a/scripts/check-prompt-contract.py +++ b/scripts/check-prompt-contract.py @@ -69,6 +69,7 @@ ), "operator": ( "- `OP-DELEGATE` | required | Delegate implementation and repairs to fresh implementers.", + "- `OP-CONTINUE` | required | For every actionable in-scope reviewer FAIL dispatch a fresh implementer, run focused and full gates, and dispatch a fresh scoped reviewer; repeat until PASS because attempt or retry budgets are scheduling controls and never terminal blockers.", "- `OP-VERIFY` | required | Run claimed verification on the returned commit without trusting the implementer report.", "- `OP-REVIEW` | required | Dispatch a fresh reviewer for independent static review and targeted scratch mutation.", "- `OP-DISPOSITION` | required | Merge a verified PR in-session or close an obsolete PR with its recorded reason.", @@ -116,9 +117,10 @@ "- `STOP-EVIDENCE` | FAIL | Required source, diff, test, or gate evidence is unavailable.", ), "operator": ( - "- `STOP-AUTHORITY` | BLOCKED | A required action exceeds Authority.", + "- `STOP-AUTHORITY` | BLOCKED | A required external action exceeds Authority and the precise missing authority is named.", + "- `STOP-RESOURCE` | BLOCKED | A required external resource is unavailable and the precise resource is named.", + "- `STOP-DEVELOPER` | BLOCKED | The developer explicitly directs the review loop to stop before PASS.", "- `STOP-REMOTE` | REMOTE_UNVERIFIED | Required remote state cannot be queried.", - "- `STOP-BLOCKER` | BLOCKED | Non-terminal work cannot name its blocker on the PR.", ), } diff --git a/scripts/check-protocol-consistency.py b/scripts/check-protocol-consistency.py index accdafbc0..99d19167e 100644 --- a/scripts/check-protocol-consistency.py +++ b/scripts/check-protocol-consistency.py @@ -49,6 +49,18 @@ INTERVIEW_MARKER = "" INTERVIEW_REQUIRED = ("claim operator", "claim helper --row", "claim read-only", "--headless") +# The authoritative policy routes session start through one executable before +# role declaration and preflight. Welcome copy stays in source, not context. +ENTRYPOINT_DOCUMENT = ".agents/workflow.md" +ENTRYPOINT_MARKER = "" +ENTRYPOINT_END = "" +ENTRYPOINT_REQUIRED = ( + "POL-BOOT-ENTRYPOINT", + "scripts/agent-start.py", + "--intent", + "scripts/agent-preflight.sh", +) + # The same manual must carry the operator's LOOP. The prompts handed to # sub-agents cannot by themselves tell the operator how to run one, and # the three rules that carry the whole return are exactly the ones an operator @@ -70,6 +82,11 @@ "mutate, not read", "run the row's gate yourself", "never fix findings yourself", + "repeat this cycle until pass", + "attempt and retry budgets are scheduling controls", + "never terminal blockers for correctable findings", + "explicit developer direction", + "precise external authority or resource blocker", ) CUTOVER_WIRING = { @@ -89,7 +106,10 @@ "tests/scripts/test_agent_gates.py", ".agents/policy-cutover", ), - ".githooks/pre-push": ("check-policy.py", "check-prompt-contract.py"), + ".githooks/pre-push": ( + "CHECKERS=(check-policy.py", + "check-prompt-contract.py", + ), "scripts/agent-ready.py": ( "REMOTE_UNVERIFIED", "if not run_local_preflight():", @@ -166,6 +186,39 @@ def interview_errors(text: str) -> list[str]: ] +def entrypoint_block(text: str) -> str | None: + """Return the bounded universal-entrypoint procedure, when complete.""" + start = text.find(ENTRYPOINT_MARKER) + if start == -1: + return None + end = text.find(ENTRYPOINT_END, start) + if end == -1: + return None + return text[start + len(ENTRYPOINT_MARKER) : end] + + +def entrypoint_errors(text: str) -> list[str]: + """Require the canonical start command to precede preflight.""" + block = entrypoint_block(text) + if block is None: + return [ + f"{ENTRYPOINT_DOCUMENT} is missing the session-entrypoint block " + f"({ENTRYPOINT_MARKER} ... {ENTRYPOINT_END})" + ] + errors = [ + f"{ENTRYPOINT_DOCUMENT} session entrypoint omits {needle!r}" + for needle in ENTRYPOINT_REQUIRED + if needle not in block + ] + start = block.find("scripts/agent-start.py") + preflight = block.find("scripts/agent-preflight.sh") + if start != -1 and preflight != -1 and start > preflight: + errors.append( + f"{ENTRYPOINT_DOCUMENT} session entrypoint must route agent-start before preflight" + ) + return errors + + def loop_block(text: str) -> str | None: """Return the orchestration-loop block's body, or None if there isn't one. @@ -251,6 +304,12 @@ def main() -> int: interview_errors(interview.read_text(encoding="utf-8")) ) + entrypoint = ROOT / ENTRYPOINT_DOCUMENT + if not entrypoint.exists(): + failures.append(f"{ENTRYPOINT_DOCUMENT} does not exist") + else: + failures.extend(entrypoint_errors(entrypoint.read_text(encoding="utf-8"))) + # INTERVIEW_DOCUMENT and LOOP_DOCUMENT are the same manual today, but the # two obligations are independent and either may move, so each resolves its # own path rather than sharing one read. @@ -270,8 +329,12 @@ def main() -> int: "Public-document policy rows must fully parse through " "scripts/check-doc-checkpoint.py. The role interview is the block between " f"{INTERVIEW_MARKER} and its :end in {INTERVIEW_DOCUMENT}; it must " - "name every answer agent-role.py accepts. The operator's loop is " - f"the block between {LOOP_MARKER} and its :end in {LOOP_DOCUMENT}; " + "name every answer agent-role.py accepts. The session entrypoint is " + f"the block between {ENTRYPOINT_MARKER} " + f"and {ENTRYPOINT_END} in {ENTRYPOINT_DOCUMENT}; it must route " + "scripts/agent-start.py before scripts/agent-preflight.sh. " + f"The operator's loop is the block between {LOOP_MARKER} and its " + f":end in {LOOP_DOCUMENT}; " f"it must carry {', '.join(repr(n) for n in LOOP_REQUIRED)} inside " "the block. Every runtime prompt must satisfy the closed grammar " "in scripts/check-prompt-contract.py; unknown prose is a failure.", @@ -282,7 +345,8 @@ def main() -> int: print( "OK: public-document policy matches scripts/check-doc-checkpoint.py, " f"{INTERVIEW_DOCUMENT} carries the " - f"role interview and the orchestration loop, all runtime prompts satisfy " + "role interview, universal session entrypoint, and orchestration loop, " + "all runtime prompts satisfy " "the closed semantic contract, and cutover wiring is complete." ) return 0 diff --git a/scripts/policy_contract.py b/scripts/policy_contract.py index 006229469..c0c8c635f 100644 --- a/scripts/policy_contract.py +++ b/scripts/policy_contract.py @@ -47,6 +47,7 @@ } T0_RULE_IDS = ( "POL-AUTH-REGISTRY", + "POL-BOOT-ENTRYPOINT", "POL-ROLE-DECLARED", "POL-SPIKE-FIRST", "POL-MIRROR-VLLM", @@ -61,7 +62,7 @@ "POL-PR-DISPOSITION", ) BOOT_BLOCK = """ -1. Resolve the worktree role with `python3 scripts/agent-role.py show`; claim the developer-selected role only when no valid role exists. +1. Run `scripts/agent-start.py`; pass explicit intent when known, follow its role action, and rerun it after declaration. 2. Resolve `.env` and `.agents/developer-preferences.md` from the shared checkout, requesting only values required by the current task. 3. Read `.agents/NOW.md` for the live snapshot. 4. Read `.agents/policy.csv`, then the procedure named by each applicable rule. diff --git a/tests/scripts/test_agent_onboard.py b/tests/scripts/test_agent_onboard.py index 9ef8a8df1..fe79303fb 100644 --- a/tests/scripts/test_agent_onboard.py +++ b/tests/scripts/test_agent_onboard.py @@ -199,6 +199,35 @@ def test_probe_reports_the_role_and_row_that_were_claimed(self) -> None: self.assertEqual(state["mode"], "interactive") self.assertEqual(state["reason"], "declared") self.assertIs(state["blocked_by_other_operator"], False) + self.assertEqual(state["branch"], "master") + self.assertEqual( + state["worktree"], + subprocess.check_output( + ["git", "rev-parse", "--absolute-git-dir"], + cwd=self.repo, + text=True, + ).strip(), + ) + + def test_probe_projects_real_helper_worktree_identity_and_branch(self) -> None: + helper = self.worktree("probe-helper") + claimed = self.claim( + helper, "a", "helper", "--row", "PROBE-REAL-WORKTREE" + ) + self.assertEqual(claimed.returncode, 0, claimed.stderr) + + state = self.probe_in(helper) + + self.assertEqual(state["branch"], "probe-helper") + self.assertEqual( + state["worktree"], + subprocess.check_output( + ["git", "rev-parse", "--absolute-git-dir"], + cwd=helper, + text=True, + ).strip(), + ) + self.assertNotEqual(state["worktree"], str(helper)) def test_probe_reports_a_mode_that_was_declared(self) -> None: # The other half of the mode pin: a hardcoded "interactive" survives the @@ -306,10 +335,9 @@ def test_require_role_defaults_on(self): def test_opt_out_flag_exists(self): self.assertIn("--no-require-role", self.TEXT) - def test_failure_text_carries_the_interview(self): - # An error code alone gets routed around. The gate must say what to ask. - self.assertIn("claim read-only", self.TEXT) - self.assertIn("claim helper --row", self.TEXT) + def test_failure_text_points_to_the_canonical_entrypoint(self): + # Preflight remains a backstop, but the role interview has one owner. + self.assertIn("scripts/agent-start.py", self.TEXT) def test_staged_refuses_read_only(self): self.assertIn("read-only", self.TEXT) @@ -330,6 +358,9 @@ def test_the_gate_records_a_failure_and_not_only_a_print(self): def test_onboard_suite_is_registered(self): self.assertIn("test_agent_onboard", self.TEXT) + def test_agent_start_suite_is_registered(self): + self.assertIn("test_agent_start", self.TEXT) + def test_read_only_alone_does_not_satisfy_a_write_gate(self): # agent-role.py show exits 0 for read-only, so --require-role is # satisfied by a declared ABSENCE of claim. That is correct for a plain diff --git a/tests/scripts/test_agent_start.py b/tests/scripts/test_agent_start.py new file mode 100644 index 000000000..d6e1e5fc6 --- /dev/null +++ b/tests/scripts/test_agent_start.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +"""Behavior and CLI checks for the universal session entrypoint.""" + +from __future__ import annotations + +import contextlib +import importlib.util +import io +import subprocess +import sys +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +SCRIPT = ROOT / "scripts/agent-start.py" + + +def load_start(): + if not SCRIPT.is_file(): + raise AssertionError("scripts/agent-start.py does not exist") + spec = importlib.util.spec_from_file_location("agent_start", SCRIPT) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules["agent_start"] = module + spec.loader.exec_module(module) + return module + + +def state(**changes): + complete = { + "role": None, + "row": None, + "mode": "interactive", + "branch": "main", + "worktree": "/repo/.git", + "blocked_by_other_operator": False, + "reason": "undeclared", + "env": "present", + "env_missing": [], + "queue": ["ENGINE-READY", "MODEL-READY"], + "queue_error": None, + } + complete.update(changes) + return complete + + +class RendererTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.start = load_start() + + def render(self, route=None, intent=None, row=None, headless=False): + return self.start.render_route(route or state(), intent, row, headless) + + def test_first_time_route_has_compact_ascii_welcome_and_actions(self): + out = self.render() + self.assertEqual(out.count("--- WELCOME: RELAY VERBATIM ---"), 1) + self.assertEqual(out.count("--- END WELCOME ---"), 1) + self.assertEqual(out.count("--- AGENT NEXT ACTIONS ---"), 1) + self.assertEqual(out.count("--- END ACTIONS ---"), 1) + self.assertIn("Helper", out) + self.assertIn("one focused contribution", out.lower()) + self.assertIn("Operator", out) + self.assertIn("long multi-agent campaign", out) + self.assertIn("Looking", out) + self.assertIn("read, review, or ask questions", out.lower()) + self.assertIn("Relay only the welcome block above verbatim", out) + self.assertIn("ask what the contributor is here to do", out) + self.assertIn("scripts/agent-role.py claim", out) + self.assertIn("rerun scripts/agent-start.py", out) + self.assertIn("scripts/agent-preflight.sh", out) + self.assertTrue(out.isascii()) + self.assertLessEqual(max(map(len, out.splitlines())), 72) + + def test_welcome_constant_is_the_single_rendered_banner(self): + out = self.render() + welcome = out.split("--- WELCOME: RELAY VERBATIM ---\n", 1)[1].split( + "\n--- END WELCOME ---", 1 + )[0] + self.assertEqual(welcome, self.start.WELCOME) + + def test_explicit_operator_prints_exact_claim_and_suppresses_welcome(self): + out = self.render(intent="operator") + self.assertNotIn("WELCOME: RELAY", out) + self.assertIn("scripts/agent-role.py claim operator", out) + self.assertNotIn("--headless", out) + self.assertIn("rerun scripts/agent-start.py", out) + + def test_explicit_headless_operator_propagates_only_explicit_mode(self): + out = self.render(intent="operator", headless=True) + self.assertIn("scripts/agent-role.py claim operator --headless", out) + + def test_explicit_read_only_prints_exact_claim(self): + out = self.render(intent="read-only") + self.assertNotIn("WELCOME: RELAY", out) + self.assertIn("scripts/agent-role.py claim read-only", out) + + def test_helper_with_row_prints_exact_claim(self): + out = self.render(intent="helper", row="ENGINE-FOCUSED") + self.assertNotIn("WELCOME: RELAY", out) + self.assertIn( + "scripts/agent-role.py claim helper --row ENGINE-FOCUSED", out + ) + + def test_headless_helper_appends_mode_after_row(self): + out = self.render(intent="helper", row="ENGINE-FOCUSED", headless=True) + self.assertIn( + "scripts/agent-role.py claim helper --row ENGINE-FOCUSED --headless", + out, + ) + + def test_helper_without_row_shows_ready_queue_but_no_placeholder_claim(self): + out = self.render(intent="helper") + self.assertIn("identify or create one scoped row", out.lower()) + self.assertIn("ENGINE-READY", out) + self.assertIn("MODEL-READY", out) + self.assertNotIn("claim helper --row ", out) + self.assertNotIn("claim helper --row ENGINE-READY", out) + self.assertNotIn("ROW-ID", out) + + def test_helper_without_row_distinguishes_unavailable_and_empty_queue(self): + unavailable = self.render( + state(queue=[], queue_error="record cannot be parsed"), intent="helper" + ) + empty = self.render(state(queue=[], queue_error=None), intent="helper") + self.assertIn("READY queue unavailable", unavailable) + self.assertIn("record cannot be parsed", unavailable) + self.assertIn("READY queue is empty", empty) + + def test_declared_roles_report_real_materialized_state_and_no_welcome(self): + cases = ( + ("operator", None), + ("helper", "ENGINE-FOCUSED"), + ("read-only", None), + ) + for role, row in cases: + with self.subTest(role=role): + out = self.render( + state( + role=role, + row=row, + mode="headless", + branch="row/real-branch", + worktree="/repo/.git/worktrees/real-worktree", + reason="declared", + ) + ) + self.assertNotIn("WELCOME: RELAY", out) + self.assertIn(f"role: {role}", out) + if row: + self.assertIn(f"row: {row}", out) + self.assertIn("mode: headless", out) + self.assertIn("branch: row/real-branch", out) + self.assertIn("worktree: /repo/.git/worktrees/real-worktree", out) + self.assertIn("confirm this inherited role fits", out.lower()) + self.assertIn("scripts/agent-preflight.sh", out) + self.assertIn("printed .agents/NOW.md", out) + self.assertIn(".agents/developer-preferences.md", out) + self.assertIn(".agents/coordination.md", out) + self.assertIn(".agents/state.md", out) + + def test_declared_marker_owns_mode_even_when_no_conflict(self): + out = self.render( + state(role="operator", mode="interactive", reason="declared"), + intent="operator", + headless=True, + ) + self.assertIn("mode: interactive", out) + self.assertNotIn("claim operator --headless", out) + + def test_conflicting_declared_intent_reports_mismatch_without_claim(self): + out = self.render( + state(role="helper", row="ENGINE-FOCUSED", reason="declared"), + intent="operator", + ) + self.assertNotIn("WELCOME: RELAY", out) + self.assertIn("intent operator conflicts with declared role helper", out) + self.assertIn("stop and obtain direction", out.lower()) + self.assertNotIn("scripts/agent-role.py claim operator", out) + self.assertIn("No worktree or PR was created by this command", out) + + def test_external_operator_lock_blocks_known_failing_claim(self): + out = self.render( + state( + blocked_by_other_operator=True, + reason="operator lock held elsewhere", + ), + intent="operator", + ) + self.assertIn("operator lock is held by another live worktree", out) + self.assertIn("operator lock held elsewhere", out) + self.assertNotIn("scripts/agent-role.py claim operator", out) + self.assertNotIn("claim helper", out) + + def test_first_time_route_surfaces_operator_lock_before_role_choice(self): + out = self.render( + state( + blocked_by_other_operator=True, + reason="operator lock held elsewhere", + ) + ) + self.assertIn("WELCOME: RELAY VERBATIM", out) + self.assertIn("operator lock is held by another live worktree", out) + self.assertIn("operator lock held elsewhere", out) + self.assertNotIn( + "Use the matching scripts/agent-role.py claim command.", out + ) + self.assertIn( + "If the contributor chooses operator, report the conflict", out + ) + + def test_environment_reports_status_only_and_never_secret_values(self): + secret = "TOP-SECRET-TOKEN" + out = self.render( + state(env="incomplete", env_missing=["VLLM_ORACLE", secret]) + ) + self.assertIn("environment: incomplete", out) + self.assertNotIn("VLLM_ORACLE", out) + self.assertNotIn(secret, out) + unreadable = self.render(state(env="unreadable", env_missing=[secret])) + missing = self.render(state(env="missing", env_missing=[secret])) + self.assertIn("environment: unreadable", unreadable) + self.assertIn("environment: missing", missing) + self.assertNotIn(secret, unreadable + missing) + + +class MainTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.start = load_start() + + def run_main(self, argv, route=None): + saved = self.start.onboard.probe + self.start.onboard.probe = lambda: route or state() + stdout, stderr = io.StringIO(), io.StringIO() + try: + with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + code = self.start.main(argv) + finally: + self.start.onboard.probe = saved + return code, stdout.getvalue(), stderr.getvalue() + + def test_valid_routes_exit_zero(self): + for argv in ( + [], + ["--intent", "operator"], + ["--intent", "read-only"], + ["--intent", "helper"], + ["--intent", "helper", "--row", "ENGINE-FOCUSED"], + ["--intent", "operator", "--headless"], + ): + with self.subTest(argv=argv): + code, out, err = self.run_main(argv) + self.assertEqual(code, 0, err) + self.assertIn("--- AGENT NEXT ACTIONS ---", out) + + def test_invalid_flag_combinations_exit_nonzero(self): + for argv in ( + ["--row", "ENGINE-FOCUSED"], + ["--intent", "operator", "--row", "ENGINE-FOCUSED"], + ["--intent", "read-only", "--row", "ENGINE-FOCUSED"], + ["--headless"], + ): + with self.subTest(argv=argv): + code, _, err = self.run_main(argv) + self.assertNotEqual(code, 0) + self.assertIn("ERROR", err) + + def test_probe_failure_is_nonzero_and_actionable(self): + def explode(): + raise RuntimeError("probe broke") + + saved = self.start.onboard.probe + self.start.onboard.probe = explode + stdout, stderr = io.StringIO(), io.StringIO() + try: + with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + code = self.start.main([]) + finally: + self.start.onboard.probe = saved + self.assertNotEqual(code, 0) + self.assertEqual(stdout.getvalue(), "") + self.assertIn("probe broke", stderr.getvalue()) + + def test_real_cli_renders_branch_and_worktree_from_real_probe(self): + result = subprocess.run( + [sys.executable, str(SCRIPT)], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(result.returncode, 0, result.stderr) + branch = subprocess.check_output( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=ROOT, text=True + ).strip() + self.assertIn(f"branch: {branch}", result.stdout) + expected = subprocess.check_output( + ["git", "rev-parse", "--absolute-git-dir"], cwd=ROOT, text=True + ).strip() + self.assertIn(f"worktree: {expected}", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/scripts/test_check_prompt_contract.py b/tests/scripts/test_check_prompt_contract.py index 950468dd6..9852e1fd6 100644 --- a/tests/scripts/test_check_prompt_contract.py +++ b/tests/scripts/test_check_prompt_contract.py @@ -230,6 +230,31 @@ def test_reviewer_contract_has_three_lenses_and_no_repair_escape(self) -> None: self.assertIn("static review", text.lower()) self.assertIn("full declared gate", text.lower()) + def test_operator_continues_actionable_failures_until_pass(self) -> None: + continue_row = ( + "- `OP-CONTINUE` | required | For every actionable in-scope reviewer " + "FAIL dispatch a fresh implementer, run focused and full gates, and " + "dispatch a fresh scoped reviewer; repeat until PASS because attempt " + "or retry budgets are scheduling controls and never terminal blockers." + ) + stop_rows = ( + "- `STOP-AUTHORITY` | BLOCKED | A required external action exceeds " + "Authority and the precise missing authority is named.", + "- `STOP-RESOURCE` | BLOCKED | A required external resource is " + "unavailable and the precise resource is named.", + "- `STOP-DEVELOPER` | BLOCKED | The developer explicitly directs the " + "review loop to stop before PASS.", + "- `STOP-REMOTE` | REMOTE_UNVERIFIED | Required remote state cannot be queried.", + ) + text = self.shipped("operator") + self.assertIn(continue_row, prompt_contract.METHOD_ROWS["operator"]) + self.assertIn(continue_row, text) + self.assertEqual(prompt_contract.STOP_ROWS["operator"], stop_rows) + for row in stop_rows: + with self.subTest(row=row): + self.assertIn(row, text) + self.assertNotIn("STOP-BLOCKER", text) + def test_reviewer_findings_are_first_structured_and_severity_ordered(self) -> None: text = self.shipped("reviewer") finding = prompt_contract.OUTPUT_ROWS["reviewer"][0] diff --git a/tests/scripts/test_check_protocol_consistency.py b/tests/scripts/test_check_protocol_consistency.py index 5bdb5faff..e6a0d6b38 100644 --- a/tests/scripts/test_check_protocol_consistency.py +++ b/tests/scripts/test_check_protocol_consistency.py @@ -458,6 +458,52 @@ def test_each_required_answer_is_pinned_individually(self): self.assertTrue(consistency.interview_errors(text.replace(needle, ""))) +class SessionEntrypointTests(unittest.TestCase): + def _manual(self) -> str: + return (ROOT / consistency.ENTRYPOINT_DOCUMENT).read_text(encoding="utf-8") + + def test_workflow_carries_start_before_preflight(self): + text = self._manual() + self.assertEqual(text.count(consistency.ENTRYPOINT_MARKER), 1) + self.assertEqual(text.count(consistency.ENTRYPOINT_END), 1) + self.assertEqual(consistency.entrypoint_errors(text), []) + + def test_missing_or_reordered_entrypoint_is_rejected(self): + missing = consistency.entrypoint_errors("run preflight") + self.assertTrue(any("session-entrypoint" in error for error in missing)) + reordered = "\n".join( + ( + consistency.ENTRYPOINT_MARKER, + "Run scripts/agent-preflight.sh.", + "Run scripts/agent-start.py --intent helper.", + consistency.ENTRYPOINT_END, + ) + ) + self.assertTrue( + any( + "before preflight" in error + for error in consistency.entrypoint_errors(reordered) + ) + ) + + def test_each_required_route_is_mutation_bound(self): + text = self._manual() + for needle in consistency.ENTRYPOINT_REQUIRED: + with self.subTest(needle=needle): + self.assertTrue( + consistency.entrypoint_errors(text.replace(needle, "")) + ) + + def test_main_calls_the_entrypoint_gate(self): + text = self._manual() + damaged = text.replace("scripts/agent-start.py", "", 1) + self.assertNotEqual(damaged, text) + with _repo_copy(damaged) as run: + code, _, err = run() + self.assertEqual(code, 1) + self.assertIn("session entrypoint", err) + + class InterviewWiring(unittest.TestCase): """The checker must CALL interview_errors, not merely define it. @@ -653,6 +699,11 @@ def test_the_loop_states_the_rules_that_carry_it(self): "mutate, not read", "run the row's gate yourself", "never fix findings yourself", + "repeat this cycle until pass", + "attempt and retry budgets are scheduling controls", + "never terminal blockers for correctable findings", + "explicit developer direction", + "precise external authority or resource blocker", ): with self.subTest(needle=needle): self.assertIn(needle, lowered) @@ -727,6 +778,11 @@ def test_the_checker_enforces_the_phrases_this_suite_demands(self): "mutate, not read", "run the row's gate yourself", "never fix findings yourself", + "repeat this cycle until pass", + "attempt and retry budgets are scheduling controls", + "never terminal blockers for correctable findings", + "explicit developer direction", + "precise external authority or resource blocker", }, "LOOP_REQUIRED no longer enforces exactly the phrases this suite " "demands; narrowing one is how the gate stops catching what it was " diff --git a/tests/scripts/test_policy_contract.py b/tests/scripts/test_policy_contract.py index 70989fa97..f056b5cc2 100644 --- a/tests/scripts/test_policy_contract.py +++ b/tests/scripts/test_policy_contract.py @@ -314,6 +314,23 @@ def test_cli_passes_valid_registry_and_fails_bootstrap_defect(self) -> None: class RepositoryRegistry(unittest.TestCase): + def test_review_failure_policy_requires_continuation_until_pass(self) -> None: + rule = load_policy(ROOT)["POL-REVIEW-NO-REPAIR"] + for clause in ( + "actionable in-scope findings", + "fresh implementer", + "without repair in the coordinating session", + "focused and full gates", + "fresh scoped review", + "until PASS", + "attempt budgets never terminate correctable findings", + "explicit developer direction", + "precise external authority or resource blocker", + ): + with self.subTest(clause=clause): + self.assertIn(clause, rule.requirement) + self.assertEqual(rule.waiver_class, "never") + def test_accepted_design_inventory_passes_schema(self) -> None: rules = load_policy(ROOT) self.assertLessEqual(len(rules), 60) @@ -375,7 +392,7 @@ def test_agents_budget_boot_order_and_generated_t0_are_enforced(self) -> None: self.assertTrue(any("12 KiB" in error for error in self.errors())) shutil.copy2(ROOT / "AGENTS.md", path) - self.mutate("AGENTS.md", "1. Resolve the worktree role", "1. Read task state before role; then resolve the worktree role") + self.mutate("AGENTS.md", "1. Run `scripts/agent-start.py`", "1. Read task state before running `scripts/agent-start.py`") self.assertTrue(any("boot block" in error for error in self.errors())) shutil.copy2(ROOT / "AGENTS.md", path)