AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning

This commit is contained in:
Krilly
2026-03-04 13:29:22 +00:00
parent 29a98137a7
commit 57dd294675
13706 changed files with 2114953 additions and 237629 deletions
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
import argparse
import shlex
import subprocess
from pathlib import Path
def run(cmd, cwd=None):
p = subprocess.run(cmd, cwd=cwd, text=True, capture_output=True)
if p.stdout:
print(p.stdout.strip())
if p.returncode != 0:
if p.stderr:
print(p.stderr.strip())
raise SystemExit(p.returncode)
def main():
parser = argparse.ArgumentParser(description="Execute a gate prompt with selected coding agent")
parser.add_argument("--root", default=".", help="Project root")
parser.add_argument("--agent", required=True, choices=["codex", "claude", "opencode", "pi"])
parser.add_argument("--prompt-file", required=True, help="Prompt file path")
parser.add_argument("--spec-ref", default="", help="Spec reference for dispatch safety checks")
parser.add_argument(
"--enforce-spec-ref",
action="store_true",
help="Fail dispatch if --spec-ref is missing (use for implementation gates)",
)
parser.add_argument("--full-auto", action="store_true", help="Use full-auto mode where supported")
parser.add_argument("--dry-run", action="store_true", help="Print command without executing")
args = parser.parse_args()
root = Path(args.root).resolve()
prompt = Path(args.prompt_file).resolve()
if not prompt.exists():
raise SystemExit(f"Prompt file not found: {prompt}")
if args.enforce_spec_ref and not args.spec_ref.strip():
raise SystemExit("Dispatch blocked: --spec-ref is required for this run.")
text = prompt.read_text(encoding="utf-8")
# Additional dispatch-time guard: do not launch if prompt itself indicates missing spec.
if args.enforce_spec_ref and "Spec Reference: (not provided for this gate)" in text:
raise SystemExit("Dispatch blocked: prompt indicates missing spec reference.")
if args.agent == "codex":
cmd = ["codex", "exec"]
if args.full_auto:
cmd.append("--full-auto")
cmd.append(text)
elif args.agent == "claude":
# Claude CLI generally accepts task text directly
cmd = ["claude", text]
elif args.agent == "opencode":
cmd = ["opencode", "run", text]
else: # pi
cmd = ["pi", "-p", text]
print("Agent command:")
print(" ".join(shlex.quote(c) for c in cmd))
if args.dry_run:
return
run(cmd, cwd=str(root))
if __name__ == "__main__":
main()
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
import argparse
from pathlib import Path
from datetime import datetime, timezone
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def append_line(path: Path, line: str):
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists():
path.write_text("", encoding="utf-8")
with path.open("a", encoding="utf-8") as f:
f.write(line)
def ensure_section(path: Path, header: str):
if not path.exists():
path.write_text(f"{header}\n\n", encoding="utf-8")
else:
txt = path.read_text(encoding="utf-8")
if header not in txt:
path.write_text(txt + f"\n{header}\n\n", encoding="utf-8")
def main():
parser = argparse.ArgumentParser(description="Record change request impact")
parser.add_argument("--root", default=".", help="Project root")
parser.add_argument("--request", required=True, help="Change request text")
args = parser.parse_args()
root = Path(args.root).resolve()
ts = now_iso()
change_log = root / "docs" / "change-log.md"
append_line(change_log, f"| {ts} | {args.request} | User-requested change | requirements/architecture/tests/tasks | orchestrator |\n")
tasks = root / "docs" / "tasks.md"
append_line(tasks, f"| CR-{ts} | Assess and implement change: {args.request} | TODO | G1/G2+ | orchestrator | {ts} |\n")
trace = root / "docs" / "traceability.md"
append_line(trace, f"| CR-{ts} | docs/requirements.md | implementation TBD | tests TBD | TODO |\n")
impact = root / "docs" / "change-impact.md"
ensure_section(impact, "# Change Impact")
append_line(
impact,
(
f"\n## {ts}\n"
f"Request: {args.request}\n"
f"Impacted docs (review/update):\n"
f"- docs/requirements.md\n"
f"- docs/architecture.md\n"
f"- docs/test-plan.md\n"
f"- docs/test-results.md\n"
f"- docs/tasks.md\n"
f"- docs/progress.md\n"
f"Validation actions:\n"
f"- Re-run impacted unit/integration/e2e tests\n"
f"- Re-run manual scenarios tied to changed behavior\n"
),
)
print("Change impact recorded. Review docs/change-impact.md for TODOs.")
if __name__ == "__main__":
main()
@@ -0,0 +1,254 @@
#!/usr/bin/env python3
import argparse
import json
from pathlib import Path
from datetime import datetime, timezone
GATES = ["G0", "G1", "G2", "G3", "G4", "G5", "G6", "G7"]
STATES = ["PENDING", "IN_PROGRESS", "PASS", "FAIL", "BLOCKED"]
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def default_status():
return {
"meta": {
"createdAt": now_iso(),
"updatedAt": now_iso(),
"status": "IN_PROGRESS",
},
"gates": {g: {"state": "PENDING", "updatedAt": None, "note": ""} for g in GATES},
"history": [],
}
def load_json(path: Path, default):
if not path.exists():
return default
return json.loads(path.read_text(encoding="utf-8"))
def save(path: Path, data):
path.parent.mkdir(parents=True, exist_ok=True)
data["meta"]["updatedAt"] = now_iso()
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
def status_path(root: Path) -> Path:
return root / ".orchestrator" / "status.json"
def context_path(root: Path) -> Path:
return root / ".orchestrator" / "context.json"
def load(root: Path):
return load_json(status_path(root), default_status())
def load_context(root: Path):
return load_json(
context_path(root),
{
"projectMode": "greenfield",
"executionMode": "gated",
"researchMode": False,
},
)
def validate_status_schema(data):
if not isinstance(data, dict):
return False, "status.json must be an object"
if "meta" not in data or "gates" not in data or "history" not in data:
return False, "status.json missing required top-level keys: meta/gates/history"
if not isinstance(data["gates"], dict):
return False, "gates must be an object"
for g in GATES:
if g not in data["gates"]:
return False, f"missing gate entry: {g}"
entry = data["gates"][g]
if not isinstance(entry, dict):
return False, f"gate entry must be object: {g}"
if entry.get("state") not in STATES:
return False, f"invalid state for {g}: {entry.get('state')}"
if not isinstance(data["history"], list):
return False, "history must be an array"
return True, "OK"
def doc_has_substance(path: Path) -> bool:
if not path.exists():
return False
text = path.read_text(encoding="utf-8", errors="ignore")
for raw in text.splitlines():
line = raw.strip()
if not line:
continue
if line.startswith("#"):
continue
# Ignore markdown table separator lines like |---|---|
if set(line.replace("|", "").replace("-", "").replace(":", "").strip()) == set():
continue
if line.startswith("-") and len(line) <= 3:
continue
if "TBD" in line.upper():
continue
return True
return False
def sequential_prereq(gate: str):
i = GATES.index(gate)
if i == 0:
return []
return GATES[:i]
def mode_preconditions(root: Path, gate: str, project_mode: str):
checks = []
if gate == "G2" and project_mode == "greenfield":
checks.extend(
[
root / "docs" / "requirements.md",
root / "docs" / "architecture.md",
root / "docs" / "adr" / "ADR-0001-initial-architecture.md",
]
)
if gate == "G2" and project_mode == "brownfield":
checks.extend(
[
root / "docs" / "as-is-architecture.md",
root / "docs" / "system-inventory.md",
root / "docs" / "dependency-map.md",
root / "docs" / "legacy-risk-register.md",
root / "docs" / "compatibility-matrix.md",
root / "docs" / "migration-plan.md",
root / "docs" / "characterization-tests.md",
]
)
if gate in ("G4", "G6") and project_mode == "brownfield":
checks.extend(
[
root / "docs" / "compatibility-matrix.md",
root / "docs" / "migration-plan.md",
]
)
return checks
def check_preconditions(root: Path, data, gate: str, target_state: str):
if target_state not in ("IN_PROGRESS", "PASS"):
return True, "No preconditions required for this transition"
# Sequential progression
required_prev = sequential_prereq(gate)
for g in required_prev:
if data["gates"][g]["state"] != "PASS":
return False, f"Precondition failed: previous gate {g} must be PASS"
# Mode-specific docs checks
ctx = load_context(root)
project_mode = ctx.get("projectMode", "greenfield")
for p in mode_preconditions(root, gate, project_mode):
if not p.exists():
return False, f"Precondition failed: missing required document {p.relative_to(root)}"
if gate in ("G2",) and not doc_has_substance(p):
return False, f"Precondition failed: document lacks substantive content {p.relative_to(root)}"
return True, "OK"
def set_meta_status(data):
if all(data["gates"][g]["state"] == "PASS" for g in GATES):
data["meta"]["status"] = "COMPLETE"
elif any(data["gates"][g]["state"] in ("FAIL", "BLOCKED") for g in GATES):
data["meta"]["status"] = "ATTENTION"
else:
data["meta"]["status"] = "IN_PROGRESS"
def cmd_set(args):
root = Path(args.root).resolve()
path = status_path(root)
data = load(root)
ok, msg = validate_status_schema(data)
if not ok:
raise SystemExit(f"Invalid status schema: {msg}")
if args.gate not in GATES:
raise SystemExit(f"Invalid gate: {args.gate}")
if args.state not in STATES:
raise SystemExit(f"Invalid state: {args.state}")
if not args.no_enforce:
ok, msg = check_preconditions(root, data, args.gate, args.state)
if not ok:
raise SystemExit(msg)
data["gates"][args.gate] = {"state": args.state, "updatedAt": now_iso(), "note": args.note or ""}
data["history"].append(
{
"timestamp": now_iso(),
"gate": args.gate,
"state": args.state,
"note": args.note or "",
}
)
set_meta_status(data)
save(path, data)
print(f"Updated {args.gate} -> {args.state}")
def cmd_show(args):
root = Path(args.root).resolve()
data = load(root)
print(json.dumps(data, indent=2))
def cmd_validate(args):
root = Path(args.root).resolve()
p = status_path(root)
if not p.exists():
raise SystemExit(f"Missing {p}")
data = load(root)
ok, msg = validate_status_schema(data)
if not ok:
raise SystemExit(f"Invalid: {msg}")
print("status.json schema: OK")
def main():
parser = argparse.ArgumentParser(description="Gate status manager")
sub = parser.add_subparsers(dest="cmd", required=True)
p_set = sub.add_parser("set", help="Set a gate state")
p_set.add_argument("--root", default=".", help="Project root")
p_set.add_argument("--gate", required=True, help="Gate id (G0..G7)")
p_set.add_argument("--state", required=True, help="State")
p_set.add_argument("--note", default="", help="Optional note")
p_set.add_argument("--no-enforce", action="store_true", help="Disable precondition enforcement")
p_set.set_defaults(func=cmd_set)
p_show = sub.add_parser("show", help="Show current status")
p_show.add_argument("--root", default=".", help="Project root")
p_show.set_defaults(func=cmd_show)
p_validate = sub.add_parser("validate", help="Validate status schema")
p_validate.add_argument("--root", default=".", help="Project root")
p_validate.set_defaults(func=cmd_validate)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
import argparse
from pathlib import Path
TEMPLATES = {
"G1": """Objective: Complete planning artifacts.
Tasks:
1) Finalize requirements with testable acceptance criteria.
2) Capture Definition of Done.
3) List assumptions and risks.
4) If research_mode=true, produce docs/research-notes.md with options and recommendation.
Validations:
- requirements are testable and unambiguous
- acceptance criteria map to tests
Done condition:
- docs/requirements.md, docs/plan.md, docs/progress.md updated""",
"G2": """Objective: Complete architecture baseline and ADR.
Tasks:
1) Update docs/architecture.md.
2) Update docs/adr/ADR-0001-initial-architecture.md.
3) For brownfield, ensure onboarding artifacts are complete.
Validations:
- architecture supports must-have journeys
- ADR has alternatives + trade-offs
Done condition:
- G2 docs complete and traceability updated""",
"G3": """Objective: Deliver and verify first vertical slice.
Tasks:
1) Implement first slice for top priority journey.
2) Add unit + integration tests.
3) Run manual smoke test.
Validations:
- unit/integration pass
- manual smoke recorded
Done condition:
- slice works end-to-end with evidence""",
"G4": """Objective: Complete full build and baseline verification.
Tasks:
1) Implement remaining v1 scope.
2) Run full validation suite.
3) Resolve failures or document blockers.
Validations:
- lint/type/build pass
- unit/integration/e2e pass
Done condition:
- in-scope v1 complete with evidence""",
"G5": """Objective: Execute security and quality gate.
Tasks:
1) Run secret/dependency checks.
2) Verify auth/input/error handling.
3) Run performance smoke checks.
Validations:
- no unresolved critical/high findings
Done condition:
- security evidence captured""",
"G6": """Objective: Prepare and verify release candidate.
Tasks:
1) Complete release checklist.
2) Validate rollback instructions.
3) Confirm monitoring/alerts baseline.
Validations:
- release checklist complete
- rollback validated
Done condition:
- RC ready for approval/deploy""",
"G7": """Objective: Complete handover.
Tasks:
1) Run post-deploy smoke tests.
2) Finalize handover notes.
3) Create next-iteration backlog.
Validations:
- critical journeys pass in deployed env
Done condition:
- progress=100% and handover complete""",
}
HEADER = """You are implementing Gate {gate} for this project.
## SPEC-DRIVEN RULES (NON-NEGOTIABLE)
1. You are implementing ONLY what is specified in the spec document.
2. Do NOT add features, abstractions, or "improvements" not in the spec.
3. If the spec is unclear or incomplete, STOP and ask for clarification.
4. Do NOT guess at requirements. Ever.
5. Your output will be verified against the acceptance criteria from the spec.
6. No spec = No implementation. Period.
## TASK-LEVEL DOC UPDATE RULES (NON-NEGOTIABLE)
After THIS task, you MUST directly update all relevant docs yourself:
- docs/tasks.md
- docs/progress.md
- docs/change-log.md
- docs/traceability.md
- docs/test-results.md
- docs/agent-handoff.md
In docs/agent-handoff.md include:
- Task summary (what you changed)
- Spec reference used
- CLI checks for OpenClaw agent to run
- Browser/manual checks for OpenClaw agent to run (or N/A)
- Known risks / caveats
## OUTPUT CONTRACT (STRICT)
Return a final completion block that includes:
- STATUS: DONE | BLOCKED
- TASK: <exact task>
- SPEC_REF: <exact spec ref>
- FILES_CHANGED: <list>
- VALIDATION_RUN: <commands and outcomes>
- OPENCLAW_VERIFY: <cli checks + browser checks or N/A>
- RISKS: <explicit list or NONE>
If any required input is missing (especially spec), output STATUS: BLOCKED and do not implement.
## CONSTRAINTS
- Follow AGENTS.md workflow rules exactly.
- This run is for ONE task only; do not attempt whole-project implementation in one pass.
- Do not claim completion without evidence.
- Implementation must match spec acceptance criteria exactly.
"""
def main():
parser = argparse.ArgumentParser(description="Generate gate-specific coding-agent prompt")
parser.add_argument("--gate", required=True, choices=["G1", "G2", "G3", "G4", "G5", "G6", "G7"])
parser.add_argument("--agent", required=True, choices=["codex", "claude", "opencode", "pi"])
parser.add_argument("--project-mode", choices=["greenfield", "brownfield"], default="greenfield")
parser.add_argument("--execution-mode", choices=["autonomous", "gated"], default="gated")
parser.add_argument("--research-mode", choices=["true", "false"], default="false")
parser.add_argument("--task", required=True, help="Single task summary")
parser.add_argument("--spec-ref", default="", help="Spec reference for this task")
parser.add_argument("--output", help="Write prompt to file")
args = parser.parse_args()
body = TEMPLATES[args.gate]
if args.gate == "G4":
body += "\nTask slicing requirement:\n- Read docs/g4-task-plan.md and execute only one unchecked task for this run.\n- Mark only that task as done, leave others untouched.\n"
mode_note = f"Coding agent: {args.agent}\nProject mode: {args.project_mode}\nExecution mode: {args.execution_mode}\nResearch mode: {args.research_mode}\n"
if args.project_mode == "brownfield" and args.gate in ("G2", "G4"):
mode_note += "Brownfield emphasis: preserve compatibility, run parity and rollback checks.\n"
if args.project_mode == "brownfield" and args.gate in ("G1", "G2"):
mode_note += (
"Brownfield onboarding requirement: you (coding agent) must directly update onboarding docs: "
"as-is-architecture, system-inventory, dependency-map, legacy-risk-register, "
"compatibility-matrix, migration-plan, characterization-tests.\n"
)
spec_section = ""
if args.spec_ref:
spec_section = f"\n## TASK INPUTS\n- Task: {args.task}\n- Spec Reference: {args.spec_ref}\n"
else:
spec_section = f"\n## TASK INPUTS\n- Task: {args.task}\n- Spec Reference: (not provided for this gate)\n"
handoff_instruction = (
"\n## WAKE HANDOFF FORMAT (MANDATORY)\n"
"When done, run exactly one wake command that includes: task done + check instructions.\n"
"Format:\n"
"openclaw gateway wake --text \"Done: <gate> | task: <short> | handoff: see docs/agent-handoff.md for CLI+Browser checks\" --mode now\n"
)
prompt = (
HEADER.format(gate=args.gate)
+ "\n"
+ mode_note
+ spec_section
+ "\n"
+ body
+ "\n"
+ handoff_instruction
)
if args.output:
Path(args.output).write_text(prompt, encoding="utf-8")
print(f"Prompt written to {args.output}")
else:
print(prompt)
if __name__ == "__main__":
main()
@@ -0,0 +1,375 @@
#!/usr/bin/env python3
from pathlib import Path
from datetime import datetime, timezone
import argparse
import json
BASE_TEMPLATES = {
"docs/plan.md": """# Plan
- Status: DRAFT
- Project Mode: TBD
- Execution Mode: TBD
- Research Mode: TBD
- Owner: TBD
## Summary
TBD
## Milestones
- [ ] G0 Intake
- [ ] G1 Planning
- [ ] G2 Architecture
- [ ] G3 Slice-1
- [ ] G4 Full Build
- [ ] G5 Security/Quality
- [ ] G6 Release Candidate
- [ ] G7 Handover
""",
"docs/requirements.md": """# Requirements
## Problem Statement
TBD
## Users
TBD
## In Scope (v1)
- TBD
## Out of Scope
- TBD
## Functional Requirements
- FR-001: TBD
## Non-Functional Requirements
- NFR-001: TBD
## Definition of Done
TBD
""",
"docs/architecture.md": """# Architecture
## Context
TBD
## High-Level Design
TBD
## Data Flow
TBD
## Deployment Topology
TBD
## Security Baseline
TBD
""",
"docs/adr/ADR-0001-initial-architecture.md": """# ADR-0001: Initial Architecture
- Status: Proposed
- Date: TBD
## Context
TBD
## Decision
TBD
## Alternatives Considered
1. TBD
2. TBD
## Consequences
TBD
""",
"docs/tasks.md": """# Tasks
| ID | Task | Status | Gate | Owner | Updated |
|---|---|---|---|---|---|
""",
"docs/progress.md": """# Progress
- Overall: 0%
- Current Gate: G0
- Status: IN_PROGRESS
## Gate Status
- G0 Intake: PENDING
- G1 Planning: PENDING
- G2 Architecture: PENDING
- G3 Slice-1: PENDING
- G4 Full Build: PENDING
- G5 Security/Quality: PENDING
- G6 Release Candidate: PENDING
- G7 Handover: PENDING
## Activity Log
- [TBD] Orchestration initialized
""",
"docs/test-plan.md": """# Test Plan
## Test Levels
- Unit
- Integration
- E2E
- Security Baseline
## Critical Journeys
1. TBD
""",
"docs/test-results.md": """# Test Results
| Gate | Test | Steps/Command | Expected | Actual | Result | Evidence | Timestamp |
|---|---|---|---|---|---|---|---|
""",
"docs/change-log.md": """# Change Log
| Date | Change | Why | Impact | Owner |
|---|---|---|---|---|
""",
"docs/release-checklist.md": """# Release Checklist
- [ ] All required tests pass
- [ ] Security baseline complete
- [ ] Rollback steps documented
- [ ] Monitoring configured
- [ ] Docs updated
- [ ] Final approval obtained
""",
"docs/g4-task-plan.md": """# G4 Task Plan (Task-by-Task)
Use this checklist to break G4 into discrete tasks.
Only one task should be executed per `run_gate.py` invocation.
Each task MUST reference a spec section.
- [ ] T1: TBD (Spec: specs/TBD.md#section)
- [ ] T2: TBD (Spec: specs/TBD.md#section)
- [ ] T3: TBD (Spec: specs/TBD.md#section)
""",
"docs/specs/README.md": """# Specs Directory
This directory contains feature specifications.
**No implementation without a spec.**
## Spec Template
Each spec must include:
1. **What** is being built
2. **Why** it's needed (user story)
3. **Acceptance criteria** (testable)
4. **Constraints** (tech/perf/security)
5. **Out of scope**
## Naming Convention
- `feature-name.md` for feature specs
- `api-endpoint.md` for API specs
- Use lowercase with hyphens
## Status
Mark specs with status:
- DRAFT: Being written
- REVIEW: Ready for approval
- APPROVED: Ready for implementation
- IMPLEMENTED: Done and verified
""",
"docs/traceability.md": """# Traceability Matrix
| Requirement | Design Ref | Implementation Ref | Test Ref | Status |
|---|---|---|---|---|
""",
"docs/agent-handoff.md": """# Agent Handoff (Coding Agent -> OpenClaw Verifier)
Update this file after EVERY task.
## Latest Task
- Gate: TBD
- Task: TBD
- Spec Ref: TBD
- Summary of changes: TBD
## Acceptance Criteria Mapping
- AC-1: PASS/FAIL - evidence
- AC-2: PASS/FAIL - evidence
## OpenClaw Verification Checklist
### CLI Checks (OpenClaw agent runs in terminal)
- [ ] <command 1>
- [ ] <command 2>
### Browser/Manual Checks (OpenClaw agent runs in browser tools)
- [ ] <flow 1>
- [ ] <flow 2>
- [ ] N/A (if no web surface)
## Known Risks / Caveats
- TBD
## If Verification Fails
- Describe the failure clearly and hand back to coding agent with exact repro + logs.
""",
"AGENTS.md": """# AGENTS.md (Project Master Workflow)
## Branching
- Prefer trunk-based flow with short-lived branches.
## Coding Standards
- Keep changes small, testable, and documented.
## Test Expectations
- Unit + integration required for feature logic.
- E2E required for critical user journeys.
## Gate Criteria
- Do not advance gates without recorded evidence.
## Documentation Obligations
- Update docs/progress.md, docs/tasks.md, docs/change-log.md after each meaningful step.
## Change Control
- Any scope change requires change-log entry and affected docs updates.
## Release Readiness
- Must satisfy docs/release-checklist.md.
""",
}
BROWNFIELD_TEMPLATES = {
"docs/as-is-architecture.md": """# As-Is Architecture (Brownfield)
## Current System Overview
TBD
## Components and Responsibilities
TBD
## Known Pain Points
TBD
""",
"docs/system-inventory.md": """# System Inventory (Brownfield)
| Area | Current State | Notes |
|---|---|---|
| Repositories | TBD | |
| Services | TBD | |
| Data Stores | TBD | |
| External Integrations | TBD | |
""",
"docs/dependency-map.md": """# Dependency Map (Brownfield)
## Service/Module Dependencies
TBD
## Critical Dependencies
TBD
""",
"docs/legacy-risk-register.md": """# Legacy Risk Register (Brownfield)
| Risk | Severity | Likelihood | Mitigation | Owner |
|---|---|---|---|---|
""",
"docs/compatibility-matrix.md": """# Compatibility Matrix (Brownfield)
| Interface | Legacy Behavior | New Behavior | Compatible? | Notes |
|---|---|---|---|---|
""",
"docs/migration-plan.md": """# Migration Plan (Brownfield)
## Strategy
- Incremental modernization (strangler-style slices)
## Cutover Plan
TBD
## Rollback Plan
TBD
""",
"docs/characterization-tests.md": """# Characterization Tests (Brownfield)
## Objective
Capture current legacy behavior before change.
## Baseline Scenarios
- CTB-001: TBD
""",
}
def write_if_missing(path: Path, content: str):
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists():
path.write_text(content, encoding="utf-8")
def append_progress(root: Path, message: str):
p = root / "docs" / "progress.md"
now = datetime.now(timezone.utc).isoformat()
if not p.exists():
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text("# Progress\n\n## Activity Log\n", encoding="utf-8")
text = p.read_text(encoding="utf-8")
text += f"\n- [{now}] {message}\n"
p.write_text(text, encoding="utf-8")
def init_status_json(root: Path):
now = datetime.now(timezone.utc).isoformat()
status = {
"meta": {
"createdAt": now,
"updatedAt": now,
"status": "IN_PROGRESS",
},
"gates": {
g: {"state": "PENDING", "updatedAt": None, "note": ""}
for g in ["G0", "G1", "G2", "G3", "G4", "G5", "G6", "G7"]
},
"history": [],
}
p = root / ".orchestrator" / "status.json"
p.parent.mkdir(parents=True, exist_ok=True)
if not p.exists():
p.write_text(json.dumps(status, indent=2), encoding="utf-8")
def init_context_json(root: Path, mode: str):
ctx = {
"projectMode": mode,
"executionMode": "gated",
"researchMode": False,
"primaryAgent": "",
"fallbackAgent": "",
}
p = root / ".orchestrator" / "context.json"
p.parent.mkdir(parents=True, exist_ok=True)
if not p.exists():
p.write_text(json.dumps(ctx, indent=2), encoding="utf-8")
def main():
parser = argparse.ArgumentParser(description="Initialize codex-orchestrator project docs")
parser.add_argument("--root", default=".", help="Project root")
parser.add_argument("--mode", choices=["greenfield", "brownfield"], default="greenfield")
args = parser.parse_args()
root = Path(args.root).resolve()
for rel, content in BASE_TEMPLATES.items():
write_if_missing(root / rel, content)
if args.mode == "brownfield":
for rel, content in BROWNFIELD_TEMPLATES.items():
write_if_missing(root / rel, content)
init_status_json(root)
init_context_json(root, args.mode)
append_progress(root, f"Documentation scaffold initialized (mode={args.mode})")
print(f"Initialized docs scaffold at {root} (mode={args.mode})")
if __name__ == "__main__":
main()
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
import argparse
import zipfile
from pathlib import Path
def validate(skill_dir: Path):
required = [
skill_dir / "SKILL.md",
skill_dir / "scripts" / "init_project_docs.py",
skill_dir / "references" / "planning-questionnaire.md",
skill_dir / "references" / "testing-matrix.md",
skill_dir / "scripts" / "run_gate.py",
skill_dir / "scripts" / "gate_status.py",
skill_dir / "scripts" / "agent_exec.py",
]
missing = [str(p) for p in required if not p.exists()]
if missing:
raise SystemExit("Missing required files:\n" + "\n".join(missing))
def package(skill_dir: Path, out_dir: Path):
out_dir.mkdir(parents=True, exist_ok=True)
skill_name = skill_dir.name
out_file = out_dir / f"{skill_name}.skill"
with zipfile.ZipFile(out_file, "w", zipfile.ZIP_DEFLATED) as zf:
for path in skill_dir.rglob("*"):
if path.is_file():
arcname = f"{skill_name}/{path.relative_to(skill_dir)}"
zf.write(path, arcname)
return out_file
def main():
parser = argparse.ArgumentParser(description="Package codex-orchestrator skill without external deps")
parser.add_argument("--skill-dir", default=".", help="Skill directory")
parser.add_argument("--out", default="dist", help="Output directory")
args = parser.parse_args()
skill_dir = Path(args.skill_dir).resolve()
out_dir = Path(args.out).resolve()
validate(skill_dir)
out_file = package(skill_dir, out_dir)
print(f"Packaged: {out_file}")
if __name__ == "__main__":
main()
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
import argparse
import json
from pathlib import Path
def read_status(root: Path):
p = root / ".orchestrator" / "status.json"
if not p.exists():
return None
return json.loads(p.read_text(encoding="utf-8"))
def main():
parser = argparse.ArgumentParser(description="Render orchestrator progress dashboard")
parser.add_argument("--root", default=".", help="Project root")
args = parser.parse_args()
root = Path(args.root).resolve()
data = read_status(root)
if not data:
print("No .orchestrator/status.json found")
return
gates = data.get("gates", {})
total = len(gates)
passed = sum(1 for g in gates.values() if g.get("state") == "PASS")
pct = int((passed / total) * 100) if total else 0
print("=== Codex Orchestrator Dashboard ===")
print(f"Root: {root}")
print(f"Overall Status: {data.get('meta', {}).get('status', 'UNKNOWN')}")
print(f"Completion: {pct}% ({passed}/{total} gates passed)")
print()
print("Gate States:")
for gate in sorted(gates.keys()):
state = gates[gate].get("state")
note = gates[gate].get("note")
print(f"- {gate}: {state} {('- ' + note) if note else ''}")
hist = data.get("history", [])
if hist:
print()
print("Recent Activity:")
for item in hist[-5:]:
print(f"- {item.get('timestamp')} | {item.get('gate')} -> {item.get('state')} | {item.get('note', '')}")
if __name__ == "__main__":
main()
@@ -0,0 +1,663 @@
#!/usr/bin/env python3
import argparse
import hashlib
import json
import subprocess
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parent
DOC_UPDATE_FILES = [
"docs/tasks.md",
"docs/change-log.md",
"docs/traceability.md",
"docs/test-results.md",
"docs/progress.md",
"docs/agent-handoff.md",
]
BROWNFIELD_ONBOARD_FILES = [
"docs/as-is-architecture.md",
"docs/system-inventory.md",
"docs/dependency-map.md",
"docs/legacy-risk-register.md",
"docs/compatibility-matrix.md",
"docs/migration-plan.md",
"docs/characterization-tests.md",
]
ASSUMPTION_MARKERS = [
"i assumed",
"we assumed",
"assumed that",
"probably",
"likely",
"guessed",
"defaulted to",
"for convenience",
]
def run(cmd, cwd=None):
p = subprocess.run(cmd, cwd=cwd, text=True, capture_output=True)
if p.stdout:
print(p.stdout.strip())
if p.returncode != 0:
if p.stderr:
print(p.stderr.strip())
raise SystemExit(p.returncode)
def run_capture(cmd, cwd=None):
p = subprocess.run(cmd, cwd=cwd, text=True, capture_output=True)
out = (p.stdout or "") + ("\n" + p.stderr if p.stderr else "")
return p.returncode, out.strip()
def file_hash(path: Path) -> str:
if not path.exists():
return "MISSING"
h = hashlib.sha256()
h.update(path.read_bytes())
return h.hexdigest()
def snapshot_files(root: Path, files: list[str]):
snap = {}
for rel in files:
snap[rel] = file_hash(root / rel)
return snap
def changed_files(root: Path, files: list[str], before: dict):
changed = []
for rel in files:
if before.get(rel) != file_hash(root / rel):
changed.append(rel)
return changed
def load_context(root: Path):
p = root / ".orchestrator" / "context.json"
if not p.exists():
raise SystemExit(f"Missing context file: {p}. Run init_project_docs.py first.")
return p, json.loads(p.read_text(encoding="utf-8"))
def save_context(path: Path, ctx):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(ctx, indent=2), encoding="utf-8")
def append_validation_log(root: Path, gate: str, lines: list[str]):
log_path = root / "docs" / "validation-log.md"
log_path.parent.mkdir(parents=True, exist_ok=True)
with log_path.open("a", encoding="utf-8") as f:
f.write(f"\n## {gate} Validation\n")
for line in lines:
f.write(f"- {line}\n")
def set_gate_state(root: Path, gate: str, state: str, note: str):
run(
[
"python3",
str(ROOT / "gate_status.py"),
"set",
"--root",
str(root),
"--gate",
gate,
"--state",
state,
"--note",
note,
]
)
def check_g4_task_plan(root: Path):
path = root / "docs" / "g4-task-plan.md"
if not path.exists():
raise SystemExit("G4 requires docs/g4-task-plan.md with task breakdown.")
text = path.read_text(encoding="utf-8", errors="ignore")
if "[ ]" not in text and "[x]" not in text and "[X]" not in text:
raise SystemExit("G4 task plan must use checklist format ([ ] and [x]).")
def check_g4_ready_for_pass(root: Path):
path = root / "docs" / "g4-task-plan.md"
text = path.read_text(encoding="utf-8", errors="ignore")
if "[ ]" in text:
raise SystemExit("Cannot mark G4 PASS while unchecked tasks remain in docs/g4-task-plan.md")
def build_fix_prompt(
proj_root: Path,
gate: str,
task: str,
spec_ref: str,
failing_cmd: str,
failure_output: str,
retry_num: int,
max_retries: int,
):
prompt_path = proj_root / "docs" / f"prompt-{gate}-fix-{retry_num}.txt"
safe_out = (failure_output or "(no output)")[:1200]
spec_line = spec_ref if spec_ref else "requirements.md#relevant-section"
prompt = f"""You are fixing Gate {gate} after validation failure.
## SPEC-DRIVEN RULES (NON-NEGOTIABLE)
1. Implement ONLY what is specified.
2. Do NOT add unrequested features.
3. Do NOT guess at requirements.
4. If ambiguity remains, document open questions and stop.
## TASK CONTEXT
- Task: {task}
- Spec reference: {spec_line}
- Retry attempt: {retry_num}/{max_retries}
## FAILURE TO FIX
- Command: {failing_cmd}
- Output:
{safe_out}
## REQUIRED ACTIONS
1. Fix the concrete failure above.
2. Re-run the relevant local checks you can run.
3. Update these docs yourself:
- docs/tasks.md
- docs/progress.md
- docs/change-log.md
- docs/traceability.md
- docs/test-results.md
- docs/agent-handoff.md
4. In docs/agent-handoff.md include:
- What you changed
- Why it failed
- Exact CLI checks for OpenClaw agent to run
- Exact browser checks for OpenClaw agent to run (or N/A)
When fully done, run:
openclaw gateway wake --text "Done: {gate} fix attempt {retry_num} complete | verify: docs/agent-handoff.md" --mode now
"""
prompt_path.write_text(prompt, encoding="utf-8")
return prompt_path
def execute_agent(agent_cmd_base: list[str], prompt_file: str):
cmd = list(agent_cmd_base) + ["--prompt-file", str(prompt_file)]
run(cmd)
def run_validation_commands(validate_cmds: list[str], proj_root: Path):
lines = []
for cmd in validate_cmds:
code, out = run_capture(["bash", "-lc", cmd], cwd=str(proj_root))
snippet = (out[:500] + "...") if len(out) > 500 else out
if code != 0:
lines.append(f"FAIL `{cmd}` exit={code}")
if snippet:
lines.append(f"Output: {snippet}")
return False, lines, cmd, snippet
lines.append(f"PASS `{cmd}`")
if snippet:
lines.append(f"Output: {snippet}")
return True, lines, "", ""
def check_assumption_markers(proj_root: Path):
handoff = proj_root / "docs" / "agent-handoff.md"
if not handoff.exists():
return True, []
text = handoff.read_text(encoding="utf-8", errors="ignore").lower()
hits = [m for m in ASSUMPTION_MARKERS if m in text]
if hits:
return False, hits
return True, []
def parse_spec_acceptance_criteria(spec_file: Path):
if not spec_file.exists():
return []
lines = spec_file.read_text(encoding="utf-8", errors="ignore").splitlines()
ids = []
for line in lines:
s = line.strip()
if s.startswith("AC-") and ":" in s:
ids.append(s.split(":", 1)[0].strip())
return sorted(set(ids))
def collect_g4_task_spec_refs(root: Path):
path = root / "docs" / "g4-task-plan.md"
if not path.exists():
return []
refs = []
for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
s = line.strip()
if not s.startswith("-"):
continue
marker = "(Spec: "
if marker in s and ")" in s.split(marker, 1)[1]:
ref = s.split(marker, 1)[1].split(")", 1)[0].strip()
if ref:
refs.append(ref)
return refs
def validate_spec_coverage_for_g4(root: Path, spec_ref: str):
refs = collect_g4_task_spec_refs(root)
if not refs:
return False, "G4 spec coverage check failed: docs/g4-task-plan.md has no '(Spec: ...)' references."
if spec_ref and spec_ref not in refs:
return False, f"G4 spec coverage check failed: {spec_ref} not present in docs/g4-task-plan.md task refs."
return True, ""
def validate_ac_mapping(proj_root: Path, spec_ref: str):
if not spec_ref:
return True, []
spec_rel = spec_ref.split("#", 1)[0]
spec_file = proj_root / "docs" / spec_rel
ac_ids = parse_spec_acceptance_criteria(spec_file)
if not ac_ids:
return True, []
handoff = proj_root / "docs" / "agent-handoff.md"
if not handoff.exists():
return False, ["AC mapping check failed: docs/agent-handoff.md missing."]
text = handoff.read_text(encoding="utf-8", errors="ignore")
missing = [ac for ac in ac_ids if ac not in text]
if missing:
return False, ["AC mapping missing in agent handoff: " + ", ".join(missing)]
return True, []
def get_git_changed_files(proj_root: Path):
code, out = run_capture(["git", "status", "--porcelain"], cwd=str(proj_root))
if code != 0:
return []
changed = []
for line in out.splitlines():
if not line.strip():
continue
path = line[3:].strip() if len(line) > 3 else ""
if path:
changed.append(path)
return changed
def parse_allowed_scope_from_spec(spec_file: Path):
if not spec_file.exists():
return []
lines = spec_file.read_text(encoding="utf-8", errors="ignore").splitlines()
allowed = []
capture = False
for line in lines:
s = line.strip()
lower = s.lower()
if lower.startswith("##") and "allowed scope files" in lower:
capture = True
continue
if capture and s.startswith("##") and "allowed scope files" not in lower:
break
if capture and s.startswith("-"):
candidate = s.lstrip("-").strip().strip("`")
if candidate:
allowed.append(candidate)
return allowed
def validate_drift_against_spec(proj_root: Path, spec_ref: str):
if not spec_ref:
return True, []
spec_rel = spec_ref.split("#", 1)[0]
spec_file = proj_root / "docs" / spec_rel
allowed = parse_allowed_scope_from_spec(spec_file)
if not allowed:
return True, []
changed = get_git_changed_files(proj_root)
if not changed:
return True, []
violations = []
for path in changed:
if path.startswith("docs/") or path.startswith(".orchestrator/"):
continue
if not any(path == a or path.startswith(a.rstrip("/") + "/") for a in allowed):
violations.append(path)
if violations:
return False, ["Spec drift detected (changed outside allowed scope): " + ", ".join(sorted(set(violations)))]
return True, []
def write_validation_artifact(
proj_root: Path,
gate: str,
task: str,
spec_ref: str,
validate_cmds: list[str],
validation_lines: list[str],
ui_review_note: str,
status: str,
):
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
out_dir = proj_root / "docs" / "validation-artifacts"
out_dir.mkdir(parents=True, exist_ok=True)
path = out_dir / f"{gate}-{ts}.json"
payload = {
"timestamp": ts,
"gate": gate,
"task": task,
"specRef": spec_ref,
"status": status,
"validateCmds": validate_cmds,
"uiReviewNote": ui_review_note,
"results": validation_lines,
}
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
return path
def main():
parser = argparse.ArgumentParser(description="Single-task gate runner (agent-executed, evidence-driven)")
parser.add_argument("--root", default=".", help="Project root")
parser.add_argument("--gate", required=True, choices=["G1", "G2", "G3", "G4", "G5", "G6", "G7"])
parser.add_argument(
"--agent",
required=True,
choices=["codex", "claude", "opencode", "pi"],
help="Coding agent to execute implementation work",
)
parser.add_argument(
"--fallback-agent",
choices=["codex", "claude", "opencode", "pi"],
help="Fallback coding agent (required first time if context has no fallback)",
)
parser.add_argument("--project-mode", choices=["greenfield", "brownfield"], default="greenfield")
parser.add_argument("--execution-mode", choices=["autonomous", "gated"], default="gated")
parser.add_argument("--research-mode", choices=["true", "false"], default="false")
parser.add_argument("--task", required=True, help="Single task summary (one task per run)")
parser.add_argument("--evidence", default="", help="Evidence summary")
parser.add_argument(
"--status",
default="IN_PROGRESS",
choices=["IN_PROGRESS", "PASS", "FAIL", "BLOCKED"],
help="Gate status to set for this run",
)
parser.add_argument("--prompt-out", help="Optional prompt output path")
parser.add_argument("--full-auto", action="store_true", help="Pass full-auto to codex in agent_exec")
parser.add_argument("--agent-dry-run", action="store_true", help="Print coding-agent command without executing it")
parser.add_argument(
"--validate-cmd",
action="append",
default=[],
help="Validation command to run after agent execution (repeatable). Required when status=PASS.",
)
parser.add_argument(
"--ui-review-note",
default="",
help="Manual browser/UI verification notes produced by OpenClaw agent after checks.",
)
parser.add_argument(
"--requires-browser-check",
action="store_true",
help="Require explicit browser/manual review note for this task.",
)
parser.add_argument(
"--spec-ref",
default="",
help="Spec reference for this task (required for G3/G4). Format: specs/feature.md#section or requirements.md#feature",
)
parser.add_argument(
"--auto-fix-retries",
type=int,
default=2,
help="Autonomous mode: retries with fix prompts after failed validations (default: 2).",
)
parser.add_argument(
"--auto-block-on-retry-exhaust",
action="store_true",
help="When retries are exhausted, auto-set gate state to BLOCKED with failure reason before exit.",
)
args = parser.parse_args()
proj_root = Path(args.root).resolve()
prompt_out = args.prompt_out or str(proj_root / "docs" / f"prompt-{args.gate}.txt")
# Context + agent persistence
ctx_path, ctx = load_context(proj_root)
ctx.setdefault("primaryAgent", "")
ctx.setdefault("fallbackAgent", "")
if not ctx["primaryAgent"]:
ctx["primaryAgent"] = args.agent
elif ctx["primaryAgent"] != args.agent:
print(f"[warn] overriding primaryAgent from {ctx['primaryAgent']} to {args.agent}")
ctx["primaryAgent"] = args.agent
if not ctx["fallbackAgent"]:
if not args.fallback_agent:
raise SystemExit("Missing fallback agent. Provide --fallback-agent for first run.")
ctx["fallbackAgent"] = args.fallback_agent
elif args.fallback_agent and args.fallback_agent != ctx["fallbackAgent"]:
print(f"[warn] overriding fallbackAgent from {ctx['fallbackAgent']} to {args.fallback_agent}")
ctx["fallbackAgent"] = args.fallback_agent
ctx["projectMode"] = args.project_mode
ctx["executionMode"] = args.execution_mode
ctx["researchMode"] = True if args.research_mode == "true" else False
save_context(ctx_path, ctx)
# Evidence and manual-check policy
if args.status == "PASS" and not args.validate_cmd:
raise SystemExit("status=PASS requires at least one --validate-cmd to prove working behavior.")
if args.status == "PASS" and args.agent_dry_run:
raise SystemExit("status=PASS is not allowed with --agent-dry-run. Execute the coding agent for real.")
# Every task should include validation activity (CLI and/or browser)
if args.status in ("IN_PROGRESS", "PASS") and not args.validate_cmd and not args.ui_review_note:
raise SystemExit(
"Each task must include post-task validation evidence. Provide --validate-cmd and/or --ui-review-note."
)
if args.requires_browser_check and not args.ui_review_note:
raise SystemExit("This task requires browser checks. Provide --ui-review-note after manual browser validation.")
# Spec-driven enforcement: G3/G4 require spec reference
if args.gate in ("G3", "G4") and not args.spec_ref:
raise SystemExit(
f"{args.gate} requires --spec-ref. No implementation without a spec.\n"
"Format: --spec-ref specs/feature.md#section or --spec-ref requirements.md#feature"
)
# Verify spec file exists
if args.spec_ref:
spec_path = args.spec_ref.split("#")[0]
full_spec_path = proj_root / "docs" / spec_path
if not full_spec_path.exists():
raise SystemExit(f"Spec file not found: {full_spec_path}. Create the spec before implementation.")
if args.gate == "G4":
check_g4_task_plan(proj_root)
if args.status == "PASS":
check_g4_ready_for_pass(proj_root)
# 1) Generate initial gate prompt
run(
[
"python3",
str(ROOT / "generate_gate_prompt.py"),
"--gate",
args.gate,
"--agent",
args.agent,
"--project-mode",
args.project_mode,
"--execution-mode",
args.execution_mode,
"--research-mode",
args.research_mode,
"--task",
args.task,
"--spec-ref",
args.spec_ref,
"--output",
prompt_out,
]
)
# Build agent command base (prompt-file added per attempt)
agent_cmd_base = [
"python3",
str(ROOT / "agent_exec.py"),
"--root",
str(proj_root),
"--agent",
args.agent,
"--spec-ref",
args.spec_ref,
]
if args.gate in ("G3", "G4"):
agent_cmd_base.append("--enforce-spec-ref")
if args.full_auto:
agent_cmd_base.append("--full-auto")
if args.agent_dry_run:
agent_cmd_base.append("--dry-run")
max_fix_retries = args.auto_fix_retries if args.execution_mode == "autonomous" else 0
attempt = 0
current_prompt = prompt_out
all_validation_lines = []
while True:
before_docs = snapshot_files(proj_root, DOC_UPDATE_FILES)
before_brownfield = snapshot_files(proj_root, BROWNFIELD_ONBOARD_FILES)
# 2) Execute coding agent
execute_agent(agent_cmd_base, current_prompt)
# 3) Verify docs were updated by coding agent (every task, every run)
changed = changed_files(proj_root, DOC_UPDATE_FILES, before_docs)
if not args.agent_dry_run and args.status in ("IN_PROGRESS", "PASS") and not changed:
raise SystemExit(
"Coding agent did not update required docs (tasks/change-log/traceability/test-results/progress/agent-handoff). "
"Docs updates must be done by the coding agent after each task."
)
# Brownfield onboarding (G1/G2) must also be authored by coding agent
changed_brownfield = []
if args.project_mode == "brownfield" and args.gate in ("G1", "G2") and not args.agent_dry_run:
changed_brownfield = changed_files(proj_root, BROWNFIELD_ONBOARD_FILES, before_brownfield)
if not changed_brownfield:
raise SystemExit(
"Brownfield onboarding docs were not updated by the coding agent during this run. "
"Agent must update onboarding artifacts directly."
)
# 4) CLI/manual validations by orchestrator
ok, validation_lines, failing_cmd, failure_snippet = run_validation_commands(args.validate_cmd, proj_root)
assumptions_ok, assumption_hits = check_assumption_markers(proj_root)
if not assumptions_ok:
ok = False
failing_cmd = "assumption-detector:docs/agent-handoff.md"
failure_snippet = "Assumption language detected: " + ", ".join(assumption_hits)
validation_lines.append("FAIL assumption detector: " + ", ".join(assumption_hits))
if args.gate == "G4":
coverage_ok, coverage_msg = validate_spec_coverage_for_g4(proj_root, args.spec_ref)
if not coverage_ok:
ok = False
failing_cmd = "g4-spec-coverage:docs/g4-task-plan.md"
failure_snippet = coverage_msg
validation_lines.append("FAIL " + coverage_msg)
if args.gate in ("G3", "G4") and args.spec_ref:
ac_ok, ac_msgs = validate_ac_mapping(proj_root, args.spec_ref)
if not ac_ok:
ok = False
failing_cmd = "ac-mapping:docs/agent-handoff.md"
failure_snippet = " | ".join(ac_msgs)
validation_lines.extend(["FAIL " + msg for msg in ac_msgs])
drift_ok, drift_msgs = validate_drift_against_spec(proj_root, args.spec_ref)
if not drift_ok:
ok = False
failing_cmd = "spec-drift:git-status"
failure_snippet = " | ".join(drift_msgs)
validation_lines.extend(["FAIL " + msg for msg in drift_msgs])
if args.ui_review_note:
validation_lines.append(f"UI review: {args.ui_review_note}")
if changed:
validation_lines.append("Docs updated by agent: " + ", ".join(changed))
if changed_brownfield:
validation_lines.append("Brownfield onboarding docs updated by agent: " + ", ".join(changed_brownfield))
all_validation_lines.extend(validation_lines)
if ok:
break
# Validation failed: autonomous fix-retry loop
if args.agent_dry_run:
append_validation_log(proj_root, args.gate, all_validation_lines)
raise SystemExit(f"Validation failed in dry-run mode: `{failing_cmd}`")
if attempt >= max_fix_retries:
append_validation_log(proj_root, args.gate, all_validation_lines)
failure_note = (
f"Retry exhausted after {attempt} attempts. Last failed command: {failing_cmd}. "
f"Failure: {failure_snippet[:240]}"
)
if args.auto_block_on_retry_exhaust:
set_gate_state(proj_root, args.gate, "BLOCKED", failure_note)
append_validation_log(proj_root, args.gate, ["Auto-classified as BLOCKED due to retry exhaustion."])
raise SystemExit(
f"Validation failed after {attempt} fix retries. Last failed command: `{failing_cmd}`"
)
attempt += 1
all_validation_lines.append(
f"Auto-fix retry {attempt}/{max_fix_retries}: re-invoking coding agent with failure details."
)
fix_prompt_path = build_fix_prompt(
proj_root=proj_root,
gate=args.gate,
task=args.task,
spec_ref=args.spec_ref,
failing_cmd=failing_cmd,
failure_output=failure_snippet,
retry_num=attempt,
max_retries=max_fix_retries,
)
current_prompt = str(fix_prompt_path)
# 5) Persist validation evidence log + machine artifact
if all_validation_lines:
append_validation_log(proj_root, args.gate, all_validation_lines)
artifact_path = write_validation_artifact(
proj_root=proj_root,
gate=args.gate,
task=args.task,
spec_ref=args.spec_ref,
validate_cmds=args.validate_cmd,
validation_lines=all_validation_lines,
ui_review_note=args.ui_review_note,
status=args.status,
)
append_validation_log(proj_root, args.gate, [f"Validation artifact: {artifact_path.relative_to(proj_root)}"])
# 6) Gate status + dashboard
set_gate_state(proj_root, args.gate, args.status, args.task)
run(["python3", str(ROOT / "progress_dashboard.py"), "--root", str(proj_root)])
if __name__ == "__main__":
main()
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
import argparse
from pathlib import Path
from datetime import datetime, timezone
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def append(path: Path, text: str):
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists():
path.write_text("", encoding="utf-8")
with path.open("a", encoding="utf-8") as f:
f.write(text)
def main():
parser = argparse.ArgumentParser(description="Append standard per-step doc updates")
parser.add_argument("--root", default=".", help="Project root")
parser.add_argument("--gate", required=True, help="Gate id, e.g. G3")
parser.add_argument("--task", required=True, help="Task summary")
parser.add_argument("--status", default="DONE", help="Task status")
parser.add_argument("--evidence", default="", help="Evidence summary")
parser.add_argument("--owner", default="orchestrator", help="Owner")
args = parser.parse_args()
root = Path(args.root).resolve()
ts = now_iso()
append(root / "docs" / "tasks.md", f"| {args.gate}-{ts} | {args.task} | {args.status} | {args.gate} | {args.owner} | {ts} |\n")
append(root / "docs" / "change-log.md", f"| {ts} | {args.task} | Gate {args.gate} execution | code/docs/tests | {args.owner} |\n")
append(root / "docs" / "traceability.md", f"| {args.gate}-{ts} | docs/architecture.md/docs/requirements.md | implementation update | test run | {args.status} |\n")
append(root / "docs" / "test-results.md", f"| {args.gate} | {args.task} | see commands/logs | expected met | {args.evidence or 'see logs'} | {'PASS' if args.status.upper() in ('DONE','PASS') else args.status.upper()} | {args.evidence or 'n/a'} | {ts} |\n")
append(root / "docs" / "progress.md", f"\n- [{ts}] {args.gate}: {args.task} ({args.status})\n")
print("Documentation step updates appended.")
if __name__ == "__main__":
main()