23 Commits
Author SHA1 Message Date
robbond 9f6933423b docs: add discovery gap analysis model 2026-06-04 12:48:46 +01:00
robbond 65dda68dbe feat(task-023): improve generated context quality 2026-06-04 12:11:48 +01:00
robbond 152096d433 feat(task-023): improve generated context quality 2026-06-04 09:42:05 +01:00
robbond c72ad6995e feat(task-022): expand discovery coverage 2026-06-04 07:42:48 +01:00
robbond a0b28e7d8d feat(task-021): add context health reporting 2026-06-03 19:05:04 +01:00
robbond 5bad6ad0f8 feat(task-020): generate context from discovery answers 2026-06-03 18:19:25 +01:00
robbond 250cf9316a feat(task-019): define discovery-to-context mappings 2026-06-02 18:36:42 +01:00
robbond 1c2faa3162 feat(task-018): add role-based context selection 2026-06-02 18:09:11 +01:00
robbond 34f7364c86 feat: support role-based agent prompts 2026-06-02 17:56:38 +01:00
robbond 3ccdea330a feat: add standard context file templates 2026-06-02 17:18:35 +01:00
robbond 13f5e10f6f doc: reafirmed project goals and direction 2026-06-02 17:07:03 +01:00
robbond 6eef97582d feat: use telemetry in guardrails 2026-06-02 16:44:36 +01:00
robbond 2fcc822394 feat: record CLI command telemetry 2026-06-02 15:49:27 +01:00
robbond eb30450f34 feat: add telemetry foundation 2026-06-02 14:21:17 +01:00
robbond 8222660a3c doc:updated tasks 2026-06-02 13:43:58 +01:00
robbond 8b96d2c758 doc:updated backlog 2026-06-02 12:52:16 +01:00
robbond 7a1d36c03c feat: improve generated agent prompts 2026-06-02 12:49:55 +01:00
robbond 318b9471ae docs: mark TASK-012 add todo status 2026-06-02 11:44:07 +01:00
robbond aee13b045e chore: mark TASK-006 complete 2026-06-02 11:41:27 +01:00
robbond 21611b7c4e feat(TASK-005): wire project state and agent handoff into start/complete commands
- Add update_project_state() helper: updates Current Task + Last Updated
  in PROJECT_STATE.md via regex sub, preserving all surrounding formatting.
- Add update_agent_handoff() helper: updates Current Stage + Current Task
  sections in AGENT_HANDOFF.md, auto-detected from project_stage().
- Wire both helpers into rdb start TASK-ID and rdb complete TASK-ID CLI
  commands so task lifecycle transitions propagate to all control files.
- ADDING test_status.py with 5 tests: update_project_state task/timestamp
  updates, missing-file false return, agent handoff update and miss.

Acceptance criteria met:
- rdb start/complete now update PROJECT_STATE.md
- rdb start/complete now update AGENT_HANDOFF.md
- RUN_LOG.md already updated (pre-existing)
- Task formatting preserved (regex sub targets single line only)
2026-06-02 10:37:38 +01:00
robbond 9536f5d0ff chore: mark TASK-004 complete 2026-06-02 10:06:11 +01:00
robbond 5456947ac9 docs: document Claude test commands 2026-06-02 09:59:21 +01:00
robbond 39728667a7 feat: add read_discovery_answers, append_followup_answer, and ask-more command
- Add read_discovery_answers() to parse discovery-log.md rows into dicts
  with low_confidence and needs_followup boolean flags.
- Add append_followup_answer() to append follow-up rows linked to original Q-A IDs.
- Add 'ask-more' CLI command that detects Low-confidence and Follow-up-needed
  answers, prompts for additional details, and records them in the log.
- Add 6 tests: read_discovery_answers round-trip, empty-log handling,
  low-confidence detection, follow-up flag detection, and append_followup.
2026-06-02 09:57:02 +01:00
31 changed files with 4941 additions and 70 deletions
+154
View File
@@ -0,0 +1,154 @@
{"timestamp": "2026-06-02T14:38:58.532898+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T14:43:05.281782+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T14:47:33.841698+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T14:49:48.692247+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T14:49:51.767548+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-02T14:49:58.009037+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
{"timestamp": "2026-06-02T15:21:45.848114+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T15:40:37.716111+00:00", "event_type": "command", "target": "rdb guardrails", "details": {}}
{"timestamp": "2026-06-02T15:42:28.735379+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T15:44:50.746110+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T15:44:50.931122+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-02T15:58:40.572784+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T15:58:44.609967+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-02T15:58:49.677959+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
{"timestamp": "2026-06-02T16:01:01.182387+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T16:05:14.118862+00:00", "event_type": "command", "target": "rdb complete", "details": {"task_id": "TASK-015"}}
{"timestamp": "2026-06-02T16:05:53.807097+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T16:07:10.365159+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-02T16:07:20.003871+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
{"timestamp": "2026-06-02T16:13:04.692285+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T16:13:54.568414+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T16:15:17.596114+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T16:19:12.880619+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T16:19:17.322970+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-02T16:25:21.115719+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T16:25:26.933534+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-02T16:25:36.969679+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-02T16:25:40.245143+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T16:25:42.679647+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-02T16:25:52.729189+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
{"timestamp": "2026-06-02T16:27:38.835967+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T16:27:46.509757+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-02T16:28:22.926960+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T16:28:27.925682+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-02T16:29:40.982618+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T16:29:43.405985+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-02T16:29:59.879010+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
{"timestamp": "2026-06-02T16:47:40.958013+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T16:57:02.286495+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T16:57:05.774698+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-02T16:57:29.853052+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
{"timestamp": "2026-06-02T17:05:41.754949+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T17:07:48.597850+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T17:10:43.959746+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T17:10:48.601943+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-02T17:10:57.679687+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
{"timestamp": "2026-06-02T17:22:38.052771+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-02T17:35:19.867986+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-03T15:52:18.064218+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-03T15:52:21.772271+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-03T15:59:27.187091+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
{"timestamp": "2026-06-03T17:06:45.302298+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-03T17:08:49.404465+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-03T17:12:09.213759+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-03T17:13:19.590940+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-03T17:15:34.075091+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-03T17:20:37.309119+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-03T17:20:41.625422+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-03T17:20:45.764811+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
{"timestamp": "2026-06-03T17:28:07.458337+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:30:17.726863+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:31:32.041267+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:32:48.717496+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:36:14.760622+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:36:14.783409+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:48:59.085912+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:48:59.111197+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:48:59.174102+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:51:06.935561+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:51:06.956310+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:51:06.984767+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:52:21.749548+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:52:21.770919+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:52:21.809957+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:52:21.950628+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-03T17:56:58.542853+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:57:26.778515+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-03T17:58:40.502126+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:58:40.531803+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:58:40.575979+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T17:58:40.679715+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-03T17:59:54.816018+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-03T18:02:22.183984+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-04T05:14:08.600178+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-04T05:14:24.061826+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-04T05:14:38.336849+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-04T05:14:49.894695+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
{"timestamp": "2026-06-04T05:19:45.385984+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T05:19:45.410121+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T05:19:45.438892+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T05:19:45.530897+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-04T05:21:43.139977+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-04T05:39:05.966198+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T05:39:05.991663+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T05:39:06.022398+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T05:39:06.155411+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-04T05:51:40.975066+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T05:51:41.000251+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T05:51:41.030414+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T05:51:41.111721+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-04T06:06:03.436361+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-04T06:27:22.527128+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T06:27:22.551624+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T06:27:22.585367+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T06:27:22.683899+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-04T06:33:49.312031+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T06:33:49.331790+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T06:33:49.361065+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T06:33:49.443458+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-04T06:38:18.966356+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T06:38:18.991294+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T06:38:19.019668+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T06:38:19.108828+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-04T06:38:19.717117+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-04T06:42:57.711062+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-04T06:43:06.047568+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
{"timestamp": "2026-06-04T06:44:15.981811+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-04T06:46:32.334206+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T06:46:32.354546+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T06:46:32.384086+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T06:46:32.469898+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-04T07:55:17.697713+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-04T08:04:05.989316+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-04T08:05:02.054038+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T08:05:02.078565+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T08:05:02.105459+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T08:05:02.195107+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-04T08:07:55.244668+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-04T08:12:44.332202+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T08:12:44.356488+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T08:12:44.386225+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T08:12:44.484757+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-04T08:24:16.640627+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-04T08:41:45.929837+00:00", "event_type": "command", "target": "rdb complete", "details": {"task_id": "TASK-023"}}
{"timestamp": "2026-06-04T08:45:33.415489+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-04T08:46:14.771099+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
{"timestamp": "2026-06-04T09:15:26.855951+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-04T09:16:03.623379+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T09:16:03.649310+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T09:16:03.678552+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T09:16:03.768573+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-04T10:58:23.669753+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T10:58:23.696327+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T10:58:23.726939+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T10:58:23.821232+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
{"timestamp": "2026-06-04T11:15:13.529302+00:00", "event_type": "command", "target": "rdb status", "details": {}}
{"timestamp": "2026-06-04T11:15:20.344313+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-04T11:23:06.251069+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-04T11:23:20.288400+00:00", "event_type": "command", "target": "rdb next", "details": {}}
{"timestamp": "2026-06-04T11:23:25.065294+00:00", "event_type": "command", "target": "rdb prompt", "details": {}}
{"timestamp": "2026-06-04T11:44:23.218539+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T11:44:23.245046+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T11:44:23.277672+00:00", "event_type": "command", "target": "rdb context-status", "details": {}}
{"timestamp": "2026-06-04T11:44:23.373376+00:00", "event_type": "command", "target": "rdb generate", "details": {}}
+2 -2
View File
@@ -2,11 +2,11 @@
## Current Stage ## Current Stage
BUILDING TASKS_READY
## Current Task ## Current Task
TASK-002 — Run initial project discovery TASK-023
## Instructions For Agent ## Instructions For Agent
+31
View File
@@ -18,3 +18,34 @@ Rules:
- Stop. - Stop.
Do not start the next task automatically. Do not start the next task automatically.
## Test Commands
Use the existing virtual environment.
From the repository root, run:
```bash
source .venv/bin/activate
python -m pytest
```
Do not search the filesystem for pytest.
Do not create a new virtual environment unless explicitly asked.
If pytest is unavailable, run:
```bash
pip install -e '.[dev]'
python -m pytest
```
## Validation Commands
For normal task validation, run:
```bash
python -m pytest
rdb status
rdb next
```
+2 -2
View File
@@ -4,7 +4,7 @@ Current Stage: BUILDING
Previous Stage: BOOTSTRAP_READY Previous Stage: BOOTSTRAP_READY
Next Stage: REVIEW_READY Next Stage: REVIEW_READY
Current Task: TASK-004 Current Task: TASK-025
Active Branch: main Active Branch: main
Last Updated: 2026-06-02 Last Updated: 2026-06-04
+31 -13
View File
@@ -1,8 +1,10 @@
# rdb-discovery # rdb-discovery
A small CLI-first tool for repeatable software project discovery and delivery. A small CLI-first tool that helps you generate and maintain useful project context files for AI-assisted development.
It creates markdown files that help humans and AI coding agents understand: ## Purpose
rdb-discovery exists to make it easier for humans and AI coding agents to understand:
- what is being built - what is being built
- why it exists - why it exists
@@ -10,6 +12,20 @@ It creates markdown files that help humans and AI coding agents understand:
- what task should be done next - what task should be done next
- what stage the project is currently in - what stage the project is currently in
The tool creates and manages a set of markdown files — your project's source-of-truth context.
## Core workflow
1. **Ask discovery questions**`rdb discover` captures answers about the product, architecture, risks, and open questions.
2. **Capture answers** — Structured responses are stored in `context/discovery-log.md` alongside confidence scores and follow-ups.
3. **Generate context files**`rdb init` creates standardised project files (`product-brief.md`, `architecture.md`, `decisions.md`, etc.).
4. **Support agent implementation**`rdb prompt` produces ready-to-paste implementation prompts for Claude Code or Cline, and `rdb start` / `rdb complete` manage task lifecycles.
## Support features (not the product)
- **Telemetry** records structured agent activity events in `.rdb/session-log.jsonl`. It enables future analysis but is not the main offering.
- **Guardrails** detect agent stalls, repeated reads, and inconsistent state to keep projects on track. They support the workflow but are secondary.
## Install for local development ## Install for local development
```bash ```bash
@@ -21,23 +37,25 @@ pip install -e '.[dev]'
## Commands ## Commands
```bash ```bash
rdb init rdb init # Create standard project structure and context files
rdb discover rdb discover # Ask 10 core discovery questions, record answers
rdb status rdb guardrails # Check for agent stalls and state inconsistencies
rdb next rdb prompt # Generate implementation prompt for the next task
rdb start TASK-001 rdb context-status # Report health and completeness of project context
rdb complete TASK-001 rdb start TASK-001 # Mark a task as in progress
rdb handoff rdb complete TASK-001# Mark a task as done
rdb status # Show current stage and active task
rdb next # Print the next task to work on
rdb handoff # Generate agent handoff notes
``` ```
## Bootstrap workflow ## Bootstrap workflow
1. Run `rdb init` 1. Run `rdb init`
2. Run `rdb discover` 2. Run `rdb discover`
3. Run `rdb status` 3. Review `rdb status` and `rdb next`
4. Run `rdb next` 4. Give the next prompt to Claude Code or Cline via `rdb prompt`
5. Give the next task to Claude Code or Cline 5. Commit after each completed task
6. Commit after each completed task
## Project structure ## Project structure
+6
View File
@@ -8,3 +8,9 @@ TASK-001 marked Done — rdb init creates all project control files.
2026-06-02 2026-06-02
Added TASK-002 for agent prompt generation. Added TASK-002 for agent prompt generation.
TASK-002 marked Done — agent prompt generation complete. TASK-002 marked Done — agent prompt generation complete.
| 2026-06-02T10:01:31 | Task completed | TASK-004 | 28 tests passed |
| 2026-06-02T10:02:18 | Task completed | TASK-004 | rdb ask-more implemented and tested |
| 2026-06-02T12:51:20 | Task completed | TASK-007 | enhanced prompts |
| 2026-06-02T13:45:00 | Task completed | TASK-012 | telemetry foundation — writer, JSONL storage, event reader, 12 tests passed |
| 2026-06-02T17:05:41 | Task completed | TASK-015 | updated README.md |
| 2026-06-04T09:41:51 | Task completed | TASK-023 | updated |
+717 -1
View File
@@ -63,7 +63,7 @@ Acceptance Criteria:
## TASK-004 — Implement ask-more command ## TASK-004 — Implement ask-more command
Status: Todo Status: Done
Goal: Add a command that finds weak answers and asks deeper follow-up questions. Goal: Add a command that finds weak answers and asks deeper follow-up questions.
@@ -72,3 +72,719 @@ Acceptance Criteria:
- command reads discovery-log.md - command reads discovery-log.md
- low-confidence answers are detected - low-confidence answers are detected
- follow-up answers are appended to discovery-log.md - follow-up answers are appended to discovery-log.md
## TASK-005 — Task lifecycle commands
Status: Done
Goal:
Allow tasks to be managed from the CLI rather than manually editing TASKS.md.
Acceptance Criteria:
- `rdb start TASK-ID`
- Marks task In Progress
- `rdb complete TASK-ID`
- Marks task Done
- Updates PROJECT_STATE.md
- Updates AGENT_HANDOFF.md
- Updates RUN_LOG.md
- Preserves task formatting
- Add/update tests
- Run python -m pytest
## TASK-006 — Agent execution guardrails
Status: Done
Goal:
Detect agent stalls, repeated reads, long reasoning loops, and non-progressing execution.
Acceptance Criteria:
- `rdb guardrails` command exists ✓
- Detects repeated file reads via AGENT_HANDOFF.md analysis ✓
- Detects repeated command entries in RUN_LOG.md ✓
- Flags project files not modified in >48 hours ✓
- Flags missing test run records in RUN_LOG.md ✓
- Checks TASKS.md ↔ RUN_LOG.md consistency ✓
- Produces clear human-readable report with overall status ✓
- All existing tests still pass (33 → 51) ✓
- Guardrail-specific tests added (18 new tests) ✓
## TASK-007 — Improve generated agent prompts
Status: Done
Goal:
Make `rdb prompt` produce smaller, more direct prompts for Claude Code/local LLM agents.
Acceptance Criteria:
- Prompt includes exact known implementation gap when available
- Prompt includes existing test command from CLAUDE.md
- Prompt tells agent not to repeatedly reread unchanged files
- Prompt tells agent to inspect first, then edit
- Prompt limits scope to one small implementation step
- Add/update tests
## TASK-012 — Telemetry foundation
Status: Done
Goal:
Create a minimal telemetry system that can record structured agent activity for future guardrail and analysis features.
Implementation Gap:
The project currently infers agent behaviour from documentation, task files, and run logs.
There is no structured event log showing what actions an agent actually performed during a session.
Acceptance Criteria:
- Create a telemetry module
- Create `.rdb/session-log.jsonl` automatically when recording an event
- Support recording events in JSONL format
- Each event contains:
- timestamp
- event_type
- target
- details
- Provide a simple public function for writing telemetry events
- Add/update tests
Example Event:
```json
{
"timestamp": "2026-06-02T12:00:00Z",
"event_type": "command",
"target": "rdb prompt",
"details": {}
}
```
Constraints:
- Do not integrate telemetry into existing commands yet
- Do not modify guardrails yet
- Do not implement dashboards or reporting
- Build the smallest useful telemetry foundation only
Definition of Done:
- Telemetry writer exists
- JSONL file is created correctly
- Events append correctly
- Tests pass
- Documentation updated if required
## TASK-013 — Record CLI command execution
Status: Done
Goal:
Record rdb CLI command execution using the telemetry system.
Implementation Gap:
Telemetry storage exists but no command activity is recorded.
Acceptance Criteria:
- Record command execution events
- Include command name
- Include timestamp
- Add/update tests
Definition of Done:
- All 10 CLI commands record a telemetry event on invocation
- Events have event_type "command" and target "rdb <cmd_name>"
- Timestamps are present in UTC ISO format
- New integration tests added to test_telemetry.py (4 new tests)
- All 75 tests pass
## TASK-014 — Integrate telemetry with guardrails
Status: Done
Goal:
Use structured telemetry data in guardrail analysis.
Implementation Gap:
Guardrails currently rely on heuristics and markdown files rather than actual activity records.
Acceptance Criteria:
- Read telemetry events ✓
- Detect repeated commands ✓
- Detect repeated reads when available ✓
- Fall back gracefully when telemetry is absent ✓
- Add/update tests ✓
Result:
Two new guardrail checks added:
- `check_repeated_commands_telemetry` — uses session-log.jsonl to detect repeated CLI commands (>3x)
- `check_repeated_reads_telemetry` — uses session-log.jsonl to detect repeated file reads (>3x)
Both integrate into `run_all_guardrails` alongside existing heuristic checks.
When telemetry data is absent, both return `"ok"` with an informative fallback message instead of failing.
## TASK-015 — Re-centre project purpose
Status: Done
Goal:
Clarify that rdb-discovery exists to help generate and maintain useful project context files for AI-assisted development.
Implementation Gap:
The project now has task, prompt, guardrail, and telemetry features, but the core product purpose needs to be made explicit again.
Acceptance Criteria:
- Update README.md with a clear project purpose
- Explain the core workflow:
- ask discovery questions
- capture answers
- generate context files
- support agent implementation
- Clarify that telemetry and guardrails support the workflow but are not the main product
- Add/update tests only if required
## TASK-016 — Define standard context file templates
Status: Done
Goal:
Define the standard context files that rdb-discovery should help generate.
Implementation Gap:
There is not yet a clear built-in definition of the context files the tool should produce.
Acceptance Criteria:
- Define templates for:
- company-context.md
- development-context.md
- infrastructure-context.md
- agent-guidelines.md
- project-brief.md
- architecture.md
- Templates include headings and placeholder guidance
- Keep templates simple markdown
- Add/update tests
## TASK-017 — Support task roles in generated prompts
Status: Done
Goal:
Allow tasks to define the agent role used by `rdb prompt`.
Implementation Gap:
Tasks can now include a `Role:` field, but `rdb prompt` still always generates prompts beginning with `You are an implementation agent.`
Acceptance Criteria:
- Parse optional `Role:` field from task markdown
- If `Role:` exists, use it in the generated prompt opening
- If `Role:` is missing, default to `Implementation Agent`
- Add/update tests
- Do not change task execution behaviour
Definition of Done:
- `Role: Architecture Agent` generates `You are an architecture agent.`
- Tasks without a role still generate implementation prompts
- Tests pass
## TASK-018 — Role-based context selection
Status: Done
Role: Implementation Agent
Goal:
Allow `rdb prompt` to select different context files based on task role.
Implementation Gap:
Tasks can now define a role, but every generated prompt still asks the agent to read the same set of files regardless of task type.
Acceptance Criteria:
- Architecture Agent receives architecture-focused context
- Implementation Agent receives implementation-focused context
- Documentation Agent receives documentation-focused context
- If no role exists, use current default file list
- Add/update tests
Definition of Done:
- Context files differ by role
- Existing prompts remain backward compatible
- Tests pass
## TASK-019 — Define discovery-to-context mappings
Status: Done
Role: Architecture Agent
Goal:
Define how discovery answers should be transformed into project context files.
Implementation Gap:
Discovery answers are collected and stored, but there is no documented mapping between discovery questions and the context files they should populate.
Acceptance Criteria:
- Every discovery question maps to one or more context files
- Every discovery question maps to a specific section within those files
- Mapping is documented in markdown
- Mapping is understandable by future agents
- No context generation implementation yet
- Add/update tests if required
Definition of Done:
- Mapping document exists
- Mapping covers all discovery questions
- Future implementation work is clearly defined
Result
Created `context/discovery-context-mapping.md` which documents:
- Reference table of all 10 core discovery questions (Q-001 through Q-010)
- Reference table of all target context files and their purpose
- Detailed mapping for each question to primary and secondary context files with specific section guidance
- Summary question-to-file matrix for quick reference
- Implementation notes defining how future code should read discovery answers and populate context files
- Constraints for future implementation (preserve existing content, skip low-confidence answers)
- Test requirements for when code is eventually written
Added `tests/test_discovery_mapping.py` with 6 tests validating:
- Mapping file existence
- All 10 core questions are present
- Table-format documentation section exists
- Summary matrix section exists
- Future implementation notes exist (and clarify no code has been implemented yet)
- All context files from templates.py are referenced in the mapping
No context generation implementation was added — this task is a planning artifact only.
## TASK-020 — Generate context files from discovery mappings
Status: Done
Role: Implementation Agent
Goal:
Generate context file content using the approved
discovery-context-mapping.md document.
Implementation Gap:
Mappings now exist, but discovery answers are not yet transformed into context file content.
Acceptance Criteria:
- Read discovery-context-mapping.md ✓
- Read discovery-log.md ✓
- Populate mapped sections in context files ✓
- Create missing context files safely ✓
- Do not overwrite existing content ✓
- Skip low-confidence answers ✓
- Add/update tests ✓
Definition of Done:
- Discovery answers appear in the correct context files ✓
- Existing content is preserved ✓
- Tests pass ✓ (114 tests, all passing)
Result
Created `src/rdb_discovery/generate_context.py` module with:
- `CONTEXT_MAP`: Rules mapping each of the 10 discovery questions to target context files and sections (body-fill, table-row, or append-new-section strategies)
- `generate_context_files(root, min_confidence)`: Main entry point that reads discovery answers, filters by confidence, applies mapping rules, and writes/updates context files safely
- Three write strategies: TBD-replacement for empty sections, content-appending for existing body-text sections, table-row insertion for risks.md and assumptions.md, and new-section appending when headers don't exist yet
Added `rdb generate` CLI command (accepts `--min-confidence` option).
Fixed a parsing bug in `discovery.py`: escaped pipe characters (`\|`) in discovery answers were creating spurious extra columns during markdown table splitting — now handled with placeholder-based escaping.
Added `tests/test_generate_context.py` with 15 tests:
- Mapping completeness (all 10 questions, all target files)
- Confidence filtering (Low → skipped, High/Medium → generated)
- Body text filling (TBD replacement, existing content append)
- Table row generation (risks.md and assumptions.md formats)
- Content preservation verification
- CLI command availability and error handling
- Missing file creation safety
- End-to-end integration flow
## TASK-021 — Context health report
Status: Done
Role: Implementation Agent
Goal:
Provide a single command that reports the health and completeness of project context.
Implementation Gap:
Context files can now be generated, but there is no way to assess whether sufficient context exists for effective AI-assisted development.
Acceptance Criteria:
- Add `rdb context-status`
- Report expected context files ✓
- Report missing context files ✓
- Report sections still containing TBD placeholders ✓
- Report low-confidence discovery answers ✓
- Display an overall health score ✓
- Add/update tests ✓ (26 tests)
Definition of Done:
- Command runs successfully ✓
- Missing context is reported clearly ✓
- Health score is displayed ✓
- Tests pass ✓ (140 total, all passing)
Result
Created `src/rdb_discovery/context_status.py` module with:
- `_expected_files()` — returns the 18 expected context file paths
- `_check_expected_files()` — checks each file for existence and size
- `_check_tbd_sections()` — scans context files for TBD/TDB placeholders (skips blank lines)
- `_check_low_confidence()` — reads discovery-log.md for low-confidence answers
- `compute_health_score()` — 0-100 score with weighted breakdown (45 pts file completeness, 30 pts no TBDs, 15 pts no low-conf, 10 pts discovery data)
- `context_status()` — orchestrates all checks and returns structured report
Added `rdb context-status` CLI command with:
- Rich table of expected files with presence/absence indicators
- TBD placeholder listing with file, section, and line number
- Low-confidence discovery answer listing with ID and confidence level
- Color-coded health score (green ≥ 70, yellow ≥ 40, red < 40)
- Summary line showing counts
Added `tests/test_context_status.py` with 26 tests across 4 classes:
- TestExpectedFiles — file existence detection
- TestTbdDetection — TBD/TDB placeholder scanning
- TestLowConfidence — low-confidence answer detection
- TestHealthScore — score computation and degradation
- TestContextStatus — structured report verification
- TestCLICommand — CLI registration, output, and edge cases
- TestHealthScoreColor — score boundary validation
All 140 tests pass.
## TASK-022 — Expand discovery coverage
Status: Done
Role: Architecture Agent
Goal:
Collect enough information to populate all standard context files.
Implementation Gap:
Several context files remain mostly placeholders because discovery questions do not collect the information required to populate them.
Acceptance Criteria:
- Review all context templates ✓
- Identify unmapped sections ✓
- Add additional discovery questions where required ✓
- Update discovery-to-context mapping ✓
- Add/update tests ✓
Definition of Done:
- Every major template section has a discovery source ✓
- Discovery-to-context mapping updated ✓
- Tests pass ✓ (140, all passing)
Result
Added 4 new grouped discovery questions (Q-011 through Q-014) covering all major unmapped sections:
| Question ID | Category | Target Sections |
| ----------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Q-011 | Organisation/project ownership | company-context.md Mission, project-brief.md Target Audience |
| Q-012 | Technology stack and repository structure | development-context.md Tech Stack, Coding Standards; architecture.md Overview, Core Components |
| Q-013 | Infrastructure/deployment/security | infrastructure-context.md Hosting, Environments, Monitoring & Alerting, Security |
| Q-014 | Agent/developer workflow | agent-guidelines.md Purpose and Preferred Tools; repository-context.md Purpose and Contributing |
Coverage expanded from 16 section targets (10 questions) to 28 section targets (14 questions).
Files modified:
- `src/rdb_discovery/templates.py` — Added 4 new questions to CORE_QUESTIONS
- `src/rdb_discovery/generate_context.py` — Added Q-011 through Q-014 mappings; updated all_questions list in \_write_table_row
- `context/discovery-context-mapping.md` — Added 4 new question entries, detailed mapping tables, and summary matrix rows
- `tests/test_generate_context.py` — Updated question count assertions to use dynamic CORE_QUESTIONS
- `tests/test_discovery_mapping.py` — Renamed test to match dynamic question count
- `tests/test_discovery.py` — Updated assertion from exact 10 to >= 10
## TASK-023 — Improve generated context quality
Status: Done
Role: Implementation Agent
Goal:
Improve the quality and usefulness of generated context files after `rdb generate`.
Implementation Gap:
`rdb generate` now writes discovery answers into some context files, but several useful context files remain mostly placeholders and some answers are mapped to weak or incorrect sections.
Acceptance Criteria:
- Populate `company-context.md` when discovery answers include users, product purpose, or stakeholders
- Populate `agent-guidelines.md` when answers include tools, constraints, risks, or testing preferences
- Populate `repository-context.md` when answers include project purpose, dependencies, or contribution/testing approach
- Do not map testing answers into timeline/milestone sections
- Fix `TDB` placeholder typos to `TBD`
- Preserve existing non-placeholder content
- Add/update regression tests using the current sample discovery-log data
Definition of Done:
- Running `rdb generate` on the sample discovery log produces useful content in more than just product/project brief files
- Context files contain fewer irrelevant placeholders
- Tests pass
## TASK-024 — Add objectivity and efficiency guidelines
Status: Done
Role: Documentation Agent
Goal:
Reduce token waste and unnecessary agreement in agent interactions.
Implementation Gap:
Agent guidance does not yet explicitly discourage praise, repeated context reading, task restatement, or unnecessary narrative output.
Acceptance Criteria:
- Update `context/agent-guidelines.md`
- Add objectivity guidance
- Add context-discipline guidance
- Add communication-efficiency guidance
- Add execution-efficiency guidance
- Keep guidance concise and role-neutral
- Add/update tests only if required
Definition of Done:
- `context/agent-guidelines.md` includes objectivity and efficiency guidance
- Guidance helps reduce token usage without preventing useful technical reasoning
- Existing workflow remains unchanged
## TASK-025 — Define discovery gap analysis model
Status: Done
Role: Architecture Agent
Goal:
Define how missing or weak project context should be detected and converted into targeted follow-up discovery questions.
Acceptance Criteria:
- Define gap detection rules
- Define gap severity levels
- Define question generation rules
- Define question prioritisation rules
- Define mapping between missing context sections and follow-up questions
- Document the model
- Define the follow-on implementation task
- No implementation
Definition of Done:
- Gap analysis model documented
- Follow-up question model documented
- Example gap-to-question mappings included
- TASK-026 implementation task defined
Result
Created `context/gap-analysis-model.md` (320 lines) as a planning artifact documenting:
1. **Gap detection rules** — 4 rule types (GAP-01 through GAP-04): TBD placeholders, unmapped sections, partial content, near-empty files
2. **Severity levels** — S1 (Critical), S2 (Important), S3 (Low) with assignment criteria per stage and mapping status
3. **Question generation rules** — 6 rules (QG-01 through QG-06): one-gap-per-question, answerable format, section reference, table-column specificity, skip-filled sections, owner-answer alignment
4. **Question prioritisation** — Ordered by severity → file completeness → dependency order → question scope
5. **Gap-to-question mapping table** — 40+ section-level mappings across all 16 template context files (excludes CLI-managed project-level files)
6. **End-to-end example flow** — Step-by-step walkthrough from detection through prioritisation to follow-up generation to regeneration
7. **Edge cases** — Partial sections, acceptable TBDs, new templates, multi-question sections, heading-only files
8. **TASK-026 implementation scope** — CLI command spec (`rdb gap`), module structure, test requirements (~45 tests across 6 categories)
## TASK-026 — Implement discovery gap analysis
Status: Todo
Role: Implementation Agent
Goal:
Implement the approved discovery gap analysis model.
Acceptance Criteria:
- Analyse generated context files
- Detect missing sections
- Generate follow-up questions
- Link questions to missing context sections
- Add/update tests
## TASK-027 — Context-driven follow-up questions
Status: Todo
Role: Implementation Agent
Goal:
Generate follow-up discovery questions based on previous answers.
Implementation Gap:
Discovery questions are currently static and do not adapt based on project-specific answers.
Acceptance Criteria:
- Follow-up questions are generated from discovery answers
- Different answers produce different follow-up questions
- Rules are deterministic and testable
- Add/update tests
Definition of Done:
- Discovery flow becomes adaptive
- Follow-up questions are linked to discovery answers
- Tests pass
## TASK-028 — Improve discovery answer reliability UX
Status: Todo
Role: Architecture Agent
Goal:
Make answer reliability easier for users to understand during discovery.
Implementation Gap:
Users are asked to provide a confidence level, but confidence is an internal implementation concept and may not reflect how users think about the reliability of information.
Acceptance Criteria:
- Review confidence collection workflow
- Define a more intuitive reliability model
- Maintain compatibility with existing confidence filtering
- Update discovery-to-context documentation
- Add/update tests if required
Definition of Done:
- Reliability terminology is documented
- Mapping to internal confidence levels is defined
- Future implementation work is clearly specified
## TASK-029 — Capture AI tool targets
Status: Todo
Role: Architecture Agent
Goal:
Allow discovery to capture which AI development tools will consume generated project context.
Implementation Gap:
Generated context is currently tool-agnostic, but different tools require different guidance files and configuration formats.
Acceptance Criteria:
- Define supported tool categories:
- Claude Code
- Cline
- Cursor
- ChatGPT
- GitHub Copilot
- Local LLMs
- Other
- Add discovery question for AI tool usage
- Update discovery-to-context mapping
- Define which generated files are generic
- Define which generated files are tool-specific
- Do not implement tool-specific generation yet
Definition of Done:
- Tool model documented
- Discovery captures tool preferences
- Future generation requirements are defined
## TASK-040 — Generate Claude Code permission profile
Status: Todo
Role: Documentation Agent
Goal:
Create a recommended Claude Code permissions configuration for RDB workflows.
Implementation Gap:
Claude Code permission settings are not currently documented, resulting in unnecessary approval prompts and inconsistent behaviour between projects.
Acceptance Criteria:
- Define recommended safe commands for auto-approval
- Define commands that should require approval
- Document the profile in CLAUDE.md
- Include rationale for each category
Definition of Done:
- Permission profile documented
- CLAUDE.md updated
## TASK-050 — AI-assisted discovery questioning
Status: Todo
Goal:
Use an LLM to propose the most valuable next discovery questions.
Acceptance Criteria:
- Review existing context
- Review discovery history
- Suggest follow-up questions
- Keep human approval in the loop
+14
View File
@@ -15,3 +15,17 @@ Expected result:
Tests pass Tests pass
CLI help displays CLI help displays
## Guardrails Validation
Run:
```bash
rdb guardrails
```
Expected result:
A report listing at least 5 checks (repeated reads, repeated commands,
recent file changes, test run record, RUN_LOG consistency) with an
overall status summary.
+34
View File
@@ -23,6 +23,40 @@ Agents should favour many small successful changes over large risky changes.
--- ---
# Objectivity
- Do not praise, flatter, or agree unnecessarily.
- Do not repeat information the user already knows.
- State problems directly without hedging or softening.
- Provide factual assessments; separate observation from opinion.
---
# Context Discipline
- Read each file at most once unless its content changed between reads.
- Do not re-read context files that were read earlier in the same session unless explicitly asked.
- Track which files you have already read in your working notes rather than reloading them.
---
# Communication Efficiency
- Do not restate the task or requirements before acting.
- Skip summarising instructions the user has already given.
- Provide results, not process descriptions, unless asked for a rationale.
- Use concise technical language; avoid filler phrases ("sure", "great", "I can help with that").
---
# Execution Efficiency
- Act on the task immediately after understanding it.
- Avoid unnecessary planning or confirmation steps when the next action is clear.
- Report only what changed, not everything you considered.
---
# Before Starting Work # Before Starting Work
Agents should first read: Agents should first read:
+236
View File
@@ -0,0 +1,236 @@
# Discovery-to-Context Mapping
This document defines how answers to the 14 core discovery questions should be transformed into project context files.
## Reference: Core Discovery Questions
| ID | Question |
|---|---|
| Q-001 | What problem are we solving? |
| Q-002 | Who is the user? |
| Q-003 | What does success look like? |
| Q-004 | What is the minimum useful version? |
| Q-005 | What data do we need? |
| Q-006 | What systems must it connect to? |
| Q-007 | What are the risks? |
| Q-008 | What must not happen? |
| Q-009 | How will we test it? |
| Q-010 | How will it be deployed? |
| Q-011 | Who owns and directs the project, and what are its organisational goals? |
| Q-012 | What is the technology stack, repository structure, and architecture approach? |
| Q-013 | Where is it hosted, which environments exist, and what are the security requirements? |
| Q-014 | What agent and developer workflow tools, constraints, and guidelines apply? |
## Reference: Target Context Files
All paths are relative to the project root.
| File | Purpose |
|---|---|
| `context/discovery-log.md` | Source of discovery answers — not a target, but the input to mapping |
| `context/product-brief.md` | Product vision, users, success criteria, MVP scope |
| `context/project-brief.md` | Detailed project brief with problem statement and timeline |
| `context/architecture.md` | System design, components, data flow, integrations, deployment |
| `context/decisions.md` | Architecture and design decision records (ADR-style table) |
| `context/risks.md` | Risk register with impact and mitigation columns |
| `context/assumptions.md` | Project assumptions with confidence and validation status |
| `context/open-questions.md` | Open questions needing further investigation |
| `context/development-context.md` | Tech stack, coding standards, build/test instructions |
| `context/infrastructure-context.md` | Hosting, environments, CI/CD, monitoring, security |
| `context/agent-guidelines.md` | Agent operating rules for this project |
| `context/company-context.md` | Company goals and constraints that shape the project |
| `context/repository-context.md` | Repository structure, workflow, and contributing info |
| `TEST_PLAN.md` | Manual and automated test plan |
## Mapping: Question → Context Files + Sections
### Q-001 — What problem are we solving?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/product-brief.md` | `## Problem` | The raw answer fills in the product problem statement. If the answer is vague, split it into customer pain and desired outcome. |
| `context/project-brief.md` | `## Problem Statement` | Restate in project-specific terms. Include scope boundaries if mentioned. |
| `context/company-context.md` | (append to Goals) | Link back to company-level goals if the answer references them. |
### Q-002 — Who is the user?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/product-brief.md` | `## Users` | Record user personas, roles, or segments. Use bullet points for each distinct user type. |
| `context/company-context.md` | `## Customers / Users` | If the answer identifies external customers rather than internal users, append to this section. |
| `context/development-context.md` | `## IDEs and Editors` | Note any tooling constraints specific to the target users (if they are developers). |
### Q-003 — What does success look like?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/product-brief.md` | `## Success Criteria` | Record measurable success criteria as a numbered or bulleted list. Each criterion should be verifiable. |
| `context/project-brief.md` | `## Success Metrics` | Restate with specific measurement approach (e.g., user count, latency threshold, coverage %). |
### Q-004 — What is the minimum useful version?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/product-brief.md` | `## Minimum Useful Version` | List MVP features as bullet points. Each feature should be independently valuable. |
| `context/project-brief.md` | `## Key Features (MVP)` | Expand with brief descriptions for each MVP feature. |
| `TASKS.md` | New TASK entries | Consider generating initial task rows from MVP features. |
### Q-005 — What data do we need?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/architecture.md` | `## Data Flow` | Describe data entities, storage locations, and movement between components. |
| `context/development-context.md` | `## Dependencies` | Record database platforms, storage services, or data stores required. |
| `context/infrastructure-context.md` | `## Environments` | Note any data requirements per environment (dev/staging/prod data parity). |
### Q-006 — What systems must it connect to?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/architecture.md` | `## External Integrations` | List each external system with its role (API, database, auth provider, etc.). |
| `context/infrastructure-context.md` | Append as section | If the integration requires infrastructure changes, document there. |
| `context/decisions.md` | New row | If integration choices involved trade-offs, record as a decision. |
### Q-007 — What are the risks?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/risks.md` | Data rows | Each risk becomes a row with columns: ID, Risk Description, Impact (High/Med/Low), Mitigation, Status. Assign sequential IDs like RISK-001. |
| `context/open-questions.md` | New rows | Any unresolved aspects of a risk become open questions. |
### Q-008 — What must not happen?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/assumptions.md` | Data rows | Constraints that are actually assumptions about what won't be done. Record confidence and validation need. |
| `context/decisions.md` | New row | If a "must not" is the result of an active decision (e.g., "no third-party auth"), record as a decision with reasoning. |
| `context/open-questions.md` | New rows | Any constraint that needs clarification becomes an open question. |
### Q-009 — How will we test it?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/development-context.md` | `## Build & Test` | Document test framework, test execution commands, and local setup steps. |
| `context/agent-guidelines.md` | `## Testing Preferences` (append) | Add a new section describing how tests should be written, run, and verified. Do **not** map testing answers into Timeline / Milestones sections in other files. |
### Q-010 — How will it be deployed?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/architecture.md` | `## Deployment Architecture` | Describe the deployment layout, scaling strategy, and process. |
| `context/infrastructure-context.md` | `## CI / CD Pipeline`, `## Hosting`, `## Environments` | Fill in hosting platform, environment details, and pipeline steps from the discovery answer. |
### Q-011 — Who owns and directs the project, and what are its organisational goals?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/company-context.md` | `## Mission` | Record the organisation's core mission, vision, or strategic goals that relate to this project. If no company-level mission exists, record the team's purpose. |
| `context/company-context.md` | `## Products & Services` (append) | Add a new section describing the company's products and services if the answer includes them. |
| `context/project-brief.md` | `## Target Audience` | Restate in project-specific terms — who will use or benefit from the final product? |
### Q-012 — What is the technology stack, repository structure, and architecture approach?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/development-context.md` | `## Tech Stack` | Extract languages, frameworks, libraries, tooling, and build/test tools. Record each as a distinct item. |
| `context/development-context.md` | `## Coding Standards` | Note any style conventions, naming patterns, linters, formatters, or agreed-upon patterns. If none exist, note "TBD". |
| `context/repository-context.md` | `## Dependencies` (append) | Add a new section describing project dependencies if not yet documented. |
| `context/architecture.md` | `## Overview` | Record high-level design principles, architectural patterns (e.g., monolith, microservices), and key design decisions. |
| `context/architecture.md` | `## Core Components` | List the main modules, services, or packages and their responsibilities. Use bullet points for each component. |
### Q-013 — Where is it hosted, which environments exist, and what are the security requirements?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/infrastructure-context.md` | `## Hosting` | Record hosting platform (cloud provider, on-prem, container orchestration, etc.). |
| `context/infrastructure-context.md` | `## Environments` | List all environments (dev/staging/prod), their purpose, and data management approach. |
| `context/infrastructure-context.md` | `## Monitoring & Alerting` | Document monitoring tools, alerting channels, and observability requirements. |
| `context/infrastructure-context.md` | `## Security` | Record authentication models, data protection requirements, compliance needs, and access control policies. |
### Q-014 — What agent and developer workflow tools, constraints, and guidelines apply?
Maps to:
| Context File | Section | Guidance |
|---|---|---|
| `context/agent-guidelines.md` | `## Purpose` | Record why agents need specific context for this project (constraints, conventions, expectations). |
| `context/agent-guidelines.md` | `## Preferred Tools` | List recommended editors, debuggers, testing frameworks, CLI tools, and automation tools. |
| `context/repository-context.md` | `## Purpose` | Record the project's overall purpose, scope boundaries, and what it is / is not responsible for. |
| `context/repository-context.md` | `## Contributing` | Document contribution process, branch strategy, code review requirements, and testing expectations for contributors. |
## Summary: Question-to-File Matrix
This matrix provides a quick-reference view of all mappings. "Primary" means the answer should go there first; "Secondary" means it may also be updated if relevant.
| Q-ID | Question | Primary Context Files | Secondary Context Files |
|---|---|---|---|
| Q-001 | What problem are we solving? | product-brief.md, project-brief.md | company-context.md |
| Q-002 | Who is the user? | product-brief.md, company-context.md | development-context.md |
| Q-003 | What does success look like? | product-brief.md, project-brief.md | — |
| Q-004 | Minimum useful version? | product-brief.md, project-brief.md | TASKS.md |
| Q-005 | What data do we need? | architecture.md, development-context.md | infrastructure-context.md |
| Q-006 | Systems to connect to? | architecture.md, infrastructure-context.md | decisions.md |
| Q-007 | What are the risks? | risks.md, open-questions.md | — |
| Q-008 | What must not happen? | assumptions.md, decisions.md | open-questions.md |
| Q-009 | How will we test it? | development-context.md, agent-guidelines.md | — |
| Q-010 | How will it be deployed? | architecture.md, infrastructure-context.md | — |
| Q-011 | Who owns and directs the project? What are its organisational goals? | company-context.md, project-brief.md | — |
| Q-012 | What is the technology stack, repository structure, architecture approach? | development-context.md, repository-context.md, architecture.md | — |
| Q-013 | Where is it hosted, which environments exist, and what are the security requirements? | infrastructure-context.md | — |
| Q-014 | What agent and developer workflow tools, constraints, and guidelines apply? | agent-guidelines.md, repository-context.md | — |
## Implementation Notes for Future Work
This document is a planning artifact. No code generation has been implemented yet.
### What future implementation should do
1. **Read** discovery-log.md (via `read_discovery_answers` in `discovery.py`)
2. **Classify** each answer by its question ID
3. **Transform** the answer text according to this mapping's section guidance
4. **Write** or update target context files, creating rows in tables where applicable
5. **Preserve** existing content — never overwrite sections without explicit user intent
6. **Record** what was generated so the user can review
### Constraints for future implementation
- Only process answers with High or Medium confidence
- Skip Low confidence answers (these should be handled by `ask-more` first)
- Use template format from `templates.py` as the base for context files
- Add new sections only if a discovery answer introduces entirely new content areas
- Log each mapping action to `.rdb/session-log.jsonl` for traceability
### Test requirements for future implementation
When code is eventually written, tests should verify:
- Each question ID maps to exactly the files listed in this document
- Empty/placeholder sections are populated correctly
- Existing file content is preserved (not overwritten)
- The matrix above passes programmatic validation against the mapping data
+320
View File
@@ -0,0 +1,320 @@
# Discovery Gap Analysis Model
This document defines how missing or weak project context should be detected and converted into targeted follow-up discovery questions.
It is a planning artifact for TASK-026 implementation. No code has been implemented.
---
## Overview
When `rdb generate` populates context files from discovery answers, some sections will remain empty or contain only placeholders because:
- No discovery question addressed them
- The answer had Low confidence (filtered out)
- The mapping did not route data to that section
- The section does not exist in the template
Gap analysis detects these cases and generates targeted follow-up questions instead of leaving them as TBD.
---
## 1. Gap Detection Rules
A gap exists when one or more of the following conditions hold:
| Rule ID | Condition | Example |
|---|---|---|
| GAP-01 | A context file section contains only a `TBD` or `TDB` placeholder | `## Mission\n\nTBD — What is the organisation's core mission?` |
| GAP-02 | A context file section exists in the template but no discovery answer mapped to it | `## Monitoring & Alerting` has no mapped source from any Q-id |
| GAP-03 | A section contains partial content with unresolved sub-items (e.g., blank list entries, empty brackets) | Section has text but a required sub-list item is missing |
| GAP-04 | An entire context file exists but has fewer than 50 characters of non-whitespace content beyond the heading | File is effectively empty |
Detection operates per **section** within each **context file**, not at the file level. A file with partial data may have gaps in some sections and none in others.
---
## 2. Gap Severity Levels
| Severity | Label | Criteria | Action Required |
|---|---|---|---|
| S1 | Critical | The gap is in a section required for the current BUILDING stage. Blocking on this gap would stop all downstream work. | Must be resolved before moving to REVIEW_READY |
| S2 | Important | The gap is in a section that will affect future implementation decisions but does not block immediate work. | Resolve within 1 BUILDING cycle (next discovery round) |
| S3 | Low | The gap adds useful detail but is not needed for any active or planned task. | Can be deferred to REVIEW_READY or later |
Severity assignment logic:
- A gap is **S1** if its section appears in the *current stage's expected files* and any open TASK references that section
- A gap is **S2** if its section is referenced by a mapped discovery question (Q-001 to Q-014) but no High/Medium confidence answer was produced
- A gap is **S3** if the section exists in templates.py, has no mapped discovery question, and does not appear in any active task
---
## 3. Question Generation Rules
Follow-up questions are generated according to these rules:
| Rule | Guidance | Example |
|---|---|---|
| QG-01 | One gap maps to exactly one follow-up question — do not bundle multiple gaps into a single question | Separate `## Mission` and `## Customers / Users` even if both are in company-context.md |
| QG-02 | Questions must be answerable with the original discovery format (text answer + confidence) | Ask for specific facts, not open-ended essays |
| QG-03 | The question text should reference the target section so the responder understands context | "What is the team's core mission statement?" rather than "Tell us about your company" |
| QG-04 | If a gap maps to a table format section, specify the table columns needed | For risks.md: "What security risk affects data retention? Provide impact and mitigation." |
| QG-05 | Do not generate questions for sections already filled with non-TBD content | Skip `## Mission` if it reads "We build developer tools" |
| QG-06 | Prefer questions that can be answered by the project owner or a technical lead, not external experts | "Which cloud provider hosts production?" rather than "Describe your AWS architecture" unless already established |
---
## 4. Question Prioritisation Rules
Follow-up questions are prioritised using this ordered scheme:
1. **Severity first** — All S1 gaps before all S2 gaps; all S2 before all S3
2. **File completeness within severity** — Files with more gaps are addressed earlier (reduce file-level churn)
3. **Dependency order** — If answering gap A enables answers to gap B, place A first
4. **Question scope** — Within equal priority, shorter/closer-to-answer questions come first
For example: `## Mission` (S1, company-context.md) and `## Hosting` (S1, infrastructure-context.md) are both S1; company-context.md goes first because it has 3 unresolved gaps versus infrastructure-context.md's 2.
---
## 5. Gap-to-Question Mapping
This mapping covers every template section that can contain TBD placeholders. For each section it defines: the gap trigger, the follow-up question to ask, and the target discovery question ID for the follow-up entry.
### context/company-context.md
| Section | Trigger | Follow-up Question | Discovery Q-ID |
|---|---|---|---|
| `## Mission` | Contains only TBD/placeholder text | "What is the organisation's core mission or purpose?" | New (Q-015-style) |
| `## Products & Services` | Contains only TBD/placeholder text | "List the products or services the organisation offers. What does each one do?" | New |
| `## Customers / Users` | Contains only TBD/placeholder text | "Who are the primary customers or users of your products? Use bullet points for each distinct user type." | New |
| `## Brand & Positioning` | Contains only TBD/placeholder text | "How does the organisation position itself in the market?" | New |
| `## Key Stakeholders` | Contains only TBD/placeholder text | "Who are the key stakeholders (names, roles, contact) for this project?" | New |
### context/product-brief.md
| Section | Trigger | Follow-up Question | Discovery Q-ID |
|---|---|---|---|
| `## Problem` | Contains only TBD/placeholder text | "What problem is this product solving? Describe the customer pain and desired outcome." | Mirrors Q-001 |
| `## Users` | Contains only TBD/placeholder text | "Who are the users of this product? List personas, roles, or segments." | Mirrors Q-002 |
| `## Success Criteria` | Contains only TBD/placeholder text | "How will you measure success for this product? Be specific and verifiable." | Mirrors Q-003 |
| `## Minimum Useful Version` | Contains only TBD/placeholder text | "What is the minimum set of features that delivers real value to users?" | Mirrors Q-004 |
### context/project-brief.md
| Section | Trigger | Follow-up Question | Discovery Q-ID |
|---|---|---|---|
| `## Problem Statement` | Contains only TBD/placeholder text | "Restate the product problem in project-specific terms. Include scope boundaries." | Mirrors Q-001 |
| `## Target Audience` | Contains only TBD/placeholder text | "Who is the intended audience for this project? Who benefits from the final product?" | Mirrors Q-011 |
| `## Key Features (MVP)` | Contains only TBD/placeholder text | "List the minimum set of features for the first release. Each feature should be independently valuable." | Mirrors Q-004 |
| `## Success Metrics` | Contains only TBD/placeholder text | "How will you measure success for this project? Include specific thresholds or targets." | Mirrors Q-003 |
| `## Timeline & Milestones` | Contains only TBD/placeholder text | "What are the expected delivery dates and key milestones?" | New (Q-015-style) |
### context/architecture.md
| Section | Trigger | Follow-up Question | Discovery Q-ID |
|---|---|---|---|
| `## Overview` | Contains only TBD/placeholder text | "Describe the high-level system architecture. What design principles or patterns apply?" | Mirrors Q-012 |
| `## Core Components` | Contains only TBD/placeholder text | "List the main modules, services, or packages and their responsibilities." | Mirrors Q-012 |
| `## Data Flow` | Contains only TBD/placeholder text | "How does data move through the system? Describe entities, storage, and movement between components." | Mirrors Q-005 |
| `## External Integrations` | Contains only TBD/placeholder text | "What external systems, APIs, or services must this connect to?" | Mirrors Q-006 |
| `## Deployment Architecture` | Contains only TBD/placeholder text | "Describe the deployment layout, scaling strategy, and process." | Mirrors Q-010 |
### context/decisions.md
| Section | Trigger | Follow-up Question | Discovery Q-ID |
|---|---|---|---|
| Table body (no rows) | Has 0 data rows in the decision table | "What architecture or design decisions have been made? Record each with reason and date." | New |
### context/risks.md
| Section | Trigger | Follow-up Question | Discovery Q-ID |
|---|---|---|---|
| Table body (no rows) | Has 0 data rows in the risks table | "What are the project risks? For each, describe impact and mitigation strategy." | Mirrors Q-007 |
### context/assumptions.md
| Section | Trigger | Follow-up Question | Discovery Q-ID |
|---|---|---|---|
| Table body (no rows) | Has 0 data rows in the assumptions table | "What are you assuming about this project? For each, note confidence and whether validation is needed." | Mirrors Q-008 |
### context/development-context.md
| Section | Trigger | Follow-up Question | Discovery Q-ID |
|---|---|---|---|
| `## Tech Stack` | Contains only TBD/placeholder text | "What is the technology stack? List languages, frameworks, libraries, and tooling." | Mirrors Q-012 |
| `## Coding Standards` | Contains only TBD/placeholder text | "Are there agreed coding standards? Describe style conventions, naming patterns, linters, and formatters." | New |
| `## Repository Structure` | Contains only TBD/placeholder text | "Describe the repository directory layout and module organisation." | Mirrors Q-012 |
| `## Build & Test` | Contains only TBD/placeholder text | "How do you build, test, and run this codebase locally? Include commands and setup steps." | Mirrors Q-009 |
| `## Dependencies` | Contains only TBD/placeholder text | "What external services, databases, or third-party APIs are required?" | Mirrors Q-006 |
### context/infrastructure-context.md
| Section | Trigger | Follow-up Question | Discovery Q-ID |
|---|---|---|---|
| `## Hosting` | Contains only TBD/placeholder text | "Where is the application hosted? Specify cloud provider, on-prem details, or container orchestration." | Mirrors Q-013 |
| `## Environments` | Contains only TBD/placeholder text | "What environments exist (dev/staging/prod)? Describe each and data management approach." | Mirrors Q-013 |
| `## CI / CD Pipeline` | Contains only TBD/placeholder text | "Describe the build, test, and deployment pipeline configuration." | Mirrors Q-013 |
| `## Monitoring & Alerting` | Contains only TBD/placeholder text | "What tools are used for monitoring, logging, and alerting?" | Mirrors Q-013 |
| `## Security` | Contains only TBD/placeholder text | "What authentication models, data protection requirements, compliance needs, and access control policies apply?" | Mirrors Q-013 |
### context/agent-guidelines.md
| Section | Trigger | Follow-up Question | Discovery Q-ID |
|---|---|---|---|
| `## Purpose` | Contains only TBD/placeholder text | "What should agents or developers know about working on this project? Note constraints and conventions." | Mirrors Q-014 |
| `## Preferred Tools` | Contains only TBD/placeholder text | "Which editors, debuggers, testing frameworks, and CLI tools are recommended?" | Mirrors Q-014 |
| `## Testing Preferences` | Contains only TBD/placeholder text | "How should tests be written, run, and verified for this project?" | Mirrors Q-009 |
| `## Common Tasks` | Contains only TBD/placeholder text | "What are the typical workflows for developers or AI agents on this project?" | New |
| `## Known Gotchas` | Contains only TBD/placeholder text | "Are there known pitfalls, quirks, or important caveats to be aware of?" | New |
### context/repository-context.md
| Section | Trigger | Follow-up Question | Discovery Q-ID |
|---|---|---|---|
| `## Purpose` | Contains only TBD/placeholder text | "What is this repository's overall purpose and scope? What is it responsible for (and not responsible for)?" | Mirrors Q-014 |
| `## Key Components` | Contains only TBD/placeholder text | "List the key components or modules in this repository." | Mirrors Q-012 |
| `## Dependencies` | Contains only TBD/placeholder text | "What project dependencies exist? List external services, databases, and third-party APIs." | Mirrors Q-006 |
| `## Contributing` | Contains only TBD/placeholder text | "Describe the contribution process: branch strategy, code review requirements, testing expectations." | Mirrors Q-014 |
### context/open-questions.md
| Section | Trigger | Follow-up Question | Discovery Q-ID |
|---|---|---|---|
| Table body (no rows) | Has 0 data rows | "Are there any open questions that need further investigation before implementation begins?" | New |
### project-level files (PROJECT_STATE.md, AGENT_HANDOFF.md, TASKS.md, TEST_PLAN.md, RUN_LOG.md)
These files are managed by CLI commands, not by discovery answers. They are **not** subject to gap analysis. Gaps in these files are handled by command logic, not by question generation.
---
## 6. Example Gap-to-Question Flow
Here is an end-to-end example of how the model operates:
### Step 1 — Gap Detection
`rdb context-status` or a dedicated gap analysis scan finds:
| File | Section | Gap Rule | Severity |
|---|---|---|---|
| `context/company-context.md` | `## Mission` | GAP-01 (TBD placeholder) | S1 |
| `context/company-context.md` | `## Key Stakeholders` | GAP-01 (TBD placeholder) | S2 |
| `context/infrastructure-context.md` | `## Hosting` | GAP-01 (TBD placeholder) | S1 |
| `context/infrastructure-context.md` | `## Monitoring & Alerting` | GAP-01 (TBD placeholder) | S2 |
| `context/development-context.md` | `## Coding Standards` | GAP-01 (TBD placeholder) | S3 |
### Step 2 — Prioritisation
Sorted by severity, then file completeness:
1. S1 — `company-context.md` / `## Mission` (company has 5 gaps vs infra's 4)
2. S1 — `infrastructure-context.md` / `## Hosting`
3. S2 — `company-context.md` / `## Key Stakeholders`
4. S2 — `infrastructure-context.md` / `## Monitoring & Alerting`
5. S3 — `development-context.md` / `## Coding Standards`
### Step 3 — Question Generation
Follow-up questions appended to `context/discovery-log.md`:
| New ID | Follow-up Question | Source Gap | Confidence Expected |
|---|---|---|---|
| Q-015 | "What is the organisation's core mission or purpose?" | company-context.md `## Mission` | High |
| Q-016 | "Where is the application hosted? Specify cloud provider, on-prem details, or container orchestration." | infrastructure-context.md `## Hosting` | High |
| Q-017 | "Who are the key stakeholders (names, roles, contact) for this project?" | company-context.md `## Key Stakeholders` | Medium |
| Q-018 | "What tools are used for monitoring, logging, and alerting?" | infrastructure-context.md `## Monitoring & Alerting` | Medium |
| Q-019 | "Are there agreed coding standards? Describe style conventions, naming patterns, linters, and formatters." | development-context.md `## Coding Standards` | Low |
### Step 4 — Regeneration
After answers to Q-015 through Q-019 are collected:
- Run `rdb generate` (or re-run gap analysis)
- Sections populated from new answers should resolve the S1 and S2 gaps
- Remaining gaps re-prioritise for the next cycle
---
## 7. Edge Cases
| Case | Handling |
|---|---|
| Section contains partial text + TBD sub-items | Only generate a question for the missing sub-item, not the entire section |
| Section text reads "TBD" but it is actually correct (intentional placeholder) | Provide a skip/confirm mechanism — let the user mark as "acceptable" to suppress the gap |
| Template adds new sections in future versions | Sections with no mapping entry default to S3 severity with a generic question: "What information belongs in this section?" |
| A section maps to multiple discovery questions | Generate separate follow-ups per unmapped question, each linked to its source Q-id |
| File has fewer than 50 characters (GAP-04) but the heading is meaningful | Count content beyond the heading line only |
---
## 8. Implementation Task: TASK-026
### Scope for TASK-026 (Implementation)
TASK-026 will implement a CLI command `rdb gap` that:
1. Reads all 18 template-based context files listed in `templates.py CONTEXT_FILES`
2. For each file, scans every section for GAP-01 (TBD), GAP-03 (partial content), or GAP-04 (near-empty file)
3. For each gap found, applies severity rules from Section 2
4. Generates follow-up questions using the mapping in Section 5
5. Prioritises questions per Section 4
6. Outputs the prioritised question list to stdout
7. Optionally appends answers to `context/discovery-log.md` when a user provides them interactively
### What TASK-026 Does NOT Do
- Does not auto-populate context files — that remains `rdb generate`'s scope
- Does not replace `rdb discover` — follow-up questions are new entries in discovery-log.md
- Does not change the existing gap analysis model document
### Expected Module Structure (TASK-026)
```
src/rdb_discovery/gap_analysis.py # Core detection and question generation logic
tests/test_gap_analysis.py # Unit tests for each section mapping
```
### Expected Test Requirements (TASK-026)
| Test Category | Count | Description |
|---|---|---|
| Gap detection per template section | ~25 | One per section in the mapping table |
| Severity classification | 3+ | S1, S2, S3 on representative inputs |
| Question generation format | 5+ | Follow-up questions match QG-01 through QG-06 rules |
| Prioritisation ordering | 3+ | Sorted output matches severity → completeness → dependency → scope |
| Edge cases (Section 6) | 5+ | Partial sections, acceptable TBDs, new template sections, multi-question sections |
| CLI command `rdb gap` | 4+ | Help text, empty project handling, discovery-log append mode |
---
## 9. How This Model Relates to the Rest of RDB
```
discovery-log.md (source of truth for all answers)
rdb discover / rdb gap (collects or generates questions)
rdb generate (maps answers → context files via discovery-context-mapping.md)
rdb context-status (reports health, can trigger gap analysis)
rdb gap (detects remaining gaps → new questions)
└───────────────────► loop back to rdb discover for follow-up answers
```
This model connects to:
- `context/discovery-context-mapping.md` — the existing Q-001 to Q-014 mapping (this model extends it with follow-up question generation)
- `src/rdb_discovery/templates.py` — the authoritative list of template sections used for detection
- `rdb context-status` — health reporting can surface gap counts as an additional metric
---
*This document is a planning artifact. No code has been implemented. TASK-026 will implement the CLI command based on this model.*
+49 -13
View File
@@ -11,9 +11,11 @@ Requires-Dist: pytest>=8.0.0; extra == "dev"
# rdb-discovery # rdb-discovery
A small CLI-first tool for repeatable software project discovery and delivery. A small CLI-first tool that helps you generate and maintain useful project context files for AI-assisted development.
It creates markdown files that help humans and AI coding agents understand: ## Purpose
rdb-discovery exists to make it easier for humans and AI coding agents to understand:
- what is being built - what is being built
- why it exists - why it exists
@@ -21,6 +23,20 @@ It creates markdown files that help humans and AI coding agents understand:
- what task should be done next - what task should be done next
- what stage the project is currently in - what stage the project is currently in
The tool creates and manages a set of markdown files — your project's source-of-truth context.
## Core workflow
1. **Ask discovery questions** — `rdb discover` captures answers about the product, architecture, risks, and open questions.
2. **Capture answers** — Structured responses are stored in `context/discovery-log.md` alongside confidence scores and follow-ups.
3. **Generate context files** — `rdb init` creates standardised project files (`product-brief.md`, `architecture.md`, `decisions.md`, etc.).
4. **Support agent implementation** — `rdb prompt` produces ready-to-paste implementation prompts for Claude Code or Cline, and `rdb start` / `rdb complete` manage task lifecycles.
## Support features (not the product)
- **Telemetry** records structured agent activity events in `.rdb/session-log.jsonl`. It enables future analysis but is not the main offering.
- **Guardrails** detect agent stalls, repeated reads, and inconsistent state to keep projects on track. They support the workflow but are secondary.
## Install for local development ## Install for local development
```bash ```bash
@@ -32,23 +48,43 @@ pip install -e '.[dev]'
## Commands ## Commands
```bash ```bash
rdb init rdb init # Create standard project structure and context files
rdb discover rdb discover # Ask 10 core discovery questions, record answers
rdb status rdb guardrails # Check for agent stalls and state inconsistencies
rdb next rdb prompt # Generate implementation prompt for the next task
rdb start TASK-001 rdb start TASK-001 # Mark a task as in progress
rdb complete TASK-001 rdb complete TASK-001# Mark a task as done
rdb handoff rdb status # Show current stage and active task
rdb next # Print the next task to work on
rdb handoff # Generate agent handoff notes
``` ```
## Bootstrap workflow ## Bootstrap workflow
1. Run `rdb init` 1. Run `rdb init`
2. Run `rdb discover` 2. Run `rdb discover`
3. Run `rdb status` 3. Review `rdb status` and `rdb next`
4. Run `rdb next` 4. Give the next prompt to Claude Code or Cline via `rdb prompt`
5. Give the next task to Claude Code or Cline 5. Commit after each completed task
6. Commit after each completed task
## Project structure
`rdb init` creates the following files (skips existing ones):
- context/discovery-log.md
- context/product-brief.md
- context/architecture.md
- context/decisions.md
- context/risks.md
- context/assumptions.md
- context/open-questions.md
- context/repository-context.md
- TASKS.md
- TEST_PLAN.md
- RUN_LOG.md
- PROJECT_STATE.md
- AGENT_HANDOFF.md
- .rdb/project.json
## Principle ## Principle
+7
View File
@@ -3,9 +3,11 @@ pyproject.toml
src/rdb_discovery/__init__.py src/rdb_discovery/__init__.py
src/rdb_discovery/cli.py src/rdb_discovery/cli.py
src/rdb_discovery/discovery.py src/rdb_discovery/discovery.py
src/rdb_discovery/guardrails.py
src/rdb_discovery/handoff.py src/rdb_discovery/handoff.py
src/rdb_discovery/status.py src/rdb_discovery/status.py
src/rdb_discovery/tasks.py src/rdb_discovery/tasks.py
src/rdb_discovery/telemetry.py
src/rdb_discovery/templates.py src/rdb_discovery/templates.py
src/rdb_discovery.egg-info/PKG-INFO src/rdb_discovery.egg-info/PKG-INFO
src/rdb_discovery.egg-info/SOURCES.txt src/rdb_discovery.egg-info/SOURCES.txt
@@ -13,5 +15,10 @@ src/rdb_discovery.egg-info/dependency_links.txt
src/rdb_discovery.egg-info/entry_points.txt src/rdb_discovery.egg-info/entry_points.txt
src/rdb_discovery.egg-info/requires.txt src/rdb_discovery.egg-info/requires.txt
src/rdb_discovery.egg-info/top_level.txt src/rdb_discovery.egg-info/top_level.txt
tests/test_discovery.py
tests/test_guardrails.py
tests/test_prompt.py
tests/test_status.py
tests/test_tasks.py tests/test_tasks.py
tests/test_telemetry.py
tests/test_templates.py tests/test_templates.py
+158 -4
View File
@@ -7,11 +7,15 @@ import typer
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
from .discovery import append_discovery_answer, core_questions from .discovery import append_discovery_answer, append_followup_answer, core_questions, read_discovery_answers
from .handoff import build_handoff from .handoff import build_handoff
from .status import project_stage, task_counts from .status import project_stage, task_counts, update_project_state, update_agent_handoff
from .tasks import generate_agent_prompt, get_next_task, update_task_status from .tasks import generate_agent_prompt, get_next_task, update_task_status
from .templates import CONTEXT_FILES, write_file_if_missing from .templates import CONTEXT_FILES, write_file_if_missing
from .guardrails import run_all_guardrails, format_report
from .telemetry import record_event
from .generate_context import generate_context_files
from .context_status import context_status as get_context_status, compute_health_score
app = typer.Typer(help="RDB discovery and delivery workflow CLI.") app = typer.Typer(help="RDB discovery and delivery workflow CLI.")
console = Console() console = Console()
@@ -34,6 +38,7 @@ def append_run_log(root: Path, event: str, task_id: str = "", notes: str = "") -
def init() -> None: def init() -> None:
"""Create the initial markdown project structure.""" """Create the initial markdown project structure."""
root = root_path() root = root_path()
record_event(root, "command", "rdb init")
created: list[str] = [] created: list[str] = []
skipped: list[str] = [] skipped: list[str] = []
@@ -58,6 +63,7 @@ def init() -> None:
def discover() -> None: def discover() -> None:
"""Ask the core discovery questions and append answers to the discovery ledger.""" """Ask the core discovery questions and append answers to the discovery ledger."""
root = root_path() root = root_path()
record_event(root, "command", "rdb discover")
write_file_if_missing(root, "context/discovery-log.md", CONTEXT_FILES["context/discovery-log.md"]) write_file_if_missing(root, "context/discovery-log.md", CONTEXT_FILES["context/discovery-log.md"])
for index, question in enumerate(core_questions(), start=1): for index, question in enumerate(core_questions(), start=1):
@@ -71,10 +77,55 @@ def discover() -> None:
console.print("[bold green]Discovery complete.[/bold green]") console.print("[bold green]Discovery complete.[/bold green]")
@app.command()
def ask_more() -> None:
"""Ask for additional details on Low-confidence or follow-up-needed answers."""
root = root_path()
record_event(root, "command", "rdb ask_more")
answers = read_discovery_answers(root)
if not answers:
console.print("[yellow]No discovery answers found. Run `discover` first.[/yellow]")
raise typer.Exit(code=0)
low_conf = [a for a in answers if a["low_confidence"]]
follow_ups = [a for a in answers if a["needs_followup"]]
flagged: dict[str, list[str]] = {} # question_id -> list of reasons
for a in low_conf:
flagged.setdefault(a["id"], []).append("Low confidence")
for a in follow_ups:
flagged.setdefault(a["id"], []).append("Follow-up needed")
if not flagged:
console.print("[green]No Low-confidence or Follow-up-needed answers found.[/green]")
raise typer.Exit(code=0)
count = 0
for qid, reasons in sorted(flagged.items()):
original = next(a for a in answers if a["id"] == qid)
console.print(f"\n[yellow]Question:[/yellow] {original['question']}")
console.print(f"[dim]Reasons: {', '.join(reasons)}[/dim]")
fu_question = typer.prompt(
f"Follow-up for {qid}",
default=f"Additional detail on: {original['question'][:40]}",
)
fu_answer = typer.prompt("New answer")
fu_confidence = typer.prompt("Confidence?", default="Medium")
append_followup_answer(root, qid, fu_question, fu_answer, fu_confidence)
count += 1
console.print(f"[green]Recorded follow-up for {qid}.[/green]")
console.print(f"\n[bold green]Ask-more complete: recorded {count} follow-up(s).[/bold green]")
@app.command() @app.command()
def status() -> None: def status() -> None:
"""Show current project stage and task counts.""" """Show current project stage and task counts."""
root = root_path() root = root_path()
record_event(root, "command", "rdb status")
console.print(f"[bold]Project stage:[/bold] {project_stage(root)}") console.print(f"[bold]Project stage:[/bold] {project_stage(root)}")
counts = task_counts(root) counts = task_counts(root)
@@ -94,7 +145,9 @@ def status() -> None:
@app.command(name="next") @app.command(name="next")
def next_task() -> None: def next_task() -> None:
"""Show the next Todo task.""" """Show the next Todo task."""
task = get_next_task(root_path()) root = root_path()
record_event(root, "command", "rdb next")
task = get_next_task(root)
if not task: if not task:
console.print("[yellow]No Todo task found.[/yellow]") console.print("[yellow]No Todo task found.[/yellow]")
raise typer.Exit(code=0) raise typer.Exit(code=0)
@@ -108,10 +161,13 @@ def next_task() -> None:
def start(task_id: str) -> None: def start(task_id: str) -> None:
"""Mark a task as In Progress.""" """Mark a task as In Progress."""
root = root_path() root = root_path()
record_event(root, "command", "rdb start", {"task_id": task_id})
if not update_task_status(root, task_id, "In Progress"): if not update_task_status(root, task_id, "In Progress"):
console.print(f"[red]Task not found:[/red] {task_id}") console.print(f"[red]Task not found:[/red] {task_id}")
raise typer.Exit(code=1) raise typer.Exit(code=1)
append_run_log(root, "Task started", task_id=task_id) append_run_log(root, "Task started", task_id=task_id)
update_project_state(root, task_id)
update_agent_handoff(root, task_id)
console.print(f"[green]Started {task_id}.[/green]") console.print(f"[green]Started {task_id}.[/green]")
@@ -119,23 +175,121 @@ def start(task_id: str) -> None:
def complete(task_id: str) -> None: def complete(task_id: str) -> None:
"""Mark a task as Done and record validation notes.""" """Mark a task as Done and record validation notes."""
root = root_path() root = root_path()
record_event(root, "command", "rdb complete", {"task_id": task_id})
notes = typer.prompt("Validation notes", default="Not tested") notes = typer.prompt("Validation notes", default="Not tested")
if not update_task_status(root, task_id, "Done"): if not update_task_status(root, task_id, "Done"):
console.print(f"[red]Task not found:[/red] {task_id}") console.print(f"[red]Task not found:[/red] {task_id}")
raise typer.Exit(code=1) raise typer.Exit(code=1)
append_run_log(root, "Task completed", task_id=task_id, notes=notes) append_run_log(root, "Task completed", task_id=task_id, notes=notes)
update_project_state(root, task_id)
update_agent_handoff(root, task_id)
console.print(f"[green]Completed {task_id}.[/green]") console.print(f"[green]Completed {task_id}.[/green]")
@app.command() @app.command()
def handoff() -> None: def handoff() -> None:
"""Print an AI-agent handoff summary.""" """Print an AI-agent handoff summary."""
console.print(build_handoff(root_path())) root = root_path()
record_event(root, "command", "rdb handoff")
console.print(build_handoff(root))
@app.command() @app.command()
def prompt() -> None: def prompt() -> None:
"""Generate a ready-to-paste implementation prompt for the next Todo task.""" """Generate a ready-to-paste implementation prompt for the next Todo task."""
root = root_path() root = root_path()
record_event(root, "command", "rdb prompt")
result = generate_agent_prompt(root) result = generate_agent_prompt(root)
console.print(result) console.print(result)
@app.command()
def guardrails() -> None:
"""Review agent runs for signs of non-progress (stalls, repeats, loops)."""
root = root_path()
record_event(root, "command", "rdb guardrails")
results = run_all_guardrails(root)
report = format_report(results)
console.print(report)
@app.command()
def generate(min_confidence: str = typer.Option("Medium", help="Minimum confidence to process (High or Medium).")) -> None:
"""Generate context files from discovery answers using the mapping document."""
root = root_path()
record_event(root, "command", "rdb generate")
result = generate_context_files(root, min_confidence)
if result["generated"]:
console.print("[bold green]Generated context content:[/bold green]")
for f in result["generated"]:
console.print(f"- {f}")
else:
console.print("[yellow]No context files generated.[/yellow]")
if result["skipped"]:
console.print("\n[dim]Skipped:[/dim]")
for s in result["skipped"]:
console.print(f" - {s}")
@app.command()
def context_status() -> None:
"""Report the health and completeness of project context."""
root = root_path()
record_event(root, "command", "rdb context-status")
report = get_context_status(root)
score = report["score"]
summary = report["summary"]
# Score bar
if score >= 70:
score_color = "green"
elif score >= 40:
score_color = "yellow"
else:
score_color = "red"
console.print(f"\n[bold]Context Health Report[/bold]")
score_text = f"{score}/100"
console.print(f"[bold][{score_color}]{score_text}[/{score_color}]")
console.print("")
# File status table
table = Table(title="Expected Files")
table.add_column("File")
table.add_column("Status")
table.add_column("Size")
for f in report["expected_files"]:
color = "green" if f["status"] == "present" else "red"
icon = "[green]✓[/green]" if f["status"] == "present" else "[red]✗[/red]"
table.add_row(f["path"], f"[{color}]{icon}[/{color}]", f["size"])
console.print(table)
# TBD placeholders
if report["tbd_sections"]:
console.print(f"\n[bold yellow]TBD Placeholders ({summary['tbd_sections']} still open):[/bold yellow]")
for item in report["tbd_sections"]:
console.print(f" [yellow]• {item['file']}{item['section']} (line {item['line']})[/yellow]")
else:
console.print("[green]\nNo TBD placeholders found.[/green]")
# Low-confidence discovery answers
if report["low_confidence"]:
console.print(f"\n[bold yellow]Low-Confidence Discovery Answers ({summary['low_confidence_answers']}):[/bold yellow]")
for item in report["low_confidence"]:
console.print(f" [yellow]• {item['id']}: {item['question']} (confidence: {item['confidence']})[/yellow]")
else:
console.print("[green]\nNo low-confidence discovery answers.[/green]")
# Summary line
missing = summary["missing"]
if missing > 0:
console.print(f"\n[dim]{summary['present']} of {summary['total_expected']} context files present. "
f"{missing} missing, {summary['tbd_sections']} TBD sections, "
f"{summary['low_confidence_answers']} low-confidence answers.[/dim]")
else:
console.print(f"\n[dim]{summary['present']} of {summary['total_expected']} context files present. "
f"All expected files exist.{'' if summary['tbd_sections'] == 0 and summary['low_confidence_answers'] == 0 else ' Review TBD sections and low-confidence answers above.'}[/dim]")
+150
View File
@@ -0,0 +1,150 @@
"""Context health report — assess whether sufficient context exists for AI-assisted development."""
from __future__ import annotations
import re
from pathlib import Path
from typing import Any
def _expected_files(root: Path) -> list[str]:
"""Return the list of expected context files for this project."""
return [
"context/discovery-log.md",
"context/product-brief.md",
"context/architecture.md",
"context/decisions.md",
"context/risks.md",
"context/assumptions.md",
"context/open-questions.md",
"context/repository-context.md",
"context/company-context.md",
"context/development-context.md",
"context/infrastructure-context.md",
"context/agent-guidelines.md",
"context/project-brief.md",
"TASKS.md",
"TEST_PLAN.md",
"RUN_LOG.md",
"PROJECT_STATE.md",
"AGENT_HANDOFF.md",
]
def _check_expected_files(root: Path) -> list[dict[str, str]]:
"""Check which expected files exist and which are missing."""
results: list[dict[str, str]] = []
for path_str in _expected_files(root):
full = root / path_str
status = "missing" if not full.exists() else "present"
size = full.stat().st_size if full.exists() else 0
results.append({"path": path_str, "status": status, "size": str(size)})
return results
def _check_tbd_sections(root: Path) -> list[dict[str, Any]]:
"""Find sections still containing TBD/TDB placeholders."""
context_dir = root / "context"
if not context_dir.exists():
return []
findings: list[dict[str, Any]] = []
for file_path in sorted(context_dir.iterdir()):
if not file_path.is_file() or file_path.suffix != ".md":
continue
content = file_path.read_text(encoding="utf-8")
lines = content.splitlines()
for i, line in enumerate(lines):
stripped = line.strip()
if re.match(r"^## .+$", stripped):
# Look past blank lines to find the first non-empty content line
content_line = None
for j in range(i + 1, len(lines)):
candidate = lines[j].strip()
if not candidate:
continue
if re.match(r"^## ", candidate) or candidate.startswith("# "):
break
content_line = candidate
break
if content_line and (content_line.startswith("TBD") or content_line.startswith("TDB")):
placeholder_text = content_line.split("")[0].split()[0] if content_line.split() else ""
findings.append({
"file": file_path.name,
"section": stripped,
"line": i + 2,
"placeholder": placeholder_text,
})
return findings
def _check_low_confidence(root: Path) -> list[dict[str, str]]:
"""Find low-confidence discovery answers from discovery-log.md."""
from .discovery import read_discovery_answers
answers = read_discovery_answers(root)
low_conf = [a for a in answers if a["low_confidence"]]
return [{"id": a["id"], "question": a["question"], "confidence": a["confidence"]} for a in low_conf]
def compute_health_score(expected: list[dict[str, str]], tbd_sections: int, low_conf_count: int) -> float:
"""Compute an overall health score (0-100).
Scoring breakdown:
- 45 pts for file completeness (each file is equal share of 45)
- 30 pts for no TBD placeholders in body sections (proportional decay from 30 down to 0 at 20+ TBDs)
- 15 pts for no low-confidence answers (proportional decay from 15 down to 0 at 10+ low-conf)
- 10 pts if discovery-log.md exists with data
"""
total_expected = len(expected)
present_count = sum(1 for f in expected if f["status"] == "present")
# File completeness: up to 45 points
file_score = (present_count / max(total_expected, 1)) * 45
# TBD penalty: start at 30, lose 1.5 per TBD (min 0)
tbd_score = max(30 - (tbd_sections * 1.5), 0)
# Low-confidence penalty: start at 15, lose 1.5 per low-conf answer (min 0)
lc_score = max(15 - (low_conf_count * 1.5), 0)
return round(min(file_score + tbd_score + lc_score, 100), 1)
def context_status(root: Path) -> dict[str, Any]:
"""Run all health checks and return a structured report."""
expected = _check_expected_files(root)
tbd_sections = _check_tbd_sections(root)
low_confidence = _check_low_confidence(root)
missing_count = sum(1 for f in expected if f["status"] == "missing")
present_count = len(expected) - missing_count
total_expected = len(expected)
score = compute_health_score(expected, len(tbd_sections), len(low_confidence))
# discovery log bonus (up to 10 points)
discovery_log = root / "context" / "discovery-log.md"
discovery_score = 0
if discovery_log.exists():
content = discovery_log.read_text(encoding="utf-8")
if any(line.startswith("| Q-") for line in content.splitlines()):
discovery_score = 10
else:
discovery_score = 5
score += discovery_score
return {
"score": score,
"summary": {
"total_expected": total_expected,
"present": present_count,
"missing": missing_count,
"tbd_sections": len(tbd_sections),
"low_confidence_answers": len(low_confidence),
},
"expected_files": expected,
"tbd_sections": tbd_sections,
"low_confidence": low_confidence,
}
+71
View File
@@ -36,3 +36,74 @@ def append_discovery_answer(
def core_questions() -> list[str]: def core_questions() -> list[str]:
return CORE_QUESTIONS return CORE_QUESTIONS
def read_discovery_answers(root: Path) -> list[dict]:
"""Read discovery-log.md and return parsed rows as dicts.
Each dict has keys: id, question, answer, confidence, follow_up_needed.
Rows with 'Low' confidence or follow_up_needed == 'Yes' are flagged
via ``needs_followup`` and ``low_confidence`` boolean fields.
"""
log_path = root / "context" / "discovery-log.md"
if not log_path.exists():
return []
content = log_path.read_text(encoding="utf-8")
lines = content.splitlines()
answers: list[dict] = []
for line in lines:
# Skip header and non-data rows
if not line.startswith("| Q-"):
continue
# Replace escaped pipes with a placeholder before splitting,
# so they don't create extra columns. Restore after splitting.
safe_line = line.replace("\\|", "\x00PIPE\x00")
cells = [c.strip().replace("\x00PIPE\x00", "|") for c in safe_line.split("|")[1:-1]]
if len(cells) < 5:
continue
answers.append({
"id": cells[0],
"question": cells[1],
"answer": cells[2].replace("\\|", "|"),
"confidence": cells[3],
"follow_up_needed": cells[4],
"needs_followup": cells[4] == "Yes",
"low_confidence": cells[3] in ("Low", "low"),
})
return answers
def append_followup_answer(
root: Path,
question_id: str,
follow_up_question: str,
answer: str,
confidence: str = "Medium",
) -> None:
"""Append a follow-up answer row to discovery-log.md.
The original ``question_id`` is stored in the 'Linked decision' column.
"""
log_path = root / "context" / "discovery-log.md"
log_path.parent.mkdir(parents=True, exist_ok=True)
if not log_path.exists():
log_path.write_text(
"# Discovery Log\n\n"
"| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n"
"|---|---|---|---|---|---|---|---|\n",
encoding="utf-8",
)
safe_answer = answer.replace("|", "\\|").replace("\n", " ").strip()
line = (
f"| {question_id} | {follow_up_question} | {safe_answer} | {confidence} | "
f"Yes | | | {date.today().isoformat()} |\n"
)
with log_path.open("a", encoding="utf-8") as handle:
handle.write(line)
+293
View File
@@ -0,0 +1,293 @@
"""Generate context files from discovery answers.
Uses the mapping defined in context/discovery-context-mapping.md to transform
discovery-log.md entries into the appropriate context files.
Only processes High/Medium confidence answers. Low-confidence answers are
skipped and must be handled by `rdb ask-more` first.
Preserves existing content in context files - never overwrites.
"""
from __future__ import annotations
from pathlib import Path
from .discovery import read_discovery_answers
# Mapping from question ID to list of (file_path, section_name, format_type)
# format_type: "body" = fill under heading with body text
# "table" = append row to table-based section
# "append" = append as a new section at end of file
CONTEXT_MAP = {
"Q-001": [ # What problem are we solving?
("context/product-brief.md", "## Problem", "body"),
("context/project-brief.md", "## Problem Statement", "body"),
],
"Q-002": [ # Who is the user?
("context/product-brief.md", "## Users", "body"),
("context/company-context.md", "## Customers / Users", "body"),
("context/company-context.md", "## Key Stakeholders", "body"),
("context/development-context.md", "## IDEs and Editors", "body"),
],
"Q-003": [ # What does success look like?
("context/product-brief.md", "## Success Criteria", "body"),
("context/project-brief.md", "## Success Metrics", "body"),
],
"Q-004": [ # What is the minimum useful version?
("context/product-brief.md", "## Minimum Useful Version", "body"),
("context/project-brief.md", "## Key Features (MVP)", "body"),
],
"Q-005": [ # What data do we need?
("context/architecture.md", "## Data Flow", "body"),
("context/development-context.md", "## Dependencies", "body"),
],
"Q-006": [ # What systems must it connect to?
("context/architecture.md", "## External Integrations", "body"),
("context/infrastructure-context.md", None, "append"),
],
"Q-007": [ # What are the risks?
("context/risks.md", "# Risks", "table"),
],
"Q-008": [ # What must not happen?
("context/assumptions.md", "# Assumptions", "table"),
],
"Q-009": [ # How will we test it?
("context/development-context.md", "## Build & Test", "body"),
("context/agent-guidelines.md", "## Testing Preferences", "append"),
],
"Q-010": [ # How will it be deployed?
("context/architecture.md", "## Deployment Architecture", "body"),
("context/infrastructure-context.md", None, "append"),
],
"Q-011": [ # Who owns and directs the project, and what are its organisational goals?
("context/company-context.md", "## Mission", "body"),
("context/company-context.md", "## Products & Services", "body"),
("context/project-brief.md", "## Target Audience", "body"),
],
"Q-012": [ # What is the technology stack, repository structure, and architecture approach?
("context/development-context.md", "## Tech Stack", "body"),
("context/development-context.md", "## Coding Standards", "body"),
("context/repository-context.md", "## Dependencies", "body"),
("context/architecture.md", "## Overview", "body"),
("context/architecture.md", "## Core Components", "body"),
],
"Q-013": [ # Where is it hosted, which environments exist, and what are the security requirements?
("context/infrastructure-context.md", "## Hosting", "body"),
("context/infrastructure-context.md", "## Environments", "body"),
("context/infrastructure-context.md", "## Monitoring & Alerting", "body"),
("context/infrastructure-context.md", "## Security", "body"),
],
"Q-014": [ # What agent and developer workflow tools, constraints, and guidelines apply?
("context/agent-guidelines.md", "## Purpose", "body"),
("context/agent-guidelines.md", "## Preferred Tools", "body"),
("context/repository-context.md", "## Purpose", "body"),
("context/repository-context.md", "## Contributing", "body"),
],
}
def generate_context_files(
root: Path, min_confidence: str = "Medium"
) -> dict[str, list[str]]:
"""Generate context files from discovery answers.
Args:
root: Project root path.
min_confidence: Minimum confidence to process (High, Medium).
Returns:
Dict with keys: 'generated', 'skipped' each a list of strings.
"""
valid_confidences = {"High", "Medium"}
answers = read_discovery_answers(root)
if not answers:
return {"generated": [], "skipped": ["No discovery answers found in discovery-log.md"]}
filtered = [a for a in answers if a["confidence"] in valid_confidences]
skipped = [a for a in answers if a["confidence"] not in valid_confidences]
generated_files: set[str] = set()
for answer in filtered:
qid = answer["id"]
rules = CONTEXT_MAP.get(qid)
if not rules:
skipped.append(f"{qid} (no mapping)")
continue
for file_path, section_name, fmt in rules:
_write_section(
root=root,
file_path=file_path,
section_name=section_name,
format_type=fmt,
answer=answer,
rules=rules,
)
generated_files.add(file_path)
skipped_ids = [a["id"] for a in skipped]
if skipped_ids:
skipped = ["Low-confidence or flagged answers (skipped): " + ", ".join(skipped_ids)]
return {"generated": sorted(generated_files), "skipped": skipped}
def _write_section(root, file_path, section_name, format_type, answer, rules):
"""Dispatch to the appropriate write strategy."""
target = root / file_path
target.parent.mkdir(parents=True, exist_ok=True)
if format_type == "table":
_write_table_row(target, section_name, answer)
elif format_type == "append":
_append_new_section(target, answer, rules, file_path)
else:
# body text type
existing = target.read_text(encoding="utf-8") if target.exists() else ""
lines = existing.splitlines() if existing else []
section_idx = _find_section(lines, section_name) if section_name else -1
if section_idx >= 0:
_fill_or_append_body(lines, target, section_idx, answer)
else:
_create_new_section(target, section_name, answer, format_type)
def _find_section(lines, section_header):
"""Find the index of a section header line in the markdown."""
for i, line in enumerate(lines):
if line.strip() == section_header:
return i
return -1
def _fill_or_append_body(lines, target, section_idx, answer):
"""Fill or append body text under an existing section header.
Preserves the rest of the document - never drops subsequent sections.
"""
start = section_idx + 1
# Find another section header to know the boundary
end = len(lines)
for i in range(start, len(lines)):
if lines[i].startswith("## "):
end = i
break
content_lines = [l.strip() for l in lines[start:end] if l.strip()]
# Check if section is empty or only has TBD placeholder
is_tbd = (
len(content_lines) == 1 and content_lines[0].lower().startswith("tbd")
) or len(content_lines) == 0
answer_text = _format_answer_text(answer)
if is_tbd:
# Replace TBD with discovery answer, keep rest of document
new_lines = (
lines[:section_idx + 1]
+ [answer_text, ""]
+ lines[end:]
)
else:
# Append under existing content, after the last non-empty line in this section
insert_at = start
for i in range(start, end):
if lines[i].strip():
insert_at = i + 1
new_lines = (
lines[:insert_at]
+ [answer_text, ""]
+ lines[insert_at:]
)
target.write_text("\n".join(new_lines), encoding="utf-8")
def _write_table_row(target, section_header, answer):
"""Append a row to a table-based context file."""
existing = target.read_text(encoding="utf-8") if target.exists() else ""
q_num = answer["id"].split("-")[1]
all_questions = [
"What problem are we solving?",
"Who is the user?",
"What does success look like?",
"What is the minimum useful version?",
"What data do we need?",
"What systems must it connect to?",
"What are the risks?",
"What must not happen?",
"How will we test it?",
"How will it be deployed?",
"Who owns and directs the project, and what are its organisational goals?",
"What is the technology stack, repository structure, and architecture approach?",
"Where is it hosted, which environments exist, and what are the security requirements?",
"What agent and developer workflow tools, constraints, and guidelines apply?",
]
question_text = all_questions[int(q_num) - 1] if int(q_num) <= len(all_questions) else answer["question"]
fname = target.name
if fname == "risks.md":
risk_id = f"RISK-{q_num}"
clean = answer["answer"].replace("\\|", "|").strip()
row = f"| {risk_id} | {question_text}: {clean} | Medium | Monitor and review | Open |\n"
elif fname == "assumptions.md":
assump_id = f"ASSUMPTION-{q_num}"
clean = answer["answer"].replace("\\|", "|").strip()
row = f"| {assump_id} | {question_text}: {clean} | Medium | Yes |\n"
else:
row = f"| {answer['id']} | {question_text} | {answer['answer']} | Open |\n"
target.write_text(existing + "\n" + row, encoding="utf-8")
def _append_new_section(target, answer, rules, file_path):
"""Append a new section to the end of an existing file."""
existing = target.read_text(encoding="utf-8") if target.exists() else ""
display_name = None
for fpath, sec, _ in rules:
if fpath == file_path and sec and sec.startswith("## "):
display_name = sec
break
if not display_name:
q_num = answer["id"].split("-")[1]
display_name = f"## Q-{q_num} Discovery Answer"
section_text = (
f"\n{display_name}\n\n"
f"**Discovery Question:** {answer['question']}\n\n"
f"**Answer:** {answer['answer'].strip()}\n\n"
f"**Confidence:** {answer['confidence']}\n"
)
target.write_text(existing + section_text, encoding="utf-8")
def _create_new_section(target, header, answer, fmt):
"""Create a new section in a context file when it doesn't exist yet."""
existing = target.read_text(encoding="utf-8") if target.exists() else ""
if not header:
q_num = answer["id"].split("-")[1]
header = f"## Q-{q_num} Discovery Answer"
section_text = (
f"\n{header}\n\n"
f"**Discovery Question:** {answer['question']}\n\n"
f"**Answer:** {answer['answer'].strip()}\n\n"
f"**Confidence:** {answer['confidence']}\n"
)
target.write_text(existing + section_text, encoding="utf-8")
def _format_answer_text(answer):
"""Format a discovery answer into readable body text."""
return answer["answer"].replace("\\|", "|").strip()
+289
View File
@@ -0,0 +1,289 @@
from __future__ import annotations
from collections import Counter
from datetime import datetime, timedelta
import re
from pathlib import Path
from .telemetry import read_events
def _read_safe(root: Path, relative: str) -> str | None:
"""Read a file if it exists, otherwise return None."""
path = root / relative
if path.exists():
return path.read_text(encoding="utf-8")
return None
def _last_modified(root: Path, relative: str) -> datetime | None:
"""Return the last-modified time of a file, or None."""
path = root / relative
if path.exists():
ts = path.stat().st_mtime
return datetime.fromtimestamp(ts)
return None
def _now() -> datetime:
return datetime.now()
# ---------------------------------------------------------------------------
# Individual guardrail checks
# ---------------------------------------------------------------------------
def check_repeated_reads(root: Path) -> dict:
"""Detect files that appear to be read repeatedly in AGENT_HANDOFF.md or project context.
Returns a dict with keys: status, details.
- status: 'ok', 'warning', 'error'
- details: human-readable explanation
"""
handoff_text = _read_safe(root, "AGENT_HANDOFF.md")
prompt_log = _read_safe(root, ".rdb/prompt-history.md") # optional
text_parts = []
if handoff_text is not None:
text_parts.append(handoff_text)
if prompt_log is not None:
text_parts.append(prompt_log)
if not text_parts:
return {"status": "ok", "details": "No agent history files to analyse."}
combined = "\n".join(text_parts)
file_mentions = re.findall(r"[A-Za-z0-9_/.-]+\.md", combined)
counts = Counter(file_mentions)
repeated = {path: count for path, count in counts.items() if count > 2}
if not repeated:
return {"status": "ok", "details": "No files appear to be read excessively."}
top_files = ", ".join(f"{p} ({c}x)" for p, c in list(repeated.items())[:5])
return {
"status": "warning",
"details": f"Files mentioned more than twice: {top_files}",
}
def check_repeated_commands(root: Path) -> dict:
"""Detect repeated command entries in RUN_LOG.md.
Returns a dict with keys: status, details.
"""
run_log = _read_safe(root, "RUN_LOG.md")
if not run_log:
return {"status": "ok", "details": "No RUN_LOG.md found."}
lines = [l.strip() for l in run_log.splitlines() if l.startswith("|")]
events = []
for line in lines:
parts = [p.strip() for p in line.split("|")][1:-1]
if len(parts) >= 2:
events.append(f"{parts[0]}|{parts[1]}")
counts = Counter(events)
repeated = {k: v for k, v in counts.items() if v > 2}
if not repeated:
return {"status": "ok", "details": "No significantly repeated commands detected."}
items = ", ".join(f"'{k}' ({v}x)" for k, v in list(repeated.items())[:5])
return {
"status": "warning",
"details": f"Repeated entries: {items}",
}
def check_repeated_commands_telemetry(root: Path) -> dict:
"""Detect repeated commands from telemetry events.
Reads structured session-log.jsonl to find command targets
that have been issued more than 3 times. Falls back gracefully
when the telemetry log is absent.
Returns a dict with keys: status, details.
"""
events = read_events(root)
if not events:
return {"status": "ok", "details": "No telemetry data available."}
command_events = [e for e in events if e.event_type == "command"]
if not command_events:
return {"status": "ok", "details": "No command events found in telemetry."}
counts = Counter(e.target for e in command_events)
repeated = {target: count for target, count in counts.items() if count > 3}
if not repeated:
return {"status": "ok", "details": "No significantly repeated commands detected in telemetry."}
items = ", ".join(f"{t} ({c}x)" for t, c in list(repeated.items())[:5])
return {
"status": "warning",
"details": f"Repeated commands: {items}",
}
def check_repeated_reads_telemetry(root: Path) -> dict:
"""Detect repeated file reads from telemetry events.
Reads structured session-log.jsonl to find read/read_file target events
that have been issued more than 3 times. Falls back gracefully
when the telemetry log is absent.
Returns a dict with keys: status, details.
"""
events = read_events(root)
if not events:
return {"status": "ok", "details": "No telemetry data available."}
read_events_list = [e for e in events if e.event_type in ("read", "read_file")]
if not read_events_list:
return {"status": "ok", "details": "No file read events found in telemetry."}
counts = Counter(e.target for e in read_events_list)
repeated = {target: count for target, count in counts.items() if count > 3}
if not repeated:
return {"status": "ok", "details": "No significantly repeated file reads detected in telemetry."}
items = ", ".join(f"{t} ({c}x)" for t, c in list(repeated.items())[:5])
return {
"status": "warning",
"details": f"Repeated file reads: {items}",
}
def check_no_recent_file_changes(root: Path) -> dict:
"""Flag if project files have not been modified in a long time (> 48 h).
Returns a dict with keys: status, details.
"""
monitored = [
"TASKS.md",
"PROJECT_STATE.md",
"AGENT_HANDOFF.md",
"context/product-brief.md",
"context/architecture.md",
]
now = _now()
stale: list[str] = []
for rel in monitored:
ts = _last_modified(root, rel)
if ts is None:
continue
age = now - ts
if age > timedelta(hours=48):
stale.append(rel)
if not stale:
return {"status": "ok", "details": "Project files have recent updates."}
return {
"status": "warning",
"details": f"No changes in the last 48 hours: {', '.join(stale)}",
}
def check_no_test_run_recorded(root: Path) -> dict:
"""Flag if RUN_LOG.md does not mention tests or pytest.
Returns a dict with keys: status, details.
"""
run_log = _read_safe(root, "RUN_LOG.md")
if not run_log:
return {"status": "ok", "details": "No RUN_LOG.md found — can't check."}
lower = run_log.lower()
keywords = ["pytest", "test passed", "tests passed", "all tests"]
if any(kw in lower for kw in keywords):
return {"status": "ok", "details": "Test activity recorded."}
return {
"status": "warning",
"details": "No test execution recorded in RUN_LOG.md.",
}
def check_run_log_updated(root: Path) -> dict:
"""Check whether TASKS.md status changes are reflected in RUN_LOG.md.
Returns a dict with keys: status, details.
"""
tasks_text = _read_safe(root, "TASKS.md")
run_log = _read_safe(root, "RUN_LOG.md")
if not tasks_text or not run_log:
return {"status": "ok", "details": "Cannot compare — missing files."}
log_lines = [l.strip() for l in run_log.splitlines() if l.startswith("|")]
if not log_lines:
return {
"status": "warning",
"details": "RUN_LOG.md exists but contains no log entries.",
}
# Check done tasks have corresponding RUN_LOG entries
done_tasks = re.findall(r"(TASK-\d+).*?Status:\s*Done", tasks_text, re.DOTALL)
for task_id in done_tasks:
found = any(task_id in line for line in log_lines)
if not found:
return {
"status": "warning",
"details": f"Task {task_id} marked Done but no corresponding RUN_LOG entry found.",
}
return {"status": "ok", "details": "TASKS.md and RUN_LOG.md appear consistent."}
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def run_all_guardrails(root: Path) -> dict:
"""Run all guardrail checks and return combined results."""
checks = {
"Repeated reads (heuristics)": check_repeated_reads(root),
"Repeated commands (RUN_LOG)": check_repeated_commands(root),
"Repeated commands (telemetry)": check_repeated_commands_telemetry(root),
"Repeated file reads (telemetry)": check_repeated_reads_telemetry(root),
"No recent file changes": check_no_recent_file_changes(root),
"No test run recorded": check_no_test_run_recorded(root),
"Run log updated": check_run_log_updated(root),
}
statuses = [v["status"] for v in checks.values()]
if "error" in statuses:
overall = "action required"
elif "warning" in statuses:
overall = "review recommended"
else:
overall = "all clear"
return {"checks": checks, "overall_status": overall}
def format_report(results: dict) -> str:
"""Format guardrail results as a human-readable report."""
lines = ["# Guardrail Report", ""]
for name, result in results["checks"].items():
icon_map = {"ok": "[green]✓[/green]", "warning": "[yellow]⚠[/yellow]", "error": "[red]✗[/red]"}
icon = icon_map.get(result["status"], "?")
lines.append(f"- {icon} **{name}:** {result['details']}")
status_icon_map = {
"all clear": "[green]✓[/green]",
"review recommended": "[yellow]⚠[/yellow]",
"action required": "[red]✗[/red]",
}
icon = status_icon_map.get(results["overall_status"], "?")
lines.append("")
lines.append(f"**Overall status:** {icon} {results['overall_status']}")
return "\n".join(lines)
+37
View File
@@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
from datetime import date
from pathlib import Path from pathlib import Path
import re
from .tasks import read_tasks from .tasks import read_tasks
@@ -23,3 +25,38 @@ def task_counts(root: Path) -> dict[str, int]:
for task in read_tasks(root): for task in read_tasks(root):
counts[task.status] = counts.get(task.status, 0) + 1 counts[task.status] = counts.get(task.status, 0) + 1
return counts return counts
_CURRENT_TASK_RE = re.compile(r"^(Current Task): .+$", re.MULTILINE)
_UPDATED_RE = re.compile(r"^(Last Updated): .+$", re.MULTILINE)
def update_project_state(root: Path, task_id: str = "None") -> bool:
"""Update PROJECT_STATE.md current task and timestamp."""
path = root / "PROJECT_STATE.md"
if not path.exists():
return False
text = path.read_text(encoding="utf-8")
text = _CURRENT_TASK_RE.sub(f"Current Task: {task_id}", text, count=1)
text = _UPDATED_RE.sub(f"Last Updated: {date.today().isoformat()}", text, count=1)
path.write_text(text, encoding="utf-8")
return True
_STAGE_RE = re.compile(r"(## Current Stage\n\n)[\s\S]+?(?=\n)", re.MULTILINE | re.DOTALL)
_TASK_RE = re.compile(r"(## Current Task\n\n)[\s\S]+?(?=\n)", re.MULTILINE | re.DOTALL)
def update_agent_handoff(root: Path, task_id: str = "None") -> bool:
"""Update AGENT_HANDOFF.md current stage and task."""
path = root / "AGENT_HANDOFF.md"
if not path.exists():
return False
text = path.read_text(encoding="utf-8")
stage = project_stage(root)
text = _STAGE_RE.sub(f"## Current Stage\n\n{stage}", text, count=1)
text = _TASK_RE.sub(f"## Current Task\n\n{task_id}", text, count=1)
path.write_text(text, encoding="utf-8")
return True
+157 -12
View File
@@ -7,9 +7,19 @@ import re
TASK_HEADING_RE = re.compile(r"^##\s+(TASK-\d+)\s+[-–—]\s+(.+)$", re.MULTILINE) TASK_HEADING_RE = re.compile(r"^##\s+(TASK-\d+)\s+[-–—]\s+(.+)$", re.MULTILINE)
STATUS_RE = re.compile(r"^Status:\s*(.+)$", re.MULTILINE) STATUS_RE = re.compile(r"^Status:\s*(.+)$", re.MULTILINE)
GOAL_RE = re.compile(r"^Goal:\s*(.+)", re.MULTILINE) GOAL_RE = re.compile(r"^Goal:\s*(.+)", re.MULTILINE)
AC_RE = re.compile(r"^- (.+)$", re.MULTILINE) AC_RE = re.compile(r"^- (.+)$", re.MULTILINE)
GAP_RE = re.compile(r"^Implementation Gap:\s*(.+)", re.MULTILINE)
def _indefinite_article(word: str) -> str:
"""Return 'a' or 'an' based on the first letter of word."""
if not word:
return "an"
first = word[0].lower()
if first in "aeiou":
return "an"
return "a"
@dataclass @dataclass
@@ -19,6 +29,15 @@ class Task:
status: str status: str
body: str body: str
def role(self, root: Path) -> tuple[str, str]:
if "Role:" not in self.body:
return ("Implementation Agent", _indefinite_article("Implementation Agent"))
parts = self.body.split("Role:", 1)
value = parts[1].splitlines()[0].strip()
if value:
return (value, _indefinite_article(value))
return ("Implementation Agent", _indefinite_article("Implementation Agent"))
def goal(self, root: Path) -> str: def goal(self, root: Path) -> str:
match = GOAL_RE.search(self.body) match = GOAL_RE.search(self.body)
if match: if match:
@@ -28,6 +47,12 @@ class Task:
def ac_lines(self) -> list[str]: def ac_lines(self) -> list[str]:
return AC_RE.findall(self.body) return AC_RE.findall(self.body)
def implementation_gap(self, root: Path) -> str | None:
match = GAP_RE.search(self.body)
if match and match.group(1).strip():
return match.group(1).strip()
return None
def read_tasks(root: Path) -> list[Task]: def read_tasks(root: Path) -> list[Task]:
tasks_path = root / "TASKS.md" tasks_path = root / "TASKS.md"
@@ -84,7 +109,7 @@ def update_task_status(root: Path, task_id: str, new_status: str) -> bool:
return False return False
CONTEXT_FILES_TO_READ = [ DEFAULT_CONTEXT_FILES = [
"README.md", "README.md",
"TASKS.md", "TASKS.md",
"PROJECT_STATE.md", "PROJECT_STATE.md",
@@ -92,6 +117,44 @@ CONTEXT_FILES_TO_READ = [
"context/agent-guidelines.md", "context/agent-guidelines.md",
] ]
ROLE_CONTEXT_FILES: dict[str, list[str]] = {
"Architecture Agent": [
"README.md",
"TASKS.md",
"PROJECT_STATE.md",
"context/architecture.md",
"context/product-brief.md",
"context/decisions.md",
],
"Implementation Agent": [
"README.md",
"TASKS.md",
"PROJECT_STATE.md",
"AGENT_HANDOFF.md",
"context/agent-guidelines.md",
"CLAUDE.md",
],
"Documentation Agent": [
"README.md",
"TASKS.md",
"PROJECT_STATE.md",
"AGENT_HANDOFF.md",
"context/agent-guidelines.md",
"TEST_PLAN.md",
],
}
def _context_files_for_role(role: str) -> list[str]:
"""Return the context file list for a given role, falling back to default."""
return ROLE_CONTEXT_FILES.get(role, DEFAULT_CONTEXT_FILES)
TEST_CMD_SECTION_RE = re.compile(
r"^##\s*Test Commands\s*\n([\s\S]*?)(?=^##|\Z)",
re.MULTILINE,
)
BASH_BLOCK_RE = re.compile(r"```bash\n(.*?)```", re.DOTALL)
def _read_file_safe(root: Path, relative: str) -> str: def _read_file_safe(root: Path, relative: str) -> str:
path = root / relative path = root / relative
@@ -100,25 +163,79 @@ def _read_file_safe(root: Path, relative: str) -> str:
return f"# {relative} — not found" return f"# {relative} — not found"
def _extract_test_commands_from_claude(root: Path) -> list[str]:
"""Extract bash test commands from CLAUDE.md Test Commands section."""
def _looks_like_command(line: str) -> bool:
"""Heuristic: keep lines that look like shell commands."""
if not line.strip():
return False
lower = line.lower()
# Skip markdown, prose, headings
if any(lower.startswith(p) for p in ("# ", "- ", "if ", "do not", "for ", "use ")) or lower in (
"validation commands",
):
return False
# Skip lines that look like sentences (contain spaces followed by lowercase words)
# but keep things like `source .venv/bin/activate` and `pip install ...`
parts = line.split()
if len(parts) <= 1:
return True
# If the first word is a known shell builtin / command prefix, accept it
known_prefixes = ("source", "cd", "ls", "cp", "mv", "rm", "mkdir", "echo", "grep",
"git", "pip", "python", "pytest", "rdb", "cat", "head", "tail",
"sed", "awk", "find", "install")
return parts[0].lower() in known_prefixes
claude_path = root / "CLAUDE.md"
if not claude_path.exists():
return []
text = claude_path.read_text(encoding="utf-8")
section_match = TEST_CMD_SECTION_RE.search(text)
if not section_match:
return []
section_text = section_match.group(1)
blocks = BASH_BLOCK_RE.findall(section_text)
seen: set[str] = set()
commands: list[str] = []
for block in blocks:
for line in block.strip().splitlines():
stripped = line.strip()
if not stripped:
continue
if _looks_like_command(stripped):
if stripped not in seen:
seen.add(stripped)
commands.append(stripped)
return commands
def generate_agent_prompt(root: Path) -> str: def generate_agent_prompt(root: Path) -> str:
task = get_next_task(root) task = get_next_task(root)
if not task: if not task:
return "No Todo task found." return "No Todo task found."
role_text, article = task.role(root)
context_files = _context_files_for_role(role_text)
sections = [ sections = [
"You are an implementation agent.", f"You are {article} {role_text.lower()}.",
"", "",
"---", "---",
"", "",
"# Read these files first", "# Read these files first",
"", "",
f"""- README.md ]
- TASKS.md for i, f in enumerate(context_files):
- PROJECT_STATE.md line = f"- {f}"
- AGENT_HANDOFF.md""", if i == len(context_files) - 1:
"", sections.append(line)
"- context/agent-guidelines.md", else:
"", sections.append(line)
sections.extend([
"---", "---",
"", "",
"# Task", "# Task",
@@ -128,9 +245,20 @@ def generate_agent_prompt(root: Path) -> str:
f"Status: {task.status}", f"Status: {task.status}",
"", "",
f"Goal:\n{task.goal(root)}", f"Goal:\n{task.goal(root)}",
])
gap = task.implementation_gap(root)
if gap:
sections.extend([
"",
"Implementation Gap:",
gap,
])
sections.extend([
"", "",
"Acceptance Criteria:", "Acceptance Criteria:",
] ])
for line in task.ac_lines(): for line in task.ac_lines():
sections.append(f"- {line}") sections.append(f"- {line}")
@@ -141,8 +269,25 @@ def generate_agent_prompt(root: Path) -> str:
"", "",
"# Constraints", "# Constraints",
"", "",
"- Implement ONE task only. Do not combine with other tasks.", "- Inspect the codebase first, then edit.",
"- Do not repeatedly reread unchanged files.",
"- Limit your work to ONE small implementation step only. Do not combine tasks or features.",
"- Make the smallest useful change possible.", "- Make the smallest useful change possible.",
])
test_cmds = _extract_test_commands_from_claude(root)
if test_cmds:
sections.extend([
"",
"---",
"",
"# Test Commands (from CLAUDE.md)",
"",
"Use these existing test commands:",
("```bash\n" + "\n".join(test_cmds) + "\n```\n"),
])
sections.extend([
"", "",
"---", "---",
"", "",
+78
View File
@@ -0,0 +1,78 @@
from __future__ import annotations
import json
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
@dataclass
class TelemetryEvent:
"""A single structured telemetry event."""
timestamp: str
event_type: str
target: str
details: dict = field(default_factory=dict)
def to_dict(self) -> dict:
return asdict(self)
def _session_log_path(root: Path) -> Path:
"""Return the path to the session-log.jsonl file under .rdb/."""
rdb_dir = root / ".rdb"
rdb_dir.mkdir(parents=True, exist_ok=True)
return rdb_dir / "session-log.jsonl"
def record_event(root: Path, event_type: str, target: str, details: dict | None = None) -> TelemetryEvent:
"""Record a telemetry event to the session log.
Creates the .rdb/session-log.jsonl file if it does not exist.
Each call appends one JSON line to the file.
Args:
root: Project root directory.
event_type: Category of event, e.g. 'command', 'task', 'guardrail'.
target: The entity the event relates to, e.g. 'rdb prompt'.
details: Optional extra key-value pairs for context.
Returns:
The TelemetryEvent that was recorded.
"""
event = TelemetryEvent(
timestamp=datetime.now(timezone.utc).isoformat(),
event_type=event_type,
target=target,
details=details or {},
)
log_path = _session_log_path(root)
with open(log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(event.to_dict()) + "\n")
return event
def read_events(root: Path) -> list[TelemetryEvent]:
"""Read all events from the session log.
Returns an empty list if the log does not yet exist or is empty.
Malformed lines are silently skipped.
"""
log_path = _session_log_path(root)
if not log_path.exists():
return []
events: list[TelemetryEvent] = []
for line in log_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
data = json.loads(line)
events.append(TelemetryEvent(**data))
except (json.JSONDecodeError, TypeError):
pass
return events
+136 -18
View File
@@ -13,6 +13,10 @@ CORE_QUESTIONS = [
"What must not happen?", "What must not happen?",
"How will we test it?", "How will we test it?",
"How will it be deployed?", "How will it be deployed?",
"Who owns and directs the project, and what are its organisational goals?",
"What is the technology stack, repository structure, and architecture approach?",
"Where is it hosted, which environments exist, and what are the security requirements?",
"What agent and developer workflow tools, constraints, and guidelines apply?",
] ]
CONTEXT_FILES: dict[str, str] = { CONTEXT_FILES: dict[str, str] = {
@@ -37,24 +41,6 @@ TBD
## Minimum Useful Version ## Minimum Useful Version
TBD
""",
"context/architecture.md": """# Architecture
## Overview
TBD
## Components
TBD
## Integrations
TBD
## Deployment
TBD TBD
""", """,
"context/decisions.md": """# Decisions "context/decisions.md": """# Decisions
@@ -127,6 +113,138 @@ DISCOVERY
| ID | Question | Reason | Owner | Status | | ID | Question | Reason | Owner | Status |
|---|---|---|---|---| |---|---|---|---|---|
""",
"context/company-context.md": """# Company Context
## Mission
TBD What is the organisation's core mission?
## Products & Services
TBD List of products and services offered.
## Customers / Users
TBD Who are the primary customers or users?
## Brand & Positioning
TBD How does the company position itself in the market?
## Key Stakeholders
TBD Names, roles, and contact information.
""",
"context/development-context.md": """# Development Context
## Tech Stack
TBD Languages, frameworks, libraries, and tooling.
## Coding Standards
TBD Style guide conventions, naming patterns, and linting rules.
## Repository Structure
TBD Overview of directory layout and module organisation.
## Build & Test
TBD How to build, test, and run the codebase locally.
## Dependencies
TBD External services, databases, and third-party APIs required.
""",
"context/infrastructure-context.md": """# Infrastructure Context
## Hosting
TBD Where is the application hosted (cloud provider, on-prem, etc.)?
## Environments
TBD Development, staging, production environment details.
## CI / CD Pipeline
TBD Build, test, and deployment pipeline configuration.
## Monitoring & Alerting
TBD Tools used for monitoring, logging, and alerting.
## Security
TBD Authentication, data protection, and access control measures.
""",
"context/agent-guidelines.md": """# Agent Guidelines
## Purpose
TBD What should agents know about working with this project?
## Preferred Tools
TBD Recommended editors, debuggers, testing frameworks, and CLI tools.
## Testing Preferences
TBD How the team prefers tests to be written, run, and verified.
## Common Tasks
TBD Typical workflows for developers and AI coding agents.
## Known Gotchas
TBD Pitfalls, quirks, or important caveats to be aware of.
""",
"context/project-brief.md": """# Project Brief
## Problem Statement
TBD What problem does this project solve?
## Target Audience
TBD Who is the intended audience?
## Key Features (MVP)
TBD Minimum set of features for the first release.
## Success Metrics
TBD How will success be measured?
## Timeline & Milestones
TBD Expected delivery dates and key milestones.
""",
"context/architecture.md": """# Architecture
## Overview
TBD High-level system description and design principles.
## Core Components
TBD Key modules, services, and their responsibilities.
## Data Flow
TBD How data moves through the system (APIs, queues, databases).
## External Integrations
TBD Third-party services, APIs, and dependencies.
## Deployment Architecture
TBD Infrastructure layout, scaling strategy, and deployment process.
""", """,
"TASKS.md": """# TASKS.md "TASKS.md": """# TASKS.md
+431
View File
@@ -0,0 +1,431 @@
"""Tests for context health report (TASK-021)."""
import pytest
from pathlib import Path
from textwrap import dedent
def _get_project_root() -> Path:
current = Path(__file__).resolve().parent.parent
while current != current.parent:
if (current / "pyproject.toml").exists() or (current / ".git").exists():
return current
current = current.parent
return Path.cwd()
@pytest.fixture()
def tmp_project(tmp_path):
"""Create a minimal project root with context files and discovery answers."""
root = tmp_path / "testproject"
root.mkdir()
(root / "context").mkdir()
# Write discovery-log.md with various confidence levels
(root / "context" / "discovery-log.md").write_text(
"# Discovery Log\n\n"
"| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n"
"|---|---|---|---|---|---|---|---|\n"
'| Q-001 | What problem are we solving? | A CLI tool for discovery workflow | High | No | | | 2026-06-03 |\n'
'| Q-002 | Who is the user? | Internal developers and small dev teams | Medium | No | | | 2026-06-03 |\n'
'| Q-008 | What must not happen? | No third-party SaaS dependencies | Low | No | | | 2026-06-03 |\n',
encoding="utf-8",
)
# Write product-brief.md with TBD placeholders
(root / "context" / "product-brief.md").write_text(
"# Product Brief\n\n## Problem\n\nTBD\n\n## Users\n\nTBD\n\n## Success Criteria\n\nDone\n",
encoding="utf-8",
)
# Write architecture.md with some TBDs
(root / "context" / "architecture.md").write_text(
"# Architecture\n\n## Overview\n\nTDB — Not yet defined.\n\n## Core Components\n\nTBD\n\n## Data Flow\n\nDone\n",
encoding="utf-8",
)
# Write decisions.md (present, non-empty)
(root / "context" / "decisions.md").write_text(
"# Decisions\n\n| ID | Decision | Reason | Date |\n|---|---|---|---|\n",
encoding="utf-8",
)
# Write risks.md and assumptions.md
(root / "context" / "risks.md").write_text(
"# Risks\n\n| ID | Risk | Impact | Mitigation | Status |\n|---|---|---|---|---|\n",
encoding="utf-8",
)
(root / "context" / "assumptions.md").write_text(
"# Assumptions\n\n| ID | Assumption | Confidence | Validation Needed |\n|---|---|---|---|\n",
encoding="utf-8",
)
# Write open-questions.md (present)
(root / "context" / "open-questions.md").write_text(
"# Open Questions\n\n| ID | Question | Reason | Owner | Status |\n|---|---|---|---|---|\n",
encoding="utf-8",
)
# Write company-context.md, development-context.md, infrastructure-context.md with TBDs
(root / "context" / "company-context.md").write_text(
"# Company Context\n\n## Mission\n\nTBD\n\n## Products & Services\n\nDone.\n",
encoding="utf-8",
)
(root / "context" / "development-context.md").write_text(
"# Development Context\n\n## Tech Stack\n\nPython 3.12, Typer, Rich.\n\n## Coding Standards\n\nTBD\n",
encoding="utf-8",
)
(root / "context" / "infrastructure-context.md").write_text(
"# Infrastructure Context\n\n## Hosting\n\nTBD — Cloud provider details.\n",
encoding="utf-8",
)
# Write agent-guidelines.md and repository-context.md (present)
(root / "context" / "agent-guidelines.md").write_text("# Agent Guidelines\n\nDone.\n", encoding="utf-8")
(root / "context" / "repository-context.md").write_text("# Repository Context\n\nPurpose: Test project.\n", encoding="utf-8")
# Write PROJECT_STATE.md, TASKS.md, TEST_PLAN.md, RUN_LOG.md, AGENT_HANDOFF.md
(root / "PROJECT_STATE.md").write_text("# Project State\n\nCurrent Stage: BUILDING\n", encoding="utf-8")
(root / "TASKS.md").write_text("# TASKS.md\n## TASK-001\nStatus: Done\n", encoding="utf-8")
(root / "TEST_PLAN.md").write_text("# TEST_PLAN.md\n## Tests\nDone.\n", encoding="utf-8")
(root / "RUN_LOG.md").write_text("# RUN_LOG.md\n| Date | Event | Task | Notes |\n|---|---|---|---|\n", encoding="utf-8")
(root / "AGENT_HANDOFF.md").write_text("# Agent Handoff\n## Current Stage\nBUILDING\n", encoding="utf-8")
# Create .rdb directory
(root / ".rdb").mkdir()
return root
class TestExpectedFiles:
"""Tests for expected file reporting."""
def test_all_expected_files_returned(self, tmp_project):
"""Every standard context file should appear in the expected list."""
from rdb_discovery.context_status import _expected_files
files = _expected_files(tmp_project)
assert len(files) == 18
def test_missing_file_detected(self, tmp_project):
"""Files that don't exist should be marked as missing."""
# project-brief.md is not created by this fixture
(tmp_project / "context" / "project-brief.md").unlink(missing_ok=True)
from rdb_discovery.context_status import _check_expected_files
results = _check_expected_files(tmp_project)
paths_by_status = {r["path"]: r["status"] for r in results}
assert paths_by_status.get("context/project-brief.md") == "missing"
def test_present_file_detected(self, tmp_project):
"""Files that exist should be marked as present."""
from rdb_discovery.context_status import _check_expected_files
results = _check_expected_files(tmp_project)
paths_by_status = {r["path"]: r["status"] for r in results}
assert paths_by_status["TASKS.md"] == "present"
assert paths_by_status["PROJECT_STATE.md"] == "present"
class TestTbdDetection:
"""Tests for TBD placeholder detection."""
def test_tbd_sections_detected(self, tmp_project):
"""Sections with TBD/TDB placeholders should be reported."""
from rdb_discovery.context_status import _check_tbd_sections
findings = _check_tbd_sections(tmp_project)
assert len(findings) > 0
paths = {f["file"] for f in findings}
assert "product-brief.md" in paths # has TBD sections
assert "architecture.md" in paths # has TDB section
def test_no_tbd_in_clean_files(self, tmp_project):
"""Files without TBD placeholders should not appear."""
from rdb_discovery.context_status import _check_tbd_sections
findings = _check_tbd_sections(tmp_project)
# TASKS.md has "Status: Done" — no TBD
for f in findings:
assert f["file"] != "TASKS.md" or not any(
"TBD" in str(getattr(f, "placeholder", "")) or "TDB" in str(getattr(f, "placeholder", ""))
for _ in [1]
)
def test_tbd_sections_returns_empty_for_no_context_dir(self):
"""Should return empty list when no context dir exists."""
from rdb_discovery.context_status import _check_tbd_sections
findings = _check_tbd_sections(_get_project_root() / "nonexistent")
assert findings == []
class TestLowConfidence:
"""Tests for low-confidence discovery answer detection."""
def test_low_confidence_detected(self, tmp_project):
"""Low-confidence answers should be detected."""
from rdb_discovery.context_status import _check_low_confidence
low = _check_low_confidence(tmp_project)
assert len(low) == 1
assert low[0]["id"] == "Q-008"
assert low[0]["confidence"] == "Low"
def test_high_confidence_not_flagged(self, tmp_project):
"""High-confidence answers should not appear in low_confidence results."""
from rdb_discovery.context_status import _check_low_confidence
low = _check_low_confidence(tmp_project)
ids = {a["id"] for a in low}
assert "Q-001" not in ids # Q-001 is High confidence
def test_medium_confidence_not_flagged(self, tmp_project):
"""Medium-confidence answers should not appear in low_confidence results."""
from rdb_discovery.context_status import _check_low_confidence
low = _check_low_confidence(tmp_project)
ids = {a["id"] for a in low}
assert "Q-002" not in ids # Q-002 is Medium confidence
class TestHealthScore:
"""Tests for health score computation."""
def test_full_score_when_everything_complete(self):
"""Score should be near 100 when all files present, no TBDs, no low-conf."""
from rdb_discovery.context_status import compute_health_score
expected = [{"path": "a", "status": "present", "size": "0"} for _ in range(18)]
score = compute_health_score(expected, tbd_sections=0, low_conf_count=0)
# 45 (files) + 30 (no TBD) + 15 (no low-conf) = 90, discovery bonus later adds to it
assert score >= 90
def test_score_degrades_with_missing_files(self):
"""Score should decrease as files are missing."""
from rdb_discovery.context_status import compute_health_score
full = [
{"path": f"file{i}", "status": "present", "size": "0"} for i in range(18)
]
score_full = compute_health_score(full, tbd_sections=0, low_conf_count=0)
partial = [
{"path": "a", "status": "present", "size": "0"},
{"path": "b", "status": "missing", "size": "0"},
{"path": "c", "status": "missing", "size": "0"},
] + [
{"path": f"file{i}", "status": "present", "size": "0"} for i in range(15)
]
score_partial = compute_health_score(partial, tbd_sections=0, low_conf_count=0)
assert score_full > score_partial
def test_score_degrades_with_tbd_sections(self):
"""Score should decrease as TBD sections increase."""
from rdb_discovery.context_status import compute_health_score
expected = [{"path": "a", "status": "present", "size": "0"} for _ in range(18)]
score_0 = compute_health_score(expected, tbd_sections=0, low_conf_count=0)
score_5 = compute_health_score(expected, tbd_sections=5, low_conf_count=0)
score_20 = compute_health_score(expected, tbd_sections=20, low_conf_count=0)
assert score_0 > score_5
assert score_5 > score_20
# At 20+ TBDs, TBD score component is 0
assert score_20 <= score_5 - 15
class TestContextStatus:
"""Tests for the main context_status function."""
def test_returns_score(self, tmp_project):
"""context_status should return a score in the report."""
from rdb_discovery.context_status import context_status
report = context_status(tmp_project)
assert "score" in report
assert 0 <= report["score"] <= 110 # allows for discovery bonus
def test_returns_summary(self, tmp_project):
"""Report should contain a summary dict."""
from rdb_discovery.context_status import context_status
report = context_status(tmp_project)
assert "summary" in report
assert "total_expected" in report["summary"]
assert "missing" in report["summary"]
def test_returns_expected_files(self, tmp_project):
"""Report should list all expected files."""
from rdb_discovery.context_status import context_status
report = context_status(tmp_project)
assert len(report["expected_files"]) == 18
def test_returns_tbd_sections(self, tmp_project):
"""Report should include TBD section findings."""
from rdb_discovery.context_status import context_status
report = context_status(tmp_project)
assert "tbd_sections" in report
assert len(report["tbd_sections"]) > 0
def test_returns_low_confidence(self, tmp_project):
"""Report should include low-confidence answers."""
from rdb_discovery.context_status import context_status
report = context_status(tmp_project)
assert "low_confidence" in report
assert len(report["low_confidence"]) == 1
class TestCLICommand:
"""Tests that the CLI 'context-status' command is available and works."""
def test_context_status_command_exists(self):
"""The 'rdb context-status' command should be registered."""
from typer.testing import CliRunner
from rdb_discovery.cli import app
runner = CliRunner()
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "context-status" in result.output.lower()
def test_context_status_runs_successfully(self):
"""The CLI command should exit cleanly."""
from typer.testing import CliRunner
from rdb_discovery.cli import app
runner = CliRunner()
result = runner.invoke(app, ["context-status"], catch_exceptions=False)
assert result.exit_code == 0
def test_context_status_output_contains_score(self):
"""The output should contain the health score."""
from typer.testing import CliRunner
from rdb_discovery.cli import app
runner = CliRunner()
result = runner.invoke(app, ["context-status"], catch_exceptions=False)
assert result.exit_code == 0
# Score is in format like "92.5/100"
assert "/100" in result.output
def test_context_status_reports_missing_files(self):
"""Missing files should be detected by the core function."""
from pathlib import Path
import tempfile
with tempfile.TemporaryDirectory() as td:
root = Path(td) / "proj"
root.mkdir()
(root / "context").mkdir()
# No context files created — everything missing
from rdb_discovery.context_status import _check_expected_files
results = _check_expected_files(root)
paths_by_status = {r["path"]: r["status"] for r in results}
assert paths_by_status.get("TASKS.md") == "missing"
def test_context_status_reports_tbd_sections(self):
"""TBD placeholders should be detected by the core function."""
from pathlib import Path
import tempfile
with tempfile.TemporaryDirectory() as td:
root = Path(td) / "proj"
root.mkdir()
(root / "context").mkdir()
# Write a file with TBD placeholder
(root / "context" / "product-brief.md").write_text(
"# Product Brief\n\n## Problem\n\nTBD\n",
encoding="utf-8",
)
from rdb_discovery.context_status import _check_tbd_sections
findings = _check_tbd_sections(root)
assert len(findings) > 0
paths = {f["file"] for f in findings}
assert "product-brief.md" in paths
def test_context_status_reports_low_confidence(self):
"""Low-confidence answers should be detected by the core function."""
from pathlib import Path
import tempfile
with tempfile.TemporaryDirectory() as td:
root = Path(td) / "proj"
root.mkdir()
(root / "context").mkdir()
# Write discovery-log.md with a Low-confidence answer
(root / "context" / "discovery-log.md").write_text(
"# Discovery Log\n\n"
"| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n"
"|---|---|---|---|---|---|---|---|\n"
'| Q-003 | What does success look like? | Not sure yet | Low | Yes | | | 2026-06-03 |\n',
encoding="utf-8",
)
from rdb_discovery.context_status import _check_low_confidence
low = _check_low_confidence(root)
assert len(low) == 1
assert low[0]["id"] == "Q-003"
class TestHealthScoreColor:
"""Tests for health score color coding."""
def test_full_score_is_high(self):
"""All files present, no TBDs, no low-conf should produce a high score."""
from pathlib import Path
import tempfile
with tempfile.TemporaryDirectory() as td:
root = Path(td) / "proj"
root.mkdir()
(root / "context").mkdir()
for f in [
"discovery-log.md", "product-brief.md", "architecture.md",
"decisions.md", "risks.md", "assumptions.md", "open-questions.md",
]:
(root / "context" / f).write_text(f"# {f}\nDone\n", encoding="utf-8")
for f in ["TASKS.md", "TEST_PLAN.md", "RUN_LOG.md", "PROJECT_STATE.md", "AGENT_HANDOFF.md"]:
(root / f).write_text(f"# {f}\nDone\n", encoding="utf-8")
from rdb_discovery.context_status import context_status
report = context_status(root)
assert report["score"] >= 70
def test_empty_project_score_is_below_full(self):
"""A completely empty project should score well below a full project."""
from pathlib import Path
import tempfile
with tempfile.TemporaryDirectory() as td:
root = Path(td) / "proj"
root.mkdir()
# Don't create any files
from rdb_discovery.context_status import context_status
report = context_status(root)
assert report["score"] < 70
def test_cli_score_appears_in_output(self):
"""The CLI should display the numeric score in output."""
from typer.testing import CliRunner
from rdb_discovery.cli import app
runner = CliRunner()
result = runner.invoke(app, ["context-status"], catch_exceptions=False)
assert result.exit_code == 0
# Score format is like "92.5/100" — digits followed by /100
assert "/100" in result.output
+92 -2
View File
@@ -3,9 +3,9 @@ from pathlib import Path
import rdb_discovery.discovery as discovery_mod import rdb_discovery.discovery as discovery_mod
def test_core_questions_returns_10_items() -> None: def test_core_questions_returns_at_least_10_items() -> None:
questions = discovery_mod.core_questions() questions = discovery_mod.core_questions()
assert len(questions) == 10 assert len(questions) >= 10
def test_append_creates_file_when_missing(tmp_path: Path) -> None: def test_append_creates_file_when_missing(tmp_path: Path) -> None:
@@ -105,6 +105,96 @@ def test_append_preserves_existing_history(tmp_path: Path) -> None:
assert len(data_rows) == 2 assert len(data_rows) == 2
def test_read_discovery_answers_returns_dicts(tmp_path: Path) -> None:
discovery_mod.append_discovery_answer(
tmp_path,
question_id="Q-001",
question="What is this?",
answer="A thing",
confidence="High",
follow_up_needed="No",
)
answers = discovery_mod.read_discovery_answers(tmp_path)
assert len(answers) == 1
assert answers[0]["id"] == "Q-001"
assert answers[0]["question"] == "What is this?"
assert answers[0]["answer"] == "A thing"
assert answers[0]["confidence"] == "High"
assert answers[0]["follow_up_needed"] == "No"
assert answers[0]["needs_followup"] is False
assert answers[0]["low_confidence"] is False
def test_read_discovery_answers_returns_empty_when_no_log(tmp_path: Path) -> None:
log_path = tmp_path / "context" / "discovery-log.md"
assert not log_path.exists()
answers = discovery_mod.read_discovery_answers(tmp_path)
assert answers == []
def test_read_discovery_answers_detects_low_confidence(tmp_path: Path) -> None:
discovery_mod.append_discovery_answer(
tmp_path,
question_id="Q-002",
question="How much?",
answer="About half",
confidence="Low",
follow_up_needed="No",
)
answers = discovery_mod.read_discovery_answers(tmp_path)
low_conf_rows = [a for a in answers if a["low_confidence"]]
assert len(low_conf_rows) == 1
assert low_conf_rows[0]["id"] == "Q-002"
def test_read_discovery_answers_detects_followup_flag(tmp_path: Path) -> None:
discovery_mod.append_discovery_answer(
tmp_path,
question_id="Q-003",
question="What next?",
answer="TBD",
confidence="High",
follow_up_needed="Yes",
)
answers = discovery_mod.read_discovery_answers(tmp_path)
fu_rows = [a for a in answers if a["needs_followup"]]
assert len(fu_rows) == 1
assert fu_rows[0]["id"] == "Q-003"
def test_append_followup_appends_row(tmp_path: Path) -> None:
# First create the log with an existing entry
discovery_mod.append_discovery_answer(
tmp_path,
question_id="Q-010",
question="Original?",
answer="Original answer",
confidence="High",
follow_up_needed="No",
)
original_count = len(discovery_mod.read_discovery_answers(tmp_path))
discovery_mod.append_followup_answer(
tmp_path,
question_id="Q-010",
follow_up_question="Can you elaborate?",
answer="Yes, it's bigger than expected.",
confidence="Medium",
)
answers = discovery_mod.read_discovery_answers(tmp_path)
assert len(answers) == original_count + 1
# The new row should have the original Q-010 id and follow-up data
new_row = [a for a in answers if a["question"] == "Can you elaborate?"][0]
assert new_row["follow_up_needed"] == "Yes"
assert new_row["confidence"] == "Medium"
def test_header_is_written_on_create(tmp_path: Path) -> None: def test_header_is_written_on_create(tmp_path: Path) -> None:
discovery_mod.append_discovery_answer( discovery_mod.append_discovery_answer(
tmp_path, tmp_path,
+103
View File
@@ -0,0 +1,103 @@
"""Tests for the discovery-to-context mapping document."""
from pathlib import Path
# Path to the mapping document (relative to project root)
MAPPING_FILE = "context/discovery-context-mapping.md"
def _get_project_root() -> Path:
"""Return the project root directory.
Walk up from this file's location to find the repo root.
"""
# Start from tests/ and walk up
current = Path(__file__).resolve().parent.parent
# Look for pyproject.toml or .git as markers
while current != current.parent:
if (current / "pyproject.toml").exists() or (current / ".git").exists():
return current
current = current.parent
return Path.cwd()
def test_mapping_file_exists() -> None:
"""The discovery-to-context mapping file must exist."""
root = _get_project_root()
mapping_path = root / MAPPING_FILE
assert mapping_path.exists(), f"Mapping file missing: {mapping_path}"
def test_mapping_contains_all_questions() -> None:
"""Every core discovery question must appear in the mapping."""
from rdb_discovery.templates import CORE_QUESTIONS
root = _get_project_root()
mapping_path = root / MAPPING_FILE
content = mapping_path.read_text(encoding="utf-8")
for question in CORE_QUESTIONS:
assert question in content, f"Question not mapped: {question}"
def test_mapping_documentation_contains_table_format() -> None:
"""The mapping must be documented in a table format (machine-readable)."""
root = _get_project_root()
mapping_path = root / MAPPING_FILE
content = mapping_path.read_text(encoding="utf-8")
# Accept both the exact header and any variant that contains the key phrase
has_section = ("Mapping: Question" in content or "Question → Context" in content)
assert has_section, (
"Missing 'Mapping: Question -> Context Files + Sections' section header"
)
def test_mapping_contains_summary_matrix() -> None:
"""A summary question-to-file matrix table must exist."""
root = _get_project_root()
mapping_path = root / MAPPING_FILE
content = mapping_path.read_text(encoding="utf-8")
assert "Question-to-File Matrix" in content, (
"Missing 'Question-to-File Matrix' summary section"
)
def test_mapping_contains_future_implementation_notes() -> None:
"""The mapping must define future implementation guidance."""
root = _get_project_root()
mapping_path = root / MAPPING_FILE
content = mapping_path.read_text(encoding="utf-8")
assert "## Implementation Notes" in content or "## What future implementation should do" in content, (
"Missing 'Implementation Notes' section"
)
# Verify it clearly states no code has been implemented yet
assert "No code generation has been implemented" in content, (
"Must clarify that mapping is a planning artifact with no code yet"
)
def test_mapping_covers_all_context_files() -> None:
"""Every context file listed in templates.py must appear in the mapping."""
from rdb_discovery.templates import CONTEXT_FILES
# Filter out control files (TASKS.md, PROJECT_STATE.md, etc.) and keep context files
context_paths = {p for p in CONTEXT_FILES if p.startswith("context/")}
root = _get_project_root()
mapping_path = root / MAPPING_FILE
content = mapping_path.read_text(encoding="utf-8")
# Extract file paths from the mapping's Reference section
found_files = set()
for line in content.splitlines():
for cp in context_paths:
if f"`{cp}`" in line or cp in line:
found_files.add(cp)
for cp in context_paths:
assert any(cp in fp for fp in found_files), (
f"Context file not referenced in mapping: {cp}"
)
+459
View File
@@ -0,0 +1,459 @@
"""Tests for context file generation from discovery answers."""
import pytest
from pathlib import Path
from textwrap import dedent
# Use a helper to find the project root
def _get_project_root() -> Path:
current = Path(__file__).resolve().parent.parent
while current != current.parent:
if (current / "pyproject.toml").exists() or (current / ".git").exists():
return current
current = current.parent
return Path.cwd()
@pytest.fixture()
def tmp_project(tmp_path):
"""Create a minimal project root with discovery-log.md and context files."""
root = tmp_path / "testproject"
root.mkdir()
# Create directory structure
(root / "context").mkdir()
# Write a discovery log with various confidence levels
(root / "context" / "discovery-log.md").write_text(
"# Discovery Log\n\n"
"| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n"
"|---|---|---|---|---|---|---|---|\n"
'| Q-001 | What problem are we solving? | \\|A CLI tool for discovery workflow\\| | High | No | | | 2026-06-03 |\n'
'| Q-002 | Who is the user? | Internal developers and small dev teams | Medium | No | | | 2026-06-03 |\n'
'| Q-003 | What does success look like? | 80% reduction in onboarding time | High | No | | | 2026-06-03 |\n'
'| Q-007 | What are the risks? | Scope creep if requirements change frequently | Medium | Yes | | | 2026-06-03 |\n'
'| Q-008 | What must not happen? | No third-party SaaS dependencies | Low | No | | | 2026-06-03 |\n',
encoding="utf-8",
)
# Write product-brief.md with TBD placeholders
(root / "context" / "product-brief.md").write_text(
"# Product Brief\n\n## Problem\n\nTBD\n\n## Users\n\nTBD\n\n## Success Criteria\n\nTBD\n\n## Minimum Useful Version\n\nTBD\n",
encoding="utf-8",
)
# Write architecture.md with TBD placeholders
(root / "context" / "architecture.md").write_text(
"# Architecture\n\n## Overview\n\nTBD\n\n## Core Components\n\nTBD\n\n## Data Flow\n\nTDB\n\n## External Integrations\n\nTBD\n\n## Deployment Architecture\n\nTBD\n",
encoding="utf-8",
)
# Write risks.md with existing table header only
(root / "context" / "risks.md").write_text(
"# Risks\n\n| ID | Risk | Impact | Mitigation | Status |\n|---|---|---|---|---|\n",
encoding="utf-8",
)
# Write assumptions.md with existing table header only
(root / "context" / "assumptions.md").write_text(
"# Assumptions\n\n| ID | Assumption | Confidence | Validation Needed |\n|---|---|---|---|\n",
encoding="utf-8",
)
# Write infrastructure-context.md with existing content
(root / "context" / "infrastructure-context.md").write_text(
"# Infrastructure Context\n\n## Hosting\n\nTBD — Cloud provider details.\n\n## Environments\n\nTBD — Dev/staging/prod setup.\n",
encoding="utf-8",
)
# Write development-context.md with existing content (non-TBD)
(root / "context" / "development-context.md").write_text(
"# Development Context\n\n## Tech Stack\n\nPython 3.12, Typer, Rich.\n\n## IDEs and Editors\n\nVS Code and Cursor.\n\n## Dependencies\n\nPostgreSQL, Docker.\n\n## Build & Test\n\npytest for unit tests.\n",
encoding="utf-8",
)
return root
class TestGenerateContextFiles:
"""Tests for the generate_context_files function."""
def test_mapping_file_exists(self):
"""The mapping document must exist."""
root = _get_project_root()
mapping_path = root / "context/discovery-context-mapping.md"
assert mapping_path.exists()
def test_mapping_covers_all_questions(self):
"""All core questions should have mappings."""
from rdb_discovery.generate_context import CONTEXT_MAP
from rdb_discovery.templates import CORE_QUESTIONS
expected_count = len(CORE_QUESTIONS)
for i in range(1, expected_count + 1):
qid = f"Q-{i:03d}"
assert qid in CONTEXT_MAP, f"Missing mapping for {qid}"
def test_mapping_all_target_files_are_valid(self):
"""All target files in the map should exist as templates."""
from rdb_discovery.generate_context import CONTEXT_MAP
from rdb_discovery.templates import CONTEXT_FILES
template_paths = set(CONTEXT_FILES.keys()) | {"TASKS.md", "TEST_PLAN.md"}
for qid, targets in CONTEXT_MAP.items():
for file_path, _, _ in targets:
# The file should either be a known template or be context/
assert any(file_path.startswith(p.rstrip('/').split('/')[-1] if '/' not in p else '')
for p in template_paths) or 'context/' in file_path, \
f"{qid} maps to unknown file: {file_path}"
def test_no_discovery_answers_returns_skipped(self, tmp_project):
"""When no discovery answers exist, return skipped message."""
# Overwrite discovery log with empty content
(tmp_project / "context" / "discovery-log.md").write_text(
"# Discovery Log\n\n| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n|---|---|---|---|---|---|---|---|\n",
encoding="utf-8",
)
from rdb_discovery.generate_context import generate_context_files
result = generate_context_files(tmp_project)
assert result["generated"] == []
assert len(result["skipped"]) > 0
assert "No discovery answers" in result["skipped"][0]
class TestLowConfidenceFiltering:
"""Tests for low-confidence answer filtering."""
def test_low_confidence_answers_are_skipped(self, tmp_project):
"""Answers with Low confidence should not generate any context content."""
from rdb_discovery.generate_context import generate_context_files
# Overwrite to have only Low confidence answers
(tmp_project / "context" / "discovery-log.md").write_text(
"# Discovery Log\n\n"
"| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n"
"|---|---|---|---|---|---|---|---|\n"
'| Q-001 | What problem are we solving? | Some answer | Low | No | | | 2026-06-03 |\n',
encoding="utf-8",
)
result = generate_context_files(tmp_project)
assert result["generated"] == []
assert "skipped" in result
assert len(result["skipped"]) > 0
class TestBodyTextGeneration:
"""Tests for body-text section filling."""
def test_tbd_placeholder_replaced(self, tmp_project):
"""TBD placeholders should be replaced with discovery answer."""
from rdb_discovery.generate_context import generate_context_files
result = generate_context_files(tmp_project)
assert "context/product-brief.md" in result["generated"]
content = (tmp_project / "context/product-brief.md").read_text()
# Q-001 answer should replace TBD under ## Problem
assert "CLI tool for discovery workflow" in content
assert "## Problem" in content
# The rest of the file must be preserved
assert "## Success Criteria" in content
assert "## Minimum Useful Version" in content
class TestTableGeneration:
"""Tests for table-based section row generation."""
def test_risks_table_gets_row(self, tmp_project):
"""Q-007 (risks) should add a row to risks.md table."""
from rdb_discovery.generate_context import generate_context_files
result = generate_context_files(tmp_project)
assert "context/risks.md" in result["generated"]
content = (tmp_project / "context/risks.md").read_text()
assert "| RISK-007 |" in content
assert "Scope creep" in content
class TestContentPreservation:
"""Tests that existing content is preserved."""
def test_existing_development_context_preserved(self, tmp_project):
"""Existing non-TBD content should be preserved and new content appended."""
from rdb_discovery.generate_context import generate_context_files
generate_context_files(tmp_project)
content = (tmp_project / "context/development-context.md").read_text()
assert "Python 3.12, Typer, Rich." in content
assert "VS Code and Cursor." in content
class TestContextMapCompleteness:
"""Tests for mapping document completeness."""
def test_all_questions_mapped(self):
"""Every core question should have a mapping entry."""
from rdb_discovery.generate_context import CONTEXT_MAP
from rdb_discovery.templates import CORE_QUESTIONS
expected_count = len(CORE_QUESTIONS)
expected_ids = {f"Q-{i:03d}" for i in range(1, expected_count + 1)}
mapped_ids = set(CONTEXT_MAP.keys())
assert expected_ids == mapped_ids, f"Missing mappings: {expected_ids - mapped_ids}"
def test_each_question_has_at_least_one_target(self):
"""Every question mapping should target at least one file."""
from rdb_discovery.generate_context import CONTEXT_MAP
for qid, targets in CONTEXT_MAP.items():
assert len(targets) >= 1, f"{qid} has no target files"
for file_path, section_name, fmt in targets:
assert file_path, f"{qid}: empty file path"
assert fmt in ("body", "table", "append"), f"{qid}: invalid format {fmt}"
class TestCLICommand:
"""Tests that the CLI 'generate' command is available."""
def test_generate_command_exists(self):
"""The 'rdb generate' command should be registered in the CLI."""
from typer.testing import CliRunner
from rdb_discovery.cli import app
runner = CliRunner()
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "generate" in result.output.lower() or "-g" in result.output.lower()
def test_generate_command_with_empty_discovery(self):
"""The CLI generate command handles empty discovery gracefully."""
from typer.testing import CliRunner
from rdb_discovery.cli import app
from pathlib import Path
import tempfile
with tempfile.TemporaryDirectory() as td:
root = Path(td) / "proj"
root.mkdir()
(root / "context").mkdir()
(root / "context" / "discovery-log.md").write_text(
"# Discovery Log\n\n| ID | Question |\n|---|---|\n",
encoding="utf-8",
)
runner = CliRunner()
result = runner.invoke(app, ["generate"], catch_exceptions=False)
assert result.exit_code == 0
def test_generate_creates_missing_files(self):
"""Generate should create context files that don't yet exist."""
from pathlib import Path
import tempfile
import shutil
tmp_path = Path(tempfile.mkdtemp())
root = tmp_path / "testproject"
root.mkdir()
(root / "context").mkdir()
# Write only discovery-log.md — no context files
(root / "context" / "discovery-log.md").write_text(
"# Discovery Log\n\n"
"| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n"
"|---|---|---|---|---|---|---|---|\n"
'| Q-007 | What are the risks? | Some risk description | High | No | | | 2026-06-03 |\n',
encoding="utf-8",
)
from rdb_discovery.generate_context import generate_context_files
result = generate_context_files(root)
assert "context/risks.md" in result["generated"]
assert (root / "context" / "risks.md").exists()
shutil.rmtree(tmp_path)
class TestIntegration:
"""End-to-end integration tests for the full generation pipeline."""
def test_full_generation_flow(self, tmp_project):
"""All mapped answers are written to correct files with correct confidence filtering."""
from rdb_discovery.generate_context import generate_context_files
result = generate_context_files(tmp_project)
# High/Medium answers should be generated
assert "context/product-brief.md" in result["generated"]
assert "context/project-brief.md" in result["generated"]
assert "context/risks.md" in result["generated"]
assert "context/company-context.md" in result["generated"] # Q-002 → company-context
assert "context/assumptions.md" not in result["generated"] # Q-008 is Low confidence
# Check that product-brief has filled sections
content = (tmp_project / "context/product-brief.md").read_text()
assert "CLI tool for discovery workflow" in content
assert "Internal developers and small dev teams" in content
# Check company-context got users data from Q-002
company = (tmp_project / "context/company-context.md").read_text()
assert "## Customers / Users" in company
assert "Internal developers and small dev teams" in company
# Check that risks.md got a table row
risks = (tmp_project / "context/risks.md").read_text()
assert "| RISK-007 |" in risks
assert "Scope creep" in risks
def test_preserves_existing_infrastructure_content(self, tmp_project):
"""Existing content in infrastructure-context.md is preserved."""
from rdb_discovery.generate_context import generate_context_files
original = (tmp_project / "context/infrastructure-context.md").read_text()
generate_context_files(tmp_project)
result = (tmp_project / "context/infrastructure-context.md").read_text()
assert "Cloud provider details" in result # Original content preserved
class TestContextQuality:
"""Tests for TASK-023: improved context generation quality."""
def test_company_context_populated_from_users(self):
"""company-context.md should be populated when discovery answers include users/stakeholders."""
from pathlib import Path
import tempfile
import shutil
tmp_path = Path(tempfile.mkdtemp())
root = tmp_path / "testproject"
root.mkdir()
(root / "context").mkdir()
# Only Q-002 (users) - no other discovery data
(root / "context" / "discovery-log.md").write_text(
"# Discovery Log\n\n"
"| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n"
"|---|---|---|---|---|---|---|---|\n"
'| Q-002 | Who is the user? | Internal dev teams and external customers | High | No | | | 2026-06-03 |\n',
encoding="utf-8",
)
from rdb_discovery.generate_context import generate_context_files
result = generate_context_files(root)
assert "context/company-context.md" in result["generated"]
assert "context/product-brief.md" in result["generated"]
company = (root / "context/company-context.md").read_text()
assert "## Customers / Users" in company
assert "Internal dev teams and external customers" in company
shutil.rmtree(tmp_path)
def test_agent_guidelines_gets_testing_preferences(self):
"""agent-guidelines.md should get Testing Preferences from testing discovery answers."""
from pathlib import Path
import tempfile
import shutil
tmp_path = Path(tempfile.mkdtemp())
root = tmp_path / "testproject"
root.mkdir()
(root / "context").mkdir()
# Only Q-009 (testing) - no agent tooling questions
(root / "context" / "discovery-log.md").write_text(
"# Discovery Log\n\n"
"| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n"
"|---|---|---|---|---|---|---|---|\n"
'| Q-009 | How will we test it? | pytest with coverage, integration tests via docker-compose | Medium | No | | | 2026-06-03 |\n',
encoding="utf-8",
)
from rdb_discovery.generate_context import generate_context_files
result = generate_context_files(root)
assert "context/agent-guidelines.md" in result["generated"]
guidelines = (root / "context/agent-guidelines.md").read_text()
assert "## Testing Preferences" in guidelines
assert "pytest with coverage" in guidelines
shutil.rmtree(tmp_path)
def test_no_testing_answers_in_timeline(self):
"""Testing answers must not be mapped to Timeline & Milestones sections."""
from pathlib import Path
import tempfile
import shutil
tmp_path = Path(tempfile.mkdtemp())
root = tmp_path / "testproject"
root.mkdir()
(root / "context").mkdir()
# Only Q-009 (testing) - verify no timeline data appears
(root / "context" / "discovery-log.md").write_text(
"# Discovery Log\n\n"
"| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n"
"|---|---|---|---|---|---|---|\n"
'| Q-009 | How will we test it? | pytest with coverage | High | No | | | 2026-06-03 |\n',
encoding="utf-8",
)
# Create project-brief.md that has a Timeline section already
(root / "context" / "project-brief.md").write_text(
"# Project Brief\n\n## Timeline & Milestones\n\nTBD\n",
encoding="utf-8",
)
from rdb_discovery.generate_context import generate_context_files
result = generate_context_files(root)
assert "context/project-brief.md" not in result["generated"]
timeline_content = (root / "context/project-brief.md").read_text()
# The Timeline section should remain as TBD (not filled with testing data)
assert "pytest with coverage" not in timeline_content
# Original TBD placeholder preserved
assert "TBD" in timeline_content
shutil.rmtree(tmp_path)
def test_repository_context_populated_from_dependencies(self):
"""repository-context.md should be populated when answers include dependencies."""
from pathlib import Path
import tempfile
import shutil
tmp_path = Path(tempfile.mkdtemp())
root = tmp_path / "testproject"
root.mkdir()
(root / "context").mkdir()
# Q-012 includes technology stack info that maps to repository dependencies
(root / "context" / "discovery-log.md").write_text(
"# Discovery Log\n\n"
"| ID | Question | Answer | Confidence | Follow-up needed | Linked decision | Linked task | Date |\n"
"|---|---|---|---|---|---|---|---|\n"
'| Q-012 | What is the technology stack, repository structure, and architecture approach? | Python 3.12, Typer, Rich; Docker; PostgreSQL | High | No | | | 2026-06-03 |\n',
encoding="utf-8",
)
from rdb_discovery.generate_context import generate_context_files
result = generate_context_files(root)
assert "context/repository-context.md" in result["generated"]
repo_ctx = (root / "context/repository-context.md").read_text()
assert "## Dependencies" in repo_ctx
assert "Python 3.12, Typer, Rich" in repo_ctx
shutil.rmtree(tmp_path)
+321
View File
@@ -0,0 +1,321 @@
from pathlib import Path
from datetime import datetime, timedelta
import os
import pytest
from rdb_discovery.guardrails import (
check_repeated_reads,
check_repeated_commands,
check_repeated_commands_telemetry,
check_repeated_reads_telemetry,
check_no_recent_file_changes,
check_no_test_run_recorded,
check_run_log_updated,
run_all_guardrails,
format_report,
)
# -- check_repeated_reads --
def test_check_repeated_reads_ok(tmp_path: Path) -> None:
"""No agent history files → status ok."""
result = check_repeated_reads(tmp_path)
assert result["status"] == "ok"
def test_check_repeated_reads_warning(tmp_path: Path) -> None:
"""AGENT_HANDOFF.md mentions the same file >2 times."""
ah = tmp_path / "AGENT_HANDOFF.md"
ah.write_text(
"# Agent Handoff\n\n"
"- README.md\n- README.md\n- README.md\n"
"- TASKS.md\n",
encoding="utf-8",
)
result = check_repeated_reads(tmp_path)
assert result["status"] == "warning"
assert "README.md (3x)" in result["details"]
def test_check_repeated_reads_no_over_threshold(tmp_path: Path) -> None:
"""Files mentioned <=2 times should not trigger warning."""
ah = tmp_path / "AGENT_HANDOFF.md"
ah.write_text(
"# Agent Handoff\n\n"
"- README.md\n- README.md\n"
"- TASKS.md\n- TASKS.md\n",
encoding="utf-8",
)
result = check_repeated_reads(tmp_path)
assert result["status"] == "ok"
# -- check_repeated_commands --
def test_check_repeated_commands_ok_no_log(tmp_path: Path) -> None:
result = check_repeated_commands(tmp_path)
assert result["status"] == "ok"
def test_check_repeated_commands_ok(tmp_path: Path) -> None:
rl = tmp_path / "RUN_LOG.md"
rl.write_text(
"| date | event1 |\n"
"| date | event2 |\n",
encoding="utf-8",
)
result = check_repeated_commands(tmp_path)
assert result["status"] == "ok"
def test_check_repeated_commands_warning(tmp_path: Path) -> None:
rl = tmp_path / "RUN_LOG.md"
line = "| date | Task completed |\n"
rl.write_text(
"# RUN_LOG\n\n| Date | Event | Task | Notes |\n" + line * 5,
encoding="utf-8",
)
result = check_repeated_commands(tmp_path)
assert result["status"] == "warning"
# -- check_repeated_commands_telemetry --
def test_check_repeated_commands_telemetry_no_events(tmp_path: Path) -> None:
"""No telemetry events → status ok with fallback message."""
result = check_repeated_commands_telemetry(tmp_path)
assert result["status"] == "ok"
assert "No telemetry data available" in result["details"]
def test_check_repeated_commands_telemetry_warning(tmp_path: Path) -> None:
"""Same command repeated >3 times in telemetry → warning."""
from rdb_discovery.telemetry import record_event
for _ in range(5):
record_event(tmp_path, "command", "rdb prompt")
result = check_repeated_commands_telemetry(tmp_path)
assert result["status"] == "warning"
assert "rdb prompt" in result["details"]
def test_check_repeated_commands_telemetry_no_over_threshold(tmp_path: Path) -> None:
"""Same command repeated <=3 times → ok."""
from rdb_discovery.telemetry import record_event
for _ in range(3):
record_event(tmp_path, "command", "rdb prompt")
result = check_repeated_commands_telemetry(tmp_path)
assert result["status"] == "ok"
# -- check_repeated_reads_telemetry --
def test_check_repeated_reads_telemetry_no_events(tmp_path: Path) -> None:
"""No telemetry events → status ok with fallback message."""
result = check_repeated_reads_telemetry(tmp_path)
assert result["status"] == "ok"
assert "No telemetry data available" in result["details"]
def test_check_repeated_reads_telemetry_no_read_events(tmp_path: Path) -> None:
"""Only command events, no read events → ok with message."""
from rdb_discovery.telemetry import record_event
record_event(tmp_path, "command", "rdb prompt")
result = check_repeated_reads_telemetry(tmp_path)
assert result["status"] == "ok"
assert "No file read events found" in result["details"]
def test_check_repeated_reads_telemetry_warning(tmp_path: Path) -> None:
"""Same file read >3 times in telemetry → warning."""
from rdb_discovery.telemetry import record_event
for _ in range(5):
record_event(tmp_path, "read", "README.md")
result = check_repeated_reads_telemetry(tmp_path)
assert result["status"] == "warning"
assert "README.md" in result["details"]
def test_check_repeated_reads_telemetry_no_over_threshold(tmp_path: Path) -> None:
"""Same file read <=3 times → ok."""
from rdb_discovery.telemetry import record_event
for _ in range(3):
record_event(tmp_path, "read", "TASKS.md")
result = check_repeated_reads_telemetry(tmp_path)
assert result["status"] == "ok"
# -- check_no_recent_file_changes --
def test_check_no_recent_file_changes_ok(tmp_path: Path) -> None:
"""Files modified recently → ok."""
for rel in ["TASKS.md", "PROJECT_STATE.md"]:
(tmp_path / rel).write_text("x", encoding="utf-8")
# Patch _last_modified to return a recent time
import rdb_discovery.guardrails as mod
original_now = mod._now
def fake_now() -> datetime:
return datetime.now()
mod._now = fake_now
try:
result = check_no_recent_file_changes(tmp_path)
assert result["status"] == "ok"
finally:
mod._now = original_now
def test_check_no_recent_file_changes_warning(tmp_path: Path) -> None:
"""Files modified >48h ago → warning."""
(tmp_path / "TASKS.md").write_text("x", encoding="utf-8")
import rdb_discovery.guardrails as mod
old_ts = datetime.now() - timedelta(hours=72)
# Set a fake mtime via os.utime (avoids pathlib.touch times= on macOS)
ts = old_ts.timestamp()
(tmp_path / "TASKS.md").touch()
os.utime(str(tmp_path / "TASKS.md"), (ts, ts))
result = check_no_recent_file_changes(tmp_path)
assert result["status"] == "warning"
# -- check_no_test_run_recorded --
def test_check_no_test_run_recorded_ok(tmp_path: Path) -> None:
rl = tmp_path / "RUN_LOG.md"
rl.write_text("# RUN\n\n| date | pytest passed |\n", encoding="utf-8")
result = check_no_test_run_recorded(tmp_path)
assert result["status"] == "ok"
def test_check_no_test_run_recorded_warning(tmp_path: Path) -> None:
rl = tmp_path / "RUN_LOG.md"
rl.write_text("# RUN\n\n| date | task done |\n", encoding="utf-8")
result = check_no_test_run_recorded(tmp_path)
assert result["status"] == "warning"
def test_check_no_test_run_recorded_no_log(tmp_path: Path) -> None:
result = check_no_test_run_recorded(tmp_path)
assert result["status"] == "ok"
# -- check_run_log_updated --
def test_check_run_log_updated_ok(tmp_path: Path) -> None:
tasks = tmp_path / "TASKS.md"
tasks.write_text(
"## TASK-001 - First\nStatus: Done\n",
encoding="utf-8",
)
rl = tmp_path / "RUN_LOG.md"
rl.write_text(
"# RUN\n\n| date | Task completed | TASK-001 |\n",
encoding="utf-8",
)
result = check_run_log_updated(tmp_path)
assert result["status"] == "ok"
def test_check_run_log_updated_warning(tmp_path: Path) -> None:
tasks = tmp_path / "TASKS.md"
tasks.write_text(
"## TASK-001 - First\nStatus: Done\n",
encoding="utf-8",
)
rl = tmp_path / "RUN_LOG.md"
rl.write_text("# RUN\n\n| date | something else |\n", encoding="utf-8")
result = check_run_log_updated(tmp_path)
assert result["status"] == "warning"
def test_check_run_log_updated_no_files(tmp_path: Path) -> None:
result = check_run_log_updated(tmp_path)
assert result["status"] == "ok"
# -- run_all_guardrails --
def test_run_all_guardrails_returns_keys() -> None:
import tempfile
with tempfile.TemporaryDirectory() as td:
root = Path(td)
# Create minimal files so checks don't return 'ok' for missing file
(root / "RUN_LOG.md").write_text("# RUN\n", encoding="utf-8")
result = run_all_guardrails(root)
assert "checks" in result
assert "overall_status" in result
def test_run_all_guardrails_overall_all_clear(tmp_path: Path) -> None:
"""All checks pass → overall 'all clear'."""
# Create files with recent mtimes and ok content
(tmp_path / "TASKS.md").write_text("# TASKS\n", encoding="utf-8")
(tmp_path / "RUN_LOG.md").write_text("# RUN\n| date | pytest passed |\n", encoding="utf-8")
import rdb_discovery.guardrails as mod
old_now = mod._now
now = datetime.now()
class FakePath:
def __init__(self, path: Path):
self._path = path
def exists(self) -> bool:
return self._path.exists()
def stat(self):
class StatResult:
st_mtime = now.timestamp()
return StatResult()
orig_last_modified = mod._last_modified
def fake_last_modified(root: Path, rel: str) -> datetime | None:
p = root / rel
if p.exists():
return now
return None
mod._now = lambda: now
mod._last_modified = fake_last_modified
try:
result = run_all_guardrails(tmp_path)
# Should be clear or have a non-error overall status (guardrails may flag other things)
assert result["overall_status"] in ("all clear", "review recommended")
finally:
mod._now = old_now
mod._last_modified = orig_last_modified
# -- format_report --
def test_format_report_includes_overall() -> None:
results = {
"checks": {"Test check": {"status": "ok", "details": "fine"}},
"overall_status": "all clear",
}
report = format_report(results)
assert "# Guardrail Report" in report
assert "all clear" in report
def test_format_report_with_warning() -> None:
results = {
"checks": {"Test check": {"status": "warning", "details": "watch out"}},
"overall_status": "review recommended",
}
report = format_report(results)
assert "review recommended" in report
+287 -3
View File
@@ -2,7 +2,25 @@ from pathlib import Path
import pytest import pytest
from rdb_discovery.tasks import generate_agent_prompt, get_next_task from rdb_discovery.tasks import (
_extract_test_commands_from_claude,
generate_agent_prompt,
get_next_task,
)
def _write_claude_with_test_cmds(tmp_path: Path) -> None:
"""Write a CLAUDE.md with Test Commands section."""
(tmp_path / "CLAUDE.md").write_text(
"# Claude Code Instructions\n\n"
"## Test Commands\n\n"
"Use the existing virtual environment.\n\n"
"```bash\n"
"source .venv/bin/activate\n"
"python -m pytest\n"
"```\n",
encoding="utf-8",
)
def test_generate_agent_prompt_includes_task_info(tmp_path: Path) -> None: def test_generate_agent_prompt_includes_task_info(tmp_path: Path) -> None:
@@ -67,7 +85,7 @@ def test_generate_agent_prompt_includes_read_instructions(tmp_path: Path) -> Non
assert "context/agent-guidelines.md" in prompt assert "context/agent-guidelines.md" in prompt
def test_generate_agent_prompt_includes_one_task_constraint(tmp_path: Path) -> None: def test_generate_agent_prompt_includes_one_step_constraint(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text( (tmp_path / "TASKS.md").write_text(
"# TASKS\n\n" "# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n" "## TASK-001 — First\nStatus: Done\n\n"
@@ -79,7 +97,7 @@ def test_generate_agent_prompt_includes_one_task_constraint(tmp_path: Path) -> N
prompt = generate_agent_prompt(tmp_path) prompt = generate_agent_prompt(tmp_path)
assert "ONE task only" in prompt or "one task only" in prompt assert "ONE small implementation step only" in prompt
def test_generate_agent_prompt_includes_validation(tmp_path: Path) -> None: def test_generate_agent_prompt_includes_validation(tmp_path: Path) -> None:
@@ -129,3 +147,269 @@ def test_generate_agent_prompt_no_tasks_file(tmp_path: Path) -> None:
prompt = generate_agent_prompt(tmp_path) prompt = generate_agent_prompt(tmp_path)
assert prompt == "No Todo task found." assert prompt == "No Todo task found."
def test_generate_agent_prompt_includes_implementation_gap(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"Goal: x.\n\n"
"## TASK-002 — Second\nStatus: Todo\n\n"
"Goal: y.\n"
"Implementation Gap:\n"
"Missing validation handler in cli.py\n",
encoding="utf-8",
)
prompt = generate_agent_prompt(tmp_path)
assert "Implementation Gap:" in prompt
assert "Missing validation handler in cli.py" in prompt
def test_generate_agent_prompt_omits_gap_when_missing(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"Goal: x.\n\n"
"## TASK-002 — Second\nStatus: Todo\n\n"
"Goal: y.\n",
encoding="utf-8",
)
prompt = generate_agent_prompt(tmp_path)
assert "Implementation Gap:" not in prompt
def test_generate_agent_prompt_includes_test_commands_from_claude(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"## TASK-002 — Second\nStatus: Todo\n\n"
"Goal: y.\n",
encoding="utf-8",
)
_write_claude_with_test_cmds(tmp_path)
prompt = generate_agent_prompt(tmp_path)
assert "Test Commands (from CLAUDE.md)" in prompt
assert "source .venv/bin/activate" in prompt
assert "python -m pytest" in prompt
def test_generate_agent_prompt_includes_no_reread_constraint(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"## TASK-002 — Second\nStatus: Todo\n\n"
"Goal: y.\n",
encoding="utf-8",
)
prompt = generate_agent_prompt(tmp_path)
assert "reread" in prompt.lower() or "read" in prompt.lower()
def test_generate_agent_prompt_includes_inspect_first_constraint(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"## TASK-002 — Second\nStatus: Todo\n\n"
"Goal: y.\n",
encoding="utf-8",
)
prompt = generate_agent_prompt(tmp_path)
assert "Inspect" in prompt or "inspect" in prompt
def test_extract_test_commands_no_claude_file(tmp_path: Path) -> None:
commands = _extract_test_commands_from_claude(tmp_path)
assert commands == []
def test_extract_test_commands_from_claude(tmp_path: Path) -> None:
(tmp_path / "CLAUDE.md").write_text(
"# Instructions\n\n"
"## Test Commands\n\n"
"Run the following:\n\n"
"```bash\n"
"source .venv/bin/activate\n"
"python -m pytest\n"
"```\n",
encoding="utf-8",
)
commands = _extract_test_commands_from_claude(tmp_path)
assert any(".venv/bin/activate" in cmd for cmd in commands)
assert "python -m pytest" in commands
def test_extract_test_commands_no_section(tmp_path: Path) -> None:
(tmp_path / "CLAUDE.md").write_text(
"# Instructions\n\n"
"No test section here.\n",
encoding="utf-8",
)
commands = _extract_test_commands_from_claude(tmp_path)
assert commands == []
def test_generate_agent_prompt_with_role(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"Goal: x.\n\n"
"## TASK-002 — Second\nStatus: Todo\n\n"
"Role: Architecture Agent\n"
"Goal: y.\n",
encoding="utf-8",
)
prompt = generate_agent_prompt(tmp_path)
assert "You are an architecture agent." in prompt
def test_generate_agent_prompt_default_role_when_missing(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"Goal: x.\n\n"
"## TASK-002 — Second\nStatus: Todo\n\n"
"Goal: y.\n",
encoding="utf-8",
)
prompt = generate_agent_prompt(tmp_path)
assert "You are an implementation agent." in prompt
def test_generate_agent_prompt_with_empty_role_defaults(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"Goal: x.\n\n"
"## TASK-002 — Second\nStatus: Todo\n\n"
"Role:\n"
"Goal: y.\n",
encoding="utf-8",
)
prompt = generate_agent_prompt(tmp_path)
assert "You are an implementation agent." in prompt
def test_generate_agent_prompt_with_custom_role(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"Goal: x.\n\n"
"## TASK-002 — Second\nStatus: Todo\n\n"
"Role: DevOps Engineer\n"
"Goal: y.\n",
encoding="utf-8",
)
prompt = generate_agent_prompt(tmp_path)
assert "You are a devops engineer." in prompt
def test_architecture_agent_receives_architecture_context(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"Goal: x.\n\n"
"## TASK-002 — Second\nStatus: Todo\n\n"
"Role: Architecture Agent\n"
"Goal: y.\n",
encoding="utf-8",
)
prompt = generate_agent_prompt(tmp_path)
assert "context/architecture.md" in prompt
assert "context/product-brief.md" in prompt
assert "context/decisions.md" in prompt
def test_implementation_agent_receives_implementation_context(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"Goal: x.\n\n"
"## TASK-002 — Second\nStatus: Todo\n\n"
"Role: Implementation Agent\n"
"Goal: y.\n",
encoding="utf-8",
)
prompt = generate_agent_prompt(tmp_path)
assert "AGENT_HANDOFF.md" in prompt
assert "context/agent-guidelines.md" in prompt
assert "CLAUDE.md" in prompt
def test_documentation_agent_receives_documentation_context(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"Goal: x.\n\n"
"## TASK-002 — Second\nStatus: Todo\n\n"
"Role: Documentation Agent\n"
"Goal: y.\n",
encoding="utf-8",
)
prompt = generate_agent_prompt(tmp_path)
assert "TEST_PLAN.md" in prompt
assert "AGENT_HANDOFF.md" in prompt
def test_context_files_differ_by_role(tmp_path: Path) -> None:
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"Goal: x.\n\n"
"## TASK-002 — Architect\nStatus: Todo\n\n"
"Role: Architecture Agent\n"
"Goal: y.\n",
encoding="utf-8",
)
prompt = generate_agent_prompt(tmp_path)
# Architecture role should have architecture-specific files
assert "context/architecture.md" in prompt
def test_no_role_uses_default_context(tmp_path: Path) -> None:
"""When no Role is set, the default file list is used (backward compatible)."""
(tmp_path / "TASKS.md").write_text(
"# TASKS\n\n"
"## TASK-001 — First\nStatus: Done\n\n"
"Goal: x.\n\n"
"## TASK-002 — Second\nStatus: Todo\n\n"
"Goal: y.\n",
encoding="utf-8",
)
prompt = generate_agent_prompt(tmp_path)
# Default files must still be present
assert "README.md" in prompt
assert "TASKS.md" in prompt
assert "PROJECT_STATE.md" in prompt
assert "AGENT_HANDOFF.md" in prompt
assert "context/agent-guidelines.md" in prompt
+56
View File
@@ -0,0 +1,56 @@
from pathlib import Path
from rdb_discovery.status import (
update_project_state,
update_agent_handoff,
)
def test_update_project_state_updates_task_id(tmp_path: Path) -> None:
ps = tmp_path / "PROJECT_STATE.md"
ps.write_text(
"# Project State\n\nCurrent Stage: DISCOVERY\nPrevious Stage: NONE\nNext Stage: BOOTSTRAP_READY\n\nCurrent Task: None\nActive Branch: main\n\nLast Updated: 2026-01-01\n",
encoding="utf-8",
)
assert update_project_state(tmp_path, "TASK-005") is True
content = ps.read_text(encoding="utf-8")
assert "Current Task: TASK-005" in content
def test_update_project_state_updates_timestamp(tmp_path: Path) -> None:
ps = tmp_path / "PROJECT_STATE.md"
ps.write_text(
"# Project State\n\nCurrent Stage: DISCOVERY\nPrevious Stage: NONE\nNext Stage: BOOTSTRAP_READY\n\nCurrent Task: None\nActive Branch: main\n\nLast Updated: 2026-01-01\n",
encoding="utf-8",
)
assert update_project_state(tmp_path, "TASK-999") is True
content = ps.read_text(encoding="utf-8")
from datetime import date
expected_date = date.today().isoformat()
assert f"Last Updated: {expected_date}" in content
def test_update_project_state_no_file_returns_false(tmp_path: Path) -> None:
assert update_project_state(tmp_path, "TASK-001") is False
def test_update_agent_handoff_updates_task_and_stage(tmp_path: Path) -> None:
ah = tmp_path / "AGENT_HANDOFF.md"
ah.write_text(
"# Agent Handoff\n\n## Current Stage\n\nDISCOVERY\n\n## Current Task\n\nNone\n\n## Instructions For Agent\n\n- Complete one task only\n",
encoding="utf-8",
)
assert update_agent_handoff(tmp_path, "TASK-005") is True
content = ah.read_text(encoding="utf-8")
assert "Current Task" in content
assert "TASK-005" in content
def test_update_agent_handoff_no_file_returns_false(tmp_path: Path) -> None:
assert update_agent_handoff(tmp_path, "TASK-001") is False
+190
View File
@@ -0,0 +1,190 @@
from __future__ import annotations
import json
from pathlib import Path
import pytest
from rdb_discovery.telemetry import TelemetryEvent, record_event, read_events
# -- TelemetryEvent --
def test_telemetry_event_to_dict() -> None:
event = TelemetryEvent(
timestamp="2026-06-02T12:00:00+00:00",
event_type="command",
target="rdb prompt",
details={"flag": True},
)
d = event.to_dict()
assert d["event_type"] == "command"
assert d["target"] == "rdb prompt"
assert d["details"]["flag"] is True
def test_telemetry_event_defaults_to_empty_details() -> None:
event = TelemetryEvent(
timestamp="2026-06-02T12:00:00+00:00",
event_type="command",
target="rdb prompt",
)
assert event.details == {}
def test_telemetry_event_required_fields() -> None:
event = TelemetryEvent(
timestamp="2026-06-02T12:00:00+00:00",
event_type="command",
target="rdb prompt",
)
assert event.timestamp == "2026-06-02T12:00:00+00:00"
assert event.event_type == "command"
assert event.target == "rdb prompt"
# -- record_event (file creation) --
def test_record_event_creates_session_log(tmp_path: Path) -> None:
log = tmp_path / ".rdb" / "session-log.jsonl"
assert not log.exists()
record_event(tmp_path, "command", "rdb prompt")
assert log.exists()
# -- record_event (appending) --
def test_record_event_appends_jsonl_line(tmp_path: Path) -> None:
record_event(tmp_path, "command", "rdb prompt")
record_event(tmp_path, "task", "TASK-012")
lines = log_lines(tmp_path)
assert len(lines) == 2
def test_record_event_writes_valid_json_per_line(tmp_path: Path) -> None:
record_event(tmp_path, "command", "rdb prompt", {"arg": "value"})
record_event(tmp_path, "task", "TASK-012")
for line in log_lines(tmp_path):
data = json.loads(line)
assert data["event_type"] in ("command", "task")
def test_record_event_has_required_keys(tmp_path: Path) -> None:
record_event(tmp_path, "command", "rdb prompt")
event = TelemetryEvent(**json.loads(log_lines(tmp_path)[0]))
assert hasattr(event, "timestamp")
assert hasattr(event, "event_type")
assert hasattr(event, "target")
assert hasattr(event, "details")
def test_record_event_details_default_empty_dict(tmp_path: Path) -> None:
record_event(tmp_path, "command", "rdb prompt")
event = TelemetryEvent(**json.loads(log_lines(tmp_path)[0]))
assert event.details == {}
# -- read_events (no file) --
def test_read_events_returns_empty_when_no_log(tmp_path: Path) -> None:
events = read_events(tmp_path)
assert events == []
# -- read_events (round-trip) --
def test_read_events_returns_recorded_events(tmp_path: Path) -> None:
record_event(tmp_path, "command", "rdb prompt", {"count": 42})
events = read_events(tmp_path)
assert len(events) == 1
assert events[0].event_type == "command"
assert events[0].target == "rdb prompt"
assert events[0].details["count"] == 42
def test_read_events_returns_multiple_events(tmp_path: Path) -> None:
record_event(tmp_path, "task", "TASK-001")
record_event(tmp_path, "task", "TASK-002")
record_event(tmp_path, "command", "rdb status")
events = read_events(tmp_path)
assert len(events) == 3
# -- read_events (malformed lines) --
def test_read_events_skips_malformed_lines(tmp_path: Path) -> None:
log_path = tmp_path / ".rdb" / "session-log.jsonl"
log_path.parent.mkdir(parents=True, exist_ok=True)
log_path.write_text(
'{"timestamp":"2026-01-01T00:00:00+00:00","event_type":"ok","target":"x","details":{}}\n'
'this is not json\n'
'{"timestamp":"2026-01-01T00:00:00+00:00","event_type":"also_ok","target":"y","details":{}}\n',
encoding="utf-8",
)
events = read_events(tmp_path)
assert len(events) == 2
assert events[0].event_type == "ok"
assert events[1].event_type == "also_ok"
# -- CLI command events --
COMMANDS = [
"rdb init",
"rdb discover",
"rdb ask_more",
"rdb status",
"rdb next",
"rdb start",
"rdb complete",
"rdb handoff",
"rdb prompt",
"rdb guardrails",
]
def test_record_event_creates_command_events(tmp_path: Path) -> None:
"""Each CLI command records an event with event_type 'command' and target 'rdb <name>'."""
for target in COMMANDS:
record_event(tmp_path, "command", target)
events = read_events(tmp_path)
assert len(events) == len(COMMANDS)
for event in events:
assert event.event_type == "command"
def test_record_event_target_matches_command_name(tmp_path: Path) -> None:
"""Event target matches 'rdb <cmd>' format."""
for expected in COMMANDS:
record_event(tmp_path, "command", expected)
events = read_events(tmp_path)
targets = [e.target for e in events]
assert set(targets) == set(COMMANDS)
def test_record_command_event_has_timestamp(tmp_path: Path) -> None:
"""Each command event has a non-empty timestamp."""
record_event(tmp_path, "command", "rdb status")
events = read_events(tmp_path)
assert len(events) == 1
assert events[0].timestamp
assert "+" in events[0].timestamp # UTC offset
def test_record_command_event_with_details(tmp_path: Path) -> None:
"""Commands like start/complete can pass details dict."""
record_event(tmp_path, "command", "rdb start", {"task_id": "TASK-013"})
events = read_events(tmp_path)
assert events[0].details["task_id"] == "TASK-013"
record_event(tmp_path, "command", "rdb complete", {"task_id": "TASK-007"})
events = read_events(tmp_path)
assert events[-1].details["task_id"] == "TASK-007"
# -- Helpers --
def log_lines(root: Path) -> list[str]:
return (root / ".rdb" / "session-log.jsonl").read_text(encoding="utf-8").strip().splitlines()
+30
View File
@@ -36,3 +36,33 @@ def test_init_skips_existing_files(tmp_path: Path) -> None:
assert skipped == list(CONTEXT_FILES) assert skipped == list(CONTEXT_FILES)
assert (tmp_path / "TASKS.md").read_text(encoding="utf-8") == "existing" assert (tmp_path / "TASKS.md").read_text(encoding="utf-8") == "existing"
def test_required_context_templates_exist() -> None:
"""Ensure all six required standard context files are defined."""
required = [
"context/company-context.md",
"context/development-context.md",
"context/infrastructure-context.md",
"context/agent-guidelines.md",
"context/project-brief.md",
"context/architecture.md",
]
for rel_path in required:
assert rel_path in CONTEXT_FILES, f"Missing template: {rel_path}"
def test_required_templates_have_headings_and_guidance() -> None:
"""Each required context file must contain headings and placeholder guidance."""
required = [
"context/company-context.md",
"context/development-context.md",
"context/infrastructure-context.md",
"context/agent-guidelines.md",
"context/project-brief.md",
"context/architecture.md",
]
for rel_path in required:
content = CONTEXT_FILES[rel_path]
assert "# " in content, f"{rel_path} missing H1 heading"
assert "## " in content, f"{rel_path} missing H2 headings"