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.
Files changed (28) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/claude/command-adapter.js +3 -2
  3. package/bundled/dev-pipeline/run.sh +54 -1
  4. package/bundled/dev-pipeline/scripts/update-feature-status.py +287 -7
  5. package/bundled/dev-pipeline/templates/feature-list-schema.json +1 -1
  6. package/bundled/dev-pipeline/tests/conftest.py +1 -0
  7. package/bundled/dev-pipeline/tests/test_auto_skip.py +446 -0
  8. package/bundled/skills/_metadata.json +9 -1
  9. package/bundled/skills/app-planner/SKILL.md +110 -28
  10. package/bundled/skills/app-planner/references/architecture-decisions.md +48 -0
  11. package/bundled/skills/app-planner/references/brainstorm-guide.md +101 -0
  12. package/bundled/skills/app-planner/references/browser-interaction.md +34 -0
  13. package/bundled/skills/app-planner/references/error-recovery.md +109 -0
  14. package/bundled/skills/app-planner/references/frontend-design-guide.md +71 -0
  15. package/bundled/skills/app-planner/references/incremental-feature-planning.md +112 -0
  16. package/bundled/skills/app-planner/references/new-app-planning.md +85 -0
  17. package/bundled/skills/app-planner/references/project-conventions.md +93 -0
  18. package/bundled/skills/app-planner/references/red-team-checklist.md +40 -0
  19. package/bundled/skills/app-planner/scripts/validate-and-generate.py +1 -1
  20. package/bundled/skills/prizm-kit/SKILL.md +3 -1
  21. package/bundled/skills/prizmkit-committer/SKILL.md +1 -1
  22. package/bundled/skills/prizmkit-deploy/SKILL.md +112 -0
  23. package/bundled/skills/prizmkit-deploy/assets/deploy-template.md +108 -0
  24. package/bundled/skills/prizmkit-plan/SKILL.md +30 -8
  25. package/bundled/skills/prizmkit-plan/assets/plan-template.md +19 -0
  26. package/bundled/skills/prizmkit-retrospective/SKILL.md +3 -1
  27. package/package.json +1 -1
  28. package/src/scaffold.js +5 -4
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.0.147",
3
- "bundledAt": "2026-03-30T17:09:52.213Z",
4
- "bundledFrom": "4cdb143"
2
+ "frameworkVersion": "1.0.149",
3
+ "bundledAt": "2026-03-31T17:50:05.806Z",
4
+ "bundledFrom": "9c1abf7"
5
5
  }
@@ -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 " All features completed! Pipeline finished."
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 eight actions:
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")