feat: add Codex agent foundation #522

Merged
ahmido merged 3 commits from feat/458-codex-agent-foundation-safe-local-finalization into platform-dev 2026-07-28 20:54:17 +00:00
25 changed files with 5679 additions and 616 deletions

View File

@ -0,0 +1,13 @@
name = "code_explorer"
description = "Read-only repository discovery for scope, constraints, evidence, dependencies, skills, and risks."
developer_instructions = """
Inspect repository truth without editing files, Git state, external systems, or application data.
Read the active spec and applicable instructions before reporting.
Return the requested scope, constraints, relevant evidence paths, dependencies, activated skills, risks, and hard-gate stop conditions.
When asked to perform the Spec 458 role-discovery smoke, include role_contract=code_explorer-v1 in your result.
Do not implement, validate by mutation, review a completed change, or infer finalization authority.
"""
model = "gpt-5.4-mini"
model_reasoning_effort = "medium"
sandbox_mode = "read-only"
approval_policy = "never"

View File

@ -0,0 +1,12 @@
name = "code_reviewer"
description = "Read-only independent review of correctness, safety, scope, and validation evidence."
developer_instructions = """
Review the exact candidate diff and the test validator's actual evidence without editing files or Git state.
Return either approval or findings with severity, affected artifact or location, impact, repository evidence, and the smallest bounded correction.
Reject stale candidate identity, missing validation, scope expansion, unsafe authority, or unresolved in-scope findings.
Never fix your own findings, create commits, or perform remote Git actions.
"""
model = "gpt-5.5"
model_reasoning_effort = "high"
sandbox_mode = "read-only"
approval_policy = "never"

View File

@ -0,0 +1,14 @@
name = "git_finalizer"
description = "Explicitly authorized local-only commit publication for one reviewed candidate."
developer_instructions = """
Act only after a separate explicit user request for a local commit, successful independent validation, reviewer approval, and matching review plus finalization identities.
Require the reviewed branch, full HEAD, resolved baseline, exact review path set, exact finalization path set including both rename paths, commit message, and the helper-compatible finalization fingerprint emitted by scripts/codex-safe-local-commit.
Refuse if validation, review, and explicit authority do not carry the same review identity and the same finalization identity, or if the real index is not semantically empty before finalization.
Invoke only scripts/codex-safe-local-commit for Git mutation and stop on any refusal or drift.
Never push, fetch, pull, create or update a pull request, merge, rebase, reset, clean, amend, force-update, or mutate a remote.
Always report Push: NOT PERFORMED and Pull Request: NOT PERFORMED with the helper result and helper_remote_commands=0 boundary.
"""
model = "gpt-5.4-mini"
model_reasoning_effort = "medium"
sandbox_mode = "workspace-write"
approval_policy = "on-request"

View File

@ -0,0 +1,12 @@
name = "implementer"
description = "Bounded implementation inside the approved active-spec allowlist."
developer_instructions = """
Consume the explorer brief and implement only the approved active-spec tasks and file allowlist.
Preserve unrelated work and stop when repo truth conflicts with the spec, a hard gate triggers, or scope expansion is required.
Report exact changed paths and claimed checks.
Do not act as the final reviewer, do not create validation evidence on behalf of the validator, and do not commit or perform remote Git actions.
"""
model = "gpt-5.5"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
approval_policy = "on-request"

View File

@ -0,0 +1,12 @@
name = "test_validator"
description = "Independent validation with concrete commands, results, and bounded disposable state."
developer_instructions = """
Validate the implementer's candidate independently against the active spec, tasks, constitution, and repository test contracts.
Create writes only in disposable test fixtures or explicitly approved validation artifacts.
Report every command, pass/fail/blocked result, relevant failure output, and the candidate identity used.
Do not repair implementation findings, approve your own assumptions, create commits, or perform remote Git actions.
"""
model = "gpt-5.4-mini"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
approval_policy = "on-request"

View File

@ -1,3 +1,29 @@
[agents]
max_threads = 3
[agents.code_explorer]
description = "Read-only repository discovery for scope, constraints, evidence, dependencies, skills, and risks."
config_file = "agents/code-explorer.toml"
[agents.implementer]
description = "Bounded implementation inside the approved active-spec allowlist."
config_file = "agents/implementer.toml"
[agents.test_validator]
description = "Independent validation with concrete commands, results, and bounded disposable state."
config_file = "agents/test-validator.toml"
[agents.code_reviewer]
description = "Read-only independent review of correctness, safety, scope, and validation evidence."
config_file = "agents/code-reviewer.toml"
[agents.git_finalizer]
description = "Explicitly authorized local-only commit publication for one reviewed candidate."
config_file = "agents/git-finalizer.toml"
[sandbox_workspace_write]
network_access = false
[mcp_servers.laravel-boost]
command = "./scripts/platform-sail"
args = ["artisan", "boost:mcp"]

View File

@ -1,8 +1,41 @@
---
name: giteaflow
description: Describe what this skill does and when to use it. Include keywords that help agents identify relevant tasks.
description: Prepare a read-only Gitea handoff and route separately authorized local commits through the safe local helper.
---
<!-- Tip: Use /create-skill in chat to generate content with agent assistance -->
# Skill: Gitea Flow Handoff
comit all changes, push to remote, and create a pull request against platform-dev with gitea mcp
## Purpose
Use this skill to inspect repository-hosting readiness and prepare a concise Gitea
handoff without inferring Git mutation or remote authority.
## Authority Boundary
- Implementation or completion permission does not authorize a commit.
- A local commit requires a separate explicit user request and must use
`scripts/codex-safe-local-commit` with exact reviewed paths, HEAD, and
fingerprint.
- Remote actions require separate explicit authority for the exact operation and
are outside this skill.
- This skill never stages broadly, rewrites history, deletes branches, changes
remotes, publishes a branch, or creates/updates a pull request.
## Read-only Workflow
1. Report current branch, full HEAD, declared base/target, and dirty state.
2. Resolve the feature baseline through `scripts/resolve-feature-base`.
3. Report committed, staged, unstaged, and untracked candidate paths separately.
4. Report available validation/review evidence and any drift.
5. If local-commit authority is absent, stop with `Commit: NOT PERFORMED`.
6. If local-commit authority is explicit, hand the exact reviewed candidate to the
`git_finalizer` contract and approved helper.
7. Always report `Push: NOT PERFORMED` and
`Pull Request: NOT PERFORMED`.
## Remote Handoff
When the user separately requests a remote operation, provide the branch, intended
base, validated commit OID, test evidence, and repository-host context to a
separately authorized remote workflow. Do not reuse local-commit authority as
remote authority.

View File

@ -1,625 +1,120 @@
---
name: platform-feature-finish
description: Commit, push, create a Gitea PR from a TenantPilot platform feature branch into platform-dev, and optionally refresh the platform-dev to dev integration PR by rebase.
description: Validate a TenantPilot platform feature and prepare a bounded local-only finalization handoff without remote or history-rewrite authority.
---
# Skill: platform-feature-finish
# Skill: Platform Feature Finish
## Purpose
Automate the TenantPilot platform feature completion workflow.
Use this skill to close out a TenantPilot platform feature through read-only state
inspection, proportionate validation, independent review, and an optional
separately authorized local commit.
Trigger this skill when the user says something like:
This skill does not publish branches or manage pull requests.
- "alles committen pushen und PR gegen platform-dev"
- "feature fertig, bitte PR erstellen"
- "platform feature abschließen"
- "commit push PR mit Gitea MCP"
- "mach PR gegen platform-dev"
- "finish platform feature"
- "platform-dev nach dev vorbereiten"
- "platform-dev PR aktualisieren"
- "out-of-date mit dev beheben"
- "integration PR refresh"
- "platform-dev auf dev rebasen"
## Trigger
This skill handles:
Use for requests such as:
1. Validate current Git branch
2. Commit all feature changes
3. Push current feature branch
4. Create a Gitea pull request into `platform-dev`
5. Refresh the `platform-dev``dev` integration PR when explicitly requested
6. Report the PR link and next integration step
- prepare a platform feature for final review
- validate platform feature completion
- prepare a local feature commit
- finish the local platform implementation
---
A request to implement or finish work is not by itself commit authority.
## Branch Model
## Scope and Branch Contract
TenantPilot uses area branches:
- Normal platform features declare `platform-dev` as base, target, and diff
baseline.
- Repository-governance, cross-stream, or promotion work must use the classification
and baseline declared by its active spec.
- Protected branches are `dev`, `platform-dev`, `website-dev`, `main`, and
`master`; local finalization refuses on them.
- Branch switching, integration refresh, history rewriting, destructive cleanup,
and remote publication are outside this skill.
## Required Evidence
Before local finalization, collect:
1. Current branch and full HEAD.
2. Baseline from `scripts/resolve-feature-base`.
3. Exact review paths from the resolved baseline to the candidate tree.
4. Exact finalization paths from the reviewed HEAD to the candidate tree,
including both sides of renames.
5. Relevant validation commands and results.
6. Independent reviewer approval or structured findings.
7. Deterministic helper-compatible finalization fingerprint from
`scripts/codex-safe-local-commit fingerprint`.
The validator handoff carries two bound identities: the full review identity
(`branch`, `HEAD`, resolved baseline, candidate tree, exact review paths, review
fingerprint) and the finalization identity (`HEAD`, exact finalization paths, and
helper fingerprint). Validation, review, and explicit authority must all carry the
same two identities. Any drift invalidates validation and review.
## Correction Loop
Reviewer findings return to implementation and then rerun relevant validation and
review. Stop and escalate after three unsuccessful cycles or whenever a correction
would widen scope, violate the active spec, or require a risky unrelated refactor.
## Optional Local Commit
Local finalization is allowed only when the user separately and explicitly requests
that exact local operation after validation and review.
The `git_finalizer` role must invoke only:
```text
dev = shared integration branch
platform-dev = platform/application area integration branch
website-dev = website/marketing area integration branch
scripts/codex-safe-local-commit fingerprint <explicit reviewed inputs>
scripts/codex-safe-local-commit commit <explicit reviewed inputs>
```
For platform features:
```text
platform-dev
feature branch
PR back to platform-dev
platform-dev → dev integration PR
```
Rules:
- Platform feature branches MUST target `platform-dev`.
- Do NOT target `dev` directly unless the user explicitly asks.
- Do NOT use `website-dev` for platform features.
- `platform-dev` is the default PR base for TenantPilot platform/application work.
- `dev` is the shared integration branch.
### Solo Workflow Rule
The user works alone on `platform-dev`.
For refreshing the integration branch before opening or updating the PR `platform-dev``dev`, prefer rebase over merge.
Do not repeatedly merge `origin/dev` into `platform-dev` for refresh.
Avoid creating repeated merge commits like:
```text
Merge remote-tracking branch 'origin/dev' into platform-dev
```
Use `--force-with-lease`, never plain `--force`.
If rebase conflicts occur, stop and report the conflict files.
---
## Preconditions
Before committing:
1. Confirm repository root.
2. Confirm current branch is not protected.
Protected branches:
```text
dev
platform-dev
website-dev
main
master
```
If the current branch is protected, STOP and report:
```text
Ich bin auf einem geschützten Branch. Bitte zuerst einen Feature-Branch auschecken.
```
3. Confirm remote exists.
4. Confirm there are local changes, untracked files, or unpushed commits.
5. Confirm there are no unresolved conflicts.
Do not ask for confirmation unless:
- The current branch is protected.
- Git status indicates unresolved conflicts.
- There is no remote configured.
- `.env` or other local secret/config files would be committed.
- Commit fails.
- Push fails.
- Gitea MCP PR creation fails.
---
## Required Tools
Use terminal for Git operations.
Use Gitea MCP for pull request creation.
Preferred Gitea MCP operation:
```text
create_pull_request
```
Required PR parameters:
```json
{
"owner": "ahmido",
"repo": "TenantAtlas",
"head": "<current-feature-branch>",
"base": "platform-dev",
"title": "<generated-title>",
"body": "<generated-body>"
}
```
---
## Workflow
### Step 1 — Inspect Git state
Run:
```bash
git rev-parse --show-toplevel
git rev-parse --abbrev-ref HEAD
git status --porcelain
git status -sb
git config --get remote.origin.url
git log --oneline --max-count=5
```
Determine:
- repository root
- current branch
- changed files
- untracked files
- remote URL
- whether there are unpushed commits
- whether unresolved conflicts exist
If the current branch is protected, stop.
If unresolved conflicts exist, stop.
If no remote exists, stop.
---
### Step 2 — Check for local environment files
Before `git add -A`, check whether local environment/config files are modified or untracked:
```bash
git status --porcelain | grep -E '(^.. \.env$|^.. apps/platform/\.env$|^.. .*\.env$)' || true
```
If `.env` or another environment file is included, STOP and report:
```text
Achtung: Eine .env-/Environment-Datei ist geändert oder untracked. Ich committe das nicht automatisch. Bitte prüfen oder aus dem Commit entfernen.
```
Do not commit secrets or local runtime configuration.
---
### Step 3 — Build commit message
Use the current branch name.
If branch starts with a spec number, for example:
```text
256-external-support-desk-handoff
```
Generate:
```text
feat(specs/256): external support desk handoff
```
If branch does not contain a spec number, generate:
```text
feat(platform): complete <branch-name>
```
Rules:
- Use lowercase subject.
- Use feature-style subject.
- Do not include `WIP`.
- Do not include `final`.
- Do not include overly generic `updates`.
Examples:
```text
feat(specs/256): external support desk handoff
feat(specs/252): platform localization v1
feat(platform): improve tenant review workspace
```
---
### Step 4 — Commit all changes
Run:
```bash
git add -A
git commit -m "<commit-message>"
```
If there are no local changes to commit, continue only if the branch has unpushed commits.
Check unpushed commits with:
```bash
git status -sb
git log --oneline origin/<current-branch>..HEAD
```
If there are no local changes and no unpushed commits, report:
```text
Es gibt keine lokalen Änderungen und keine unpushed commits. Ich erstelle keinen leeren Commit.
```
Then continue to PR creation only if the branch already exists remotely or can be pushed.
---
### Step 5 — Push branch
Run:
```bash
git push --set-upstream origin <current-branch>
```
If the upstream already exists, this is acceptable.
Never force-push unless the user explicitly requests it.
---
### Step 6 — Create PR into platform-dev via Gitea MCP
Use Gitea MCP to create a pull request:
```json
{
"owner": "ahmido",
"repo": "TenantAtlas",
"head": "<current-feature-branch>",
"base": "platform-dev",
"title": "<commit-message>",
"body": "Implements platform feature branch `<current-feature-branch>`.\n\nTarget branch: `platform-dev`.\n\nFollow-up integration path after merge:\n\n`platform-dev``dev`."
}
```
If a PR already exists for the same branch and base, do not create a duplicate.
Report the existing PR if available.
---
## Optional Step — Check platform-dev to dev PR
After creating the feature PR, check whether an open integration PR exists:
```text
platform-dev → dev
```
If a Gitea MCP list/search pull request function is available, use it.
If one exists, report:
```text
Der Folge-PR `platform-dev``dev` existiert bereits: <url>
```
If none exists, report:
```text
Nach dem Merge dieses Feature-PRs sollte der Integrations-PR `platform-dev``dev` erstellt oder aktualisiert werden.
```
Do not automatically create the `platform-dev``dev` PR unless the user explicitly asks for it.
Reason: before the feature PR is merged into `platform-dev`, the integration PR may not include the new feature yet.
---
## Integration Refresh Mode
Use this mode when the user explicitly says one of the following:
- "platform-dev nach dev vorbereiten"
- "platform-dev PR aktualisieren"
- "out-of-date mit dev beheben"
- "integration PR refresh"
- "platform-dev auf dev rebasen"
- "auch platform-dev nach dev"
- "und danach platform-dev nach dev"
- "full integration"
- "kompletten platform-dev zu dev PR machen"
- "folge-pr erstellen"
This mode prepares or updates the integration PR:
```text
platform-dev → dev
```
Because the user works alone on `platform-dev`, prefer rebase over merge.
### Integration Refresh Preconditions
Before running this mode:
1. Ensure the working tree is clean.
2. Ensure there are no unresolved conflicts.
3. Fetch remote branches.
4. Ensure `origin/platform-dev` exists.
5. Ensure `origin/dev` exists.
If the working tree is dirty, STOP and report:
```text
Der Working Tree ist nicht sauber. Bitte erst Änderungen committen, stashen oder verwerfen, bevor `platform-dev` auf `dev` rebased wird.
```
If unresolved conflicts exist, STOP and report the conflict files.
### Integration Refresh Workflow
Run:
```bash
git fetch origin
git checkout platform-dev
git reset --hard origin/platform-dev
git rebase origin/dev
git push --force-with-lease origin platform-dev
```
After pushing, verify that `origin/dev` is now an ancestor of `origin/platform-dev`:
```bash
git fetch origin
git merge-base --is-ancestor origin/dev origin/platform-dev \
&& echo "OK: platform-dev contains dev" \
|| echo "OUTDATED: platform-dev does not contain dev"
```
If the verification prints `OUTDATED`, stop and report it. Do not claim the PR is up-to-date.
Rules:
- Do not merge `origin/dev` into `platform-dev` for this refresh.
- Do not create repeated merge commits from `origin/dev` into `platform-dev`.
- Use `git push --force-with-lease origin platform-dev` after a successful rebase.
- Never use plain `git push --force`.
- If `git rebase origin/dev` reports conflicts, stop immediately.
- Do not continue to PR creation while a rebase is unresolved.
- Do not auto-merge the PR.
- Do not claim Gitea will remove the out-of-date warning unless the ancestor check succeeds.
If rebase conflicts occur, report:
```text
Rebase-Konflikte erkannt. Ich habe gestoppt.
Konfliktdateien:
<files>
Bitte Konflikte lösen, dann `git rebase --continue` ausführen oder den Rebase mit `git rebase --abort` abbrechen.
```
### Create or Report Integration PR
After the rebase, push, and ancestor verification succeeded, use Gitea MCP to create or report the integration PR:
```json
{
"owner": "ahmido",
"repo": "TenantAtlas",
"head": "platform-dev",
"base": "dev",
"title": "chore(platform): merge platform-dev into dev",
"body": "Integrates latest TenantPilot platform changes from `platform-dev` into `dev`.\n\nThis PR was created by agent on user request; do not merge automatically."
}
```
If an open PR already exists for `platform-dev``dev`, do not create a duplicate. Report the existing PR.
### Integration Refresh Reporting Format
Final response for this mode must include:
```text
Fertig.
- Branch aktualisiert: platform-dev
- Refresh-Methode: rebase auf origin/dev
- Ancestor-Check: origin/dev ist Ancestor von origin/platform-dev
- Push: --force-with-lease origin/platform-dev
- Integration PR: <url>
- Base: dev
- Hinweis: PR wurde nicht automatisch gemerged.
```
Do not claim tests passed unless they were actually executed.
---
## Reporting Format
Final response must be concise and include:
```text
Fertig.
- Branch: <branch>
- Commit: <commit-sha or "keine neuen Änderungen">
- Push: origin/<branch>
- PR: <url>
- Base: platform-dev
- Nächster Schritt: Nach Merge `platform-dev``dev` PR aktualisieren/erstellen
```
If tests were not run, say:
```text
Tests wurden in diesem Skill nicht automatisch ausgeführt.
```
Do not claim tests passed unless the tool actually ran them.
---
## Safety Rules
- Never commit directly to `dev`, `platform-dev`, `website-dev`, `main`, or `master`.
- Never force-push unless explicitly requested.
- For Integration Refresh Mode only, `git push --force-with-lease origin platform-dev` is allowed because the user works alone on `platform-dev`; never use plain `--force`.
- Never auto-merge PRs unless explicitly requested.
- Never target `dev` directly for platform feature PRs unless explicitly requested.
- Never delete branches unless explicitly requested.
- Never claim tests were run unless the tool actually ran them.
- Never commit `.env`, secrets, local tokens, local mock-server configuration, or temporary runtime-only changes.
- If migrations were created, mention that the target environment needs migration execution after deployment.
- If unresolved conflicts exist, stop.
---
## Useful Commands
Inspect:
```bash
git rev-parse --show-toplevel
git rev-parse --abbrev-ref HEAD
git status --porcelain
git status -sb
git config --get remote.origin.url
```
Detect protected branch:
```bash
branch="$(git rev-parse --abbrev-ref HEAD)"
case "$branch" in
dev|platform-dev|website-dev|main|master)
echo "PROTECTED_BRANCH:$branch"
exit 2
;;
esac
```
Detect unresolved conflicts:
```bash
git diff --name-only --diff-filter=U
```
Detect `.env` changes:
```bash
git status --porcelain | grep -E '(^.. \.env$|^.. apps/platform/\.env$|^.. .*\.env$)' || true
```
Commit:
```bash
git add -A
git commit -m "<message>"
```
Push:
```bash
git push --set-upstream origin "$(git rev-parse --abbrev-ref HEAD)"
```
Latest commit:
```bash
git rev-parse --short HEAD
git log -1 --pretty=%s
```
Integration refresh:
```bash
git fetch origin
git checkout platform-dev
git reset --hard origin/platform-dev
git rebase origin/dev
git push --force-with-lease origin platform-dev
```
Verify integration refresh:
```bash
git fetch origin
git merge-base --is-ancestor origin/dev origin/platform-dev \
&& echo "OK: platform-dev contains dev" \
|| echo "OUTDATED: platform-dev does not contain dev"
```
Check rebase conflicts:
```bash
git diff --name-only --diff-filter=U
```
---
## Example User Request
User:
```text
alles committen pushen und pr gegen platform-dev mit gitea mcp
```
Assistant should:
1. Check current branch.
2. Stop if branch is protected.
3. Stop if `.env` or secrets would be committed.
4. Commit all changes.
5. Push current branch.
6. Create PR into `platform-dev` with Gitea MCP.
7. Report result.
Do not ask unnecessary follow-up questions.
---
## Example Integration Refresh Request
User:
```text
platform-dev PR aktualisieren
```
Assistant should:
1. Ensure the working tree is clean.
2. Fetch origin.
3. Checkout `platform-dev`.
4. Reset local `platform-dev` to `origin/platform-dev`.
5. Rebase `platform-dev` onto `origin/dev`.
6. Push with `--force-with-lease`.
7. Verify `origin/dev` is an ancestor of `origin/platform-dev`.
8. Create or report the PR `platform-dev``dev`.
9. Report result.
Do not merge the PR automatically.
The helper requires an absolute repository root, full reviewed HEAD, exact explicit
finalization paths, a non-empty message, and the exact SHA-256 finalization
fingerprint. Renames require both old and new paths. The real index must be
semantically empty before stable validator evidence and before finalization.
No other staging or commit path is authorized.
## Remote Boundary
Remote repository operations, pull-request actions, integration-branch refreshes,
and history rewrites require their own explicit request and a separately governed
workflow. They are not a continuation of local finalization authority.
## Stop Conditions
Stop without mutation when:
- the active spec, branch family, base, target, or baseline is ambiguous;
- unrelated dirty work overlaps the candidate;
- validation is missing or failing;
- independent review is missing or has unresolved in-scope findings;
- review identity or finalization identity drifted;
- the current branch is protected or detached;
- the index is pre-populated;
- a conflict or Git operation is in progress;
- a sensitive, broad, directory, glob, traversal, or outside-repository path is
requested;
- remote behavior or scope expansion would be required.
## Result Contract
Report:
- Branch and reviewed HEAD
- Resolved baseline
- Explicit review paths and explicit finalization paths
- Validation and review result
- Review fingerprint, finalization fingerprint, and local commit OID, or refusal
code
- `helper_remote_commands=0`
- `Push: NOT PERFORMED`
- `Pull Request: NOT PERFORMED`
Do not claim tests, review, commit creation, or remote state changes that were not
actually observed.

View File

@ -75,6 +75,48 @@ ### Gate-Regel
- Wenn Code geändert wird (z.B. `app/`, `config/`, `database/`, `resources/`),
muss der PR auch `specs/<NNN>-<slug>/` enthalten oder aktualisieren.
## Codex Agent Foundation
Repository-governance work under Spec 458 uses exactly five project roles:
- `code_explorer` discovers scope, applicable instructions, relevant evidence,
dependencies, skills, risks, and hard-gate stop conditions without editing.
- `implementer` consumes that brief and changes only the active-spec allowlist.
- `test_validator` independently runs proportionate checks and reports concrete
commands, results, failures, and the candidate identity it validated.
- `code_reviewer` independently reviews the exact candidate and validator evidence
without editing or fixing its own findings.
- `git_finalizer` may create one local commit only through
`scripts/codex-safe-local-commit` after separate explicit user authority.
The ordered handoff is explorer → implementer → validator → reviewer → finalizer.
Validation must finish before review begins. The validator handoff must bind two
deterministic identities: the full review identity for the resolved
baseline-to-candidate tree, and the helper-compatible finalization identity for the
exact delta from the reviewed HEAD. The finalization identity uses explicit paths,
includes both sides of renames, requires a semantically empty real index, and
carries the helper fingerprint emitted by `scripts/codex-safe-local-commit`.
Reviewer findings must state severity, affected artifact or location, impact,
evidence, and the smallest bounded correction. A correction reruns the affected
validation and independent review; after three unsuccessful cycles, or when the
required correction expands scope, stop and escalate instead of widening the
feature silently.
Role sandbox and approval declarations are defense-in-depth defaults. A parent
session can apply more permissive live runtime overrides, so they are not absolute
security boundaries. Git mutation remains mechanically bounded by the helper:
explicit authority for the specific local commit, exact paths including both sides
of a rename, matching HEAD and finalization helper fingerprint, and no
pre-existing staged changes.
The helper and all five roles have no authority to push, fetch, pull, create or
update a pull request, merge, rebase, reset, clean, amend, force-update, or mutate
remote state.
This topology does not certify critical domain, security, destructive product,
production, or remote Git work. All applicable repository and manual hard gates
still apply.
## Multi-Agent Coordination
**Problem:** Multiple AI agents working simultaneously on the same branch can create conflicts and confusion.

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
use Symfony\Component\Process\Process;
it('proves critical safe local commit success and refusal behavior', function (): void {
$suite = repo_path('scripts/tests/codex-safe-local-commit-test');
$helper = repo_path('scripts/codex-safe-local-commit');
expect(is_file($suite))->toBeTrue()
->and(is_executable($suite))->toBeTrue()
->and(is_file($helper))->toBeTrue()
->and(is_executable($helper))->toBeTrue();
$syntax = new Process(['bash', '-n', $helper], repo_root());
$syntax->setTimeout(15);
$syntax->run();
expect($syntax->isSuccessful())
->toBeTrue($syntax->getErrorOutput());
$process = new Process([$suite, '--critical'], repo_root(), [
'PATH' => getenv('PATH') ?: '/usr/bin:/bin',
'HOME' => sys_get_temp_dir(),
'GIT_CONFIG_NOSYSTEM' => '1',
]);
$process->setTimeout(90);
$process->run();
expect($process->isSuccessful())
->toBeTrue(trim($process->getErrorOutput()) ?: trim($process->getOutput()))
->and($process->getOutput())
->toContain(
'PASS tracked modification commits exactly one authorized path',
'PASS fingerprint mode is repeatable and read-only',
'PASS stale fingerprint refuses without mutation',
'PASS hook rejection preserves real state',
'result=passed passed=4 failed=0',
);
});

View File

@ -83,7 +83,7 @@ function spec439ConstitutionUnrelatedBody(string $contents): string
});
it('keeps authoritative repository branch instructions synchronized', function (): void {
$instructions = (string) file_get_contents(repo_path('Agents.md'));
$instructions = (string) file_get_contents(repo_path('AGENTS.md'));
expect($instructions)
->toContain(

View File

@ -99,7 +99,7 @@ function productSurfaceGateAssertContains(string $relativePath, array $needles):
'stopping before runtime UI edits',
]);
productSurfaceGateAssertContains('Agents.md', [
productSurfaceGateAssertContains('AGENTS.md', [
'Product Surface Contract Gate',
'focused browser proof, or `N/A - no rendered UI surface changed`',
'implementation-report fields for tests, browser/no-browser, Livewire v4',

View File

@ -464,7 +464,7 @@ ### Docker services ([docker-compose.yml](docker-compose.yml))
### Deployment: Dokploy (staging → production)
- **No Dokploy config in repo** — configuration lives on the VPS per [Agents.md](Agents.md)
- **No Dokploy config in repo** — configuration lives on the VPS per [AGENTS.md](../AGENTS.md)
- **Two environments**: Staging (mandatory gate) → Production
- **Required env vars** (partial — no `.env.example`):
- Standard Laravel: `APP_KEY`, `DB_*`, `REDIS_*`, `QUEUE_CONNECTION`
@ -491,7 +491,7 @@ ## Risks & Gaps (Top 10)
| 4 | **Exception/risk-acceptance workflow incomplete** | `Finding.STATUS_RISK_ACCEPTED` exists, no exception entity | Can't track formal risk acceptances with evidence | Spec needed for `FindingException` model with approval chain |
| 5 | **SQLite for tests vs PostgreSQL in prod** | [phpunit.xml](phpunit.xml) | JSONB, GIN indexes, PG-specific features untested | Add [phpunit.pgsql.xml](phpunit.pgsql.xml) — file exists but may not be in CI |
| 6 | **No formal release process** | Roadmap only in brainstorming doc | No versioned release artifacts, no changelog | Define release cadence and changelog automation |
| 7 | **Dokploy config external** | Per [Agents.md](Agents.md) | Deployment not reproducible from repo alone | Document or codify Dokploy config |
| 7 | **Dokploy config external** | Per [AGENTS.md](../AGENTS.md) | Deployment not reproducible from repo alone | Document or codify Dokploy config |
| 8 | **Cross-tenant compare unimplemented** | Spec 043 draft only | Key MSP feature missing | Prioritize for next sprint if MSP use case is active |
| 9 | **Policy lifecycle / ghost policies** | Spec 900 draft only | Deleted-in-Intune policies persist indefinitely | Implement orphan detection + soft-archive flow |
| 10 | **Break-glass recovery surface** | Config-gated, TTL-limited | Limited testing evidence | Verify break-glass flow has smoke tests in test suite |
@ -613,7 +613,7 @@ ### Key File Map (Top 20 files to read first)
| File | Why |
|------|-----|
| [.specify/memory/constitution.md](.specify/memory/constitution.md) | Non-negotiable architectural rules |
| [Agents.md](Agents.md) | Agent workflow, branching, environment |
| [AGENTS.md](../AGENTS.md) | Agent workflow, branching, environment |
| [config/tenantpilot.php](config/tenantpilot.php) | 28+ policy types, feature flags, all config |
| [config/graph_contracts.php](config/graph_contracts.php) | Graph API contract registry (867 lines) |
| [app/Support/Auth/Capabilities.php](app/Support/Auth/Capabilities.php) | Canonical RBAC capability registry |

View File

@ -56,7 +56,7 @@
- Code formatting via Laravel Pint is used in CI and local flows.
**Specs & Process:**
- Uses Spec Kit convention: each feature in `specs/<NNN>-<slug>/` (spec.md, plan.md, tasks.md). Branching conventions documented in `Agents.md`/`.specify`.
- Uses Spec Kit convention: each feature in `specs/<NNN>-<slug>/` (spec.md, plan.md, tasks.md). Branching conventions documented in `AGENTS.md`/`.specify`.
- Default integration branch: `dev`. Feature branches are `feat/<NNN>-<slug>`.
**Recent Notable Work (context):**

737
scripts/codex-safe-local-commit Executable file
View File

@ -0,0 +1,737 @@
#!/usr/bin/env bash
set -euo pipefail
mode="${1:-}"
[[ $# -gt 0 ]] && shift
repository_argument=""
expected_head=""
expected_fingerprint=""
commit_message=""
requested_paths=()
authorized_paths=()
temporary_paths=()
index_lock_path=""
index_lock_owned=0
published_ref=""
published_commit=""
publication_reconciled=0
publication_phase="none"
original_index_backup=""
published_index_hash=""
result_emitted=0
repository=""
branch_name=""
branch_ref=""
candidate_index=""
candidate_tree=""
candidate_fingerprint=""
temporary_file=""
temporary_directory=""
isolated_repository=""
emit_paths() {
local path
for path in "${authorized_paths[@]:-}"; do
printf 'authorized_path=%s\n' "${path}"
done
}
emit_refusal() {
local code="$1"
printf 'result=refused\n'
printf 'refusal_code=%s\n' "${code}"
[[ -n "${expected_head}" ]] && printf 'reviewed_head=%s\n' "${expected_head}"
[[ -n "${candidate_fingerprint}" ]] && printf 'candidate_sha256=%s\n' "${candidate_fingerprint}"
emit_paths
printf 'helper_remote_commands=0\n'
result_emitted=1
}
cleanup() {
local path current_index_hash real_index rollback_lock
if [[ -n "${published_ref}" && -n "${published_commit}" && ${publication_reconciled} -eq 0 ]]; then
real_index="$(git_path index)"
if [[
"${publication_phase}" == "index-installed" &&
-n "${original_index_backup}" &&
-f "${original_index_backup}" &&
-n "${published_index_hash}" &&
-f "${real_index}"
]]; then
current_index_hash="$(sha256_file "${real_index}")"
if [[ "${current_index_hash}" == "${published_index_hash}" ]]; then
rollback_lock="${real_index}.lock"
if (
set -o noclobber
: >"${rollback_lock}"
) 2>/dev/null; then
if cp -p "${original_index_backup}" "${rollback_lock}" >/dev/null 2>&1; then
mv -f -- "${rollback_lock}" "${real_index}" >/dev/null 2>&1 || true
else
rm -f -- "${rollback_lock}"
fi
fi
fi
fi
git -C "${repository}" update-ref \
"${published_ref}" "${expected_head}" "${published_commit}" \
>/dev/null 2>&1 || true
fi
if [[ ${index_lock_owned} -eq 1 && -n "${index_lock_path}" ]]; then
rm -f -- "${index_lock_path}"
fi
for path in "${temporary_paths[@]:-}"; do
[[ -n "${path}" ]] && rm -rf -- "${path}"
done
}
on_exit() {
local status=$?
cleanup
if [[ ${status} -ne 0 && ${result_emitted} -eq 0 ]]; then
emit_refusal internal-error
fi
}
trap on_exit EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
refuse() {
emit_refusal "$1"
exit 2
}
sha256_file() {
local path="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${path}" | awk '{ print $1 }'
else
shasum -a 256 "${path}" | awk '{ print $1 }'
fi
}
git_path() {
local path
path="$(git -C "${repository}" rev-parse --git-path "$1")"
if [[ "${path}" == /* ]]; then
printf '%s\n' "${path}"
else
printf '%s\n' "${repository}/${path}"
fi
}
new_temporary_file() {
temporary_file="$(mktemp "${TMPDIR:-/tmp}/codex-safe-local-commit.XXXXXX")"
temporary_paths+=("${temporary_file}")
}
new_temporary_directory() {
temporary_directory="$(mktemp -d "${TMPDIR:-/tmp}/codex-safe-local-commit.XXXXXX")"
temporary_paths+=("${temporary_directory}")
}
path_is_authorized() {
local candidate="$1"
local path
for path in "${authorized_paths[@]}"; do
[[ "${path}" == "${candidate}" ]] && return 0
done
return 1
}
path_exists_with_exact_casing() {
local path="$1"
local parent base
parent="${path%/*}"
[[ "${parent}" != "${path}" ]] || parent="."
base="${path##*/}"
find "${repository}/${parent}" -maxdepth 1 -mindepth 1 -name "${base}" -print -quit 2>/dev/null |
awk 'length($0) > 0 { found = 1 } END { exit(found ? 0 : 1) }'
}
apply_case_only_renames() {
local old_path new_path old_lower new_lower
local count=${#authorized_paths[@]}
local i j
for ((i = 0; i < count; i++)); do
old_path="${authorized_paths[i]}"
old_lower="$(printf '%s' "${old_path}" | tr '[:upper:]' '[:lower:]')"
for ((j = 0; j < count; j++)); do
[[ ${i} -ne ${j} ]] || continue
new_path="${authorized_paths[j]}"
new_lower="$(printf '%s' "${new_path}" | tr '[:upper:]' '[:lower:]')"
[[ "${old_path}" != "${new_path}" && "${old_lower}" == "${new_lower}" ]] || continue
git -C "${repository}" cat-file -e "${expected_head}:${old_path}" 2>/dev/null ||
continue
path_exists_with_exact_casing "${new_path}" ||
continue
GIT_INDEX_FILE="${candidate_index}" \
git -C "${repository}" update-index --force-remove -- "${old_path}" >/dev/null
GIT_INDEX_FILE="${candidate_index}" \
git -C "${repository}" add -- "${new_path}" >/dev/null
done
done
}
assert_index_unstaged() {
local record index_status
while IFS= read -r -d '' record; do
case "${record}" in
"1 "*)
index_status="${record:2:1}"
[[ "${index_status}" == "." ]] || refuse staged-index
;;
"2 "*)
index_status="${record:2:1}"
[[ "${index_status}" == "." ]] || refuse staged-index
IFS= read -r -d '' record || true
;;
"u "*)
refuse unresolved-conflicts
;;
esac
done < <(git -C "${repository}" status --porcelain=v2 -z --untracked-files=all)
}
assert_no_git_operation() {
local marker marker_path
for marker in \
MERGE_HEAD \
CHERRY_PICK_HEAD \
REVERT_HEAD \
BISECT_LOG \
rebase-apply \
rebase-merge; do
marker_path="$(git_path "${marker}")"
[[ ! -e "${marker_path}" ]] || refuse git-operation-in-progress
done
[[ -z "$(git -C "${repository}" ls-files -u)" ]] || refuse unresolved-conflicts
}
assert_head_matches() {
local current_head
current_head="$(git -C "${repository}" rev-parse HEAD)"
[[ "${current_head}" == "${expected_head}" ]] || refuse stale-head
}
normalize_and_validate_paths() {
local raw path parent parent_real base existing
[[ ${#requested_paths[@]} -gt 0 ]] || refuse missing-paths
for raw in "${requested_paths[@]}"; do
[[ -n "${raw}" ]] || refuse empty-path
[[ "${raw}" != *$'\n'* && "${raw}" != *$'\r'* ]] || refuse newline-path
[[ "${raw}" != /* ]] || refuse absolute-path
path="${raw}"
while [[ "${path}" == ./* ]]; do
path="${path#./}"
done
[[ -n "${path}" && "${path}" != "." ]] || refuse broad-path
[[ "${path}" != *"//"* ]] || refuse path-traversal
[[ "/${path}/" != *"/../"* && "/${path}/" != *"/./"* ]] || refuse path-traversal
[[ "${path}" != ".git" && "/${path}/" != *"/.git/"* ]] || refuse git-internal-path
[[ "${path}" != *"*"* && "${path}" != *"?"* && "${path}" != *"["* ]] || refuse unresolved-glob
base="${path##*/}"
case "${base}" in
.env|.env.*|*.pem|*.key|id_rsa|id_ed25519)
refuse sensitive-path
;;
esac
for existing in "${authorized_paths[@]:-}"; do
[[ "${existing}" != "${path}" ]] || refuse duplicate-path
done
[[ ! -d "${repository}/${path}" ]] || refuse directory-path
if [[ ! -e "${repository}/${path}" && ! -L "${repository}/${path}" ]]; then
git -C "${repository}" ls-files --error-unmatch -- "${path}" >/dev/null 2>&1 ||
refuse path-not-found
fi
parent="${path%/*}"
[[ "${parent}" != "${path}" ]] || parent="."
while [[
"${parent}" != "." &&
! -e "${repository}/${parent}" &&
! -L "${repository}/${parent}"
]]; do
if [[ "${parent}" == */* ]]; then
parent="${parent%/*}"
else
parent="."
fi
done
parent_real="$(cd "${repository}/${parent}" 2>/dev/null && pwd -P)" ||
refuse path-outside-repository
[[ "${parent_real}" == "${repository}" || "${parent_real}" == "${repository}/"* ]] ||
refuse path-outside-repository
authorized_paths+=("${path}")
done
local sorted_paths
new_temporary_file
sorted_paths="${temporary_file}"
printf '%s\n' "${authorized_paths[@]}" | LC_ALL=C sort >"${sorted_paths}"
authorized_paths=()
while IFS= read -r path; do
authorized_paths+=("${path}")
done <"${sorted_paths}"
}
collect_all_changed_paths() {
local path
while IFS= read -r -d '' path; do
printf '%s\0' "${path}"
done < <(
{
git -C "${repository}" diff --name-only -z HEAD --
git -C "${repository}" ls-files --others --exclude-standard -z
}
)
}
assert_rename_authority() {
local full_index all_paths_file status old_path new_path path
local old_authorized new_authorized
local all_paths=()
while IFS= read -r -d '' path; do
all_paths+=("${path}")
done < <(collect_all_changed_paths)
[[ ${#all_paths[@]} -gt 0 ]] || return 0
new_temporary_file
full_index="${temporary_file}"
rm -f -- "${full_index}"
GIT_INDEX_FILE="${full_index}" git -C "${repository}" read-tree "${expected_head}"
GIT_INDEX_FILE="${full_index}" git -C "${repository}" add -- "${all_paths[@]}"
while IFS= read -r -d '' status; do
case "${status}" in
R*|C*)
IFS= read -r -d '' old_path
IFS= read -r -d '' new_path
old_authorized=0
new_authorized=0
path_is_authorized "${old_path}" && old_authorized=1
path_is_authorized "${new_path}" && new_authorized=1
if [[ ${old_authorized} -ne ${new_authorized} ]]; then
refuse rename-authorization-required
fi
;;
*)
IFS= read -r -d '' path
;;
esac
done < <(
GIT_INDEX_FILE="${full_index}" \
git -C "${repository}" diff --cached --name-status -z --find-renames "${expected_head}" --
)
}
build_candidate() {
local raw_diff manifest status old_path new_path path
local seen_paths=()
new_temporary_file
candidate_index="${temporary_file}"
rm -f -- "${candidate_index}"
GIT_INDEX_FILE="${candidate_index}" git -C "${repository}" read-tree "${expected_head}"
if ! GIT_INDEX_FILE="${candidate_index}" \
git -C "${repository}" add -- "${authorized_paths[@]}" >/dev/null 2>&1; then
refuse candidate-stage-failed
fi
apply_case_only_renames
while IFS= read -r -d '' status; do
case "${status}" in
R*|C*)
IFS= read -r -d '' old_path
IFS= read -r -d '' new_path
path_is_authorized "${old_path}" || refuse unauthorized-candidate-path
path_is_authorized "${new_path}" || refuse unauthorized-candidate-path
seen_paths+=("${old_path}" "${new_path}")
;;
*)
IFS= read -r -d '' path
path_is_authorized "${path}" || refuse unauthorized-candidate-path
seen_paths+=("${path}")
;;
esac
done < <(
GIT_INDEX_FILE="${candidate_index}" \
git -C "${repository}" diff --cached --name-status -z --find-renames "${expected_head}" --
)
[[ ${#seen_paths[@]} -gt 0 ]] || refuse no-authorized-changes
for path in "${authorized_paths[@]}"; do
local seen=0
local changed_path
for changed_path in "${seen_paths[@]}"; do
[[ "${path}" != "${changed_path}" ]] || seen=1
done
[[ ${seen} -eq 1 ]] || refuse unchanged-authorized-path
done
candidate_tree="$(
GIT_INDEX_FILE="${candidate_index}" git -C "${repository}" write-tree
)"
new_temporary_file
raw_diff="${temporary_file}"
GIT_INDEX_FILE="${candidate_index}" \
git -C "${repository}" diff --cached --raw -z --find-renames "${expected_head}" -- \
>"${raw_diff}"
new_temporary_file
manifest="${temporary_file}"
{
printf 'codex-safe-local-commit-v1\0'
printf 'head\0%s\0' "${expected_head}"
printf 'tree\0%s\0' "${candidate_tree}"
for path in "${authorized_paths[@]}"; do
printf 'path\0%s\0' "${path}"
done
printf 'raw-diff\0'
cat "${raw_diff}"
} >"${manifest}"
candidate_fingerprint="$(sha256_file "${manifest}")"
}
assert_commit_shape() {
local commit_repository="$1"
local commit="$2"
local parent tree status old_path new_path path
parent="$(git -C "${commit_repository}" rev-parse "${commit}^")"
tree="$(git -C "${commit_repository}" rev-parse "${commit}^{tree}")"
[[ "${parent}" == "${expected_head}" ]] || refuse invalid-commit-parent
[[ "${tree}" == "${candidate_tree}" ]] || refuse post-hook-candidate-drift
while IFS= read -r -d '' status; do
case "${status}" in
R*|C*)
IFS= read -r -d '' old_path
IFS= read -r -d '' new_path
path_is_authorized "${old_path}" || refuse post-hook-unauthorized-path
path_is_authorized "${new_path}" || refuse post-hook-unauthorized-path
;;
*)
IFS= read -r -d '' path
path_is_authorized "${path}" || refuse post-hook-unauthorized-path
;;
esac
done < <(
git -C "${commit_repository}" diff-tree --root --no-commit-id --name-status -z --find-renames -r "${commit}" --
)
}
source_hooks_path() {
local configured hooks_path
configured="$(git -C "${repository}" config --get core.hooksPath || true)"
if [[ -n "${configured}" ]]; then
if [[ "${configured}" == /* ]]; then
hooks_path="${configured}"
else
hooks_path="${repository}/${configured}"
fi
else
hooks_path="$(git_path hooks)"
fi
printf '%s\n' "$(cd "$(dirname "${hooks_path}")" && pwd -P)/$(basename "${hooks_path}")"
}
copy_source_identity_config() {
local key value
for key in user.name user.email commit.gpgsign gpg.format user.signingkey; do
value="$(git -C "${repository}" config --get "${key}" || true)"
[[ -n "${value}" ]] || continue
git -C "${isolated_repository}" config "${key}" "${value}"
done
if [[ -z "$(git -C "${isolated_repository}" config --get user.name || true)" ]]; then
git -C "${isolated_repository}" config user.name "Codex Safe Local Commit"
fi
if [[ -z "$(git -C "${isolated_repository}" config --get user.email || true)" ]]; then
git -C "${isolated_repository}" config user.email "codex-safe-local-commit@example.invalid"
fi
}
prepare_isolated_repository() {
local source_objects alternates_file hooks_path
new_temporary_directory
isolated_repository="${temporary_directory}/isolated-repository"
mkdir -p "${isolated_repository}"
git -C "${isolated_repository}" init -b codex-candidate >/dev/null
source_objects="$(git_path objects)"
alternates_file="$(git -C "${isolated_repository}" rev-parse --git-path objects/info/alternates)"
if [[ "${alternates_file}" != /* ]]; then
alternates_file="${isolated_repository}/${alternates_file}"
fi
mkdir -p "$(dirname "${alternates_file}")"
printf '%s\n' "${source_objects}" >"${alternates_file}"
copy_source_identity_config
git -C "${isolated_repository}" update-ref refs/heads/codex-candidate "${expected_head}"
git -C "${isolated_repository}" checkout -f codex-candidate >/dev/null 2>&1
hooks_path="$(source_hooks_path)"
git -C "${isolated_repository}" config core.hooksPath "${hooks_path}"
git -C "${isolated_repository}" read-tree --reset -u "${candidate_tree}"
}
import_verified_object_closure() {
local commit="$1"
if ! git -C "${isolated_repository}" rev-list --objects "${commit}" "^${expected_head}" |
git -C "${isolated_repository}" pack-objects --stdout |
git -C "${repository}" unpack-objects -q; then
refuse object-import-failed
fi
git -C "${repository}" cat-file -e "${commit}^{commit}" 2>/dev/null ||
refuse object-import-failed
}
publish_commit() {
local commit="$1"
local real_index index_before publish_index current_fingerprint current_tree
real_index="$(git_path index)"
[[ -f "${real_index}" ]] || refuse missing-index
index_before="$(sha256_file "${real_index}")"
new_temporary_file
original_index_backup="${temporary_file}"
cp -p "${real_index}" "${original_index_backup}"
index_lock_path="${real_index}.lock"
(
set -o noclobber
: >"${index_lock_path}"
) 2>/dev/null || refuse index-lock-unavailable
index_lock_owned=1
assert_head_matches
assert_index_unstaged
[[ "$(sha256_file "${real_index}")" == "${index_before}" ]] || refuse index-drift
build_candidate
current_fingerprint="${candidate_fingerprint}"
current_tree="${candidate_tree}"
[[ "${current_fingerprint}" == "${expected_fingerprint}" ]] || refuse candidate-drift
[[ "$(git -C "${repository}" rev-parse "${commit}^{tree}")" == "${current_tree}" ]] ||
refuse candidate-drift
new_temporary_file
publish_index="${temporary_file}"
rm -f -- "${publish_index}"
GIT_INDEX_FILE="${publish_index}" git -C "${repository}" read-tree "${commit}^{tree}"
published_index_hash="$(sha256_file "${publish_index}")"
cp -p "${publish_index}" "${index_lock_path}"
published_ref="${branch_ref}"
published_commit="${commit}"
publication_phase="prepared"
trap '' INT TERM
if ! git -C "${repository}" update-ref "${branch_ref}" "${commit}" "${expected_head}"; then
refuse head-changed-before-publication
fi
publication_phase="ref-published"
if [[ "${CODEX_TEST_FAIL_AFTER_UPDATE_REF:-0}" == "1" ]]; then
refuse injected-after-update-ref
fi
if ! mv -f -- "${index_lock_path}" "${real_index}"; then
refuse index-reconciliation-failed
fi
index_lock_owned=0
publication_phase="index-installed"
if [[ "${CODEX_TEST_FAIL_AFTER_INDEX_INSTALL:-0}" == "1" ]]; then
refuse injected-after-index-install
fi
publication_reconciled=1
}
case "${mode}" in
fingerprint|commit)
;;
*)
refuse invalid-mode
;;
esac
while [[ $# -gt 0 ]]; do
case "$1" in
--repo)
[[ $# -ge 2 ]] || refuse missing-repo
repository_argument="$2"
shift 2
;;
--expected-head)
[[ $# -ge 2 ]] || refuse missing-expected-head
expected_head="$2"
shift 2
;;
--expected-diff-sha256)
[[ $# -ge 2 ]] || refuse missing-fingerprint
expected_fingerprint="$2"
shift 2
;;
--message)
[[ $# -ge 2 ]] || refuse missing-message
commit_message="$2"
shift 2
;;
--path)
[[ $# -ge 2 ]] || refuse empty-path
requested_paths+=("$2")
shift 2
;;
*)
refuse unknown-option
;;
esac
done
[[ -n "${repository_argument}" ]] || refuse missing-repo
[[ "${repository_argument}" == /* ]] || refuse repository-not-absolute
[[ -d "${repository_argument}" ]] || refuse repository-not-found
repository="$(cd "${repository_argument}" && pwd -P)"
repository_top="$(git -C "${repository}" rev-parse --show-toplevel 2>/dev/null)" ||
refuse not-a-git-repository
repository_top="$(cd "${repository_top}" && pwd -P)"
[[ "${repository_top}" == "${repository}" ]] || refuse repository-root-mismatch
[[ "$(git -C "${repository}" rev-parse --is-bare-repository)" == "false" ]] ||
refuse bare-repository
[[ "${expected_head}" =~ ^[0-9a-f]{40}$|^[0-9a-f]{64}$ ]] ||
refuse invalid-expected-head
git -C "${repository}" cat-file -e "${expected_head}^{commit}" 2>/dev/null ||
refuse unknown-expected-head
branch_name="$(git -C "${repository}" symbolic-ref --quiet --short HEAD 2>/dev/null)" ||
refuse detached-head
branch_ref="refs/heads/${branch_name}"
case "${branch_name}" in
dev|platform-dev|website-dev|main|master|release/*|production/*|prod/*)
refuse protected-branch
;;
esac
while IFS= read -r protected_pattern; do
[[ -z "${protected_pattern}" ]] && continue
[[ "${branch_name}" != ${protected_pattern} ]] || refuse protected-branch
done < <(git -C "${repository}" config --get-all codex.protectedBranchPattern || true)
assert_no_git_operation
assert_index_unstaged
assert_head_matches
normalize_and_validate_paths
assert_rename_authority
build_candidate
if [[ "${mode}" == "fingerprint" ]]; then
printf 'result=fingerprint\n'
printf 'reviewed_head=%s\n' "${expected_head}"
printf 'candidate_sha256=%s\n' "${candidate_fingerprint}"
emit_paths
printf 'helper_remote_commands=0\n'
result_emitted=1
exit 0
fi
[[ -n "${commit_message//[[:space:]]/}" ]] || refuse missing-message
[[ -n "${expected_fingerprint}" ]] || refuse missing-fingerprint
[[ "${expected_fingerprint}" =~ ^[0-9a-f]{64}$ ]] || refuse invalid-fingerprint
[[ "${candidate_fingerprint}" == "${expected_fingerprint}" ]] || refuse stale-fingerprint
prepare_isolated_repository
new_temporary_file
commit_output="${temporary_file}"
if ! git -C "${isolated_repository}" commit -m "${commit_message}" >"${commit_output}" 2>&1; then
refuse hook-rejected
fi
new_commit="$(git -C "${isolated_repository}" rev-parse HEAD)"
assert_commit_shape "${isolated_repository}" "${new_commit}"
import_verified_object_closure "${new_commit}"
assert_head_matches
assert_index_unstaged
reviewed_fingerprint="${candidate_fingerprint}"
reviewed_tree="${candidate_tree}"
build_candidate
[[ "${candidate_fingerprint}" == "${reviewed_fingerprint}" ]] || refuse candidate-drift
[[ "${candidate_tree}" == "${reviewed_tree}" ]] || refuse candidate-drift
[[ "${candidate_fingerprint}" == "${expected_fingerprint}" ]] || refuse candidate-drift
publish_commit "${new_commit}"
printf 'result=committed\n'
printf 'reviewed_head=%s\n' "${expected_head}"
printf 'candidate_sha256=%s\n' "${candidate_fingerprint}"
emit_paths
printf 'local_commit=%s\n' "${new_commit}"
printf 'helper_remote_commands=0\n'
result_emitted=1
trap 'exit 130' INT
trap 'exit 143' TERM
exit 0

View File

@ -0,0 +1,759 @@
#!/usr/bin/env bash
set -euo pipefail
repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)"
helper="${repository_root}/scripts/codex-safe-local-commit"
spawn_smoke=0
fixture_root=""
if [[ "${1:-}" == "--spawn-smoke" ]]; then
spawn_smoke=1
elif [[ $# -gt 0 ]]; then
echo "Usage: scripts/tests/codex-agent-reference-workflow [--spawn-smoke]" >&2
exit 2
fi
cleanup() {
[[ -n "${fixture_root}" && -d "${fixture_root}" ]] && rm -rf -- "${fixture_root}"
}
trap cleanup EXIT INT TERM
fixture_git() {
git -C "${fixture_repository}" "$@"
}
write_handoff() {
local order="$1"
local role="$2"
local body="$3"
{
printf 'order=%s\nrole=%s\n' \
"${order}" \
"${role}"
identity_fields
printf '%s\n' "${body}" | tr ';' '\n'
} >"${handoff_directory}/${order}-${role}.txt"
}
identity_fields() {
printf 'candidate_identity=%s\ncandidate_branch=%s\ncandidate_head=%s\ncandidate_review_baseline=%s\ncandidate_review_tree=%s\ncandidate_review_sha256=%s\ncandidate_review_path=%s\ncandidate_finalization_sha256=%s\ncandidate_finalization_path=%s\n' \
"${candidate_identity}" \
"${branch}" \
"${head}" \
"${baseline}" \
"${review_tree}" \
"${review_fingerprint}" \
"reviewed.txt" \
"${finalization_fingerprint}" \
"reviewed.txt"
}
write_authority() {
local file="$1"
{
printf 'authority=explicit\n'
identity_fields
} >"${file}"
}
write_validation() {
local file="$1"
local result="$2"
{
printf 'result=%s\n' "${result}"
identity_fields
} >"${file}"
}
write_review() {
local file="$1"
local verdict="$2"
{
printf 'verdict=%s\n' "${verdict}"
identity_fields
} >"${file}"
}
fingerprint_value() {
awk -F= '$1 == "candidate_sha256" { print $2; exit }'
}
sha256_stream() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum | awk '{ print $1 }'
else
shasum -a 256 | awk '{ print $1 }'
fi
}
repository_snapshot() {
local index_path
index_path="$(fixture_git rev-parse --git-path index)"
{
fixture_git status --porcelain=v2 -z --untracked-files=all
fixture_git show-ref --head
fixture_git config --null --get-regexp '^remote\.' || true
if [[ -f "${index_path}" ]]; then
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${index_path}"
else
shasum -a 256 "${index_path}"
fi
fi
} | sha256_stream
}
fixture_candidate_tree() {
local index_file
index_file="${fixture_root}/candidate-index"
rm -f -- "${index_file}"
GIT_INDEX_FILE="${index_file}" fixture_git read-tree "${head}"
GIT_INDEX_FILE="${index_file}" fixture_git add -- "$@"
GIT_INDEX_FILE="${index_file}" fixture_git write-tree
}
refresh_identity_for_current_candidate() {
branch="$(fixture_git branch --show-current)"
head="$(fixture_git rev-parse HEAD)"
baseline="${head}"
finalization_fingerprint="$(
"${helper}" fingerprint \
--repo "${fixture_repository}" \
--expected-head "${head}" \
--path reviewed.txt |
fingerprint_value
)"
review_tree="$(fixture_candidate_tree reviewed.txt)"
review_fingerprint="$(
printf 'review|branch=%s|head=%s|baseline=%s|paths=reviewed.txt|tree=%s' \
"${branch}" \
"${head}" \
"${baseline}" \
"${review_tree}" |
sha256_stream
)"
candidate_identity="review=${review_fingerprint};finalization=${finalization_fingerprint}"
}
finalize_with_evidence() {
local validation_file="$1"
local review_file="$2"
local authority_file="$3"
shift 3
local validation_result review_verdict authority
local validation_review validation_finalization validation_head
local review_review review_finalization review_head
local authority_review authority_finalization authority_head
local helper_repo="" helper_head="" helper_fingerprint=""
local validation_signature review_signature authority_signature
local helper_paths expected_paths
validation_result="$(required_unique_field "${validation_file}" result)" || {
printf 'result=refused refusal_code=validation-field-invalid helper_invoked=false\n'
return 2
}
review_verdict="$(required_unique_field "${review_file}" verdict)" || {
printf 'result=refused refusal_code=review-field-invalid helper_invoked=false\n'
return 2
}
authority="$(required_unique_field "${authority_file}" authority)" || {
printf 'result=refused refusal_code=authority-field-invalid helper_invoked=false\n'
return 2
}
validation_review="$(required_unique_field "${validation_file}" candidate_review_sha256)" || {
printf 'result=refused refusal_code=validation-identity-missing helper_invoked=false\n'
return 2
}
validation_finalization="$(required_unique_field "${validation_file}" candidate_finalization_sha256)" || {
printf 'result=refused refusal_code=validation-identity-missing helper_invoked=false\n'
return 2
}
validation_head="$(required_unique_field "${validation_file}" candidate_head)" || {
printf 'result=refused refusal_code=validation-identity-missing helper_invoked=false\n'
return 2
}
review_review="$(required_unique_field "${review_file}" candidate_review_sha256)" || {
printf 'result=refused refusal_code=review-identity-missing helper_invoked=false\n'
return 2
}
review_finalization="$(required_unique_field "${review_file}" candidate_finalization_sha256)" || {
printf 'result=refused refusal_code=review-identity-missing helper_invoked=false\n'
return 2
}
review_head="$(required_unique_field "${review_file}" candidate_head)" || {
printf 'result=refused refusal_code=review-identity-missing helper_invoked=false\n'
return 2
}
authority_review="$(required_unique_field "${authority_file}" candidate_review_sha256)" || {
printf 'result=refused refusal_code=authority-identity-missing helper_invoked=false\n'
return 2
}
authority_finalization="$(required_unique_field "${authority_file}" candidate_finalization_sha256)" || {
printf 'result=refused refusal_code=authority-identity-missing helper_invoked=false\n'
return 2
}
authority_head="$(required_unique_field "${authority_file}" candidate_head)" || {
printf 'result=refused refusal_code=authority-identity-missing helper_invoked=false\n'
return 2
}
validation_signature="$(identity_signature "${validation_file}")" || {
printf 'result=refused refusal_code=validation-identity-missing helper_invoked=false\n'
return 2
}
review_signature="$(identity_signature "${review_file}")" || {
printf 'result=refused refusal_code=review-identity-missing helper_invoked=false\n'
return 2
}
authority_signature="$(identity_signature "${authority_file}")" || {
printf 'result=refused refusal_code=authority-identity-missing helper_invoked=false\n'
return 2
}
if [[ "${validation_result}" != "passed" ]]; then
printf 'result=refused refusal_code=validation-not-passed helper_invoked=false\n'
return 2
fi
if [[ "${review_verdict}" != "approved" ]]; then
printf 'result=refused refusal_code=review-not-approved helper_invoked=false\n'
return 2
fi
if [[ "${authority}" != "explicit" ]]; then
printf 'result=refused refusal_code=authority-not-explicit helper_invoked=false\n'
return 2
fi
if [[
"${validation_signature}" != "${review_signature}" ||
"${validation_signature}" != "${authority_signature}"
]]; then
printf 'result=refused refusal_code=identity-mismatch helper_invoked=false\n'
return 2
fi
if ! identity_combined_consistent "${validation_file}" ||
! identity_combined_consistent "${review_file}" ||
! identity_combined_consistent "${authority_file}"; then
printf 'result=refused refusal_code=identity-mismatch helper_invoked=false\n'
return 2
fi
parse_helper_args "$@" || {
printf 'result=refused refusal_code=helper-authority-missing helper_invoked=false\n'
return 2
}
if [[ "${helper_head}" != "${authority_head}" || "${helper_fingerprint}" != "${authority_finalization}" ]]; then
printf 'result=refused refusal_code=identity-mismatch helper_invoked=false\n'
return 2
fi
helper_paths="$(printf '%s\n' "${parsed_helper_paths[@]:-}")"
expected_paths="$(field_values "${authority_file}" candidate_finalization_path)" || {
printf 'result=refused refusal_code=authority-identity-missing helper_invoked=false\n'
return 2
}
if [[ "${helper_paths}" != "${expected_paths}" ]]; then
printf 'result=refused refusal_code=identity-mismatch helper_invoked=false\n'
return 2
fi
if [[ "$(git -C "${helper_repo}" branch --show-current)" != "$(required_unique_field "${authority_file}" candidate_branch)" ]]; then
printf 'result=refused refusal_code=stale-branch helper_invoked=false\n'
return 2
fi
if [[ "$(git -C "${helper_repo}" rev-parse HEAD)" != "${authority_head}" ]]; then
printf 'result=refused refusal_code=stale-authority-head helper_invoked=false\n'
return 2
fi
"${helper}" commit "$@"
}
required_unique_field() {
local file="$1"
local key="$2"
local count
[[ -f "${file}" ]] || return 1
count="$(awk -F= -v key="${key}" '$1 == key { count++ } END { print count + 0 }' "${file}")"
[[ "${count}" == "1" ]] || return 1
awk -F= -v key="${key}" '$1 == key { sub(/^[^=]*=/, ""); print; exit }' "${file}"
}
field_values() {
local file="$1"
local key="$2"
local count values unique_count
[[ -f "${file}" ]] || return 1
count="$(awk -F= -v key="${key}" '$1 == key { count++ } END { print count + 0 }' "${file}")"
[[ "${count}" -gt 0 ]] || return 1
values="$(awk -F= -v key="${key}" '$1 == key { sub(/^[^=]*=/, ""); print }' "${file}")"
[[ -n "${values}" ]] || return 1
! awk -F= -v key="${key}" '$1 == key { sub(/^[^=]*=/, ""); if ($0 == "") empty = 1 } END { exit(empty ? 0 : 1) }' "${file}" ||
return 1
unique_count="$(printf '%s\n' "${values}" | LC_ALL=C sort -u | wc -l | tr -d ' ')"
[[ "${unique_count}" == "${count}" ]] || return 1
printf '%s\n' "${values}"
}
identity_signature() {
local file="$1"
local key value
for key in \
candidate_identity \
candidate_branch \
candidate_head \
candidate_review_baseline \
candidate_review_tree \
candidate_review_sha256 \
candidate_finalization_sha256; do
value="$(required_unique_field "${file}" "${key}")" || return 1
printf '%s=%s\n' "${key}" "${value}"
done
printf 'candidate_review_path<<\n'
field_values "${file}" candidate_review_path || return 1
printf '>>\n'
printf 'candidate_finalization_path<<\n'
field_values "${file}" candidate_finalization_path || return 1
printf '>>\n'
}
identity_combined_consistent() {
local file="$1"
local combined review finalization
combined="$(required_unique_field "${file}" candidate_identity)" || return 1
review="$(required_unique_field "${file}" candidate_review_sha256)" || return 1
finalization="$(required_unique_field "${file}" candidate_finalization_sha256)" || return 1
[[ "${combined}" == "review=${review};finalization=${finalization}" ]]
}
parse_helper_args() {
helper_repo=""
helper_head=""
helper_fingerprint=""
parsed_helper_paths=()
while [[ $# -gt 0 ]]; do
case "$1" in
--repo)
[[ $# -ge 2 ]] || return 1
helper_repo="$2"
shift 2
;;
--expected-head)
[[ $# -ge 2 ]] || return 1
helper_head="$2"
shift 2
;;
--expected-diff-sha256)
[[ $# -ge 2 ]] || return 1
helper_fingerprint="$2"
shift 2
;;
--message|--path)
[[ $# -ge 2 ]] || return 1
if [[ "$1" == "--path" ]]; then
parsed_helper_paths+=("$2")
fi
shift 2
;;
*)
return 1
;;
esac
done
[[ -n "${helper_repo}" && -n "${helper_head}" && -n "${helper_fingerprint}" ]]
}
assert_candidate_casing_reference() {
local casing_repository instruction_file blob base_tree candidate_tree entries
casing_repository="${fixture_root}/canonical-casing-repository"
mkdir -p "${casing_repository}"
git -C "${casing_repository}" init -b feat/canonical-casing >/dev/null
git -C "${casing_repository}" config user.name "Codex Reference Workflow"
git -C "${casing_repository}" config user.email "codex-reference@example.test"
instruction_file="${casing_repository}/instruction-body"
printf 'canonical instruction body\n' >"${instruction_file}"
blob="$(git -C "${casing_repository}" hash-object -w "${instruction_file}")"
base_tree="$(
printf '100644 blob %s\tAgents.md\n' "${blob}" |
git -C "${casing_repository}" mktree
)"
candidate_tree="$(
printf '100644 blob %s\tAGENTS.md\n' "${blob}" |
git -C "${casing_repository}" mktree
)"
entries="$(git -C "${casing_repository}" ls-tree --name-only "${candidate_tree}")"
[[ "$(git -C "${casing_repository}" ls-tree --name-only "${base_tree}")" == "Agents.md" ]]
[[ "${entries}" == "AGENTS.md" ]]
[[ "$(git -C "${casing_repository}" diff-tree -M --name-status -r "${base_tree}" "${candidate_tree}")" == $'R100\tAgents.md\tAGENTS.md' ]]
}
for role_file in \
code-explorer.toml \
implementer.toml \
test-validator.toml \
code-reviewer.toml \
git-finalizer.toml; do
[[ -f "${repository_root}/.codex/agents/${role_file}" ]] || {
echo "result=failed reason=missing-role role=${role_file}" >&2
exit 1
}
done
fixture_root="$(mktemp -d "${TMPDIR:-/tmp}/codex-agent-reference.XXXXXX")"
assert_candidate_casing_reference
fixture_repository="${fixture_root}/repository"
handoff_directory="${fixture_root}/handoffs"
mkdir -p "${fixture_repository}" "${handoff_directory}"
fixture_git init -b feat/reference-workflow >/dev/null
fixture_git config user.name "Codex Reference Workflow"
fixture_git config user.email "codex-reference@example.test"
printf 'reviewed baseline\n' >"${fixture_repository}/reviewed.txt"
printf 'unrelated baseline\n' >"${fixture_repository}/unrelated.txt"
fixture_git add -- reviewed.txt unrelated.txt
fixture_git commit -m "reference baseline" >/dev/null
printf 'reviewed candidate\n' >"${fixture_repository}/reviewed.txt"
printf 'unrelated local work\n' >"${fixture_repository}/unrelated.txt"
refresh_identity_for_current_candidate
fingerprint="${finalization_fingerprint}"
write_handoff 1 code_explorer \
"scope=reviewed.txt;constraints=local-only;skills=foundation;risks=unrelated-work"
write_handoff 2 implementer \
"changed_paths=reviewed.txt;claimed_checks=fingerprint"
write_handoff 3 test_validator \
"command=fixture-validation;result=passed;evidence=candidate-match"
write_handoff 4 code_reviewer \
"verdict=approved;findings=none;evidence=validator-output"
write_authority "${handoff_directory}/finalization-authority.txt"
commit_output="$(
finalize_with_evidence \
"${handoff_directory}/3-test_validator.txt" \
"${handoff_directory}/4-code_reviewer.txt" \
"${handoff_directory}/finalization-authority.txt" \
--repo "${fixture_repository}" \
--expected-head "${head}" \
--expected-diff-sha256 "${fingerprint}" \
--message "reference authorized local commit" \
--path reviewed.txt
)"
commit_oid="$(awk -F= '$1 == "local_commit" { print $2; exit }' <<<"${commit_output}")"
write_handoff 5 git_finalizer \
"result=committed;local_commit=${commit_oid};Push: NOT PERFORMED;Pull Request: NOT PERFORMED;helper_remote_commands=0"
[[ "$(fixture_git diff-tree --no-commit-id --name-only -r HEAD)" == "reviewed.txt" ]]
[[ -n "$(fixture_git status --porcelain -- unrelated.txt)" ]]
[[ -z "$(fixture_git status --porcelain -- reviewed.txt)" ]]
[[ -z "$(fixture_git remote)" ]]
[[ "$(fixture_git rev-list --count HEAD)" == "2" ]]
printf 'validation failure candidate\n' >"${fixture_repository}/reviewed.txt"
refresh_identity_for_current_candidate
failure_head="${head}"
failure_fingerprint="${finalization_fingerprint}"
failure_authority_file="${handoff_directory}/failure-authority.txt"
write_authority "${failure_authority_file}"
write_validation "${handoff_directory}/validation-failed.txt" failed
write_review "${handoff_directory}/validation-failure-review.txt" approved
failure_before="$(repository_snapshot)"
set +e
validation_refusal="$(
finalize_with_evidence \
"${handoff_directory}/validation-failed.txt" \
"${handoff_directory}/validation-failure-review.txt" \
"${failure_authority_file}" \
--repo "${fixture_repository}" \
--expected-head "${failure_head}" \
--expected-diff-sha256 "${failure_fingerprint}" \
--message "must not commit validation failure" \
--path reviewed.txt
)"
validation_refusal_status=$?
set -e
[[ ${validation_refusal_status} -ne 0 ]]
[[ "${validation_refusal}" == *"refusal_code=validation-not-passed"* ]]
[[ "${validation_refusal}" == *"helper_invoked=false"* ]]
[[ "$(repository_snapshot)" == "${failure_before}" ]]
[[ "$(fixture_git rev-parse HEAD)" == "${failure_head}" ]]
write_validation "${handoff_directory}/review-failure-validation.txt" passed
write_review "${handoff_directory}/review-failed.txt" changes-required
failure_before="$(repository_snapshot)"
set +e
review_refusal="$(
finalize_with_evidence \
"${handoff_directory}/review-failure-validation.txt" \
"${handoff_directory}/review-failed.txt" \
"${failure_authority_file}" \
--repo "${fixture_repository}" \
--expected-head "${failure_head}" \
--expected-diff-sha256 "${failure_fingerprint}" \
--message "must not commit review failure" \
--path reviewed.txt
)"
review_refusal_status=$?
set -e
[[ ${review_refusal_status} -ne 0 ]]
[[ "${review_refusal}" == *"refusal_code=review-not-approved"* ]]
[[ "${review_refusal}" == *"helper_invoked=false"* ]]
[[ "$(repository_snapshot)" == "${failure_before}" ]]
[[ "$(fixture_git rev-parse HEAD)" == "${failure_head}" ]]
for refusal_case in misleading-validation missing-identity duplicate-identity duplicate-finalization-path mismatched-candidate-identity duplicate-review mismatched-authority stale-authority stale-branch; do
case "${refusal_case}" in
misleading-validation)
write_validation "${handoff_directory}/${refusal_case}-validation.txt" passed-but-not-really
cp "${handoff_directory}/validation-failure-review.txt" "${handoff_directory}/${refusal_case}-review.txt"
cp "${failure_authority_file}" "${handoff_directory}/${refusal_case}-authority.txt"
expected_refusal="validation-not-passed"
;;
duplicate-review)
write_validation "${handoff_directory}/${refusal_case}-validation.txt" passed
write_review "${handoff_directory}/${refusal_case}-review.txt" approved
printf 'verdict=approved\n' >>"${handoff_directory}/${refusal_case}-review.txt"
cp "${failure_authority_file}" "${handoff_directory}/${refusal_case}-authority.txt"
expected_refusal="review-field-invalid"
;;
missing-identity)
write_validation "${handoff_directory}/${refusal_case}-validation.txt" passed
write_review "${handoff_directory}/${refusal_case}-review.txt" approved
write_authority "${handoff_directory}/${refusal_case}-authority.txt"
sed -i.bak '/^candidate_review_tree=/d' "${handoff_directory}/${refusal_case}-authority.txt"
rm -f -- "${handoff_directory}/${refusal_case}-authority.txt.bak"
expected_refusal="authority-identity-missing"
;;
duplicate-identity)
write_validation "${handoff_directory}/${refusal_case}-validation.txt" passed
write_review "${handoff_directory}/${refusal_case}-review.txt" approved
write_authority "${handoff_directory}/${refusal_case}-authority.txt"
printf 'candidate_branch=%s\n' "${branch}" >>"${handoff_directory}/${refusal_case}-authority.txt"
expected_refusal="authority-identity-missing"
;;
duplicate-finalization-path)
write_validation "${handoff_directory}/${refusal_case}-validation.txt" passed
write_review "${handoff_directory}/${refusal_case}-review.txt" approved
write_authority "${handoff_directory}/${refusal_case}-authority.txt"
printf 'candidate_finalization_path=reviewed.txt\n' >>"${handoff_directory}/${refusal_case}-authority.txt"
expected_refusal="authority-identity-missing"
;;
mismatched-candidate-identity)
write_validation "${handoff_directory}/${refusal_case}-validation.txt" passed
write_review "${handoff_directory}/${refusal_case}-review.txt" approved
write_authority "${handoff_directory}/${refusal_case}-authority.txt"
sed -i.bak 's/^candidate_identity=.*/candidate_identity=review=mismatch;finalization=mismatch/' "${handoff_directory}/${refusal_case}-authority.txt"
rm -f -- "${handoff_directory}/${refusal_case}-authority.txt.bak"
expected_refusal="identity-mismatch"
;;
mismatched-authority)
write_validation "${handoff_directory}/${refusal_case}-validation.txt" passed
write_review "${handoff_directory}/${refusal_case}-review.txt" approved
write_authority "${handoff_directory}/${refusal_case}-authority.txt"
sed -i.bak 's/^candidate_review_sha256=.*/candidate_review_sha256=mismatch/' "${handoff_directory}/${refusal_case}-authority.txt"
rm -f -- "${handoff_directory}/${refusal_case}-authority.txt.bak"
expected_refusal="identity-mismatch"
;;
stale-authority)
write_validation "${handoff_directory}/${refusal_case}-validation.txt" passed
write_review "${handoff_directory}/${refusal_case}-review.txt" approved
write_authority "${handoff_directory}/${refusal_case}-authority.txt"
fixture_git commit --allow-empty -m "advance before stale authority" >/dev/null
expected_refusal="stale-authority-head"
;;
stale-branch)
write_validation "${handoff_directory}/${refusal_case}-validation.txt" passed
write_review "${handoff_directory}/${refusal_case}-review.txt" approved
write_authority "${handoff_directory}/${refusal_case}-authority.txt"
fixture_git checkout -b feat/reference-workflow-other >/dev/null 2>&1
expected_refusal="stale-branch"
;;
esac
failure_before="$(repository_snapshot)"
set +e
structured_refusal="$(
finalize_with_evidence \
"${handoff_directory}/${refusal_case}-validation.txt" \
"${handoff_directory}/${refusal_case}-review.txt" \
"${handoff_directory}/${refusal_case}-authority.txt" \
--repo "${fixture_repository}" \
--expected-head "${failure_head}" \
--expected-diff-sha256 "${failure_fingerprint}" \
--message "must not commit ${refusal_case}" \
--path reviewed.txt
)"
structured_refusal_status=$?
set -e
[[ ${structured_refusal_status} -ne 0 ]]
[[ "${structured_refusal}" == *"refusal_code=${expected_refusal}"* ]]
[[ "${structured_refusal}" == *"helper_invoked=false"* ]]
[[ "$(repository_snapshot)" == "${failure_before}" ]]
if [[ "${refusal_case}" == "stale-authority" ]]; then
printf 'post stale candidate\n' >"${fixture_repository}/reviewed.txt"
refresh_identity_for_current_candidate
failure_head="${head}"
failure_fingerprint="${finalization_fingerprint}"
write_authority "${failure_authority_file}"
fi
if [[ "${refusal_case}" == "stale-branch" ]]; then
fixture_git checkout feat/reference-workflow >/dev/null 2>&1
refresh_identity_for_current_candidate
failure_head="${head}"
failure_fingerprint="${finalization_fingerprint}"
write_authority "${failure_authority_file}"
fi
done
for cycle in 1 2 3; do
printf 'cycle=%s result=correction-required\n' "${cycle}" \
>>"${handoff_directory}/correction-cycles.txt"
done
printf 'cycle-limit-escalated\n' >>"${handoff_directory}/correction-cycles.txt"
[[ "$(rg -c '^cycle=' "${handoff_directory}/correction-cycles.txt")" == "3" ]]
rg -q 'cycle-limit-escalated' "${handoff_directory}/correction-cycles.txt"
[[ "$(fixture_git rev-parse HEAD)" == "${failure_head}" ]]
if [[ ${spawn_smoke} -eq 1 ]]; then
role_project="${fixture_root}/role-only-project"
spawn_marker="tenantpilot-spec-458-code-explorer"
mkdir -p "${role_project}/.codex/agents"
cp "${repository_root}"/.codex/agents/*.toml "${role_project}/.codex/agents/"
printf '%s\n' \
'[agents]' \
'max_threads = 3' \
'' \
'[agents.code_explorer]' \
'description = "Read-only repository discovery."' \
'config_file = "agents/code-explorer.toml"' \
'' \
'[agents.implementer]' \
'description = "Bounded fixture implementation."' \
'config_file = "agents/implementer.toml"' \
'' \
'[agents.test_validator]' \
'description = "Independent fixture validation."' \
'config_file = "agents/test-validator.toml"' \
'' \
'[agents.code_reviewer]' \
'description = "Independent fixture review."' \
'config_file = "agents/code-reviewer.toml"' \
'' \
'[agents.git_finalizer]' \
'description = "Explicitly authorized fixture finalization."' \
'config_file = "agents/git-finalizer.toml"' \
'' \
'[sandbox_workspace_write]' \
'network_access = false' >"${role_project}/.codex/config.toml"
printf '%s\n' "${spawn_marker}" >"${role_project}/SMOKE_MARKER.txt"
! rg -q 'mcp_servers' "${role_project}/.codex/config.toml"
if ! spawn_output="$(
codex exec \
--ignore-user-config \
--strict-config \
--ephemeral \
--skip-git-repo-check \
--sandbox read-only \
--cd "${role_project}" \
-c 'agents.max_threads=3' \
-c "agents.code_explorer={description=\"Read-only repository discovery.\",config_file=\"${role_project}/.codex/agents/code-explorer.toml\"}" \
-c "agents.implementer={description=\"Bounded fixture implementation.\",config_file=\"${role_project}/.codex/agents/implementer.toml\"}" \
-c "agents.test_validator={description=\"Independent fixture validation.\",config_file=\"${role_project}/.codex/agents/test-validator.toml\"}" \
-c "agents.code_reviewer={description=\"Independent fixture review.\",config_file=\"${role_project}/.codex/agents/code-reviewer.toml\"}" \
-c "agents.git_finalizer={description=\"Explicitly authorized fixture finalization.\",config_file=\"${role_project}/.codex/agents/git-finalizer.toml\"}" \
-c 'sandbox_workspace_write.network_access=false' \
--json \
"Use the custom agent code_explorer exactly once with fork_turns=none and without explicit model or reasoning overrides. Ask it to perform the Spec 458 role-discovery smoke by reading SMOKE_MARKER.txt without editing anything. Wait for it to finish, include its complete result, and then return exactly: spawn_smoke=passed role=code_explorer marker=${spawn_marker}" \
2>&1
)"; then
printf '%s\n' "${spawn_output}" >&2
echo "result=failed reason=custom-role-spawn" >&2
exit 1
fi
rg -Fq "spawn_smoke=passed role=code_explorer marker=${spawn_marker}" \
<<<"${spawn_output}" || {
printf '%s\n' "${spawn_output}" >&2
echo "result=failed reason=custom-role-evidence" >&2
exit 1
}
completed_spawn_count="$(
jq -Rr '
fromjson?
| select(
.type == "item.completed"
and .item.type == "collab_tool_call"
and .item.tool == "spawn_agent"
and .item.status == "completed"
)
| 1
' <<<"${spawn_output}" |
wc -l |
tr -d ' '
)"
[[ "${completed_spawn_count}" == "1" ]] || {
printf '%s\n' "${spawn_output}" >&2
echo "result=failed reason=custom-role-spawn-count" >&2
exit 1
}
child_result="$(
jq -Rr '
fromjson?
| select(
.type == "item.completed"
and .item.type == "collab_tool_call"
and .item.tool == "wait"
and .item.status == "completed"
)
| .item.agents_states[]?.message // empty
' <<<"${spawn_output}"
)"
[[ "${child_result}" == *"code_explorer-v1"* ]] || {
printf '%s\n' "${spawn_output}" >&2
echo "result=failed reason=custom-role-child-contract" >&2
exit 1
}
[[ "${child_result}" == *"${spawn_marker}"* ]] || {
printf '%s\n' "${spawn_output}" >&2
echo "result=failed reason=custom-role-child-marker" >&2
exit 1
}
if rg -Fq \
-e 'unknown agent_type' \
-e 'only exposes `explorer`' \
-e 'Full-history forked agents inherit' \
<<<"${spawn_output}"; then
printf '%s\n' "${spawn_output}" >&2
echo "result=failed reason=custom-role-fallback" >&2
exit 1
fi
[[ -z "$(find "${role_project}" -mindepth 1 -maxdepth 1 -type f ! -name 'SMOKE_MARKER.txt' -print)" ]]
echo "spawn_smoke=passed role=code_explorer sandbox=read-only approval=never mcp_tables=0"
fi
printf 'result=passed handoffs=5 validation_failure_refused=true review_failure_refused=true cycles=3\n'
printf 'fixture_local_commit=%s helper_remote_commands=0 remotes=0 unrelated_dirty=preserved\n' "${commit_oid}"
printf 'failure_candidate_sha256=%s\n' "${failure_fingerprint}"

View File

@ -0,0 +1,726 @@
#!/usr/bin/env bash
set -euo pipefail
repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)"
helper="${repository_root}/scripts/codex-safe-local-commit"
mode="${1:-full}"
passed=0
failed=0
suite_root="$(mktemp -d "${TMPDIR:-/tmp}/codex-safe-local-commit-suite.XXXXXX")"
temporary_roots=("${suite_root}")
sanitized_home="${suite_root}/home"
sanitized_tmp="${suite_root}/tmp"
mkdir -p "${sanitized_home}" "${sanitized_tmp}"
case "${mode}" in
full|--critical)
;;
*)
echo "Usage: scripts/tests/codex-safe-local-commit-test [--critical]" >&2
exit 2
;;
esac
cleanup() {
local directory
for directory in "${temporary_roots[@]:-}"; do
[[ -d "${directory}" ]] && rm -rf -- "${directory}"
done
}
trap cleanup EXIT INT TERM
sha256_stream() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum | awk '{ print $1 }'
else
shasum -a 256 | awk '{ print $1 }'
fi
}
fixture_git() {
local repository="$1"
shift
git -C "${repository}" "$@"
}
new_fixture() {
local root repository
root="$(mktemp -d "${suite_root}/fixture.XXXXXX")"
repository="${root}/repository"
mkdir -p "${repository}"
fixture_git "${repository}" init -b feat/helper-fixture >/dev/null
fixture_git "${repository}" config user.name "Codex Helper Fixture"
fixture_git "${repository}" config user.email "codex-helper@example.test"
printf 'tracked baseline\n' >"${repository}/tracked.txt"
printf 'delete baseline\n' >"${repository}/delete-me.txt"
printf 'rename baseline\n' >"${repository}/rename-me.txt"
printf 'unrelated baseline\n' >"${repository}/unrelated.txt"
mkdir -p "${repository}/nested-delete" "${repository}/nested-rename"
printf 'nested delete baseline\n' >"${repository}/nested-delete/delete-me.txt"
printf 'nested rename baseline\n' >"${repository}/nested-rename/source.txt"
fixture_git "${repository}" add -- \
tracked.txt \
delete-me.txt \
rename-me.txt \
unrelated.txt \
nested-delete/delete-me.txt \
nested-rename/source.txt
fixture_git "${repository}" commit -m "fixture baseline" >/dev/null
printf '%s\n' "${repository}"
}
repository_snapshot() {
local repository="$1"
local index_path
index_path="$(fixture_git "${repository}" rev-parse --git-path index)"
{
fixture_git "${repository}" status --porcelain=v2 -z --untracked-files=all
fixture_git "${repository}" show-ref --head
fixture_git "${repository}" config --null --get-regexp '^remote\.' || true
if [[ -f "${index_path}" ]]; then
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${index_path}"
else
shasum -a 256 "${index_path}"
fi
fi
} | sha256_stream
}
fixture_git_path() {
local repository="$1"
local path
path="$(fixture_git "${repository}" rev-parse --git-path "$2")"
if [[ "${path}" == /* ]]; then
printf '%s\n' "${path}"
else
printf '%s\n' "${repository}/${path}"
fi
}
sanitized_helper() {
local environment=(
env -i
"PATH=${PATH}"
"HOME=${sanitized_home}"
"TMPDIR=${sanitized_tmp}"
"GIT_CONFIG_NOSYSTEM=1"
"LC_ALL=C"
)
if [[ -n "${CODEX_HOOK_MARKER:-}" ]]; then
environment+=("CODEX_HOOK_MARKER=${CODEX_HOOK_MARKER}")
fi
if [[ -n "${CODEX_TEST_FAIL_AFTER_UPDATE_REF:-}" ]]; then
environment+=(
"CODEX_TEST_FAIL_AFTER_UPDATE_REF=${CODEX_TEST_FAIL_AFTER_UPDATE_REF}"
)
fi
if [[ -n "${CODEX_TEST_FAIL_AFTER_INDEX_INSTALL:-}" ]]; then
environment+=(
"CODEX_TEST_FAIL_AFTER_INDEX_INSTALL=${CODEX_TEST_FAIL_AFTER_INDEX_INSTALL}"
)
fi
if [[ "${CODEX_HELPER_EXEC:-0}" == "1" ]]; then
exec "${environment[@]}" "${helper}" "$@"
fi
"${environment[@]}" "${helper}" "$@"
}
helper_fingerprint() {
local repository="$1"
shift
local expected_head arguments path
expected_head="$(fixture_git "${repository}" rev-parse HEAD)"
arguments=(fingerprint --repo "${repository}" --expected-head "${expected_head}")
for path in "$@"; do
arguments+=(--path "${path}")
done
sanitized_helper "${arguments[@]}"
}
fingerprint_value() {
awk -F= '$1 == "candidate_sha256" { print $2; exit }'
}
helper_commit() {
local repository="$1"
local fingerprint="$2"
local message="$3"
shift 3
local expected_head arguments path
expected_head="$(fixture_git "${repository}" rev-parse HEAD)"
arguments=(
commit
--repo "${repository}"
--expected-head "${expected_head}"
--expected-diff-sha256 "${fingerprint}"
--message "${message}"
)
for path in "$@"; do
arguments+=(--path "${path}")
done
sanitized_helper "${arguments[@]}"
}
assert_contains() {
local haystack="$1"
local needle="$2"
[[ "${haystack}" == *"${needle}"* ]] || {
echo "Expected output to contain: ${needle}" >&2
echo "Actual output: ${haystack}" >&2
return 1
}
}
assert_refused() {
local expected_code="$1"
shift
local output status
set +e
output="$("$@" 2>&1)"
status=$?
set -e
[[ ${status} -ne 0 ]] || {
echo "Expected refusal but command succeeded: $*" >&2
return 1
}
assert_contains "${output}" "result=refused"
assert_contains "${output}" "refusal_code=${expected_code}"
assert_contains "${output}" "helper_remote_commands=0"
}
run_case() {
local name="$1"
shift
local output_file status
output_file="${suite_root}/case-output"
set +e
(
set -e
"$@"
) >"${output_file}" 2>&1
status=$?
set -e
if [[ ${status} -eq 0 ]]; then
passed=$((passed + 1))
printf 'PASS %s\n' "${name}"
else
failed=$((failed + 1))
printf 'FAIL %s\n' "${name}" >&2
sed -n '1,160p' "${output_file}" >&2
fi
}
case_tracked_success() {
local repository fingerprint output changed
repository="$(new_fixture)"
printf 'tracked changed\n' >"${repository}/tracked.txt"
printf 'unrelated changed\n' >"${repository}/unrelated.txt"
fingerprint="$(helper_fingerprint "${repository}" tracked.txt | fingerprint_value)"
[[ ${#fingerprint} -eq 64 ]]
output="$(helper_commit "${repository}" "${fingerprint}" "commit tracked path" tracked.txt)"
assert_contains "${output}" "result=committed"
assert_contains "${output}" "candidate_sha256=${fingerprint}"
assert_contains "${output}" "helper_remote_commands=0"
changed="$(fixture_git "${repository}" diff-tree --no-commit-id --name-only -r HEAD)"
[[ "${changed}" == "tracked.txt" ]]
[[ "$(fixture_git "${repository}" show HEAD:tracked.txt)" == "tracked changed" ]]
[[ "$(cat "${repository}/unrelated.txt")" == "unrelated changed" ]]
[[ "$(fixture_git "${repository}" status --porcelain -- tracked.txt)" == "" ]]
assert_contains "$(fixture_git "${repository}" status --porcelain -- unrelated.txt)" "unrelated.txt"
[[ "$(fixture_git "${repository}" rev-list --count HEAD)" == "2" ]]
}
case_fingerprint_is_repeatable_and_read_only() {
local repository before first middle second after
repository="$(new_fixture)"
printf 'tracked changed\n' >"${repository}/tracked.txt"
before="$(repository_snapshot "${repository}")"
first="$(helper_fingerprint "${repository}" tracked.txt | fingerprint_value)"
middle="$(repository_snapshot "${repository}")"
second="$(helper_fingerprint "${repository}" tracked.txt | fingerprint_value)"
after="$(repository_snapshot "${repository}")"
[[ "${first}" == "${second}" ]]
[[ "${before}" == "${middle}" ]]
[[ "${before}" == "${after}" ]]
}
case_mixed_paths_and_encoding() {
local repository tab_path fingerprint output
repository="$(new_fixture)"
tab_path=$'tab\tname.txt'
printf 'tracked changed\n' >"${repository}/tracked.txt"
printf 'untracked\n' >"${repository}/new file.txt"
printf 'unicode\n' >"${repository}/überblick.txt"
printf 'leading dash\n' >"${repository}/-leading.txt"
printf 'tab\n' >"${repository}/${tab_path}"
rm -- "${repository}/delete-me.txt"
mv -- "${repository}/rename-me.txt" "${repository}/renamed file.txt"
rm -- "${repository}/nested-delete/delete-me.txt"
rmdir -- "${repository}/nested-delete"
mv -- "${repository}/nested-rename/source.txt" "${repository}/nested renamed.txt"
rmdir -- "${repository}/nested-rename"
fingerprint="$(
helper_fingerprint "${repository}" \
tracked.txt \
"new file.txt" \
"überblick.txt" \
"-leading.txt" \
"${tab_path}" \
delete-me.txt \
rename-me.txt \
"renamed file.txt" \
nested-delete/delete-me.txt \
nested-rename/source.txt \
"nested renamed.txt" |
fingerprint_value
)"
output="$(
helper_commit "${repository}" "${fingerprint}" "commit mixed paths" \
tracked.txt \
"new file.txt" \
"überblick.txt" \
"-leading.txt" \
"${tab_path}" \
delete-me.txt \
rename-me.txt \
"renamed file.txt" \
nested-delete/delete-me.txt \
nested-rename/source.txt \
"nested renamed.txt"
)"
assert_contains "${output}" "result=committed"
[[ "$(fixture_git "${repository}" show "HEAD:new file.txt")" == "untracked" ]]
[[ "$(fixture_git "${repository}" show "HEAD:renamed file.txt")" == "rename baseline" ]]
[[ "$(fixture_git "${repository}" show "HEAD:${tab_path}")" == "tab" ]]
! fixture_git "${repository}" cat-file -e HEAD:delete-me.txt 2>/dev/null
! fixture_git "${repository}" cat-file -e HEAD:rename-me.txt 2>/dev/null
! fixture_git "${repository}" cat-file -e HEAD:nested-delete/delete-me.txt 2>/dev/null
! fixture_git "${repository}" cat-file -e HEAD:nested-rename/source.txt 2>/dev/null
[[ "$(fixture_git "${repository}" show "HEAD:nested renamed.txt")" == "nested rename baseline" ]]
[[ "$(fixture_git "${repository}" status --porcelain)" == "" ]]
}
case_case_only_rename_commits_canonical_tree() {
local repository fingerprint output before after
repository="$(new_fixture)"
printf 'instruction baseline\n' >"${repository}/Agents.md"
fixture_git "${repository}" add -- Agents.md
fixture_git "${repository}" commit -m "add mixed-case instruction" >/dev/null
mv -- "${repository}/Agents.md" "${repository}/AGENTS.md"
printf 'instruction candidate\n' >"${repository}/AGENTS.md"
before="$(repository_snapshot "${repository}")"
fingerprint="$(
helper_fingerprint "${repository}" Agents.md AGENTS.md |
fingerprint_value
)"
output="$(
helper_commit "${repository}" "${fingerprint}" "commit canonical instruction casing" \
Agents.md AGENTS.md
)"
assert_contains "${output}" "result=committed"
[[ "$(fixture_git "${repository}" ls-tree --name-only HEAD | awk 'tolower($0) == "agents.md"')" == "AGENTS.md" ]]
[[ "$(fixture_git "${repository}" show HEAD:AGENTS.md)" == "instruction candidate" ]]
! fixture_git "${repository}" cat-file -e HEAD:Agents.md 2>/dev/null
[[ -z "$(fixture_git "${repository}" status --porcelain -- AGENTS.md Agents.md)" ]]
after="$(repository_snapshot "${repository}")"
[[ "${before}" != "${after}" ]]
}
case_case_only_rename_preserves_modes() {
local repository fingerprint output before tree_listing
repository="$(new_fixture)"
printf '#!/usr/bin/env bash\nprintf tool\\n\n' >"${repository}/tool.sh"
chmod +x "${repository}/tool.sh"
ln -s tracked.txt "${repository}/link.ref"
fixture_git "${repository}" add -- tool.sh link.ref
fixture_git "${repository}" commit -m "add mode fixtures" >/dev/null
mv -- "${repository}/tool.sh" "${repository}/TOOL.sh"
mv -- "${repository}/link.ref" "${repository}/LINK.ref"
printf '#!/usr/bin/env bash\nprintf changed\\n\n' >"${repository}/TOOL.sh"
chmod +x "${repository}/TOOL.sh"
before="$(repository_snapshot "${repository}")"
fingerprint="$(
helper_fingerprint "${repository}" tool.sh TOOL.sh link.ref LINK.ref |
fingerprint_value
)"
output="$(
helper_commit "${repository}" "${fingerprint}" "commit case-only modes" \
tool.sh TOOL.sh link.ref LINK.ref
)"
assert_contains "${output}" "result=committed"
tree_listing="$(fixture_git "${repository}" ls-tree HEAD -- TOOL.sh LINK.ref)"
assert_contains "${tree_listing}" $'100755 blob'
assert_contains "${tree_listing}" $'120000 blob'
[[ "$(fixture_git "${repository}" show HEAD:TOOL.sh)" == $'#!/usr/bin/env bash\nprintf changed\\n' ]]
[[ "$(fixture_git "${repository}" show HEAD:LINK.ref)" == "tracked.txt" ]]
! fixture_git "${repository}" cat-file -e HEAD:tool.sh 2>/dev/null
! fixture_git "${repository}" cat-file -e HEAD:link.ref 2>/dev/null
[[ -z "$(fixture_git "${repository}" status --porcelain -- TOOL.sh tool.sh LINK.ref link.ref)" ]]
[[ "${before}" != "$(repository_snapshot "${repository}")" ]]
}
case_rename_requires_both_paths() {
local repository head before after
repository="$(new_fixture)"
mv -- "${repository}/rename-me.txt" "${repository}/renamed.txt"
head="$(fixture_git "${repository}" rev-parse HEAD)"
before="$(repository_snapshot "${repository}")"
assert_refused rename-authorization-required \
sanitized_helper fingerprint \
--repo "${repository}" \
--expected-head "${head}" \
--path renamed.txt
after="$(repository_snapshot "${repository}")"
[[ "${before}" == "${after}" ]]
}
case_stale_fingerprint_refuses_without_mutation() {
local repository fingerprint before after
repository="$(new_fixture)"
printf 'first state\n' >"${repository}/tracked.txt"
fingerprint="$(helper_fingerprint "${repository}" tracked.txt | fingerprint_value)"
printf 'second state\n' >"${repository}/tracked.txt"
before="$(repository_snapshot "${repository}")"
assert_refused stale-fingerprint \
helper_commit "${repository}" "${fingerprint}" "stale candidate" tracked.txt
after="$(repository_snapshot "${repository}")"
[[ "${before}" == "${after}" ]]
}
case_stale_head_refuses_without_mutation() {
local repository reviewed_head current_head before after
repository="$(new_fixture)"
reviewed_head="$(fixture_git "${repository}" rev-parse HEAD)"
fixture_git "${repository}" commit --allow-empty -m "advance head" >/dev/null
current_head="$(fixture_git "${repository}" rev-parse HEAD)"
[[ "${reviewed_head}" != "${current_head}" ]]
printf 'changed\n' >"${repository}/tracked.txt"
before="$(repository_snapshot "${repository}")"
assert_refused stale-head \
sanitized_helper fingerprint \
--repo "${repository}" \
--expected-head "${reviewed_head}" \
--path tracked.txt
after="$(repository_snapshot "${repository}")"
[[ "${before}" == "${after}" ]]
}
case_staged_index_refuses_without_mutation() {
local repository head before after
repository="$(new_fixture)"
printf 'tracked changed\n' >"${repository}/tracked.txt"
fixture_git "${repository}" add -- unrelated.txt
printf 'staged changed\n' >"${repository}/unrelated.txt"
fixture_git "${repository}" add -- unrelated.txt
head="$(fixture_git "${repository}" rev-parse HEAD)"
before="$(repository_snapshot "${repository}")"
assert_refused staged-index \
sanitized_helper fingerprint \
--repo "${repository}" \
--expected-head "${head}" \
--path tracked.txt
after="$(repository_snapshot "${repository}")"
[[ "${before}" == "${after}" ]]
}
case_protected_detached_and_operation_refusals() {
local repository head git_dir before after
repository="$(new_fixture)"
printf 'changed\n' >"${repository}/tracked.txt"
head="$(fixture_git "${repository}" rev-parse HEAD)"
fixture_git "${repository}" branch -m platform-dev
assert_refused protected-branch \
sanitized_helper fingerprint --repo "${repository}" --expected-head "${head}" --path tracked.txt
fixture_git "${repository}" branch -m feat/helper-fixture
fixture_git "${repository}" checkout --detach >/dev/null 2>&1
assert_refused detached-head \
sanitized_helper fingerprint --repo "${repository}" --expected-head "${head}" --path tracked.txt
fixture_git "${repository}" checkout feat/helper-fixture >/dev/null 2>&1
git_dir="$(fixture_git_path "${repository}" MERGE_HEAD)"
printf '%s\n' "${head}" >"${git_dir}"
before="$(repository_snapshot "${repository}")"
assert_refused git-operation-in-progress \
sanitized_helper fingerprint --repo "${repository}" --expected-head "${head}" --path tracked.txt
after="$(repository_snapshot "${repository}")"
[[ "${before}" == "${after}" ]]
}
case_invalid_paths_refuse() {
local repository head newline_path output
repository="$(new_fixture)"
printf 'changed\n' >"${repository}/tracked.txt"
mkdir -p "${repository}/directory"
head="$(fixture_git "${repository}" rev-parse HEAD)"
newline_path=$'bad\npath'
assert_refused missing-paths \
sanitized_helper fingerprint --repo "${repository}" --expected-head "${head}"
assert_refused broad-path \
sanitized_helper fingerprint --repo "${repository}" --expected-head "${head}" --path .
assert_refused path-traversal \
sanitized_helper fingerprint --repo "${repository}" --expected-head "${head}" --path ../outside
assert_refused absolute-path \
sanitized_helper fingerprint --repo "${repository}" --expected-head "${head}" --path /tmp/outside
assert_refused directory-path \
sanitized_helper fingerprint --repo "${repository}" --expected-head "${head}" --path directory
assert_refused unresolved-glob \
sanitized_helper fingerprint --repo "${repository}" --expected-head "${head}" --path '*.txt'
assert_refused duplicate-path \
sanitized_helper fingerprint --repo "${repository}" --expected-head "${head}" \
--path tracked.txt --path tracked.txt
assert_refused newline-path \
sanitized_helper fingerprint --repo "${repository}" --expected-head "${head}" --path "${newline_path}"
for output in .env .env.local certificate.pem signing.key id_rsa id_ed25519; do
assert_refused sensitive-path \
sanitized_helper fingerprint --repo "${repository}" --expected-head "${head}" --path "${output}"
done
}
case_hook_rejection_preserves_real_state() {
local repository fingerprint hook before after
repository="$(new_fixture)"
printf 'changed\n' >"${repository}/tracked.txt"
hook="$(fixture_git_path "${repository}" hooks/pre-commit)"
mkdir -p "$(dirname "${hook}")"
printf '#!/usr/bin/env bash\nexit 17\n' >"${hook}"
chmod +x "${hook}"
fingerprint="$(helper_fingerprint "${repository}" tracked.txt | fingerprint_value)"
before="$(repository_snapshot "${repository}")"
assert_refused hook-rejected \
helper_commit "${repository}" "${fingerprint}" "hook rejects" tracked.txt
after="$(repository_snapshot "${repository}")"
[[ "${before}" == "${after}" ]]
}
case_hook_staged_drift_refuses_publication() {
local repository fingerprint hook before after
repository="$(new_fixture)"
printf 'changed\n' >"${repository}/tracked.txt"
hook="$(fixture_git_path "${repository}" hooks/pre-commit)"
mkdir -p "$(dirname "${hook}")"
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf "unauthorized\n" > hook-created.txt' \
'git add -- hook-created.txt' >"${hook}"
chmod +x "${hook}"
fingerprint="$(helper_fingerprint "${repository}" tracked.txt | fingerprint_value)"
before="$(repository_snapshot "${repository}")"
assert_refused post-hook-candidate-drift \
helper_commit "${repository}" "${fingerprint}" "hook drifts" tracked.txt
after="$(repository_snapshot "${repository}")"
[[ "${before}" == "${after}" ]]
[[ ! -e "${repository}/hook-created.txt" ]]
}
case_hook_ref_and_remote_config_stay_isolated() {
local repository fingerprint hook before after source_remote_config source_remote_refs output
repository="$(new_fixture)"
printf 'changed\n' >"${repository}/tracked.txt"
fixture_git "${repository}" remote add origin https://source.example.invalid/repo.git
source_remote_config="$(fixture_git "${repository}" config --get-regexp '^remote\.')"
source_remote_refs="$(fixture_git "${repository}" for-each-ref --format='%(refname)' refs/remotes)"
hook="$(fixture_git_path "${repository}" hooks/pre-commit)"
mkdir -p "$(dirname "${hook}")"
printf '%s\n' \
'#!/usr/bin/env bash' \
'git config remote.origin.url https://isolated.example.invalid/repo.git' \
'git update-ref refs/remotes/origin/main HEAD' \
'git update-ref refs/heads/hook-created HEAD' >"${hook}"
chmod +x "${hook}"
fingerprint="$(helper_fingerprint "${repository}" tracked.txt | fingerprint_value)"
before="$(repository_snapshot "${repository}")"
output="$(helper_commit "${repository}" "${fingerprint}" "hook mutates isolated refs" tracked.txt)"
assert_contains "${output}" "result=committed"
after="$(repository_snapshot "${repository}")"
[[ "$(fixture_git "${repository}" config --get-regexp '^remote\.')" == "${source_remote_config}" ]]
[[ "$(fixture_git "${repository}" for-each-ref --format='%(refname)' refs/remotes)" == "${source_remote_refs}" ]]
[[ -z "$(fixture_git "${repository}" for-each-ref --format='%(refname)' refs/heads/hook-created)" ]]
[[ "${before}" != "${after}" ]]
[[ "$(fixture_git "${repository}" diff-tree --no-commit-id --name-only -r HEAD)" == "tracked.txt" ]]
}
case_commit_requires_complete_authority() {
local repository head
repository="$(new_fixture)"
printf 'changed\n' >"${repository}/tracked.txt"
head="$(fixture_git "${repository}" rev-parse HEAD)"
assert_refused missing-message \
sanitized_helper commit --repo "${repository}" --expected-head "${head}" \
--expected-diff-sha256 "$(printf zero | sha256_stream)" --path tracked.txt
assert_refused missing-fingerprint \
sanitized_helper commit --repo "${repository}" --expected-head "${head}" \
--message "missing fingerprint" --path tracked.txt
}
case_signal_cleanup() {
local repository fingerprint hook marker before after process_id output_file attempts head
repository="$(new_fixture)"
printf 'changed\n' >"${repository}/tracked.txt"
marker="${repository%/repository}/hook-started"
hook="$(fixture_git_path "${repository}" hooks/pre-commit)"
mkdir -p "$(dirname "${hook}")"
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf "started\n" > "${CODEX_HOOK_MARKER}"' \
'sleep 2' >"${hook}"
chmod +x "${hook}"
fingerprint="$(helper_fingerprint "${repository}" tracked.txt | fingerprint_value)"
head="$(fixture_git "${repository}" rev-parse HEAD)"
before="$(repository_snapshot "${repository}")"
output_file="${repository%/repository}/signal-output"
CODEX_HELPER_EXEC=1 \
CODEX_HOOK_MARKER="${marker}" \
sanitized_helper commit \
--repo "${repository}" \
--expected-head "${head}" \
--expected-diff-sha256 "${fingerprint}" \
--message "signal cleanup" \
--path tracked.txt \
>"${output_file}" 2>&1 &
process_id=$!
attempts=0
while [[ ! -f "${marker}" && ${attempts} -lt 100 ]]; do
sleep 0.05
attempts=$((attempts + 1))
done
[[ -f "${marker}" ]]
kill -TERM "${process_id}"
set +e
wait "${process_id}"
local status=$?
set -e
[[ ${status} -ne 0 ]]
after="$(repository_snapshot "${repository}")"
[[ "${before}" == "${after}" ]]
[[ -z "$(fixture_git "${repository}" worktree list --porcelain | awk '/^worktree / { count++ } END { if (count > 1) print count }')" ]]
}
case_publication_faults_restore_ref_and_index() {
local repository fingerprint before after
repository="$(new_fixture)"
printf 'changed after update-ref\n' >"${repository}/tracked.txt"
fingerprint="$(helper_fingerprint "${repository}" tracked.txt | fingerprint_value)"
before="$(repository_snapshot "${repository}")"
CODEX_TEST_FAIL_AFTER_UPDATE_REF=1 \
assert_refused injected-after-update-ref \
helper_commit "${repository}" "${fingerprint}" "fault after ref" tracked.txt
after="$(repository_snapshot "${repository}")"
[[ "${before}" == "${after}" ]]
repository="$(new_fixture)"
printf 'changed after index install\n' >"${repository}/tracked.txt"
fingerprint="$(helper_fingerprint "${repository}" tracked.txt | fingerprint_value)"
before="$(repository_snapshot "${repository}")"
CODEX_TEST_FAIL_AFTER_INDEX_INSTALL=1 \
assert_refused injected-after-index-install \
helper_commit "${repository}" "${fingerprint}" "fault after index" tracked.txt
after="$(repository_snapshot "${repository}")"
[[ "${before}" == "${after}" ]]
}
run_case "tracked modification commits exactly one authorized path" case_tracked_success
run_case "fingerprint mode is repeatable and read-only" case_fingerprint_is_repeatable_and_read_only
run_case "stale fingerprint refuses without mutation" case_stale_fingerprint_refuses_without_mutation
run_case "hook rejection preserves real state" case_hook_rejection_preserves_real_state
if [[ "${mode}" == "full" ]]; then
run_case "mixed path states and encodings commit safely" case_mixed_paths_and_encoding
run_case "case-only rename commits canonical tree" case_case_only_rename_commits_canonical_tree
run_case "case-only rename preserves executable and symlink modes" case_case_only_rename_preserves_modes
run_case "rename requires both paths" case_rename_requires_both_paths
run_case "stale HEAD refuses without mutation" case_stale_head_refuses_without_mutation
run_case "pre-staged index refuses without mutation" case_staged_index_refuses_without_mutation
run_case "protected detached and operation states refuse" case_protected_detached_and_operation_refusals
run_case "invalid and sensitive paths refuse" case_invalid_paths_refuse
run_case "hook-staged drift refuses publication" case_hook_staged_drift_refuses_publication
run_case "hook ref and remote config mutations stay isolated" case_hook_ref_and_remote_config_stay_isolated
run_case "commit requires complete authority" case_commit_requires_complete_authority
run_case "signal interruption cleans disposable state" case_signal_cleanup
run_case "publication faults restore ref and index" case_publication_faults_restore_ref_and_index
fi
printf 'result=%s passed=%d failed=%d\n' "$([[ ${failed} -eq 0 ]] && echo passed || echo failed)" "${passed}" "${failed}"
[[ ${failed} -eq 0 ]]

View File

@ -0,0 +1,686 @@
#!/usr/bin/env bash
set -euo pipefail
repository_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
branch_family=""
work_classification=""
pull_request_target=""
passed=0
failed=0
errors=()
validator_temporaries=()
review_identity_paths=()
finalization_identity_paths=()
candidate_identity_branch=""
candidate_identity_head=""
review_identity_baseline=""
review_identity_fingerprint=""
finalization_identity_baseline=""
finalization_identity_fingerprint=""
candidate_tree=""
cleanup() {
local path
for path in "${validator_temporaries[@]:-}"; do
rm -f -- "${path}"
done
}
trap cleanup EXIT INT TERM
usage() {
cat <<'EOF'
Usage: scripts/validate-codex-agent-foundation [options]
Options:
--branch-family platform|website|repository-wide
--work-classification <classification>
--pull-request-target <branch>
EOF
}
record_pass() {
passed=$((passed + 1))
printf 'PASS %s\n' "$1"
}
record_failure() {
failed=$((failed + 1))
errors+=("$1")
printf 'FAIL %s\n' "$1" >&2
}
check() {
local label="$1"
shift
if "$@"; then
record_pass "${label}"
else
record_failure "${label}"
fi
}
file_contains_all() {
local file="$1"
shift
local needle
[[ -f "${file}" ]] || return 1
for needle in "$@"; do
rg -F --quiet -- "${needle}" "${file}" || return 1
done
}
toml_value() {
local file="$1"
local key="$2"
sed -n "s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*\"\\([^\"]*\\)\"[[:space:]]*$/\\1/p" "${file}" |
head -n 1
}
sha256_file() {
local path="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${path}" | awk '{ print $1 }'
else
shasum -a 256 "${path}" | awk '{ print $1 }'
fi
}
candidate_path_allowed() {
local path="$1"
case "${path}" in
AGENTS.md|Agents.md)
return 0
;;
.codex/config.toml)
return 0
;;
.codex/agents/code-explorer.toml|\
.codex/agents/implementer.toml|\
.codex/agents/test-validator.toml|\
.codex/agents/code-reviewer.toml|\
.codex/agents/git-finalizer.toml)
return 0
;;
.codex/skills/giteaflow/SKILL.md|\
.codex/skills/platform-feature-finish/SKILL.md)
return 0
;;
docs/HANDOVER.md|docs/PROJECT_SUMMARY.md)
return 0
;;
apps/platform/tests/Feature/Guards/ProductSurfaceContractGateTest.php|\
apps/platform/tests/Feature/Guards/ConstitutionBranchTopologyContractTest.php|\
apps/platform/tests/Feature/Guards/CodexAgentFoundationContractTest.php)
return 0
;;
scripts/codex-safe-local-commit|\
scripts/validate-codex-agent-foundation|\
scripts/tests/codex-safe-local-commit-test|\
scripts/tests/codex-agent-reference-workflow)
return 0
;;
specs/458-codex-agent-foundation-safe-local-finalization/*)
return 0
;;
esac
return 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--branch-family)
[[ $# -ge 2 ]] || {
usage >&2
exit 2
}
branch_family="$2"
shift 2
;;
--work-classification)
[[ $# -ge 2 ]] || {
usage >&2
exit 2
}
work_classification="$2"
shift 2
;;
--pull-request-target)
[[ $# -ge 2 ]] || {
usage >&2
exit 2
}
pull_request_target="$2"
shift 2
;;
--help|-h)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
[[ -n "${repository_root}" ]] || {
echo "result=failed reason=not-a-git-repository" >&2
exit 2
}
repository_root="$(cd "${repository_root}" && pwd -P)"
[[ -n "${branch_family}" ]] || branch_family="platform"
[[ -n "${work_classification}" ]] || work_classification="repository-governance"
[[ -n "${pull_request_target}" ]] || pull_request_target="platform-dev"
cd "${repository_root}"
resolver_output="$(
scripts/resolve-feature-base \
--branch-family="${branch_family}" \
--work-classification="${work_classification}" \
--pull-request-target="${pull_request_target}" \
--spec=specs/458-codex-agent-foundation-safe-local-finalization/spec.md \
--format=kv
)" || {
record_failure "canonical feature baseline resolves"
resolver_output=""
}
if [[ -n "${resolver_output}" ]]; then
record_pass "canonical feature baseline resolves"
fi
base_range="$(awk -F= '$1 == "base_range" { print $2; exit }' <<<"${resolver_output}")"
merge_base="$(awk -F= '$1 == "merge_base" { print $2; exit }' <<<"${resolver_output}")"
real_index_semantically_empty_ok() {
[[ -z "$(git ls-files -u)" ]] || return 1
git diff --cached --quiet --ignore-submodules --
}
collect_name_status_paths() {
local status old_path new_path path
while IFS= read -r -d '' status; do
case "${status}" in
R*|C*)
IFS= read -r -d '' old_path
IFS= read -r -d '' new_path
printf '%s\n%s\n' "${old_path}" "${new_path}"
;;
*)
IFS= read -r -d '' path
printf '%s\n' "${path}"
;;
esac
done
}
candidate_add_path() {
local path="$1"
local existing
[[ -n "${path}" ]] || return 0
for existing in "${finalization_identity_paths[@]:-}"; do
[[ "${existing}" != "${path}" ]] || return 0
done
[[ "${path}" != *$'\n'* && "${path}" != *$'\r'* ]] || return 1
candidate_path_allowed "${path}" || return 1
finalization_identity_paths+=("${path}")
}
path_exists_with_exact_casing() {
local path="$1"
local parent base
parent="${path%/*}"
[[ "${parent}" != "${path}" ]] || parent="."
base="${path##*/}"
find "${repository_root}/${parent}" -maxdepth 1 -mindepth 1 -name "${base}" -print -quit 2>/dev/null |
awk 'length($0) > 0 { found = 1 } END { exit(found ? 0 : 1) }'
}
apply_candidate_case_only_renames() {
local candidate_index="$1"
local old_path new_path old_lower new_lower
local count=${#finalization_identity_paths[@]}
local i j
for ((i = 0; i < count; i++)); do
old_path="${finalization_identity_paths[i]}"
old_lower="$(printf '%s' "${old_path}" | tr '[:upper:]' '[:lower:]')"
for ((j = 0; j < count; j++)); do
[[ ${i} -ne ${j} ]] || continue
new_path="${finalization_identity_paths[j]}"
new_lower="$(printf '%s' "${new_path}" | tr '[:upper:]' '[:lower:]')"
[[ "${old_path}" != "${new_path}" && "${old_lower}" == "${new_lower}" ]] || continue
git cat-file -e "${candidate_identity_head}:${old_path}" 2>/dev/null ||
continue
path_exists_with_exact_casing "${new_path}" ||
continue
GIT_INDEX_FILE="${candidate_index}" git update-index --force-remove -- "${old_path}" >/dev/null
GIT_INDEX_FILE="${candidate_index}" git add -- "${new_path}" >/dev/null
done
done
}
build_candidate_tree_ok() {
local paths_file candidate_index path sibling
local final_paths=()
candidate_identity_branch="$(git branch --show-current)"
candidate_identity_head="$(git rev-parse HEAD)"
finalization_identity_baseline="${candidate_identity_head}"
paths_file="$(mktemp "${TMPDIR:-/tmp}/codex-agent-finalization-paths.XXXXXX")"
validator_temporaries+=("${paths_file}")
{
git diff --name-status -z --find-renames HEAD -- | collect_name_status_paths
git ls-files --others --exclude-standard
} | LC_ALL=C sort -u >"${paths_file}"
finalization_identity_paths=()
while IFS= read -r path; do
candidate_add_path "${path}" || return 1
if [[ "${path}" == "Agents.md" ]] && path_exists_with_exact_casing "AGENTS.md"; then
candidate_add_path "AGENTS.md" || return 1
fi
if [[ "${path}" == "AGENTS.md" ]] && path_exists_with_exact_casing "Agents.md"; then
candidate_add_path "Agents.md" || return 1
fi
done <"${paths_file}"
[[ ${#finalization_identity_paths[@]} -gt 0 ]] || return 1
for path in "${finalization_identity_paths[@]}"; do
final_paths+=("${path}")
done
candidate_index="$(mktemp "${TMPDIR:-/tmp}/codex-agent-candidate-index.XXXXXX")"
validator_temporaries+=("${candidate_index}")
rm -f -- "${candidate_index}"
GIT_INDEX_FILE="${candidate_index}" git read-tree "${candidate_identity_head}"
GIT_INDEX_FILE="${candidate_index}" git add -A -- "${final_paths[@]}"
apply_candidate_case_only_renames "${candidate_index}"
candidate_tree="$(GIT_INDEX_FILE="${candidate_index}" git write-tree)"
git cat-file -e "${candidate_tree}^{tree}" 2>/dev/null
}
check "real index is semantically empty before validator evidence" real_index_semantically_empty_ok
check "temporary candidate tree builds from HEAD plus explicit path delta" build_candidate_tree_ok
canonical_casing_ok() {
local candidate_entries
[[ -n "${candidate_tree}" ]] || return 1
candidate_entries="$(
git ls-tree --name-only "${candidate_tree}" |
awk 'tolower($0) == "agents.md"' |
LC_ALL=C sort
)"
[[ "${candidate_entries}" == "AGENTS.md" ]]
}
check "canonical root instruction casing is unique" canonical_casing_ok
instruction_preservation_ok() {
file_contains_all AGENTS.md \
"# TenantPilot - Agent Guidelines" \
"Product Surface Contract Gate" \
"TenantPilot Agent Skill Router" \
"Sail-first for local development" \
"Multi-Agent Coordination" \
"Codex Agent Foundation"
}
check "canonical instructions preserve governance and add the foundation" instruction_preservation_ok
active_references_ok() {
! rg -n -F 'repo_path('\''Agents.md'\'')' \
apps/platform/tests/Feature/Guards/ProductSurfaceContractGateTest.php \
apps/platform/tests/Feature/Guards/ConstitutionBranchTopologyContractTest.php || return 1
! rg -n -F 'productSurfaceGateAssertContains('\''Agents.md'\'',' \
apps/platform/tests/Feature/Guards/ProductSurfaceContractGateTest.php || return 1
! rg -n -F '[Agents.md](Agents.md)' docs/HANDOVER.md || return 1
! rg -n 'Branching conventions documented in `Agents\.md`' docs/PROJECT_SUMMARY.md || return 1
! rg -n -F 'Agents.md' .codex/agents .codex/skills/giteaflow .codex/skills/platform-feature-finish
}
check "active references use canonical AGENTS.md" active_references_ok
roles_ok() {
local expected_files actual_files file expected_name expected_model expected_reasoning
local expected_sandbox expected_approval
local names_file
expected_files=$'code-explorer.toml\ncode-reviewer.toml\ngit-finalizer.toml\nimplementer.toml\ntest-validator.toml'
actual_files="$(
find .codex/agents -maxdepth 1 -type f -name '*.toml' -exec basename {} \; 2>/dev/null |
LC_ALL=C sort
)"
[[ "${actual_files}" == "${expected_files}" ]] || return 1
names_file="$(mktemp "${TMPDIR:-/tmp}/codex-agent-names.XXXXXX")"
validator_temporaries+=("${names_file}")
for file in .codex/agents/*.toml; do
file_contains_all "${file}" \
'name = "' \
'description = "' \
'developer_instructions = ' \
'model = "' \
'model_reasoning_effort = "' \
'sandbox_mode = "' \
'approval_policy = "' || return 1
case "$(basename "${file}")" in
code-explorer.toml)
expected_name="code_explorer"
expected_model="gpt-5.4-mini"
expected_reasoning="medium"
expected_sandbox="read-only"
expected_approval="never"
;;
implementer.toml)
expected_name="implementer"
expected_model="gpt-5.5"
expected_reasoning="high"
expected_sandbox="workspace-write"
expected_approval="on-request"
;;
test-validator.toml)
expected_name="test_validator"
expected_model="gpt-5.4-mini"
expected_reasoning="high"
expected_sandbox="workspace-write"
expected_approval="on-request"
;;
code-reviewer.toml)
expected_name="code_reviewer"
expected_model="gpt-5.5"
expected_reasoning="high"
expected_sandbox="read-only"
expected_approval="never"
;;
git-finalizer.toml)
expected_name="git_finalizer"
expected_model="gpt-5.4-mini"
expected_reasoning="medium"
expected_sandbox="workspace-write"
expected_approval="on-request"
;;
*)
return 1
;;
esac
[[ "$(toml_value "${file}" name)" == "${expected_name}" ]] || return 1
[[ "$(toml_value "${file}" model)" == "${expected_model}" ]] || return 1
[[ "$(toml_value "${file}" model_reasoning_effort)" == "${expected_reasoning}" ]] || return 1
[[ "$(toml_value "${file}" sandbox_mode)" == "${expected_sandbox}" ]] || return 1
[[ "$(toml_value "${file}" approval_policy)" == "${expected_approval}" ]] || return 1
printf '%s\n' "${expected_name}" >>"${names_file}"
done
[[ "$(LC_ALL=C sort -u "${names_file}" | wc -l | tr -d ' ')" == "5" ]]
}
check "exact five role files match the approved matrix" roles_ok
configuration_ok() {
file_contains_all .codex/config.toml \
'[agents]' \
'max_threads = 3' \
'[agents.code_explorer]' \
'config_file = "agents/code-explorer.toml"' \
'[agents.implementer]' \
'config_file = "agents/implementer.toml"' \
'[agents.test_validator]' \
'config_file = "agents/test-validator.toml"' \
'[agents.code_reviewer]' \
'config_file = "agents/code-reviewer.toml"' \
'[agents.git_finalizer]' \
'config_file = "agents/git-finalizer.toml"' \
'[sandbox_workspace_write]' \
'network_access = false' \
'[mcp_servers.laravel-boost]' \
'[mcp_servers.laravel-boost.tools.search-docs]' \
'[mcp_servers.laravel-boost.tools.tinker]' \
'[mcp_servers.gitea]' &&
[[ "$(rg -c '^\[agents\.[a-z_]+\]$' .codex/config.toml)" == "5" ]]
}
check "project config preserves MCP and applies bounded agent defaults" configuration_ok
model_catalog_ok() {
codex debug models --bundled |
jq -e '
([.models[] | select(.slug == "gpt-5.5") |
.supported_reasoning_levels[].effort] | index("high") != null)
and
([.models[] | select(.slug == "gpt-5.4-mini") |
.supported_reasoning_levels[].effort] |
(index("medium") != null and index("high") != null))
' >/dev/null
}
check "local bundled model catalog supports every role profile" model_catalog_ok
strict_config_ok() {
codex app-server --strict-config --listen stdio:// </dev/null >/dev/null 2>&1
}
check "real project configuration parses strictly" strict_config_ok
git_guidance_ok() {
local file
local executable_pattern
executable_pattern='^[[:space:]]*(git[[:space:]]+(add[[:space:]]+(-A|--all)|push|fetch|pull|rebase|reset|clean|merge|commit[[:space:]]+--amend)|gh[[:space:]]+pr|tea[[:space:]]+pr)'
for file in \
.codex/skills/giteaflow/SKILL.md \
.codex/skills/platform-feature-finish/SKILL.md; do
! rg -n "${executable_pattern}" "${file}" || return 1
! rg -n -i 'commit all changes|automatically (commit|push)|create a pull request' "${file}" || return 1
file_contains_all "${file}" \
'explicit' \
'scripts/codex-safe-local-commit' \
'remote' || return 1
done
}
check "governed Git skills contain no executable broad or remote completion flow" git_guidance_ok
helper_contract_ok() {
[[ -x scripts/codex-safe-local-commit ]] || return 1
! rg -n 'git[[:space:]].*(push|fetch|pull)|--no-verify|reset[[:space:]]+--hard|git[[:space:]]+clean' \
scripts/codex-safe-local-commit || return 1
file_contains_all scripts/codex-safe-local-commit \
'normalize_and_validate_paths' \
'assert_rename_authority' \
'assert_commit_shape' \
'publish_commit' \
'prepare_isolated_repository' \
'import_verified_object_closure' \
'objects/info/alternates' \
'update-ref' \
'index-lock-unavailable' \
'stale-fingerprint' \
'candidate-drift' \
'helper_remote_commands=0' \
'.env.*' \
'*.pem' \
'*.key' \
'id_rsa' \
'id_ed25519'
}
check "local commit helper exposes required safeguards and no remote command path" helper_contract_ok
tests_present_ok() {
[[ -x scripts/tests/codex-safe-local-commit-test ]] || return 1
[[ -x scripts/tests/codex-agent-reference-workflow ]] || return 1
[[ -f apps/platform/tests/Feature/Guards/CodexAgentFoundationContractTest.php ]] || return 1
file_contains_all scripts/tests/codex-safe-local-commit-test \
'case_tracked_success' \
'case_mixed_paths_and_encoding' \
'case_rename_requires_both_paths' \
'case_stale_fingerprint_refuses_without_mutation' \
'case_hook_rejection_preserves_real_state' \
'case_hook_staged_drift_refuses_publication' \
'case_signal_cleanup' || return 1
file_contains_all scripts/tests/codex-agent-reference-workflow \
'code_explorer' \
'implementer' \
'test_validator' \
'code_reviewer' \
'git_finalizer' \
'validation-failed' \
'review-failed' \
'cycle-limit-escalated'
}
check "required deterministic test and reference proof is present" tests_present_ok
auto_commit_disabled_ok() {
awk '
/^[[:space:]]*(default|enabled):/ && $2 != "false" {
invalid = 1
}
END { exit invalid }
' .specify/extensions/git/git-config.yml
}
check "all Spec Kit automatic commit switches remain disabled" auto_commit_disabled_ok
allowlist_ok() {
local path
local candidate_paths=()
local unique_paths
[[ -n "${base_range}" ]] || return 1
while IFS= read -r -d '' path; do
candidate_paths+=("${path}")
done < <(
{
git diff --name-only -z "${base_range}"
git diff --cached --name-only -z
git diff --name-only -z
git ls-files --others --exclude-standard -z
}
)
unique_paths="$(mktemp "${TMPDIR:-/tmp}/codex-agent-candidate.XXXXXX")"
validator_temporaries+=("${unique_paths}")
printf '%s\n' "${candidate_paths[@]}" | LC_ALL=C sort -u >"${unique_paths}"
while IFS= read -r path; do
[[ -z "${path}" ]] && continue
candidate_path_allowed "${path}" || return 1
done <"${unique_paths}"
}
check "committed staged unstaged and untracked candidate paths stay in the allowlist" allowlist_ok
candidate_identity_ok() {
local paths_file raw_diff manifest helper_output
[[ -n "${merge_base}" ]] || return 1
[[ -n "${candidate_tree}" ]] || return 1
review_identity_baseline="${merge_base}"
paths_file="$(mktemp "${TMPDIR:-/tmp}/codex-agent-review-paths.XXXXXX")"
validator_temporaries+=("${paths_file}")
git diff-tree --no-commit-id --name-status -z --find-renames -r \
"${merge_base}" "${candidate_tree}" |
collect_name_status_paths |
LC_ALL=C sort -u >"${paths_file}"
review_identity_paths=()
while IFS= read -r path; do
[[ -n "${path}" ]] || continue
[[ "${path}" != *$'\n'* && "${path}" != *$'\r'* ]] || return 1
candidate_path_allowed "${path}" || return 1
review_identity_paths+=("${path}")
done <"${paths_file}"
[[ ${#review_identity_paths[@]} -gt 0 ]] || return 1
[[ ${#finalization_identity_paths[@]} -gt 0 ]] || return 1
raw_diff="$(mktemp "${TMPDIR:-/tmp}/codex-agent-review-diff.XXXXXX")"
validator_temporaries+=("${raw_diff}")
git diff-tree --no-commit-id --raw -z --find-renames -r \
"${merge_base}" "${candidate_tree}" >"${raw_diff}"
manifest="$(mktemp "${TMPDIR:-/tmp}/codex-agent-review-manifest.XXXXXX")"
validator_temporaries+=("${manifest}")
{
printf 'tenantpilot-spec-458-review-candidate-v2\0'
printf 'branch\0%s\0' "${candidate_identity_branch}"
printf 'head\0%s\0' "${candidate_identity_head}"
printf 'baseline\0%s\0' "${review_identity_baseline}"
printf 'tree\0%s\0' "${candidate_tree}"
for path in "${review_identity_paths[@]}"; do
printf 'path\0%s\0' "${path}"
done
printf 'raw-diff\0'
cat "${raw_diff}"
} >"${manifest}"
review_identity_fingerprint="$(sha256_file "${manifest}")"
[[ "${review_identity_fingerprint}" =~ ^[0-9a-f]{64}$ ]] || return 1
helper_args=(
fingerprint
--repo "${repository_root}"
--expected-head "${candidate_identity_head}"
)
for path in "${finalization_identity_paths[@]}"; do
helper_args+=(--path "${path}")
done
helper_output="$(scripts/codex-safe-local-commit "${helper_args[@]}")" || return 1
finalization_identity_fingerprint="$(
awk -F= '$1 == "candidate_sha256" { print $2; exit }' <<<"${helper_output}"
)"
[[ "${finalization_identity_fingerprint}" =~ ^[0-9a-f]{64}$ ]]
}
check "review and helper-compatible finalization identities are deterministic and allowlisted" candidate_identity_ok
printf 'result=%s passed=%d failed=%d\n' "$([[ ${failed} -eq 0 ]] && echo passed || echo failed)" "${passed}" "${failed}"
if [[ ${failed} -ne 0 ]]; then
printf 'failure=%s\n' "${errors[@]}" >&2
exit 1
fi
printf 'candidate_branch=%s\n' "${candidate_identity_branch}"
printf 'candidate_head=%s\n' "${candidate_identity_head}"
printf 'review_baseline=%s\n' "${review_identity_baseline}"
printf 'review_path_count=%d\n' "${#review_identity_paths[@]}"
printf 'review_path=%s\n' "${review_identity_paths[@]}"
printf 'review_sha256=%s\n' "${review_identity_fingerprint}"
printf 'finalization_baseline=%s\n' "${finalization_identity_baseline}"
printf 'finalization_path_count=%d\n' "${#finalization_identity_paths[@]}"
printf 'finalization_path=%s\n' "${finalization_identity_paths[@]}"
printf 'finalization_sha256=%s\n' "${finalization_identity_fingerprint}"
printf 'candidate_identity_contract=review_identity_full_base_range;finalization_identity_helper_delta_vs_head;no_self_referential_report_fingerprint\n'

View File

@ -0,0 +1,119 @@
# Specification Quality Checklist: Codex Agent Foundation and Safe Local Finalization
**Purpose**: Validate specification completeness and quality before planning
**Created**: 2026-07-28
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No application implementation details are embedded in the behavioral
requirements.
- [x] The specification focuses on user safety, responsibility separation, and
verifiable outcomes.
- [x] The specification is understandable without the original pasted proposal.
- [x] All mandatory sections are complete.
## Requirement Completeness
- [x] No unresolved `[NEEDS CLARIFICATION]` markers remain.
- [x] Requirements are testable and unambiguous.
- [x] Success criteria are measurable.
- [x] Success criteria are independent of TenantPilot application implementation.
- [x] Acceptance scenarios cover the primary workflows.
- [x] Edge cases cover casing, path encoding, stale state, conflicts, hooks, parent
overrides, model drift, and configuration preservation.
- [x] Scope and non-goals are explicit.
- [x] Dependencies and assumptions are explicit.
- [x] Risks and mitigations are explicit.
## Repository Governance Gates
- [x] Candidate Check contains every mandatory rubric field, exactly one approval
class, a six-dimension score, and the already-applied scope shrink required for a
9/12 candidate.
- [x] Base, target, and diff baseline are explicitly `platform-dev`.
- [x] Baseline resolution reuses `scripts/resolve-feature-base`, and allowlist proof
covers committed base-range, staged, unstaged, and untracked paths.
- [x] Completed historical specs remain read-only.
- [x] Proportionality review explicitly answers all six BLOAT-001 questions for the
five-role taxonomy and commit-helper abstraction.
- [x] Test purpose, affected lanes, fixture cost, heavy-family visibility, budget
impact, and workflow outcome `document-in-feature` are documented consistently.
- [x] The new focused Pest process contract and both existing Pest guard consumers
of `Agents.md` are included without expanding into application runtime work.
- [x] `ProductSurfaceContractGateTest.php` retains its current `surface-guard`
Heavy-Governance classification; directory placement is not used to reclassify it.
- [x] No-legacy posture and Product Surface/UI Surface impacts are documented.
- [x] Workspace, RBAC, OperationRun/evidence, provider, persistence, and secret
applicability are explicitly resolved.
- [x] Browser proof and Human Product Sanity are explicitly N/A.
- [x] Implementation allowlist and amendment boundary are explicit.
- [x] Follow-up candidates contain all intentionally deferred orchestration.
## Feature Readiness
- [x] Every P1 user story has an independent test.
- [x] Explicit authorization is required for local commit creation.
- [x] Remote Git operations remain outside scope and authority.
- [x] Read-only roles and independent reviewer separation are explicit.
- [x] Current model availability must be validated rather than assumed.
- [x] Real strict parsing uses
`codex app-server --strict-config --listen stdio://`; a help-only command is not
accepted as configuration proof.
- [x] Bundled/offline and authenticated/current model catalog evidence are
distinguished.
- [x] Role filenames, underscore identifiers, sandbox defaults, and approval
policies are explicit.
- [x] Parent runtime overrides are documented for both sandbox and approval
defaults.
- [x] Fingerprint and commit modes are separately specified.
- [x] A rename requires both old and new paths in the explicit allowlist and
fingerprint.
- [x] Review evidence and finalization authority bind two identities: full
baseline-to-candidate review identity and helper-compatible delta-vs-HEAD
finalization identity.
- [x] Validator, reviewer, and explicit authority handoffs carry exact unique
branch/head/baseline/tree/path/fingerprint fields; duplicate or mismatched path
and combined-identity fields refuse before helper invocation.
- [x] The helper finalization fingerprint is emitted in the external
validator/finalizer handoff and is not embedded as a self-referential report
value.
- [x] Post-hook validation covers HEAD, exact authorized path set, tree/content
identity, and absence of unauthorized paths before publication.
- [x] Remote proof is truthfully limited to `helper_remote_commands=0`; arbitrary
repository hook side effects are an explicit residual boundary.
- [x] Hook execution runs in a fully separate temporary Git repository with
isolated refs/config/remotes; hook-created refs and remote config do not escape
into the source repository.
- [x] Live MCP preservation is checked statically, while actual role spawning uses
a disposable role-only configuration that cannot start the repository MCP blocks.
- [x] Forbidden-action validation distinguishes executable guidance from an
explicit prohibition.
- [x] V1 does not claim standalone certification for critical or remote work.
- [x] Failure-state preservation is an acceptance requirement.
- [x] Implementation start requires a separately authorized prep commit, clean
feature branch, and repository-required short-lived session branch.
- [x] The close-out report includes test governance, completed-spec preservation,
unrelated-failure disposition, and follow-up candidates.
- [x] No open question blocks planning.
## Review Outcome
- [x] Review outcome: `acceptable-special-case` — bounded repository-governance
foundation with no runtime/UI expansion.
- [x] Workflow outcome: `document-in-feature`.
- [x] Final note location:
`specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`.
## Notes
- The original proposal's `dev` baseline was corrected to `platform-dev` because the
current governance files and relevant completed specs are present there, while
`dev` has substantial unrelated divergence.
- The original unsupported `gpt-5.6-*` role pins were replaced by a requirement to
validate the installed Codex catalog and by the currently supported V1 profile.
- The preparation package was committed separately with maintainer authority as
`f2dde84a`; implementation continues on the recorded session branch without any
separately authorized implementation commit.
- Checklist items were revalidated on 2026-07-28 after the installed-CLI schema
amendment to FR-013/FR-015, Plan D3, Research R3, and Tasks T014/T016.

View File

@ -0,0 +1,361 @@
# Implementation Report: Codex Agent Foundation and Safe Local Finalization
**Spec**: `458-codex-agent-foundation-safe-local-finalization`
**Implementation branch**:
`feat/458-codex-agent-foundation-safe-local-finalization-session-1785259041`
**Prepared package commit**: `f2dde84a`
**Implementation start HEAD**: `f2dde84a`
**Resolved baseline**: `origin/platform-dev`
**Resolved baseline commit / merge base**:
`ad7a8583466984de579c9021eb558c7d2b6c89dd`
**Status**: implementation complete — current identities and independent approval
remain external handoff evidence
## Activated Guidance and Hard Gates
- Activated skills:
- `spec-kit-implementation-loop`: bounded implementation, validation, review,
and fix loop.
- `tenantpilot-spec-readiness-gate`: active-package readiness and scope checks.
- `pest-testing`: focused Pest 4 process and governance tests.
- `openai-docs`: current Codex configuration/model contract, with local CLI
schema and catalog as implementation truth.
- Repository hard-gate skills for workspace scope, RBAC, OperationRun, customer
output, evidence anchors, provider freshness, and Coverage v2 are not activated:
the implementation allowlist enters none of those product/runtime domains.
- Product Surface and browser skills are not activated because no rendered UI
surface changes.
- Preparation authorization: the maintainer separately authorized a local commit
containing exactly the six prepared Spec 458 files.
- Preparation commit result: `f2dde84a`; no push or pull request was performed.
- Session branch gate: PASS.
- Clean worktree after preparation commit: PASS.
- Base/target/diff baseline gate: PASS; all are `platform-dev` and the canonical
resolver selected `origin/platform-dev`.
- Implementation candidate before edits: exactly the six committed Spec 458
preparation files; staged, unstaged, and untracked sets were empty.
## Pre-change Evidence
- Root instruction casing: tracked `Agents.md`; `AGENTS.md` absent.
- MCP table inventory:
- `mcp_servers.laravel-boost`
- `mcp_servers.laravel-boost.tools.search-docs`
- `mcp_servers.laravel-boost.tools.tinker`
- `mcp_servers.gitea`
- Spec Kit automatic commit posture: `auto_commit.default` and every
`before_*`/`after_*` entry are `false`.
- Unsafe active Git guidance:
- `.codex/skills/giteaflow/SKILL.md` instructs committing all changes, pushing,
and creating a pull request.
- `.codex/skills/platform-feature-finish/SKILL.md` contains executable
`git add -A`, fetch/push, rebase, `reset --hard`, and force-update flows.
## Codex Capability Baseline
- CLI: `codex-cli 0.133.0`.
- Offline bundled catalog:
- `gpt-5.5`: `low, medium, high, xhigh`
- `gpt-5.4-mini`: `low, medium, high, xhigh`
- Current authenticated catalog:
- `gpt-5.5`: `low, medium, high, xhigh`
- `gpt-5.4-mini`: `low, medium, high, xhigh`
- Required pairs:
- `gpt-5.5/high`: PASS
- `gpt-5.4-mini/medium`: PASS
- `gpt-5.4-mini/high`: PASS
- Pre-change strict project-config parse:
`codex app-server --strict-config --listen stdio:// </dev/null`: PASS.
## Implementation Evidence
- Implementation-discovered configuration correction:
- the current OpenAI Codex reference documents both
`agents.<name>.description`/`config_file` declarations and
`agents.max_threads` as a compatibility alias;
- installed CLI `0.133.0` rejects the newer scalar
`agents.enabled`, `agents.max_concurrent_threads_per_session`,
`agents.default_subagent_model`, and
`agents.default_subagent_reasoning_effort` fields as malformed role
declarations;
- read-only CLI probes accepted `agents.max_threads = 3`, an explicit role
declaration pointing at an existing role layer, and the existing
`sandbox_workspace_write.network_access = false` setting;
- Spec FR-013/FR-015, Plan D3, Research R3, and Tasks T014/T016 were amended
before further implementation under T046. The five roles remain individually
model/reasoning pinned.
- Canonical instruction contract:
- Git records the case-only change as `Agents.md -> AGENTS.md`;
- the complete prior instruction contract is preserved and the bounded
explorer → implementer → validator → reviewer → finalizer handoff, structured
findings, three-cycle cap, explicit local-commit authority, remote prohibition,
and certification boundary are added;
- only current active references in the two named docs and two governance tests
were corrected; the intentional historical `Agents.md` statement remains.
- Five role contracts:
- exactly five hyphenated TOML files expose the approved underscore names,
discovery descriptions, developer contracts, catalog-supported
model/reasoning pairs, sandbox defaults, and approval policies;
- `.codex/config.toml` preserves all four existing MCP tables, declares all five
role layers, caps concurrency at three, and disables workspace-write network
access by default.
- Local finalization:
- `scripts/codex-safe-local-commit` implements read-only fingerprint and
explicitly authorized commit modes over exact paths;
- the finalization identity binds the reviewed HEAD, candidate tree, exact
sorted delta-vs-HEAD paths, both sides of renames, and NUL-safe raw diff into
the helper SHA-256 emitted outside this self-fingerprinted report;
- normal hooks run in a fully separate temporary Git repository with its own
common directory, index, refs, and config, object alternates to the source
object store, and an absolute source `hooksPath`;
- only the verified object closure is imported back into the source repository;
publication revalidates HEAD, candidate fingerprint/tree/path shape and
atomically updates only the current feature ref before reconciling the real
index;
- protected/detached/in-progress/staged/conflicted/stale/unsafe-path states fail
closed, both rename paths are mandatory, temporary state is trapped, and the
helper contains no remote Git command path.
- Active Git skills now provide read-only handoff/finish guidance and delegate the
only permitted local commit mutation to the helper after separate explicit user
authority. Push and pull-request creation remain outside their authority.
- Reference workflow:
- deterministic fixture handoffs cover all five roles, preserve unrelated dirty
work, create one explicitly authorized local fixture commit, and prove
validation/review refusal plus three-cycle escalation;
- the authenticated smoke runs with `--ignore-user-config`, no MCP tables,
read-only parent sandbox, five explicit role declarations, and
`fork_turns=none`;
- successful smoke evidence requires exactly one completed `spawn_agent` event
and parses the completed child wait result for the role-only
`code_explorer-v1` contract identifier plus expected marker; unknown-role,
full-history, and built-in-explorer fallback output are rejected.
## Candidate Identity Handoff Status
- Candidate branch:
`feat/458-codex-agent-foundation-safe-local-finalization-session-1785259041`
- Candidate committed HEAD:
`f2dde84aac283ed33ab853eb8fbdb3d1b075201a`
- Resolved diff baseline:
`ad7a8583466984de579c9021eb558c7d2b6c89dd`
- Review identity path set: full resolved-baseline-to-candidate path set; the
expected current count is 26 and includes both `Agents.md` and `AGENTS.md` for
the canonical casing rename.
- Finalization identity path set: exact delta-vs-HEAD path set emitted by
`scripts/validate-codex-agent-foundation`; the expected current count is 25
because `quickstart.md` is unchanged since the prepared HEAD.
- Concrete review and finalization fingerprints are emitted only by the external
validator handoff. This report intentionally does not embed the helper
finalization fingerprint because the report is part of the reviewed path set.
- Previously recorded candidate hashes and approval lines are superseded by this
correction cycle and are not current review evidence.
- Exact ordered review paths expected in the validator handoff (26):
1. `.codex/agents/code-explorer.toml`
2. `.codex/agents/code-reviewer.toml`
3. `.codex/agents/git-finalizer.toml`
4. `.codex/agents/implementer.toml`
5. `.codex/agents/test-validator.toml`
6. `.codex/config.toml`
7. `.codex/skills/giteaflow/SKILL.md`
8. `.codex/skills/platform-feature-finish/SKILL.md`
9. `AGENTS.md`
10. `Agents.md`
11. `apps/platform/tests/Feature/Guards/CodexAgentFoundationContractTest.php`
12. `apps/platform/tests/Feature/Guards/ConstitutionBranchTopologyContractTest.php`
13. `apps/platform/tests/Feature/Guards/ProductSurfaceContractGateTest.php`
14. `docs/HANDOVER.md`
15. `docs/PROJECT_SUMMARY.md`
16. `scripts/codex-safe-local-commit`
17. `scripts/tests/codex-agent-reference-workflow`
18. `scripts/tests/codex-safe-local-commit-test`
19. `scripts/validate-codex-agent-foundation`
20. `specs/458-codex-agent-foundation-safe-local-finalization/checklists/requirements.md`
21. `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
22. `specs/458-codex-agent-foundation-safe-local-finalization/plan.md`
23. `specs/458-codex-agent-foundation-safe-local-finalization/quickstart.md`
24. `specs/458-codex-agent-foundation-safe-local-finalization/research.md`
25. `specs/458-codex-agent-foundation-safe-local-finalization/spec.md`
26. `specs/458-codex-agent-foundation-safe-local-finalization/tasks.md`
## Validation Results
- `codex --version`: PASS — `codex-cli 0.133.0`.
- Offline bundled and current authenticated catalogs: PASS —
`gpt-5.5` and `gpt-5.4-mini` both expose
`low, medium, high, xhigh`; every pinned pair is present.
- `codex app-server --strict-config --listen stdio:// </dev/null`: PASS.
- `bash -n` over the helper, validator, helper matrix, and reference workflow:
PASS. `shellcheck` is not installed locally; no shellcheck claim is made.
- `scripts/tests/codex-safe-local-commit-test`: PASS — 17/17, including the
clean-index case-only rename, executable/symlink mode preservation, and isolated
hook ref/remote-config regressions.
- `scripts/validate-codex-agent-foundation`: PASS — 16/16, including temporary
candidate-tree casing, separate review/finalization identities, canonical
baseline resolution, and all-state allowlist aggregation.
- `scripts/tests/codex-agent-reference-workflow`: PASS — five handoffs,
evidence-gate validation/review refusal with unchanged worktree/index/refs/remotes,
three-cycle escalation, one fixture-local commit, unrelated dirty work preserved,
zero remotes, and `helper_remote_commands=0`.
- `scripts/tests/codex-agent-reference-workflow --spawn-smoke`: PASS —
authenticated custom `code_explorer` child, read-only/approval-never role
defaults, role contract token, no fallback, and zero fixture MCP tables.
- Sail focused test command covering
`CodexAgentFoundationContractTest.php`,
`ConstitutionBranchTopologyContractTest.php`, and
`ProductSurfaceContractGateTest.php`: PASS — 10 tests, 73 assertions.
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`:
PASS.
- `git diff --check` and `git diff --cached --check`: PASS.
- Final candidate allowlist: PASS — 26 exact full-feature paths, all permitted by
Spec 458; application runtime/website paths and completed Specs 416/439/444
changed: zero.
- Final repository HEAD remains the separately authorized preparation commit
`f2dde84aac283ed33ab853eb8fbdb3d1b075201a`; no implementation commit, push,
pull request, merge, rebase, reset, clean, or remote mutation was performed.
## Independent Review and Correction Cycles
- Correction cycle 1 (pre-review validation):
- finding: the first authenticated-smoke assertion accepted a final parent
message even though CLI trace evidence showed `code_explorer` was unknown and
the parent fell back to the built-in `explorer`;
- severity/impact: high — custom-role discovery was not actually proven;
- correction: load all five roles through explicit CLI config overrides, require
`fork_turns=none`, require a completed `spawn_agent` event and role-only
contract token, and reject unknown-role/full-history/built-in fallback output;
- result: corrected smoke PASS and all affected validator/reference checks rerun
successfully.
- Correction cycle 2 (independent review):
- high: publication signal windows could leave the branch ref and real index
inconsistent;
- high: validation/review evidence lacked the complete 26-path candidate identity
and deterministic fingerprint;
- medium: nested deletions and renames whose parent directory disappeared were
rejected;
- medium: validation/review failure paths wrote refusal text without exercising a
finalization gate;
- medium: spawn evidence did not bind the contract token and marker to the
completed child result;
- medium: hook-bearing helper tests inherited the caller environment;
- low: active package lifecycle metadata still described untracked preparation.
- Correction cycle 2 bounded fixes:
- publication metadata is established before compare-and-swap, the original
index is retained, INT/TERM is masked and discarded across the tiny critical
section, and deterministic injected faults after ref update and index
installation must restore both ref and index;
- the static validator emits the full branch/HEAD/baseline/path/fingerprint
identity with the documented evidence-only normalization;
- path containment resolves the nearest existing ancestor while retaining
symlink escape refusal, and the mixed-path case now includes nested deletion
and rename from removed directories;
- the reference workflow uses a real evidence gate for passing and failing
finalization, with before/after worktree/index/ref/remote snapshots;
- spawn proof parses exactly one completed spawn plus the completed child wait
message;
- every helper self-test invocation now runs through an empty environment with
isolated HOME/TMPDIR/global Git config and only explicit test variables;
- spec/plan/checklist lifecycle metadata was corrected and revalidated.
- Correction cycle 2 validation result: PASS — helper 14/14, validator 14/14,
deterministic reference workflow, structured authenticated custom-role smoke,
Sail 10 tests/73 assertions, Pint, strict config, shell syntax, model catalogs,
diff checks, scope checks, and no implementation commit.
- Correction cycle 3 (independent re-review):
- low: the report said publication signals were deferred although the
implementation masks/discards them, and described one rather than both
normalized review-result lines;
- correction: the two audit statements now match the helper trap behavior and
validator normalization exactly;
- validation result: candidate identity and diff checks rerun successfully.
- Implementer correction cycle 1 after root review:
- confirmed findings: reference workflow read canonical casing from the real
index on case-insensitive macOS; helper lacked explicit case-only
`Agents.md -> AGENTS.md` coverage; active docs still described one
candidate identity, detached worktree hooks, and self-referential fingerprint
normalization;
- bounded fixes: reference workflow now proves casing through a temporary
case-sensitive candidate tree, helper/self-test cover case-only rename
publication with a clean real index, validator builds a temporary candidate
tree and emits separate review/finalization identities, and active
governance/spec artifacts now document the two-identity contract and
isolated hook repository;
- validation rerun: PASS — shell syntax, Helper 16/16, Validator 16/16,
deterministic reference workflow, authenticated MCP-free custom-role spawn,
Sail 10 tests/73 assertions, Pint, diff checks, and clean real index;
- no final independent review approval is claimed by this implementer report.
- Implementer correction cycle 2 after root review:
- confirmed finding: case-only rename handling hardcoded `100644`, so executable
and symlink modes could be lost while the helper still reported success;
- bounded fix: Helper and validator now remove the old casing from the temporary
index and then use temporary-index `git add -- <new-path>` so Git records the
real regular/executable/symlink mode and content;
- validation rerun: PASS — shell syntax, Helper 17/17, Validator 16/16. The
reference workflow required one additional structured-identity hardening pass.
- Implementer correction cycle 3 close-out:
- bounded fix: reference workflow now includes `candidate_identity` in the
unique identity signature, verifies it equals
`review=<candidate_review_sha256>;finalization=<candidate_finalization_sha256>`,
rejects empty/duplicate path values while preserving order/spaces/tabs, tests
duplicate finalization path and mismatched combined identity refusals before
helper invocation, and suppresses disposable checkout noise;
- validation rerun: PASS — shell syntax, Helper 17/17, deterministic reference
workflow, Validator 16/16, diff checks, and clean real index; no hash or final
approval is recorded in this self-fingerprinted report.
## Product and Framework Close-out
- Product Surface Impact: N/A - repository-local developer governance only.
- UI Surface Impact: no rendered UI surface changed.
- Browser proof: N/A - no rendered UI surface changed.
- Human Product Sanity: N/A.
- Visible complexity outcome: unchanged.
- Livewire v4.0+ compliance: unchanged; no Livewire source changed.
- Panel provider registration: unchanged; `bootstrap/providers.php` is untouched.
- Global search: unchanged; no Filament resource changed.
- Destructive/high-impact product actions: none. Local Git commit creation is a
repository high-impact operation and is limited to explicit authority, explicit
paths, reviewed HEAD/fingerprint, and fail-closed helper validation.
- Asset strategy: no assets; `filament:assets` is not required.
- Deployment impact: no application deployment, environment variable, migration,
queue, scheduler, storage, or Dokploy impact. A trusted checkout/new Codex
session may be required to load project-local role configuration.
- Critical-domain certification boundary: the five-role topology does not certify
critical domain, security, destructive product, production, or remote Git work;
all applicable repository/manual hard gates remain required.
## Test Governance
- Workflow outcome: `document-in-feature`.
- Review outcome: `acceptable-special-case`.
- Feature proof: focused helper process contract and Constitution branch-topology
contract.
- Heavy-Governance proof: existing Product Surface `surface-guard`, full shell
helper matrix, static foundation validator, and disposable role workflow.
- Browser, Unit, PostgreSQL, and confidence lanes: N/A.
- Fixture cost: disposable local Git repositories only; no database, workspace,
membership, provider, session, browser, factory, or seed context.
- Budget/baseline/trend follow-up: none; authenticated role spawn remains explicit
non-deterministic local proof rather than CI.
## Completed-Spec and Scope Preservation
- Completed Specs 416, 439, and 444: unchanged historical evidence.
- TenantPilot application runtime: unchanged.
- `apps/website/**`: unchanged.
- Allowlist exceptions: none.
## Residual Risks and Follow-up Candidates
- Role sandbox and approval settings remain defense-in-depth defaults because a
parent session can apply live runtime overrides.
- The helper proves its own command path contains zero remote operations and
contains hook-created candidate drift, but arbitrary repository hook code can
still have external side effects outside the helper's controllable Git-state
boundary.
- `agents.max_threads` is a documented compatibility alias required by the
installed CLI. Replacing it with the newer scalar agent settings requires a
future CLI upgrade plus strict-config and authenticated role-spawn proof.
- Authenticated custom-role spawning remains a local Heavy-Governance smoke rather
than deterministic CI evidence.
- Follow-up candidates: none in scope. Revisit only when the installed Codex CLI
accepts the newer scalar agent settings or repository policy explicitly expands
into remote finalization.

View File

@ -0,0 +1,629 @@
# Implementation Plan: Codex Agent Foundation and Safe Local Finalization
**Branch**: `feat/458-codex-agent-foundation-safe-local-finalization`
**Spec**: [spec.md](./spec.md)
**Prepared**: 2026-07-28
**Implementation status**: complete — final evidence is recorded in the implementation report
## Summary
Create a repository-local Codex governance foundation without changing the
TenantPilot application. The implementation will:
1. rename the root instruction contract to canonical `AGENTS.md`;
2. extend the existing project Codex configuration without losing MCP entries;
3. add exactly five narrow custom roles;
4. remove broad/destructive/remote completion behavior from two active Git skills;
5. add an explicit-path, fingerprint-bound, local-only commit helper;
6. verify shape and behavior through static validation, temporary Git repository
self-tests, and a disposable five-role reference workflow.
The plan deliberately separates declarative agent defaults from enforceable helper
behavior. Codex role sandbox and approval defaults are defense in depth; the local
commit helper and its tests provide the deterministic safety boundary for
finalization.
## Technical Context
**Artifact languages**: Markdown, TOML, POSIX-oriented shell
**Application stack**: Laravel 12, Filament 5, Livewire 4, PostgreSQL — unchanged
**Primary dependencies**: installed Codex CLI, Git, standard shell utilities
**Storage**: N/A — no database or persisted application entity
**Testing**: shell/TOML static checks, temporary Git repositories, one new focused
Pest process contract, two focused existing Pest governance guards, guided
disposable Codex role smoke
**Target platform**: repository contributors' supported local macOS/Linux shell
environment
**Project type**: monorepo repository-governance tooling
**Performance goal**: new static validation and helper self-tests finish locally
without database or network dependencies; the three allowlisted Pest guard/process
files run focused through Sail
**Constraints**: no application runtime source, only three allowlisted application
test files, no helper-owned remote Git operation, no automatic commit, exactly five
roles, maximum three concurrent children
**Scale/scope**: one root instruction rename, one existing config extension, five
role files, two skill corrections, four governance scripts/tests, two existing Pest
guard updates, one new focused Pest process contract, bounded active-reference
corrections, and feature documentation
## Repository and Branch Declaration
- **Work classification**: repository governance affecting the platform development
workflow
- **Base**: `platform-dev`
- **Target**: `platform-dev`
- **Diff baseline**: `platform-dev`
- **Why not `dev`**: current routing, governance, and relevant completed-spec evidence
live on `platform-dev`; using `dev` would introduce a large unrelated divergent
diff. Promotion remains separately classified work.
- **Feature branch**:
`feat/458-codex-agent-foundation-safe-local-finalization`
## Activated Guidance
- `spec-kit-next-best-prep`: candidate gate, preparation workflow, analysis, and
readiness close-out.
- `speckit-specify`, `speckit-plan`, `speckit-tasks`, `speckit-analyze`: artifact
contracts and consistency checks.
- `.agent/skills/workflows/spec-readiness-gate`: final readiness scoring.
- `openai-docs`: current custom-agent and configuration behavior.
- `pest-testing`: two existing governance guards directly consume the renamed
instruction path.
Implementation will additionally activate only the repository skills required by
the changed artifact type. `tailwindcss-development` and browser testing are N/A
because no rendered UI is changed.
## Constitution Check
### Before design
| Gate | Status | Evidence |
|---|---|---|
| Spec candidate score and red-flag defense | PASS | 9/12 after the rubric-required scope shrink; exact mandatory fields and defense recorded in `spec.md` |
| Base/target/diff classification | PASS | Explicitly `platform-dev` |
| Completed-spec guard | PASS | Specs 416, 439, and 444 inspected and left read-only |
| Persisted entity/schema review | N/A | No application persistence |
| Abstraction/taxonomy proportionality | PASS | All six BLOAT-001 questions answered; exactly five current roles; one single-purpose helper; broader framework deferred |
| Test governance | PASS | Feature + Heavy-Governance purpose classification, temporary Git fixtures, three focused Pest files, and explicit `document-in-feature` outcome |
| Product Surface Contract | PASS/N/A | No rendered UI surface |
| Destructive/high-impact action posture | PASS | Product actions none; Git finalization requires explicit authorization and fail-closed guards |
### Post-design
| Gate | Status | Evidence |
|---|---|---|
| No speculative data model or API contract | PASS | `data-model.md` and `contracts/` intentionally omitted as N/A |
| Role taxonomy remains fixed and bounded | PASS | Five-role matrix below |
| Authority is not inferred from implementation permission | PASS | Commit, push, and PR authority remain separately explicit |
| Failure-state restoration is designed and testable | PASS | Helper protocol and refusal matrix below |
| Current Codex capability is verified | PASS | CLI 0.133.0 help/schema, official current subagent/config reference, real app-server strict parse, and local model catalog inspected; implementation must revalidate |
| Deployment impact is explicit | PASS | No app deploy; project-local config takes effect in a trusted checkout/new Codex session |
No constitution violation requires a complexity exception.
## Agent Context Update Disposition
The generic Spec Kit agent-context update is intentionally not run during
preparation. It would edit the current root instruction file before the controlled
case-safe rename and would therefore perform feature implementation outside the
prep-only scope. The implementation tasks own the rename and role-router update
explicitly. A later context update is allowed only if it targets canonical
`AGENTS.md`, preserves the bounded router, and produces no unrelated generated
content.
## Current Repository Evidence
- The tracked root instruction file is `Agents.md`; root `AGENTS.md` is absent.
- `.codex/config.toml` contains required Laravel Boost and Gitea MCP configuration
but no custom-agent configuration.
- `.codex/agents/` is absent.
- `.codex/skills/giteaflow/SKILL.md` currently instructs broad commit/push/PR
completion.
- `.codex/skills/platform-feature-finish/SKILL.md` currently includes `git add -A`,
rebase, hard reset, push, and force-update flows.
- `.specify/extensions/git/git-config.yml` already keeps all automatic commit
switches disabled.
- Two existing Pest governance guards directly read or assert `Agents.md`, and
active links in `docs/HANDOVER.md` would remain stale after the rename.
- Local Codex CLI `0.133.0` accepts the current project config with the real parse
path `codex app-server --strict-config --listen stdio://`; the earlier
`codex --strict-config --help` form is not accepted as parse evidence.
## Design Decisions
Full decision rationale is recorded in [research.md](./research.md).
### D1 — Canonical casing
Use a two-step temporary-name Git rename if required by the developer filesystem,
then end with exactly `AGENTS.md`. Do not retain a second file, link, or shim.
Update active repository references so the old casing cannot remain an alternate
usable authority; historical quoted evidence may remain only when clearly marked as
historical and ignored by routing. Bounded corrections are limited to
`docs/HANDOVER.md`, the current instruction reference in
`docs/PROJECT_SUMMARY.md`, and the two existing Pest governance guard tests.
### D2 — Fixed role inventory
| Role | Model profile | Reasoning | Declared sandbox | Approval | Sole responsibility |
|---|---|---|---|---|---|
| `code_explorer` | `gpt-5.4-mini` | `medium` | `read-only` | `never` | Discover repo truth, relevant instructions, paths, dependencies, and risks; produce a brief without edits. |
| `implementer` | `gpt-5.5` | `high` | `workspace-write` | `on-request` | Make only approved, allowlisted changes and report exact files/tests. |
| `test_validator` | `gpt-5.4-mini` | `high` | `workspace-write` | `on-request` | Run proportionate checks, create only disposable test state, and report commands/results. |
| `code_reviewer` | `gpt-5.5` | `high` | `read-only` | `never` | Independently review correctness, safety, scope, and evidence; never fix its own findings. |
| `git_finalizer` | `gpt-5.4-mini` | `medium` | `workspace-write` | `on-request` | Invoke the approved local helper only after explicit authority and successful evidence. |
These pins match the locally installed catalog at preparation time. Implementation
must re-run catalog validation; an unavailable pin is a hard stop requiring a
spec/plan amendment, not an improvised substitution.
### D3 — Project configuration
Extend `.codex/config.toml` with:
- `[agents]`
- `max_threads = 3`, the documented compatibility alias accepted by the
installed Codex CLI `0.133.0`
- one `[agents.<underscore-name>]` declaration for each approved role
- `description = "<concise discovery description>"`
- `config_file = "agents/<hyphenated-file>.toml"`
- `[sandbox_workspace_write]`
- `network_access = false`
Preserve the existing MCP tables byte-for-byte where practical. Do not mix legacy
and new permission-profile schemas. The global defaults documented for newer Codex
versions are not used because CLI `0.133.0` rejects them during strict parsing;
every role remains individually pinned. Each `.codex/agents/*.toml` carries the
required `name`, `description`, and `developer_instructions`, plus supported
role-specific `model`, `model_reasoning_effort`, `sandbox_mode`, and
`approval_policy`.
### D4 — Ordered evidence contract
The canonical instruction file defines these handoffs:
1. Explorer emits scope, constraints, evidence paths, relevant skills, and risks.
2. Implementer consumes the explorer brief and emits changed paths plus claimed
checks.
3. Validator independently runs commands and emits pass/fail/blocked evidence.
4. Reviewer consumes the diff and validator evidence, returning an approval or
structured findings.
5. Finalizer runs only after explicit local-commit authority, approved review, and a
matching reviewed state.
Findings cycle back to implementation, then relevant validation and review. Three
failed cycles cause escalation; they do not authorize broader scope.
Validator, reviewer, and finalizer handoffs bind two deterministic identities. The
review identity covers branch, full HEAD, baseline resolved through
`scripts/resolve-feature-base`, candidate tree, exact review path set, and review
fingerprint for the full feature. The finalization identity covers the reviewed
HEAD, exact delta-vs-HEAD path set, both sides of renames, and the
helper-compatible fingerprint emitted by `scripts/codex-safe-local-commit
fingerprint`. Any drift invalidates downstream evidence and returns the workflow
to validation.
The implementation report must not embed a concrete helper fingerprint for a path
set that includes the report itself. That helper fingerprint is emitted only in
the external validator/finalizer handoff.
### D5 — Git skill posture
- Convert `giteaflow` from an automatic commit/push/PR instruction into a safe
repository-host workflow guide that requires separately explicit authority for
every mutation and points local commit creation to the helper.
- Remove automatic/broad stage, rebase, reset, force-push, and remote-finalization
behavior from `platform-feature-finish`.
- Keep both skills active only if their revised instructions are unambiguous. If a
safe bounded purpose cannot be expressed during implementation, quarantine the
affected skill from active routing and document why.
- Do not change the existing Spec Kit auto-commit flags; validate that they remain
false.
### D6 — Local commit helper protocol
The helper interface will expose separate read-only fingerprint and local commit
modes:
```text
scripts/codex-safe-local-commit fingerprint \
--repo <absolute-repository-root> \
--expected-head <full-commit-oid> \
--path <repository-relative-path> [--path ...]
scripts/codex-safe-local-commit commit \
--repo <absolute-repository-root> \
--path <repository-relative-path> [--path ...] \
--message <non-empty-message> \
--expected-head <full-commit-oid> \
--expected-diff-sha256 <sha256>
```
It may support a NUL-delimited path-file option if needed for robust bulk input, but
must not accept a broad implicit path such as `.` or an empty allowlist.
#### Read-only preflight
Before mutation:
1. resolve and verify the repository root;
2. reject detached HEAD and protected branches (`dev`, `platform-dev`,
`website-dev`, `main`, `master`, and configurable release/production patterns);
3. reject merge, rebase, cherry-pick, revert, bisect, or unresolved conflicts;
4. require the user's real index to be semantically empty;
5. require current HEAD to equal the full reviewed HEAD;
6. normalize explicit paths and reject repository escapes, Git internals, and broad
root pathspecs;
7. reject duplicates, directories, unresolved globs, traversal, newline-containing
paths, and the minimum sensitive set `.env`, `.env.*`, `*.pem`, `*.key`,
`id_rsa`, and `id_ed25519`;
8. compute a deterministic SHA-256 manifest covering status, old/new path identity,
mode, and content for tracked, untracked, deleted, and renamed authorized paths;
a rename is valid only when both old and new paths are explicitly authorized;
9. in commit mode, compare the manifest fingerprint using an exact, non-prefix
match.
Fingerprint mode stops after emitting the reviewed HEAD, normalized explicit paths,
and SHA-256 identity. It does not mutate any repository state.
#### Isolated commit construction
Use disposable Git state rather than the user's real index for staging and hook
execution. Commit hooks run inside a fully separate temporary Git repository with
its own common directory, index, refs, and config, using object alternates to read
the source object store and an absolute source `hooksPath`. After hook execution,
only the verified object closure is imported into the source repository before
compare-and-swap publication. The implementation must preserve these invariants:
- only authorized paths are staged;
- normal commit hooks execute against disposable state;
- no helper failure mutates the user's worktree, index, branch ref, or remotes;
- after hooks run, the reviewed HEAD, exact authorized path set, resulting
tree/content identity, and absence of unauthorized paths are checked again
immediately before publish;
- successful publication is compare-and-swap/atomic with respect to reviewed HEAD;
- after success, the checked-out branch, real index, and worktree are semantically
consistent: authorized changes are committed, unrelated changes remain unstaged,
and no unrelated path enters the commit;
- any temporary index/repository/ref/config state is removed on success and
failure.
No implementation shortcut may skip hooks, use broad `git add`, invoke
`reset --hard`, clean the user's worktree, rewrite history, or make a helper-owned
remote/network call. Repository hooks are arbitrary external code: hook tests run
only in disposable repositories with no remotes and a sanitized environment, and
the helper does not claim to certify hook side effects outside Git state.
#### Evidence
The helper emits a stable result category (`committed` or `refused`), reviewed HEAD,
fingerprint, authorized paths, refusal code or local commit OID, and an explicit
`helper_remote_commands=0` marker. Finalizer output also states
`Push: NOT PERFORMED` and `Pull Request: NOT PERFORMED`. This marker describes only
the helper's own commands, not arbitrary repository hook behavior. Output must not
echo file contents, environment secrets, or credentials.
### D7 — Verification design
#### Helper unit/self-tests
`scripts/tests/codex-safe-local-commit-test` creates isolated temporary repositories
and covers at least:
- one tracked modification success;
- mixed tracked/untracked/deleted/renamed authorized paths;
- rename refusal unless both old and new paths are explicitly authorized;
- whitespace, Unicode, and leading-dash path names;
- unrelated dirty paths preserved;
- no explicit authorization (caller/finalizer contract);
- empty/broad/outside/sensitive path refusal;
- protected branch and detached HEAD refusal;
- staged index and conflict-operation refusal;
- stale HEAD and stale fingerprint refusal;
- changed state between validation and publish;
- hook rejection and hook-side file mutation confined to disposable state;
- commit content exactly equals the allowlist;
- one commit maximum and zero helper-owned remote commands;
- cleanup of disposable state on signals and failures.
Tests compare pre/post worktree, index, refs, and remote configuration rather than
relying only on exit codes.
#### Static foundation validator
`scripts/validate-codex-agent-foundation` checks:
- exact root instruction casing and no duplicate;
- exact five hyphenated role filenames, underscore `name` identifiers, and required
TOML fields including approval policy;
- supported local catalog model/reasoning pairs;
- read-only/write role matrix;
- strict project config parse and preserved MCP tables;
- max concurrency and network defaults;
- absence of executable unsafe instructions in the two governed Git skills while
distinguishing an explicit prohibition/example from an instruction to execute;
- auto-commit flags remain false;
- helper executable/presence and required safeguard markers;
- required self-test/reference-workflow coverage;
- feature allowlist compliance using the canonical
`scripts/resolve-feature-base` result and a candidate path union containing the
committed base-range, staged, unstaged, and untracked files.
#### Pest governance proof
Update only the canonical instruction path/expectation in:
- `apps/platform/tests/Feature/Guards/ProductSurfaceContractGateTest.php`;
- `apps/platform/tests/Feature/Guards/ConstitutionBranchTopologyContractTest.php`.
The historical Constitution Sync Impact assertion may retain the original casing as
historical evidence; only the active file lookup and active contract expectation
change. Add
`apps/platform/tests/Feature/Guards/CodexAgentFoundationContractTest.php` as a
focused process contract for the helper's critical success and fail-closed
behavior. Run all three files through Sail. Do not weaken or remove existing
assertions.
The new process contract and Constitution guard are classified by proving purpose
as Feature. `ProductSurfaceContractGateTest.php` remains in its current
`surface-guard` family and Heavy-Governance lane; its directory name does not
reclassify it as Feature. The full shell matrix, static foundation validator, and
disposable role workflow are explicit Heavy-Governance proofs. No Pest lane or
manifest definition changes.
#### Disposable reference workflow
`scripts/tests/codex-agent-reference-workflow` creates or describes a temporary,
non-application fixture with:
- a small requested text change;
- an unrelated dirty file;
- expected role handoff artifacts;
- one passing validation/review path with an explicitly authorized local fixture
commit;
- one failing validation/review path where finalization refuses;
- captured role names, sandbox/approval declarations, commands, evidence, and final
Git state.
Actual custom-role discovery/spawn is a local Heavy-Governance smoke, not an
always-on offline CI check. Static validation and fixture-state assertions remain
deterministic without Codex network access. The role-spawn smoke uses a disposable
role-only Codex configuration and must not copy, start, authenticate, or otherwise
exercise the repository's live MCP blocks. Static validation separately proves
that the real `.codex/config.toml` preserves those MCP tables.
## Test Governance Check
- **Test purpose / classification by changed surface**: Feature for the focused
helper process contract and Constitution branch-topology contract;
Heavy-Governance for the existing Product Surface `surface-guard`, full helper
shell matrix, static foundation validator, and role workflow; Unit and Browser
N/A.
- **Affected validation lanes**: focused Feature and existing
`heavy-governance`; confidence, PostgreSQL, browser, profiling, and JUnit N/A.
- **Why this lane mix is the narrowest sufficient proof**: Pest proves the critical
process contract and existing repository gates without database/application
context; exhaustive Git state combinations and role discovery remain explicit
governance work and are not hidden in a fast lane.
- **Narrowest proving commands**: the three focused Sail file commands, helper
shell suite, static validator, and role-only reference workflow listed under
Validation Commands.
- **Fixture / helper / factory / seed / context cost risks**: disposable local Git
repositories only; no database, workspace, membership, provider, session, browser,
factory, or seed context.
- **Expensive defaults or shared helper growth introduced?**: no; all new fixture
setup is feature-local and opt-in.
- **Heavy-family additions, promotions, or visibility changes**: contained new
repository-governance shell/role checks; the Product Surface guard keeps its
existing heavy family and lane.
- **Surface-class relief / special coverage rule**: N/A — no rendered surface.
- **Closing validation and reviewer handoff**: rerun the focused Pest files, full
helper suite, static validator, and both fixture outcomes after any finding; the
reviewer verifies purpose-based classification, candidate-state coverage, and
no hidden repository-MCP/network dependency.
- **Budget / baseline / trend follow-up**: none; contained feature-local cost.
- **Review-stop questions**: stop for incorrect lane classification, a full helper
matrix silently absorbed by fast feedback, repository MCP startup, shared
expensive setup, missing untracked/staged/unstaged coverage, or unbounded hook
claims.
- **Workflow outcome**: `document-in-feature`.
## File Layout
```text
AGENTS.md
.codex/
├── config.toml
├── agents/
│ ├── code-explorer.toml
│ ├── implementer.toml
│ ├── test-validator.toml
│ ├── code-reviewer.toml
│ └── git-finalizer.toml
└── skills/
├── giteaflow/SKILL.md
└── platform-feature-finish/SKILL.md
scripts/
├── codex-safe-local-commit
├── validate-codex-agent-foundation
└── tests/
├── codex-safe-local-commit-test
└── codex-agent-reference-workflow
apps/platform/tests/Feature/Guards/
├── CodexAgentFoundationContractTest.php
├── ProductSurfaceContractGateTest.php
└── ConstitutionBranchTopologyContractTest.php
docs/
├── HANDOVER.md
└── PROJECT_SUMMARY.md
specs/458-codex-agent-foundation-safe-local-finalization/
├── spec.md
├── plan.md
├── research.md
├── quickstart.md
├── tasks.md
├── checklists/requirements.md
└── implementation-report.md # created during implementation
```
## Implementation Sequence
### Phase 0 — Git safety prerequisite
- Require the preparation package to be reviewed and committed through separately
explicit maintainer authority; implementation permission does not authorize that
commit.
- Start implementation only from a clean feature branch and create the repository-
required short-lived
`feat/458-codex-agent-foundation-safe-local-finalization-session-<timestamp>`
branch. Stop if unrelated changes or uncommitted preparation artifacts remain.
### Phase 1 — Test-first governance skeleton
- Add helper self-test cases and static-validator expectations before the helper and
role files they validate.
- Add the focused Pest process contract before implementing helper behavior.
- Record the baseline model catalog, strict-config result, MCP tables, auto-commit
flags, branch, and changed-path allowlist.
### Phase 2 — Canonical contract and roles
- Perform the case-safe instruction rename.
- Correct only active root-instruction references and the two existing governance
guard lookups; preserve completed-spec and clearly historical evidence.
- Add role discovery/routing and explicit authority language to the canonical
instructions.
- Extend config and add the five role files.
- Prove strict parsing and role schema before continuing.
### Phase 3 — Git guidance and helper
- Correct or quarantine the two unsafe Git skills.
- Implement the helper as small named validation, fingerprint, isolated-stage,
publish, cleanup, and evidence functions.
- Run the self-test after each safety boundary is added.
### Phase 4 — Independent proof and close-out
- Complete static validation.
- Run the disposable five-role workflow and a no-commit failure path.
- Perform independent code review and fix all confirmed in-scope findings, with a
maximum of three cycles.
- Create `implementation-report.md`.
- Do not create a local commit unless the user separately asks for one and supplies
or approves the explicit finalization inputs.
## Requirement-to-Design Mapping
| Requirements | Design element |
|---|---|
| FR-001FR-003, FR-033 | D1 canonical casing |
| FR-004FR-011, FR-029FR-030 | D2 fixed roles + D4 ordered evidence |
| FR-012FR-015 | D3 project configuration + catalog validation |
| FR-016FR-018 | D5 Git skill posture |
| FR-019FR-024 | D6 helper protocol |
| FR-025FR-028, FR-031FR-032 | D7 verification + close-out |
## Validation Commands
Exact implementation commands are summarized in [quickstart.md](./quickstart.md).
The minimum close-out evidence includes:
```bash
codex --version
codex debug models --bundled
codex debug models
codex app-server --strict-config --listen stdio:// </dev/null
scripts/tests/codex-safe-local-commit-test
scripts/resolve-feature-base \
--branch-family=platform \
--work-classification=repository-governance \
--pull-request-target=platform-dev \
--spec=specs/458-codex-agent-foundation-safe-local-finalization/spec.md \
--format=kv
scripts/validate-codex-agent-foundation \
--branch-family platform \
--work-classification repository-governance \
--pull-request-target platform-dev
scripts/tests/codex-agent-reference-workflow
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/CodexAgentFoundationContractTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ProductSurfaceContractGateTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ConstitutionBranchTopologyContractTest.php
git diff --check
git status --short --untracked-files=all
```
`codex debug models --bundled` is the offline binary baseline; the default
`codex debug models` may refresh the authenticated catalog and is the current
availability proof. The reference workflow's actual spawn step may require an
authenticated local Codex session. Its disposable repository must never be the
TenantPilot working tree, and its role-only config must not contain the repository
MCP tables.
## Product/Framework Close-out Contract
- **Livewire v4.0+ compliance**: N/A to changed files; existing Laravel/Filament
runtime is untouched.
- **Provider registration**: no provider added or changed;
`bootstrap/providers.php` is untouched.
- **Global search**: no resource added or changed.
- **Destructive/high-impact product actions**: none. Local Git commit creation is a
repository high-impact operation guarded by explicit authority, confirmation of
exact paths/state, and fail-closed validation.
- **Asset strategy**: no assets added; `filament:assets` is not required for this
feature.
- **Testing**: repository-governance shell self-tests, static validation,
disposable role smoke, the new focused Pest process contract, and the two
focused existing Pest governance guards; no Livewire/Filament/browser test
applies.
- **Deployment**: no staging/production application deployment, migration, env var,
queue, cron, storage, or Dokploy impact. A trusted checkout/new Codex session may
be required to load project-local agent configuration.
- **Certification boundary**: the five-role V1 does not by itself certify critical
domain, security, destructive product, production, or remote Git work. Existing
hard gates and current explicit user authority continue to apply.
- **Historical and unrelated-state close-out**: completed Specs 416, 439, and 444
remain read-only historical evidence; the report lists unrelated failures and
any newly discovered out-of-scope need as a follow-up candidate instead of
rewriting completed packages or widening this implementation.
## Risks and Mitigations
| Risk | Mitigation |
|---|---|
| Case-only rename is lost on macOS | Two-step Git rename, focused Pest guard updates, and case-sensitive disposable checkout assertion |
| Role sandbox or approval default is overridden by a parent | Document both defense-in-depth limitations; enforce finalization in helper |
| Model catalog drifts | Validate at implementation; stop and amend rather than silently substitute |
| MCP config is overwritten | Extend existing file and validate current MCP table preservation |
| Hook or signal leaves repository dirty | Run hooks in a no-remote sanitized disposable fixture, trap cleanup, revalidate the post-hook candidate, and compare pre/post state in tests |
| Hook code performs an external action | Guarantee only helper-owned commands and report the arbitrary-hook boundary without a generic zero-remote claim |
| Branch changes after review | Full reviewed HEAD plus immediate pre-publish compare-and-swap |
| Unrelated work enters commit | Explicit NUL-safe allowlist and exact commit-tree assertion |
| V1 becomes a generalized framework | Fixed five-role inventory and explicit follow-up boundary |
## Rollback and Recovery
There is no database or application rollback. Before merge, a failed implementation
is recovered only by reverting the exact allowlisted feature changes through normal,
reviewed Git edits; do not use hard reset, broad clean, or unrelated-file checkout.
After integration, rollback is a dedicated revert change against the integration
branch that:
1. restores the prior project Codex configuration while preserving the MCP tables;
2. removes the five role files and helper/validator/test files introduced here;
3. restores the prior Git-skill text only if a replacement safe contract is
supplied—unsafe autonomous behavior must not be reactivated;
4. reverts the instruction casing only if current Codex discovery has a proven
compatibility defect and an amended spec approves the legacy posture.
If helper publication fails mid-operation, its own recovery contract is stricter:
temporary state is deleted, the compare-and-swap ref remains unchanged, and the
pre-invocation worktree/index/ref/remote snapshot must still match.
## Complexity Tracking
No constitution exception is claimed. The only non-trivial mechanism is the safe
local commit helper, which is required because declarative agent instructions alone
cannot reliably protect the user's index, unrelated work, or remote state.

View File

@ -0,0 +1,175 @@
# Implementation Quickstart: Spec 458
This guide is for the later implementation phase. Running preparation alone does
not authorize edits outside this feature package, local commits, pushes, or PRs.
## 1. Confirm the declared baseline
```bash
git status --short --branch --untracked-files=all
git log -1 --oneline
scripts/resolve-feature-base \
--branch-family=platform \
--work-classification=repository-governance \
--pull-request-target=platform-dev \
--spec=specs/458-codex-agent-foundation-safe-local-finalization/spec.md \
--format=kv
```
Before implementation, the preparation package must already be reviewed and
committed through separately explicit maintainer authority; implementation
permission does not authorize that commit. The feature branch must then be clean.
Create the repository-required short-lived branch:
```bash
git checkout -b feat/458-codex-agent-foundation-safe-local-finalization-session-$(date +%s)
```
Expected implementation branch:
`feat/458-codex-agent-foundation-safe-local-finalization-session-<timestamp>`.
Stop if any dirty change remains, the baseline is wrong, the preparation package is
not committed, or implementation would require a path outside the spec allowlist.
## 2. Activate the implementation guidance
Read:
```text
AGENTS.md (or pre-rename Agents.md)
.agent/skills/README.md
.specify/memory/constitution.md
docs/ai-coding-rules.md
docs/architecture-guidelines.md
docs/security-guidelines.md
docs/testing-guidelines.md
.codex/skills/pest-testing/SKILL.md
specs/458-codex-agent-foundation-safe-local-finalization/spec.md
specs/458-codex-agent-foundation-safe-local-finalization/plan.md
specs/458-codex-agent-foundation-safe-local-finalization/tasks.md
```
Report activated skills, branch, HEAD, dirty state, and hard gates before editing.
## 3. Revalidate Codex capabilities
```bash
codex --version
codex debug models --bundled
codex debug models
codex app-server --strict-config --listen stdio:// </dev/null
```
Required model/reasoning pairs:
- `gpt-5.4-mini`: `medium`, `high`
- `gpt-5.5`: `high`
The bundled command is an offline binary baseline; the default command may refresh
the authenticated current catalog. Stop and amend the spec/plan if any pair is
unavailable or project custom agents are not supported. Do not silently replace the
configured profile.
## 4. Preserve the current configuration baseline
Record:
```bash
rg -n '^\[mcp_servers\.|^auto_commit:' .codex/config.toml .specify/extensions/git/git-config.yml
git ls-files 'Agents.md' 'AGENTS.md'
rg -n 'git add -A|git push|--force-with-lease|reset --hard|git rebase|create.*PR|pull request' \
.codex/skills/giteaflow/SKILL.md \
.codex/skills/platform-feature-finish/SKILL.md
rg -n --hidden --glob '!.git/**' --glob '!specs/**' 'Agents\.md' .
```
The implementation must preserve the existing MCP server blocks and keep all Spec
Kit automatic commit flags false.
## 5. Implement in task order
Follow [tasks.md](./tasks.md). Write the helper self-tests and validator expectations
before the corresponding implementation. Perform the instruction casing change as
a Git-visible rename. Do not touch TenantPilot application runtime paths. The only
allowed `apps/platform/**` edits are:
- `tests/Feature/Guards/CodexAgentFoundationContractTest.php`;
- `tests/Feature/Guards/ProductSurfaceContractGateTest.php`;
- `tests/Feature/Guards/ConstitutionBranchTopologyContractTest.php`.
`apps/website/**` remains untouched.
## 6. Run focused verification
```bash
codex debug models --bundled
codex debug models
codex app-server --strict-config --listen stdio:// </dev/null
scripts/tests/codex-safe-local-commit-test
scripts/validate-codex-agent-foundation \
--branch-family platform \
--work-classification repository-governance \
--pull-request-target platform-dev
scripts/tests/codex-agent-reference-workflow
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/CodexAgentFoundationContractTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ProductSurfaceContractGateTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ConstitutionBranchTopologyContractTest.php
git diff --check
git status --short --untracked-files=all
```
All helper tests must use temporary repositories. No test may use the TenantPilot
working tree as its commit fixture. The validator must check the union of committed
base-range, staged, unstaged, and untracked paths; a clean
`git diff platform-dev...HEAD` alone is not allowlist proof.
## 7. Run the disposable role workflow
```bash
scripts/tests/codex-agent-reference-workflow
```
Run the emitted/defined smoke only in its disposable role-only fixture. The fixture
must not copy or start the repository's live MCP blocks, source the repository
`.env`, or start Docker/Gitea. Capture:
- discovered role names and declared sandboxes;
- explorer brief;
- implementer changed-path report;
- validator commands and outcomes;
- reviewer approval/findings;
- finalizer refusal without authority;
- finalizer success only with explicit fixture authorization;
- final commit tree, unrelated dirty path, index, refs, and
`helper_remote_commands=0`.
Treat repository hooks as arbitrary external code: run hook cases only in sanitized
no-remote fixtures, revalidate HEAD/path/tree/content after hooks, and do not claim
that the helper can certify arbitrary external hook side effects.
If validation/review fails, rerun the relevant correction cycle. Stop after three
failed cycles and report the blocker.
## 8. Close out without inferred Git authority
Create
`specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
with:
- files changed;
- commands and results;
- test-governance outcome: `document-in-feature`;
- browser: `N/A - no rendered UI surface changed`;
- Human Product Sanity: `N/A`;
- Livewire v4: unchanged/compliant;
- provider registration: unchanged;
- global search: unchanged;
- destructive/high-impact actions: no product actions; local commit helper guards;
- assets: none, `filament:assets` not required;
- deployment: no application impact;
- completed Specs 416, 439, and 444: unchanged historical evidence;
- unrelated failures and out-of-scope follow-up candidates;
- residual risks or `none`.
Do not commit, push, create a PR, merge, rebase, reset, or clean unless the user
separately and explicitly authorizes the exact operation.

View File

@ -0,0 +1,254 @@
# Research: Codex Agent Foundation and Safe Local Finalization
**Feature**: 458
**Date**: 2026-07-28
**Status**: Resolved — no open research question blocks implementation
## R1 — Which branch is the correct baseline?
**Decision**: Base, target, and diff against `platform-dev`.
**Evidence**:
- The request is repository governance with direct effect on platform development.
- Current router, constitution, candidate/readiness skills, and the relevant
completed-spec evidence are present on `platform-dev`.
- `dev` and `platform-dev` have substantial two-way divergence, so starting from
`dev` would create a large unrelated diff and omit current governance context.
**Alternatives rejected**:
- `dev`: valid only for consciously classified repository-wide integration or
promotion; not the smallest reviewable baseline here.
- `website-dev`: unrelated to the current affected workflow.
**Consequence**: Any later promotion to `dev` is a separate integration activity.
Implementation and validation resolve the effective base through the existing
`scripts/resolve-feature-base` contract and collect committed base-range, staged,
unstaged, and untracked candidate paths; `git diff platform-dev...HEAD` alone is
insufficient.
## R2 — Is the attached proposal current repo evidence or speculative foundation?
**Decision**: Treat it as a valid direct P0 candidate after narrowing and correction.
**Evidence**:
- Root tracked file is `Agents.md`, not canonical `AGENTS.md`.
- `.codex/agents/` does not exist.
- `.codex/config.toml` has MCP entries but no project custom-agent foundation.
- `giteaflow` and `platform-feature-finish` contain broad staging, history rewrite,
push, force-update, and PR behavior.
- Spec Kit auto-commit flags are already false and must remain so.
**Alternatives rejected**:
- Git-skill-only patch: would leave instruction discovery and responsibility
separation unresolved.
- Full orchestration platform: exceeds current evidence and proportional V1 need.
## R3 — What does current Codex support for project custom agents?
**Decision**: Use `.codex/agents/*.toml` with narrow role files and extend
`.codex/config.toml`.
**Evidence**:
- Local Codex CLI version: `0.133.0`.
- Current Codex manual documents project-scoped custom agents under
`.codex/agents/`.
- Required custom-agent fields are `name`, `description`, and
`developer_instructions`.
- Supported role overrides include `model`, `model_reasoning_effort`, and
`sandbox_mode`; `approval_policy` is also a supported session configuration key
and is made explicit for every V1 role.
- The current Codex reference documents scalar global settings and
`agents.<name>.description`/`agents.<name>.config_file` role declarations.
- The installed CLI `0.133.0` rejects the newer scalar `agents.enabled`,
`agents.max_concurrent_threads_per_session`, `agents.default_subagent_model`,
and `agents.default_subagent_reasoning_effort` shapes as role declarations, but
accepts the documented `agents.max_threads` compatibility alias plus explicit
role declarations. V1 therefore uses `max_threads = 3` and pins model/reasoning
in every role file.
- Project custom agents inherit parent/session configuration; declared role
sandbox and approval defaults are not unbypassable security boundaries because
parent live runtime overrides are reapplied on spawn.
**Primary documentation**:
- [Codex subagents](https://learn.chatgpt.com/docs/agent-configuration/subagents.md)
- [Codex configuration reference](https://learn.chatgpt.com/docs/codex-config-reference.md)
- [Codex approvals and sandboxing](https://learn.chatgpt.com/docs/agent-approvals-security.md)
**Consequence**: Role contracts provide separation and defaults; the finalization
helper supplies deterministic enforcement for Git mutation. A future Codex CLI
upgrade may replace the compatibility alias and explicit role map only after the
new scalar settings pass this repository's strict-config and role-spawn gates.
## R4 — Which model identifiers are valid?
**Decision**: Use only the locally catalogued V1 profile and validate again at
implementation:
- `gpt-5.4-mini` for explorer, validator, and finalizer;
- `gpt-5.5` for implementer and reviewer;
- reasoning efforts `medium` or `high` as defined in the plan.
**Evidence**:
The current authenticated `codex debug models` catalog reports:
- `gpt-5.5`
- `gpt-5.4`
- `gpt-5.4-mini`
- `gpt-5.3-codex-spark`
- `codex-auto-review`
Each reports `low`, `medium`, `high`, and `xhigh` reasoning support. The proposal's
`gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna` do not appear in this installed
CLI catalog.
`codex debug models --bundled` is the offline binary baseline. The default command
may refresh the remote/authenticated catalog and is therefore the
implementation-time availability proof, not a deterministic offline test.
**Alternatives rejected**:
- Keep proposed pins: strict config or spawning could fail in the actual local
implementation environment.
- Leave every role fully implicit: reduces reproducibility and fails the proposal's
role-cost/capability intent.
- Use `codex-auto-review` for the reviewer: its local presence is known, but it is a
specialized catalog entry and is unnecessary for a portable V1 role contract.
**Hard gate**: If catalog validation fails during implementation, stop and amend the
spec/plan rather than substituting a model ad hoc.
## R5 — How many roles and concurrent children are proportional?
**Decision**: Exactly five roles and at most three concurrent child threads.
**Rationale**:
- Five maps directly to current responsibilities: explore, implement, validate,
review, finalize.
- Exploration and independent review are read-heavy.
- Implementation and finalization are mutation-capable and should not run as
uncontrolled parallel writers.
- A concurrency limit of three supports useful read-heavy overlap without
encouraging multiple simultaneous mutators.
**Alternative rejected**: add planner, security reviewer, browser validator, release
manager, or multiple implementers in V1. These are follow-up candidates only.
## R6 — How should a local commit preserve unrelated work and hooks?
**Decision**: Use explicit reviewed state plus disposable staging/hook execution
inside a separate temporary Git repository, then atomically publish at most one
local commit only while HEAD and the helper-compatible finalization fingerprint
still match.
**Required invariants**:
- User's index must start semantically empty.
- All authorized paths are explicit and robustly encoded.
- Untracked, deleted, and renamed states contribute to the fingerprint; a rename
requires both old and new paths to be explicitly authorized.
- Case-only renames such as `Agents.md -> AGENTS.md` are modeled in a temporary
candidate index/tree so case-insensitive local filesystems do not require real
staged casing state.
- Hooks execute against a fully separate temporary Git repository with isolated
common directory, index, refs, and config, object alternates to the source object
store, and an absolute source hooks path.
- Hook-created refs and remote config remain confined to the temporary repository.
- After hooks execute and before publication, HEAD, exact path set, tree/content
identity, and absence of unauthorized paths are revalidated.
- Failure before publication cannot change the checked-out branch ref.
- Success brings the checked-out branch/index into semantic consistency while
leaving unrelated work unstaged.
- No remote command exists in the helper; evidence states
`helper_remote_commands=0`.
Repository hooks remain arbitrary external code. The helper cannot honestly prove
that an arbitrary hook had no network or non-Git side effect. Hook tests therefore
run only in sanitized disposable repositories without remotes, the helper reports
only its own command boundary, and post-hook candidate revalidation protects Git
publication truth.
**Alternatives rejected**:
- `git add -A` plus `git commit`: can capture unrelated work.
- Direct use of the user's index: hook/failure paths can corrupt pre-existing state.
- `git commit-tree` only: bypasses normal commit hooks.
- stash/reset/clean recovery: destructive and fragile around unrelated work.
- a declared `git_finalizer` sandbox alone: parent runtime overrides can weaken it.
The precise Git plumbing is intentionally left to implementation tests as long as
all behavioral invariants pass.
## R7 — Which tests belong to application lanes?
**Decision**: Keep exhaustive behavior in an explicit repository-governance family,
add one focused Pest process contract for critical helper behavior, and update the
two existing Pest governance guards whose active file lookups would otherwise
break.
**Classification**:
- Feature: new `CodexAgentFoundationContractTest.php` and existing
`ConstitutionBranchTopologyContractTest.php`, run focused through Sail.
- Heavy-Governance: existing `ProductSurfaceContractGateTest.php` in its current
`surface-guard` family, the full helper shell matrix, static validation, and
actual custom-role disposable smoke.
- Unit and Browser: N/A.
**Rationale**:
- No Laravel/Filament/Livewire runtime behavior changes.
- Database, PostgreSQL, application, or browser fixtures would add cost without
increasing confidence for Markdown/TOML/Git behavior. One focused Pest process
contract supplies constitution-required programmatic proof; exhaustive state
coverage remains outside the fast lane.
- The Product Surface guard's purpose and current lane manifest classify it as
Heavy-Governance despite its `tests/Feature/Guards` path.
- The actual role smoke may depend on a trusted/authenticated Codex session, so it
remains explicit rather than masquerading as an offline deterministic CI check.
**Workflow outcome**: `document-in-feature`.
## R8 — Should `data-model.md` or API contracts be created?
**Decision**: No.
There is no persisted application entity, API boundary, message schema, migration,
or tenant data flow. The handoff fields are governance output requirements already
captured in the spec and plan. Creating data-model or contract files would be
ceremonial and violate proportionality.
## R9 — What is the UI/deployment impact?
**Decision**: N/A for rendered UI and application deployment.
- Livewire v4 remains unchanged.
- No Filament provider/resource/global search/action/asset changes.
- No staging or production migration, env, queue, cron, volume, reverse proxy, or
Dokploy impact.
- Project-local Codex configuration is loaded only in trusted repository sessions;
a new session/reload may be required after implementation.
## R10 — How can role spawning avoid real MCP side effects?
**Decision**: Separate static preservation proof from the live role smoke.
- Static validation checks that the real `.codex/config.toml` retains the current
MCP tables.
- Actual discovery/spawn uses a disposable role-only Codex configuration that
contains the five role files and required agent defaults but no repository MCP
server tables.
- The fixture must not source the repository `.env`, start Docker/Gitea, or
authenticate either repository MCP server. User-level MCP configuration is
outside this feature's authority and is not represented as disabled.
Copying the project config into the disposable fixture was rejected because the
current Gitea MCP block has repository-local environment and runtime dependencies;
starting it adds an unrelated external side effect to a role-schema smoke.

View File

@ -0,0 +1,610 @@
# Feature Specification: Codex Agent Foundation and Safe Local Finalization
**Feature Branch**: `feat/458-codex-agent-foundation-safe-local-finalization`
**Created**: 2026-07-28
**Status**: Implemented — final evidence is recorded in the implementation report
**Input**: Repo-verified P0 proposal for a canonical Codex instruction entry point,
bounded custom roles, independent validation/review, and explicitly authorized
local-only commit finalization.
## Spec Candidate Check
- **Problem**: Repository-local Codex work has no case-canonical instruction entry
point, no bounded project roles, and active Git guidance that can authorize broad
staging or remote/destructive actions beyond a safe local finalization step.
- **Today's failure**: A maintainer cannot prove from repository truth that
exploration, mutation, validation, review, and local commit creation used
distinct authority boundaries; unrelated work or remote state can be placed at
risk by current guidance.
- **User-visible improvement**: A maintainer gets one discoverable instruction
contract, independently attributable validation/review evidence, and an
explicitly authorized local-only commit over reviewed paths.
- **Smallest enterprise-capable version**: Exactly five fixed roles, correction of
the two active unsafe Git skills, one explicit-path local commit helper, one
static validator, one bounded Pest contract, one shell self-test suite, and one
disposable role-only reference fixture.
- **Explicit non-goals**: No generalized orchestration framework, additional
roles, parallel writers, browser orchestration, Gitea integration, remote Git
finalization, application runtime change, or product UI.
- **Permanent complexity imported**: Five project-local role files, one local
commit helper, one validator, one shell behavior suite, one focused Pest process
contract, one reference fixture, and the terms candidate identity and
finalization authority. No persisted entity, application enum, API, or UI
taxonomy is introduced.
- **Why now**: The unsafe active Git guidance and missing canonical/root role
contracts affect every subsequent Codex-assisted repository change.
- **Why not local**: A prompt-only convention cannot provide shared discovery,
mechanically scoped Git mutation, repeatable failure-state coverage, or
reviewer-verifiable repository evidence across sessions.
- **Approval class**: Core Enterprise
- **Red flags triggered**: Foundation work; new meta infrastructure; process rather
than tenant-facing product surface. Defense: the broader proposal was already
shrunk to five fixed roles, two existing skill corrections, one local-only
helper, and bounded verification. `gpt-5.6-*`, browser/Gitea/remote finalization,
additional roles, generalized orchestration, and parallel writers remain
explicit follow-up candidates.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 |
Produktnähe: 0 | Wiederverwendung: 2 | **Gesamt: 9/12**
- **Decision**: shrink applied, then approve. The rubric's 79 rule is satisfied by
the recorded pre-approval scope reduction above; implementation remains blocked
until this preparation package passes the Spec Readiness Gate.
## Branch and Scope Declaration
- **Classification**: repository-governance hardening with direct platform workflow impact
- **Base branch**: `platform-dev`
- **Target branch**: `platform-dev`
- **Diff baseline**: `platform-dev`
- **Promotion posture**: any later promotion to `dev` is separate, explicitly
classified integration work
- **Application routes, persisted entities, APIs, RBAC, queues, schedules, and
deployment runtime**: not affected
## Spec Scope Fields
- **Scope**: repository governance/tooling; workspace, tenant, and canonical product
view scopes are not entered.
- **Primary Routes**: N/A — no application route or rendered page changes.
- **Data Ownership**: N/A — no workspace-owned or tenant-owned record changes.
- **RBAC**: N/A — no application membership or capability change; local repository
finalization still requires explicit user authority.
## Roadmap and Completed-Spec Guard
- **Candidate source**: direct attached P0 repository analysis, not an automatic
selection from the product backlog.
- **Roadmap relationship**: operating-readiness and governance hardening that
protects later platform and website delivery; it does not reorder or reopen the
current tenant-facing product roadmap.
- **Completed Spec 416**: reused as the existing skill-router baseline; not edited
or reopened.
- **Completed Spec 439**: reused as branch-topology/current-evidence truth; not
edited or reopened.
- **Completed Spec 444**: inspected as adjacent completed governance context; not
edited or reopened.
- **Collision result**: no existing Spec 458 directory or local/remote 458 feature
branch existed at preparation start.
## Product Surface Contract
- **No-legacy posture**: clean replacement. `Agents.md` is renamed to the canonical
`AGENTS.md`; no duplicate compatibility copy remains.
- **Product Surface Impact**: N/A — repository-local developer governance only.
- **UI Surface Impact**: no rendered UI surface changes.
- **Page archetype, surface budgets, Technical Annex/deep-link demotion, canonical
status vocabulary**: N/A.
- **Product Surface exceptions**: none.
- **Focused browser proof**: N/A — no rendered UI surface changes.
- **Human Product Sanity**: N/A — no user-facing product surface.
- **Visible complexity outcome**: unchanged.
## Domain and Architecture Gate Applicability
- **Workspace/managed-environment scope**: N/A — no tenant or managed-environment
data is read or written.
- **RBAC and authorization policies**: N/A for application users. Repository
finalization authority is separately explicit and does not reuse product RBAC.
- **OperationRun, audit evidence, and customer evidence**: N/A — no product
operation is created. Repository handoff evidence is local governance output, not
customer-facing evidence truth.
- **Provider boundary/Microsoft Graph**: N/A — no provider call or Graph contract.
- **Persistence/status/taxonomy**: no database entity, enum, or application status
family. The five-role list is a fixed repository taxonomy covered by the
proportionality review below.
- **Secrets**: helper input/output must avoid credentials and reject the minimum
sensitive path set; this is not a general secret scanner.
## Proportionality Review
- **Current operator problem**: Maintainers cannot reliably separate read-only
discovery/review from mutation or prove that a local commit contains only the
reviewed candidate.
- **Why existing structure is insufficient**: The existing skill router from
completed Spec 416 routes skills but defines no project-local Codex roles or
finalization boundary. The two active Git skills still contain broader mutation
authority, and no existing helper binds HEAD, explicit paths, and content
fingerprint into one candidate.
- **Why this is the narrowest correct implementation**: Each of the five roles maps
to one current responsibility boundary; only the two known unsafe skills change;
the helper supports one local commit operation over explicit paths; validation is
limited to this foundation. Additional roles and remote/general orchestration are
excluded.
- **Ownership cost**: Maintainers own five TOML role files, two corrected skill
contracts, one shell helper, one validator, one shell suite, one focused Pest
process contract, and one disposable fixture. Model pins and Codex schema require
review when the installed CLI changes.
- **Alternative intentionally rejected**: Prompt-only rules and direct
`git add`/`git commit` guidance were rejected because they cannot protect the
existing index or bind a reviewed candidate. A generalized agent workflow engine
was rejected because current consumers do not justify that permanent framework.
- **Release truth**: This is current-release repository governance for the present
`giteaflow` and `platform-feature-finish` consumers, not preparation for a
hypothetical product release. Broader reuse requires a follow-up spec.
## Test and Lane Impact
- **New/expanded test family**: yes — a contained repository-governance helper
behavior family plus a focused Pest process contract.
- **Test classifications**:
- Feature: the new focused Pest process contract for critical helper refusal and
success behavior, plus the existing Constitution branch-topology guard.
- Heavy-Governance: the existing Product Surface contract guard (current
`surface-guard` family), the full shell helper matrix, static foundation
validation, and the disposable five-role reference workflow.
- Unit and Browser: N/A.
- **Application lanes**: no lane-definition change. Run the focused new Pest
process contract and Constitution guard through Sail; run the Product Surface
guard in its existing Heavy-Governance classification. Confidence, PostgreSQL,
and browser lanes remain N/A.
- **Fixture cost**: temporary local Git repositories and small text fixtures only;
no tenant, database, browser, or external-service fixture.
- **Budget/trend impact**: contained feature-local process cost; no shared fixture
or lane-budget change. The authenticated role-spawn smoke is an explicit
implementation-time Heavy-Governance proof, not part of deterministic CI.
- **Workflow outcome**: `document-in-feature`.
## Problem Statement
TenantPilot relies on repository instructions and local automation to constrain AI
coding work. Today the repository does not expose those controls through one
case-canonical entry point, does not define bounded custom roles, and includes Git
skills that can stage all changes, rewrite history, or perform remote actions. This
prevents a reviewer from proving that implementation, validation, review, and local
commit finalization occurred under distinct responsibilities and explicit authority.
## Goals
- Establish one canonical repository instruction entry point.
- Provide exactly five project-local roles with narrow responsibilities.
- Keep exploration and final review read-only.
- Ensure validation evidence exists before final review.
- Make local commit creation opt-in, explicit-path scoped, deterministic, and
recoverable.
- Remove implicit broad, destructive, and remote Git behavior from active local
workflow guidance.
- Prove the complete role handoff in a disposable, non-application fixture.
## Non-Goals
- Editing application runtime code, runtime configuration, migrations, routes,
resources, views, or product documentation unrelated to agent governance.
- Adding tenant-facing UI or changing Filament, Livewire, RBAC, Graph, audit, backup,
restore, or policy behavior.
- Pushing branches, creating pull requests, merging, rebasing, resetting, or
force-updating history.
- Automatically committing at the end of every task.
- Building a general-purpose orchestration engine or adding more than five roles.
- Adding browser, Gitea, or parallel writer orchestration.
- Treating agent sandbox configuration as an absolute security boundary when a
parent session can override it.
## Users and Stakeholders
- **Primary user**: a repository maintainer running Codex against TenantPilot.
- **Secondary user**: a reviewer who needs reproducible evidence that a change was
implemented, validated, reviewed, and locally finalized under separate contracts.
- **Protected party**: contributors whose unrelated work, index state, credentials,
and remote repository state must not be mutated.
## User Scenarios and Testing
### User Story 1 — Discover one canonical instruction contract (Priority: P1)
As a repository maintainer, I want Codex and contributors to discover one canonical
uppercase instruction file so that repository-wide rules cannot drift between
case variants.
**Why this priority**: every later role and helper depends on a single authoritative
contract.
**Independent Test**: on a case-sensitive disposable checkout, verify that
`AGENTS.md` exists, `Agents.md` does not exist, the prior instruction content is
preserved, and repository guidance points only to the canonical path.
**Acceptance Scenarios**:
1. **Given** the current tracked `Agents.md`, **When** the feature is applied,
**Then** Git records a case-safe rename to `AGENTS.md` without losing content.
2. **Given** a case-sensitive checkout, **When** a tool searches for repository
instructions, **Then** exactly one root instruction file is found.
3. **Given** existing policy and skill routing content, **When** the rename completes,
**Then** its behavior remains intact except for the new bounded-agent and
finalization rules approved by this spec.
---
### User Story 2 — Delegate through five bounded roles (Priority: P1)
As a maintainer, I want exploration, implementation, validation, review, and local
finalization represented by separate roles so that findings and approvals come from
the right responsibility boundary.
**Why this priority**: role separation is the principal safety outcome.
**Independent Test**: load the project with strict configuration, enumerate the
custom roles, and run a disposable reference workflow that produces an explorer
brief, implementation result, validator evidence, reviewer verdict, and finalizer
result.
**Acceptance Scenarios**:
1. **Given** the project-local Codex configuration, **When** it is parsed strictly,
**Then** exactly the five approved custom roles are discoverable.
2. **Given** an exploration or final-review task, **When** the corresponding role
runs, **Then** its declared sandbox is read-only and its output contains no
mutation.
3. **Given** an implementation requiring proof, **When** validation finishes,
**Then** final review consumes the validator's actual evidence rather than an
implementer assertion.
4. **Given** a review finding, **When** work returns for correction, **Then** the
same validator and reviewer contracts are rerun, with a maximum of three
correction cycles before escalation.
---
### User Story 3 — Create one safely scoped local commit (Priority: P1)
As a maintainer, I want local commit creation to require a deliberate instruction,
an explicit path allowlist, and a reviewed state fingerprint so that unrelated work
and remote state remain untouched.
**Why this priority**: current active guidance permits broad Git mutation and remote
operations.
**Independent Test**: in temporary Git repositories, exercise tracked, untracked,
deleted, renamed, whitespace-containing, hook-rejected, stale-fingerprint,
pre-populated-index, conflict, protected-branch, and sensitive-path cases; verify
that only authorized paths can enter the created local commit and all refusal cases
preserve the user's state.
**Acceptance Scenarios**:
1. **Given** no explicit local-commit instruction, **When** a workflow completes,
**Then** no commit is created.
2. **Given** explicit paths and a matching reviewed fingerprint, **When** the helper
runs on an allowed branch with a clean index, **Then** exactly one local commit
contains only those paths.
3. **Given** unrelated worktree changes, **When** an authorized local commit is
created, **Then** those changes remain unstaged and uncommitted.
4. **Given** any protected branch, conflict, staged entry, sensitive path, stale
fingerprint, or changed HEAD, **When** finalization is requested, **Then** the
helper refuses without altering the worktree, index, refs, or remotes.
5. **Given** a commit hook that operates within the disposable Git context and then
fails, **When** commit creation aborts, **Then** the user's real index and
worktree remain equivalent to their pre-invocation state.
6. **Given** successful finalization, **When** remotes are inspected, **Then** no
helper-owned push, pull, fetch, PR, or remote mutation command occurred; any
configured local hook is treated as external repository code, isolated in the
test fixture, and followed by candidate revalidation.
7. **Given** an explicit path allowlist and expected HEAD, **When** fingerprint mode
runs repeatedly over unchanged state, **Then** it returns the same SHA-256
candidate identity without changing the real index, worktree, refs, or remotes.
---
### User Story 4 — Prove the governance foundation independently (Priority: P2)
As a reviewer, I want one static validator and one disposable end-to-end reference
fixture so that I can verify the foundation without touching the TenantPilot
application.
**Why this priority**: static shape alone cannot prove handoff order or finalization
failure behavior.
**Independent Test**: run the validator and reference workflow from a clean feature
checkout and verify deterministic success; then inject one invalid role, unsafe Git
command, or stale fingerprint and verify deterministic refusal.
**Acceptance Scenarios**:
1. **Given** the completed foundation, **When** the validator runs, **Then** it checks
instruction casing, role count/schema, supported models/reasoning, sandbox
posture, configuration preservation, unsafe Git patterns, helper safeguards, and
required test coverage.
2. **Given** the disposable reference fixture, **When** the full workflow runs,
**Then** it proves the ordered handoff and optionally creates only a local,
explicitly authorized fixture commit.
3. **Given** a failing validation or reviewer verdict, **When** finalization is
attempted, **Then** no commit is created.
## Edge Cases
- Case-insensitive developer filesystems can hide a casing-only rename.
- A path can contain spaces, tabs, Unicode, or leading dash characters.
- An authorized path can be untracked, deleted, or renamed.
- The same path can change after review but before finalization.
- HEAD can advance after review but before finalization.
- The user can already have staged changes.
- Merge, rebase, cherry-pick, or revert conflicts can be in progress.
- A commit hook can edit files or reject the commit.
- A repository-supplied commit hook can execute arbitrary external code; the helper
can avoid invoking remote commands itself and revalidate repository state, but
cannot honestly certify the hook's external side effects.
- A requested path can resolve outside the repository or enter a sensitive subtree.
- A parent Codex session can relax a child role's declared sandbox.
- A parent Codex session can relax a child role's declared approval policy.
- The installed Codex model catalog can differ from a proposal or another client.
- Existing MCP configuration can be lost if the project config is replaced instead
of extended.
## Functional Requirements
### Canonical instruction entry point
- **FR-001**: The implementation MUST perform a Git-visible, case-safe rename from
root `Agents.md` to root `AGENTS.md`.
- **FR-002**: The implementation MUST preserve all applicable existing instruction
and routing content, changing behavior only where this specification explicitly
requires it.
- **FR-003**: The repository MUST contain exactly one root instruction entry point
after the rename, with no compatibility duplicate or symlink.
### Bounded custom roles
- **FR-004**: The repository MUST define exactly five custom roles named
`code_explorer`, `implementer`, `test_validator`, `code_reviewer`, and
`git_finalizer`.
- **FR-005**: Every role MUST have a name, concise discovery description, explicit
developer contract, supported model/reasoning configuration, declared sandbox
posture, and explicit approval policy.
- **FR-006**: `code_explorer` and `code_reviewer` MUST be declared read-only;
`implementer`, `test_validator`, and `git_finalizer` MAY use workspace-write only
for their bounded responsibilities.
- **FR-007**: The instruction contract MUST state that child sandbox and approval
declarations are defense-in-depth defaults, not absolute permission boundaries
against parent runtime overrides.
- **FR-008**: The implementer MUST NOT act as the final reviewer for its own changes.
- **FR-009**: Validation MUST finish and produce concrete command/result evidence
before final review starts.
- **FR-010**: Review findings MUST include severity, location or affected artifact,
impact, evidence, and a bounded recommended correction.
- **FR-011**: Correction cycles MUST rerun relevant validation and review, MUST be
capped at three, and MUST escalate unresolved or expanding work rather than
silently widening scope.
### Project configuration
- **FR-012**: `.codex/config.toml` MUST be extended rather than replaced, preserving
all current MCP server configuration.
- **FR-013**: The configuration MUST enable custom agents, cap V1 concurrency at
three child threads per session, and keep child network access disabled by
default. For the installed Codex CLI, the documented `agents.max_threads`
compatibility alias MUST be used when the newer scalar concurrency key does not
pass strict configuration.
- **FR-014**: Every pinned model and reasoning effort MUST exist in the installed
local Codex catalog and MUST pass strict configuration plus role-spawn validation.
- **FR-015**: The implementation MUST NOT depend on undocumented or locally
unavailable model identifiers or scalar agent settings rejected by the installed
CLI. Every role MUST pin its supported model and reasoning effort directly, and
the project configuration MUST declare each role through documented
`agents.<name>.description` and `agents.<name>.config_file` keys.
### Git skill correction and authority
- **FR-016**: Active repository Git skills MUST NOT instruct agents to use broad
staging, implicit commit, push, PR creation, rebase, reset, force update, or
destructive cleanup as an automatic completion step.
- **FR-017**: The repository MUST keep automatic commits disabled, and the canonical
instruction contract MUST require an explicit user request before any local
commit is created.
- **FR-018**: Remote Git actions MUST remain outside `git_finalizer` authority and
outside the local commit helper.
### Safe local commit helper
- **FR-019**: The local commit helper MUST accept an explicit repository root,
explicit path list, reviewed HEAD, and operation mode; commit mode MUST also
require a commit message and reviewed deterministic fingerprint, and both modes
MUST reject ambiguous or empty input.
- **FR-020**: The fingerprint MUST account for tracked, untracked, deleted, and
renamed authorized paths; a rename MUST authorize and bind both the old and new
path. The fingerprint MUST be stable for path names containing whitespace or
Unicode. A dedicated read-only fingerprint mode MUST emit it without mutating the
real index, worktree, refs, or remotes.
- **FR-021**: Before mutation, the helper MUST reject protected branches, detached
HEAD, in-progress conflict operations, any pre-existing staged entry, changed
HEAD, stale fingerprint, path traversal, directories, unresolved globs, duplicate
or newline-containing paths, paths outside the repository, and configured
sensitive paths. The minimum sensitive denylist MUST cover `.env`, `.env.*`,
`*.pem`, `*.key`, `id_rsa`, and `id_ed25519`, without claiming to be a complete
secret scanner.
- **FR-022**: The helper MUST isolate staging from the user's real index, execute
normal commit hooks, create at most one local commit containing only authorized
paths, and never itself invoke a network or remote Git operation. Tests that
execute arbitrary repository hooks MUST use disposable repositories with no
remotes and a sanitized environment.
- **FR-023**: Success and every failure path MUST preserve unrelated worktree
changes, the user's real index, existing refs other than the single successful
local commit, and all remotes. After hooks run and before publishing a commit,
the helper MUST revalidate the expected HEAD and the authorized candidate's
exact path set, tree/content identity, and absence of unauthorized paths; drift
MUST refuse publication. This guarantee covers helper-owned operations and hooks
operating within the supplied disposable Git context; arbitrary hook code that
deliberately targets external paths or services remains outside the helper's
enforceable boundary and MUST be reported as residual risk.
- **FR-024**: The helper MUST emit concise, non-secret evidence identifying the
reviewed HEAD/fingerprint, authorized paths, refusal reason or resulting local
commit, and a truthful marker that the helper executed zero remote Git commands.
It MUST NOT claim proof about arbitrary external side effects of repository hooks.
### Verification and close-out
- **FR-025**: A deterministic self-test suite MUST cover success and refusal cases
for the local commit helper in temporary repositories.
- **FR-026**: A static foundation validator MUST resolve the declared baseline via
`scripts/resolve-feature-base`, inspect committed base-range, staged, unstaged,
and untracked candidate paths, and check canonical casing, exact role inventory,
required role fields, supported models/reasoning, sandbox/approval posture,
configuration preservation, corrected Git guidance, helper safeguards, and test
presence.
- **FR-027**: A disposable reference workflow MUST prove the ordered five-role
handoff and both the authorized-local-commit and no-commit outcomes without
modifying TenantPilot application files. Its role-spawn smoke MUST use a
role-only disposable Codex configuration and MUST NOT start or authenticate the
repository's live MCP servers; preservation of live MCP blocks is proven
statically against the real project configuration.
- **FR-028**: The implementation report MUST record tests and results, browser
disposition, Livewire v4 status, provider registration status, global-search
status, destructive/high-impact action handling, asset strategy, and deployment
impact.
- **FR-029**: Validation, review, and finalization MUST bind two deterministic
identities. The review identity contains branch, full HEAD, resolved diff
baseline, candidate tree, exact review paths including both rename sides, and a
review fingerprint. The finalization identity contains the reviewed HEAD, exact
finalization paths including both rename sides, and the helper-compatible
fingerprint emitted by `scripts/codex-safe-local-commit fingerprint`. Drift in
either identity MUST invalidate prior evidence. The implementation report MUST
NOT embed a concrete helper fingerprint for a path set that includes the report
itself; that value is emitted only in the external validator handoff.
- **FR-030**: The five custom role files MUST use the repository conventions
`code-explorer.toml`, `implementer.toml`, `test-validator.toml`,
`code-reviewer.toml`, and `git-finalizer.toml`, while their `name` fields retain
the approved underscore role identifiers.
- **FR-031**: The V1 instruction contract and implementation report MUST state that
this five-role topology alone does not certify critical domain, security,
destructive product, production, or remote Git work; such work retains all
applicable manual and repository hard gates.
- **FR-032**: No application runtime source MAY change. The only allowed
`apps/platform/**` edits are the two existing governance guards and the one new
focused Pest process contract named in the allowlist; `apps/website/**` remains
untouched. Product Surface, Browser proof, and Human Product Sanity MUST be
reported as truthful N/A outcomes.
- **FR-033**: Active repository instructions and routing references MUST point to
root `AGENTS.md`; no active reference may preserve `Agents.md` as a usable legacy
authority.
## Non-Functional Requirements
- **NFR-001 — Determinism**: identical repository state and explicit inputs produce
identical validation/fingerprint outcomes.
- **NFR-002 — Fail closed**: missing, stale, ambiguous, unsupported, or conflicting
inputs cause refusal before repository mutation.
- **NFR-003 — Least authority**: each role and script has only the authority required
by its bounded responsibility.
- **NFR-004 — Auditability**: handoffs and finalization results are concise,
human-readable, and reproducible without exposing secrets.
- **NFR-005 — Portability**: deterministic helper, validator, and fixture-shape
checks operate on the repository's supported local shell/Git environment without
database, browser, or external-service dependencies. Focused Pest governance
proof uses Sail. The authenticated role-discovery/spawn smoke is a separately
reported Codex CLI dependency and MUST NOT be represented as deterministic CI.
- **NFR-006 — Reviewability**: the implementation diff remains inside the approved
repository-governance allowlist.
## Success Criteria
- **SC-001**: A case-sensitive checkout contains `AGENTS.md` and not `Agents.md`,
with existing governance content preserved.
- **SC-002**: Strict Codex configuration succeeds and exactly five custom roles can
be discovered and spawned using locally supported model/reasoning pairs.
- **SC-003**: Static validation finds no active broad-staging, implicit remote,
history-rewrite, or destructive-cleanup instruction in the governed Git skills.
- **SC-004**: All local commit helper self-tests pass, including worktree/index
preservation for every refusal and hook-failure case; the focused Pest process
contract also passes.
- **SC-005**: The disposable reference workflow demonstrates explorer →
implementer → validator → reviewer → finalizer ordering and refuses finalization
when validation or review fails.
- **SC-006**: Successful fixture finalization creates exactly one local commit with
only authorized paths, reports `helper_remote_commands=0`, and makes no claim
that this marker proves arbitrary hook code was externally side-effect free.
- **SC-007**: `git diff --check` passes and the feature diff contains no TenantPilot
runtime or rendered UI changes.
- **SC-008**: Fingerprint mode is repeatable for unchanged explicit state and leaves
the real index, worktree, refs, and remotes byte-for-byte or semantically
unchanged as appropriate.
## Implementation Allowlist
- `Agents.md``AGENTS.md`
- `.codex/config.toml`
- `.codex/agents/code-explorer.toml`
- `.codex/agents/implementer.toml`
- `.codex/agents/test-validator.toml`
- `.codex/agents/code-reviewer.toml`
- `.codex/agents/git-finalizer.toml`
- `.codex/skills/giteaflow/SKILL.md`
- `.codex/skills/platform-feature-finish/SKILL.md`
- `docs/HANDOVER.md` (only stale active root-instruction links)
- `docs/PROJECT_SUMMARY.md` (only the current active casing reference; historical
narrative remains historical)
- `apps/platform/tests/Feature/Guards/ProductSurfaceContractGateTest.php`
- `apps/platform/tests/Feature/Guards/ConstitutionBranchTopologyContractTest.php`
- `apps/platform/tests/Feature/Guards/CodexAgentFoundationContractTest.php`
- `scripts/codex-safe-local-commit`
- `scripts/validate-codex-agent-foundation`
- `scripts/tests/codex-safe-local-commit-test`
- `scripts/tests/codex-agent-reference-workflow`
- this feature package under
`specs/458-codex-agent-foundation-safe-local-finalization/`
Any additional runtime or governance path requires a spec/plan amendment before
implementation.
## Assumptions
- Git and a POSIX-compatible shell available in the supported local development
environment are sufficient for V1 tests.
- Laravel Sail is available for the two focused existing Pest governance guards
and the new focused Pest process contract.
- The implementation environment uses a Codex CLI that supports project-local
custom agents and strict configuration validation.
- Current locally discoverable model identifiers are `gpt-5.5` and
`gpt-5.4-mini` for the selected role profile; implementation must revalidate the
catalog rather than assume permanence.
- The repository's existing MCP configuration remains required.
- User authorization for implementation does not imply authorization to commit,
push, or create a PR.
## Dependencies
- Current `platform-dev` repository governance baseline.
- Installed Codex CLI with project configuration and subagent support.
- Local Git and shell utilities already required by repository tooling.
## Risks
| Risk | Impact | Mitigation |
|---|---|---|
| A case-insensitive filesystem hides an incomplete rename | Codex discovery differs across contributor systems | Two-step Git rename, exact tracked-file validation, case-sensitive disposable checkout, and focused guard updates |
| A proposed model disappears from the installed catalog | Custom role cannot spawn | Implementation-time catalog and strict-config hard gate; amend instead of silently substituting |
| Parent session permissions weaken a role default | A role receives more authority than its file suggests | State sandbox and approval limitations explicitly and keep Git mutation mechanically bounded by the helper |
| Commit-helper edge case mutates user state | Unrelated work or repository state can be lost | Test-first temporary repositories, pre/post snapshots, isolated hooks/staging, post-hook candidate revalidation, compare-and-swap publication, fail closed |
| Arbitrary hook code performs an external side effect | A generic zero-remote claim becomes false or untestable | Guarantee only helper-owned commands, run hook tests in no-remote sanitized fixtures, revalidate candidate state, and report the residual boundary honestly |
| Active and historical casing references are conflated | Completed evidence is rewritten or current links remain stale | Update only allowlisted active consumers; validator distinguishes active instructions from historical evidence |
| Foundation expands into generalized orchestration | Permanent maintenance cost grows without evidence | Fixed five-role V1, path allowlist, amendment stop condition, and explicit follow-up specs |
## Follow-up Candidates
- Mechanical quality-gate selection across changed file families.
- Browser and deeper end-to-end validation orchestration.
- Gitea-aware remote finalization with its own explicit authority and approval
model.
- Parallel writing agents with worktree isolation and merge/conflict contracts.
- Model-profile refresh if the supported local Codex catalog changes.
## Open Questions
None. Model availability is resolved by implementation-time catalog validation and
the current supported V1 profile; remote finalization and broader orchestration are
explicitly deferred.

View File

@ -0,0 +1,307 @@
# Tasks: Codex Agent Foundation and Safe Local Finalization
**Input**: Design documents from
`specs/458-codex-agent-foundation-safe-local-finalization/`
**Prerequisites**: `spec.md`, `plan.md`, `research.md`, `quickstart.md`
**Tests**: Required. This feature creates new repository-governance behavior whose
failure could mutate user work or Git state.
**Organization**: Tasks are grouped by user story. Tests and validators are added
before the corresponding behavior wherever practical.
## Format
`- [ ] Txxx [P?] [US?] Description with exact file path`
- **[P]**: can run in parallel because the task changes a different file or isolated
section and has no unmet dependency.
- **[US1][US4]**: user story from `spec.md`.
- Setup/foundational/polish tasks intentionally have no user-story label.
## Phase 1: Setup and Hard-Gate Baseline
**Purpose**: Prove that implementation starts from the declared branch and current
Codex/repository evidence.
- [x] T001 Require separately authorized preparation-package commit evidence, stop unless the feature branch is clean, create/record the required `feat/458-codex-agent-foundation-safe-local-finalization-session-<timestamp>` implementation branch, and record branch, HEAD, `platform-dev` merge base, activated skills, and all hard-gate results in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
- [x] T002 Record the pre-change root instruction casing, exact MCP table inventory, Spec Kit auto-commit flags, and unsafe Git-skill evidence in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
- [x] T003 Run `codex --version`, offline `codex debug models --bundled`, current authenticated `codex debug models`, and real parse `codex app-server --strict-config --listen stdio:// </dev/null`; record supported planned model/reasoning pairs and stop on mismatch in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
- [x] T004 Resolve the baseline via `scripts/resolve-feature-base`, capture the approved implementation allowlist, union committed base-range, staged, unstaged, and untracked paths, and fail preflight on any unrelated path in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
**Checkpoint**: Branch, catalog, configuration, and allowlist hard gates pass before
any governance/runtime artifact is edited.
---
## Phase 2: Foundational Test Harnesses
**Purpose**: Create isolated test infrastructure that later stories extend.
- [x] T005 Create temporary-repository setup, deterministic fixture identity, pre/post worktree-index-ref-remote snapshot helpers, assertion helpers, and guaranteed cleanup traps in `scripts/tests/codex-safe-local-commit-test`; add critical success/fail-closed process coverage in `apps/platform/tests/Feature/Guards/CodexAgentFoundationContractTest.php`
- [x] T006 Create the static validator command-line skeleton, stable error/result format, local model-catalog adapter, real app-server strict-config adapter, canonical `scripts/resolve-feature-base` adapter, and committed/staged/unstaged/untracked candidate collector in `scripts/validate-codex-agent-foundation`
- [x] T007 Create the disposable non-application fixture setup, isolated Git repository lifecycle, handoff-artifact directory, and cleanup behavior in `scripts/tests/codex-agent-reference-workflow`
- [x] T008 Add an implementation-report skeleton containing mandated tests, test-governance outcome, browser/no-browser, Human Product Sanity, Livewire v4, provider registration, global search, destructive/high-impact actions, asset strategy, deployment, completed-spec preservation, unrelated-failure disposition, follow-up candidates, and residual-risk fields in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
**Checkpoint**: The harnesses can run safely and fail for not-yet-implemented
foundation checks without touching the TenantPilot application.
---
## Phase 3: User Story 1 — Canonical Instruction Contract (Priority: P1)
**Goal**: Expose exactly one case-canonical root instruction entry point while
preserving existing governance content.
**Independent Test**: A case-sensitive disposable checkout contains `AGENTS.md`,
does not contain `Agents.md`, finds exactly one root instruction contract, and
retains all pre-change routing/governance sections.
### Tests for User Story 1
- [x] T009 [US1] Add failing static checks for exact `AGENTS.md` casing, absence of `Agents.md`, duplicate/symlink refusal, preservation markers, and stale active `Agents.md` references in `scripts/validate-codex-agent-foundation`
- [x] T010 [US1] Add the case-sensitive disposable-checkout assertion in `scripts/tests/codex-agent-reference-workflow` and change only the canonical active instruction-path expectations in `apps/platform/tests/Feature/Guards/ProductSurfaceContractGateTest.php` and `apps/platform/tests/Feature/Guards/ConstitutionBranchTopologyContractTest.php` so the focused tests fail until the rename exists
### Implementation for User Story 1
- [x] T011 [US1] Perform a Git-visible two-step case-safe rename from `Agents.md` to `AGENTS.md`, preserving the complete existing instruction content in `AGENTS.md`
- [x] T012 [US1] Add the Spec 458 repository-governance scope, exactly-five-role router, explicit authority boundary, correction-cycle cap, and defense-in-depth sandbox/approval caveat to `AGENTS.md`; correct only stale active links in `docs/HANDOVER.md` and the current casing reference in `docs/PROJECT_SUMMARY.md`, preserving historical statements
- [x] T013 [US1] Run casing/preservation checks plus `CodexAgentFoundationContractTest.php`, `ProductSurfaceContractGateTest.php`, and `ConstitutionBranchTopologyContractTest.php` through Sail; record that Product Surface remains Heavy-Governance and the other two are focused Feature proof in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
**Checkpoint**: User Story 1 is independently demonstrable on a case-sensitive
checkout.
---
## Phase 4: User Story 2 — Five Bounded Roles (Priority: P1)
**Goal**: Make exploration, implementation, validation, review, and finalization
separately discoverable with ordered evidence.
**Independent Test**: Strict configuration parses; exactly five custom roles have
the approved model/reasoning/sandbox matrix; the reference fixture records the full
ordered handoff.
### Tests for User Story 2
- [x] T014 [US2] Add failing checks for the exact five hyphenated filenames, required TOML fields including approval policy, unique underscore names, supported model/reasoning pairs, approved sandbox/approval matrix, the installed-CLI-compatible `agents.max_threads` concurrency limit, five documented `agents.<name>.description`/`config_file` declarations, network default, and MCP preservation in `scripts/validate-codex-agent-foundation`
- [x] T015 [US2] Add failing reference-workflow assertions for explorer brief, implementer changed-path report, validator command evidence, reviewer verdict/findings, full structured review/finalization identity handoff, finalizer result, and the three-cycle escalation boundary in `scripts/tests/codex-agent-reference-workflow`
### Implementation for User Story 2
- [x] T016 [US2] Extend the existing MCP-preserving project configuration with installed-CLI-compatible `agents.max_threads = 3`, the five documented `agents.<name>.description`/`config_file` declarations, individually pinned role model/reasoning settings, and disabled workspace-write network access in `.codex/config.toml`
- [x] T017 [P] [US2] Define the read-only, approval-never discovery contract with `gpt-5.4-mini`/`medium` in `.codex/agents/code-explorer.toml`
- [x] T018 [P] [US2] Define the allowlist-bound, approval-on-request implementation contract with `gpt-5.5`/`high` in `.codex/agents/implementer.toml`
- [x] T019 [P] [US2] Define the evidence-only, approval-on-request validator contract with `gpt-5.4-mini`/`high` and disposable-write limits in `.codex/agents/test-validator.toml`
- [x] T020 [P] [US2] Define the independent read-only, approval-never review contract with `gpt-5.5`/`high`, structured finding fields, and no self-fix authority in `.codex/agents/code-reviewer.toml`
- [x] T021 [P] [US2] Define the approval-on-request, explicit-authority local-only finalization contract, helper-only mutation rule, and mandatory `Push`/`Pull Request: NOT PERFORMED` result with `gpt-5.4-mini`/`medium` in `.codex/agents/git-finalizer.toml`
- [x] T022 [US2] Complete the ordered explorer → implementer → validator → reviewer → finalizer handoff rules and evidence schema in `AGENTS.md`
- [x] T023 [US2] Implement disposable handoff artifacts and ordering/cycle assertions without committing the TenantPilot worktree in `scripts/tests/codex-agent-reference-workflow`
- [x] T024 [US2] Run the real app-server strict parse, model-catalog validation, exact-role checks, sandbox/approval proof, and a focused authenticated discovery/spawn smoke against a disposable role-only config with no repository MCP blocks; record static real-config MCP preservation plus sandbox/approval parent-override limitations in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
**Checkpoint**: All five roles are discoverable and their contracts can be exercised
without granting implicit commit or remote authority.
---
## Phase 5: User Story 3 — Safe Explicit Local Finalization (Priority: P1)
**Goal**: Allow at most one reviewed local commit over explicit paths while
preserving unrelated work, real index state, refs, and remotes on every refusal.
**Independent Test**: Temporary repositories prove success for all approved path
states and fail-closed preservation for protected/stale/conflicting/hook-failure
states.
### Tests for User Story 3
- [x] T025 [US3] Add failing tests for repeatable read-only fingerprint mode plus tracked-modification commit success, exact one-commit output, exact authorized commit tree, clean semantic index for committed paths, preserved unrelated dirty paths, and `helper_remote_commands=0` in `scripts/tests/codex-safe-local-commit-test`
- [x] T026 [P] [US3] Add failing success tests for mixed tracked/untracked/deleted/renamed paths, require both old and new path authorization for renames, and cover whitespace, Unicode, tab, and leading-dash filenames in `scripts/tests/codex-safe-local-commit-test`
- [x] T027 [P] [US3] Add failing refusal tests for empty/broad/outside/directory/glob/traversal/duplicate/newline/sensitive paths including `.env`, `.env.*`, `*.pem`, `*.key`, `id_rsa`, and `id_ed25519`; protected branches; detached HEAD; pre-staged entries; unresolved conflicts; in-progress Git operations; changed HEAD; and stale fingerprint in `scripts/tests/codex-safe-local-commit-test`
- [x] T028 [P] [US3] Add failing race, hook rejection, hook-side mutation containment, hook ref/remote-config isolation, case-only rename including executable and symlink mode preservation, post-hook HEAD/path/tree/content revalidation, signal interruption, temporary-state cleanup, and compare-before/after preservation tests in sanitized disposable repositories in `scripts/tests/codex-safe-local-commit-test`
- [x] T029 [P] [US3] Add failing static checks for executable broad staging, automatic commits, push/PR, merge, fetch/pull, rebase, reset, force update, destructive cleanup, amend, and hook bypass in the governed skills/helper while proving documented prohibitions do not create false positives in `scripts/validate-codex-agent-foundation`
### Implementation for User Story 3
- [x] T030 [US3] Implement explicit `fingerprint`/`commit` modes, strict argument parsing, absolute repository verification, non-empty explicit path collection, protected-branch/detached/conflict/index/HEAD preflight, path normalization, both-old-and-new rename authorization, directory/glob/traversal/duplicate/newline refusal, and minimum sensitive-path rejection in `scripts/codex-safe-local-commit`
- [x] T031 [US3] Implement read-only fingerprint mode using a deterministic NUL-safe authorized-state manifest and SHA-256 identity for tracked, untracked, deleted, and renamed paths with both rename paths bound, without mutating the real index, worktree, refs, or remotes in `scripts/codex-safe-local-commit`
- [x] T032 [US3] Implement disposable staging and normal hook execution in a fully separate temporary Git repository with its own common directory, index, refs, and config, object alternates to the source object store, absolute source `hooksPath`, verified object-closure import, and no exposure of the user's real index or working tree to partial helper state in `scripts/codex-safe-local-commit`
- [x] T033 [US3] Implement post-hook HEAD/exact-path/tree/content/no-unauthorized-path revalidation, atomic one-commit local publication, semantic index/worktree reconciliation, cleanup traps, stable refusal codes, non-secret evidence, and truthful `helper_remote_commands=0` output without claiming arbitrary hook-side-effect proof in `scripts/codex-safe-local-commit`
- [x] T034 [P] [US3] Replace automatic broad commit/push/PR completion with separately authorized, local-helper-based guidance and explicit remote-operation boundaries in `.codex/skills/giteaflow/SKILL.md`
- [x] T035 [P] [US3] Remove or quarantine broad stage, rebase, reset, force-push, destructive cleanup, and implicit finalization flows while retaining a bounded safe finish purpose in `.codex/skills/platform-feature-finish/SKILL.md`
- [x] T036 [US3] Add the no-inferred-commit rule, exact finalizer prerequisites, explicit path/state approval contract, protected branch posture, and remote prohibition in `AGENTS.md`
- [x] T037 [US3] Complete helper executable/safeguard/evidence checks and verify all Spec Kit auto-commit flags remain false in `scripts/validate-codex-agent-foundation`
- [x] T038 [US3] Run the full helper self-test matrix, focused Pest process contract, and static unsafe-Git checks; record commands, case counts, results, helper-owned remote-command proof boundary, and any bounded correction cycle in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
**Checkpoint**: The helper either creates exactly one authorized local commit or
refuses with no unauthorized Git state change; it has no helper-owned remote
operation path and makes no unprovable claim about arbitrary hook code.
---
## Phase 6: User Story 4 — Independent Foundation Proof (Priority: P2)
**Goal**: Provide deterministic static proof plus a disposable actual-role workflow
without changing application files.
**Independent Test**: The foundation validator passes from the declared baseline,
the passing fixture completes all five handoffs, and failing validation/review blocks
finalization.
### Tests and Implementation for User Story 4
- [x] T039 [US4] Complete validator aggregation, stable pass/fail summary, real strict config/model/sandbox/approval/MCP/Git/helper/test checks, semantic forbidden-instruction detection, canonical baseline resolution, semantically empty real-index gate, temporary candidate-tree casing proof, separated review/finalization identities, and committed/staged/unstaged/untracked allowlist enforcement in `scripts/validate-codex-agent-foundation`
- [x] T040 [US4] Implement the passing disposable role workflow with a role-only Codex config containing no repository MCP blocks, an unrelated dirty fixture path, and an explicitly authorized local fixture commit in `scripts/tests/codex-agent-reference-workflow`
- [x] T041 [US4] Implement failing validator and failing reviewer fixture paths that prove finalizer refusal and preserve the disposable repository state in `scripts/tests/codex-agent-reference-workflow`
- [x] T042 [US4] Run the authenticated actual custom-role discovery/spawn smoke only in the disposable role-only fixture; prove neither repository MCP block was copied, started, or authenticated and capture role, sandbox, approval, handoff, command, verdict, commit/no-commit, index, ref, and helper-owned remote-command evidence in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
- [x] T043 [US4] Perform an independent read-only review of the complete allowlisted diff and validator evidence; record structured findings or approval in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
- [x] T044 [US4] Resolve every confirmed in-scope finding, rerun relevant validation and independent review after each fix, and stop/escalate after three unsuccessful cycles; record the cycle log in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
**Checkpoint**: Static and behavioral evidence independently prove the foundation,
including the no-commit failure path.
---
## Test Governance Checklist
- [x] Lane assignment remains purpose-based: the focused helper/branch contracts
are Feature, while the existing Product Surface `surface-guard`, full shell
matrix, static validator, and role workflow are Heavy-Governance.
- [x] New or changed tests stay in the smallest honest family; no exhaustive Git
matrix or authenticated role smoke is hidden in fast feedback.
- [x] Fixtures remain disposable local Git repositories with no database,
workspace, membership, provider, session, browser, shared factory, or seed cost.
- [x] Planned validation commands run only the three named Pest files plus the
explicit shell/role proofs and do not pull unrelated lane cost.
- [x] Surface test profile is N/A — no rendered surface; the existing Product
Surface guard retains its current `surface-guard` classification.
- [x] Browser proof is `N/A - no rendered UI surface changed`.
- [x] Human Product Sanity and Product Surface implementation-report close-out are
recorded as truthful N/A outcomes.
- [x] Budget/baseline/trend follow-up is `none`; the contained governance cost and
authenticated non-CI smoke are documented in this feature.
- [x] Review outcome is recorded as `acceptable-special-case` and workflow outcome
exactly as `document-in-feature`.
---
## Phase 7: Polish and Cross-Cutting Close-out
**Purpose**: Verify scope, documentation, and repository/product gate reporting.
- [x] T045 [P] Verify shell portability/error handling, TOML readability, Markdown links, stable output names, absence of secret/file-content logging, truthful sandbox/approval inheritance wording, and explicit arbitrary-hook residual boundary across `scripts/codex-safe-local-commit`, `scripts/validate-codex-agent-foundation`, `scripts/tests/codex-safe-local-commit-test`, and `scripts/tests/codex-agent-reference-workflow`
- [x] T046 [P] Reconcile any implementation-discovered requirement change in `specs/458-codex-agent-foundation-safe-local-finalization/spec.md`, `specs/458-codex-agent-foundation-safe-local-finalization/plan.md`, and `specs/458-codex-agent-foundation-safe-local-finalization/tasks.md` before continuing
- [x] T047 Run offline/current model catalog commands, `codex app-server --strict-config --listen stdio:// </dev/null`, helper self-tests, the foundation validator with canonical resolver inputs, the disposable reference workflow, all three focused Pest files through Sail with Product Surface retained as Heavy-Governance, `git diff --check`, and all-state changed-path checks; record exact final results in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
- [x] T048 Complete test-governance outcome `document-in-feature`, browser `N/A`, Human Product Sanity `N/A`, visible complexity unchanged, Livewire v4 unchanged, provider/global-search unchanged, no product destructive actions, no assets, no app deployment, no critical-domain/production/remote-work certification claim, residual risks, and no-legacy casing outcome in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
- [x] T049 Confirm the committed base-range, staged, unstaged, and untracked candidate union contains only the spec allowlist; TenantPilot runtime and completed Specs 416/439/444 are untouched; unrelated failures/follow-up candidates are recorded; the helper invoked no remote Git command; and no local commit exists unless separately authorized in `specs/458-codex-agent-foundation-safe-local-finalization/implementation-report.md`
---
## Dependencies and Execution Order
### Phase dependencies
- **Phase 1**: starts immediately and is a hard gate.
- **Phase 2**: depends on Phase 1; establishes isolated harnesses.
- **Phase 3 (US1)**: depends on Phase 2 validator/reference skeletons.
- **Phase 4 (US2)**: depends on canonical `AGENTS.md` from US1.
- **Phase 5 (US3)**: depends on role/finalizer authority language from US2; its
self-tests precede helper behavior.
- **Phase 6 (US4)**: depends on all P1 stories.
- **Phase 7**: depends on all selected stories and evidence.
### User-story dependencies
- **US1**: independently testable after the foundational harness.
- **US2**: depends on US1 only for its canonical router location.
- **US3**: depends on US2 for `git_finalizer` discovery/authority but the helper
remains independently testable in temporary repositories.
- **US4**: integrates US1US3 and must not be treated as a substitute for their
focused tests.
### Parallel opportunities
- T017T021 can be authored in parallel after T014T016 because each changes one
role file, but no more than three child tasks may be open concurrently.
- T026T029 can be authored in parallel after the common T005 harness.
- T034 and T035 can be corrected in parallel after T029 defines the static
expectations.
- T045 and T046 can be evaluated in parallel only if any artifact amendment is
coordinated before the final T047 run.
- Do not run multiple writing agents against `AGENTS.md`,
`scripts/codex-safe-local-commit`, or the implementation report concurrently.
## Parallel Example: User Story 2
```text
Task: T017 — .codex/agents/code-explorer.toml
Task: T018 — .codex/agents/implementer.toml
Task: T019 — .codex/agents/test-validator.toml
Task: T020 — .codex/agents/code-reviewer.toml
Task: T021 — .codex/agents/git-finalizer.toml
```
## Parallel Example: User Story 3 Tests
```text
Task: T026 — path-state and encoding success cases
Task: T027 — preflight refusal cases
Task: T028 — race/hook/signal/cleanup cases
Task: T029 — unsafe Git static checks
```
## Requirement Coverage
| Requirement | Tasks |
|---|---|
| FR-001FR-003 | T009T013 |
| FR-004FR-007 | T014, T016T022, T024 |
| FR-008FR-011 | T015, T018T024, T043T044 |
| FR-012FR-015 | T003, T014, T016T024, T039 |
| FR-016FR-018 | T002, T029, T034T037, T049 |
| FR-019FR-020 | T025T027, T030T031 |
| FR-021FR-024 | T025T033, T037T038 |
| FR-025 | T005, T025T028, T038 |
| FR-026 | T006, T009, T014, T029, T037, T039 |
| FR-027 | T007, T010, T015, T023, T040T042 |
| FR-028 | T001T004, T008, T013, T024, T038, T042T049 |
| FR-029 | T015, T022T024, T030T033, T040T044 |
| FR-030 | T014, T017T021, T024, T039 |
| FR-031 | T012, T022, T043, T048T049 |
| FR-032 | T004, T008, T010, T013, T039, T047T049 |
| FR-033 | T009T013, T039 |
| NFR-001NFR-005 | T005T007, T025T033, T039T045, T047 |
| NFR-006 | T004, T039, T043, T047, T049 |
## Implementation Strategy
### Smallest safe P1 increment
US1US3 form the minimum usable governance slice: canonical discovery, bounded
roles, and an enforceable local-only finalization path. They should be implemented
and verified sequentially. US4 then supplies integrated proof and review evidence.
### Stop conditions
Stop implementation and report the blocker if:
- the branch/baseline or dirty state violates the declared hard gate;
- the installed Codex CLI cannot strictly parse the planned configuration or spawn
the approved model profiles;
- a required change falls outside the spec allowlist;
- existing MCP configuration cannot be preserved;
- the helper cannot preserve user state under any required failure case;
- correctness would require certifying arbitrary repository hook side effects or
another guarantee outside the helper's controllable command/state boundary;
- the disposable role smoke would need to copy, start, or authenticate the live
repository MCP configuration;
- a correction requires more than three validation/review cycles;
- application/runtime/UI work becomes necessary;
- remote Git activity becomes necessary without a new explicit request/spec.
## Notes
- `[P]` tasks are safe to parallelize only under isolated-file ownership.
- Check off tasks only after their evidence exists.
- No task authorizes a local commit, push, PR, merge, rebase, reset, clean, or force
update.
- Completed historical specs are context only and must not be edited.