Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f6933423b | ||
|
|
65dda68dbe | ||
|
|
152096d433 | ||
|
|
c72ad6995e | ||
|
|
a0b28e7d8d | ||
|
|
5bad6ad0f8 | ||
|
|
250cf9316a | ||
|
|
1c2faa3162 | ||
|
|
34f7364c86 | ||
|
|
3ccdea330a | ||
|
|
13f5e10f6f | ||
|
|
6eef97582d | ||
|
|
2fcc822394 | ||
|
|
eb30450f34 | ||
|
|
8222660a3c | ||
|
|
8b96d2c758 | ||
|
|
7a1d36c03c | ||
|
|
318b9471ae | ||
|
|
aee13b045e | ||
|
|
21611b7c4e | ||
|
|
9536f5d0ff | ||
|
|
5456947ac9 | ||
|
|
39728667a7 |
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.*
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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]")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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([
|
||||||
"",
|
"",
|
||||||
"---",
|
"---",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user