From 3c5a17c2880ec0ad92db211a7d317c959a40be96 Mon Sep 17 00:00:00 2001 From: jared Date: Fri, 10 Jul 2026 13:27:42 -0400 Subject: [PATCH] os-status: add tracker-configured SessionStart check (refs #11) Reads the `tracker` key from .cc-os/config, validating the planka: | forgejo:/ | github:/ | repo: grammar. Present+valid is silent, malformed is a one-line warn (never a crash), and absent-in-a-git-project is a daily-snoozed nudge pointing at the os-backlog routing skill. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XknQRvihHDpYE47RTmUR4N --- plugins/os-status/hooks/checks.py | 35 ++++++++++++++++ plugins/os-status/tests/hook_test.py | 61 ++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/plugins/os-status/hooks/checks.py b/plugins/os-status/hooks/checks.py index 5259e2b..e86a60c 100644 --- a/plugins/os-status/hooks/checks.py +++ b/plugins/os-status/hooks/checks.py @@ -195,6 +195,40 @@ def orchestration_audit_due(ctx: Ctx) -> CheckResult: ) +TRACKER_ABSENT_NOTE = ( + "No 'tracker' key set in .cc-os/config — this project's issue tracker is" + " unregistered. Run the os-backlog routing skill to pick a tracker and" + " record it (planka: | forgejo:/ |" + " github:/ | repo:)." +) + +_TRACKER_PATTERNS = { + "planka": re.compile(r"^[^/\s]+$"), + "forgejo": re.compile(r"^[^/\s]+/[^/\s]+$"), + "github": re.compile(r"^[^/\s]+/[^/\s]+$"), + "repo": re.compile(r"^\S+$"), +} + + +def tracker_configured(ctx: Ctx) -> CheckResult: + """Read the 'tracker' key from .cc-os/config. Present + valid -> silent + (configured projects cost nothing at session start); present + malformed + -> one-line warn, never a crash; absent -> daily-snoozed nudge toward the + os-backlog routing skill.""" + raw = str(ctx.config.get("tracker", "")).strip() + if not raw: + return warn(TRACKER_ABSENT_NOTE) + scheme, sep, rest = raw.partition(":") + pattern = _TRACKER_PATTERNS.get(scheme) + if not sep or not pattern or not pattern.match(rest): + return warn( + f".cc-os/config sets tracker = '{raw}', which does not match the" + " expected grammar (planka: | forgejo:/ |" + " github:/ | repo:) — fix the value." + ) + return OK + + @dataclass(frozen=True) class Check: name: str @@ -207,4 +241,5 @@ REGISTRY = [ Check("adr-system-present", adr_system_present, project_scoped=True), Check("vault-hub-note-present", vault_hub_note_present, project_scoped=True), Check("orchestration-audit-due", orchestration_audit_due, project_scoped=False), + Check("tracker-configured", tracker_configured, project_scoped=True), ] diff --git a/plugins/os-status/tests/hook_test.py b/plugins/os-status/tests/hook_test.py index 57c4b13..9bbb733 100644 --- a/plugins/os-status/tests/hook_test.py +++ b/plugins/os-status/tests/hook_test.py @@ -22,11 +22,13 @@ from checks import ( # noqa: E402 ABSENT_NOTE, ENV_VAR, PRESENT_NOTE, + TRACKER_ABSENT_NOTE, Check, CheckResult, Ctx, adr_system_present, subagent_model_env_override, + tracker_configured, vault_hub_note_present, ) from state import StateDir, find_project_root, read_config # noqa: E402 @@ -230,6 +232,65 @@ class AuditDueCheckTest(unittest.TestCase): self.assertEqual("ok", checks.orchestration_audit_due(make_ctx(environ=env)).status) +class TrackerCheckTest(unittest.TestCase): + def test_absent_warns_pointing_to_routing_skill(self): + with TemporaryDirectory() as tmp: + root = git_project(tmp) + result = tracker_configured(make_ctx(project_root=root)) + self.assertEqual(CheckResult("warn", TRACKER_ABSENT_NOTE), result) + self.assertIn("os-backlog routing skill", result.message) + + def test_planka_value_is_ok(self): + with TemporaryDirectory() as tmp: + root = git_project(tmp) + ctx = make_ctx(project_root=root, config={"tracker": "planka:my-board"}) + self.assertEqual("ok", tracker_configured(ctx).status) + + def test_forgejo_value_is_ok(self): + with TemporaryDirectory() as tmp: + root = git_project(tmp) + ctx = make_ctx(project_root=root, config={"tracker": "forgejo:jared/cc-os"}) + self.assertEqual("ok", tracker_configured(ctx).status) + + def test_github_value_is_ok(self): + with TemporaryDirectory() as tmp: + root = git_project(tmp) + ctx = make_ctx(project_root=root, config={"tracker": "github:jared/cc-os"}) + self.assertEqual("ok", tracker_configured(ctx).status) + + def test_repo_value_is_ok(self): + with TemporaryDirectory() as tmp: + root = git_project(tmp) + ctx = make_ctx(project_root=root, config={"tracker": "repo:issues/"}) + self.assertEqual("ok", tracker_configured(ctx).status) + + def test_unknown_scheme_warns_without_crashing(self): + with TemporaryDirectory() as tmp: + root = git_project(tmp) + ctx = make_ctx(project_root=root, config={"tracker": "trello:board123"}) + result = tracker_configured(ctx) + self.assertEqual("warn", result.status) + self.assertIn("trello:board123", result.message) + + def test_missing_separator_warns_without_crashing(self): + with TemporaryDirectory() as tmp: + root = git_project(tmp) + ctx = make_ctx(project_root=root, config={"tracker": "planka-my-board"}) + result = tracker_configured(ctx) + self.assertEqual("warn", result.status) + + def test_forgejo_missing_repo_part_warns(self): + with TemporaryDirectory() as tmp: + root = git_project(tmp) + ctx = make_ctx(project_root=root, config={"tracker": "forgejo:jared"}) + result = tracker_configured(ctx) + self.assertEqual("warn", result.status) + + def test_is_project_scoped_and_daily_snoozed_via_registry(self): + entry = next(c for c in checks.REGISTRY if c.name == "tracker-configured") + self.assertTrue(entry.project_scoped) + + class RunnerTest(unittest.TestCase): """run() contract: routing, aggregation, snooze/suppress, isolation."""