prizmkit 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/bin/create-prizmkit.js +4 -1
  2. package/bundled/VERSION.json +3 -3
  3. package/bundled/adapters/claude/command-adapter.js +35 -4
  4. package/bundled/adapters/claude/rules-adapter.js +6 -58
  5. package/bundled/adapters/claude/team-adapter.js +2 -2
  6. package/bundled/adapters/codebuddy/agent-adapter.js +0 -1
  7. package/bundled/adapters/codebuddy/rules-adapter.js +30 -0
  8. package/bundled/adapters/shared/frontmatter.js +3 -1
  9. package/bundled/dev-pipeline/README.md +13 -3
  10. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +10 -0
  11. package/bundled/dev-pipeline/launch-daemon.sh +18 -4
  12. package/bundled/dev-pipeline/lib/common.sh +105 -0
  13. package/bundled/dev-pipeline/retry-bug.sh +12 -0
  14. package/bundled/dev-pipeline/retry-feature.sh +12 -0
  15. package/bundled/dev-pipeline/run-bugfix.sh +71 -57
  16. package/bundled/dev-pipeline/run.sh +87 -57
  17. package/bundled/dev-pipeline/scripts/check-session-status.py +47 -2
  18. package/bundled/dev-pipeline/scripts/cleanup-logs.py +192 -0
  19. package/bundled/dev-pipeline/scripts/detect-stuck.py +15 -3
  20. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +32 -27
  21. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +23 -23
  22. package/bundled/dev-pipeline/scripts/update-feature-status.py +73 -2
  23. package/bundled/dev-pipeline/scripts/utils.py +22 -0
  24. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +38 -2
  25. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +39 -2
  26. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +39 -2
  27. package/bundled/dev-pipeline/templates/session-status-schema.json +7 -1
  28. package/bundled/dev-pipeline/tests/__init__.py +0 -0
  29. package/bundled/dev-pipeline/tests/conftest.py +133 -0
  30. package/bundled/dev-pipeline/tests/test_check_session.py +127 -0
  31. package/bundled/dev-pipeline/tests/test_cleanup_logs.py +119 -0
  32. package/bundled/dev-pipeline/tests/test_detect_stuck.py +207 -0
  33. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +181 -0
  34. package/bundled/dev-pipeline/tests/test_generate_prompt.py +190 -0
  35. package/bundled/dev-pipeline/tests/test_init_bugfix_pipeline.py +153 -0
  36. package/bundled/dev-pipeline/tests/test_init_pipeline.py +241 -0
  37. package/bundled/dev-pipeline/tests/test_update_bug_status.py +142 -0
  38. package/bundled/dev-pipeline/tests/test_update_feature_status.py +277 -0
  39. package/bundled/dev-pipeline/tests/test_utils.py +141 -0
  40. package/bundled/rules/USAGE.md +153 -0
  41. package/bundled/rules/_rules-metadata.json +43 -0
  42. package/bundled/rules/general/prefer-linux-commands.md +9 -0
  43. package/bundled/rules/prizm/prizm-commit-workflow.md +10 -0
  44. package/bundled/rules/prizm/prizm-documentation.md +19 -0
  45. package/bundled/rules/prizm/prizm-progressive-loading.md +11 -0
  46. package/bundled/skills/_metadata.json +130 -67
  47. package/bundled/skills/app-planner/SKILL.md +252 -499
  48. package/bundled/skills/app-planner/assets/evaluation-guide.md +44 -0
  49. package/bundled/skills/app-planner/scripts/validate-and-generate.py +143 -4
  50. package/bundled/skills/bug-planner/SKILL.md +58 -13
  51. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +5 -7
  52. package/bundled/skills/dev-pipeline-launcher/SKILL.md +16 -7
  53. package/bundled/skills/feature-workflow/SKILL.md +175 -234
  54. package/bundled/skills/prizm-kit/SKILL.md +17 -31
  55. package/bundled/skills/{prizmkit-adr-manager → prizmkit-tool-adr-manager}/SKILL.md +6 -7
  56. package/bundled/skills/{prizmkit-api-doc-generator → prizmkit-tool-api-doc-generator}/SKILL.md +4 -5
  57. package/bundled/skills/{prizmkit-bug-reproducer → prizmkit-tool-bug-reproducer}/SKILL.md +4 -5
  58. package/bundled/skills/{prizmkit-ci-cd-generator → prizmkit-tool-ci-cd-generator}/SKILL.md +4 -5
  59. package/bundled/skills/{prizmkit-db-migration → prizmkit-tool-db-migration}/SKILL.md +4 -5
  60. package/bundled/skills/{prizmkit-dependency-health → prizmkit-tool-dependency-health}/SKILL.md +3 -4
  61. package/bundled/skills/{prizmkit-deployment-strategy → prizmkit-tool-deployment-strategy}/SKILL.md +4 -5
  62. package/bundled/skills/{prizmkit-error-triage → prizmkit-tool-error-triage}/SKILL.md +4 -5
  63. package/bundled/skills/{prizmkit-log-analyzer → prizmkit-tool-log-analyzer}/SKILL.md +4 -5
  64. package/bundled/skills/{prizmkit-monitoring-setup → prizmkit-tool-monitoring-setup}/SKILL.md +4 -5
  65. package/bundled/skills/{prizmkit-onboarding-generator → prizmkit-tool-onboarding-generator}/SKILL.md +4 -5
  66. package/bundled/skills/{prizmkit-perf-profiler → prizmkit-tool-perf-profiler}/SKILL.md +4 -5
  67. package/bundled/skills/{prizmkit-security-audit → prizmkit-tool-security-audit}/SKILL.md +3 -4
  68. package/bundled/skills/{prizmkit-tech-debt-tracker → prizmkit-tool-tech-debt-tracker}/SKILL.md +3 -4
  69. package/bundled/skills/refactor-skill/SKILL.md +371 -0
  70. package/bundled/skills/refactor-workflow/SKILL.md +17 -119
  71. package/package.json +1 -1
  72. package/src/external-skills.js +71 -0
  73. package/src/index.js +62 -4
  74. package/src/metadata.js +36 -0
  75. package/src/scaffold.js +136 -32
  76. package/bundled/skills/prizmkit-bug-fix-workflow/SKILL.md +0 -356
  77. package/bundled/templates/claude-md-template.md +0 -38
  78. package/bundled/templates/codebuddy-md-template.md +0 -35
  79. /package/bundled/skills/{prizmkit-adr-manager → prizmkit-tool-adr-manager}/assets/adr-template.md +0 -0
@@ -130,6 +130,7 @@ Prompt:
130
130
  > 2. Run prizmkit-code-review: verify all acceptance criteria, check code quality and correctness. Only read files mentioned in the Implementation Log.
131
131
  > 3. Run the test suite and report results.
132
132
  > 4. Append a 'Review Notes' section to `context-snapshot.md`: issues found (severity), test results, final verdict.
133
+ > 5. If review uncovers durable pitfalls or conventions, add corresponding TRAPS/RULES notes to relevant `.prizm-docs/` files.
133
134
  > Report verdict: PASS, PASS_WITH_WARNINGS, or NEEDS_FIXES."
134
135
 
135
136
  Wait for Reviewer to return.
@@ -137,6 +138,21 @@ Wait for Reviewer to return.
137
138
 
138
139
  **CP-2**: Tests pass, verdict is not NEEDS_FIXES.
139
140
 
141
+ ### Phase 4.5: Prizm Doc Update (mandatory for feature sessions)
142
+
143
+ Run `prizmkit.doc.update` and sync project docs before commit:
144
+ 1. Use `git diff --cached --name-status` (fallback: `git diff --name-status`) to locate changed modules
145
+ 2. Update affected `.prizm-docs/` files (L1/L2, changelog.prizm)
146
+ 3. Stage documentation updates (`git add .prizm-docs/`) if changed
147
+
148
+ Doc maintenance pass condition (pipeline-enforced): `REGISTRY.md` **or** `.prizm-docs/` changed in the final commit.
149
+
150
+ ### Phase 4.7: Retrospective (feature sessions only, before commit)
151
+
152
+ If this session is a feature (not a bug-fix-only commit), run `prizmkit.retrospective` now — **before committing**.
153
+ Retrospective must update relevant `.prizm-docs/` sections (TRAPS/RULES/DECISIONS) when applicable, so those changes are included in the feature commit.
154
+ Stage any `.prizm-docs/` changes produced: `git add .prizm-docs/`
155
+
140
156
  ### Phase 5: Commit
141
157
 
142
158
  - Run `prizmkit.summarize` → archive to REGISTRY.md
@@ -148,6 +164,7 @@ Wait for Reviewer to return.
148
164
  --feature-id "{{FEATURE_ID}}" --session-id "{{SESSION_ID}}" --action complete
149
165
  ```
150
166
  - Run `prizmkit.committer` → `feat({{FEATURE_ID}}): {{FEATURE_TITLE}}`, do NOT push
167
+ - MANDATORY: commit must be done via `prizmkit.committer` skill. Do NOT run manual `git add`/`git commit` as a substitute.
151
168
 
152
169
  ---
153
170
 
@@ -161,7 +178,7 @@ Write to: `{{SESSION_STATUS_PATH}}`
161
178
  "feature_id": "{{FEATURE_ID}}",
162
179
  "feature_slug": "{{FEATURE_SLUG}}",
163
180
  "exec_tier": 2,
164
- "status": "<success|partial|failed>",
181
+ "status": "<success|partial|failed|commit_missing|docs_missing>",
165
182
  "completed_phases": [0, 1, 2, 3, 4, 5],
166
183
  "current_phase": 5,
167
184
  "checkpoint_reached": "CP-2",
@@ -170,6 +187,8 @@ Write to: `{{SESSION_STATUS_PATH}}`
170
187
  "errors": [],
171
188
  "can_resume": false,
172
189
  "resume_from_phase": null,
190
+ "docs_maintained": true,
191
+ "retrospective_done": true,
173
192
  "artifacts": {
174
193
  "context_snapshot_path": ".prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md",
175
194
  "plan_path": ".prizmkit/specs/{{FEATURE_SLUG}}/plan.md",
@@ -180,6 +199,23 @@ Write to: `{{SESSION_STATUS_PATH}}`
180
199
  }
181
200
  ```
182
201
 
202
+ ### Step 3.1: Final Clean Check (before exit)
203
+
204
+ After writing `session-status.json`, verify repository is clean:
205
+
206
+ ```bash
207
+ git status --short
208
+ ```
209
+
210
+ If any files remain, include them in the last commit:
211
+
212
+ ```bash
213
+ git add -A
214
+ git commit --amend --no-edit
215
+ ```
216
+
217
+ Re-check `git status --short` and ensure it is empty before exiting.
218
+
183
219
  ## Critical Paths
184
220
 
185
221
  | Resource | Path |
@@ -197,5 +233,6 @@ Write to: `{{SESSION_STATUS_PATH}}`
197
233
  - Build context-snapshot.md FIRST; all subagents read it instead of re-reading source files
198
234
  - Do NOT use `run_in_background=true` when spawning subagents
199
235
  - ALWAYS write session-status.json before exiting
200
- - `prizmkit.committer` is mandatory
236
+ - `prizmkit.committer` is mandatory, and must not be replaced with manual git commit commands
237
+ - Before exiting, `git status --short` must be empty
201
238
  - On timeout: check snapshot → model:lite → remaining steps only → max 2 retries → orchestrator fallback
@@ -234,7 +234,7 @@ Wait for Reviewer to return.
234
234
 
235
235
  ### Phase 7: Summarize & Commit — DO NOT SKIP
236
236
 
237
- **For bug fixes**: skip `prizmkit.summarize`, use `fix(<scope>):` commit prefix.
237
+ **For bug fixes**: skip `prizmkit.summarize`, skip retrospective, and use `fix(<scope>):` commit prefix.
238
238
 
239
239
  **7a.** Check if feature already committed:
240
240
  ```bash
@@ -245,6 +245,20 @@ git log --oneline | grep "{{FEATURE_ID}}" | head -3
245
245
 
246
246
  **7b.** Run `prizmkit.summarize` → archive to REGISTRY.md
247
247
 
248
+ **7b.5.** Update prizm-docs (feature sessions):
249
+ - Read the 'Implementation Log' in `context-snapshot.md` to identify changed files
250
+ - For each changed file, check if any related `.prizm-docs/` L1/L2 doc needs updating (new APIs, changed behavior, new TRAPS)
251
+ - Update `.prizm-docs/changelog.prizm`: append one-line entry `- feat({{FEATURE_ID}}): {{FEATURE_TITLE}}`
252
+ - Stage all modified prizm-docs: `git add .prizm-docs/`
253
+ - Pipeline docs pass condition is `REGISTRY.md` OR `.prizm-docs/` changed in final commit
254
+
255
+ **7b.6.** Run `prizmkit.retrospective` (feature sessions only, **before commit**):
256
+ - Extract lessons from completed feature — compare spec/plan/tasks vs actual
257
+ - Update relevant `.prizm-docs/` sections: TRAPS, RULES, DECISIONS
258
+ - Write `.prizmkit/specs/{{FEATURE_SLUG}}/retrospective.md`
259
+ - Stage any `.prizm-docs/` changes: `git add .prizm-docs/`
260
+ - **Skip if this session is bug-fix-only**
261
+
248
262
  **7c.** Mark feature complete:
249
263
  ```bash
250
264
  python3 {{VALIDATOR_SCRIPTS_DIR}}/update-feature-status.py \
@@ -255,6 +269,8 @@ python3 {{VALIDATOR_SCRIPTS_DIR}}/update-feature-status.py \
255
269
 
256
270
  **7d.** Run `prizmkit.committer` → `feat({{FEATURE_ID}}): {{FEATURE_TITLE}}`, do NOT push
257
271
 
272
+ **7e.** MANDATORY: commit must be done via `prizmkit.committer` skill. Do NOT run manual `git add`/`git commit` as a substitute.
273
+
258
274
  ---
259
275
 
260
276
  ## Step 3: Write Session Status
@@ -267,7 +283,7 @@ Write to: `{{SESSION_STATUS_PATH}}`
267
283
  "feature_id": "{{FEATURE_ID}}",
268
284
  "feature_slug": "{{FEATURE_SLUG}}",
269
285
  "exec_tier": 3,
270
- "status": "<success|partial|failed>",
286
+ "status": "<success|partial|failed|commit_missing|docs_missing>",
271
287
  "completed_phases": [0, 1, 2, 3, 4, 5, 6, 7],
272
288
  "current_phase": 7,
273
289
  "checkpoint_reached": "CP-3",
@@ -276,6 +292,8 @@ Write to: `{{SESSION_STATUS_PATH}}`
276
292
  "errors": [],
277
293
  "can_resume": false,
278
294
  "resume_from_phase": null,
295
+ "docs_maintained": true,
296
+ "retrospective_done": true,
279
297
  "artifacts": {
280
298
  "context_snapshot_path": ".prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md",
281
299
  "spec_path": ".prizmkit/specs/{{FEATURE_SLUG}}/spec.md",
@@ -287,6 +305,23 @@ Write to: `{{SESSION_STATUS_PATH}}`
287
305
  }
288
306
  ```
289
307
 
308
+ ### Step 3.1: Final Clean Check (before exit)
309
+
310
+ After writing `session-status.json`, verify repository is clean:
311
+
312
+ ```bash
313
+ git status --short
314
+ ```
315
+
316
+ If any files remain, include them in the last commit:
317
+
318
+ ```bash
319
+ git add -A
320
+ git commit --amend --no-edit
321
+ ```
322
+
323
+ Re-check `git status --short` and ensure it is empty before exiting.
324
+
290
325
  ## Step 4: Team Cleanup
291
326
 
292
327
  No team cleanup needed — agents were spawned directly without TeamCreate.
@@ -311,4 +346,6 @@ No team cleanup needed — agents were spawned directly without TeamCreate.
311
346
  - context-snapshot.md is the team knowledge base: PM writes it once, all agents read it
312
347
  - Do NOT use `run_in_background=true` when spawning agents
313
348
  - ALWAYS write session-status.json before exiting
349
+ - Commit phase must use `prizmkit.committer`; do NOT replace with manual git commit commands
350
+ - Before exiting, `git status --short` must be empty
314
351
  - On timeout: check snapshot → model:lite → remaining steps only → max 2 retries → orchestrator fallback
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "status": {
17
17
  "type": "string",
18
- "enum": ["success", "partial", "failed"]
18
+ "enum": ["success", "partial", "failed", "commit_missing", "docs_missing"]
19
19
  },
20
20
  "completed_phases": {
21
21
  "type": "array",
@@ -58,6 +58,12 @@
58
58
  "can_resume": {
59
59
  "type": "boolean"
60
60
  },
61
+ "docs_maintained": {
62
+ "type": "boolean"
63
+ },
64
+ "retrospective_done": {
65
+ "type": "boolean"
66
+ },
61
67
  "resume_from_phase": {
62
68
  "type": ["integer", "null"]
63
69
  },
File without changes
@@ -0,0 +1,133 @@
1
+ """Shared fixtures for dev-pipeline tests."""
2
+
3
+ import json
4
+ import os
5
+ import pytest
6
+
7
+
8
+ @pytest.fixture
9
+ def sample_feature_list():
10
+ """A valid feature-list.json structure."""
11
+ return {
12
+ "$schema": "dev-pipeline-feature-list-v1",
13
+ "app_name": "TestApp",
14
+ "global_context": {
15
+ "language": "Python",
16
+ "framework": "FastAPI",
17
+ },
18
+ "features": [
19
+ {
20
+ "id": "F-001",
21
+ "title": "Project Infrastructure Setup",
22
+ "description": "Set up the project structure.",
23
+ "priority": 1,
24
+ "estimated_complexity": "low",
25
+ "dependencies": [],
26
+ "acceptance_criteria": ["Has package.json", "Has README.md"],
27
+ "status": "pending",
28
+ },
29
+ {
30
+ "id": "F-002",
31
+ "title": "User Authentication",
32
+ "description": "Implement user login and registration.",
33
+ "priority": 2,
34
+ "estimated_complexity": "high",
35
+ "dependencies": ["F-001"],
36
+ "acceptance_criteria": ["Login works", "Registration works"],
37
+ "status": "pending",
38
+ },
39
+ {
40
+ "id": "F-003",
41
+ "title": "Dashboard View",
42
+ "description": "Create main dashboard.",
43
+ "priority": 3,
44
+ "estimated_complexity": "medium",
45
+ "dependencies": ["F-001", "F-002"],
46
+ "acceptance_criteria": ["Dashboard renders"],
47
+ "status": "pending",
48
+ },
49
+ ],
50
+ }
51
+
52
+
53
+ @pytest.fixture
54
+ def sample_bug_list():
55
+ """A valid bug-fix-list.json structure."""
56
+ return {
57
+ "$schema": "dev-pipeline-bug-fix-list-v1",
58
+ "project_name": "TestProject",
59
+ "global_context": {
60
+ "language": "Python",
61
+ },
62
+ "bugs": [
63
+ {
64
+ "id": "B-001",
65
+ "title": "Login crash on empty password",
66
+ "description": "App crashes when password is empty.",
67
+ "severity": "critical",
68
+ "priority": 1,
69
+ "error_source": {
70
+ "type": "stack_trace",
71
+ "stack_trace": "Traceback ... ValueError",
72
+ "error_message": "Password cannot be empty",
73
+ },
74
+ "verification_type": "automated",
75
+ "acceptance_criteria": ["No crash on empty password"],
76
+ "status": "pending",
77
+ "affected_feature": "F-002",
78
+ "affected_modules": ["auth"],
79
+ },
80
+ {
81
+ "id": "B-002",
82
+ "title": "Slow dashboard loading",
83
+ "description": "Dashboard takes 10s to load.",
84
+ "severity": "medium",
85
+ "priority": 2,
86
+ "error_source": {
87
+ "type": "user_report",
88
+ "reproduction_steps": ["Open dashboard", "Wait 10 seconds"],
89
+ },
90
+ "verification_type": "manual",
91
+ "acceptance_criteria": ["Dashboard loads in <2s"],
92
+ "status": "pending",
93
+ "affected_feature": "F-003",
94
+ "environment": {"os": "macOS", "browser": "Chrome"},
95
+ },
96
+ ],
97
+ }
98
+
99
+
100
+ @pytest.fixture
101
+ def feature_list_file(tmp_path, sample_feature_list):
102
+ """Write sample feature list to a temp file and return the path."""
103
+ path = tmp_path / "feature-list.json"
104
+ path.write_text(json.dumps(sample_feature_list, indent=2), encoding="utf-8")
105
+ return str(path)
106
+
107
+
108
+ @pytest.fixture
109
+ def bug_list_file(tmp_path, sample_bug_list):
110
+ """Write sample bug list to a temp file and return the path."""
111
+ path = tmp_path / "bug-fix-list.json"
112
+ path.write_text(json.dumps(sample_bug_list, indent=2), encoding="utf-8")
113
+ return str(path)
114
+
115
+
116
+ @pytest.fixture
117
+ def state_dir(tmp_path):
118
+ """Create and return a basic state directory."""
119
+ sd = tmp_path / "state"
120
+ sd.mkdir()
121
+ features_dir = sd / "features"
122
+ features_dir.mkdir()
123
+ return str(sd)
124
+
125
+
126
+ @pytest.fixture
127
+ def bugfix_state_dir(tmp_path):
128
+ """Create and return a basic bugfix state directory."""
129
+ sd = tmp_path / "bugfix-state"
130
+ sd.mkdir()
131
+ bugs_dir = sd / "bugs"
132
+ bugs_dir.mkdir()
133
+ return str(sd)
@@ -0,0 +1,127 @@
1
+ """Tests for check-session-status.py."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ import pytest
7
+
8
+
9
+ def _import_check_session():
10
+ import importlib.util
11
+ path = os.path.join(
12
+ os.path.dirname(__file__), "..", "scripts", "check-session-status.py"
13
+ )
14
+ spec = importlib.util.spec_from_file_location("check_session_status", path)
15
+ mod = importlib.util.module_from_spec(spec)
16
+ sys.modules["check_session_status"] = mod
17
+ spec.loader.exec_module(mod)
18
+ return mod
19
+
20
+
21
+ css = _import_check_session()
22
+ validate_required_fields = css.validate_required_fields
23
+ determine_status = css.determine_status
24
+
25
+
26
+ class TestValidateRequiredFields:
27
+ def test_all_present(self):
28
+ data = {
29
+ "session_id": "s-001",
30
+ "feature_id": "F-001",
31
+ "status": "success",
32
+ "timestamp": "2024-01-01T00:00:00Z",
33
+ }
34
+ missing = validate_required_fields(data)
35
+ assert missing == []
36
+
37
+ def test_missing_session_id(self):
38
+ data = {
39
+ "feature_id": "F-001",
40
+ "status": "success",
41
+ "timestamp": "2024-01-01T00:00:00Z",
42
+ }
43
+ missing = validate_required_fields(data)
44
+ assert "session_id" in missing
45
+
46
+ def test_missing_multiple(self):
47
+ data = {"feature_id": "F-001"}
48
+ missing = validate_required_fields(data)
49
+ assert "session_id" in missing
50
+ assert "status" in missing
51
+ assert "timestamp" in missing
52
+
53
+ def test_empty_string_field(self):
54
+ data = {
55
+ "session_id": "",
56
+ "feature_id": "F-001",
57
+ "status": "success",
58
+ "timestamp": "2024-01-01T00:00:00Z",
59
+ }
60
+ missing = validate_required_fields(data)
61
+ assert "session_id" in missing
62
+
63
+ def test_whitespace_only_field(self):
64
+ data = {
65
+ "session_id": " ",
66
+ "feature_id": "F-001",
67
+ "status": "success",
68
+ "timestamp": "2024-01-01T00:00:00Z",
69
+ }
70
+ missing = validate_required_fields(data)
71
+ assert "session_id" in missing
72
+
73
+ def test_non_string_field(self):
74
+ data = {
75
+ "session_id": 123,
76
+ "feature_id": "F-001",
77
+ "status": "success",
78
+ "timestamp": "2024-01-01T00:00:00Z",
79
+ }
80
+ missing = validate_required_fields(data)
81
+ assert "session_id" in missing
82
+
83
+ def test_empty_data(self):
84
+ missing = validate_required_fields({})
85
+ assert len(missing) == 4
86
+
87
+
88
+ class TestDetermineStatus:
89
+ def test_success(self):
90
+ data = {"status": "success"}
91
+ assert determine_status(data) == "success"
92
+
93
+ def test_failed(self):
94
+ data = {"status": "failed"}
95
+ assert determine_status(data) == "failed"
96
+
97
+ def test_partial_resumable(self):
98
+ data = {"status": "partial", "can_resume": True}
99
+ assert determine_status(data) == "partial_resumable"
100
+
101
+ def test_partial_not_resumable(self):
102
+ data = {"status": "partial", "can_resume": False}
103
+ assert determine_status(data) == "partial_not_resumable"
104
+
105
+ def test_partial_no_resume_key(self):
106
+ data = {"status": "partial"}
107
+ assert determine_status(data) == "partial_not_resumable"
108
+
109
+ def test_commit_missing(self):
110
+ data = {"status": "commit_missing"}
111
+ assert determine_status(data) == "commit_missing"
112
+
113
+ def test_docs_missing(self):
114
+ data = {"status": "docs_missing"}
115
+ assert determine_status(data) == "docs_missing"
116
+
117
+ def test_unknown_status(self):
118
+ data = {"status": "something_weird"}
119
+ assert determine_status(data) == "crashed"
120
+
121
+ def test_empty_status(self):
122
+ data = {"status": ""}
123
+ assert determine_status(data) == "crashed"
124
+
125
+ def test_missing_status(self):
126
+ data = {}
127
+ assert determine_status(data) == "crashed"
@@ -0,0 +1,119 @@
1
+ """Tests for cleanup-logs.py."""
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ import time
8
+
9
+
10
+ def _script_path():
11
+ return os.path.join(
12
+ os.path.dirname(__file__), "..", "scripts", "cleanup-logs.py"
13
+ )
14
+
15
+
16
+ def _write_log_file(path, content):
17
+ os.makedirs(os.path.dirname(path), exist_ok=True)
18
+ with open(path, "w", encoding="utf-8") as f:
19
+ f.write(content)
20
+
21
+
22
+ def test_iter_log_files_only_sessions_logs(state_dir):
23
+ # Targeted log path under sessions/*/logs/
24
+ valid_log = os.path.join(
25
+ state_dir, "features", "F-001", "sessions", "S-001", "logs", "session.log"
26
+ )
27
+ _write_log_file(valid_log, "ok")
28
+
29
+ # logs/ not under sessions should be ignored
30
+ ignored_log = os.path.join(state_dir, "features", "F-001", "logs", "misc.log")
31
+ _write_log_file(ignored_log, "ignore me")
32
+
33
+ # Import script module for direct function test
34
+ import importlib.util
35
+
36
+ path = _script_path()
37
+ spec = importlib.util.spec_from_file_location("cleanup_logs", path)
38
+ mod = importlib.util.module_from_spec(spec)
39
+ sys.modules["cleanup_logs"] = mod
40
+ spec.loader.exec_module(mod)
41
+
42
+ files = list(mod.iter_log_files(state_dir))
43
+ assert valid_log in files
44
+ assert ignored_log not in files
45
+
46
+
47
+ def test_cleanup_removes_old_logs_by_retention(state_dir):
48
+ old_log = os.path.join(
49
+ state_dir, "features", "F-001", "sessions", "S-001", "logs", "session.log"
50
+ )
51
+ new_log = os.path.join(
52
+ state_dir, "features", "F-001", "sessions", "S-002", "logs", "session.log"
53
+ )
54
+
55
+ _write_log_file(old_log, "old")
56
+ _write_log_file(new_log, "new")
57
+
58
+ # Make old log 20 days old
59
+ old_ts = time.time() - (20 * 86400)
60
+ os.utime(old_log, (old_ts, old_ts))
61
+
62
+ result = subprocess.run(
63
+ [
64
+ sys.executable,
65
+ _script_path(),
66
+ "--state-dir",
67
+ state_dir,
68
+ "--retention-days",
69
+ "14",
70
+ "--max-total-mb",
71
+ "1024",
72
+ ],
73
+ check=True,
74
+ capture_output=True,
75
+ text=True,
76
+ )
77
+
78
+ report = json.loads(result.stdout)
79
+ assert report["deleted_by_reason"]["retention"] >= 1
80
+ assert not os.path.exists(old_log)
81
+ assert os.path.exists(new_log)
82
+
83
+
84
+ def test_cleanup_enforces_total_size_cap(state_dir):
85
+ log1 = os.path.join(
86
+ state_dir, "features", "F-001", "sessions", "S-001", "logs", "session.log"
87
+ )
88
+ log2 = os.path.join(
89
+ state_dir, "features", "F-001", "sessions", "S-002", "logs", "session.log"
90
+ )
91
+
92
+ _write_log_file(log1, "a" * 1024)
93
+ _write_log_file(log2, "b" * 1024)
94
+
95
+ # Ensure deterministic ordering: log1 older than log2
96
+ old_ts = time.time() - 120
97
+ new_ts = time.time() - 60
98
+ os.utime(log1, (old_ts, old_ts))
99
+ os.utime(log2, (new_ts, new_ts))
100
+
101
+ result = subprocess.run(
102
+ [
103
+ sys.executable,
104
+ _script_path(),
105
+ "--state-dir",
106
+ state_dir,
107
+ "--retention-days",
108
+ "999",
109
+ "--max-total-mb",
110
+ "0",
111
+ ],
112
+ check=True,
113
+ capture_output=True,
114
+ text=True,
115
+ )
116
+
117
+ report = json.loads(result.stdout)
118
+ assert report["deleted_by_reason"]["size"] >= 1
119
+ assert report["final_total_bytes"] == 0