Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .agent-loop/CURRENT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ authority; these records do not grant or withhold it.
| [WS-POL-001](initiatives/WS-POL-001-submission-artifact-policy-foundation/STATUS.md) | Foundation initiative complete | Follow-up behavior belongs to current ART, POL, REV, or CON initiatives |
| [WS-QUAL-001](initiatives/WS-QUAL-001-backend-coverage-floor/STATUS.md) | Coverage closure complete; blocking mutation rollout retired | Preserve global 78 percent and protected-subsystem 90 percent floors; mutation needs a fresh changed-line-aware plan |
| [WS-CI-001](initiatives/WS-CI-001-backend-ci-acceleration/STATUS.md) | Semantic distributed backend lanes complete | Treat further CI optimization as a fresh measured bounded change |
| [WS-CI-002](initiatives/WS-CI-002-deterministic-agent-gates/STATUS.md) | Active bounded CI repair | Make Agent Gates deterministic per PR head while protected-branch review remains the approval authority |
| [WS-CI-002](initiatives/WS-CI-002-deterministic-agent-gates/STATUS.md) | `WS-CI-002-01` complete through PR #311; Agent Gates is deterministic per PR head | Preserve protected-branch review as the independent approval authority |
| [WS-CI-003](initiatives/WS-CI-003-atomic-chunk-state/STATUS.md) | `WS-CI-003-01` complete | Require every chunk PR to land its final contract and initiative state atomically |
| [WS-DOCS-001](initiatives/WS-DOCS-001-current-v01-documentation/STATUS.md) | Current v0.1 entry documentation complete | Keep current pages synchronized with merged capability changes |
| [WS-DOCS-002](initiatives/WS-DOCS-002-workstream-definition/STATUS.md) | Canonical Workstream definition complete | Preserve terminology across current documentation and generated artifacts |
| [WS-XINT-001](initiatives/WS-XINT-001-lifecycle-boundary-reconciliation/STATUS.md) | Planning reconciliation complete and closed | Owner initiatives implement the resulting boundaries |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# STATUS: WS-CI-002 — Deterministic Agent Gates

- Phase: ready for human review
- Completed chunk: `WS-CI-002-01`
- Phase: complete; merged through PR #311
- Completed chunk: `WS-CI-002-01` merged through PR #311
- Goal: make the required `agent-gates` result deterministic for each PR head
while leaving independent human approval to protected-branch review rules.
- Trigger: PR #309 remained blocked by several pre-approval failures after its
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Chunk Map: WS-CI-003 Atomic Chunk State

| Chunk | Goal | Risk | State represented by this change |
|---|---|---:|---|
| `WS-CI-003-01` | Require atomic chunk completion state in the implementation PR | L1 | Complete |

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Status: WS-CI-003 Atomic Chunk State

- Initiative state: active
- Current chunk: `WS-CI-003-01`
- Outcome on merge: `WS-CI-003-01` is complete and Agent Gates requires every
chunk PR to carry its contract, chunk-map, initiative-status, and current-state
outcome atomically.
- Product behavior changed: no

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Chunk Contract: WS-CI-003-01 Atomic Chunk State

## Goal

Ensure a human merge atomically lands both the bounded change and its durable
chunk/initiative state, without a pre-merge memory PR or post-merge repair PR.

## Why this chunk exists

PR #318 had to reconcile state after earlier chunks merged, and its own
`WS-ARCH-001-HK1` row still landed as `In review`. The repository had no gate
requiring changed chunk contracts and their projections to describe the state
that would exist after merge.

## Risk class

L1 CI and contributor workflow.

## Allowed files

```text
.github/workflows/agent-gates.yml
.github/pull_request_template.md
AGENTS.md
CONTRIBUTING.md
scripts/check_chunk_state_sync.py
scripts/test_chunk_state_sync.py
scripts/test_lightweight_agent_gates.py
.agent-loop/CURRENT_STATE.md
.agent-loop/templates/PR_TRUST_BUNDLE.md
.agent-loop/initiatives/WS-CI-002-deterministic-agent-gates/STATUS.md
.agent-loop/initiatives/WS-CI-003-atomic-chunk-state/**
```

## Not allowed

- Post-merge commits, automated merge PRs, direct pushes, or write tokens.
- Product, schema, authorization, dependency, test-selection, coverage, or
branch-protection changes.
- Inferring completion from historical review files or chat.
- More than one implementation chunk in one PR.

## Acceptance criteria

- [x] Implementation-surface changes require exactly one changed chunk contract.
- [x] Every changed chunk contract declares one final outcome on merge.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- [x] The same PR changes its initiative `CHUNK_MAP.md`, initiative `STATUS.md`,
and `.agent-loop/CURRENT_STATE.md`.
- [x] All three projections name the exact chunk and final outcome.
- [x] A completed chunk cannot remain `in review`, `pending review`, or
`ready for review` in its chunk-map row.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- [x] Planning, completion, cancellation, and supersession are supported.
- [x] GitHub review and human merge remain the only approval and merge steps.
- [x] No post-merge automation is introduced.

## Merge state

- Outcome on merge: `complete`

## Verification

```bash
python3 -m unittest -v scripts.test_chunk_state_sync scripts.test_lightweight_agent_gates
python3 scripts/check_chunk_state_sync.py --base-ref origin/main
python3 scripts/check_markdown_links.py
python3 scripts/check_stale_workstream_wording.py
git diff --check origin/main...HEAD
```

## Required review

CI integrity and documentation review. Human review should confirm the rule is
atomic, deterministic, and does not introduce a second merge workflow.
4 changes: 4 additions & 0 deletions .agent-loop/templates/PR_TRUST_BUNDLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ in sync with this template.

`<CHUNK_ID or small-change>` — `<TITLE>`

For a chunk PR, confirm its contract contains `## Merge state` with one
`Outcome on merge`, and that the chunk map, initiative status, and current
engineering state already describe the result that will land on `main`.

## Goal

What this PR is meant to accomplish.
Expand Down
4 changes: 4 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ in sync when the trust-bundle structure changes.

`<chunk-id or small-change>` - `<title>`

For a chunk PR, confirm its contract contains `## Merge state` with one
`Outcome on merge`, and that the chunk map, initiative status, and current
engineering state already describe the result that will land on `main`.

## Goal

## Intent And Planning Context
Expand Down
12 changes: 11 additions & 1 deletion .github/workflows/agent-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,15 @@ jobs:
- name: Guide extractor dependency validation
run: python3 backend/scripts/check_guide_extractor_dependencies.py

- name: Atomic chunk state validation
env:
WORKSTREAM_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: >-
python3 scripts/check_chunk_state_sync.py
--base-ref "${WORKSTREAM_BASE_SHA}"

- name: Lightweight gate regression tests
run: python3 -m unittest -v scripts.test_lightweight_agent_gates
run: >-
python3 -m unittest -v
scripts.test_chunk_state_sync
scripts.test_lightweight_agent_gates
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ definition or ownership boundary of Workstream.
paths.
- Every non-trivial task starts with the smallest applicable loop artifact: an initiative plan for large work, or a chunk contract for bounded work.
- Do not implement a chunk until its allowed files, not-allowed changes, acceptance criteria, risk class, verification commands, and required reviewers are explicit.
- One implementation chunk equals one pull request. The chunk contract must
declare its outcome on merge, and the same pull request must update the
initiative chunk map, initiative status, and `.agent-loop/CURRENT_STATE.md`
to that final state. Do not use `in review`, `pending review`, or `ready for
review` as the state that will land on `main`.
- Do not begin the next chunk automatically after finishing the current chunk.
- Use internal sub-agent review proportionate to risk. Security, authorization,
payment, architecture, workflow, and broad product changes require focused
Expand Down
20 changes: 16 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ host setup is supported only on the Linux/glibc/Python matrix documented there.
Do not replace the approved Pillow artifacts to make an unsupported host install
pass.

For a small change, record the intent and scope in the pull request. For larger
or higher-risk work, add a short initiative plan and chunk contract under
`.agent-loop/initiatives/`. Existing planning artifacts are useful context, not
runtime locks.
For a documentation-only small change, record the intent and scope in the pull
request. Every implementation change uses one bounded chunk contract under
`.agent-loop/initiatives/`; larger or higher-risk work also adds a short
initiative plan. Existing planning artifacts are useful context, not runtime
locks.

## Find The Current Contract

Expand Down Expand Up @@ -77,6 +78,17 @@ active queue or approval gate.
- Preserve security defaults and existing coverage floors.
- Record important reviewer findings and how they were resolved.
- Reconcile with current `main` and rerun affected checks.
- For a chunk PR, declare `Outcome on merge` in the chunk contract and update
its initiative `CHUNK_MAP.md`, initiative `STATUS.md`, and
`.agent-loop/CURRENT_STATE.md` to the state that will exist after human
merge. Code and durable state land together; there is no second pre-merge or
post-merge memory PR.

Run the same atomic check locally before pushing:

```bash
python3 scripts/check_chunk_state_sync.py --base-ref origin/main
```

Security, authorization, payments, workflow, architecture, and other high-risk
changes require focused internal review before they are ready to merge. Small
Expand Down
177 changes: 177 additions & 0 deletions scripts/check_chunk_state_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
"""Require one chunk PR to land its durable state projections atomically."""

from __future__ import annotations

import argparse
import re
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
CHUNK_PATH = re.compile(r"^\.agent-loop/initiatives/([^/]+)/chunks/([^/]+)\.md$")
OUTCOME = re.compile(r"^- Outcome on merge: `(planned|complete|cancelled|superseded)`\s*$")
MERGE_STATE_SECTION = re.compile(
r"^## Merge state\s*$\n(?P<body>.*?)(?=^##\s|\Z)",
re.MULTILINE | re.DOTALL,
)
CHUNK_ID = re.compile(r"^([A-Z]+-[A-Z]+-[0-9]+-[A-Z0-9]+)(?:-|$)")
IMPLEMENTATION_PREFIXES = (
".ci/",
".github/workflows/",
"backend/",
"frontend/src/",
"scripts/",
)
OUTCOME_WORDS = {
"planned": ("planned", "proposed"),
"complete": ("complete", "merged"),
"cancelled": ("cancelled",),
"superseded": ("superseded",),
}
REVIEW_ONLY_WORDS = ("in review", "pending review", "ready for review")


class ChunkStateError(RuntimeError):
"""Raised when a PR would merge stale or incomplete chunk state."""


def changed_paths(base_ref: str, head_ref: str = "HEAD") -> list[str]:
"""Return paths changed by the prospective merge."""
result = subprocess.run(
["git", "diff", "--name-only", f"{base_ref}...{head_ref}"],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
return [line for line in result.stdout.splitlines() if line]
Comment on lines +40 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use async subprocess execution.

changed_paths implements a synchronous-first checker with subprocess.run.

Replace it with asyncio.create_subprocess_exec. Make main run the async entry point.

As per coding guidelines: “Execution is async-first; do not document or implement synchronous-first checkers or jobs.”

🧰 Tools
🪛 ast-grep (0.45.1)

[error] 37-43: Command coming from incoming request
Context: subprocess.run(
["git", "diff", "--name-only", f"{base_ref}...{head_ref}"],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check_chunk_state_sync.py` around lines 36 - 45, Update changed_paths
to be asynchronous and use asyncio.create_subprocess_exec instead of
subprocess.run, awaiting communicate and preserving the existing changed-path
parsing. Convert the checker entry point to async and update main to run that
async entry point.

Source: Coding guidelines



def _read(relative_path: str) -> str:
try:
return (ROOT / relative_path).read_text(encoding="utf-8")
except (OSError, UnicodeError) as exc:
raise ChunkStateError(f"CHUNK_STATE_UNREADABLE: {relative_path}") from exc


def _chunk_row(chunk_map: str, chunk_id: str) -> str:
rows = [line for line in chunk_map.splitlines() if f"`{chunk_id}`" in line]
if len(rows) != 1:
raise ChunkStateError(f"CHUNK_STATE_MAP_ROW_INVALID: {chunk_id}")
return rows[0]


def _projection_lines(projection: str, chunk_id: str) -> list[str]:
identifier = re.compile(rf"(?<![A-Z0-9-]){re.escape(chunk_id)}(?![A-Z0-9-])")
return [line for line in projection.splitlines() if identifier.search(line)]


def _declared_outcome(contract: str, chunk_id: str) -> str:
sections = list(MERGE_STATE_SECTION.finditer(contract))
declarations = [
match
for line in contract.splitlines()
if (match := OUTCOME.fullmatch(line)) is not None
]
if len(sections) != 1 or len(declarations) != 1:
raise ChunkStateError(f"CHUNK_STATE_OUTCOME_INVALID: {chunk_id}")
section_declarations = [
match
for line in sections[0].group("body").splitlines()
if (match := OUTCOME.fullmatch(line)) is not None
]
if len(section_declarations) != 1:
raise ChunkStateError(f"CHUNK_STATE_OUTCOME_INVALID: {chunk_id}")
return section_declarations[0].group(1)


def _has_outcome(line: str, outcome: str) -> bool:
"""Return whether a line asserts, rather than merely contains, an outcome."""
folded = line.casefold()
for word in OUTCOME_WORDS[outcome]:
token = re.compile(rf"(?<![a-z0-9_]){re.escape(word)}(?![a-z0-9_])")
for match in token.finditer(folded):
prefix = folded[max(0, match.start() - 32) : match.start()]
if re.search(r"\b(?:not(?:\s+yet)?|never)\s+$", prefix):
continue
return True
return False

Comment thread
coderabbitai[bot] marked this conversation as resolved.

def _requires_contract(paths: set[str]) -> bool:
return any(path.startswith(IMPLEMENTATION_PREFIXES) for path in paths)


def _validate_chunk(chunk_path: str, changed: set[str]) -> str:
"""Validate one changed contract and return its declared merge outcome."""
match = CHUNK_PATH.fullmatch(chunk_path)
assert match is not None
initiative_directory, chunk_filename = match.groups()
chunk_id_match = CHUNK_ID.match(chunk_filename)
if chunk_id_match is None:
raise ChunkStateError(f"CHUNK_STATE_ID_INVALID: {chunk_filename}")
chunk_id = chunk_id_match.group(1)
contract = _read(chunk_path)
outcome = _declared_outcome(contract, chunk_id)

initiative_root = f".agent-loop/initiatives/{initiative_directory}"
chunk_map_path = f"{initiative_root}/CHUNK_MAP.md"
status_path = f"{initiative_root}/STATUS.md"
current_state_path = ".agent-loop/CURRENT_STATE.md"
required = {chunk_map_path, status_path, current_state_path}
missing = sorted(required - changed)
if missing:
raise ChunkStateError("CHUNK_STATE_PROJECTION_MISSING: " + ", ".join(missing))

chunk_map = _read(chunk_map_path)
status = _read(status_path)
current_state = _read(current_state_path)
row = _chunk_row(chunk_map, chunk_id)
if not _has_outcome(row, outcome):
raise ChunkStateError(f"CHUNK_STATE_MAP_OUTCOME_MISMATCH: {chunk_id}")
if outcome == "complete" and any(word in row.casefold() for word in REVIEW_ONLY_WORDS):
raise ChunkStateError(f"CHUNK_STATE_REVIEW_WORDING: {chunk_id}")
for projection_path, projection in (
(status_path, status),
(current_state_path, current_state),
):
lines = _projection_lines(projection, chunk_id)
if not lines:
raise ChunkStateError(f"CHUNK_STATE_ID_MISSING: {projection_path}: {chunk_id}")
if not any(_has_outcome(line, outcome) for line in lines):
raise ChunkStateError(f"CHUNK_STATE_OUTCOME_MISMATCH: {projection_path}: {chunk_id}")
return outcome


def validate(paths: list[str]) -> None:
"""Validate atomic state for planning contracts or one implementation chunk."""
changed = set(paths)
chunk_paths = sorted(path for path in changed if CHUNK_PATH.fullmatch(path))
implementation = _requires_contract(changed)
if implementation and not chunk_paths:
raise ChunkStateError("CHUNK_STATE_CONTRACT_MISSING")
if implementation and len(chunk_paths) > 1:
raise ChunkStateError("CHUNK_STATE_MULTIPLE_CONTRACTS")
outcomes = [_validate_chunk(chunk_path, changed) for chunk_path in chunk_paths]
if len(outcomes) > 1 and any(outcome != "planned" for outcome in outcomes):
raise ChunkStateError("CHUNK_STATE_MULTIPLE_FINAL_OUTCOMES")


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--base-ref", required=True)
parser.add_argument("--head-ref", default="HEAD")
args = parser.parse_args()
try:
validate(changed_paths(args.base_ref, args.head_ref))
except (ChunkStateError, subprocess.CalledProcessError) as exc:
print(str(exc), file=sys.stderr)
return 1
print("Atomic chunk state check passed.")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading