70 lines
3.7 KiB
Python
70 lines
3.7 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 board state and no briefs (notification policy v2:
|
||
|
|
pull beats push — board state is fetched on demand via /os-backlog:list, never
|
||
|
|
pushed into a session).
|
||
|
|
|
||
|
|
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-029 contract (which supersedes
|
||
|
|
the wording in issue #16's body): hitl never picked up autonomously; semi
|
||
|
|
stops at Review for human sign-off; afk-ready goes straight to Done after
|
||
|
|
verification, skipping Review.
|
||
|
|
|
||
|
|
Pure function of the project's filesystem state; no model, no network, no
|
||
|
|
Ruby, no Planka 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 — this note never carries board state; board state is pull-only via /os-backlog:list when the user asks).
|
||
|
|
- CAPTURE: WHEN a concrete follow-up task, bug, or piece of deferred work surfaces mid-session that will NOT be done this session -> capture it as a Backlog card via /os-backlog:capture. Never a TODO comment in code, never only a mention in chat.
|
||
|
|
- ROUTING: WHEN process/backlog/spec-tracking work surfaces -> read the tracker key from .cc-os/config (`tracker=planka:<board>` | `forgejo:<owner>/<repo>` | `github:<owner>/<repo>` | `repo:<path>`) and send work where it says. WHEN this is a git project and no tracker key is configured -> suggest /os-backlog:route once; do not nag.
|
||
|
|
- PROMOTION: WHEN a Planka-tracked effort accretes significant code or design decisions in card comments -> flag promotion to the user (never silently): carve out a repo, move the durable spec into git issues via /to-issues, and keep the Planka card as a pointer (title + link). Never duplicate spec text into a card description.
|
||
|
|
- COLUMN OWNERSHIP (CLI-enforced per ADR-029; a violating card-move fails with the rule named): create cards at Backlog only. Never move a card into or out of Next — Next is human-curated in both directions. Never move a card labeled hitl; never move a card out of Done. Working a card MEANS moving it via card-move: Doing when work starts, Waiting + a blocker comment when blocked, and on shipping: Review (semi) or Done (afk-ready, only after verification).
|
||
|
|
- AUTONOMY LABELS (ADR-029): hitl -> human-owned; never picked up autonomously. semi -> run the mechanical parts, stop at named decision gates; shipped work goes to Review for human sign-off. afk-ready -> shipped + verified goes straight to Done, skipping Review.
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
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())
|