prizmkit 1.0.147 → 1.0.149
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/command-adapter.js +3 -2
- package/bundled/dev-pipeline/run.sh +54 -1
- package/bundled/dev-pipeline/scripts/update-feature-status.py +287 -7
- package/bundled/dev-pipeline/templates/feature-list-schema.json +1 -1
- package/bundled/dev-pipeline/tests/conftest.py +1 -0
- package/bundled/dev-pipeline/tests/test_auto_skip.py +446 -0
- package/bundled/skills/_metadata.json +9 -1
- package/bundled/skills/app-planner/SKILL.md +110 -28
- package/bundled/skills/app-planner/references/architecture-decisions.md +48 -0
- package/bundled/skills/app-planner/references/brainstorm-guide.md +101 -0
- package/bundled/skills/app-planner/references/browser-interaction.md +34 -0
- package/bundled/skills/app-planner/references/error-recovery.md +109 -0
- package/bundled/skills/app-planner/references/frontend-design-guide.md +71 -0
- package/bundled/skills/app-planner/references/incremental-feature-planning.md +112 -0
- package/bundled/skills/app-planner/references/new-app-planning.md +85 -0
- package/bundled/skills/app-planner/references/project-conventions.md +93 -0
- package/bundled/skills/app-planner/references/red-team-checklist.md +40 -0
- package/bundled/skills/app-planner/scripts/validate-and-generate.py +1 -1
- package/bundled/skills/prizm-kit/SKILL.md +3 -1
- package/bundled/skills/prizmkit-committer/SKILL.md +1 -1
- package/bundled/skills/prizmkit-deploy/SKILL.md +112 -0
- package/bundled/skills/prizmkit-deploy/assets/deploy-template.md +108 -0
- package/bundled/skills/prizmkit-plan/SKILL.md +30 -8
- package/bundled/skills/prizmkit-plan/assets/plan-template.md +19 -0
- package/bundled/skills/prizmkit-retrospective/SKILL.md +3 -1
- package/package.json +1 -1
- package/src/scaffold.js +5 -4
package/bundled/VERSION.json
CHANGED
|
@@ -103,8 +103,9 @@ export async function installCommand(corePath, targetRoot) {
|
|
|
103
103
|
const hasAssets = existsSync(path.join(corePath, 'assets'));
|
|
104
104
|
const hasScripts = existsSync(path.join(corePath, 'scripts'));
|
|
105
105
|
const hasRules = existsSync(path.join(corePath, 'rules'));
|
|
106
|
+
const hasReferences = existsSync(path.join(corePath, 'references'));
|
|
106
107
|
|
|
107
|
-
if (hasAssets || hasScripts || hasRules) {
|
|
108
|
+
if (hasAssets || hasScripts || hasRules || hasReferences) {
|
|
108
109
|
// Use directory structure for commands with resources
|
|
109
110
|
const targetDir = path.join(targetRoot, COMMANDS_DIR, skillName);
|
|
110
111
|
mkdirSync(targetDir, { recursive: true });
|
|
@@ -118,7 +119,7 @@ export async function installCommand(corePath, targetRoot) {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
// Copy assets and scripts
|
|
121
|
-
for (const subdir of ['scripts', 'assets', 'rules']) {
|
|
122
|
+
for (const subdir of ['scripts', 'assets', 'rules', 'references']) {
|
|
122
123
|
const srcSubdir = path.join(corePath, subdir);
|
|
123
124
|
if (existsSync(srcSubdir)) {
|
|
124
125
|
cpSync(srcSubdir, path.join(targetDir, subdir), { recursive: true });
|
|
@@ -938,10 +938,28 @@ for f in data.get('stuck_features', []):
|
|
|
938
938
|
if [[ "$next_feature" == "PIPELINE_COMPLETE" ]]; then
|
|
939
939
|
echo ""
|
|
940
940
|
log_success "════════════════════════════════════════════════════"
|
|
941
|
-
log_success "
|
|
941
|
+
log_success " Pipeline finished."
|
|
942
942
|
log_success " Total sessions: $session_count"
|
|
943
943
|
log_success " Total subagent calls: $total_subagent_calls"
|
|
944
944
|
log_success "════════════════════════════════════════════════════"
|
|
945
|
+
|
|
946
|
+
# Check for auto-skipped features
|
|
947
|
+
local auto_skipped_count
|
|
948
|
+
auto_skipped_count=$(python3 -c "
|
|
949
|
+
import json, sys
|
|
950
|
+
with open(sys.argv[1]) as f:
|
|
951
|
+
data = json.load(f)
|
|
952
|
+
count = sum(1 for f in data.get('features', []) if f.get('status') == 'auto_skipped')
|
|
953
|
+
print(count)
|
|
954
|
+
" "$feature_list" 2>/dev/null || echo "0")
|
|
955
|
+
|
|
956
|
+
if [[ "$auto_skipped_count" -gt 0 ]]; then
|
|
957
|
+
echo ""
|
|
958
|
+
log_warn "$auto_skipped_count feature(s) were auto-skipped due to failed dependencies."
|
|
959
|
+
log_warn "Run './run.sh status' to see details."
|
|
960
|
+
log_warn "Run './run.sh unskip' to reset and retry them."
|
|
961
|
+
fi
|
|
962
|
+
|
|
945
963
|
break
|
|
946
964
|
fi
|
|
947
965
|
|
|
@@ -1093,6 +1111,7 @@ show_help() {
|
|
|
1093
1111
|
echo " run [feature-list.json] [--features <filter>] Run features (all or filtered subset)"
|
|
1094
1112
|
echo " run <feature-id> [options] Run a single feature"
|
|
1095
1113
|
echo " status [feature-list.json] Show pipeline status"
|
|
1114
|
+
echo " unskip [feature-id] [feature-list.json] Reset auto-skipped/failed features"
|
|
1096
1115
|
echo " test-cli Test AI CLI: show detected CLI, version, and model"
|
|
1097
1116
|
echo " reset Clear all state and start fresh"
|
|
1098
1117
|
echo " help Show this help message"
|
|
@@ -1244,6 +1263,40 @@ case "${1:-run}" in
|
|
|
1244
1263
|
rm -rf "$STATE_DIR"
|
|
1245
1264
|
log_success "State cleared. Run './run.sh run' to start fresh."
|
|
1246
1265
|
;;
|
|
1266
|
+
unskip)
|
|
1267
|
+
check_dependencies
|
|
1268
|
+
if [[ ! -f "$STATE_DIR/pipeline.json" ]]; then
|
|
1269
|
+
log_error "No pipeline state found. Run './run.sh run' first."
|
|
1270
|
+
exit 1
|
|
1271
|
+
fi
|
|
1272
|
+
_unskip_feature_list="feature-list.json"
|
|
1273
|
+
_unskip_feature_id=""
|
|
1274
|
+
shift || true
|
|
1275
|
+
# Parse arguments: optional feature-id and feature-list path
|
|
1276
|
+
while [[ $# -gt 0 ]]; do
|
|
1277
|
+
if [[ "$1" =~ ^[Ff]-[0-9]+ ]]; then
|
|
1278
|
+
_unskip_feature_id="$1"
|
|
1279
|
+
else
|
|
1280
|
+
_unskip_feature_list="$1"
|
|
1281
|
+
fi
|
|
1282
|
+
shift
|
|
1283
|
+
done
|
|
1284
|
+
_unskip_args=(
|
|
1285
|
+
--feature-list "$_unskip_feature_list"
|
|
1286
|
+
--state-dir "$STATE_DIR"
|
|
1287
|
+
--action unskip
|
|
1288
|
+
)
|
|
1289
|
+
if [[ -n "$_unskip_feature_id" ]]; then
|
|
1290
|
+
_unskip_args+=(--feature-id "$_unskip_feature_id")
|
|
1291
|
+
fi
|
|
1292
|
+
python3 "$SCRIPTS_DIR/update-feature-status.py" "${_unskip_args[@]}"
|
|
1293
|
+
|
|
1294
|
+
# Commit the status change
|
|
1295
|
+
if ! git diff --quiet "$_unskip_feature_list" 2>/dev/null; then
|
|
1296
|
+
git add "$_unskip_feature_list"
|
|
1297
|
+
git commit -m "chore: unskip auto-skipped features" 2>/dev/null || true
|
|
1298
|
+
fi
|
|
1299
|
+
;;
|
|
1247
1300
|
help|--help|-h)
|
|
1248
1301
|
show_help
|
|
1249
1302
|
;;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Core state machine for updating feature status in the dev-pipeline.
|
|
3
3
|
|
|
4
|
-
Handles
|
|
4
|
+
Handles nine actions:
|
|
5
5
|
- get_next: Find the next feature to process based on priority and dependencies
|
|
6
6
|
- start: Mark a feature as in_progress when a session starts
|
|
7
7
|
- update: Update a feature's status based on session outcome
|
|
@@ -10,11 +10,12 @@ Handles eight actions:
|
|
|
10
10
|
- reset: Reset a feature to pending (status + retry count)
|
|
11
11
|
- clean: Reset + delete session history + delete prizmkit artifacts
|
|
12
12
|
- complete: Shortcut for manually marking a feature as completed
|
|
13
|
+
- unskip: Recover auto-skipped features (reset failed/skipped upstream + auto_skipped downstream)
|
|
13
14
|
|
|
14
15
|
Usage:
|
|
15
16
|
python3 update-feature-status.py \
|
|
16
17
|
--feature-list <path> --state-dir <path> \
|
|
17
|
-
--action <get_next|start|update|status|pause|reset|clean|complete> \
|
|
18
|
+
--action <get_next|start|update|status|pause|reset|clean|complete|unskip> \
|
|
18
19
|
[--feature-id <id>] [--session-status <status>] \
|
|
19
20
|
[--session-id <id>] [--max-retries <n>] \
|
|
20
21
|
[--features <filter>]
|
|
@@ -25,6 +26,7 @@ import json
|
|
|
25
26
|
import os
|
|
26
27
|
import re
|
|
27
28
|
import shutil
|
|
29
|
+
import sys
|
|
28
30
|
from datetime import datetime, timezone
|
|
29
31
|
|
|
30
32
|
from utils import (
|
|
@@ -48,7 +50,7 @@ SESSION_STATUS_VALUES = [
|
|
|
48
50
|
"merge_conflict",
|
|
49
51
|
]
|
|
50
52
|
|
|
51
|
-
TERMINAL_STATUSES = {"completed", "failed", "skipped"}
|
|
53
|
+
TERMINAL_STATUSES = {"completed", "failed", "skipped", "auto_skipped", "split"}
|
|
52
54
|
|
|
53
55
|
|
|
54
56
|
def parse_args():
|
|
@@ -68,7 +70,7 @@ def parse_args():
|
|
|
68
70
|
parser.add_argument(
|
|
69
71
|
"--action",
|
|
70
72
|
required=True,
|
|
71
|
-
choices=["get_next", "start", "update", "status", "pause", "reset", "clean", "complete"],
|
|
73
|
+
choices=["get_next", "start", "update", "status", "pause", "reset", "clean", "complete", "unskip"],
|
|
72
74
|
help="Action to perform",
|
|
73
75
|
)
|
|
74
76
|
parser.add_argument(
|
|
@@ -322,6 +324,116 @@ def load_session_status(state_dir, feature_id, session_id):
|
|
|
322
324
|
return data, None
|
|
323
325
|
|
|
324
326
|
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
# Auto-skip: cascade failure to blocked downstream features
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
def auto_skip_blocked_features(feature_list_path, state_dir, failed_feature_id):
|
|
332
|
+
"""Recursively mark all downstream features blocked by a failed feature as auto_skipped.
|
|
333
|
+
|
|
334
|
+
When a feature is marked as failed, any feature whose dependency chain includes
|
|
335
|
+
the failed feature can never be executed. This function propagates the failure
|
|
336
|
+
by marking those blocked features as auto_skipped, allowing the pipeline to
|
|
337
|
+
continue processing unblocked features and eventually reach PIPELINE_COMPLETE.
|
|
338
|
+
|
|
339
|
+
Re-reads feature-list.json from disk to get the latest state (including the
|
|
340
|
+
just-written failed status from update_feature_in_list).
|
|
341
|
+
|
|
342
|
+
NOTE: This function performs a read-modify-write on feature-list.json without
|
|
343
|
+
file locking. The caller (action_update) also writes to feature-list.json
|
|
344
|
+
immediately before calling this. Safe for single-pipeline execution, but if
|
|
345
|
+
multiple pipeline instances share the same feature-list.json concurrently,
|
|
346
|
+
a race condition may cause lost writes. Add file locking if parallel pipelines
|
|
347
|
+
are introduced.
|
|
348
|
+
"""
|
|
349
|
+
data, err = load_json_file(feature_list_path)
|
|
350
|
+
if err:
|
|
351
|
+
return []
|
|
352
|
+
features = data.get("features", [])
|
|
353
|
+
|
|
354
|
+
# Build current status map
|
|
355
|
+
status_map = {}
|
|
356
|
+
for f in features:
|
|
357
|
+
if isinstance(f, dict) and f.get("id"):
|
|
358
|
+
status_map[f["id"]] = f.get("status", "pending")
|
|
359
|
+
|
|
360
|
+
# Collect all features to auto-skip (recursive propagation)
|
|
361
|
+
to_skip = set()
|
|
362
|
+
changed = True
|
|
363
|
+
while changed:
|
|
364
|
+
changed = False
|
|
365
|
+
for f in features:
|
|
366
|
+
if not isinstance(f, dict):
|
|
367
|
+
continue
|
|
368
|
+
fid = f.get("id")
|
|
369
|
+
if not fid or fid in to_skip:
|
|
370
|
+
continue
|
|
371
|
+
current = status_map.get(fid, "pending")
|
|
372
|
+
if current in TERMINAL_STATUSES:
|
|
373
|
+
continue
|
|
374
|
+
deps = f.get("dependencies", [])
|
|
375
|
+
for dep_id in deps:
|
|
376
|
+
dep_status = status_map.get(dep_id, "pending")
|
|
377
|
+
if dep_status in ("failed", "skipped", "auto_skipped") or dep_id in to_skip:
|
|
378
|
+
to_skip.add(fid)
|
|
379
|
+
status_map[fid] = "auto_skipped"
|
|
380
|
+
changed = True
|
|
381
|
+
break
|
|
382
|
+
|
|
383
|
+
if not to_skip:
|
|
384
|
+
return []
|
|
385
|
+
|
|
386
|
+
# Batch-write to feature-list.json
|
|
387
|
+
for f in features:
|
|
388
|
+
if isinstance(f, dict) and f.get("id") in to_skip:
|
|
389
|
+
f["status"] = "auto_skipped"
|
|
390
|
+
write_json_file(feature_list_path, data)
|
|
391
|
+
|
|
392
|
+
# Sync status.json for each auto-skipped feature
|
|
393
|
+
for fid in to_skip:
|
|
394
|
+
fs = load_feature_status(state_dir, fid)
|
|
395
|
+
fs["status"] = "auto_skipped"
|
|
396
|
+
fs["updated_at"] = now_iso()
|
|
397
|
+
save_feature_status(state_dir, fid, fs)
|
|
398
|
+
|
|
399
|
+
# Build blocking reason map for logging
|
|
400
|
+
skipped_info = []
|
|
401
|
+
for f in features:
|
|
402
|
+
if not isinstance(f, dict):
|
|
403
|
+
continue
|
|
404
|
+
fid = f.get("id")
|
|
405
|
+
if fid not in to_skip:
|
|
406
|
+
continue
|
|
407
|
+
deps = f.get("dependencies", [])
|
|
408
|
+
blockers = [
|
|
409
|
+
d for d in deps
|
|
410
|
+
if d == failed_feature_id or d in to_skip
|
|
411
|
+
]
|
|
412
|
+
skipped_info.append({
|
|
413
|
+
"feature_id": fid,
|
|
414
|
+
"title": f.get("title", ""),
|
|
415
|
+
"blocked_by": blockers,
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
print(
|
|
419
|
+
"[auto-skip] {} feature(s) auto-skipped due to failed {}:".format(
|
|
420
|
+
len(skipped_info), failed_feature_id
|
|
421
|
+
),
|
|
422
|
+
file=sys.stderr,
|
|
423
|
+
)
|
|
424
|
+
for info in skipped_info:
|
|
425
|
+
print(
|
|
426
|
+
" {} ({}) — blocked by {}".format(
|
|
427
|
+
info["feature_id"],
|
|
428
|
+
info["title"],
|
|
429
|
+
", ".join(info["blocked_by"]),
|
|
430
|
+
),
|
|
431
|
+
file=sys.stderr,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
return skipped_info
|
|
435
|
+
|
|
436
|
+
|
|
325
437
|
# ---------------------------------------------------------------------------
|
|
326
438
|
# Action: get_next
|
|
327
439
|
# ---------------------------------------------------------------------------
|
|
@@ -330,7 +442,7 @@ def action_get_next(feature_list_data, state_dir, feature_filter=None):
|
|
|
330
442
|
"""Find the next feature to process.
|
|
331
443
|
|
|
332
444
|
Priority logic:
|
|
333
|
-
1. Skip terminal statuses (completed, failed, skipped)
|
|
445
|
+
1. Skip terminal statuses (completed, failed, skipped, auto_skipped, split)
|
|
334
446
|
2. If feature_filter is set, skip features not in the filter
|
|
335
447
|
3. Check that all dependencies are completed
|
|
336
448
|
4. Prefer in_progress features over pending ones (interrupted session resume)
|
|
@@ -551,6 +663,13 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
551
663
|
error_out("Failed to save feature status: {}".format(err))
|
|
552
664
|
return
|
|
553
665
|
|
|
666
|
+
# Auto-skip downstream features when this feature is marked as failed or skipped
|
|
667
|
+
auto_skipped_features = []
|
|
668
|
+
if fs["status"] in ("failed", "skipped"):
|
|
669
|
+
auto_skipped_features = auto_skip_blocked_features(
|
|
670
|
+
feature_list_path, state_dir, feature_id
|
|
671
|
+
)
|
|
672
|
+
|
|
554
673
|
summary = {
|
|
555
674
|
"action": "update",
|
|
556
675
|
"feature_id": feature_id,
|
|
@@ -560,6 +679,8 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
560
679
|
"resume_from_phase": fs.get("resume_from_phase"),
|
|
561
680
|
"updated_at": fs["updated_at"],
|
|
562
681
|
}
|
|
682
|
+
if auto_skipped_features:
|
|
683
|
+
summary["auto_skipped"] = [info["feature_id"] for info in auto_skipped_features]
|
|
563
684
|
if session_status in ("commit_missing", "docs_missing", "merge_conflict"):
|
|
564
685
|
summary["degraded_reason"] = session_status
|
|
565
686
|
summary["restart_policy"] = "finalization_retry"
|
|
@@ -760,6 +881,7 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
760
881
|
"failed": 0,
|
|
761
882
|
"pending": 0,
|
|
762
883
|
"skipped": 0,
|
|
884
|
+
"auto_skipped": 0,
|
|
763
885
|
"commit_missing": 0,
|
|
764
886
|
"docs_missing": 0,
|
|
765
887
|
}
|
|
@@ -800,6 +922,8 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
800
922
|
icon = COLOR_RED + "[✗]" + COLOR_RESET
|
|
801
923
|
elif fstatus == "skipped":
|
|
802
924
|
icon = COLOR_GRAY + "[—]" + COLOR_RESET
|
|
925
|
+
elif fstatus == "auto_skipped":
|
|
926
|
+
icon = COLOR_GRAY + "[⊘]" + COLOR_RESET
|
|
803
927
|
elif fstatus == "commit_missing":
|
|
804
928
|
icon = COLOR_RED + "[↑]" + COLOR_RESET
|
|
805
929
|
elif fstatus == "docs_missing":
|
|
@@ -818,6 +942,14 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
818
942
|
]
|
|
819
943
|
if blocking:
|
|
820
944
|
detail = " (blocked by {})".format(", ".join(blocking))
|
|
945
|
+
elif fstatus == "auto_skipped":
|
|
946
|
+
deps = feature.get("dependencies", [])
|
|
947
|
+
blockers = [
|
|
948
|
+
d for d in deps
|
|
949
|
+
if status_map.get(d, "pending") in ("failed", "skipped", "auto_skipped")
|
|
950
|
+
]
|
|
951
|
+
if blockers:
|
|
952
|
+
detail = " (auto-skipped: blocked by {})".format(", ".join(blockers))
|
|
821
953
|
|
|
822
954
|
# Apply color to the whole line content
|
|
823
955
|
if fstatus == "completed":
|
|
@@ -863,8 +995,8 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
863
995
|
summary_line = "Total: {} features | Completed: {} | In Progress: {}".format(
|
|
864
996
|
total, completed, counts["in_progress"]
|
|
865
997
|
)
|
|
866
|
-
summary_line2 = "Failed: {} | Pending: {} | Skipped: {}".format(
|
|
867
|
-
counts["failed"], counts["pending"], counts["skipped"]
|
|
998
|
+
summary_line2 = "Failed: {} | Pending: {} | Skipped: {} | Auto-skipped: {}".format(
|
|
999
|
+
counts["failed"], counts["pending"], counts["skipped"], counts["auto_skipped"]
|
|
868
1000
|
)
|
|
869
1001
|
summary_line3 = "Commit Missing: {} | Docs Missing: {}".format(
|
|
870
1002
|
counts["commit_missing"], counts["docs_missing"]
|
|
@@ -1086,6 +1218,152 @@ def action_clean(args, feature_list_path, state_dir):
|
|
|
1086
1218
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
1087
1219
|
|
|
1088
1220
|
|
|
1221
|
+
# ---------------------------------------------------------------------------
|
|
1222
|
+
# Action: unskip
|
|
1223
|
+
# ---------------------------------------------------------------------------
|
|
1224
|
+
|
|
1225
|
+
def action_unskip(args, feature_list_path, state_dir):
|
|
1226
|
+
"""Recover auto-skipped features by resetting them and their failed upstream.
|
|
1227
|
+
|
|
1228
|
+
Two modes:
|
|
1229
|
+
- --feature-id F-032: Reset the specified failed/skipped feature + all auto_skipped
|
|
1230
|
+
features whose dependency chain includes it.
|
|
1231
|
+
- No --feature-id: Reset ALL failed, skipped, and auto_skipped features to pending.
|
|
1232
|
+
"""
|
|
1233
|
+
feature_id = args.feature_id
|
|
1234
|
+
|
|
1235
|
+
data, err = load_json_file(feature_list_path)
|
|
1236
|
+
if err:
|
|
1237
|
+
error_out("Cannot load feature list: {}".format(err))
|
|
1238
|
+
return
|
|
1239
|
+
features = data.get("features", [])
|
|
1240
|
+
|
|
1241
|
+
to_reset = set()
|
|
1242
|
+
|
|
1243
|
+
if feature_id:
|
|
1244
|
+
# Find the target feature
|
|
1245
|
+
target = None
|
|
1246
|
+
for f in features:
|
|
1247
|
+
if isinstance(f, dict) and f.get("id") == feature_id:
|
|
1248
|
+
target = f
|
|
1249
|
+
break
|
|
1250
|
+
if not target:
|
|
1251
|
+
error_out("Feature '{}' not found in feature-list.json".format(feature_id))
|
|
1252
|
+
return
|
|
1253
|
+
if target.get("status") not in ("failed", "skipped", "auto_skipped"):
|
|
1254
|
+
error_out(
|
|
1255
|
+
"Feature '{}' has status '{}', expected 'failed', 'skipped', or 'auto_skipped'".format(
|
|
1256
|
+
feature_id, target.get("status", "unknown")
|
|
1257
|
+
)
|
|
1258
|
+
)
|
|
1259
|
+
return
|
|
1260
|
+
|
|
1261
|
+
# If target is failed or skipped, reset it and find all auto_skipped descendants
|
|
1262
|
+
if target.get("status") in ("failed", "skipped"):
|
|
1263
|
+
to_reset.add(feature_id)
|
|
1264
|
+
# Find all auto_skipped features that depend (transitively) on this one
|
|
1265
|
+
changed = True
|
|
1266
|
+
while changed:
|
|
1267
|
+
changed = False
|
|
1268
|
+
for f in features:
|
|
1269
|
+
if not isinstance(f, dict):
|
|
1270
|
+
continue
|
|
1271
|
+
fid = f.get("id")
|
|
1272
|
+
if not fid or fid in to_reset:
|
|
1273
|
+
continue
|
|
1274
|
+
if f.get("status") != "auto_skipped":
|
|
1275
|
+
continue
|
|
1276
|
+
deps = f.get("dependencies", [])
|
|
1277
|
+
if any(d in to_reset for d in deps):
|
|
1278
|
+
to_reset.add(fid)
|
|
1279
|
+
changed = True
|
|
1280
|
+
|
|
1281
|
+
# If target is auto_skipped, reset it and its failed upstream + siblings
|
|
1282
|
+
elif target.get("status") == "auto_skipped":
|
|
1283
|
+
to_reset.add(feature_id)
|
|
1284
|
+
# Transitively walk upstream to find ALL failed/auto_skipped ancestors
|
|
1285
|
+
# (e.g., F-001 failed → F-002 auto_skipped → F-003 auto_skipped;
|
|
1286
|
+
# unskip F-003 must also find and reset F-001)
|
|
1287
|
+
upstream_changed = True
|
|
1288
|
+
while upstream_changed:
|
|
1289
|
+
upstream_changed = False
|
|
1290
|
+
for f in features:
|
|
1291
|
+
if not isinstance(f, dict):
|
|
1292
|
+
continue
|
|
1293
|
+
fid = f.get("id")
|
|
1294
|
+
if not fid or fid not in to_reset:
|
|
1295
|
+
continue
|
|
1296
|
+
for dep_id in f.get("dependencies", []):
|
|
1297
|
+
if dep_id in to_reset:
|
|
1298
|
+
continue
|
|
1299
|
+
for dep_f in features:
|
|
1300
|
+
if isinstance(dep_f, dict) and dep_f.get("id") == dep_id:
|
|
1301
|
+
if dep_f.get("status") in ("failed", "skipped", "auto_skipped"):
|
|
1302
|
+
to_reset.add(dep_id)
|
|
1303
|
+
upstream_changed = True
|
|
1304
|
+
# Also reset downstream auto_skipped features blocked by the same upstreams
|
|
1305
|
+
changed = True
|
|
1306
|
+
while changed:
|
|
1307
|
+
changed = False
|
|
1308
|
+
for f in features:
|
|
1309
|
+
if not isinstance(f, dict):
|
|
1310
|
+
continue
|
|
1311
|
+
fid = f.get("id")
|
|
1312
|
+
if not fid or fid in to_reset:
|
|
1313
|
+
continue
|
|
1314
|
+
if f.get("status") != "auto_skipped":
|
|
1315
|
+
continue
|
|
1316
|
+
fdeps = f.get("dependencies", [])
|
|
1317
|
+
if any(d in to_reset for d in fdeps):
|
|
1318
|
+
to_reset.add(fid)
|
|
1319
|
+
changed = True
|
|
1320
|
+
else:
|
|
1321
|
+
# No feature-id: reset ALL failed + skipped + auto_skipped
|
|
1322
|
+
for f in features:
|
|
1323
|
+
if isinstance(f, dict) and f.get("id"):
|
|
1324
|
+
if f.get("status") in ("failed", "skipped", "auto_skipped"):
|
|
1325
|
+
to_reset.add(f["id"])
|
|
1326
|
+
|
|
1327
|
+
if not to_reset:
|
|
1328
|
+
error_out("No features to unskip")
|
|
1329
|
+
return
|
|
1330
|
+
|
|
1331
|
+
# Reset all collected features in feature-list.json
|
|
1332
|
+
reset_details = []
|
|
1333
|
+
for f in features:
|
|
1334
|
+
if isinstance(f, dict) and f.get("id") in to_reset:
|
|
1335
|
+
old_status = f.get("status", "unknown")
|
|
1336
|
+
f["status"] = "pending"
|
|
1337
|
+
reset_details.append({
|
|
1338
|
+
"feature_id": f["id"],
|
|
1339
|
+
"title": f.get("title", ""),
|
|
1340
|
+
"old_status": old_status,
|
|
1341
|
+
})
|
|
1342
|
+
|
|
1343
|
+
err = write_json_file(feature_list_path, data)
|
|
1344
|
+
if err:
|
|
1345
|
+
error_out("Failed to write feature-list.json: {}".format(err))
|
|
1346
|
+
return
|
|
1347
|
+
|
|
1348
|
+
# Reset status.json for each feature
|
|
1349
|
+
for fid in to_reset:
|
|
1350
|
+
fs = load_feature_status(state_dir, fid)
|
|
1351
|
+
fs["status"] = "pending"
|
|
1352
|
+
fs["retry_count"] = 0
|
|
1353
|
+
fs["sessions"] = []
|
|
1354
|
+
fs["last_session_id"] = None
|
|
1355
|
+
fs["resume_from_phase"] = None
|
|
1356
|
+
fs["updated_at"] = now_iso()
|
|
1357
|
+
save_feature_status(state_dir, fid, fs)
|
|
1358
|
+
|
|
1359
|
+
result = {
|
|
1360
|
+
"action": "unskip",
|
|
1361
|
+
"reset_count": len(to_reset),
|
|
1362
|
+
"features": reset_details,
|
|
1363
|
+
}
|
|
1364
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
1365
|
+
|
|
1366
|
+
|
|
1089
1367
|
# ---------------------------------------------------------------------------
|
|
1090
1368
|
# Action: pause
|
|
1091
1369
|
# ---------------------------------------------------------------------------
|
|
@@ -1167,6 +1445,8 @@ def main():
|
|
|
1167
1445
|
action_update(args, args.feature_list, args.state_dir)
|
|
1168
1446
|
elif args.action == "pause":
|
|
1169
1447
|
action_pause(args.state_dir)
|
|
1448
|
+
elif args.action == "unskip":
|
|
1449
|
+
action_unskip(args, args.feature_list, args.state_dir)
|
|
1170
1450
|
|
|
1171
1451
|
|
|
1172
1452
|
if __name__ == "__main__":
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
},
|
|
71
71
|
"status": {
|
|
72
72
|
"type": "string",
|
|
73
|
-
"enum": ["pending", "in_progress", "completed", "failed", "skipped", "split"]
|
|
73
|
+
"enum": ["pending", "in_progress", "completed", "failed", "skipped", "split", "auto_skipped"]
|
|
74
74
|
},
|
|
75
75
|
"session_granularity": {
|
|
76
76
|
"type": "string",
|
|
@@ -24,3 +24,4 @@ def _load_hyphenated_module(module_name, filename):
|
|
|
24
24
|
# Register hyphenated script files so tests can import them with underscores.
|
|
25
25
|
_load_hyphenated_module("generate_bootstrap_prompt", "generate-bootstrap-prompt.py")
|
|
26
26
|
_load_hyphenated_module("generate_bugfix_prompt", "generate-bugfix-prompt.py")
|
|
27
|
+
_load_hyphenated_module("update_feature_status", "update-feature-status.py")
|