77 lines
4.0 KiB
Python
77 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""session_start.py — deterministic SessionStart injection note for os-backlog.
|
|
|
|
Modeled on os-adr's session_start.py (the wording-source-of-record pattern).
|
|
Injects the backlog process rules — WHEN->THEN mechanical triggers only.
|
|
Deliberately contains ZERO issue state and no briefs (notification policy v2:
|
|
pull beats push — issue state is fetched on demand via /os-backlog:list, never
|
|
pushed into a session).
|
|
|
|
Per ADR-0042 (2026-07-16), Planka is retired: git issues (Forgejo via tea,
|
|
GitHub via gh, or in-repo via repo:<path>) are the single tracker for both
|
|
task state and durable specs. State is modeled with labels, not columns —
|
|
there is no card-move, no board, no triage-dispatch hook to mention here.
|
|
|
|
git project -> additionalContext note (rules only, near-zero tokens)
|
|
not a git repo -> silent (process rules only apply inside real projects)
|
|
|
|
Autonomy-label semantics in NOTE are the ADR-0042 contract (successor to
|
|
ADR-029's Planka-mechanics version): hitl never picked up autonomously; semi
|
|
stops at named decision gates and ships to the review label; afk-ready ships
|
|
and, once verified, closes directly.
|
|
|
|
Pure function of the project's filesystem state; no model, no network, no
|
|
Ruby, no tracker call. Always exits 0 — the hook must never block a session.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
NOTE = """\
|
|
[os-backlog] Backlog process rules (rules only — never carries board state; state is pull-only via /os-backlog:list when the user asks).
|
|
- CAPTURE: WHEN deferred work surfaces mid-session that will NOT be done this session -> capture it as an issue via /os-backlog:capture. Never a TODO comment in code, never only a mention in chat.
|
|
- ROUTING: read the tracker key from .cc-os/config (`tracker=forgejo:<owner>/<repo>` | `github:<owner>/<repo>` | `repo:<path>`; `planka:` is retired per ADR-0042 and rejected). WHEN no tracker key is configured in a git project -> suggest /os-backlog:route once; do not nag. A subdirectory's own .cc-os/config overrides the repo root's — umbrella repos route each subproject to its own tracker this way.
|
|
- NEXT IS HUMAN-ONLY: never apply or remove the `next` label on any issue, any project, absent an explicit direct user request naming that issue.
|
|
- WORKING AN ISSUE: comment when work starts. Blocked -> add `waiting` + a comment naming the blocker; remove `waiting` on resume. Shipping: `semi` -> add `review` label + a summary comment, stays open for sign-off; `afk-ready` -> close directly once verified, with a comment linking the evidence.
|
|
- AUTONOMY LABELS: `hitl` -> human-owned, never picked up autonomously. `semi` -> run the mechanical parts, stop at named decision gates. `afk-ready` -> run to completion and verify before closing.
|
|
- RECURRING ISSUES: never closed by the AI — comment each occurrence's outcome and leave it open.
|
|
- CROSS-PROJECT: WHEN a needed change belongs to a DIFFERENT project THEN do not edit it — query `os-backlog projects`, file an issue on that project's tracker using the Discoverer template (see os-backlog route references/cross-project-filing.md); no autonomy/priority labels applied by the filer.
|
|
"""
|
|
|
|
|
|
def find_project_root(cwd: Path) -> Optional[Path]:
|
|
"""Nearest ancestor (including cwd) containing .git, else None."""
|
|
for candidate in [cwd, *cwd.parents]:
|
|
if (candidate / ".git").exists():
|
|
return candidate
|
|
return None
|
|
|
|
|
|
def run(root: Optional[Path], out) -> str:
|
|
"""Pure decision: ('context'|'silent'). Emits hook JSON when in a project."""
|
|
if root is None:
|
|
return "silent"
|
|
out.write(json.dumps({
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "SessionStart",
|
|
"additionalContext": NOTE,
|
|
}
|
|
}))
|
|
return "context"
|
|
|
|
|
|
def main() -> int:
|
|
try:
|
|
run(find_project_root(Path.cwd()), sys.stdout)
|
|
except Exception:
|
|
pass # never block a session
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|