prizmkit 1.0.35 → 1.0.45
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/agents/prizm-dev-team-dev.md +11 -11
- package/bundled/agents/prizm-dev-team-reviewer.md +10 -10
- package/bundled/dev-pipeline/README.md +14 -17
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +16 -22
- package/bundled/dev-pipeline/launch-bugfix-daemon.sh +8 -0
- package/bundled/dev-pipeline/launch-daemon.sh +2 -0
- package/bundled/dev-pipeline/lib/worktree.sh +164 -0
- package/bundled/dev-pipeline/retry-bug.sh +5 -2
- package/bundled/dev-pipeline/retry-feature.sh +5 -2
- package/bundled/dev-pipeline/run-bugfix.sh +167 -2
- package/bundled/dev-pipeline/run.sh +169 -2
- package/bundled/dev-pipeline/scripts/check-session-status.py +3 -1
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +0 -8
- package/bundled/dev-pipeline/scripts/update-bug-status.py +24 -1
- package/bundled/dev-pipeline/scripts/update-feature-status.py +3 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +3 -9
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +2 -8
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +36 -43
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
- package/bundled/dev-pipeline/tests/test_check_session.py +4 -0
- package/bundled/dev-pipeline/tests/test_update_feature_status.py +70 -0
- package/bundled/dev-pipeline/tests/test_worktree.py +236 -0
- package/bundled/dev-pipeline/tests/test_worktree_integration.py +796 -0
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-implement/SKILL.md +4 -2
- package/bundled/team/prizm-dev-team.json +3 -17
- package/package.json +1 -1
- package/src/clean.js +0 -2
- package/src/manifest.js +8 -4
- package/src/scaffold.js +69 -3
- package/src/upgrade.js +32 -5
- package/bundled/agents/prizm-dev-team-coordinator.md +0 -141
- package/bundled/agents/prizm-dev-team-pm.md +0 -126
|
@@ -83,7 +83,7 @@ If MISSING — build it now:
|
|
|
83
83
|
- **Section 1 — Feature Brief**: feature description + acceptance criteria (copy from above)
|
|
84
84
|
- **Section 2 — Project Structure**: relevant `ls src/` output
|
|
85
85
|
- **Section 3 — Prizm Context**: full content of root.prizm and relevant L1/L2 docs
|
|
86
|
-
- **Section 4 — Existing Source Files**: full content of each related file
|
|
86
|
+
- **Section 4 — Existing Source Files**: **full verbatim content** of each related file in fenced code blocks (with `### path/to/file` heading and line count). Include ALL files needed for implementation and review — downstream subagents read this section instead of re-reading individual source files
|
|
87
87
|
- **Section 5 — Existing Tests**: full content of related test files as code blocks
|
|
88
88
|
|
|
89
89
|
### Phase 2: Plan & Tasks (you, the orchestrator)
|
|
@@ -156,15 +156,9 @@ Stage any `.prizm-docs/` changes produced: `git add .prizm-docs/`
|
|
|
156
156
|
### Phase 5: Commit
|
|
157
157
|
|
|
158
158
|
- Run `/prizmkit-summarize` → archive to REGISTRY.md
|
|
159
|
-
- Mark feature complete:
|
|
160
|
-
```bash
|
|
161
|
-
python3 {{VALIDATOR_SCRIPTS_DIR}}/update-feature-status.py \
|
|
162
|
-
--feature-list "{{FEATURE_LIST_PATH}}" \
|
|
163
|
-
--state-dir "{{PROJECT_ROOT}}/dev-pipeline/state" \
|
|
164
|
-
--feature-id "{{FEATURE_ID}}" --session-id "{{SESSION_ID}}" --action complete
|
|
165
|
-
```
|
|
166
159
|
- Run `/prizmkit-committer` → `feat({{FEATURE_ID}}): {{FEATURE_TITLE}}`, do NOT push
|
|
167
160
|
- MANDATORY: commit must be done via `/prizmkit-committer` skill. Do NOT run manual `git add`/`git commit` as a substitute.
|
|
161
|
+
- Do NOT run `update-feature-status.py` here — the pipeline runner handles feature-list.json updates automatically after session exit.
|
|
168
162
|
|
|
169
163
|
---
|
|
170
164
|
|
|
@@ -13,7 +13,7 @@ You are the **session orchestrator**. Implement Feature {{FEATURE_ID}}: "{{FEATU
|
|
|
13
13
|
|
|
14
14
|
**CRITICAL**: You MUST NOT exit until ALL work is complete and session-status.json is written. When you spawn subagents, wait for each to finish (run_in_background=false). Do NOT spawn agents in background and exit — that kills the session.
|
|
15
15
|
|
|
16
|
-
**Tier 3 — Full Team**:
|
|
16
|
+
**Tier 3 — Full Team**: Dev + Reviewer agents spawned directly via Task tool. Full 7-phase pipeline.
|
|
17
17
|
|
|
18
18
|
### Feature Description
|
|
19
19
|
|
|
@@ -68,16 +68,15 @@ LLM context is frozen at prompt time. Modifying a skill source file during this
|
|
|
68
68
|
## PrizmKit Directory Convention
|
|
69
69
|
|
|
70
70
|
```
|
|
71
|
-
.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md ←
|
|
71
|
+
.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md ← orchestrator writes, all subagents read
|
|
72
72
|
.prizmkit/specs/{{FEATURE_SLUG}}/spec.md
|
|
73
73
|
.prizmkit/specs/{{FEATURE_SLUG}}/plan.md ← includes Tasks section
|
|
74
74
|
.prizmkit/specs/REGISTRY.md
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
**`context-snapshot.md`** is the shared knowledge base.
|
|
77
|
+
**`context-snapshot.md`** is the shared knowledge base. Orchestrator writes it once; Dev and Reviewer read it instead of re-scanning source files. This eliminates redundant I/O across all agents.
|
|
78
78
|
|
|
79
79
|
### Agent Files
|
|
80
|
-
- PM Agent: `{{PM_SUBAGENT_PATH}}`
|
|
81
80
|
- Dev Agent: `{{DEV_SUBAGENT_PATH}}`
|
|
82
81
|
- Reviewer Agent: `{{REVIEWER_SUBAGENT_PATH}}`
|
|
83
82
|
|
|
@@ -129,7 +128,6 @@ No TeamCreate required. Agents are spawned directly via the `Task` tool using `s
|
|
|
129
128
|
`ls .prizmkit/specs/{{FEATURE_SLUG}}/ 2>/dev/null`
|
|
130
129
|
|
|
131
130
|
Agent files are at:
|
|
132
|
-
- PM: `{{PM_SUBAGENT_PATH}}`
|
|
133
131
|
- Dev: `{{DEV_SUBAGENT_PATH}}`
|
|
134
132
|
- Reviewer: `{{REVIEWER_SUBAGENT_PATH}}`
|
|
135
133
|
|
|
@@ -144,7 +142,7 @@ python3 {{INIT_SCRIPT_PATH}} --project-root {{PROJECT_ROOT}} --feature-id {{FEAT
|
|
|
144
142
|
After team setup: check `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md` — if exists, all agents MUST use it. Read existing artifacts and resume from Phase {{RESUME_PHASE}}.
|
|
145
143
|
{{END_IF_RESUME}}
|
|
146
144
|
|
|
147
|
-
### Phase 1-2: Specify + Plan —
|
|
145
|
+
### Phase 1-2: Specify + Plan — Orchestrator (you)
|
|
148
146
|
|
|
149
147
|
Check existing artifacts first:
|
|
150
148
|
```bash
|
|
@@ -152,40 +150,44 @@ ls .prizmkit/specs/{{FEATURE_SLUG}}/ 2>/dev/null
|
|
|
152
150
|
```
|
|
153
151
|
|
|
154
152
|
- Both (spec.md, plan.md) exist → **SKIP to CP-1**
|
|
155
|
-
- `context-snapshot.md` exists →
|
|
156
|
-
- Some missing →
|
|
153
|
+
- `context-snapshot.md` exists → use it directly, skip Phase 1
|
|
154
|
+
- Some missing → generate only missing files
|
|
157
155
|
|
|
158
|
-
Before
|
|
156
|
+
Before planning, check whether feature code already exists in the project:
|
|
159
157
|
```bash
|
|
160
158
|
grep -r "{{FEATURE_SLUG}}" src/ --include="*.js" --include="*.ts" -l 2>/dev/null | head -20
|
|
161
159
|
```
|
|
162
160
|
|
|
163
|
-
Record result as `EXISTING_CODE` (list of files, or empty).
|
|
161
|
+
Record result as `EXISTING_CODE` (list of files, or empty).
|
|
164
162
|
|
|
165
|
-
|
|
163
|
+
If `EXISTING_CODE` is non-empty: your spec/plan/tasks must reflect this existing implementation — document what exists, identify gaps, do NOT re-implement what is already done.
|
|
166
164
|
|
|
167
|
-
**
|
|
168
|
-
> "Read {{PM_SUBAGENT_PATH}}. For feature {{FEATURE_ID}} (slug: {{FEATURE_SLUG}}), complete the following IN THIS SINGLE SESSION — do NOT exit until ALL listed steps are done and files are written to disk:"
|
|
165
|
+
**Step A — Build Context Snapshot** (skip if `context-snapshot.md` already exists):
|
|
169
166
|
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
1. Read `.prizm-docs/root.prizm` and relevant L1/L2 prizm docs
|
|
168
|
+
2. Scan `src/` for files related to this feature; read each one
|
|
169
|
+
3. Write `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md`:
|
|
170
|
+
- **Section 1 — Feature Brief**: feature description + acceptance criteria (copy from above)
|
|
171
|
+
- **Section 2 — Project Structure**: relevant `ls src/` output
|
|
172
|
+
- **Section 3 — Prizm Context**: full content of root.prizm and relevant L1/L2 docs
|
|
173
|
+
- **Section 4 — Existing Source Files**: **full verbatim content** of each related file in fenced code blocks (with `### path/to/file` heading and line count). Include ALL files needed for implementation and review — downstream subagents read this section instead of re-reading individual source files
|
|
174
|
+
- **Section 5 — Existing Tests**: full content of related test files as code blocks
|
|
175
|
+
4. Confirm: `ls .prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md`
|
|
172
176
|
|
|
173
|
-
**Step A
|
|
174
|
-
> "Step A: Write `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md`. This is the team knowledge base — complete it before anything else. Include:
|
|
175
|
-
> - Section 1 'Feature Brief': feature description and acceptance criteria
|
|
176
|
-
> - Section 2 'Project Structure': output of `ls src/` and relevant subdirectories
|
|
177
|
-
> - Section 3 'Prizm Context': full content of `.prizm-docs/root.prizm` and relevant L1/L2 docs
|
|
178
|
-
> - Section 4 'Existing Source Files': full content of every related source file as a code block
|
|
179
|
-
> - Section 5 'Existing Tests': full content of related test files as code blocks
|
|
180
|
-
> Confirm with `ls .prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md`."
|
|
177
|
+
**After Step A, do NOT re-read any original source files** — use context-snapshot.md for all subsequent work.
|
|
181
178
|
|
|
182
|
-
**Step B — Planning Artifacts** (
|
|
183
|
-
- spec.md missing: "Run prizmkit-specify → generate spec.md. Resolve any `[NEEDS CLARIFICATION]` markers using the feature description — do NOT pause for interactive input."
|
|
184
|
-
- plan.md missing: "Run prizmkit-plan → generate plan.md (architecture, components, interface design, data model, testing strategy, risk assessment, and Tasks section with `[ ]` checkboxes)"
|
|
179
|
+
**Step B — Planning Artifacts** (generate only missing files):
|
|
185
180
|
|
|
186
|
-
|
|
181
|
+
```bash
|
|
182
|
+
ls .prizmkit/specs/{{FEATURE_SLUG}}/spec.md .prizmkit/specs/{{FEATURE_SLUG}}/plan.md 2>/dev/null
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- spec.md missing: Run `/prizmkit-specify` → generate spec.md. Resolve any `[NEEDS CLARIFICATION]` markers using the feature description — do NOT pause for interactive input.
|
|
186
|
+
- plan.md missing: Run `/prizmkit-plan` → generate plan.md (architecture, components, interface design, data model, testing strategy, risk assessment, and Tasks section with `[ ]` checkboxes)
|
|
187
187
|
|
|
188
|
-
|
|
188
|
+
> All files go under `.prizmkit/specs/{{FEATURE_SLUG}}/`. Confirm each with `ls` after writing.
|
|
189
|
+
|
|
190
|
+
**CP-1**: Both spec.md and plan.md exist.
|
|
189
191
|
|
|
190
192
|
### Phase 4: Analyze — Reviewer Agent
|
|
191
193
|
|
|
@@ -199,9 +201,7 @@ Prompt:
|
|
|
199
201
|
> Report: CRITICAL, HIGH, MEDIUM issues found (or 'No issues found')."
|
|
200
202
|
|
|
201
203
|
Wait for Reviewer to return.
|
|
202
|
-
- If CRITICAL issues found:
|
|
203
|
-
> "Read {{PM_SUBAGENT_PATH}}. Read `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md` FIRST for full project context. Do NOT re-read individual source files. Fix ONLY the following CRITICAL issues in spec.md/plan.md/tasks.md: `<list issues>`. Do NOT exit until all files are updated."
|
|
204
|
-
Then re-run analyze (max 1 round).
|
|
204
|
+
- If CRITICAL issues found: fix them yourself — read `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md` for full project context. Fix ONLY the listed CRITICAL issues in spec.md/plan.md/tasks.md. Then re-run analyze (max 1 round).
|
|
205
205
|
|
|
206
206
|
**CP-2**: No CRITICAL issues.
|
|
207
207
|
|
|
@@ -335,17 +335,11 @@ git log --oneline | grep "{{FEATURE_ID}}" | head -3
|
|
|
335
335
|
- Stage any `.prizm-docs/` changes: `git add .prizm-docs/`
|
|
336
336
|
- **Skip if this session is bug-fix-only**
|
|
337
337
|
|
|
338
|
-
**7c.**
|
|
339
|
-
```bash
|
|
340
|
-
python3 {{VALIDATOR_SCRIPTS_DIR}}/update-feature-status.py \
|
|
341
|
-
--feature-list "{{FEATURE_LIST_PATH}}" \
|
|
342
|
-
--state-dir "{{PROJECT_ROOT}}/dev-pipeline/state" \
|
|
343
|
-
--feature-id "{{FEATURE_ID}}" --session-id "{{SESSION_ID}}" --action complete
|
|
344
|
-
```
|
|
338
|
+
**7c.** Run `/prizmkit-committer` → `feat({{FEATURE_ID}}): {{FEATURE_TITLE}}`, do NOT push
|
|
345
339
|
|
|
346
|
-
**7d.**
|
|
340
|
+
**7d.** MANDATORY: commit must be done via `/prizmkit-committer` skill. Do NOT run manual `git add`/`git commit` as a substitute.
|
|
347
341
|
|
|
348
|
-
**7e.**
|
|
342
|
+
**7e.** Do NOT run `update-feature-status.py` here — the pipeline runner handles feature-list.json updates automatically after session exit.
|
|
349
343
|
|
|
350
344
|
---
|
|
351
345
|
|
|
@@ -409,7 +403,6 @@ No team cleanup needed — agents were spawned directly without TeamCreate.
|
|
|
409
403
|
| Feature Artifacts Dir | `.prizmkit/specs/{{FEATURE_SLUG}}/` |
|
|
410
404
|
| Context Snapshot | `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md` |
|
|
411
405
|
| Team Config | `{{TEAM_CONFIG_PATH}}` |
|
|
412
|
-
| PM Agent Def | {{PM_SUBAGENT_PATH}} |
|
|
413
406
|
| Dev Agent Def | {{DEV_SUBAGENT_PATH}} |
|
|
414
407
|
| Reviewer Agent Def | {{REVIEWER_SUBAGENT_PATH}} |
|
|
415
408
|
| Session Status Output | {{SESSION_STATUS_PATH}} |
|
|
@@ -418,8 +411,8 @@ No team cleanup needed — agents were spawned directly without TeamCreate.
|
|
|
418
411
|
|
|
419
412
|
## Reminders
|
|
420
413
|
|
|
421
|
-
- Tier 3: full team —
|
|
422
|
-
- context-snapshot.md is the team knowledge base:
|
|
414
|
+
- Tier 3: full team — Dev (implementation) → Reviewer (review) — agents spawned directly via Task tool (no TeamCreate needed)
|
|
415
|
+
- context-snapshot.md is the team knowledge base: orchestrator writes it once, all agents read it
|
|
423
416
|
- Do NOT use `run_in_background=true` when spawning agents
|
|
424
417
|
- ALWAYS write session-status.json before exiting
|
|
425
418
|
- Commit phase must use `/prizmkit-committer`; do NOT replace with manual git commit commands
|
|
@@ -18,7 +18,7 @@ You are the **bug fix session orchestrator**. Fix Bug {{BUG_ID}}: "{{BUG_TITLE}}
|
|
|
18
18
|
|
|
19
19
|
**CRITICAL SESSION LIFECYCLE RULE**: You MUST NOT exit until ALL work is complete and session-status.json is written. When you spawn subagents, you MUST **wait for each to finish** (run_in_background=false) before proceeding. Do NOT spawn an agent in the background and exit — that kills the session.
|
|
20
20
|
|
|
21
|
-
**MANDATORY TEAM REQUIREMENT**: You MUST use the `prizm-dev-team` multi-agent team. This is NON-NEGOTIABLE. All implementation and review work MUST be performed by the appropriate team agents (Dev, Reviewer).
|
|
21
|
+
**MANDATORY TEAM REQUIREMENT**: You MUST use the `prizm-dev-team` multi-agent team. This is NON-NEGOTIABLE. All implementation and review work MUST be performed by the appropriate team agents (Dev, Reviewer). You are the orchestrator — handle coordination, planning, and commit phases directly.
|
|
22
22
|
|
|
23
23
|
**BUG FIX DOCUMENTATION POLICY**: Bug fixes MUST NOT be recorded as new documentation entries:
|
|
24
24
|
- Do NOT run `/prizmkit-summarize` (no REGISTRY.md entries)
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"status": {
|
|
17
17
|
"type": "string",
|
|
18
|
-
"enum": ["success", "partial", "failed", "commit_missing", "docs_missing"]
|
|
18
|
+
"enum": ["success", "partial", "failed", "commit_missing", "docs_missing", "merge_conflict"]
|
|
19
19
|
},
|
|
20
20
|
"completed_phases": {
|
|
21
21
|
"type": "array",
|
|
@@ -114,6 +114,10 @@ class TestDetermineStatus:
|
|
|
114
114
|
data = {"status": "docs_missing"}
|
|
115
115
|
assert determine_status(data) == "docs_missing"
|
|
116
116
|
|
|
117
|
+
def test_merge_conflict(self):
|
|
118
|
+
data = {"status": "merge_conflict"}
|
|
119
|
+
assert determine_status(data) == "merge_conflict"
|
|
120
|
+
|
|
117
121
|
def test_unknown_status(self):
|
|
118
122
|
data = {"status": "something_weird"}
|
|
119
123
|
assert determine_status(data) == "crashed"
|
|
@@ -36,6 +36,9 @@ class TestSessionStatusValues:
|
|
|
36
36
|
def test_contains_docs_missing(self):
|
|
37
37
|
assert "docs_missing" in ufs.SESSION_STATUS_VALUES
|
|
38
38
|
|
|
39
|
+
def test_contains_merge_conflict(self):
|
|
40
|
+
assert "merge_conflict" in ufs.SESSION_STATUS_VALUES
|
|
41
|
+
|
|
39
42
|
|
|
40
43
|
class TestNowIso:
|
|
41
44
|
def test_returns_valid_iso_format(self):
|
|
@@ -252,6 +255,73 @@ class TestActionUpdateDegradedStatuses:
|
|
|
252
255
|
f1 = next(x for x in data["features"] if x["id"] == "F-001")
|
|
253
256
|
assert f1["status"] == "failed"
|
|
254
257
|
|
|
258
|
+
def test_merge_conflict_updates_feature_state_without_cleanup(
|
|
259
|
+
self, feature_list_file, state_dir, monkeypatch, capsys
|
|
260
|
+
):
|
|
261
|
+
"""merge_conflict is treated like commit_missing: retryable, no artifact cleanup."""
|
|
262
|
+
called = {"cleanup": 0}
|
|
263
|
+
|
|
264
|
+
def _fake_cleanup(**_kwargs):
|
|
265
|
+
called["cleanup"] += 1
|
|
266
|
+
return []
|
|
267
|
+
|
|
268
|
+
monkeypatch.setattr(ufs, "cleanup_feature_artifacts", _fake_cleanup)
|
|
269
|
+
|
|
270
|
+
args = SimpleNamespace(
|
|
271
|
+
feature_id="F-001",
|
|
272
|
+
session_status="merge_conflict",
|
|
273
|
+
session_id="s-merge-conflict",
|
|
274
|
+
max_retries=3,
|
|
275
|
+
project_root=None,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
ufs.action_update(args, feature_list_file, state_dir)
|
|
279
|
+
out = capsys.readouterr().out
|
|
280
|
+
summary = json.loads(out)
|
|
281
|
+
|
|
282
|
+
fs = ufs.load_feature_status(state_dir, "F-001")
|
|
283
|
+
assert fs["status"] == "merge_conflict"
|
|
284
|
+
assert fs["retry_count"] == 1
|
|
285
|
+
assert summary["new_status"] == "merge_conflict"
|
|
286
|
+
assert summary["degraded_reason"] == "merge_conflict"
|
|
287
|
+
assert summary["restart_policy"] == "finalization_retry"
|
|
288
|
+
assert called["cleanup"] == 0
|
|
289
|
+
|
|
290
|
+
with open(feature_list_file, "r", encoding="utf-8") as f:
|
|
291
|
+
data = json.load(f)
|
|
292
|
+
f1 = next(x for x in data["features"] if x["id"] == "F-001")
|
|
293
|
+
assert f1["status"] == "merge_conflict"
|
|
294
|
+
|
|
295
|
+
def test_merge_conflict_reaches_failed_when_retry_exhausted(
|
|
296
|
+
self, feature_list_file, state_dir, monkeypatch, capsys
|
|
297
|
+
):
|
|
298
|
+
"""merge_conflict with max_retries=1 should fail the feature."""
|
|
299
|
+
called = {"cleanup": 0}
|
|
300
|
+
|
|
301
|
+
def _fake_cleanup(**_kwargs):
|
|
302
|
+
called["cleanup"] += 1
|
|
303
|
+
return []
|
|
304
|
+
|
|
305
|
+
monkeypatch.setattr(ufs, "cleanup_feature_artifacts", _fake_cleanup)
|
|
306
|
+
|
|
307
|
+
args = SimpleNamespace(
|
|
308
|
+
feature_id="F-001",
|
|
309
|
+
session_status="merge_conflict",
|
|
310
|
+
session_id="s-merge-conflict-fail",
|
|
311
|
+
max_retries=1,
|
|
312
|
+
project_root=None,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
ufs.action_update(args, feature_list_file, state_dir)
|
|
316
|
+
out = capsys.readouterr().out
|
|
317
|
+
summary = json.loads(out)
|
|
318
|
+
|
|
319
|
+
fs = ufs.load_feature_status(state_dir, "F-001")
|
|
320
|
+
assert fs["status"] == "failed"
|
|
321
|
+
assert fs["retry_count"] == 1
|
|
322
|
+
assert summary["new_status"] == "failed"
|
|
323
|
+
assert called["cleanup"] == 0
|
|
324
|
+
|
|
255
325
|
|
|
256
326
|
class TestActionStatusForDegradedStates:
|
|
257
327
|
def test_status_output_contains_degraded_counters(self, feature_list_file, state_dir, capsys):
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Tests for dev-pipeline/lib/worktree.sh.
|
|
2
|
+
|
|
3
|
+
Uses subprocess to invoke the worktree functions in a real (temporary) git repo
|
|
4
|
+
to verify create/merge/cleanup/prune lifecycle.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "..")
|
|
14
|
+
LIB_DIR = os.path.join(SCRIPT_DIR, "lib")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _run_worktree_script(script_body, env=None):
|
|
18
|
+
"""Run a bash script that sources worktree.sh (with stub log functions), then executes body."""
|
|
19
|
+
# We define stub log functions instead of sourcing common.sh to avoid
|
|
20
|
+
# CLI/platform detection which requires cbc/claude in PATH.
|
|
21
|
+
worktree_sh_path = os.path.join(os.path.abspath(LIB_DIR), "worktree.sh")
|
|
22
|
+
full_script = "\n".join([
|
|
23
|
+
"#!/usr/bin/env bash",
|
|
24
|
+
"set -euo pipefail",
|
|
25
|
+
# Stub log functions (worktree.sh uses log_info, log_warn, log_error, log_success)
|
|
26
|
+
'log_info() { echo "[INFO] $*"; }',
|
|
27
|
+
'log_warn() { echo "[WARN] $*"; }',
|
|
28
|
+
'log_error() { echo "[ERROR] $*"; }',
|
|
29
|
+
'log_success() { echo "[SUCCESS] $*"; }',
|
|
30
|
+
'source "%s"' % worktree_sh_path,
|
|
31
|
+
script_body,
|
|
32
|
+
])
|
|
33
|
+
merged_env = dict(os.environ)
|
|
34
|
+
if env:
|
|
35
|
+
merged_env.update(env)
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["bash", "-c", full_script],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
env=merged_env,
|
|
41
|
+
)
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _init_git_repo(path):
|
|
46
|
+
"""Initialize a bare-bones git repo with an initial commit on 'main' branch."""
|
|
47
|
+
subprocess.run(["git", "init", str(path)], capture_output=True, check=True)
|
|
48
|
+
subprocess.run(
|
|
49
|
+
["git", "-C", str(path), "config", "user.email", "test@test.com"],
|
|
50
|
+
capture_output=True,
|
|
51
|
+
check=True,
|
|
52
|
+
)
|
|
53
|
+
subprocess.run(
|
|
54
|
+
["git", "-C", str(path), "config", "user.name", "Test"],
|
|
55
|
+
capture_output=True,
|
|
56
|
+
check=True,
|
|
57
|
+
)
|
|
58
|
+
# Create an initial commit so we have a branch
|
|
59
|
+
readme = os.path.join(str(path), "README.md")
|
|
60
|
+
with open(readme, "w") as f:
|
|
61
|
+
f.write("# Test Repo\n")
|
|
62
|
+
subprocess.run(
|
|
63
|
+
["git", "-C", str(path), "add", "."], capture_output=True, check=True
|
|
64
|
+
)
|
|
65
|
+
subprocess.run(
|
|
66
|
+
["git", "-C", str(path), "commit", "-m", "Initial commit"],
|
|
67
|
+
capture_output=True,
|
|
68
|
+
check=True,
|
|
69
|
+
)
|
|
70
|
+
# Ensure the branch is named 'main' regardless of git defaults
|
|
71
|
+
subprocess.run(
|
|
72
|
+
["git", "-C", str(path), "branch", "-M", "main"],
|
|
73
|
+
capture_output=True,
|
|
74
|
+
check=True,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestWorktreeCreate:
|
|
79
|
+
def test_creates_worktree_and_branch(self, tmp_path):
|
|
80
|
+
repo = tmp_path / "repo"
|
|
81
|
+
repo.mkdir()
|
|
82
|
+
_init_git_repo(repo)
|
|
83
|
+
wt_base = tmp_path / "worktrees"
|
|
84
|
+
|
|
85
|
+
script = (
|
|
86
|
+
'worktree_create "{repo}" "{wt_base}" "test-session-001" "main"\n'
|
|
87
|
+
'echo "PATH=$_WORKTREE_PATH"\n'
|
|
88
|
+
'echo "BRANCH=$_WORKTREE_BRANCH"\n'
|
|
89
|
+
).format(repo=repo, wt_base=wt_base)
|
|
90
|
+
|
|
91
|
+
result = _run_worktree_script(script)
|
|
92
|
+
assert result.returncode == 0, f"stderr: {result.stderr}"
|
|
93
|
+
|
|
94
|
+
lines = result.stdout.strip().split("\n")
|
|
95
|
+
path_line = [l for l in lines if l.startswith("PATH=")]
|
|
96
|
+
branch_line = [l for l in lines if l.startswith("BRANCH=")]
|
|
97
|
+
|
|
98
|
+
assert len(path_line) == 1
|
|
99
|
+
assert len(branch_line) == 1
|
|
100
|
+
|
|
101
|
+
wt_path = path_line[0].split("=", 1)[1]
|
|
102
|
+
wt_branch = branch_line[0].split("=", 1)[1]
|
|
103
|
+
|
|
104
|
+
assert os.path.isdir(wt_path)
|
|
105
|
+
assert wt_branch == "worktree/test-session-001"
|
|
106
|
+
|
|
107
|
+
# Verify git branch exists
|
|
108
|
+
branch_check = subprocess.run(
|
|
109
|
+
["git", "-C", str(repo), "rev-parse", "--verify", wt_branch],
|
|
110
|
+
capture_output=True,
|
|
111
|
+
)
|
|
112
|
+
assert branch_check.returncode == 0
|
|
113
|
+
|
|
114
|
+
def test_fails_when_worktree_already_exists(self, tmp_path):
|
|
115
|
+
repo = tmp_path / "repo"
|
|
116
|
+
repo.mkdir()
|
|
117
|
+
_init_git_repo(repo)
|
|
118
|
+
wt_base = tmp_path / "worktrees"
|
|
119
|
+
|
|
120
|
+
# Create the worktree directory manually to trigger failure
|
|
121
|
+
wt_path = wt_base / "dup-session"
|
|
122
|
+
wt_path.mkdir(parents=True)
|
|
123
|
+
|
|
124
|
+
script = (
|
|
125
|
+
'worktree_create "{repo}" "{wt_base}" "dup-session" "main"\n'
|
|
126
|
+
'echo "RC=$?"\n'
|
|
127
|
+
).format(repo=repo, wt_base=wt_base)
|
|
128
|
+
|
|
129
|
+
# Since set -e is on and worktree_create returns 1, the script exits with 1
|
|
130
|
+
result = _run_worktree_script(script)
|
|
131
|
+
assert result.returncode != 0 or "RC=1" in result.stdout
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class TestWorktreeMerge:
|
|
135
|
+
def test_merge_success(self, tmp_path):
|
|
136
|
+
repo = tmp_path / "repo"
|
|
137
|
+
repo.mkdir()
|
|
138
|
+
_init_git_repo(repo)
|
|
139
|
+
wt_base = tmp_path / "worktrees"
|
|
140
|
+
|
|
141
|
+
# Create worktree, add a file, commit, then merge
|
|
142
|
+
script = (
|
|
143
|
+
'worktree_create "{repo}" "{wt_base}" "merge-test" "main"\n'
|
|
144
|
+
'echo "hello" > "$_WORKTREE_PATH/newfile.txt"\n'
|
|
145
|
+
'git -C "$_WORKTREE_PATH" add newfile.txt\n'
|
|
146
|
+
'git -C "$_WORKTREE_PATH" commit -m "Add newfile"\n'
|
|
147
|
+
'worktree_merge "{repo}" "$_WORKTREE_BRANCH" "main" "F-001" "merge-test"\n'
|
|
148
|
+
'echo "MERGE=$_MERGE_RESULT"\n'
|
|
149
|
+
).format(repo=repo, wt_base=wt_base)
|
|
150
|
+
|
|
151
|
+
result = _run_worktree_script(script)
|
|
152
|
+
assert result.returncode == 0, f"stderr: {result.stderr}"
|
|
153
|
+
assert "MERGE=success" in result.stdout
|
|
154
|
+
|
|
155
|
+
# Verify the file exists on main
|
|
156
|
+
assert os.path.isfile(str(repo / "newfile.txt"))
|
|
157
|
+
|
|
158
|
+
def test_merge_conflict(self, tmp_path):
|
|
159
|
+
repo = tmp_path / "repo"
|
|
160
|
+
repo.mkdir()
|
|
161
|
+
_init_git_repo(repo)
|
|
162
|
+
wt_base = tmp_path / "worktrees"
|
|
163
|
+
|
|
164
|
+
# Create worktree, modify README on both main and worktree to cause conflict
|
|
165
|
+
script = (
|
|
166
|
+
# Create worktree
|
|
167
|
+
'worktree_create "{repo}" "{wt_base}" "conflict-test" "main"\n'
|
|
168
|
+
# Modify file in worktree and commit
|
|
169
|
+
'echo "worktree change" > "$_WORKTREE_PATH/README.md"\n'
|
|
170
|
+
'git -C "$_WORKTREE_PATH" add README.md\n'
|
|
171
|
+
'git -C "$_WORKTREE_PATH" commit -m "Worktree change"\n'
|
|
172
|
+
# Modify same file in main and commit
|
|
173
|
+
'echo "main change" > "{repo}/README.md"\n'
|
|
174
|
+
'git -C "{repo}" add README.md\n'
|
|
175
|
+
'git -C "{repo}" commit -m "Main change"\n'
|
|
176
|
+
# Attempt merge (should conflict)
|
|
177
|
+
'worktree_merge "{repo}" "worktree/conflict-test" "main" "F-001" "conflict-test" || true\n'
|
|
178
|
+
'echo "MERGE=$_MERGE_RESULT"\n'
|
|
179
|
+
).format(repo=repo, wt_base=wt_base)
|
|
180
|
+
|
|
181
|
+
result = _run_worktree_script(script)
|
|
182
|
+
# Script may exit non-zero due to merge conflict detection
|
|
183
|
+
assert "MERGE=conflict" in result.stdout
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestWorktreeCleanup:
|
|
187
|
+
def test_cleanup_removes_worktree_and_branch(self, tmp_path):
|
|
188
|
+
repo = tmp_path / "repo"
|
|
189
|
+
repo.mkdir()
|
|
190
|
+
_init_git_repo(repo)
|
|
191
|
+
wt_base = tmp_path / "worktrees"
|
|
192
|
+
|
|
193
|
+
script = (
|
|
194
|
+
'worktree_create "{repo}" "{wt_base}" "cleanup-test" "main"\n'
|
|
195
|
+
'local_path="$_WORKTREE_PATH"\n'
|
|
196
|
+
'local_branch="$_WORKTREE_BRANCH"\n'
|
|
197
|
+
'worktree_cleanup "{repo}" "$local_path" "$local_branch"\n'
|
|
198
|
+
'[ -d "$local_path" ] && echo "DIR_EXISTS" || echo "DIR_GONE"\n'
|
|
199
|
+
'git -C "{repo}" rev-parse --verify "$local_branch" 2>/dev/null && echo "BRANCH_EXISTS" || echo "BRANCH_GONE"\n'
|
|
200
|
+
).format(repo=repo, wt_base=wt_base)
|
|
201
|
+
|
|
202
|
+
result = _run_worktree_script(script)
|
|
203
|
+
assert result.returncode == 0, f"stderr: {result.stderr}"
|
|
204
|
+
assert "DIR_GONE" in result.stdout
|
|
205
|
+
assert "BRANCH_GONE" in result.stdout
|
|
206
|
+
|
|
207
|
+
def test_cleanup_idempotent(self, tmp_path):
|
|
208
|
+
repo = tmp_path / "repo"
|
|
209
|
+
repo.mkdir()
|
|
210
|
+
_init_git_repo(repo)
|
|
211
|
+
|
|
212
|
+
# Call cleanup on non-existent worktree/branch
|
|
213
|
+
script = (
|
|
214
|
+
'worktree_cleanup "{repo}" "/nonexistent/path" "nonexistent-branch"\n'
|
|
215
|
+
'echo "OK"\n'
|
|
216
|
+
).format(repo=repo)
|
|
217
|
+
|
|
218
|
+
result = _run_worktree_script(script)
|
|
219
|
+
assert result.returncode == 0
|
|
220
|
+
assert "OK" in result.stdout
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class TestWorktreePruneStale:
|
|
224
|
+
def test_prune_stale(self, tmp_path):
|
|
225
|
+
repo = tmp_path / "repo"
|
|
226
|
+
repo.mkdir()
|
|
227
|
+
_init_git_repo(repo)
|
|
228
|
+
|
|
229
|
+
script = (
|
|
230
|
+
'worktree_prune_stale "{repo}"\n'
|
|
231
|
+
'echo "OK"\n'
|
|
232
|
+
).format(repo=repo)
|
|
233
|
+
|
|
234
|
+
result = _run_worktree_script(script)
|
|
235
|
+
assert result.returncode == 0
|
|
236
|
+
assert "OK" in result.stdout
|