feat: add Codex agent foundation #522
13
.codex/agents/code-explorer.toml
Normal file
13
.codex/agents/code-explorer.toml
Normal 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"
|
||||
12
.codex/agents/code-reviewer.toml
Normal file
12
.codex/agents/code-reviewer.toml
Normal 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"
|
||||
14
.codex/agents/git-finalizer.toml
Normal file
14
.codex/agents/git-finalizer.toml
Normal 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"
|
||||
12
.codex/agents/implementer.toml
Normal file
12
.codex/agents/implementer.toml
Normal 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"
|
||||
12
.codex/agents/test-validator.toml
Normal file
12
.codex/agents/test-validator.toml
Normal 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"
|
||||
@ -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"]
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
@ -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',
|
||||
);
|
||||
});
|
||||
@ -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(
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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
737
scripts/codex-safe-local-commit
Executable 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
|
||||
759
scripts/tests/codex-agent-reference-workflow
Executable file
759
scripts/tests/codex-agent-reference-workflow
Executable 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}"
|
||||
726
scripts/tests/codex-safe-local-commit-test
Executable file
726
scripts/tests/codex-safe-local-commit-test
Executable 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 ]]
|
||||
686
scripts/validate-codex-agent-foundation
Executable file
686
scripts/validate-codex-agent-foundation
Executable 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'
|
||||
@ -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.
|
||||
@ -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.
|
||||
629
specs/458-codex-agent-foundation-safe-local-finalization/plan.md
Normal file
629
specs/458-codex-agent-foundation-safe-local-finalization/plan.md
Normal 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-001–FR-003, FR-033 | D1 canonical casing |
|
||||
| FR-004–FR-011, FR-029–FR-030 | D2 fixed roles + D4 ordered evidence |
|
||||
| FR-012–FR-015 | D3 project configuration + catalog validation |
|
||||
| FR-016–FR-018 | D5 Git skill posture |
|
||||
| FR-019–FR-024 | D6 helper protocol |
|
||||
| FR-025–FR-028, FR-031–FR-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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
610
specs/458-codex-agent-foundation-safe-local-finalization/spec.md
Normal file
610
specs/458-codex-agent-foundation-safe-local-finalization/spec.md
Normal 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 7–9 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.
|
||||
@ -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 US1–US3 and must not be treated as a substitute for their
|
||||
focused tests.
|
||||
|
||||
### Parallel opportunities
|
||||
|
||||
- T017–T021 can be authored in parallel after T014–T016 because each changes one
|
||||
role file, but no more than three child tasks may be open concurrently.
|
||||
- T026–T029 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-001–FR-003 | T009–T013 |
|
||||
| FR-004–FR-007 | T014, T016–T022, T024 |
|
||||
| FR-008–FR-011 | T015, T018–T024, T043–T044 |
|
||||
| FR-012–FR-015 | T003, T014, T016–T024, T039 |
|
||||
| FR-016–FR-018 | T002, T029, T034–T037, T049 |
|
||||
| FR-019–FR-020 | T025–T027, T030–T031 |
|
||||
| FR-021–FR-024 | T025–T033, T037–T038 |
|
||||
| FR-025 | T005, T025–T028, T038 |
|
||||
| FR-026 | T006, T009, T014, T029, T037, T039 |
|
||||
| FR-027 | T007, T010, T015, T023, T040–T042 |
|
||||
| FR-028 | T001–T004, T008, T013, T024, T038, T042–T049 |
|
||||
| FR-029 | T015, T022–T024, T030–T033, T040–T044 |
|
||||
| FR-030 | T014, T017–T021, T024, T039 |
|
||||
| FR-031 | T012, T022, T043, T048–T049 |
|
||||
| FR-032 | T004, T008, T010, T013, T039, T047–T049 |
|
||||
| FR-033 | T009–T013, T039 |
|
||||
| NFR-001–NFR-005 | T005–T007, T025–T033, T039–T045, T047 |
|
||||
| NFR-006 | T004, T039, T043, T047, T049 |
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Smallest safe P1 increment
|
||||
|
||||
US1–US3 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.
|
||||
Reference in New Issue
Block a user