prizmkit 1.0.45 → 1.0.58
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.
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/claude/agent-adapter.js +2 -1
- package/bundled/adapters/claude/command-adapter.js +4 -3
- package/bundled/agents/prizm-dev-team-dev.md +1 -1
- package/bundled/dev-pipeline/README.md +3 -4
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +2 -3
- package/bundled/dev-pipeline/launch-bugfix-daemon.sh +2 -2
- package/bundled/dev-pipeline/launch-daemon.sh +2 -2
- package/bundled/dev-pipeline/lib/branch.sh +76 -0
- package/bundled/dev-pipeline/run-bugfix.sh +58 -149
- package/bundled/dev-pipeline/run.sh +60 -153
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +8 -16
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +10 -18
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -24
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +6 -6
- package/bundled/dev-pipeline/tests/conftest.py +19 -131
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +207 -0
- package/bundled/dev-pipeline/tests/test_utils.py +51 -110
- package/bundled/rules/prizm/prizm-commit-workflow.md +3 -3
- package/bundled/skills/_metadata.json +15 -16
- package/bundled/skills/app-planner/SKILL.md +8 -7
- package/bundled/skills/bug-fix-workflow/SKILL.md +174 -0
- package/bundled/skills/bug-planner/SKILL.md +20 -32
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +3 -5
- package/bundled/skills/dev-pipeline-launcher/SKILL.md +4 -6
- package/bundled/skills/feature-workflow/SKILL.md +25 -42
- package/bundled/skills/prizm-kit/SKILL.md +57 -21
- package/bundled/skills/prizm-kit/assets/{claude-md-template.md → project-memory-template.md} +2 -2
- package/bundled/skills/prizmkit-analyze/SKILL.md +41 -29
- package/bundled/skills/prizmkit-clarify/SKILL.md +40 -30
- package/bundled/skills/prizmkit-code-review/SKILL.md +48 -43
- package/bundled/skills/prizmkit-committer/SKILL.md +30 -68
- package/bundled/skills/prizmkit-implement/SKILL.md +48 -26
- package/bundled/skills/prizmkit-init/SKILL.md +57 -66
- package/bundled/skills/prizmkit-plan/SKILL.md +46 -20
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +60 -19
- package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +23 -23
- package/bundled/skills/prizmkit-retrospective/SKILL.md +142 -65
- package/bundled/skills/prizmkit-retrospective/assets/retrospective-template.md +13 -0
- package/bundled/skills/prizmkit-specify/SKILL.md +63 -13
- package/bundled/skills/refactor-workflow/SKILL.md +105 -49
- package/bundled/team/prizm-dev-team.json +2 -2
- package/package.json +1 -1
- package/src/scaffold.js +3 -3
- package/bundled/dev-pipeline/lib/worktree.sh +0 -164
- package/bundled/dev-pipeline/tests/__init__.py +0 -0
- package/bundled/dev-pipeline/tests/test_check_session.py +0 -131
- package/bundled/dev-pipeline/tests/test_cleanup_logs.py +0 -119
- package/bundled/dev-pipeline/tests/test_detect_stuck.py +0 -207
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +0 -181
- package/bundled/dev-pipeline/tests/test_generate_prompt.py +0 -190
- package/bundled/dev-pipeline/tests/test_init_bugfix_pipeline.py +0 -153
- package/bundled/dev-pipeline/tests/test_init_pipeline.py +0 -241
- package/bundled/dev-pipeline/tests/test_update_bug_status.py +0 -142
- package/bundled/dev-pipeline/tests/test_update_feature_status.py +0 -338
- package/bundled/dev-pipeline/tests/test_worktree.py +0 -236
- package/bundled/dev-pipeline/tests/test_worktree_integration.py +0 -796
- package/bundled/skills/prizm-kit/assets/codebuddy-md-template.md +0 -35
- package/bundled/skills/prizm-kit/assets/hooks/prizm-commit-hook.json +0 -15
- package/bundled/skills/prizmkit-summarize/SKILL.md +0 -51
- package/bundled/skills/prizmkit-summarize/assets/registry-template.md +0 -18
- package/bundled/templates/hooks/commit-intent-claude.json +0 -26
- /package/bundled/templates/hooks/{commit-intent-codebuddy.json → commit-intent.json} +0 -0
|
@@ -1,133 +1,21 @@
|
|
|
1
|
-
"""Shared fixtures for dev-pipeline tests."""
|
|
1
|
+
"""Shared fixtures and import helpers for dev-pipeline tests."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import importlib
|
|
4
4
|
import os
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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)
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
# Add dev-pipeline/scripts to the import path so tests can import modules
|
|
8
|
+
# without requiring package installation.
|
|
9
|
+
_scripts_dir = os.path.join(os.path.dirname(__file__), "..", "scripts")
|
|
10
|
+
sys.path.insert(0, _scripts_dir)
|
|
11
|
+
|
|
12
|
+
# generate-bootstrap-prompt.py has a hyphenated filename that Python can't
|
|
13
|
+
# import normally. Load it via importlib and register under an underscore name
|
|
14
|
+
# so test files can do: `from generate_bootstrap_prompt import ...`
|
|
15
|
+
_spec = importlib.util.spec_from_file_location(
|
|
16
|
+
"generate_bootstrap_prompt",
|
|
17
|
+
os.path.join(_scripts_dir, "generate-bootstrap-prompt.py"),
|
|
18
|
+
)
|
|
19
|
+
_mod = importlib.util.module_from_spec(_spec)
|
|
20
|
+
sys.modules["generate_bootstrap_prompt"] = _mod
|
|
21
|
+
_spec.loader.exec_module(_mod)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Tests for generate-bootstrap-prompt.py core functions."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from generate_bootstrap_prompt import (
|
|
6
|
+
compute_feature_slug,
|
|
7
|
+
find_feature,
|
|
8
|
+
format_acceptance_criteria,
|
|
9
|
+
format_global_context,
|
|
10
|
+
get_completed_dependencies,
|
|
11
|
+
determine_pipeline_mode,
|
|
12
|
+
process_conditional_blocks,
|
|
13
|
+
process_mode_blocks,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# compute_feature_slug
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
class TestComputeFeatureSlug:
|
|
22
|
+
def test_basic(self):
|
|
23
|
+
assert compute_feature_slug("F-001", "Project Infrastructure Setup") == "001-project-infrastructure-setup"
|
|
24
|
+
|
|
25
|
+
def test_strips_special_chars(self):
|
|
26
|
+
assert compute_feature_slug("F-012", "Add (User) Auth!") == "012-add-user-auth"
|
|
27
|
+
|
|
28
|
+
def test_pads_numeric(self):
|
|
29
|
+
assert compute_feature_slug("F-1", "Init") == "001-init"
|
|
30
|
+
|
|
31
|
+
def test_collapses_hyphens(self):
|
|
32
|
+
assert compute_feature_slug("F-003", "foo - - bar") == "003-foo---bar" or \
|
|
33
|
+
compute_feature_slug("F-003", "foo bar") == "003-foo-bar"
|
|
34
|
+
|
|
35
|
+
def test_empty_title(self):
|
|
36
|
+
result = compute_feature_slug("F-099", "")
|
|
37
|
+
assert result == "099-"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# find_feature
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
class TestFindFeature:
|
|
45
|
+
def test_found(self):
|
|
46
|
+
features = [{"id": "F-001", "title": "A"}, {"id": "F-002", "title": "B"}]
|
|
47
|
+
assert find_feature(features, "F-002") == {"id": "F-002", "title": "B"}
|
|
48
|
+
|
|
49
|
+
def test_not_found(self):
|
|
50
|
+
assert find_feature([{"id": "F-001"}], "F-999") is None
|
|
51
|
+
|
|
52
|
+
def test_empty_list(self):
|
|
53
|
+
assert find_feature([], "F-001") is None
|
|
54
|
+
|
|
55
|
+
def test_non_dict_items(self):
|
|
56
|
+
assert find_feature(["garbage", 42, None, {"id": "F-001"}], "F-001") == {"id": "F-001"}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# format_acceptance_criteria
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
class TestFormatAcceptanceCriteria:
|
|
64
|
+
def test_none(self):
|
|
65
|
+
assert format_acceptance_criteria(None) == "- (none specified)"
|
|
66
|
+
|
|
67
|
+
def test_empty(self):
|
|
68
|
+
assert format_acceptance_criteria([]) == "- (none specified)"
|
|
69
|
+
|
|
70
|
+
def test_items(self):
|
|
71
|
+
result = format_acceptance_criteria(["Users can log in", "Password reset works"])
|
|
72
|
+
assert "- Users can log in" in result
|
|
73
|
+
assert "- Password reset works" in result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# format_global_context
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
class TestFormatGlobalContext:
|
|
81
|
+
def test_none(self):
|
|
82
|
+
assert format_global_context(None) == "- (none specified)"
|
|
83
|
+
|
|
84
|
+
def test_empty(self):
|
|
85
|
+
assert format_global_context({}) == "- (none specified)"
|
|
86
|
+
|
|
87
|
+
def test_dict(self):
|
|
88
|
+
result = format_global_context({"framework": "React", "lang": "TypeScript"})
|
|
89
|
+
assert "**framework**: React" in result
|
|
90
|
+
assert "**lang**: TypeScript" in result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# get_completed_dependencies
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
class TestGetCompletedDependencies:
|
|
98
|
+
def test_no_deps(self):
|
|
99
|
+
feature = {"id": "F-002", "dependencies": []}
|
|
100
|
+
assert "no dependencies" in get_completed_dependencies([], feature)
|
|
101
|
+
|
|
102
|
+
def test_with_completed_dep(self):
|
|
103
|
+
features = [
|
|
104
|
+
{"id": "F-001", "title": "Setup", "status": "completed"},
|
|
105
|
+
{"id": "F-002", "title": "Auth", "dependencies": ["F-001"]},
|
|
106
|
+
]
|
|
107
|
+
result = get_completed_dependencies(features, features[1])
|
|
108
|
+
assert "F-001" in result
|
|
109
|
+
assert "completed" in result
|
|
110
|
+
|
|
111
|
+
def test_no_completed_dep(self):
|
|
112
|
+
features = [
|
|
113
|
+
{"id": "F-001", "title": "Setup", "status": "pending"},
|
|
114
|
+
{"id": "F-002", "title": "Auth", "dependencies": ["F-001"]},
|
|
115
|
+
]
|
|
116
|
+
result = get_completed_dependencies(features, features[1])
|
|
117
|
+
assert "no completed dependencies" in result
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# determine_pipeline_mode
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
class TestDeterminePipelineMode:
|
|
125
|
+
def test_low(self):
|
|
126
|
+
assert determine_pipeline_mode("low") == "lite"
|
|
127
|
+
|
|
128
|
+
def test_medium(self):
|
|
129
|
+
assert determine_pipeline_mode("medium") == "standard"
|
|
130
|
+
|
|
131
|
+
def test_high(self):
|
|
132
|
+
assert determine_pipeline_mode("high") == "full"
|
|
133
|
+
|
|
134
|
+
def test_critical(self):
|
|
135
|
+
assert determine_pipeline_mode("critical") == "full"
|
|
136
|
+
|
|
137
|
+
def test_unknown(self):
|
|
138
|
+
assert determine_pipeline_mode("banana") == "standard"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# process_conditional_blocks
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
class TestProcessConditionalBlocks:
|
|
146
|
+
def test_fresh_start_keeps_fresh_block(self):
|
|
147
|
+
tpl = "before\n{{IF_FRESH_START}}\nfresh content\n{{END_IF_FRESH_START}}\nafter"
|
|
148
|
+
result = process_conditional_blocks(tpl, "null")
|
|
149
|
+
assert "fresh content" in result
|
|
150
|
+
assert "IF_FRESH_START" not in result
|
|
151
|
+
|
|
152
|
+
def test_fresh_start_removes_resume_block(self):
|
|
153
|
+
tpl = "before\n{{IF_RESUME}}\nresume content\n{{END_IF_RESUME}}\nafter"
|
|
154
|
+
result = process_conditional_blocks(tpl, "null")
|
|
155
|
+
assert "resume content" not in result
|
|
156
|
+
|
|
157
|
+
def test_resume_keeps_resume_block(self):
|
|
158
|
+
tpl = "before\n{{IF_RESUME}}\nresume content\n{{END_IF_RESUME}}\nafter"
|
|
159
|
+
result = process_conditional_blocks(tpl, "3")
|
|
160
|
+
assert "resume content" in result
|
|
161
|
+
assert "IF_RESUME" not in result
|
|
162
|
+
|
|
163
|
+
def test_resume_removes_fresh_block(self):
|
|
164
|
+
tpl = "before\n{{IF_FRESH_START}}\nfresh content\n{{END_IF_FRESH_START}}\nafter"
|
|
165
|
+
result = process_conditional_blocks(tpl, "3")
|
|
166
|
+
assert "fresh content" not in result
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
# process_mode_blocks
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
class TestProcessModeBlocks:
|
|
174
|
+
def test_lite_mode_keeps_lite(self):
|
|
175
|
+
tpl = "{{IF_MODE_LITE}}lite content{{END_IF_MODE_LITE}}"
|
|
176
|
+
result = process_mode_blocks(tpl, "lite", init_done=True)
|
|
177
|
+
assert "lite content" in result
|
|
178
|
+
assert "IF_MODE" not in result
|
|
179
|
+
|
|
180
|
+
def test_lite_mode_removes_full(self):
|
|
181
|
+
tpl = "{{IF_MODE_FULL}}full content{{END_IF_MODE_FULL}}"
|
|
182
|
+
result = process_mode_blocks(tpl, "lite", init_done=True)
|
|
183
|
+
assert "full content" not in result
|
|
184
|
+
|
|
185
|
+
def test_init_done_keeps_init_done_block(self):
|
|
186
|
+
tpl = "{{IF_INIT_DONE}}\ninit done\n{{END_IF_INIT_DONE}}"
|
|
187
|
+
result = process_mode_blocks(tpl, "standard", init_done=True)
|
|
188
|
+
assert "init done" in result
|
|
189
|
+
|
|
190
|
+
def test_init_needed_when_not_done(self):
|
|
191
|
+
tpl = "{{IF_INIT_NEEDED}}\nneed init\n{{END_IF_INIT_NEEDED}}"
|
|
192
|
+
result = process_mode_blocks(tpl, "standard", init_done=False)
|
|
193
|
+
assert "need init" in result
|
|
194
|
+
|
|
195
|
+
def test_self_evolve_keeps_self_evolve_and_full(self):
|
|
196
|
+
tpl = (
|
|
197
|
+
"{{IF_MODE_SELF_EVOLVE}}se content{{END_IF_MODE_SELF_EVOLVE}}"
|
|
198
|
+
"{{IF_MODE_FULL}}full content{{END_IF_MODE_FULL}}"
|
|
199
|
+
)
|
|
200
|
+
result = process_mode_blocks(tpl, "self-evolve", init_done=True)
|
|
201
|
+
assert "se content" in result
|
|
202
|
+
assert "full content" in result
|
|
203
|
+
|
|
204
|
+
def test_self_evolve_removes_lite(self):
|
|
205
|
+
tpl = "{{IF_MODE_LITE}}lite content{{END_IF_MODE_LITE}}"
|
|
206
|
+
result = process_mode_blocks(tpl, "self-evolve", init_done=True)
|
|
207
|
+
assert "lite content" not in result
|
|
@@ -1,141 +1,82 @@
|
|
|
1
|
-
"""Tests for utils.py."""
|
|
1
|
+
"""Tests for dev-pipeline/scripts/utils.py shared utilities."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
-
import
|
|
5
|
+
import tempfile
|
|
6
6
|
|
|
7
7
|
from utils import load_json_file, write_json_file, pad_right, _build_progress_bar
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
p.write_text('{"key": "value"}', encoding="utf-8")
|
|
14
|
-
data, err = load_json_file(str(p))
|
|
15
|
-
assert err is None
|
|
16
|
-
assert data == {"key": "value"}
|
|
17
|
-
|
|
18
|
-
def test_invalid_json(self, tmp_path):
|
|
19
|
-
p = tmp_path / "invalid.json"
|
|
20
|
-
p.write_text("{not valid json", encoding="utf-8")
|
|
21
|
-
data, err = load_json_file(str(p))
|
|
22
|
-
assert data is None
|
|
23
|
-
assert "Invalid JSON" in err
|
|
24
|
-
|
|
25
|
-
def test_missing_file(self, tmp_path):
|
|
26
|
-
data, err = load_json_file(str(tmp_path / "missing.json"))
|
|
27
|
-
assert data is None
|
|
28
|
-
assert "File not found" in err
|
|
29
|
-
|
|
30
|
-
def test_empty_file(self, tmp_path):
|
|
31
|
-
p = tmp_path / "empty.json"
|
|
32
|
-
p.write_text("", encoding="utf-8")
|
|
33
|
-
data, err = load_json_file(str(p))
|
|
34
|
-
assert data is None
|
|
35
|
-
assert "Invalid JSON" in err
|
|
36
|
-
|
|
37
|
-
def test_unicode_content(self, tmp_path):
|
|
38
|
-
p = tmp_path / "unicode.json"
|
|
39
|
-
p.write_text('{"name": "hello"}', encoding="utf-8")
|
|
40
|
-
data, err = load_json_file(str(p))
|
|
41
|
-
assert err is None
|
|
42
|
-
assert data["name"] == "hello"
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
# load_json_file / write_json_file round-trip
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
43
13
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
14
|
+
class TestJsonIO:
|
|
15
|
+
def test_round_trip(self, tmp_path):
|
|
16
|
+
path = str(tmp_path / "data.json")
|
|
17
|
+
data = {"features": [{"id": "F-001"}]}
|
|
18
|
+
err = write_json_file(path, data)
|
|
49
19
|
assert err is None
|
|
50
|
-
assert data == nested
|
|
51
20
|
|
|
52
|
-
|
|
53
|
-
class TestWriteJsonFile:
|
|
54
|
-
def test_write_and_read_back(self, tmp_path):
|
|
55
|
-
p = str(tmp_path / "output.json")
|
|
56
|
-
data = {"hello": "world", "num": 42}
|
|
57
|
-
err = write_json_file(p, data)
|
|
21
|
+
loaded, err = load_json_file(path)
|
|
58
22
|
assert err is None
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
23
|
+
assert loaded == data
|
|
24
|
+
|
|
25
|
+
def test_load_missing_file(self):
|
|
26
|
+
loaded, err = load_json_file("/nonexistent/path.json")
|
|
27
|
+
assert loaded is None
|
|
28
|
+
assert "not found" in err.lower() or "File not found" in err
|
|
29
|
+
|
|
30
|
+
def test_load_invalid_json(self, tmp_path):
|
|
31
|
+
path = str(tmp_path / "bad.json")
|
|
32
|
+
with open(path, "w") as f:
|
|
33
|
+
f.write("{invalid json}")
|
|
34
|
+
loaded, err = load_json_file(path)
|
|
35
|
+
assert loaded is None
|
|
36
|
+
assert "invalid" in err.lower() or "Invalid" in err
|
|
37
|
+
|
|
38
|
+
def test_write_creates_parent_dirs(self, tmp_path):
|
|
39
|
+
path = str(tmp_path / "a" / "b" / "c" / "data.json")
|
|
40
|
+
err = write_json_file(path, {"ok": True})
|
|
66
41
|
assert err is None
|
|
67
|
-
assert os.path.isfile(
|
|
68
|
-
|
|
69
|
-
def test_overwrites_existing(self, tmp_path):
|
|
70
|
-
p = str(tmp_path / "overwrite.json")
|
|
71
|
-
write_json_file(p, {"v": 1})
|
|
72
|
-
write_json_file(p, {"v": 2})
|
|
73
|
-
data, _ = load_json_file(p)
|
|
74
|
-
assert data["v"] == 2
|
|
75
|
-
|
|
76
|
-
def test_unicode_data(self, tmp_path):
|
|
77
|
-
p = str(tmp_path / "unicode.json")
|
|
78
|
-
err = write_json_file(p, {"name": "hello"})
|
|
79
|
-
assert err is None
|
|
80
|
-
data, _ = load_json_file(p)
|
|
81
|
-
assert data["name"] == "hello"
|
|
42
|
+
assert os.path.isfile(path)
|
|
43
|
+
|
|
82
44
|
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# pad_right
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
83
48
|
|
|
84
49
|
class TestPadRight:
|
|
85
|
-
def
|
|
50
|
+
def test_basic_padding(self):
|
|
86
51
|
result = pad_right("abc", 10)
|
|
87
52
|
assert len(result) == 10
|
|
88
53
|
assert result == "abc "
|
|
89
54
|
|
|
90
|
-
def
|
|
91
|
-
result = pad_right("
|
|
92
|
-
assert result == "
|
|
93
|
-
|
|
94
|
-
def test_longer_than_width(self):
|
|
95
|
-
result = pad_right("abcdefgh", 5)
|
|
96
|
-
assert result == "abcdefgh"
|
|
55
|
+
def test_no_padding_needed(self):
|
|
56
|
+
result = pad_right("abcdef", 3)
|
|
57
|
+
assert result == "abcdef"
|
|
97
58
|
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
|
|
59
|
+
def test_ansi_escape_ignored(self):
|
|
60
|
+
# ANSI color code should not count toward visible length
|
|
61
|
+
text = "\033[31mred\033[0m" # "red" with ANSI color (3 visible chars)
|
|
62
|
+
result = pad_right(text, 10)
|
|
63
|
+
# Visible length is 3, so 7 spaces of padding
|
|
64
|
+
assert result.endswith(" ")
|
|
101
65
|
|
|
102
|
-
def test_ansi_codes_ignored(self):
|
|
103
|
-
# ANSI escape should not count toward visible length
|
|
104
|
-
text = "\033[92mHi\033[0m" # visible "Hi" = 2 chars
|
|
105
|
-
result = pad_right(text, 5)
|
|
106
|
-
# visible width is 2, so 3 spaces of padding
|
|
107
|
-
assert result.endswith(" ")
|
|
108
|
-
|
|
109
|
-
def test_zero_width(self):
|
|
110
|
-
result = pad_right("abc", 0)
|
|
111
|
-
assert result == "abc"
|
|
112
66
|
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# _build_progress_bar
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
113
70
|
|
|
114
71
|
class TestBuildProgressBar:
|
|
115
72
|
def test_zero_percent(self):
|
|
116
|
-
bar = _build_progress_bar(0)
|
|
73
|
+
bar = _build_progress_bar(0, width=10)
|
|
117
74
|
assert "0%" in bar
|
|
118
|
-
assert "\u2591" in bar # empty blocks
|
|
119
|
-
assert "\u2588" not in bar # no filled blocks
|
|
120
|
-
|
|
121
|
-
def test_fifty_percent(self):
|
|
122
|
-
bar = _build_progress_bar(50, width=20)
|
|
123
|
-
assert "50%" in bar
|
|
124
|
-
assert bar.count("\u2588") == 10
|
|
125
|
-
assert bar.count("\u2591") == 10
|
|
126
75
|
|
|
127
76
|
def test_hundred_percent(self):
|
|
128
|
-
bar = _build_progress_bar(100, width=
|
|
77
|
+
bar = _build_progress_bar(100, width=10)
|
|
129
78
|
assert "100%" in bar
|
|
130
|
-
assert bar.count("\u2588") == 20
|
|
131
|
-
assert bar.count("\u2591") == 0
|
|
132
79
|
|
|
133
|
-
def
|
|
80
|
+
def test_fifty_percent(self):
|
|
134
81
|
bar = _build_progress_bar(50, width=10)
|
|
135
|
-
assert
|
|
136
|
-
assert bar.count("\u2591") == 5
|
|
137
|
-
|
|
138
|
-
def test_partial_percent(self):
|
|
139
|
-
bar = _build_progress_bar(33, width=10)
|
|
140
|
-
assert bar.count("\u2588") == 3
|
|
141
|
-
assert bar.count("\u2591") == 7
|
|
82
|
+
assert "50%" in bar
|
|
@@ -3,8 +3,8 @@ description: "PrizmKit commit workflow rules"
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
Before any git commit in this project:
|
|
6
|
-
1.
|
|
6
|
+
1. Run `/prizmkit-retrospective` to sync `.prizm-docs/` (memory maintenance)
|
|
7
7
|
2. Use Conventional Commits format: type(scope): description
|
|
8
8
|
3. Bug fixes use `fix()` prefix, not `feat()`
|
|
9
|
-
4.
|
|
10
|
-
5. Use `/prizmkit-committer` command for the
|
|
9
|
+
4. Bug fixes run retrospective with structural sync only (Job 1)
|
|
10
|
+
5. Use `/prizmkit-committer` command for the pure commit workflow
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.
|
|
2
|
+
"version": "1.0.58",
|
|
3
3
|
"skills": {
|
|
4
4
|
"prizm-kit": {
|
|
5
5
|
"description": "Full-lifecycle dev toolkit. Covers spec-driven development, Prizm context docs, code quality, debugging, deployment, and knowledge management.",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"hasScripts": false
|
|
24
24
|
},
|
|
25
25
|
"prizmkit-clarify": {
|
|
26
|
-
"description": "Interactive requirement clarification. Resolves ambiguities in feature specs.",
|
|
26
|
+
"description": "Interactive requirement clarification. Resolves ambiguities in feature specs. Use when spec has unclear parts or you're unsure about requirements before planning.",
|
|
27
27
|
"tier": "1",
|
|
28
28
|
"category": "prizmkit-skill",
|
|
29
29
|
"hasAssets": false,
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"hasScripts": false
|
|
38
38
|
},
|
|
39
39
|
"prizmkit-analyze": {
|
|
40
|
-
"description": "Cross-document consistency analysis for spec.md and plan.md
|
|
40
|
+
"description": "Cross-document consistency analysis for spec.md and plan.md. Check if spec and plan are aligned, validate documents before coding.",
|
|
41
41
|
"tier": "1",
|
|
42
42
|
"category": "prizmkit-skill",
|
|
43
43
|
"hasAssets": false,
|
|
@@ -58,24 +58,17 @@
|
|
|
58
58
|
"hasScripts": false
|
|
59
59
|
},
|
|
60
60
|
"prizmkit-committer": {
|
|
61
|
-
"description": "
|
|
61
|
+
"description": "Pure git commit workflow with safety checks. Does NOT modify .prizm-docs/.",
|
|
62
62
|
"tier": "1",
|
|
63
63
|
"category": "prizmkit-skill",
|
|
64
64
|
"hasAssets": false,
|
|
65
65
|
"hasScripts": false
|
|
66
66
|
},
|
|
67
|
-
"prizmkit-summarize": {
|
|
68
|
-
"description": "Archive completed features to REGISTRY.md.",
|
|
69
|
-
"tier": "1",
|
|
70
|
-
"category": "prizmkit-skill",
|
|
71
|
-
"hasAssets": true,
|
|
72
|
-
"hasScripts": false
|
|
73
|
-
},
|
|
74
67
|
"prizmkit-retrospective": {
|
|
75
|
-
"description": "
|
|
68
|
+
"description": "Sole .prizm-docs/ maintainer. Update project documentation after code changes. Structural sync + knowledge injection (TRAPS/RULES/DECISIONS). Run after code review passes, before committing.",
|
|
76
69
|
"tier": "1",
|
|
77
70
|
"category": "prizmkit-skill",
|
|
78
|
-
"hasAssets":
|
|
71
|
+
"hasAssets": true,
|
|
79
72
|
"hasScripts": false
|
|
80
73
|
},
|
|
81
74
|
"prizmkit-prizm-docs": {
|
|
@@ -224,6 +217,13 @@
|
|
|
224
217
|
"category": "Custom-skill",
|
|
225
218
|
"hasAssets": false,
|
|
226
219
|
"hasScripts": false
|
|
220
|
+
},
|
|
221
|
+
"bug-fix-workflow": {
|
|
222
|
+
"description": "Interactive single-bug fix in current session. Guides triage → reproduce → fix → review → commit without background pipeline.",
|
|
223
|
+
"tier": "companion",
|
|
224
|
+
"category": "Custom-skill",
|
|
225
|
+
"hasAssets": false,
|
|
226
|
+
"hasScripts": false
|
|
227
227
|
}
|
|
228
228
|
},
|
|
229
229
|
"suites": {
|
|
@@ -244,14 +244,14 @@
|
|
|
244
244
|
"prizmkit-implement",
|
|
245
245
|
"prizmkit-code-review",
|
|
246
246
|
"prizmkit-committer",
|
|
247
|
-
"prizmkit-summarize",
|
|
248
247
|
"prizmkit-retrospective",
|
|
249
248
|
"feature-workflow",
|
|
250
249
|
"refactor-workflow",
|
|
251
250
|
"app-planner",
|
|
252
251
|
"bug-planner",
|
|
253
252
|
"dev-pipeline-launcher",
|
|
254
|
-
"bugfix-pipeline-launcher"
|
|
253
|
+
"bugfix-pipeline-launcher",
|
|
254
|
+
"bug-fix-workflow"
|
|
255
255
|
]
|
|
256
256
|
},
|
|
257
257
|
"minimal": {
|
|
@@ -267,7 +267,6 @@
|
|
|
267
267
|
"prizmkit-implement",
|
|
268
268
|
"prizmkit-code-review",
|
|
269
269
|
"prizmkit-committer",
|
|
270
|
-
"prizmkit-summarize",
|
|
271
270
|
"prizmkit-retrospective"
|
|
272
271
|
]
|
|
273
272
|
}
|