@blxzer/cursor-trellis 0.1.2 → 0.2.1

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 (105) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +45 -21
  3. package/dist/configurators/workflow.d.ts +1 -1
  4. package/dist/configurators/workflow.d.ts.map +1 -1
  5. package/dist/configurators/workflow.js +20 -9
  6. package/dist/configurators/workflow.js.map +1 -1
  7. package/dist/migrations/manifests/0.1.3.json +9 -0
  8. package/dist/migrations/manifests/0.1.4.json +9 -0
  9. package/dist/templates/cursor/rules/retrieval-routing.mdc +9 -7
  10. package/dist/templates/markdown/agents.md +1 -1
  11. package/dist/templates/markdown/index.d.ts +1 -0
  12. package/dist/templates/markdown/index.d.ts.map +1 -1
  13. package/dist/templates/markdown/index.js +1 -0
  14. package/dist/templates/markdown/index.js.map +1 -1
  15. package/dist/templates/markdown/prompts/run-semantic-slice-12.md.txt +13 -29
  16. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +0 -49
  17. package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +0 -85
  18. package/dist/templates/markdown/spec/guides/cursor-semantic-compliance.md.txt +7 -21
  19. package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +6 -10
  20. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +17 -18
  21. package/dist/templates/shared-hooks/inject-retrieval-plan.py +42 -23
  22. package/dist/templates/shared-hooks/inject-subagent-context.py +6 -2
  23. package/dist/templates/shared-hooks/session-start.py +17 -9
  24. package/dist/templates/trellis/index.d.ts +8 -4
  25. package/dist/templates/trellis/index.d.ts.map +1 -1
  26. package/dist/templates/trellis/index.js +13 -7
  27. package/dist/templates/trellis/index.js.map +1 -1
  28. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +80 -31
  29. package/dist/templates/trellis/scripts/common/config.py +1 -1
  30. package/dist/templates/trellis/scripts/common/cursor_retrieval_env.py +92 -0
  31. package/dist/templates/trellis/scripts/common/parent_orchestration.py +27 -15
  32. package/dist/templates/trellis/scripts/common/retrieval_adapter_metadata.py +100 -9
  33. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +76 -31
  34. package/dist/templates/trellis/scripts/common/retrieval_tool_classification.py +18 -3
  35. package/dist/templates/trellis/scripts/common/semantic_plan_gate.py +19 -0
  36. package/dist/templates/trellis/scripts/common/smart_search_evidence.py +5 -2
  37. package/dist/templates/trellis/scripts/common/smart_search_resolve.py +28 -3
  38. package/dist/templates/trellis/scripts/common/task_gates.py +372 -73
  39. package/dist/templates/trellis/scripts/common/task_map.py +29 -1
  40. package/dist/templates/trellis/scripts/cursor_retrieval_probe.py +396 -0
  41. package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +300 -0
  42. package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +126 -0
  43. package/dist/templates/trellis/workflow.md +9 -53
  44. package/dist/utils/codebase-retrieval-router.d.ts +5 -0
  45. package/dist/utils/codebase-retrieval-router.d.ts.map +1 -1
  46. package/dist/utils/codebase-retrieval-router.js +48 -28
  47. package/dist/utils/codebase-retrieval-router.js.map +1 -1
  48. package/dist/utils/cursor-retrieval-env.d.ts +28 -0
  49. package/dist/utils/cursor-retrieval-env.d.ts.map +1 -0
  50. package/dist/utils/cursor-retrieval-env.js +89 -0
  51. package/dist/utils/cursor-retrieval-env.js.map +1 -0
  52. package/dist/utils/project-capabilities.d.ts.map +1 -1
  53. package/dist/utils/project-capabilities.js +22 -15
  54. package/dist/utils/project-capabilities.js.map +1 -1
  55. package/dist/utils/retrieval-agent-instructions.d.ts.map +1 -1
  56. package/dist/utils/retrieval-agent-instructions.js +37 -21
  57. package/dist/utils/retrieval-agent-instructions.js.map +1 -1
  58. package/dist/utils/retrieval-tool-classification.d.ts +2 -0
  59. package/dist/utils/retrieval-tool-classification.d.ts.map +1 -1
  60. package/dist/utils/retrieval-tool-classification.js +10 -2
  61. package/dist/utils/retrieval-tool-classification.js.map +1 -1
  62. package/dist/utils/semantic-plan-gate.d.ts +8 -0
  63. package/dist/utils/semantic-plan-gate.d.ts.map +1 -0
  64. package/dist/utils/semantic-plan-gate.js +42 -0
  65. package/dist/utils/semantic-plan-gate.js.map +1 -0
  66. package/package.json +5 -43
  67. package/bin/smart-search.js +0 -79
  68. package/dist/templates/trellis/scripts/aggregate_retrieval_telemetry.py +0 -342
  69. package/dist/templates/trellis/scripts/batch_plan_envelope.py +0 -105
  70. package/vendor/smart-search/LICENSE +0 -21
  71. package/vendor/smart-search/README.md +0 -412
  72. package/vendor/smart-search/README.zh-CN.md +0 -430
  73. package/vendor/smart-search/npm/bin/smart-search.js +0 -63
  74. package/vendor/smart-search/npm/scripts/postinstall.js +0 -87
  75. package/vendor/smart-search/npm/scripts/resolve-prerelease-version.js +0 -108
  76. package/vendor/smart-search/npm/scripts/set-package-version.js +0 -35
  77. package/vendor/smart-search/npm/scripts/sync-python-version.js +0 -22
  78. package/vendor/smart-search/npm/scripts/test-wrapper-repair.js +0 -99
  79. package/vendor/smart-search/npm/scripts/test.js +0 -85
  80. package/vendor/smart-search/package.json +0 -42
  81. package/vendor/smart-search/pyproject.toml +0 -37
  82. package/vendor/smart-search/skills/smart-search-cli/SKILL.md +0 -387
  83. package/vendor/smart-search/skills/smart-search-cli/agents/openai.yaml +0 -3
  84. package/vendor/smart-search/skills/smart-search-cli/examples/batch-search.md +0 -98
  85. package/vendor/smart-search/skills/smart-search-cli/examples/evidence-gathering.md +0 -89
  86. package/vendor/smart-search/skills/smart-search-cli/references/cli-contract.md +0 -337
  87. package/vendor/smart-search/src/smart_search/__init__.py +0 -1
  88. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/SKILL.md +0 -387
  89. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/agents/openai.yaml +0 -3
  90. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/examples/batch-search.md +0 -98
  91. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/examples/evidence-gathering.md +0 -89
  92. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/references/cli-contract.md +0 -337
  93. package/vendor/smart-search/src/smart_search/cli.py +0 -2003
  94. package/vendor/smart-search/src/smart_search/config.py +0 -580
  95. package/vendor/smart-search/src/smart_search/logger.py +0 -43
  96. package/vendor/smart-search/src/smart_search/providers/__init__.py +0 -16
  97. package/vendor/smart-search/src/smart_search/providers/base.py +0 -41
  98. package/vendor/smart-search/src/smart_search/providers/context7.py +0 -141
  99. package/vendor/smart-search/src/smart_search/providers/exa.py +0 -206
  100. package/vendor/smart-search/src/smart_search/providers/jina.py +0 -136
  101. package/vendor/smart-search/src/smart_search/providers/openai_compatible.py +0 -370
  102. package/vendor/smart-search/src/smart_search/providers/zhipu.py +0 -143
  103. package/vendor/smart-search/src/smart_search/service.py +0 -2865
  104. package/vendor/smart-search/src/smart_search/sources.py +0 -429
  105. package/vendor/smart-search/src/smart_search/utils.py +0 -220
@@ -78,30 +78,37 @@ MAX_REASON = 500
78
78
  REVIEWER_RE = re.compile(r"^[A-Za-z0-9_.:@/-]+$")
79
79
  VALIDATION_EVIDENCE_RE = re.compile(
80
80
  r"(?im)^\s*(?:[-*]\s*)?validation"
81
- r"(?:\s+(?:commands?|results?|evidence))?\s*:\s*\S"
81
+ r"(?:\s+(?:commands?|results?|evidence))?\s*:\s*(\S[^\r\n]*)$"
82
82
  )
83
83
  ACCEPTANCE_EVIDENCE_RE = re.compile(
84
84
  r"(?im)^\s*(?:[-*]\s*)?(?:final\s+|user\s+)?acceptance"
85
- r"(?:\s+evidence)?\s*:\s*\S"
85
+ r"(?:\s+evidence)?\s*:\s*(\S[^\r\n]*)$"
86
86
  )
87
87
  ACCEPTED_BY_USER_RE = re.compile(
88
- r"(?im)^\s*(?:[-*]\s*)?accepted\s+by\s+user\s*:\s*\S"
88
+ r"(?im)^\s*(?:[-*]\s*)?accepted\s+by\s+user\s*:\s*(\S[^\r\n]*)$"
89
89
  )
90
90
  NO_DURABLE_LEARNING_RE = re.compile(r"(?i)\bno\s+durable\s+learning\b")
91
91
  DURABLE_LEARNING_EVIDENCE_RE = re.compile(
92
- r"(?im)^\s*(?:[-*]\s*)?(?:durable\s+learning|learning\s+decision|"
92
+ r"(?im)^\s*(?:[-*]\s*)?(?:durable\s+learning(?:\s+decision)?|learning\s+decision|"
93
93
  r"spec\s+updates?|spec\s+update\s+(?:needed|evidence)|updated\s+spec|"
94
- r"retrospective(?:\.md)?|learning\s+artifact)\s*:\s*\S"
94
+ r"retrospective(?:\.md)?|learning\s+artifact)\s*:\s*(\S[^\r\n]*)$"
95
95
  )
96
96
  INTEGRATION_EVIDENCE_RE = re.compile(
97
97
  r"(?im)^\s*(?:[-*]\s*)?(?:final\s+)?integration"
98
- r"(?:\s+evidence)?\s*:\s*\S"
98
+ r"(?:\s+evidence)?\s*:\s*(\S[^\r\n]*)$"
99
99
  )
100
100
  REVIEWED_CHANGE_SET_RE = re.compile(
101
101
  r"(?im)^\s*(?:[-*]\s*)?(?:reviewed\s+)?"
102
102
  r"(?:change[- ]set|changeset|diff|git\s+diff|ref|git\s+ref)"
103
103
  r"(?:\s+(?:identity|evidence|summary|ref))?\s*:\s*(\S[^\r\n]*)$"
104
104
  )
105
+ CHECK_EVIDENCE_RE = re.compile(
106
+ r"(?im)^\s*(?:[-*]\s*)?(?:check\s+evidence|trellis-check(?:\s+evidence)?)"
107
+ r"\s*:\s*(\S[^\r\n]*)$"
108
+ )
109
+ PLACEHOLDER_VALUES_RE = re.compile(
110
+ r"(?i)^(TBD|TODO|待定|待补充|N/?A|NA|NONE|-|\.\.\.)$"
111
+ )
105
112
  PRD_ACCEPTANCE_CRITERIA_HEADING_RE = re.compile(
106
113
  r"(?im)^\s*#{1,6}\s*acceptance\s+criteria\s*$"
107
114
  )
@@ -152,6 +159,7 @@ class GateGuardResult:
152
159
  required_gates: list[str] = field(default_factory=list)
153
160
  baseline_record: dict | None = None
154
161
  is_full_task: bool = False
162
+ closeout_profile: str = "lite"
155
163
  auto_gate_records: dict[str, dict] = field(default_factory=dict)
156
164
 
157
165
 
@@ -167,9 +175,13 @@ def normalize_result(result: str) -> str:
167
175
  return result.strip().upper()
168
176
 
169
177
 
170
- def is_full_task(task_dir: Path, task_data: dict | None = None) -> bool:
171
- """Return True when a task should satisfy Full Task gates."""
178
+ def task_closeout_profile(task_dir: Path, task_data: dict | None = None) -> str:
179
+ """Return lite, full, or parent closeout profile."""
172
180
  data = task_data or {}
181
+ child_names = data.get("children")
182
+ if isinstance(child_names, list) and any(isinstance(name, str) for name in child_names):
183
+ return "parent"
184
+
173
185
  meta = data.get("meta") if isinstance(data.get("meta"), dict) else {}
174
186
  candidates = [
175
187
  data.get("task_kind"),
@@ -182,16 +194,29 @@ def is_full_task(task_dir: Path, task_data: dict | None = None) -> bool:
182
194
  meta.get("mode"),
183
195
  ]
184
196
 
197
+ explicit_lite = False
185
198
  for value in candidates:
186
199
  if not isinstance(value, str):
187
200
  continue
188
201
  normalized = value.lower().replace("_", "-")
202
+ if "parent" in normalized:
203
+ return "parent"
189
204
  if "full" in normalized:
190
- return True
205
+ return "full"
191
206
  if "lite" in normalized:
192
- return False
207
+ explicit_lite = True
208
+
209
+ if explicit_lite:
210
+ return "lite"
193
211
 
194
- return (task_dir / "design.md").is_file() and (task_dir / "implement.md").is_file()
212
+ if (task_dir / "design.md").is_file() and (task_dir / "implement.md").is_file():
213
+ return "full"
214
+ return "lite"
215
+
216
+
217
+ def is_full_task(task_dir: Path, task_data: dict | None = None) -> bool:
218
+ """Return True when a task should satisfy Full Task gates."""
219
+ return task_closeout_profile(task_dir, task_data) == "full"
195
220
 
196
221
 
197
222
  def read_strategy_contract(task_dir: Path) -> tuple[dict, list[str]]:
@@ -486,6 +511,19 @@ def build_reviewer_gate_record(
486
511
  if errors:
487
512
  return None, errors, warnings
488
513
 
514
+ if normalized_result in ("PASS", "SKIPPED"):
515
+ errors.extend(
516
+ validate_transition_readiness(
517
+ task_dir,
518
+ task_data,
519
+ transition,
520
+ gate=gate,
521
+ mode="record",
522
+ )
523
+ )
524
+ if errors:
525
+ return None, errors, warnings
526
+
489
527
  contract, contract_errors = read_strategy_contract(task_dir) if is_full_task(task_dir, task_data) else ({}, [])
490
528
  if contract_errors:
491
529
  errors.extend(contract_errors)
@@ -689,6 +727,197 @@ def validate_start_execution_check(
689
727
  return result
690
728
 
691
729
 
730
+ def validate_transition_readiness(
731
+ task_dir: Path,
732
+ task_data: dict | None,
733
+ transition: str,
734
+ *,
735
+ gate: str | None = None,
736
+ mode: str = "record",
737
+ ) -> list[str]:
738
+ """Validate transition evidence and, in complete mode, required gate records."""
739
+ if task_data is None:
740
+ return ["task.json"]
741
+
742
+ errors: list[str] = []
743
+ if transition not in KNOWN_TRANSITIONS:
744
+ errors.append(f"unknown transition: {transition}")
745
+ return errors
746
+
747
+ profile = task_closeout_profile(task_dir, task_data)
748
+
749
+ if mode == "record":
750
+ if not gate:
751
+ errors.append("gate is required for record readiness checks")
752
+ return errors
753
+ required_signals = _evidence_requirements_for_gate(transition, gate)
754
+ errors.extend(_evidence_errors_for_signals(task_dir, task_data, required_signals))
755
+ return errors
756
+
757
+ if transition == "child-review":
758
+ if profile != "full":
759
+ return errors
760
+ contract, contract_errors = read_strategy_contract(task_dir)
761
+ errors.extend(contract_errors)
762
+ if contract_errors:
763
+ return errors
764
+ return errors + _complete_transition_gate_errors(
765
+ task_dir,
766
+ task_data,
767
+ transition,
768
+ contract,
769
+ required_gates_for_transition(transition, contract),
770
+ )
771
+
772
+ if transition == "full-task-complete":
773
+ if profile != "full":
774
+ return errors
775
+ contract, contract_errors = read_strategy_contract(task_dir)
776
+ errors.extend(contract_errors)
777
+ if contract_errors:
778
+ return errors
779
+ return errors + _complete_transition_gate_errors(
780
+ task_dir,
781
+ task_data,
782
+ transition,
783
+ contract,
784
+ required_gates_for_transition(transition, contract),
785
+ )
786
+
787
+ if transition == "parent-integrated":
788
+ if profile != "parent":
789
+ return errors
790
+ child_names = task_data.get("children")
791
+ if isinstance(child_names, list):
792
+ errors.extend(
793
+ validate_parent_children_complete(
794
+ task_dir,
795
+ [name for name in child_names if isinstance(name, str)],
796
+ )
797
+ )
798
+ errors.extend(
799
+ _evidence_errors_for_signals(task_dir, task_data, ["integration"])
800
+ )
801
+ contract: dict = {}
802
+ if (task_dir / "implement.md").is_file():
803
+ contract, contract_errors = read_strategy_contract(task_dir)
804
+ errors.extend(contract_errors)
805
+ if contract_errors:
806
+ return errors
807
+ return errors + _complete_transition_gate_errors(
808
+ task_dir,
809
+ task_data,
810
+ transition,
811
+ contract,
812
+ required_gates_for_transition(transition, contract),
813
+ )
814
+
815
+ return errors
816
+
817
+
818
+ def _complete_transition_gate_errors(
819
+ task_dir: Path,
820
+ task_data: dict,
821
+ transition: str,
822
+ contract: dict,
823
+ required_gates: list[str],
824
+ ) -> list[str]:
825
+ errors: list[str] = []
826
+ contract_fingerprint = compute_contract_fingerprint(task_dir, task_data, contract)
827
+ for gate_name in required_gates:
828
+ artifact_fingerprint = compute_artifact_fingerprint(
829
+ task_dir, task_data, transition, gate_name
830
+ )
831
+ errors.extend(
832
+ _validate_gate_record_for_transition(
833
+ task_data=task_data,
834
+ transition=transition,
835
+ gate=gate_name,
836
+ contract_fingerprint=contract_fingerprint,
837
+ artifact_fingerprint=artifact_fingerprint,
838
+ )
839
+ )
840
+ errors.extend(
841
+ _evidence_errors_for_signals(
842
+ task_dir,
843
+ task_data,
844
+ _evidence_requirements_for_gate(transition, gate_name),
845
+ )
846
+ )
847
+ return errors
848
+
849
+
850
+ def _evidence_requirements_for_gate(transition: str, gate: str) -> list[str]:
851
+ if gate == "code-review" and transition in ("full-task-complete", "child-review"):
852
+ return ["validation", "check_evidence", "reviewed_change_set"]
853
+ if gate == "integration-review" and transition == "parent-integrated":
854
+ return ["integration"]
855
+ if gate in ("architecture-review", "architecture-deep-review") and transition in (
856
+ "full-task-complete",
857
+ "child-review",
858
+ ):
859
+ return ["validation", "check_evidence"]
860
+ return []
861
+
862
+
863
+ def _evidence_errors_for_signals(
864
+ task_dir: Path,
865
+ task_data: dict,
866
+ required_signals: list[str],
867
+ ) -> list[str]:
868
+ status = verify_evidence_status(task_dir, task_data)
869
+ label_map = {
870
+ "validation": "verify.md missing substantive validation evidence",
871
+ "check_evidence": "verify.md missing check evidence",
872
+ "acceptance": "verify.md missing final acceptance evidence",
873
+ "durable_learning": "verify.md missing durable-learning decision evidence",
874
+ "integration": "verify.md missing final integration evidence",
875
+ "reviewed_change_set": (
876
+ "verify.md or handoff.md missing reviewed change-set evidence"
877
+ ),
878
+ }
879
+ errors: list[str] = []
880
+ for signal in required_signals:
881
+ if not status.get(signal):
882
+ errors.append(label_map.get(signal, f"missing evidence signal: {signal}"))
883
+ return errors
884
+
885
+
886
+ def verify_evidence_status(task_dir: Path, task_data: dict) -> dict[str, bool]:
887
+ """Return substantive evidence signals from verify.md and handoff.md."""
888
+ verify_path = task_dir / "verify.md"
889
+ content = ""
890
+ if verify_path.is_file():
891
+ try:
892
+ content = verify_path.read_text(encoding="utf-8")
893
+ except OSError:
894
+ content = ""
895
+
896
+ handoff_path = task_dir / "handoff.md"
897
+ handoff_content = ""
898
+ if handoff_path.is_file():
899
+ try:
900
+ handoff_content = handoff_path.read_text(encoding="utf-8")
901
+ except OSError:
902
+ handoff_content = ""
903
+
904
+ child_names = task_data.get("children")
905
+ has_children = isinstance(child_names, list) and bool(child_names)
906
+ return {
907
+ "validation": _has_substantive_validation_evidence(content),
908
+ "check_evidence": _has_substantive_check_evidence(content),
909
+ "acceptance": _has_substantive_acceptance_evidence(content),
910
+ "durable_learning": _has_durable_learning_evidence(content),
911
+ "integration": (
912
+ _has_substantive_integration_evidence(content) if has_children else True
913
+ ),
914
+ "reviewed_change_set": (
915
+ _has_substantive_reviewed_change_set(content)
916
+ or _has_substantive_reviewed_change_set(handoff_content)
917
+ ),
918
+ }
919
+
920
+
692
921
  def validate_archive(
693
922
  task_dir: Path,
694
923
  task_data: dict | None,
@@ -720,23 +949,41 @@ def validate_archive(
720
949
  )
721
950
  )
722
951
 
723
- full_task = is_full_task(task_dir, task_data)
952
+ profile = task_closeout_profile(task_dir, task_data)
724
953
  contract: dict = {}
725
954
  required_gates: list[str] = []
726
955
  artifact_fingerprints: dict[str, str] = {}
727
956
  baseline_record = None
728
957
 
729
- if full_task:
958
+ if profile == "full":
730
959
  contract, contract_errors = read_strategy_contract(task_dir)
731
960
  errors.extend(contract_errors)
732
961
  if not contract_errors:
733
962
  required_gates = required_gates_for_transition(
734
963
  "full-task-complete", contract
735
964
  )
965
+ errors.extend(
966
+ validate_transition_readiness(
967
+ task_dir,
968
+ task_data,
969
+ "full-task-complete",
970
+ mode="complete",
971
+ )
972
+ )
973
+ elif profile == "parent":
974
+ errors.extend(
975
+ validate_transition_readiness(
976
+ task_dir,
977
+ task_data,
978
+ "parent-integrated",
979
+ mode="complete",
980
+ )
981
+ )
982
+ required_gates = required_gates_for_transition("parent-integrated", contract)
736
983
 
737
984
  contract_fingerprint = compute_contract_fingerprint(task_dir, task_data, contract)
738
985
 
739
- if full_task:
986
+ if profile == "full" and not errors:
740
987
  artifact_fingerprints[BASELINE_GATE] = compute_artifact_fingerprint(
741
988
  task_dir, task_data, "full-task-complete", BASELINE_GATE
742
989
  )
@@ -744,19 +991,9 @@ def validate_archive(
744
991
  artifact_fingerprints[gate] = compute_artifact_fingerprint(
745
992
  task_dir, task_data, "full-task-complete", gate
746
993
  )
747
- errors.extend(
748
- _validate_gate_record_for_transition(
749
- task_data=task_data,
750
- transition="full-task-complete",
751
- gate=gate,
752
- contract_fingerprint=contract_fingerprint,
753
- artifact_fingerprint=artifact_fingerprints[gate],
754
- )
755
- )
756
- if not errors:
757
- baseline_record = make_baseline_record(
758
- task_dir, task_data, "full-task-complete", contract
759
- )
994
+ baseline_record = make_baseline_record(
995
+ task_dir, task_data, "full-task-complete", contract
996
+ )
760
997
 
761
998
  return GateGuardResult(
762
999
  ok=not errors,
@@ -765,7 +1002,8 @@ def validate_archive(
765
1002
  artifact_fingerprints=artifact_fingerprints,
766
1003
  required_gates=required_gates,
767
1004
  baseline_record=baseline_record,
768
- is_full_task=full_task,
1005
+ is_full_task=profile == "full",
1006
+ closeout_profile=profile,
769
1007
  )
770
1008
 
771
1009
 
@@ -776,41 +1014,89 @@ def _verify_evidence_errors(task_dir: Path, task_data: dict) -> list[str]:
776
1014
  return []
777
1015
 
778
1016
  try:
779
- content = verify_path.read_text(encoding="utf-8")
1017
+ verify_path.read_text(encoding="utf-8")
780
1018
  except OSError:
781
1019
  return ["verify.md could not be read for archive evidence"]
782
1020
 
783
1021
  errors: list[str] = []
784
- if not _has_validation_evidence(content):
1022
+ status = verify_evidence_status(task_dir, task_data)
1023
+ if not status["validation"]:
785
1024
  errors.append("verify.md missing validation evidence")
786
- if not _has_final_acceptance_evidence(content):
1025
+ if not status["acceptance"]:
787
1026
  errors.append("verify.md missing final acceptance evidence")
788
- if not _has_durable_learning_evidence(content):
1027
+ if not status["durable_learning"]:
789
1028
  errors.append("verify.md missing durable-learning decision evidence")
790
-
791
- child_names = task_data.get("children")
792
- if isinstance(child_names, list) and child_names:
793
- if not INTEGRATION_EVIDENCE_RE.search(content):
794
- errors.append("verify.md missing final integration evidence")
1029
+ if not status["integration"]:
1030
+ errors.append("verify.md missing final integration evidence")
795
1031
 
796
1032
  return errors
797
1033
 
798
1034
 
1035
+ def _is_substantive_evidence(value: str | None) -> bool:
1036
+ if not value or not isinstance(value, str):
1037
+ return False
1038
+ stripped = value.strip()
1039
+ if len(stripped) < 3:
1040
+ return False
1041
+ return not PLACEHOLDER_VALUES_RE.match(stripped)
1042
+
1043
+
1044
+ def _has_substantive_line_match(content: str, pattern: re.Pattern[str]) -> bool:
1045
+ if not content:
1046
+ return False
1047
+ for match in pattern.finditer(content):
1048
+ if match.lastindex and match.lastindex >= 1:
1049
+ if _is_substantive_evidence(match.group(1)):
1050
+ return True
1051
+ continue
1052
+ line = match.group(0)
1053
+ if ":" in line:
1054
+ value = line.split(":", 1)[1].strip()
1055
+ if _is_substantive_evidence(value):
1056
+ return True
1057
+ return False
1058
+
1059
+
1060
+ def _has_substantive_validation_evidence(content: str) -> bool:
1061
+ return _has_substantive_line_match(content, VALIDATION_EVIDENCE_RE)
1062
+
1063
+
1064
+ def _has_substantive_check_evidence(content: str) -> bool:
1065
+ return _has_substantive_line_match(content, CHECK_EVIDENCE_RE)
1066
+
1067
+
1068
+ def _has_substantive_acceptance_evidence(content: str) -> bool:
1069
+ return (
1070
+ _has_substantive_line_match(content, ACCEPTANCE_EVIDENCE_RE)
1071
+ or _has_substantive_line_match(content, ACCEPTED_BY_USER_RE)
1072
+ )
1073
+
1074
+
1075
+ def _has_substantive_integration_evidence(content: str) -> bool:
1076
+ return _has_substantive_line_match(content, INTEGRATION_EVIDENCE_RE)
1077
+
1078
+
1079
+ def _has_substantive_reviewed_change_set(content: str) -> bool:
1080
+ if not content:
1081
+ return False
1082
+ for match in REVIEWED_CHANGE_SET_RE.finditer(content):
1083
+ if _is_substantive_evidence(match.group(1)):
1084
+ return True
1085
+ return False
1086
+
1087
+
799
1088
  def _has_validation_evidence(content: str) -> bool:
800
- return bool(VALIDATION_EVIDENCE_RE.search(content))
1089
+ return _has_substantive_validation_evidence(content)
801
1090
 
802
1091
 
803
1092
  def _has_final_acceptance_evidence(content: str) -> bool:
804
- return bool(
805
- ACCEPTANCE_EVIDENCE_RE.search(content)
806
- or ACCEPTED_BY_USER_RE.search(content)
807
- )
1093
+ return _has_substantive_acceptance_evidence(content)
808
1094
 
809
1095
 
810
1096
  def _has_durable_learning_evidence(content: str) -> bool:
811
1097
  return bool(
812
1098
  NO_DURABLE_LEARNING_RE.search(content)
813
- or DURABLE_LEARNING_EVIDENCE_RE.search(content)
1099
+ or _has_substantive_line_match(content, DURABLE_LEARNING_EVIDENCE_RE)
814
1100
  )
815
1101
 
816
1102
 
@@ -942,33 +1228,12 @@ ARCHIVE_EVIDENCE_DRAFT_MARKER = "<!-- trellis:archive-evidence-draft -->"
942
1228
 
943
1229
  def _verify_evidence_status(task_dir: Path, task_data: dict) -> dict[str, bool]:
944
1230
  """Return which archive evidence sections are present in verify.md."""
945
- verify_path = task_dir / "verify.md"
946
- if not verify_path.is_file():
947
- return {
948
- "validation": False,
949
- "acceptance": False,
950
- "durable_learning": False,
951
- "integration": False,
952
- }
953
- try:
954
- content = verify_path.read_text(encoding="utf-8")
955
- except OSError:
956
- return {
957
- "validation": False,
958
- "acceptance": False,
959
- "durable_learning": False,
960
- "integration": False,
961
- }
962
-
963
- child_names = task_data.get("children")
964
- has_children = isinstance(child_names, list) and bool(child_names)
1231
+ status = verify_evidence_status(task_dir, task_data)
965
1232
  return {
966
- "validation": _has_validation_evidence(content),
967
- "acceptance": _has_final_acceptance_evidence(content),
968
- "durable_learning": _has_durable_learning_evidence(content),
969
- "integration": (
970
- bool(INTEGRATION_EVIDENCE_RE.search(content)) if has_children else True
971
- ),
1233
+ "validation": status["validation"],
1234
+ "acceptance": status["acceptance"],
1235
+ "durable_learning": status["durable_learning"],
1236
+ "integration": status["integration"],
972
1237
  }
973
1238
 
974
1239
 
@@ -1022,11 +1287,45 @@ def archive_repair_hints(
1022
1287
  if error.startswith("missing gate record:"):
1023
1288
  _rest = error.split(":", 1)[1].strip()
1024
1289
  _transition, gate = _rest.split("/", 1)
1290
+ transition = _transition.strip()
1291
+ if transition == "parent-integrated":
1292
+ hints.append(
1293
+ f"Parent archive needs record-gate for parent-integrated/{gate}: "
1294
+ f"python ./.trellis/scripts/task.py record-gate {rel_task} "
1295
+ f"--transition parent-integrated --gate {gate} --result PASS "
1296
+ f"--reviewer parent --evidence task-map.md"
1297
+ )
1298
+ elif transition == "child-review":
1299
+ hints.append(
1300
+ f"Full Child acceptance needs record-gate for child-review/{gate}: "
1301
+ f"python ./.trellis/scripts/task.py record-gate {rel_task} "
1302
+ f"--transition child-review --gate {gate} --result PASS "
1303
+ f"--reviewer parent --evidence verify.md"
1304
+ )
1305
+ else:
1306
+ hints.append(
1307
+ f"Record reviewer gate after explicit review (never auto-PASS): "
1308
+ f"python ./.trellis/scripts/task.py record-gate {rel_task} "
1309
+ f"--transition {transition} --gate {gate} --result PASS "
1310
+ f"--reviewer <reviewer-id> --evidence verify.md"
1311
+ )
1312
+ continue
1313
+ if error == "verify.md missing check evidence":
1025
1314
  hints.append(
1026
- f"Record reviewer gate after explicit review (never auto-PASS): "
1027
- f"python ./.trellis/scripts/task.py record-gate {rel_task} "
1028
- f"--transition full-task-complete --gate {gate} --result PASS "
1029
- f"--reviewer <reviewer-id> --evidence verify.md"
1315
+ "Add a grep-friendly line such as "
1316
+ "'Check evidence: <trellis-check summary or manual review notes>' to verify.md"
1317
+ )
1318
+ continue
1319
+ if error == (
1320
+ "verify.md or handoff.md missing reviewed change-set evidence"
1321
+ ):
1322
+ hints.append(
1323
+ "Add 'Reviewed change-set: <git ref or diff summary>' to verify.md or handoff.md"
1324
+ )
1325
+ continue
1326
+ if error == "verify.md missing substantive validation evidence":
1327
+ hints.append(
1328
+ "Replace placeholder validation lines with substantive command/outcome text in verify.md"
1030
1329
  )
1031
1330
  continue
1032
1331
  if error.startswith("gate failed:"):
@@ -177,6 +177,7 @@ def validate_parent_child_integration(
177
177
  evidence: str,
178
178
  ref: str | None = None,
179
179
  reason: str | None = None,
180
+ current_state_override: str | None = None,
180
181
  ) -> list[str]:
181
182
  """Validate a Parent-controlled Child integration state transition."""
182
183
  errors: list[str] = []
@@ -209,7 +210,11 @@ def validate_parent_child_integration(
209
210
  errors.append(f"child missing from task-map.md: {child_name}")
210
211
  return errors
211
212
 
212
- current_state = child.get("state")
213
+ current_state = (
214
+ current_state_override
215
+ if current_state_override is not None
216
+ else child.get("state")
217
+ )
213
218
  if state in {"accepted", "changes"} and current_state != "review":
214
219
  errors.append(f"{state} requires current Child state 'review', got {current_state!r}")
215
220
  if state == "integrating" and current_state != "accepted":
@@ -237,6 +242,29 @@ def validate_parent_child_integration(
237
242
  f"merge_limit {merge_limit} blocks integrating {child_name}; already integrating: {', '.join(str(item) for item in integrating)}"
238
243
  )
239
244
 
245
+ if state == "accepted":
246
+ from .task_gates import task_closeout_profile, validate_transition_readiness
247
+
248
+ profile = task_closeout_profile(child_dir, child_data)
249
+ if profile == "full":
250
+ errors.extend(
251
+ validate_transition_readiness(
252
+ child_dir,
253
+ child_data,
254
+ "child-review",
255
+ mode="complete",
256
+ )
257
+ )
258
+ elif profile == "parent":
259
+ errors.extend(
260
+ validate_transition_readiness(
261
+ child_dir,
262
+ child_data,
263
+ "parent-integrated",
264
+ mode="complete",
265
+ )
266
+ )
267
+
240
268
  return errors
241
269
 
242
270