AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user