@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.
- package/CHANGELOG.md +58 -0
- package/README.md +45 -21
- package/dist/configurators/workflow.d.ts +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +20 -9
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/manifests/0.1.3.json +9 -0
- package/dist/migrations/manifests/0.1.4.json +9 -0
- package/dist/templates/cursor/rules/retrieval-routing.mdc +9 -7
- package/dist/templates/markdown/agents.md +1 -1
- package/dist/templates/markdown/index.d.ts +1 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +1 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/prompts/run-semantic-slice-12.md.txt +13 -29
- package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +0 -49
- package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +0 -85
- package/dist/templates/markdown/spec/guides/cursor-semantic-compliance.md.txt +7 -21
- package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +6 -10
- package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +17 -18
- package/dist/templates/shared-hooks/inject-retrieval-plan.py +42 -23
- package/dist/templates/shared-hooks/inject-subagent-context.py +6 -2
- package/dist/templates/shared-hooks/session-start.py +17 -9
- package/dist/templates/trellis/index.d.ts +8 -4
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +13 -7
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +80 -31
- package/dist/templates/trellis/scripts/common/config.py +1 -1
- package/dist/templates/trellis/scripts/common/cursor_retrieval_env.py +92 -0
- package/dist/templates/trellis/scripts/common/parent_orchestration.py +27 -15
- package/dist/templates/trellis/scripts/common/retrieval_adapter_metadata.py +100 -9
- package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +76 -31
- package/dist/templates/trellis/scripts/common/retrieval_tool_classification.py +18 -3
- package/dist/templates/trellis/scripts/common/semantic_plan_gate.py +19 -0
- package/dist/templates/trellis/scripts/common/smart_search_evidence.py +5 -2
- package/dist/templates/trellis/scripts/common/smart_search_resolve.py +28 -3
- package/dist/templates/trellis/scripts/common/task_gates.py +372 -73
- package/dist/templates/trellis/scripts/common/task_map.py +29 -1
- package/dist/templates/trellis/scripts/cursor_retrieval_probe.py +396 -0
- package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +300 -0
- package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +126 -0
- package/dist/templates/trellis/workflow.md +9 -53
- package/dist/utils/codebase-retrieval-router.d.ts +5 -0
- package/dist/utils/codebase-retrieval-router.d.ts.map +1 -1
- package/dist/utils/codebase-retrieval-router.js +48 -28
- package/dist/utils/codebase-retrieval-router.js.map +1 -1
- package/dist/utils/cursor-retrieval-env.d.ts +28 -0
- package/dist/utils/cursor-retrieval-env.d.ts.map +1 -0
- package/dist/utils/cursor-retrieval-env.js +89 -0
- package/dist/utils/cursor-retrieval-env.js.map +1 -0
- package/dist/utils/project-capabilities.d.ts.map +1 -1
- package/dist/utils/project-capabilities.js +22 -15
- package/dist/utils/project-capabilities.js.map +1 -1
- package/dist/utils/retrieval-agent-instructions.d.ts.map +1 -1
- package/dist/utils/retrieval-agent-instructions.js +37 -21
- package/dist/utils/retrieval-agent-instructions.js.map +1 -1
- package/dist/utils/retrieval-tool-classification.d.ts +2 -0
- package/dist/utils/retrieval-tool-classification.d.ts.map +1 -1
- package/dist/utils/retrieval-tool-classification.js +10 -2
- package/dist/utils/retrieval-tool-classification.js.map +1 -1
- package/dist/utils/semantic-plan-gate.d.ts +8 -0
- package/dist/utils/semantic-plan-gate.d.ts.map +1 -0
- package/dist/utils/semantic-plan-gate.js +42 -0
- package/dist/utils/semantic-plan-gate.js.map +1 -0
- package/package.json +5 -43
- package/bin/smart-search.js +0 -79
- package/dist/templates/trellis/scripts/aggregate_retrieval_telemetry.py +0 -342
- package/dist/templates/trellis/scripts/batch_plan_envelope.py +0 -105
- package/vendor/smart-search/LICENSE +0 -21
- package/vendor/smart-search/README.md +0 -412
- package/vendor/smart-search/README.zh-CN.md +0 -430
- package/vendor/smart-search/npm/bin/smart-search.js +0 -63
- package/vendor/smart-search/npm/scripts/postinstall.js +0 -87
- package/vendor/smart-search/npm/scripts/resolve-prerelease-version.js +0 -108
- package/vendor/smart-search/npm/scripts/set-package-version.js +0 -35
- package/vendor/smart-search/npm/scripts/sync-python-version.js +0 -22
- package/vendor/smart-search/npm/scripts/test-wrapper-repair.js +0 -99
- package/vendor/smart-search/npm/scripts/test.js +0 -85
- package/vendor/smart-search/package.json +0 -42
- package/vendor/smart-search/pyproject.toml +0 -37
- package/vendor/smart-search/skills/smart-search-cli/SKILL.md +0 -387
- package/vendor/smart-search/skills/smart-search-cli/agents/openai.yaml +0 -3
- package/vendor/smart-search/skills/smart-search-cli/examples/batch-search.md +0 -98
- package/vendor/smart-search/skills/smart-search-cli/examples/evidence-gathering.md +0 -89
- package/vendor/smart-search/skills/smart-search-cli/references/cli-contract.md +0 -337
- package/vendor/smart-search/src/smart_search/__init__.py +0 -1
- package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/SKILL.md +0 -387
- package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/agents/openai.yaml +0 -3
- package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/examples/batch-search.md +0 -98
- package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/examples/evidence-gathering.md +0 -89
- package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/references/cli-contract.md +0 -337
- package/vendor/smart-search/src/smart_search/cli.py +0 -2003
- package/vendor/smart-search/src/smart_search/config.py +0 -580
- package/vendor/smart-search/src/smart_search/logger.py +0 -43
- package/vendor/smart-search/src/smart_search/providers/__init__.py +0 -16
- package/vendor/smart-search/src/smart_search/providers/base.py +0 -41
- package/vendor/smart-search/src/smart_search/providers/context7.py +0 -141
- package/vendor/smart-search/src/smart_search/providers/exa.py +0 -206
- package/vendor/smart-search/src/smart_search/providers/jina.py +0 -136
- package/vendor/smart-search/src/smart_search/providers/openai_compatible.py +0 -370
- package/vendor/smart-search/src/smart_search/providers/zhipu.py +0 -143
- package/vendor/smart-search/src/smart_search/service.py +0 -2865
- package/vendor/smart-search/src/smart_search/sources.py +0 -429
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
171
|
-
"""Return
|
|
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
|
|
205
|
+
return "full"
|
|
191
206
|
if "lite" in normalized:
|
|
192
|
-
|
|
207
|
+
explicit_lite = True
|
|
208
|
+
|
|
209
|
+
if explicit_lite:
|
|
210
|
+
return "lite"
|
|
193
211
|
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1025
|
+
if not status["acceptance"]:
|
|
787
1026
|
errors.append("verify.md missing final acceptance evidence")
|
|
788
|
-
if not
|
|
1027
|
+
if not status["durable_learning"]:
|
|
789
1028
|
errors.append("verify.md missing durable-learning decision evidence")
|
|
790
|
-
|
|
791
|
-
|
|
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
|
|
1089
|
+
return _has_substantive_validation_evidence(content)
|
|
801
1090
|
|
|
802
1091
|
|
|
803
1092
|
def _has_final_acceptance_evidence(content: str) -> bool:
|
|
804
|
-
return
|
|
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
|
|
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
|
-
|
|
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":
|
|
967
|
-
"acceptance":
|
|
968
|
-
"durable_learning":
|
|
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
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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 =
|
|
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
|
|