@blxzer/cursor-trellis 0.3.5 → 0.4.0

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 (156) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +13 -0
  3. package/dist/cli/index.d.ts.map +1 -1
  4. package/dist/cli/index.js +3 -0
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/commands/goal/index.d.ts +3 -0
  7. package/dist/commands/goal/index.d.ts.map +1 -0
  8. package/dist/commands/goal/index.js +159 -0
  9. package/dist/commands/goal/index.js.map +1 -0
  10. package/dist/commands/goal/review.d.ts +6 -0
  11. package/dist/commands/goal/review.d.ts.map +1 -0
  12. package/dist/commands/goal/review.js +28 -0
  13. package/dist/commands/goal/review.js.map +1 -0
  14. package/dist/commands/sdk/run.d.ts.map +1 -1
  15. package/dist/commands/sdk/run.js +2 -27
  16. package/dist/commands/sdk/run.js.map +1 -1
  17. package/dist/commands/update.d.ts.map +1 -1
  18. package/dist/commands/update.js +5 -1
  19. package/dist/commands/update.js.map +1 -1
  20. package/dist/configurators/workflow.d.ts.map +1 -1
  21. package/dist/configurators/workflow.js +51 -2
  22. package/dist/configurators/workflow.js.map +1 -1
  23. package/dist/constants/paths.d.ts +4 -0
  24. package/dist/constants/paths.d.ts.map +1 -1
  25. package/dist/constants/paths.js +4 -0
  26. package/dist/constants/paths.js.map +1 -1
  27. package/dist/goal/action-packet.d.ts +89 -0
  28. package/dist/goal/action-packet.d.ts.map +1 -0
  29. package/dist/goal/action-packet.js +113 -0
  30. package/dist/goal/action-packet.js.map +1 -0
  31. package/dist/goal/audit.d.ts +14 -0
  32. package/dist/goal/audit.d.ts.map +1 -0
  33. package/dist/goal/audit.js +12 -0
  34. package/dist/goal/audit.js.map +1 -0
  35. package/dist/goal/constants.d.ts +26 -0
  36. package/dist/goal/constants.d.ts.map +1 -0
  37. package/dist/goal/constants.js +24 -0
  38. package/dist/goal/constants.js.map +1 -0
  39. package/dist/goal/paths.d.ts +8 -0
  40. package/dist/goal/paths.d.ts.map +1 -0
  41. package/dist/goal/paths.js +36 -0
  42. package/dist/goal/paths.js.map +1 -0
  43. package/dist/goal/preflight.d.ts +21 -0
  44. package/dist/goal/preflight.d.ts.map +1 -0
  45. package/dist/goal/preflight.js +97 -0
  46. package/dist/goal/preflight.js.map +1 -0
  47. package/dist/goal/review-seam.d.ts +44 -0
  48. package/dist/goal/review-seam.d.ts.map +1 -0
  49. package/dist/goal/review-seam.js +153 -0
  50. package/dist/goal/review-seam.js.map +1 -0
  51. package/dist/goal/reviewer-rules.d.ts +13 -0
  52. package/dist/goal/reviewer-rules.d.ts.map +1 -0
  53. package/dist/goal/reviewer-rules.js +153 -0
  54. package/dist/goal/reviewer-rules.js.map +1 -0
  55. package/dist/goal/reviewer.d.ts +9 -0
  56. package/dist/goal/reviewer.d.ts.map +1 -0
  57. package/dist/goal/reviewer.js +54 -0
  58. package/dist/goal/reviewer.js.map +1 -0
  59. package/dist/goal/runtime.d.ts +32 -0
  60. package/dist/goal/runtime.d.ts.map +1 -0
  61. package/dist/goal/runtime.js +242 -0
  62. package/dist/goal/runtime.js.map +1 -0
  63. package/dist/goal/sdk-client.d.ts +10 -0
  64. package/dist/goal/sdk-client.d.ts.map +1 -0
  65. package/dist/goal/sdk-client.js +34 -0
  66. package/dist/goal/sdk-client.js.map +1 -0
  67. package/dist/goal/state.d.ts +40 -0
  68. package/dist/goal/state.d.ts.map +1 -0
  69. package/dist/goal/state.js +37 -0
  70. package/dist/goal/state.js.map +1 -0
  71. package/dist/goal/task-triggers.d.ts +15 -0
  72. package/dist/goal/task-triggers.d.ts.map +1 -0
  73. package/dist/goal/task-triggers.js +23 -0
  74. package/dist/goal/task-triggers.js.map +1 -0
  75. package/dist/goal/walls.d.ts +12 -0
  76. package/dist/goal/walls.d.ts.map +1 -0
  77. package/dist/goal/walls.js +65 -0
  78. package/dist/goal/walls.js.map +1 -0
  79. package/dist/goal/worker-prompt.d.ts +13 -0
  80. package/dist/goal/worker-prompt.d.ts.map +1 -0
  81. package/dist/goal/worker-prompt.js +41 -0
  82. package/dist/goal/worker-prompt.js.map +1 -0
  83. package/dist/goal/worker.d.ts +42 -0
  84. package/dist/goal/worker.d.ts.map +1 -0
  85. package/dist/goal/worker.js +180 -0
  86. package/dist/goal/worker.js.map +1 -0
  87. package/dist/migrations/manifests/0.3.5.json +9 -0
  88. package/dist/migrations/manifests/0.3.6.json +9 -0
  89. package/dist/migrations/manifests/0.4.0.json +9 -0
  90. package/dist/templates/common/commands/continue.md +15 -3
  91. package/dist/templates/common/skills/check.md +7 -4
  92. package/dist/templates/cursor/agents/cstl-check.md +1 -0
  93. package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
  94. package/dist/templates/cursor/fixtures/expected-rules.js +6 -1
  95. package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
  96. package/dist/templates/cursor/rules/cstl-cursor-modes.mdc +32 -0
  97. package/dist/templates/cursor/rules/cstl-triage.mdc +2 -4
  98. package/dist/templates/cursor/rules/retrieval-routing.mdc +26 -7
  99. package/dist/templates/markdown/index.d.ts +9 -0
  100. package/dist/templates/markdown/index.d.ts.map +1 -1
  101. package/dist/templates/markdown/index.js +9 -0
  102. package/dist/templates/markdown/index.js.map +1 -1
  103. package/dist/templates/markdown/spec/guides/artifact-locale-guide.md.txt +93 -0
  104. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +7 -7
  105. package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +38 -21
  106. package/dist/templates/markdown/spec/guides/cursor-native-modes-guide.md.txt +125 -0
  107. package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +10 -8
  108. package/dist/templates/markdown/spec/guides/debug-loop-guide.md.txt +227 -0
  109. package/dist/templates/markdown/spec/guides/goal-release-regression-runbook.md.txt +132 -0
  110. package/dist/templates/markdown/spec/guides/index.md.txt +144 -104
  111. package/dist/templates/markdown/spec/guides/injection-budget-guide.md.txt +111 -0
  112. package/dist/templates/markdown/spec/guides/prototype-guide.md.txt +139 -0
  113. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +28 -6
  114. package/dist/templates/markdown/spec/guides/test-discipline-guide.md.txt +138 -0
  115. package/dist/templates/markdown/spec/guides/verification-strength-guide.md.txt +185 -0
  116. package/dist/templates/shared-hooks/session-start.py +10 -0
  117. package/dist/templates/trellis/config.yaml +18 -0
  118. package/dist/templates/trellis/index.d.ts +17 -0
  119. package/dist/templates/trellis/index.d.ts.map +1 -1
  120. package/dist/templates/trellis/index.js +40 -0
  121. package/dist/templates/trellis/index.js.map +1 -1
  122. package/dist/templates/trellis/pool/README.md +103 -0
  123. package/dist/templates/trellis/pool/items/.gitkeep +0 -0
  124. package/dist/templates/trellis/pool/plan.md +26 -0
  125. package/dist/templates/trellis/scripts/common/artifact_locale.py +278 -0
  126. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +37 -2
  127. package/dist/templates/trellis/scripts/common/config.py +39 -0
  128. package/dist/templates/trellis/scripts/common/injection_budget.py +317 -0
  129. package/dist/templates/trellis/scripts/common/parent_orchestration.py +161 -5
  130. package/dist/templates/trellis/scripts/common/pool_store.py +702 -0
  131. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +9 -3
  132. package/dist/templates/trellis/scripts/common/session_context.py +32 -4
  133. package/dist/templates/trellis/scripts/common/session_memory.py +26 -0
  134. package/dist/templates/trellis/scripts/common/subagent_dispatch.py +74 -9
  135. package/dist/templates/trellis/scripts/common/task_context.py +18 -0
  136. package/dist/templates/trellis/scripts/common/task_dashboard.py +24 -1
  137. package/dist/templates/trellis/scripts/common/task_dependencies.py +673 -0
  138. package/dist/templates/trellis/scripts/common/task_gates.py +58 -5
  139. package/dist/templates/trellis/scripts/common/task_map.py +40 -8
  140. package/dist/templates/trellis/scripts/common/task_store.py +400 -39
  141. package/dist/templates/trellis/scripts/common/test_depends_mode_block.py +489 -0
  142. package/dist/templates/trellis/scripts/common/test_observable_defaults.py +135 -0
  143. package/dist/templates/trellis/scripts/common/test_pool_store.py +428 -0
  144. package/dist/templates/trellis/scripts/common/test_task_dependencies.py +345 -0
  145. package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +14 -12
  146. package/dist/templates/trellis/scripts/generate_dispatch_prompt.py +182 -0
  147. package/dist/templates/trellis/scripts/injection_budget_probe.py +69 -0
  148. package/dist/templates/trellis/scripts/pool.py +192 -0
  149. package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +3 -3
  150. package/dist/templates/trellis/scripts/spec_health_outcomes.py +160 -0
  151. package/dist/templates/trellis/scripts/task.py +85 -1
  152. package/dist/templates/trellis/scripts/verify_evidence_probe.py +138 -0
  153. package/dist/templates/trellis/tasks/locale/en/default-prd.md +19 -0
  154. package/dist/templates/trellis/tasks/locale/zh/default-prd.md +19 -0
  155. package/dist/templates/trellis/workflow.md +55 -5
  156. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python3
1
+ #!/usr/bin/env python
2
2
  """
3
3
  Task CRUD operations.
4
4
 
@@ -9,6 +9,8 @@ Provides:
9
9
  cmd_set_branch - Set git branch for task
10
10
  cmd_set_base_branch - Set PR target branch
11
11
  cmd_set_scope - Set scope for PR title
12
+ cmd_set_deps - Set task-level depends_on (declare + soft checks)
13
+ cmd_set_depends_mode - Set meta.depends_mode (warn | block | off)
12
14
  cmd_add_subtask - Link child task to parent
13
15
  cmd_remove_subtask - Unlink child task from parent
14
16
  cmd_prepare_child_worktree - Create/register Child git worktree
@@ -25,6 +27,11 @@ import sys
25
27
  from datetime import datetime
26
28
  from pathlib import Path
27
29
 
30
+ from .artifact_locale import (
31
+ default_prd_content,
32
+ default_verify_content,
33
+ resolve_artifact_locale,
34
+ )
28
35
  from .config import (
29
36
  get_packages,
30
37
  get_session_auto_commit,
@@ -67,6 +74,7 @@ from .task_map import (
67
74
  get_child_state,
68
75
  record_child_worktree,
69
76
  remove_child_from_task_map,
77
+ resolve_child_map_id,
70
78
  set_child_state,
71
79
  set_parent_child_integration_state,
72
80
  validate_parent_child_integration,
@@ -249,11 +257,10 @@ _SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
249
257
  ".pi", # Pi Agent
250
258
  )
251
259
 
252
- _SEED_EXAMPLE = (
253
- "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. "
254
- "Put spec/research files only — no code paths. "
255
- "Run `python .cstl/scripts/get_context.py --mode packages` to list available specs. "
256
- "Delete this line once real entries are added."
260
+ _STABLE_SPEC_SEED_PATHS: tuple[str, ...] = (
261
+ ".cstl/spec/guides/index.md",
262
+ ".cstl/spec/guides/verification-strength-guide.md",
263
+ ".cstl/spec/guides/injection-budget-guide.md",
257
264
  )
258
265
 
259
266
 
@@ -270,41 +277,70 @@ def _has_subagent_platform(repo_root: Path) -> bool:
270
277
  return False
271
278
 
272
279
 
273
- def _write_seed_jsonl(path: Path) -> None:
274
- """Write a one-line seed JSONL file with a self-describing ``_example``.
275
-
276
- The seed row has no ``file`` field, so downstream consumers (hooks +
277
- preludes) that iterate entries via ``item.get("file")`` naturally skip
278
- it. The row exists purely as an in-file prompt for the AI curator.
279
- """
280
- seed = {"_example": _SEED_EXAMPLE}
281
- path.write_text(json.dumps(seed, ensure_ascii=False) + "\n", encoding="utf-8")
282
-
283
-
284
- def _default_prd_content(title: str, description: str | None = None) -> str:
285
- """Return the default PRD skeleton created with every task."""
286
- goal = (description or "").strip() or "TBD."
287
- heading = title.strip() or "Untitled task"
288
- return f"""# {heading}
289
-
290
- ## Goal
291
-
292
- {goal}
293
-
294
- ## Requirements
280
+ def _resolve_seed_spec_paths(
281
+ repo_root: Path,
282
+ task_dir: Path,
283
+ task_data: dict,
284
+ ) -> list[str]:
285
+ """Return 2–3 existing spec file paths for jsonl create seeds."""
286
+ from .task_gates import suggest_spec_targets
287
+
288
+ candidates: list[str] = []
289
+ for path in suggest_spec_targets(repo_root, task_dir, task_data):
290
+ if path not in candidates:
291
+ candidates.append(path)
292
+ for rel in _STABLE_SPEC_SEED_PATHS:
293
+ if rel not in candidates:
294
+ candidates.append(rel)
295
+
296
+ existing = [rel for rel in candidates if (repo_root / rel).is_file()]
297
+ if len(existing) >= 2:
298
+ return existing[:3]
299
+
300
+ fallback = [
301
+ rel for rel in _STABLE_SPEC_SEED_PATHS if (repo_root / rel).is_file()
302
+ ]
303
+ return fallback[:3]
295
304
 
296
- - TBD
297
305
 
298
- ## Acceptance Criteria
306
+ def _write_seed_jsonl(
307
+ path: Path,
308
+ repo_root: Path,
309
+ task_dir: Path,
310
+ task_data: dict,
311
+ ) -> None:
312
+ """Write 2–3 JSONL rows with real spec ``file`` paths for sub-agent context."""
313
+ spec_paths = _resolve_seed_spec_paths(repo_root, task_dir, task_data)
314
+ if len(spec_paths) < 2:
315
+ raise ValueError(
316
+ "task create requires at least two existing spec guide paths for jsonl seed"
317
+ )
299
318
 
300
- - [ ] TBD
319
+ lines: list[str] = []
320
+ for spec_path in spec_paths:
321
+ lines.append(
322
+ json.dumps(
323
+ {
324
+ "file": spec_path,
325
+ "reason": "default spec seed — curate entries for this task",
326
+ },
327
+ ensure_ascii=False,
328
+ )
329
+ )
330
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
301
331
 
302
- ## Notes
303
332
 
304
- - Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
305
- - Lightweight tasks can remain PRD-only.
306
- - For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start-execution --check`.
307
- """
333
+ def _default_prd_content(
334
+ title: str,
335
+ description: str | None = None,
336
+ *,
337
+ repo_root: Path | None = None,
338
+ task_dir: Path | None = None,
339
+ ) -> str:
340
+ """Return the default PRD skeleton created with every task."""
341
+ root = repo_root or get_repo_root()
342
+ locale = resolve_artifact_locale(task_dir, root)
343
+ return default_prd_content(title, description, locale, root)
308
344
 
309
345
 
310
346
  # =============================================================================
@@ -412,7 +448,14 @@ def cmd_create(args: argparse.Namespace) -> int:
412
448
  prd_path = task_dir / "prd.md"
413
449
  if not prd_path.exists():
414
450
  prd_path.write_text(
415
- _default_prd_content(args.title, args.description),
451
+ _default_prd_content(args.title, args.description, repo_root=repo_root, task_dir=task_dir),
452
+ encoding="utf-8",
453
+ )
454
+
455
+ verify_path = task_dir / "verify.md"
456
+ if not verify_path.exists():
457
+ verify_path.write_text(
458
+ default_verify_content(repo_root=repo_root, task_dir=task_dir),
416
459
  encoding="utf-8",
417
460
  )
418
461
 
@@ -425,7 +468,7 @@ def cmd_create(args: argparse.Namespace) -> int:
425
468
  for jsonl_name in ("implement.jsonl", "check.jsonl"):
426
469
  jsonl_path = task_dir / jsonl_name
427
470
  if not jsonl_path.exists():
428
- _write_seed_jsonl(jsonl_path)
471
+ _write_seed_jsonl(jsonl_path, repo_root, task_dir, task_data)
429
472
  seeded_jsonl = True
430
473
 
431
474
  # Handle --parent: establish bidirectional link
@@ -1086,6 +1129,129 @@ def cmd_prepare_child_worktree(args: argparse.Namespace) -> int:
1086
1129
  return 0
1087
1130
 
1088
1131
 
1132
+ # =============================================================================
1133
+ # Plan B: depends_mode block helpers
1134
+ # =============================================================================
1135
+
1136
+ def append_depends_ignore_event(
1137
+ task_data: dict,
1138
+ *,
1139
+ command: str,
1140
+ mode: str,
1141
+ blocking_summary: list[str],
1142
+ evidence: str | None = None,
1143
+ reason: str | None = None,
1144
+ ) -> None:
1145
+ """Append a depends-ignore audit event to meta.depends_ignore_events.
1146
+
1147
+ Mutates task_data in memory; the caller persists it in the SAME
1148
+ write_json as the mutation it accompanies (execution approval / child
1149
+ state), so the event can never dangle as a separate stale write. The
1150
+ list is capped at MAX_IGNORE_EVENTS (FIFO drops the oldest entry).
1151
+ """
1152
+ from .task_dependencies import MAX_IGNORE_EVENTS
1153
+ from .task_map import utc_now
1154
+
1155
+ meta = task_data.get("meta")
1156
+ if not isinstance(meta, dict):
1157
+ meta = {}
1158
+ task_data["meta"] = meta
1159
+ events = meta.get("depends_ignore_events")
1160
+ if not isinstance(events, list):
1161
+ events = []
1162
+ event: dict = {
1163
+ "at": utc_now(),
1164
+ "command": command,
1165
+ "by": "user",
1166
+ "mode": mode,
1167
+ "blocking_summary": list(blocking_summary),
1168
+ }
1169
+ if evidence:
1170
+ event["evidence"] = evidence
1171
+ if reason:
1172
+ event["reason"] = reason
1173
+ events.append(event)
1174
+ del events[:-MAX_IGNORE_EVENTS]
1175
+ meta["depends_ignore_events"] = events
1176
+
1177
+
1178
+ def _guard_child_working_deps(
1179
+ args: argparse.Namespace,
1180
+ parent_dir: Path,
1181
+ parent_data: dict,
1182
+ child_dir: Path,
1183
+ child_data: dict,
1184
+ child_map_id: str,
1185
+ ) -> int:
1186
+ """Enforce depends_mode=block before a Child transitions to `working`.
1187
+
1188
+ Reads the CHILD's meta.depends_mode (the Parent mode never proxies for
1189
+ the Child). The dependency union covers the Child task.json depends_on
1190
+ plus the Parent task-map children[].depends_on entry; either source
1191
+ blocking rejects the transition unless --ignore-deps is passed, which
1192
+ appends an audit event to the Child task.json and a line to the Parent
1193
+ task-map Event Log. Returns 0 to allow, nonzero to reject.
1194
+ """
1195
+ from .task_dependencies import describe_child_dependencies, read_depends_mode
1196
+ from .task_map import get_child_entry, load_task_map, write_task_map
1197
+
1198
+ mode = read_depends_mode(child_data)
1199
+ if mode != "block":
1200
+ return 0
1201
+
1202
+ repo_root = get_repo_root()
1203
+ map_data, _ = load_task_map(parent_dir)
1204
+ entry = get_child_entry(map_data, child_map_id)
1205
+ report = describe_child_dependencies(
1206
+ child_dir, child_data, entry, repo_root=repo_root
1207
+ )
1208
+ blocking = report.blocking_errors()
1209
+ if not blocking:
1210
+ return 0
1211
+
1212
+ if not getattr(args, "ignore_deps", False):
1213
+ print(
1214
+ colored(
1215
+ "Error: cannot set Child state to 'working': blocking dependencies "
1216
+ "(depends_mode=block).",
1217
+ Colors.RED,
1218
+ ),
1219
+ file=sys.stderr,
1220
+ )
1221
+ for item in blocking:
1222
+ print(f" - {item}", file=sys.stderr)
1223
+ print(
1224
+ "Run with --ignore-deps to override (audited via meta.depends_ignore_events).",
1225
+ file=sys.stderr,
1226
+ )
1227
+ return 1
1228
+
1229
+ append_depends_ignore_event(
1230
+ child_data,
1231
+ command="set-child-state",
1232
+ mode=mode,
1233
+ blocking_summary=blocking,
1234
+ evidence=args.evidence,
1235
+ reason=getattr(args, "reason", None),
1236
+ )
1237
+ child_json_path = child_dir / FILE_TASK_JSON
1238
+ if not write_json(child_json_path, child_data):
1239
+ print(
1240
+ colored("Error: failed to write task.json (ignore event)", Colors.RED),
1241
+ file=sys.stderr,
1242
+ )
1243
+ return 1
1244
+
1245
+ map_data_evt, map_body = load_task_map(parent_dir)
1246
+ if map_data_evt is not None:
1247
+ event = (
1248
+ f"Ignored dependencies (mode=block) for child `{child_dir.name}` "
1249
+ f"while setting state `working`: {'; '.join(blocking)}"
1250
+ )
1251
+ write_task_map(parent_dir, map_data_evt, map_body, event)
1252
+ return 0
1253
+
1254
+
1089
1255
  # =============================================================================
1090
1256
  # Command: set-child-state
1091
1257
  # =============================================================================
@@ -1136,10 +1302,23 @@ def cmd_set_child_state(args: argparse.Namespace) -> int:
1136
1302
  )
1137
1303
  return 1
1138
1304
 
1305
+ child_map_id = resolve_child_map_id(parent_data, child_dir, child_data)
1306
+ if state == "working":
1307
+ guard_rc = _guard_child_working_deps(
1308
+ args,
1309
+ parent_dir,
1310
+ parent_data,
1311
+ child_dir,
1312
+ child_data,
1313
+ child_map_id,
1314
+ )
1315
+ if guard_rc != 0:
1316
+ return guard_rc
1317
+
1139
1318
  ok, errors = set_child_state(
1140
1319
  parent_dir,
1141
1320
  parent_data,
1142
- child_dir.name,
1321
+ child_map_id,
1143
1322
  state,
1144
1323
  args.evidence,
1145
1324
  getattr(args, "reason", None),
@@ -1265,6 +1444,29 @@ def cmd_integrate_child(args: argparse.Namespace) -> int:
1265
1444
  if merge_ref:
1266
1445
  print(f"Merge executed: git merge --no-ff --no-commit {merge_ref}")
1267
1446
  print(f"Parent map: {_repo_relative_path(parent_dir / 'task-map.md', repo_root)}")
1447
+
1448
+ if state == "integrated" and not getattr(args, "no_publish_pack", False):
1449
+ from .parent_orchestration import build_publish_pack
1450
+
1451
+ summary, pack_errors = build_publish_pack(parent_dir)
1452
+ if pack_errors or summary is None:
1453
+ for item in pack_errors:
1454
+ print(f" - {item}", file=sys.stderr)
1455
+ print(
1456
+ colored(
1457
+ "Warning: integrate succeeded but publish-pack failed; "
1458
+ "PACK may be stale (see parent-status stalePack).",
1459
+ Colors.YELLOW,
1460
+ ),
1461
+ file=sys.stderr,
1462
+ )
1463
+ return 2
1464
+ print(colored("✓ PACK refreshed after integrate", Colors.GREEN))
1465
+ for line in summary.splitlines():
1466
+ if line.startswith("- Generated:"):
1467
+ print(line)
1468
+ break
1469
+
1268
1470
  return 0
1269
1471
 
1270
1472
 
@@ -1638,3 +1840,162 @@ def cmd_set_scope(args: argparse.Namespace) -> int:
1638
1840
 
1639
1841
  print(colored(f"✓ Scope set to: {scope}", Colors.GREEN))
1640
1842
  return 0
1843
+
1844
+
1845
+ # =============================================================================
1846
+ # Command: set-deps
1847
+ # =============================================================================
1848
+
1849
+ def cmd_set_deps(args: argparse.Namespace) -> int:
1850
+ """Set task-level depends_on (Plan A: declare + soft checks, no blocking).
1851
+
1852
+ Bare task ids and `pool:Pxx` refs are supported. Dangling references are
1853
+ warned but still written (Plan A is discovery-first; `warn` never errors).
1854
+ """
1855
+ from .task_dependencies import KIND_MISSING, normalize_dep_list, resolve_dep_ref
1856
+
1857
+ repo_root = get_repo_root()
1858
+ target_dir = resolve_task_dir(args.dir, repo_root)
1859
+
1860
+ task_json = target_dir / FILE_TASK_JSON
1861
+ if not task_json.is_file():
1862
+ print(
1863
+ colored(f"Error: task.json not found at {target_dir}", Colors.RED),
1864
+ file=sys.stderr,
1865
+ )
1866
+ return 1
1867
+
1868
+ data = read_json(task_json)
1869
+ if not data:
1870
+ return 1
1871
+
1872
+ normalized = normalize_dep_list(args.dep)
1873
+ data["depends_on"] = normalized
1874
+
1875
+ dangling = [
1876
+ ref
1877
+ for ref in normalized
1878
+ if resolve_dep_ref(ref, repo_root=repo_root).kind == KIND_MISSING
1879
+ ]
1880
+
1881
+ if not write_json(task_json, data):
1882
+ print(colored("Error: failed to write task.json", Colors.RED), file=sys.stderr)
1883
+ return 1
1884
+
1885
+ if normalized:
1886
+ print(
1887
+ colored(
1888
+ f"✓ depends_on set: {', '.join(normalized)}",
1889
+ Colors.GREEN,
1890
+ )
1891
+ )
1892
+ else:
1893
+ print(colored("✓ depends_on cleared to []", Colors.GREEN))
1894
+ for ref in dangling:
1895
+ print(
1896
+ colored(
1897
+ f"[dependencies] WARN: dangling dependency: {ref} "
1898
+ "(no matching task or child; declaration kept)",
1899
+ Colors.YELLOW,
1900
+ ),
1901
+ file=sys.stderr,
1902
+ )
1903
+ return 0
1904
+
1905
+
1906
+ # =============================================================================
1907
+ # Command: set-depends-mode
1908
+ # =============================================================================
1909
+
1910
+ def cmd_set_depends_mode(args: argparse.Namespace) -> int:
1911
+ """Set meta.depends_mode (warn | block | off) for a task.
1912
+
1913
+ warn is the default (= Plan A soft checks); block opts the task into
1914
+ hard dependency gates at start-execution --approved and
1915
+ set-child-state working; off silences dependency output entirely.
1916
+ """
1917
+ from .task_dependencies import DEPENDS_MODES, read_depends_mode
1918
+
1919
+ repo_root = get_repo_root()
1920
+ target_dir = resolve_task_dir(args.dir, repo_root)
1921
+
1922
+ task_json = target_dir / FILE_TASK_JSON
1923
+ if not task_json.is_file():
1924
+ print(
1925
+ colored(f"Error: task.json not found at {target_dir}", Colors.RED),
1926
+ file=sys.stderr,
1927
+ )
1928
+ return 1
1929
+
1930
+ data = read_json(task_json)
1931
+ if not data:
1932
+ return 1
1933
+
1934
+ previous = read_depends_mode(data)
1935
+ if args.mode not in DEPENDS_MODES:
1936
+ print(
1937
+ colored(
1938
+ f"Error: depends_mode must be one of {', '.join(DEPENDS_MODES)}",
1939
+ Colors.RED,
1940
+ ),
1941
+ file=sys.stderr,
1942
+ )
1943
+ return 1
1944
+
1945
+ meta = data.get("meta")
1946
+ if not isinstance(meta, dict):
1947
+ meta = {}
1948
+ data["meta"] = meta
1949
+ meta["depends_mode"] = args.mode
1950
+
1951
+ if not write_json(task_json, data):
1952
+ print(colored("Error: failed to write task.json", Colors.RED), file=sys.stderr)
1953
+ return 1
1954
+
1955
+ print(
1956
+ colored(
1957
+ f"✓ depends_mode: {previous} -> {args.mode}",
1958
+ Colors.GREEN,
1959
+ )
1960
+ )
1961
+ return 0
1962
+
1963
+
1964
+ # =============================================================================
1965
+ # Command: artifact-locale
1966
+ # =============================================================================
1967
+
1968
+ def cmd_artifact_locale(args: argparse.Namespace) -> int:
1969
+ """Get or set human-reviewed artifact locale (zh | en)."""
1970
+ from .artifact_locale import (
1971
+ resolve_artifact_locale,
1972
+ set_task_artifact_locale,
1973
+ set_workspace_artifact_locale,
1974
+ )
1975
+
1976
+ repo_root = get_repo_root()
1977
+ subcommand = getattr(args, "artifact_locale_command", None)
1978
+
1979
+ if subcommand == "get":
1980
+ task_dir = resolve_task_dir(args.task, repo_root) if args.task else None
1981
+ print(resolve_artifact_locale(task_dir, repo_root))
1982
+ return 0
1983
+
1984
+ if subcommand == "set":
1985
+ try:
1986
+ if args.task:
1987
+ task_dir = resolve_task_dir(args.task, repo_root)
1988
+ set_task_artifact_locale(task_dir, args.locale, repo_root)
1989
+ scope = f"task {task_dir.name}"
1990
+ else:
1991
+ set_workspace_artifact_locale(args.locale, repo_root)
1992
+ scope = "workspace"
1993
+ except (ValueError, FileNotFoundError) as exc:
1994
+ print(colored(f"Error: {exc}", Colors.RED), file=sys.stderr)
1995
+ return 1
1996
+
1997
+ print(colored(f"✓ artifact_locale set to {args.locale} ({scope})", Colors.GREEN))
1998
+ return 0
1999
+
2000
+ print(colored("Error: use artifact-locale get|set", Colors.RED), file=sys.stderr)
2001
+ return 1