prizmkit 1.1.129 → 1.1.131

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 (88) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/.env.example +1 -1
  3. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +445 -8
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +87 -66
  5. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +53 -3
  6. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +55 -25
  7. package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +4 -0
  8. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +15 -1
  9. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +398 -6
  10. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +61 -3
  11. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +64 -4
  12. package/bundled/dev-pipeline/scripts/prompt_framework.py +156 -7
  13. package/bundled/dev-pipeline/scripts/update-bug-status.py +53 -0
  14. package/bundled/dev-pipeline/scripts/update-checkpoint.py +294 -69
  15. package/bundled/dev-pipeline/scripts/update-feature-status.py +57 -0
  16. package/bundled/dev-pipeline/scripts/update-refactor-status.py +53 -0
  17. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
  18. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -14
  19. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +21 -4
  20. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +18 -0
  21. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
  22. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +5 -15
  23. package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +21 -0
  24. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +10 -10
  25. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +16 -0
  26. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +26 -23
  27. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +12 -8
  28. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +1 -1
  29. package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +21 -0
  30. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +362 -3
  31. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +404 -24
  32. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +132 -0
  33. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +91 -0
  34. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +411 -18
  35. package/bundled/dev-pipeline/tests/test_unified_cli.py +3 -3
  36. package/bundled/skills/_metadata.json +62 -25
  37. package/bundled/skills/app-planner/SKILL.md +4 -3
  38. package/bundled/skills/bug-fix-workflow/SKILL.md +89 -180
  39. package/bundled/skills/bug-planner/SKILL.md +12 -27
  40. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +131 -220
  41. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +36 -90
  42. package/bundled/skills/feature-pipeline-launcher/SKILL.md +129 -271
  43. package/bundled/skills/feature-pipeline-launcher/references/configuration.md +35 -76
  44. package/bundled/skills/feature-planner/SKILL.md +27 -22
  45. package/bundled/skills/feature-planner/assets/planning-guide.md +4 -4
  46. package/bundled/skills/feature-workflow/SKILL.md +94 -121
  47. package/bundled/skills/prizmkit/SKILL.md +130 -94
  48. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +181 -0
  49. package/bundled/skills/prizmkit-code-review/SKILL.md +114 -84
  50. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -1
  51. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +181 -0
  52. package/bundled/skills/prizmkit-committer/SKILL.md +131 -47
  53. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +181 -0
  54. package/bundled/skills/prizmkit-implement/SKILL.md +99 -73
  55. package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +1 -1
  56. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +181 -0
  57. package/bundled/skills/prizmkit-init/SKILL.md +5 -5
  58. package/bundled/skills/prizmkit-plan/SKILL.md +132 -105
  59. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +181 -0
  60. package/bundled/skills/prizmkit-retrospective/SKILL.md +111 -53
  61. package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +7 -6
  62. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +5 -4
  63. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +181 -0
  64. package/bundled/skills/prizmkit-test/SKILL.md +28 -6
  65. package/bundled/skills/prizmkit-test/references/examples.md +1 -1
  66. package/bundled/skills/prizmkit-test/references/test-report-template.md +1 -1
  67. package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +181 -0
  68. package/bundled/skills/prizmkit-workflow/SKILL.md +266 -0
  69. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +181 -0
  70. package/bundled/skills/recovery-workflow/SKILL.md +62 -241
  71. package/bundled/skills/recovery-workflow/evals/evals.json +17 -33
  72. package/bundled/skills/recovery-workflow/references/detection.md +28 -52
  73. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +105 -439
  74. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +136 -275
  75. package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +53 -88
  76. package/bundled/skills/refactor-planner/SKILL.md +12 -57
  77. package/bundled/skills/refactor-planner/references/behavior-preservation.md +111 -230
  78. package/bundled/skills/refactor-planner/references/error-recovery.md +37 -0
  79. package/bundled/skills/refactor-planner/references/planning-phases.md +11 -17
  80. package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +1 -1
  81. package/bundled/skills/refactor-workflow/SKILL.md +98 -139
  82. package/bundled/templates/project-memory-template.md +4 -4
  83. package/package.json +1 -1
  84. package/src/index.js +4 -0
  85. package/src/prompts.js +10 -3
  86. package/src/scaffold.js +1 -1
  87. package/bundled/dev-pipeline/tests/test-deploy-safety.sh +0 -223
  88. package/bundled/skills/feature-pipeline-launcher/scripts/preflight-check.py +0 -462
@@ -1,480 +1,146 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- detect-recovery-state.py — Universal workflow recovery detector.
2
+ """Detect resumable pipeline task branches and print exact continuation commands."""
4
3
 
5
- Auto-detects which interactive workflow or pipeline session was interrupted
6
- by inspecting git branch names, durable planning artifacts, pipeline state,
7
- current-workspace plans, review artifacts, and code changes.
8
-
9
- Does NOT run tests — the skill controls test execution so the user can see
10
- real-time output.
11
-
12
- Usage:
13
- python3 detect-recovery-state.py [--project-root .]
14
- """
4
+ from __future__ import annotations
15
5
 
16
6
  import argparse
17
7
  import json
18
- import os
19
- import re
20
8
  import subprocess
21
- import sys
22
9
  from pathlib import Path
23
10
 
24
11
 
25
- def run_git(args, cwd=None):
26
- """Run a git command and return stdout, or empty string on failure."""
12
+ FAMILIES = (
13
+ {
14
+ "name": "feature",
15
+ "state_dir": "features",
16
+ "plan_path": ".prizmkit/plans/feature-list.json",
17
+ "id_prefix": "F-",
18
+ },
19
+ {
20
+ "name": "bugfix",
21
+ "state_dir": "bugfix",
22
+ "plan_path": ".prizmkit/plans/bug-fix-list.json",
23
+ "id_prefix": "B-",
24
+ },
25
+ {
26
+ "name": "refactor",
27
+ "state_dir": "refactor",
28
+ "plan_path": ".prizmkit/plans/refactor-list.json",
29
+ "id_prefix": "R-",
30
+ },
31
+ )
32
+
33
+
34
+ def run_git(project_root: Path, *args: str) -> str:
35
+ """Return stripped Git stdout, or an empty string when Git cannot answer."""
27
36
  try:
28
37
  result = subprocess.run(
29
- ["git"] + args,
38
+ ["git", *args],
39
+ cwd=project_root,
30
40
  capture_output=True,
31
41
  text=True,
32
- cwd=cwd,
33
42
  timeout=10,
43
+ check=False,
34
44
  )
35
- return result.stdout.strip()
36
- except (subprocess.SubprocessError, FileNotFoundError):
45
+ except (OSError, subprocess.SubprocessError):
37
46
  return ""
47
+ return result.stdout.strip() if result.returncode == 0 else ""
38
48
 
39
49
 
40
- def detect_main_branch(project_root):
41
- """Detect the default branch name (main or master)."""
42
- for candidate in ["main", "master"]:
43
- check = run_git(["rev-parse", "--verify", candidate], cwd=project_root)
44
- if check:
45
- return candidate
46
- return "main"
47
-
48
-
49
- def relpath(path, project_root):
50
- return os.path.relpath(path, project_root)
51
-
52
-
53
- def file_exists(project_root, *parts):
54
- path = os.path.join(project_root, *parts)
55
- return path if os.path.isfile(path) else None
56
-
57
-
58
- def dir_has_content(project_root, *parts):
59
- path = os.path.join(project_root, *parts)
60
- return os.path.isdir(path) and bool(os.listdir(path))
61
-
62
-
63
- def extract_bug_id_from_branch(branch):
64
- """Extract bug ID from branch name like fix/B-001-login-crash -> B-001."""
65
- match = re.match(r"fix/(B-\d+)", branch or "")
66
- return match.group(1) if match else None
67
-
68
-
69
- def latest_bugfix_artifact_id(project_root):
70
- bugfix_dir = os.path.join(project_root, ".prizmkit", "bugfix")
71
- if not os.path.isdir(bugfix_dir):
50
+ def load_json(path: Path) -> dict[str, object] | None:
51
+ try:
52
+ data = json.loads(path.read_text(encoding="utf-8"))
53
+ except (OSError, json.JSONDecodeError):
72
54
  return None
73
- bug_ids = sorted(
74
- d for d in os.listdir(bugfix_dir)
75
- if os.path.isdir(os.path.join(bugfix_dir, d))
76
- )
77
- return bug_ids[-1] if bug_ids else None
78
-
79
-
80
- def detect_workflow_type(project_root):
81
- """Priority-ordered signature matching.
82
-
83
- Returns (workflow_type, context_dict) or (None, None).
84
- """
85
- branch = run_git(["branch", "--show-current"], cwd=project_root)
86
-
87
- if branch.startswith("fix/"):
88
- return ("bug-fix-workflow", {
89
- "bug_id": extract_bug_id_from_branch(branch),
90
- "branch": branch,
91
- "signal": "fix branch",
92
- })
93
-
94
- if dir_has_content(project_root, ".prizmkit", "state", "bugfix"):
95
- return ("bug-fix-workflow", {
96
- "bug_id": extract_bug_id_from_branch(branch) or latest_bugfix_artifact_id(project_root),
97
- "branch": branch,
98
- "signal": "bugfix pipeline state",
99
- })
100
-
101
- if file_exists(project_root, ".prizmkit", "plans", "bug-fix-list.json"):
102
- return ("bug-fix-workflow", {
103
- "bug_id": extract_bug_id_from_branch(branch) or latest_bugfix_artifact_id(project_root),
104
- "branch": branch,
105
- "signal": "bug-fix-list.json",
106
- })
107
-
108
- bug_id = latest_bugfix_artifact_id(project_root)
109
- if bug_id:
110
- return ("bug-fix-workflow", {
111
- "bug_id": bug_id,
112
- "branch": branch,
113
- "signal": "optional bugfix artifacts",
114
- })
115
-
116
- if branch.startswith("refactor/"):
117
- return ("refactor-workflow", {"branch": branch, "signal": "refactor branch"})
118
-
119
- if file_exists(project_root, ".prizmkit", "plans", "refactor-list.json"):
120
- return ("refactor-workflow", {"branch": branch, "signal": "refactor-list.json"})
121
-
122
- if dir_has_content(project_root, ".prizmkit", "state", "refactor"):
123
- return ("refactor-workflow", {"branch": branch, "signal": "refactor pipeline state"})
55
+ return data if isinstance(data, dict) else None
124
56
 
125
- if branch.startswith("feat/"):
126
- return ("feature-workflow", {"branch": branch, "signal": "feature branch"})
127
57
 
128
- if file_exists(project_root, ".prizmkit", "plans", "feature-list.json"):
129
- return ("feature-workflow", {"branch": branch, "signal": "feature-list.json"})
58
+ def branch_exists(project_root: Path, branch: str) -> bool:
59
+ if not branch:
60
+ return False
61
+ return bool(run_git(project_root, "rev-parse", "--verify", f"refs/heads/{branch}"))
130
62
 
131
- if dir_has_content(project_root, ".prizmkit", "state", "features"):
132
- return ("feature-workflow", {"branch": branch, "signal": "feature pipeline state"})
133
63
 
134
- if file_exists(project_root, "spec.md") or file_exists(project_root, "plan.md"):
135
- return ("unknown-fast-path", {"branch": branch, "signal": "spec.md or plan.md"})
64
+ def current_branch(project_root: Path) -> str:
65
+ return run_git(project_root, "branch", "--show-current")
136
66
 
137
- return (None, None)
138
67
 
68
+ def command_for(family: dict[str, str], task_id: str) -> str:
69
+ return f"python3 ./.prizmkit/dev-pipeline/cli.py {family['name']} run {task_id}"
139
70
 
140
- def detect_other_workflows(project_root, primary_type):
141
- """Scan for secondary workflow signals beyond the primary match."""
142
- others = []
143
- branch = run_git(["branch", "--show-current"], cwd=project_root)
144
71
 
145
- signals = [
146
- ("bug-fix-workflow", [
147
- branch.startswith("fix/"),
148
- file_exists(project_root, ".prizmkit", "plans", "bug-fix-list.json"),
149
- dir_has_content(project_root, ".prizmkit", "state", "bugfix"),
150
- latest_bugfix_artifact_id(project_root),
151
- ]),
152
- ("refactor-workflow", [
153
- branch.startswith("refactor/"),
154
- file_exists(project_root, ".prizmkit", "plans", "refactor-list.json"),
155
- dir_has_content(project_root, ".prizmkit", "state", "refactor"),
156
- ]),
157
- ("feature-workflow", [
158
- branch.startswith("feat/"),
159
- file_exists(project_root, ".prizmkit", "plans", "feature-list.json"),
160
- dir_has_content(project_root, ".prizmkit", "state", "features"),
161
- ]),
162
- ]
72
+ def detect_candidates(project_root: Path) -> list[dict[str, object]]:
73
+ """Return active durable task checkouts whose recorded branch still exists."""
74
+ candidates: list[dict[str, object]] = []
75
+ state_root = project_root / ".prizmkit" / "state"
76
+ active = current_branch(project_root)
163
77
 
164
- for workflow, checks in signals:
165
- if workflow != primary_type and any(checks):
166
- others.append(workflow)
167
- return others
168
-
169
-
170
- def find_first_existing(project_root, candidates):
171
- for parts in candidates:
172
- path = os.path.join(project_root, *parts)
173
- if os.path.isfile(path) or os.path.isdir(path):
174
- return relpath(path, project_root)
175
- return None
176
-
177
-
178
- def find_review_artifact(project_root):
179
- candidates = [
180
- ("review-report.md",),
181
- ("code-review-report.md",),
182
- (".prizmkit", "review-report.md"),
183
- (".prizmkit", "code-review", "review-report.md"),
184
- (".prizmkit", "reviews"),
185
- ]
186
- return find_first_existing(project_root, candidates)
187
-
188
-
189
- def find_session_artifact(project_root):
190
- candidates = [
191
- ("context-snapshot.md",),
192
- ("session-summary.md",),
193
- (".prizmkit", "context-snapshot.md"),
194
- (".prizmkit", "session-summary.md"),
195
- ]
196
- return find_first_existing(project_root, candidates)
197
-
198
-
199
- def detect_fast_path_artifacts(project_root):
200
- artifacts = {}
201
- for name in ["spec.md", "plan.md"]:
202
- path = file_exists(project_root, name)
203
- artifacts[f"{name.replace('.', '_')}_exists"] = bool(path)
204
- if path:
205
- artifacts[f"{name.replace('.', '_')}_path"] = relpath(path, project_root)
206
-
207
- review = find_review_artifact(project_root)
208
- artifacts["review_artifact_exists"] = bool(review)
209
- if review:
210
- artifacts["review_artifact_path"] = review
211
-
212
- session = find_session_artifact(project_root)
213
- artifacts["session_artifact_exists"] = bool(session)
214
- if session:
215
- artifacts["session_artifact_path"] = session
216
-
217
- return artifacts
218
-
219
-
220
- def infer_bugfix_phase(project_root, context, code_changes, commits_ahead):
221
- """Infer bug-fix recovery phase from optional signals and git state."""
222
- bug_id = context.get("bug_id") if context else None
223
- bugfix_dir = os.path.join(project_root, ".prizmkit", "bugfix", bug_id) if bug_id else ""
224
- has_fix_plan = bool(bugfix_dir and os.path.isfile(os.path.join(bugfix_dir, "fix-plan.md")))
225
- has_fix_report = bool(bugfix_dir and os.path.isfile(os.path.join(bugfix_dir, "fix-report.md")))
226
-
227
- artifacts = detect_fast_path_artifacts(project_root)
228
- artifacts.update({
229
- "fix_plan_exists": has_fix_plan,
230
- "fix_report_exists": has_fix_report,
231
- })
232
- if has_fix_plan:
233
- artifacts["fix_plan_path"] = relpath(os.path.join(bugfix_dir, "fix-plan.md"), project_root)
234
- if has_fix_report:
235
- artifacts["fix_report_path"] = relpath(os.path.join(bugfix_dir, "fix-report.md"), project_root)
236
-
237
- has_plan_artifact = artifacts.get("spec_md_exists") or artifacts.get("plan_md_exists") or has_fix_plan
238
- has_review_artifact = artifacts.get("review_artifact_exists") or has_fix_report
239
-
240
- if commits_ahead > 0:
241
- return 7, "Merge Decision", artifacts, \
242
- f"{commits_ahead} commit(s) ahead — fix may already be committed", \
243
- "merge decision or branch handoff"
244
-
245
- if has_review_artifact and code_changes["has_changes"]:
246
- return 6, "User Verification", artifacts, \
247
- "review artifact found with code changes", \
248
- "user verification -> commit -> merge decision"
249
-
250
- if code_changes["has_changes"]:
251
- return 5, "Review", artifacts, \
252
- "code/test changes present without review artifact", \
253
- "run tests if safe -> code review -> verification -> commit"
254
-
255
- if has_plan_artifact:
256
- return 4, "Fix", artifacts, \
257
- "plan artifact found but no implementation changes", \
258
- "implement fix -> review -> verification -> commit"
259
-
260
- return 1, "Diagnosis / Triage", artifacts, \
261
- "bug-fix signal found but no durable implementation artifacts", \
262
- "restore bug context -> diagnose -> choose recovery action"
263
-
264
-
265
- def infer_pipeline_workflow_phase(project_root, list_filename, state_subdir, workflow_label):
266
- list_path = file_exists(project_root, ".prizmkit", "plans", list_filename)
267
- has_state = dir_has_content(project_root, ".prizmkit", "state", state_subdir)
268
- fast_path = detect_fast_path_artifacts(project_root)
269
-
270
- artifacts = {
271
- f"{workflow_label}_list_exists": bool(list_path),
272
- "pipeline_state_exists": bool(has_state),
273
- **fast_path,
274
- }
275
- if list_path:
276
- artifacts[f"{workflow_label}_list_path"] = relpath(list_path, project_root)
277
-
278
- if list_path and has_state:
279
- return 4, "Monitor / Recover Pipeline", artifacts, \
280
- f"{list_filename} and pipeline state both exist", \
281
- "check launcher status or run recovery script"
282
-
283
- if list_path:
284
- return 3, "Launch", artifacts, \
285
- f"{list_filename} exists without pipeline state", \
286
- "launch pipeline or inspect/update list"
287
-
288
- if fast_path.get("spec_md_exists") or fast_path.get("plan_md_exists"):
289
- return 2, "Fast Path Continuation", artifacts, \
290
- "current-workspace spec/plan artifact found", \
291
- "continue implementation/review/commit based on diff"
292
-
293
- return 1, "Route Selection", artifacts, \
294
- f"no {list_filename} or pipeline state found", \
295
- f"clarify {workflow_label} intent and choose path"
296
-
297
-
298
- def infer_feature_phase(project_root):
299
- return infer_pipeline_workflow_phase(project_root, "feature-list.json", "features", "feature")
300
-
301
-
302
- def infer_refactor_phase(project_root):
303
- return infer_pipeline_workflow_phase(project_root, "refactor-list.json", "refactor", "refactor")
304
-
305
-
306
- def infer_unknown_fast_path_phase(project_root, code_changes):
307
- artifacts = detect_fast_path_artifacts(project_root)
308
- if code_changes["has_changes"]:
309
- return 5, "Review", artifacts, \
310
- "spec/plan plus code changes found, but workflow family is unknown", \
311
- "ask workflow family -> run tests/review -> continue"
312
- return 2, "Fast Path Continuation", artifacts, \
313
- "spec/plan found, but workflow family is unknown", \
314
- "ask whether this is feature, bug fix, or refactor work"
315
-
316
-
317
- def detect_commits_ahead(project_root, main_branch="main"):
318
- log_output = run_git(["log", f"{main_branch}..HEAD", "--oneline"], cwd=project_root)
319
- if log_output:
320
- return len(log_output.strip().split("\n"))
321
- return 0
322
-
323
-
324
- def detect_git_state(project_root, main_branch="main", cached_branch=None):
325
- current = cached_branch or run_git(["branch", "--show-current"], cwd=project_root)
326
-
327
- uncommitted = run_git(["status", "--porcelain"], cwd=project_root)
328
- commits_ahead = detect_commits_ahead(project_root, main_branch)
329
-
330
- return {
331
- "current_branch": current,
332
- "changed_paths": len([line for line in uncommitted.splitlines() if line.strip()]),
333
- "commits_ahead_of_main": commits_ahead,
334
- }
335
-
336
-
337
- def detect_code_changes(project_root, main_branch="main"):
338
- """Analyze implementation changes relative to main and working tree."""
339
- ignored_files = {
340
- "feature-list.json",
341
- "bug-fix-list.json",
342
- "refactor-list.json",
343
- "spec.md",
344
- "plan.md",
345
- "review-report.md",
346
- "code-review-report.md",
347
- "session-summary.md",
348
- "context-snapshot.md",
349
- "package-lock.json",
350
- "yarn.lock",
351
- "pnpm-lock.yaml",
352
- }
353
- ignored_prefixes = (
354
- ".prizmkit/",
355
- ".agents/",
356
- ".codex/",
357
- ".claude/",
358
- ".codebuddy/",
359
- )
360
-
361
- def is_implementation_file(filepath):
362
- basename = os.path.basename(filepath)
363
- if basename in ignored_files:
364
- return False
365
- if any(filepath.startswith(prefix) for prefix in ignored_prefixes):
366
- return False
367
- return True
368
-
369
- file_statuses = {}
370
-
371
- for command in (["diff", main_branch, "--name-status"], ["diff", "--name-status"]):
372
- output = run_git(command, cwd=project_root)
373
- if not output:
78
+ for family in FAMILIES:
79
+ family_dir = state_root / family["state_dir"]
80
+ if not family_dir.is_dir():
374
81
  continue
375
- for line in output.splitlines():
376
- parts = line.split("\t", 1)
377
- if len(parts) < 2:
82
+ for checkout_path in sorted(family_dir.glob("*/checkout.json")):
83
+ data = load_json(checkout_path)
84
+ if not data or str(data.get("state") or "") != "active":
378
85
  continue
379
- status, filepath = parts[0][0], parts[1]
380
- if is_implementation_file(filepath):
381
- file_statuses.setdefault(filepath, status)
382
-
383
- untracked = run_git(["ls-files", "--others", "--exclude-standard"], cwd=project_root)
384
- if untracked:
385
- for filepath in untracked.splitlines():
386
- filepath = filepath.strip()
387
- if filepath and is_implementation_file(filepath):
388
- file_statuses.setdefault(filepath, "A")
389
-
390
- test_pattern = re.compile(r"(test|spec|__tests__|\.test\.|\.spec\.)", re.IGNORECASE)
391
- dirs = set()
392
- result = {
393
- "files_modified": 0,
394
- "files_added": 0,
395
- "files_deleted": 0,
396
- "test_files_touched": 0,
397
- "directories_touched": [],
398
- "has_changes": bool(file_statuses),
86
+ task_id = str(data.get("task_id") or checkout_path.parent.name).strip()
87
+ branch = str(data.get("active_dev_branch") or "").strip()
88
+ if not task_id.startswith(family["id_prefix"]) or not branch_exists(project_root, branch):
89
+ continue
90
+ candidates.append(
91
+ {
92
+ "family": family["name"],
93
+ "task_id": task_id,
94
+ "branch": branch,
95
+ "base_branch": str(data.get("base_branch") or "").strip(),
96
+ "checkout_mode": str(data.get("checkout_mode") or "branch"),
97
+ "worktree_path": str(data.get("worktree_path") or "").strip() or None,
98
+ "checkout_path": checkout_path.relative_to(project_root).as_posix(),
99
+ "plan_path": family["plan_path"],
100
+ "branch_is_current": branch == active,
101
+ "continue_command": command_for(family, task_id),
102
+ }
103
+ )
104
+ return candidates
105
+
106
+
107
+ def build_report(project_root: Path) -> dict[str, object]:
108
+ candidates = detect_candidates(project_root)
109
+ if not candidates:
110
+ return {
111
+ "detected": False,
112
+ "current_branch": current_branch(project_root),
113
+ "candidates": [],
114
+ "message": "No active interrupted pipeline task branch was found.",
115
+ }
116
+ return {
117
+ "detected": True,
118
+ "current_branch": current_branch(project_root),
119
+ "selection_required": len(candidates) > 1,
120
+ "candidates": candidates,
399
121
  }
400
122
 
401
- for filepath, status in file_statuses.items():
402
- if status == "M":
403
- result["files_modified"] += 1
404
- elif status == "A":
405
- result["files_added"] += 1
406
- elif status == "D":
407
- result["files_deleted"] += 1
408
- if test_pattern.search(filepath):
409
- result["test_files_touched"] += 1
410
- parent = os.path.dirname(filepath)
411
- if parent:
412
- parts = parent.split(os.sep)
413
- dirs.add(os.sep.join(parts[:2]) + "/")
414
-
415
- result["directories_touched"] = sorted(dirs)
416
- return result
417
123
 
418
-
419
- def main():
420
- parser = argparse.ArgumentParser(description="Auto-detect interrupted workflow state for recovery")
421
- parser.add_argument("--project-root", default=None, help="Project root directory (default: auto-detect from git)")
124
+ def main() -> int:
125
+ parser = argparse.ArgumentParser(
126
+ description="Detect active interrupted pipeline task branches and return continuation commands."
127
+ )
128
+ parser.add_argument(
129
+ "--project-root",
130
+ default=None,
131
+ help="Project root; defaults to the current Git root or current directory.",
132
+ )
422
133
  args = parser.parse_args()
423
134
 
424
135
  if args.project_root:
425
- project_root = os.path.abspath(args.project_root)
136
+ project_root = Path(args.project_root).expanduser().resolve()
426
137
  else:
427
- git_root = run_git(["rev-parse", "--show-toplevel"])
428
- project_root = git_root if git_root else os.getcwd()
138
+ git_root = run_git(Path.cwd(), "rev-parse", "--show-toplevel")
139
+ project_root = Path(git_root).resolve() if git_root else Path.cwd().resolve()
429
140
 
430
- main_branch = detect_main_branch(project_root)
431
- workflow_type, context = detect_workflow_type(project_root)
432
-
433
- if workflow_type is None:
434
- print(json.dumps({
435
- "detected": False,
436
- "message": "No interrupted workflow detected. Use /feature-workflow, /bug-fix-workflow, or /refactor-workflow to start.",
437
- }, indent=2))
438
- sys.exit(0)
439
-
440
- cached_branch = context.get("branch") if context else None
441
- git_state = detect_git_state(project_root, main_branch, cached_branch=cached_branch)
442
- code_changes = detect_code_changes(project_root, main_branch)
443
- other_workflows = detect_other_workflows(project_root, workflow_type)
444
-
445
- if workflow_type == "bug-fix-workflow":
446
- phase, phase_name, artifacts, reason, remaining = infer_bugfix_phase(
447
- project_root, context, code_changes, git_state["commits_ahead_of_main"]
448
- )
449
- elif workflow_type == "feature-workflow":
450
- phase, phase_name, artifacts, reason, remaining = infer_feature_phase(project_root)
451
- elif workflow_type == "refactor-workflow":
452
- phase, phase_name, artifacts, reason, remaining = infer_refactor_phase(project_root)
453
- elif workflow_type == "unknown-fast-path":
454
- phase, phase_name, artifacts, reason, remaining = infer_unknown_fast_path_phase(project_root, code_changes)
455
- else:
456
- print(json.dumps({"detected": False, "message": "Unknown workflow type"}), file=sys.stderr)
457
- sys.exit(1)
458
-
459
- report = {
460
- "detected": True,
461
- "workflow_type": workflow_type,
462
- "phase": phase,
463
- "phase_name": phase_name,
464
- "context": context,
465
- "artifacts": artifacts,
466
- "git": git_state,
467
- "code": code_changes,
468
- "recovery": {
469
- "reason": reason,
470
- "remaining_work": remaining,
471
- },
472
- }
473
- if other_workflows:
474
- report["other_interrupted_workflows"] = other_workflows
475
-
476
- print(json.dumps(report, indent=2))
141
+ print(json.dumps(build_report(project_root), indent=2, ensure_ascii=False))
142
+ return 0
477
143
 
478
144
 
479
145
  if __name__ == "__main__":
480
- main()
146
+ raise SystemExit(main())