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.
Files changed (64) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/claude/agent-adapter.js +2 -1
  3. package/bundled/adapters/claude/command-adapter.js +4 -3
  4. package/bundled/agents/prizm-dev-team-dev.md +1 -1
  5. package/bundled/dev-pipeline/README.md +3 -4
  6. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +2 -3
  7. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +2 -2
  8. package/bundled/dev-pipeline/launch-daemon.sh +2 -2
  9. package/bundled/dev-pipeline/lib/branch.sh +76 -0
  10. package/bundled/dev-pipeline/run-bugfix.sh +58 -149
  11. package/bundled/dev-pipeline/run.sh +60 -153
  12. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +1 -1
  13. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +8 -16
  14. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +10 -18
  15. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -24
  16. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +6 -6
  17. package/bundled/dev-pipeline/tests/conftest.py +19 -131
  18. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +207 -0
  19. package/bundled/dev-pipeline/tests/test_utils.py +51 -110
  20. package/bundled/rules/prizm/prizm-commit-workflow.md +3 -3
  21. package/bundled/skills/_metadata.json +15 -16
  22. package/bundled/skills/app-planner/SKILL.md +8 -7
  23. package/bundled/skills/bug-fix-workflow/SKILL.md +174 -0
  24. package/bundled/skills/bug-planner/SKILL.md +20 -32
  25. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +3 -5
  26. package/bundled/skills/dev-pipeline-launcher/SKILL.md +4 -6
  27. package/bundled/skills/feature-workflow/SKILL.md +25 -42
  28. package/bundled/skills/prizm-kit/SKILL.md +57 -21
  29. package/bundled/skills/prizm-kit/assets/{claude-md-template.md → project-memory-template.md} +2 -2
  30. package/bundled/skills/prizmkit-analyze/SKILL.md +41 -29
  31. package/bundled/skills/prizmkit-clarify/SKILL.md +40 -30
  32. package/bundled/skills/prizmkit-code-review/SKILL.md +48 -43
  33. package/bundled/skills/prizmkit-committer/SKILL.md +30 -68
  34. package/bundled/skills/prizmkit-implement/SKILL.md +48 -26
  35. package/bundled/skills/prizmkit-init/SKILL.md +57 -66
  36. package/bundled/skills/prizmkit-plan/SKILL.md +46 -20
  37. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +60 -19
  38. package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +23 -23
  39. package/bundled/skills/prizmkit-retrospective/SKILL.md +142 -65
  40. package/bundled/skills/prizmkit-retrospective/assets/retrospective-template.md +13 -0
  41. package/bundled/skills/prizmkit-specify/SKILL.md +63 -13
  42. package/bundled/skills/refactor-workflow/SKILL.md +105 -49
  43. package/bundled/team/prizm-dev-team.json +2 -2
  44. package/package.json +1 -1
  45. package/src/scaffold.js +3 -3
  46. package/bundled/dev-pipeline/lib/worktree.sh +0 -164
  47. package/bundled/dev-pipeline/tests/__init__.py +0 -0
  48. package/bundled/dev-pipeline/tests/test_check_session.py +0 -131
  49. package/bundled/dev-pipeline/tests/test_cleanup_logs.py +0 -119
  50. package/bundled/dev-pipeline/tests/test_detect_stuck.py +0 -207
  51. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +0 -181
  52. package/bundled/dev-pipeline/tests/test_generate_prompt.py +0 -190
  53. package/bundled/dev-pipeline/tests/test_init_bugfix_pipeline.py +0 -153
  54. package/bundled/dev-pipeline/tests/test_init_pipeline.py +0 -241
  55. package/bundled/dev-pipeline/tests/test_update_bug_status.py +0 -142
  56. package/bundled/dev-pipeline/tests/test_update_feature_status.py +0 -338
  57. package/bundled/dev-pipeline/tests/test_worktree.py +0 -236
  58. package/bundled/dev-pipeline/tests/test_worktree_integration.py +0 -796
  59. package/bundled/skills/prizm-kit/assets/codebuddy-md-template.md +0 -35
  60. package/bundled/skills/prizm-kit/assets/hooks/prizm-commit-hook.json +0 -15
  61. package/bundled/skills/prizmkit-summarize/SKILL.md +0 -51
  62. package/bundled/skills/prizmkit-summarize/assets/registry-template.md +0 -18
  63. package/bundled/templates/hooks/commit-intent-claude.json +0 -26
  64. /package/bundled/templates/hooks/{commit-intent-codebuddy.json → commit-intent.json} +0 -0
@@ -19,14 +19,14 @@
19
19
  "name": "dev",
20
20
  "role": "developer",
21
21
  "agentDefinition": "prizm-dev-team-dev",
22
- "prompt": "You are a Dev Agent of the prizm-dev-team. Follow prizmkit.implement workflow with TDD. Read tasks.md/plan.md/spec.md, implement task-by-task, mark completed tasks [x]. Check .prizm-docs/ TRAPS before implementing.",
22
+ "prompt": "You are a Dev Agent of the prizm-dev-team. Follow /prizmkit-implement workflow with TDD. Read tasks.md/plan.md/spec.md, implement task-by-task, mark completed tasks [x]. Check .prizm-docs/ TRAPS before implementing.",
23
23
  "subscriptions": ["*"]
24
24
  },
25
25
  {
26
26
  "name": "reviewer",
27
27
  "role": "reviewer",
28
28
  "agentDefinition": "prizm-dev-team-reviewer",
29
- "prompt": "You are the Reviewer Agent of the prizm-dev-team. In Phase 4: run prizmkit.analyze for cross-document consistency. In Phase 6: run prizmkit.code-review for spec compliance and code quality, write and execute integration tests.",
29
+ "prompt": "You are the Reviewer Agent of the prizm-dev-team. In Phase 4: run /prizmkit-analyze for cross-document consistency. In Phase 6: run /prizmkit-code-review for spec compliance and code quality, write and execute integration tests.",
30
30
  "subscriptions": ["*"]
31
31
  }
32
32
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.0.45",
3
+ "version": "1.0.58",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/scaffold.js CHANGED
@@ -367,7 +367,7 @@ export async function installSettings(platform, projectRoot, options, dryRun) {
367
367
 
368
368
  // Read hook definition from unified template
369
369
  const templatesDir = getTemplatesDir();
370
- const hookTemplate = await fs.readJSON(path.join(templatesDir, 'hooks', 'commit-intent-codebuddy.json'));
370
+ const hookTemplate = await fs.readJSON(path.join(templatesDir, 'hooks', 'commit-intent.json'));
371
371
 
372
372
  await fs.writeFile(settingsPath, JSON.stringify(hookTemplate, null, 2));
373
373
  console.log(chalk.green(` ✓ .codebuddy/settings.json`));
@@ -392,7 +392,7 @@ export async function installSettings(platform, projectRoot, options, dryRun) {
392
392
 
393
393
  // Read hook definition from unified template
394
394
  const templatesDir = getTemplatesDir();
395
- const hookTemplate = await fs.readJSON(path.join(templatesDir, 'hooks', 'commit-intent-claude.json'));
395
+ const hookTemplate = await fs.readJSON(path.join(templatesDir, 'hooks', 'commit-intent.json'));
396
396
 
397
397
  // Settings
398
398
  const permissions = [
@@ -508,7 +508,7 @@ export async function installPrizmkitScripts(projectRoot, dryRun) {
508
508
  async function installProjectMemory(platform, projectRoot, dryRun) {
509
509
  const templatesDir = getTemplatesDir();
510
510
  const skillsDir = getSkillsDir();
511
- const templateName = platform === 'claude' ? 'claude-md-template.md' : 'codebuddy-md-template.md';
511
+ const templateName = 'project-memory-template.md';
512
512
  const targetName = platform === 'claude' ? 'CLAUDE.md' : 'CODEBUDDY.md';
513
513
  const targetPath = path.join(projectRoot, targetName);
514
514
 
@@ -1,164 +0,0 @@
1
- #!/usr/bin/env bash
2
- # ============================================================
3
- # dev-pipeline/lib/worktree.sh - Git Worktree Lifecycle Library
4
- #
5
- # Shared by run.sh and run-bugfix.sh to isolate AI CLI sessions
6
- # in separate git worktrees. Each session runs on its own branch
7
- # inside a worktree directory, enabling parallel-safe execution
8
- # and clean main branch history.
9
- #
10
- # Functions:
11
- # worktree_create — Create worktree + branch before session
12
- # worktree_merge — Merge worktree branch into target after success
13
- # worktree_cleanup — Remove worktree directory and branch
14
- # worktree_prune_stale — Prune stale worktree references
15
- #
16
- # Environment:
17
- # USE_WORKTREE — Set to 1 to enable (default), 0 to disable
18
- # AUTO_PUSH — Set to 1 to auto-push after successful merge
19
- # ============================================================
20
-
21
- # worktree_create <project_root> <worktree_base_dir> <session_id> <source_branch>
22
- #
23
- # Creates a git worktree and sets global vars:
24
- # _WORKTREE_PATH — absolute path to the worktree directory
25
- # _WORKTREE_BRANCH — branch name (worktree/<session_id>)
26
- #
27
- # Returns 0 on success, 1 on failure.
28
- worktree_create() {
29
- local project_root="$1"
30
- local worktree_base_dir="$2"
31
- local session_id="$3"
32
- local source_branch="$4"
33
-
34
- _WORKTREE_PATH=""
35
- _WORKTREE_BRANCH=""
36
-
37
- local branch_name="worktree/${session_id}"
38
- local worktree_path="${worktree_base_dir}/${session_id}"
39
-
40
- # Ensure base directory exists
41
- mkdir -p "$worktree_base_dir"
42
-
43
- # Check if worktree path already exists
44
- if [[ -d "$worktree_path" ]]; then
45
- log_warn "Worktree path already exists: $worktree_path"
46
- return 1
47
- fi
48
-
49
- # Check if branch already exists
50
- if git -C "$project_root" rev-parse --verify "$branch_name" >/dev/null 2>&1; then
51
- log_warn "Branch already exists: $branch_name"
52
- return 1
53
- fi
54
-
55
- # Create worktree with new branch
56
- if ! git -C "$project_root" worktree add -b "$branch_name" "$worktree_path" "$source_branch" 2>/dev/null; then
57
- log_error "Failed to create worktree at $worktree_path"
58
- return 1
59
- fi
60
-
61
- _WORKTREE_PATH="$worktree_path"
62
- _WORKTREE_BRANCH="$branch_name"
63
-
64
- log_info "Created worktree: $worktree_path (branch: $branch_name)"
65
- return 0
66
- }
67
-
68
- # worktree_merge <project_root> <worktree_branch> <target_branch> <item_id> <session_id>
69
- #
70
- # Merges worktree branch into target branch (no fast-forward).
71
- # Sets global var:
72
- # _MERGE_RESULT — "success", "conflict", or "error"
73
- #
74
- # Returns 0 on success, 1 on conflict, 2 on other error.
75
- worktree_merge() {
76
- local project_root="$1"
77
- local worktree_branch="$2"
78
- local target_branch="$3"
79
- local item_id="$4"
80
- local session_id="$5"
81
-
82
- _MERGE_RESULT=""
83
-
84
- # Switch to target branch in the main working tree
85
- local current_branch
86
- current_branch=$(git -C "$project_root" rev-parse --abbrev-ref HEAD 2>/dev/null) || {
87
- log_error "Failed to determine current branch"
88
- _MERGE_RESULT="error"
89
- return 2
90
- }
91
-
92
- if [[ "$current_branch" != "$target_branch" ]]; then
93
- if ! git -C "$project_root" checkout "$target_branch" 2>/dev/null; then
94
- log_error "Failed to checkout target branch: $target_branch"
95
- _MERGE_RESULT="error"
96
- return 2
97
- fi
98
- fi
99
-
100
- # Attempt merge (no fast-forward to preserve history)
101
- local merge_msg="Merge ${worktree_branch} for ${item_id} (session: ${session_id})"
102
- if git -C "$project_root" merge --no-ff -m "$merge_msg" "$worktree_branch" 2>/dev/null; then
103
- _MERGE_RESULT="success"
104
- log_success "Merged $worktree_branch into $target_branch"
105
- return 0
106
- else
107
- # Check if it's a merge conflict
108
- if git -C "$project_root" diff --name-only --diff-filter=U 2>/dev/null | head -1 | read -r _; then
109
- # Abort the merge to leave working tree clean
110
- git -C "$project_root" merge --abort 2>/dev/null || true
111
- _MERGE_RESULT="conflict"
112
- log_warn "Merge conflict detected for $worktree_branch"
113
- return 1
114
- else
115
- git -C "$project_root" merge --abort 2>/dev/null || true
116
- _MERGE_RESULT="error"
117
- log_error "Merge failed for $worktree_branch"
118
- return 2
119
- fi
120
- fi
121
- }
122
-
123
- # worktree_cleanup <project_root> <worktree_path> <worktree_branch>
124
- #
125
- # Removes worktree directory and deletes the branch.
126
- # Idempotent — safe to call even if worktree/branch don't exist.
127
- #
128
- # Returns 0 always.
129
- worktree_cleanup() {
130
- local project_root="$1"
131
- local worktree_path="$2"
132
- local worktree_branch="$3"
133
-
134
- # Remove the worktree (if it exists)
135
- if [[ -n "$worktree_path" && -d "$worktree_path" ]]; then
136
- git -C "$project_root" worktree remove --force "$worktree_path" 2>/dev/null || {
137
- # Fallback: manual removal
138
- rm -rf "$worktree_path" 2>/dev/null || true
139
- }
140
- log_info "Removed worktree: $worktree_path"
141
- fi
142
-
143
- # Delete the branch (if it exists)
144
- if [[ -n "$worktree_branch" ]]; then
145
- git -C "$project_root" branch -D "$worktree_branch" 2>/dev/null || true
146
- fi
147
-
148
- # Prune stale worktree entries
149
- git -C "$project_root" worktree prune 2>/dev/null || true
150
-
151
- return 0
152
- }
153
-
154
- # worktree_prune_stale <project_root>
155
- #
156
- # Runs `git worktree prune` to clean up stale worktree references.
157
- #
158
- # Returns 0 always.
159
- worktree_prune_stale() {
160
- local project_root="$1"
161
-
162
- git -C "$project_root" worktree prune 2>/dev/null || true
163
- return 0
164
- }
File without changes
@@ -1,131 +0,0 @@
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_merge_conflict(self):
118
- data = {"status": "merge_conflict"}
119
- assert determine_status(data) == "merge_conflict"
120
-
121
- def test_unknown_status(self):
122
- data = {"status": "something_weird"}
123
- assert determine_status(data) == "crashed"
124
-
125
- def test_empty_status(self):
126
- data = {"status": ""}
127
- assert determine_status(data) == "crashed"
128
-
129
- def test_missing_status(self):
130
- data = {}
131
- assert determine_status(data) == "crashed"
@@ -1,119 +0,0 @@
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
@@ -1,207 +0,0 @@
1
- """Tests for detect-stuck.py."""
2
-
3
- import json
4
- import os
5
- import sys
6
- import pytest
7
- from datetime import datetime, timezone, timedelta
8
-
9
-
10
- def _import_detect_stuck():
11
- import importlib.util
12
- path = os.path.join(
13
- os.path.dirname(__file__), "..", "scripts", "detect-stuck.py"
14
- )
15
- spec = importlib.util.spec_from_file_location("detect_stuck", path)
16
- mod = importlib.util.module_from_spec(spec)
17
- sys.modules["detect_stuck"] = mod
18
- spec.loader.exec_module(mod)
19
- return mod
20
-
21
-
22
- ds = _import_detect_stuck()
23
- parse_iso_timestamp = ds.parse_iso_timestamp
24
- check_max_retries = ds.check_max_retries
25
- check_stuck_checkpoint = ds.check_stuck_checkpoint
26
- check_dependency_deadlock = ds.check_dependency_deadlock
27
-
28
-
29
- class TestParseIsoTimestamp:
30
- def test_standard_z_format(self):
31
- result = parse_iso_timestamp("2024-01-15T10:30:00Z")
32
- assert result is not None
33
- assert result.year == 2024
34
- assert result.month == 1
35
- assert result.day == 15
36
- assert result.hour == 10
37
- assert result.minute == 30
38
-
39
- def test_plus_zero_offset(self):
40
- result = parse_iso_timestamp("2024-01-15T10:30:00+00:00")
41
- assert result is not None
42
- assert result.year == 2024
43
-
44
- def test_with_microseconds_z(self):
45
- result = parse_iso_timestamp("2024-01-15T10:30:00.123456Z")
46
- assert result is not None
47
-
48
- def test_with_microseconds_offset(self):
49
- result = parse_iso_timestamp("2024-01-15T10:30:00.123456+00:00")
50
- assert result is not None
51
-
52
- def test_invalid_string(self):
53
- result = parse_iso_timestamp("not a timestamp")
54
- assert result is None
55
-
56
- def test_none_input(self):
57
- result = parse_iso_timestamp(None)
58
- assert result is None
59
-
60
- def test_integer_input(self):
61
- result = parse_iso_timestamp(12345)
62
- assert result is None
63
-
64
- def test_empty_string(self):
65
- result = parse_iso_timestamp("")
66
- assert result is None
67
-
68
-
69
- class TestCheckMaxRetries:
70
- def test_below_max(self):
71
- status = {"retry_count": 1}
72
- result = check_max_retries(status, 3)
73
- assert result is None
74
-
75
- def test_at_max(self):
76
- status = {"retry_count": 3}
77
- result = check_max_retries(status, 3)
78
- assert result is not None
79
- assert result["reason"] == "max_retries_exceeded"
80
-
81
- def test_above_max(self):
82
- status = {"retry_count": 5}
83
- result = check_max_retries(status, 3)
84
- assert result is not None
85
-
86
- def test_zero_retries(self):
87
- status = {"retry_count": 0}
88
- result = check_max_retries(status, 3)
89
- assert result is None
90
-
91
- def test_missing_retry_count(self):
92
- status = {}
93
- result = check_max_retries(status, 3)
94
- assert result is None # defaults to 0
95
-
96
- def test_non_int_retry_count(self):
97
- status = {"retry_count": "not a number"}
98
- result = check_max_retries(status, 3)
99
- assert result is None
100
-
101
-
102
- class TestCheckStuckCheckpoint:
103
- def _setup_sessions(self, tmp_path, checkpoints):
104
- """Create a feature dir with sessions having given checkpoints."""
105
- feature_dir = tmp_path / "feature"
106
- sessions_dir = feature_dir / "sessions"
107
- for i, cp in enumerate(checkpoints):
108
- session_dir = sessions_dir / "session-{:03d}".format(i)
109
- session_dir.mkdir(parents=True)
110
- data = {"checkpoint_reached": cp}
111
- (session_dir / "session-status.json").write_text(
112
- json.dumps(data), encoding="utf-8"
113
- )
114
- return str(feature_dir)
115
-
116
- def test_stuck_at_same_checkpoint(self, tmp_path):
117
- feature_dir = self._setup_sessions(tmp_path, ["CP-3", "CP-3", "CP-3"])
118
- result = check_stuck_checkpoint(feature_dir)
119
- assert result is not None
120
- assert result["reason"] == "stuck_at_checkpoint"
121
- assert "CP-3" in result["details"]
122
-
123
- def test_different_checkpoints(self, tmp_path):
124
- feature_dir = self._setup_sessions(tmp_path, ["CP-1", "CP-2", "CP-3"])
125
- result = check_stuck_checkpoint(feature_dir)
126
- assert result is None
127
-
128
- def test_fewer_than_three_sessions(self, tmp_path):
129
- feature_dir = self._setup_sessions(tmp_path, ["CP-3", "CP-3"])
130
- result = check_stuck_checkpoint(feature_dir)
131
- assert result is None
132
-
133
- def test_no_sessions(self, tmp_path):
134
- feature_dir = tmp_path / "feature"
135
- feature_dir.mkdir()
136
- result = check_stuck_checkpoint(str(feature_dir))
137
- assert result is None
138
-
139
- def test_none_checkpoints(self, tmp_path):
140
- feature_dir = self._setup_sessions(tmp_path, [None, None, None])
141
- result = check_stuck_checkpoint(feature_dir)
142
- assert result is None # None checkpoints don't trigger stuck
143
-
144
- def test_mixed_with_last_three_same(self, tmp_path):
145
- feature_dir = self._setup_sessions(
146
- tmp_path, ["CP-1", "CP-2", "CP-3", "CP-3", "CP-3"]
147
- )
148
- result = check_stuck_checkpoint(feature_dir)
149
- assert result is not None
150
- assert "CP-3" in result["details"]
151
-
152
-
153
- class TestCheckDependencyDeadlock:
154
- def test_dependency_failed(self, state_dir):
155
- # Create a failed dependency
156
- dep_dir = os.path.join(state_dir, "features", "F-001")
157
- os.makedirs(dep_dir, exist_ok=True)
158
- with open(os.path.join(dep_dir, "status.json"), "w") as f:
159
- json.dump({"status": "failed"}, f)
160
-
161
- feature_list_data = {
162
- "features": [
163
- {"id": "F-001", "dependencies": []},
164
- {"id": "F-002", "dependencies": ["F-001"]},
165
- ]
166
- }
167
- result = check_dependency_deadlock("F-002", feature_list_data, state_dir)
168
- assert result is not None
169
- assert result["reason"] == "dependency_failed"
170
- assert "F-001" in result["details"]
171
-
172
- def test_dependency_completed(self, state_dir):
173
- dep_dir = os.path.join(state_dir, "features", "F-001")
174
- os.makedirs(dep_dir, exist_ok=True)
175
- with open(os.path.join(dep_dir, "status.json"), "w") as f:
176
- json.dump({"status": "completed"}, f)
177
-
178
- feature_list_data = {
179
- "features": [
180
- {"id": "F-001", "dependencies": []},
181
- {"id": "F-002", "dependencies": ["F-001"]},
182
- ]
183
- }
184
- result = check_dependency_deadlock("F-002", feature_list_data, state_dir)
185
- assert result is None
186
-
187
- def test_no_dependencies(self, state_dir):
188
- feature_list_data = {
189
- "features": [
190
- {"id": "F-001", "dependencies": []},
191
- ]
192
- }
193
- result = check_dependency_deadlock("F-001", feature_list_data, state_dir)
194
- assert result is None
195
-
196
- def test_none_feature_list(self, state_dir):
197
- result = check_dependency_deadlock("F-001", None, state_dir)
198
- assert result is None
199
-
200
- def test_feature_not_found(self, state_dir):
201
- feature_list_data = {
202
- "features": [
203
- {"id": "F-001", "dependencies": []},
204
- ]
205
- }
206
- result = check_dependency_deadlock("F-999", feature_list_data, state_dir)
207
- assert result is None