From 6bd7769e867fa238d26a156aa441dd261c53c228 Mon Sep 17 00:00:00 2001 From: rob Date: Thu, 2 Oct 2025 00:41:55 -0300 Subject: [PATCH] chore(init): bootstrap AI workflow repository --- .gitea/workflows/ci.yml | 14 ++ .gitignore | 10 + CHANGELOG.md | 4 + CODEOWNERS | 6 + CONTRIBUTING.md | 4 + README.md | 13 + SECURITY.md | 1 + decisions/README.md | 1 + decisions/adr/ADR-000-template.md | 3 + docs/README.md | 1 + docs/explanations/README.md | 1 + docs/how-to/README.md | 1 + docs/reference/README.md | 1 + docs/tutorials/README.md | 1 + feature-flags/overview.md | 1 + governance/change-policy.md | 9 + governance/ownership.md | 3 + process/acceptance_checklist.md | 4 + process/report-template.md | 19 ++ project/plan.md | 17 ++ project/rules.md | 7 + project/state.md | 13 + project/tasks.yaml | 1 + prompts/documentation_agent_prompt.txt | 1 + prompts/mission_control_prompt.txt | 10 + prompts/overseer_prompt.txt | 1 + prompts/step_agent_prompt.txt | 6 + rfcs/RFC-YYYY-00-template.md | 2 + roles/documentation_agent.md | 4 + roles/mission_control.md | 5 + roles/overseer.md | 4 + roles/step_agent.md | 5 + workflow-init.py | 104 ++++++++ workflow.py | 335 +++++++++++++++++++++++++ 34 files changed, 612 insertions(+) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CODEOWNERS create mode 100644 CONTRIBUTING.md create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 decisions/README.md create mode 100644 decisions/adr/ADR-000-template.md create mode 100644 docs/README.md create mode 100644 docs/explanations/README.md create mode 100644 docs/how-to/README.md create mode 100644 docs/reference/README.md create mode 100644 docs/tutorials/README.md create mode 100644 feature-flags/overview.md create mode 100644 governance/change-policy.md create mode 100644 governance/ownership.md create mode 100644 process/acceptance_checklist.md create mode 100644 process/report-template.md create mode 100644 project/plan.md create mode 100644 project/rules.md create mode 100644 project/state.md create mode 100644 project/tasks.yaml create mode 100644 prompts/documentation_agent_prompt.txt create mode 100644 prompts/mission_control_prompt.txt create mode 100644 prompts/overseer_prompt.txt create mode 100644 prompts/step_agent_prompt.txt create mode 100644 rfcs/RFC-YYYY-00-template.md create mode 100644 roles/documentation_agent.md create mode 100644 roles/mission_control.md create mode 100644 roles/overseer.md create mode 100644 roles/step_agent.md create mode 100755 workflow-init.py create mode 100755 workflow.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..7e17fea --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,14 @@ +name: CI +on: + push: + branches: [ main ] + pull_request: +jobs: + build: + runs-on: docker + steps: + - uses: actions/checkout@v4 + - name: Validate structure + run: | + test -f project/plan.md + test -f roles/mission_control.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d293aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +*.pyc +node_modules/ +.DS_Store +.vscode/ +.idea/ +.env +.secrets/ +dist/ +build/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2df0f70 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## [Unreleased] +- Bootstrap on 2025-10-02 diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..d041079 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,6 @@ +/governance/** @rob +/project/** @rob +/roles/** @rob +/decisions/** @rob +/rfcs/** @rob +/docs/** @rob diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..585dfc0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,4 @@ +- Use **Conventional Commits**. +- Short-lived branches off `main`; open PRs early. +- Significant changes → RFC; accepted architecture choices → ADR. +- Protected paths require CODEOWNERS approvals. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a5968f --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# ai-workflow-test — AI-Assisted Development Workflow + +This repo is a **bootstrapping pack** for a mission-controlled, role-based AI workflow. + +- Roles in `roles/` +- Plan/state in `project/` +- Governance in `governance/` +- Process templates in `process/` +- Decisions in `decisions/` and `rfcs/` +- Prompts in `prompts/` +- Docs in `docs/` + +**Start here:** run `python3 workflow-init.py` to fill placeholders and (optionally) git-init. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4ad21e8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1 @@ +# Security: never commit secrets. diff --git a/decisions/README.md b/decisions/README.md new file mode 100644 index 0000000..3f1e264 --- /dev/null +++ b/decisions/README.md @@ -0,0 +1 @@ +# Decisions: ADRs in `/decisions/adr/`, RFCs in `/rfcs/`. diff --git a/decisions/adr/ADR-000-template.md b/decisions/adr/ADR-000-template.md new file mode 100644 index 0000000..60413c4 --- /dev/null +++ b/decisions/adr/ADR-000-template.md @@ -0,0 +1,3 @@ +# ADR-000 +Date: 2025-10-02 +Status: Proposed | Accepted | Superseded diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..48d78e6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1 @@ +# Docs (Diátaxis) diff --git a/docs/explanations/README.md b/docs/explanations/README.md new file mode 100644 index 0000000..0037f49 --- /dev/null +++ b/docs/explanations/README.md @@ -0,0 +1 @@ +# Explanations diff --git a/docs/how-to/README.md b/docs/how-to/README.md new file mode 100644 index 0000000..706522e --- /dev/null +++ b/docs/how-to/README.md @@ -0,0 +1 @@ +# How-To diff --git a/docs/reference/README.md b/docs/reference/README.md new file mode 100644 index 0000000..cf5aa07 --- /dev/null +++ b/docs/reference/README.md @@ -0,0 +1 @@ +# Reference diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md new file mode 100644 index 0000000..81c8590 --- /dev/null +++ b/docs/tutorials/README.md @@ -0,0 +1 @@ +# Tutorials diff --git a/feature-flags/overview.md b/feature-flags/overview.md new file mode 100644 index 0000000..1f19887 --- /dev/null +++ b/feature-flags/overview.md @@ -0,0 +1 @@ +# Feature Flags overview diff --git a/governance/change-policy.md b/governance/change-policy.md new file mode 100644 index 0000000..fd098d9 --- /dev/null +++ b/governance/change-policy.md @@ -0,0 +1,9 @@ +# Change Policy + +- Minor: docs/refactor → PR + one review +- Standard: code/config within boundaries → PR + CODEOWNERS review +- Major: breaking/architecture → RFC + Overseer + Human approval → PR + +Rules: +- No direct pushes to `main` +- PR links to task in `project/state.md` and ADR/RFC if relevant diff --git a/governance/ownership.md b/governance/ownership.md new file mode 100644 index 0000000..450a9a2 --- /dev/null +++ b/governance/ownership.md @@ -0,0 +1,3 @@ +# Ownership + +Owners guard quality and intent for their areas. diff --git a/process/acceptance_checklist.md b/process/acceptance_checklist.md new file mode 100644 index 0000000..19d6a72 --- /dev/null +++ b/process/acceptance_checklist.md @@ -0,0 +1,4 @@ +- [ ] Meets acceptance criteria +- [ ] CI/tests pass +- [ ] CODEOWNERS approved +- [ ] Docs & Changelog updated diff --git a/process/report-template.md b/process/report-template.md new file mode 100644 index 0000000..2d7712f --- /dev/null +++ b/process/report-template.md @@ -0,0 +1,19 @@ +# Step Agent Report +TASK: <name> +STATUS: Done | Partial | Blocked + +WHAT I DID +- … + +EVIDENCE +- Commands/tests/output + +ARTIFACTS +- PR: <link> +- Patch: <path> + +OPEN QUESTIONS / BLOCKERS +- … + +RECOMMENDED NEXT STEPS +- … diff --git a/project/plan.md b/project/plan.md new file mode 100644 index 0000000..cef3f4a --- /dev/null +++ b/project/plan.md @@ -0,0 +1,17 @@ +# Master Plan + +Project: ai-workflow-test +Repo: https://gitea.brrd.tech/rob/test-workflow +Last updated: 2025-10-02 + +## Goals +- … + +## Milestones +1) Bootstrap & governance +2) First feature behind a flag +3) Observability & SLOs + +## Backlog +- [ ] Task A +- [ ] Task B diff --git a/project/rules.md b/project/rules.md new file mode 100644 index 0000000..bdc476d --- /dev/null +++ b/project/rules.md @@ -0,0 +1,7 @@ +# Rules + +- Default timebox: 40 messages +- Reports use `/process/report-template.md` +- Changes via PR; protected paths need CODEOWNERS reviews +- Major shifts require RFC + Overseer + Human approval +- Docs follow Diátaxis under `docs/` diff --git a/project/state.md b/project/state.md new file mode 100644 index 0000000..6e4afff --- /dev/null +++ b/project/state.md @@ -0,0 +1,13 @@ +# Current State (as of 2025-10-02) + +## In-Flight +- Task: … (owner: MC) Status: … + +## Recently Completed +- … + +## Blockers / Risks +- … + +## Next Task Candidates +- … diff --git a/project/tasks.yaml b/project/tasks.yaml new file mode 100644 index 0000000..7bb9491 --- /dev/null +++ b/project/tasks.yaml @@ -0,0 +1 @@ +# tasks: [] diff --git a/prompts/documentation_agent_prompt.txt b/prompts/documentation_agent_prompt.txt new file mode 100644 index 0000000..8d2c217 --- /dev/null +++ b/prompts/documentation_agent_prompt.txt @@ -0,0 +1 @@ +You are the **Documentation Agent**. diff --git a/prompts/mission_control_prompt.txt b/prompts/mission_control_prompt.txt new file mode 100644 index 0000000..59f1b2b --- /dev/null +++ b/prompts/mission_control_prompt.txt @@ -0,0 +1,10 @@ +You are **Mission Control**. + +1) Load `roles/mission_control.md` +2) Read `project/plan.md`, `project/state.md`, `project/rules.md` +3) For major plan changes: request Overseer review; Overseer must confirm with the Human +4) Emit one **Task Prompt** for a Step Agent with: + - Goal & Acceptance Criteria + - Minimal context links + - Timebox: 40 messages +5) Await the Report; then propose diffs to update plan/state diff --git a/prompts/overseer_prompt.txt b/prompts/overseer_prompt.txt new file mode 100644 index 0000000..49da8f7 --- /dev/null +++ b/prompts/overseer_prompt.txt @@ -0,0 +1 @@ +You are the **Overseer**. diff --git a/prompts/step_agent_prompt.txt b/prompts/step_agent_prompt.txt new file mode 100644 index 0000000..0c3ab0a --- /dev/null +++ b/prompts/step_agent_prompt.txt @@ -0,0 +1,6 @@ +You are a **Step Agent**. + +1) Load `roles/step_agent.md` +2) Work only on the assigned task +3) Use Proposed Diffs/PR for governed files +4) End the timebox (40 messages) with a Report (`/process/report-template.md`) diff --git a/rfcs/RFC-YYYY-00-template.md b/rfcs/RFC-YYYY-00-template.md new file mode 100644 index 0000000..2f266c4 --- /dev/null +++ b/rfcs/RFC-YYYY-00-template.md @@ -0,0 +1,2 @@ +# RFC <title> +Status: Draft | In review | Accepted | Rejected diff --git a/roles/documentation_agent.md b/roles/documentation_agent.md new file mode 100644 index 0000000..29c0195 --- /dev/null +++ b/roles/documentation_agent.md @@ -0,0 +1,4 @@ +# Role: Documentation Agent +- Update CHANGELOG on merges +- Maintain docs (Diátaxis) and ADRs +- Cross-link PRs and decisions diff --git a/roles/mission_control.md b/roles/mission_control.md new file mode 100644 index 0000000..cb43856 --- /dev/null +++ b/roles/mission_control.md @@ -0,0 +1,5 @@ +# Role: Mission Control +- Maintain plan/state; assign timeboxed tasks to Step Agents +- Require a Report each timebox +- Gate changes via PR + CODEOWNERS +- Major plan changes → request Overseer review; Overseer must obtain Human confirmation diff --git a/roles/overseer.md b/roles/overseer.md new file mode 100644 index 0000000..5dfd58f --- /dev/null +++ b/roles/overseer.md @@ -0,0 +1,4 @@ +# Role: Overseer +- Audit alignment; detect drift +- For major plan changes: obtain explicit Human confirmation +- Instruct MC to formalize changes via PRs diff --git a/roles/step_agent.md b/roles/step_agent.md new file mode 100644 index 0000000..53b5d77 --- /dev/null +++ b/roles/step_agent.md @@ -0,0 +1,5 @@ +# Role: Step Agent +- Execute one assigned task only +- Stay in scope; if blocked, propose options +- Provide Proposed Diffs/PR (no silent changes) +- End with a Report using `/process/report-template.md` diff --git a/workflow-init.py b/workflow-init.py new file mode 100755 index 0000000..5636ceb --- /dev/null +++ b/workflow-init.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""workflow-init.py +Interactive initializer for the AI workflow repo. +- Replaces placeholders like ai-workflow-test, https://gitea.brrd.tech/rob/test-workflow, rob, 40 messages, main +- Honors default syntax default +- Optionally git init + first commit +Usage: + python3 workflow-init.py [--path .] [--no-git] +""" +import argparse, os, re, sys, subprocess +from pathlib import Path + +PLACEHOLDER_PATTERN = re.compile(r"\{\{([^}|]+)(?:\|([^}]*))?\}\}") +TEXT_EXTS = {".md", ".txt", ".yml", ".yaml", ".json", ".toml", ".py", ".sh", ".gitignore", ""} + +def prompt(question, default=None): + val = input(f"{question}" + (f" [{default}]" if default else "") + ": ").strip() + return val or (default or "") + +def is_text_file(p: Path): + if p.name.startswith(".git"): return False + if p.is_dir(): return False + if p.suffix in TEXT_EXTS: return True + try: + p.read_bytes().decode("utf-8") + return True + except Exception: + return False + +def replace_placeholders(text: str, mapping: dict) -> str: + def sub(m): + var, default = m.group(1), m.group(2) + return mapping.get(var, default or "") + return PLACEHOLDER_PATTERN.sub(sub, text) + +def replace_in_file(p: Path, mapping: dict): + try: + orig = p.read_text(encoding="utf-8") + except Exception: + return False + new = replace_placeholders(orig, mapping) + if new != orig: + p.write_text(new, encoding="utf-8") + return True + return False + +def git(cmd, cwd): + return subprocess.run(["git"] + cmd, cwd=cwd, check=False, capture_output=True, text=True) + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--path", default=".", help="Repository root path") + ap.add_argument("--no-git", action="store_true", help="Do not run git init/commit") + args = ap.parse_args() + + root = Path(args.path).resolve() + if not root.exists(): + print(f"Path not found: {root}", file=sys.stderr); sys.exit(1) + + print("Welcome! Let's initialize your AI workflow repository.\n") + + mapping = { + "PROJECT_NAME": prompt("Project name", "MyProject"), + "PRIMARY_OWNER": prompt("Primary owner/handle (for CODEOWNERS)", "your-handle"), + "REPO_URL": prompt("Gitea repo URL (you can set later)", "https://gitea.example.com/you/myproject"), + "TIMEBOX": prompt("Default timebox (e.g., '40 messages or 60 minutes')", "40 messages or 60 minutes"), + "DEFAULT_BRANCH": prompt("Default branch", "main"), + } + + print("\nReplacing placeholders...") + changed = 0 + for p in root.rglob("*"): + if is_text_file(p): + if replace_in_file(p, mapping): + changed += 1 + print(f"Updated {changed} files with your settings.") + + (root / "reports").mkdir(parents=True, exist_ok=True) + (root / "project/tasks.yaml").write_text("# tasks: []\n", encoding="utf-8") if not (root / "project/tasks.yaml").exists() else None + + if not args.no_git: + if not (root / ".git").exists(): + print("\nInitializing git repository...") + r = git(["init"], cwd=root) + if r.returncode != 0: + print("git init failed:", r.stderr, file=sys.stderr) + if mapping["DEFAULT_BRANCH"] != "master": + r = git(["checkout", "-B", mapping["DEFAULT_BRANCH"]], cwd=root) + if r.returncode != 0: + print("git branch setup failed:", r.stderr, file=sys.stderr) + git(["add", "-A"], cwd=root) + r = git(["commit", "-m", "chore(init): bootstrap AI workflow repository"], cwd=root) + if r.returncode != 0: + print("git commit failed:", r.stderr, file=sys.stderr) + + print("\nDone. Next steps:") + print(f" 1) Review {root/'project/plan.md'} and {root/'project/state.md'}") + print(f" 2) Set remote: git remote add origin {mapping['REPO_URL']}") + print(f" 3) Push: git push -u origin {mapping['DEFAULT_BRANCH']}") + print(" 4) Start Mission Control with: python3 workflow.py start-mc") + +if __name__ == '__main__': + main() + diff --git a/workflow.py b/workflow.py new file mode 100755 index 0000000..2897557 --- /dev/null +++ b/workflow.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +"""workflow.py +Guides the Mission Control / Step Agent loop with prompt bundling and state updates. + +Commands: + validate -> Check that required files exist and no placeholders remain + start-mc -> Print a Mission Control prompt (reads repo docs) + start-step --task "TASK_ID" -> Print a Step Agent prompt for a task from tasks.yaml + new-task --id ID --title "..." [--owner "name"] [--status "in-progress"] [--dry-run] + complete-task --id ID [--dry-run] + submit-report --task "TASK_ID" --report path.md [--dry-run] + status -> Show top of plan/state and list tasks + +Notes: +- Uses a strict, tiny YAML subset for project/tasks.yaml: + tasks: + - id: TASK-001 + title: Implement authentication + owner: step-agent + status: in-progress # or todo, done, blocked + started: 2025-10-01 +""" +import argparse, sys, os, re, shutil, subprocess, datetime +from pathlib import Path + +REPO = Path(".").resolve() +REQ_FILES = [ + "roles/mission_control.md", + "roles/step_agent.md", + "project/plan.md", + "project/state.md", + "project/rules.md", + "project/tasks.yaml", +] +PLACEHOLDER_RE = re.compile(r"\{\{[^}]+\}\}") + +def read(p: Path) -> str: + try: + return p.read_text(encoding="utf-8").strip() + except Exception: + return "" + +def box(text: str) -> str: + lines = text.splitlines() + width = max((len(l) for l in lines), default=0) + 2 + top = "╔" + "═"*width + "╗" + bot = "╚" + "═"*width + "╝" + body = "\n".join("║ " + l.ljust(width-2) + " ║" for l in lines) + return f"{top}\n{body}\n{bot}" + +def git(cmd): + r = subprocess.run(["git"] + cmd, cwd=REPO, check=False, capture_output=True, text=True) + return r + +# --- Minimal YAML parser/writer for our simple tasks.yaml schema --- + +def parse_tasks_yaml(text: str): + """ + Accepts a tiny subset: + tasks: + - id: TASK-001 + title: Foo + owner: someone + status: in-progress + started: 2025-10-01 + """ + tasks = [] + lines = [l.rstrip("\n") for l in text.splitlines()] + i = 0 + # find "tasks:" + while i < len(lines) and not lines[i].strip().startswith("tasks:"): + i += 1 + if i == len(lines): return {"tasks": []} + i += 1 + current = None + while i < len(lines): + line = lines[i] + if not line.strip(): + i += 1; continue + if line.lstrip().startswith("- "): + if current: tasks.append(current) + current = {} + # allow "- id: TASK-001" inline + rest = line.strip()[2:].strip() + if rest: + if ":" in rest: + k,v = rest.split(":",1) + current[k.strip()] = v.strip() + elif line.startswith(" ") or line.startswith("\t"): + stripped = line.strip() + if ":" in stripped and current is not None: + k,v = stripped.split(":",1) + current[k.strip()] = v.strip() + else: + # anything else ends tasks section + break + i += 1 + if current: tasks.append(current) + return {"tasks": tasks} + +def dump_tasks_yaml(data): + out = ["tasks:"] + for t in data.get("tasks", []): + out.append(f" - id: {t.get('id','')}") + for k in ("title","owner","status","started"): + if t.get(k): + out.append(f" {k}: {t[k]}") + return "\n".join(out) + "\n" + +def load_tasks(): + p = REPO/"project/tasks.yaml" + if not p.exists(): return {"tasks": []} + return parse_tasks_yaml(read(p)) + +def save_tasks(data, dry_run=False): + content = dump_tasks_yaml(data) + if dry_run: + print("---- tasks.yaml (dry-run) ----") + print(content) + return + (REPO/"project/tasks.yaml").write_text(content, encoding="utf-8") + +# --- Commands --- + +def cmd_validate(args): + ok = True + for f in REQ_FILES: + if not (REPO/f).exists(): + print(f"Missing required file: {f}", file=sys.stderr) + ok = False + # scan for unreplaced placeholders + for f in REQ_FILES: + text = read(REPO/f) + if PLACEHOLDER_RE.search(text): + print(f"Unreplaced placeholders in: {f}", file=sys.stderr) + ok = False + if ok: + print("Repository looks valid ✅") + else: + sys.exit(2) + +def start_mc(args): + mc_role = read(REPO/"roles/mission_control.md") + plan = read(REPO/"project/plan.md") + state = read(REPO/"project/state.md") + rules = read(REPO/"project/rules.md") + prompt = f""" +You are **Mission Control**. + +--- ROLE --- +{mc_role} + +--- PLAN --- +{plan} + +--- STATE --- +{state} + +--- RULES --- +{rules} + +YOUR TASK NOW: +1) Review the PLAN and STATE. +2) Emit exactly one **Task Prompt** for a Step Agent with: + - Goal & Acceptance Criteria + - Minimal context links (paths) + - Timebox from rules +3) If you believe a **major plan change** is required, draft the change and ask for Overseer review; Overseer must obtain explicit Human confirmation before approval. +""".strip() + print(box(prompt)) + +def start_step(args): + tid = args.task.strip() + tasks = load_tasks().get("tasks", []) + task = next((t for t in tasks if t.get("id") == tid), None) + step_role = read(REPO/"roles/step_agent.md") + rules = read(REPO/"project/rules.md") + if not task: + context = "(task not found in tasks.yaml; follow the Mission Control prompt you received)" + else: + context = f"ID: {task.get('id')}\nTitle: {task.get('title')}\nOwner: {task.get('owner','')}\nStatus: {task.get('status','')}\nStarted: {task.get('started','')}" + + prompt = f""" +You are a **Step Agent** for the task: {tid} + +--- ROLE --- +{step_role} + +--- RULES --- +{rules} + +--- TASK CONTEXT (from tasks.yaml) --- +{context} + +INSTRUCTIONS: +- Work **only** on the assigned task. +- When editing governed files, output **Proposed Diffs** (unified patches) or open a PR (no silent changes). +- End the timebox with a **Report** using `/process/report-template.md`. +""".strip() + print(box(prompt)) + +def cmd_new_task(args): + tid = args.id.strip() + tasks = load_tasks() + if any(t.get("id")==tid for t in tasks["tasks"]): + print(f"Task {tid} already exists.", file=sys.stderr); sys.exit(1) + task = { + "id": tid, + "title": args.title.strip(), + "owner": (args.owner or "").strip(), + "status": (args.status or "todo").strip(), + "started": args.started or datetime.date.today().isoformat(), + } + tasks["tasks"].append(task) + save_tasks(tasks, dry_run=args.dry_run) + if not args.dry_run: + # append a small block to state.md for visibility + state_p = REPO/"project/state.md" + state = read(state_p) or "# Current State\n" + block = f"\n- Task: {tid} — {task['title']} (owner: {task['owner'] or 'MC assigned'}) Status: {task['status']}\n" + state += block + state_p.write_text(state, encoding="utf-8") + git(["add","-A"]); r = git(["commit","-m",f"docs(state): add task {tid}"]) + if r.returncode != 0: + print("git commit failed:", r.stderr, file=sys.stderr) + print(f"Task {tid} added.") + +def cmd_complete_task(args): + tid = args.id.strip() + tasks = load_tasks() + found = False + for t in tasks["tasks"]: + if t.get("id")==tid: + t["status"]="done"; found=True + if not found: + print(f"Task {tid} not found.", file=sys.stderr); sys.exit(1) + save_tasks(tasks, dry_run=args.dry_run) + if not args.dry_run: + git(["add","-A"]); r = git(["commit","-m",f"docs(state): mark task {tid} done"]) + if r.returncode != 0: + print("git commit failed:", r.stderr, file=sys.stderr) + print(f"Task {tid} marked done.") + +def submit_report(args): + tid = args.task.strip() + src = Path(args.report).resolve() + if not src.exists(): + print(f"Report file not found: {src}", file=sys.stderr); sys.exit(1) + + reports_dir = REPO/"reports" + reports_dir.mkdir(parents=True, exist_ok=True) + ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + safe_task = re.sub(r'[^A-Za-z0-9_.-]+','_',tid) + dest = reports_dir / f"{ts}-{safe_task}.md" + if args.dry_run: + print(f"(dry-run) Would copy {src} -> {dest}") + else: + shutil.copyfile(src, dest) + + # Append to Recently Completed; do not auto-remove from In-Flight + state_p = REPO/"project/state.md" + state = read(state_p) + addition = f"- {tid} — report: {dest.as_posix()} ({ts})" + if "## Recently Completed" in state: + new_state = state.replace("## Recently Completed", f"## Recently Completed\n{addition}\n", 1) + else: + new_state = state + f"\n\n## Recently Completed\n{addition}\n" + + if args.dry_run: + print("---- state.md (dry-run snippet) ----") + print(addition) + else: + state_p.write_text(new_state, encoding="utf-8") + git(["add","-A"]); r = git(["commit","-m",f"docs(report): submit report for {tid}"]) + if r.returncode != 0: + print("git commit failed:", r.stderr, file=sys.stderr) + + print(f"Report {'prepared' if args.dry_run else 'stored'} at {dest}") + print("State " + ("previewed" if args.dry_run else "updated and committed") + ".") + +def status(args): + plan = read(REPO/"project/plan.md") + state = read(REPO/"project/state.md") + tasks = load_tasks().get("tasks", []) + print("---- PLAN (top) ----") + print("\n".join(plan.splitlines()[:20])) + print("\n---- STATE (top) ----") + print("\n".join(state.splitlines()[:40])) + print("\n---- TASKS (tasks.yaml) ----") + for t in tasks: + print(f"{t.get('id')}: {t.get('title')} [{t.get('status','')}] owner={t.get('owner','')}") + +def main(): + ap = argparse.ArgumentParser() + sub = ap.add_subparsers(dest="cmd", required=True) + + p_val = sub.add_parser("validate", help="Check repository structure and placeholders") + p_val.set_defaults(func=cmd_validate) + + p_mc = sub.add_parser("start-mc", help="Generate Mission Control startup prompt") + p_mc.set_defaults(func=start_mc) + + p_step = sub.add_parser("start-step", help="Generate Step Agent startup prompt") + p_step.add_argument("--task", required=True, help='Task ID from tasks.yaml') + p_step.set_defaults(func=start_step) + + p_new = sub.add_parser("new-task", help="Add a new task (writes tasks.yaml, appends to state.md)") + p_new.add_argument("--id", required=True) + p_new.add_argument("--title", required=True) + p_new.add_argument("--owner", default="") + p_new.add_argument("--status", default="todo") + p_new.add_argument("--started", default=None) + p_new.add_argument("--dry-run", action="store_true") + p_new.set_defaults(func=cmd_new_task) + + p_done = sub.add_parser("complete-task", help="Mark a task done in tasks.yaml") + p_done.add_argument("--id", required=True) + p_done.add_argument("--dry-run", action="store_true") + p_done.set_defaults(func=cmd_complete_task) + + p_sub = sub.add_parser("submit-report", help="Attach a report and update state.md") + p_sub.add_argument("--task", required=True, help="Task ID") + p_sub.add_argument("--report", required=True, help="Path to markdown report file") + p_sub.add_argument("--dry-run", action="store_true") + p_sub.set_defaults(func=submit_report) + + p_status = sub.add_parser("status", help="Show a quick view of plan/state and tasks") + p_status.set_defaults(func=status) + + args = ap.parse_args() + args.func(args) + +if __name__ == "__main__": + main() +