@blxzer/cursor-trellis 0.3.6 → 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 (47) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +6 -6
  3. package/dist/commands/update.d.ts.map +1 -1
  4. package/dist/commands/update.js +5 -1
  5. package/dist/commands/update.js.map +1 -1
  6. package/dist/configurators/workflow.d.ts.map +1 -1
  7. package/dist/configurators/workflow.js +39 -2
  8. package/dist/configurators/workflow.js.map +1 -1
  9. package/dist/constants/paths.d.ts +4 -0
  10. package/dist/constants/paths.d.ts.map +1 -1
  11. package/dist/constants/paths.js +4 -0
  12. package/dist/constants/paths.js.map +1 -1
  13. package/dist/migrations/manifests/0.4.0.json +9 -0
  14. package/dist/templates/markdown/index.d.ts +6 -0
  15. package/dist/templates/markdown/index.d.ts.map +1 -1
  16. package/dist/templates/markdown/index.js +6 -0
  17. package/dist/templates/markdown/index.js.map +1 -1
  18. package/dist/templates/markdown/spec/guides/artifact-locale-guide.md.txt +93 -0
  19. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +7 -7
  20. package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +10 -8
  21. package/dist/templates/markdown/spec/guides/debug-loop-guide.md.txt +227 -0
  22. package/dist/templates/markdown/spec/guides/goal-release-regression-runbook.md.txt +132 -0
  23. package/dist/templates/markdown/spec/guides/index.md.txt +37 -0
  24. package/dist/templates/markdown/spec/guides/prototype-guide.md.txt +139 -0
  25. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +4 -0
  26. package/dist/templates/markdown/spec/guides/test-discipline-guide.md.txt +138 -0
  27. package/dist/templates/markdown/spec/guides/verification-strength-guide.md.txt +1 -0
  28. package/dist/templates/trellis/index.d.ts +12 -0
  29. package/dist/templates/trellis/index.d.ts.map +1 -1
  30. package/dist/templates/trellis/index.js +26 -0
  31. package/dist/templates/trellis/index.js.map +1 -1
  32. package/dist/templates/trellis/pool/README.md +103 -0
  33. package/dist/templates/trellis/pool/items/.gitkeep +0 -0
  34. package/dist/templates/trellis/pool/plan.md +26 -0
  35. package/dist/templates/trellis/scripts/common/pool_store.py +702 -0
  36. package/dist/templates/trellis/scripts/common/task_dashboard.py +8 -0
  37. package/dist/templates/trellis/scripts/common/task_dependencies.py +673 -0
  38. package/dist/templates/trellis/scripts/common/task_gates.py +58 -5
  39. package/dist/templates/trellis/scripts/common/task_store.py +256 -0
  40. package/dist/templates/trellis/scripts/common/test_depends_mode_block.py +489 -0
  41. package/dist/templates/trellis/scripts/common/test_pool_store.py +428 -0
  42. package/dist/templates/trellis/scripts/common/test_task_dependencies.py +345 -0
  43. package/dist/templates/trellis/scripts/pool.py +192 -0
  44. package/dist/templates/trellis/scripts/task.py +66 -1
  45. package/dist/templates/trellis/scripts/verify_evidence_probe.py +138 -0
  46. package/dist/templates/trellis/workflow.md +32 -2
  47. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python3
1
+ #!/usr/bin/env python
2
2
  """
3
3
  Task quality gate helpers.
4
4
 
@@ -15,6 +15,11 @@ from dataclasses import dataclass, field
15
15
  from datetime import datetime, timezone
16
16
  from pathlib import Path
17
17
 
18
+ from .task_dependencies import (
19
+ describe_dependencies,
20
+ read_depends_mode,
21
+ scope_children_for_task,
22
+ )
18
23
  from .task_map import (
19
24
  get_child_state,
20
25
  load_task_map,
@@ -144,6 +149,7 @@ STABLE_TASK_KEYS = (
144
149
  "kind",
145
150
  "mode",
146
151
  "contract_epoch",
152
+ "depends_on",
147
153
  )
148
154
 
149
155
 
@@ -161,6 +167,7 @@ class GateGuardResult:
161
167
  is_full_task: bool = False
162
168
  closeout_profile: str = "lite"
163
169
  auto_gate_records: dict[str, dict] = field(default_factory=dict)
170
+ deps_blocking_summary: list[str] = field(default_factory=list)
164
171
 
165
172
 
166
173
  def utc_now() -> str:
@@ -610,8 +617,19 @@ def validate_start_execution(
610
617
  task_dir: Path,
611
618
  task_data: dict | None,
612
619
  approved: bool,
620
+ *,
621
+ ignore_deps: bool = False,
622
+ enforce_deps_block: bool = False,
613
623
  ) -> GateGuardResult:
614
- """Validate start-execution readiness."""
624
+ """Validate start-execution readiness.
625
+
626
+ Dependency policy (Plan B, per adjudication 05 §3.1/§3.2):
627
+ - enforce_deps_block=True is passed ONLY by the mutating `--approved` path.
628
+ mode=block + unmet deps then fail unless ignore_deps=True.
629
+ - mode=off silences dependency warnings entirely.
630
+ - every other path (--check, warn mode) keeps Plan A semantics: deps are
631
+ warnings that never fail the guard.
632
+ """
615
633
  errors: list[str] = []
616
634
  if task_data is None:
617
635
  return GateGuardResult(ok=False, errors=["task.json"])
@@ -691,6 +709,29 @@ def validate_start_execution(
691
709
  if not approved:
692
710
  errors.append("--approved is required for mutation")
693
711
 
712
+ warnings: list[str] = []
713
+ deps_blocking_summary: list[str] = []
714
+ if task_data is not None:
715
+ scope = scope_children_for_task(task_dir, task_data)
716
+ report = describe_dependencies(
717
+ task_dir,
718
+ task_data,
719
+ scope_children=scope,
720
+ )
721
+ depends_mode = read_depends_mode(task_data)
722
+ if enforce_deps_block and depends_mode == "block":
723
+ deps_blocking_summary = report.blocking_errors()
724
+ if ignore_deps:
725
+ warnings.extend(report.warnings())
726
+ else:
727
+ errors.extend(deps_blocking_summary)
728
+ elif depends_mode != "off":
729
+ warnings.extend(report.warnings())
730
+ if depends_mode == "block" and not enforce_deps_block:
731
+ warnings.append(
732
+ "dependency policy mode=block: --approved would fail unless --ignore-deps"
733
+ )
734
+
694
735
  baseline_record = None
695
736
  if not errors:
696
737
  baseline_record = make_baseline_record(
@@ -700,12 +741,14 @@ def validate_start_execution(
700
741
  return GateGuardResult(
701
742
  ok=not errors,
702
743
  errors=errors,
744
+ warnings=warnings,
703
745
  contract_fingerprint=contract_fingerprint,
704
746
  artifact_fingerprints=artifact_fingerprints,
705
747
  required_gates=required_gates,
706
748
  baseline_record=baseline_record,
707
749
  is_full_task=full_task,
708
750
  auto_gate_records=auto_gate_records,
751
+ deps_blocking_summary=deps_blocking_summary,
709
752
  )
710
753
 
711
754
 
@@ -737,9 +780,19 @@ def validate_start_execution_check(
737
780
  task_dir: Path,
738
781
  task_data: dict | None,
739
782
  ) -> GateGuardResult:
740
- """Validate start-execution preflight without requiring approval."""
741
- result = validate_start_execution(task_dir, task_data, approved=True)
742
- return result
783
+ """Validate start-execution preflight without requiring approval.
784
+
785
+ NOTE (adjudication 05 §3.1 trap): the check path deliberately passes
786
+ enforce_deps_block=False — even when the task is in mode=block, --check
787
+ must never fail on dependencies; it only surfaces warnings. Only the
788
+ mutating --approved path may set enforce_deps_block=True.
789
+ """
790
+ return validate_start_execution(
791
+ task_dir,
792
+ task_data,
793
+ approved=True,
794
+ enforce_deps_block=False,
795
+ )
743
796
 
744
797
 
745
798
  def validate_transition_readiness(
@@ -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
@@ -1127,6 +1129,129 @@ def cmd_prepare_child_worktree(args: argparse.Namespace) -> int:
1127
1129
  return 0
1128
1130
 
1129
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
+
1130
1255
  # =============================================================================
1131
1256
  # Command: set-child-state
1132
1257
  # =============================================================================
@@ -1178,6 +1303,18 @@ def cmd_set_child_state(args: argparse.Namespace) -> int:
1178
1303
  return 1
1179
1304
 
1180
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
+
1181
1318
  ok, errors = set_child_state(
1182
1319
  parent_dir,
1183
1320
  parent_data,
@@ -1705,6 +1842,125 @@ def cmd_set_scope(args: argparse.Namespace) -> int:
1705
1842
  return 0
1706
1843
 
1707
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
+
1708
1964
  # =============================================================================
1709
1965
  # Command: artifact-locale
1710
1966
  # =============================================================================