@mc-and-his-agents/loom-installer 0.1.11 → 0.1.12

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 (22) hide show
  1. package/package.json +1 -1
  2. package/payload/manifest.json +42 -42
  3. package/payload/plugin/loom/skills/shared/scripts/loom_check.py +63 -12
  4. package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +25 -8
  5. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +63 -12
  6. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +25 -8
  7. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +63 -12
  8. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +25 -8
  9. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +63 -12
  10. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +25 -8
  11. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +63 -12
  12. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +25 -8
  13. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +63 -12
  14. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +25 -8
  15. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +63 -12
  16. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +25 -8
  17. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +63 -12
  18. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +25 -8
  19. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +63 -12
  20. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +25 -8
  21. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +63 -12
  22. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +25 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mc-and-his-agents/loom-installer",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Node installer for Loom plugin and single-skill installation surfaces.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,9 +2,9 @@
2
2
  "schema_version": "loom-installer-payload/v1",
3
3
  "loom_version": "0.4.0",
4
4
  "source_repository": "https://github.com/MC-and-his-Agents/Loom",
5
- "source_commit": "4f6a4074d02557304018e9f063425f974d6a08cf",
5
+ "source_commit": "f11f0f24401eca30d21786c41e39a74be05bf342",
6
6
  "source_ref": "main",
7
- "built_at": "2026-04-25T11:49:46+08:00",
7
+ "built_at": "2026-04-25T11:57:32+08:00",
8
8
  "runtime": {
9
9
  "python_minimum": "3.10",
10
10
  "python_recommended": "3.11+"
@@ -628,13 +628,13 @@
628
628
  },
629
629
  {
630
630
  "path": "plugin/loom/skills/shared/scripts/loom_check.py",
631
- "bytes": 253737,
632
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
631
+ "bytes": 256345,
632
+ "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
633
633
  },
634
634
  {
635
635
  "path": "plugin/loom/skills/shared/scripts/loom_flow.py",
636
- "bytes": 259768,
637
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
636
+ "bytes": 260571,
637
+ "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
638
638
  },
639
639
  {
640
640
  "path": "plugin/loom/skills/shared/scripts/loom_init.py",
@@ -1213,13 +1213,13 @@
1213
1213
  },
1214
1214
  {
1215
1215
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py",
1216
- "bytes": 253737,
1217
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
1216
+ "bytes": 256345,
1217
+ "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
1218
1218
  },
1219
1219
  {
1220
1220
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py",
1221
- "bytes": 259768,
1222
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
1221
+ "bytes": 260571,
1222
+ "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
1223
1223
  },
1224
1224
  {
1225
1225
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_init.py",
@@ -1828,13 +1828,13 @@
1828
1828
  },
1829
1829
  {
1830
1830
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py",
1831
- "bytes": 253737,
1832
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
1831
+ "bytes": 256345,
1832
+ "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
1833
1833
  },
1834
1834
  {
1835
1835
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py",
1836
- "bytes": 259768,
1837
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
1836
+ "bytes": 260571,
1837
+ "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
1838
1838
  },
1839
1839
  {
1840
1840
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_init.py",
@@ -2443,13 +2443,13 @@
2443
2443
  },
2444
2444
  {
2445
2445
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_check.py",
2446
- "bytes": 253737,
2447
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
2446
+ "bytes": 256345,
2447
+ "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
2448
2448
  },
2449
2449
  {
2450
2450
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py",
2451
- "bytes": 259768,
2452
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
2451
+ "bytes": 260571,
2452
+ "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
2453
2453
  },
2454
2454
  {
2455
2455
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_init.py",
@@ -3063,13 +3063,13 @@
3063
3063
  },
3064
3064
  {
3065
3065
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py",
3066
- "bytes": 253737,
3067
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
3066
+ "bytes": 256345,
3067
+ "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
3068
3068
  },
3069
3069
  {
3070
3070
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py",
3071
- "bytes": 259768,
3072
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
3071
+ "bytes": 260571,
3072
+ "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
3073
3073
  },
3074
3074
  {
3075
3075
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_init.py",
@@ -3678,13 +3678,13 @@
3678
3678
  },
3679
3679
  {
3680
3680
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py",
3681
- "bytes": 253737,
3682
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
3681
+ "bytes": 256345,
3682
+ "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
3683
3683
  },
3684
3684
  {
3685
3685
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py",
3686
- "bytes": 259768,
3687
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
3686
+ "bytes": 260571,
3687
+ "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
3688
3688
  },
3689
3689
  {
3690
3690
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -4293,13 +4293,13 @@
4293
4293
  },
4294
4294
  {
4295
4295
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py",
4296
- "bytes": 253737,
4297
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
4296
+ "bytes": 256345,
4297
+ "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
4298
4298
  },
4299
4299
  {
4300
4300
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py",
4301
- "bytes": 259768,
4302
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
4301
+ "bytes": 260571,
4302
+ "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
4303
4303
  },
4304
4304
  {
4305
4305
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_init.py",
@@ -4908,13 +4908,13 @@
4908
4908
  },
4909
4909
  {
4910
4910
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py",
4911
- "bytes": 253737,
4912
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
4911
+ "bytes": 256345,
4912
+ "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
4913
4913
  },
4914
4914
  {
4915
4915
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py",
4916
- "bytes": 259768,
4917
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
4916
+ "bytes": 260571,
4917
+ "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
4918
4918
  },
4919
4919
  {
4920
4920
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_init.py",
@@ -5523,13 +5523,13 @@
5523
5523
  },
5524
5524
  {
5525
5525
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_check.py",
5526
- "bytes": 253737,
5527
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
5526
+ "bytes": 256345,
5527
+ "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
5528
5528
  },
5529
5529
  {
5530
5530
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py",
5531
- "bytes": 259768,
5532
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
5531
+ "bytes": 260571,
5532
+ "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
5533
5533
  },
5534
5534
  {
5535
5535
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -6138,13 +6138,13 @@
6138
6138
  },
6139
6139
  {
6140
6140
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py",
6141
- "bytes": 253737,
6142
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
6141
+ "bytes": 256345,
6142
+ "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
6143
6143
  },
6144
6144
  {
6145
6145
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py",
6146
- "bytes": 259768,
6147
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
6146
+ "bytes": 260571,
6147
+ "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
6148
6148
  },
6149
6149
  {
6150
6150
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -92,6 +92,7 @@ CORE_DOCS = (
92
92
  "docs/methodology/templates/pull-request.md",
93
93
  "docs/evidence/extraction-ledger.md",
94
94
  "docs/evidence/landing-map.md",
95
+ "docs/evidence/validations/validation-closeout-reconciliation-blocking-gate.md",
95
96
  "docs/evidence/validations/validation-loom-core-runtime-parity.md",
96
97
  "docs/evidence/validations/validation-syvert-strong-governance-parity.md",
97
98
  "docs/adoption/rationale.md",
@@ -1118,10 +1119,14 @@ def require_reconciliation_payload(
1118
1119
  if not isinstance(finding, dict):
1119
1120
  failures.append(Failure(category, f"{context} reconciliation findings must be JSON objects"))
1120
1121
  continue
1121
- if finding.get("kind") not in {"absorbed_but_open", "parent_drift", "project_drift"}:
1122
+ if finding.get("category") not in {"drift", "gate_failure"}:
1123
+ failures.append(Failure(category, f"{context} reconciliation finding category must stay within the stable taxonomy"))
1124
+ if finding.get("kind") not in {"merged_but_open", "absorbed_but_open", "parent_drift", "project_drift", "host_signal_drift"}:
1122
1125
  failures.append(Failure(category, f"{context} reconciliation finding kind must stay within the stable contract"))
1123
1126
  if finding.get("severity") not in {"warn", "fix-needed", "block"}:
1124
1127
  failures.append(Failure(category, f"{context} reconciliation finding severity must stay within the stable contract"))
1128
+ if finding.get("fallback_to") not in {"reconciliation-sync", "manual-reconciliation", None}:
1129
+ failures.append(Failure(category, f"{context} reconciliation finding fallback_to must stay within the stable contract"))
1125
1130
  if not isinstance(finding.get("subject"), str) or not finding.get("subject"):
1126
1131
  failures.append(Failure(category, f"{context} reconciliation findings must include non-empty `subject`"))
1127
1132
  if not isinstance(finding.get("evidence"), dict):
@@ -4692,18 +4697,20 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
4692
4697
  "summary": "warn",
4693
4698
  "missing_inputs": [],
4694
4699
  "fallback_to": "manual-reconciliation",
4695
- "findings": [
4696
- {
4697
- "kind": "project_drift",
4698
- "severity": "warn",
4699
- "subject": "project 5",
4700
- "evidence": {},
4701
- "recommended_action": "review warning",
4702
- }
4703
- ],
4700
+ "findings": [
4701
+ {
4702
+ "category": "drift",
4703
+ "kind": "project_drift",
4704
+ "severity": "warn",
4705
+ "subject": "project 5",
4706
+ "evidence": {},
4707
+ "recommended_action": "review warning",
4708
+ "fallback_to": "reconciliation-sync",
4709
+ }
4710
+ ],
4711
+ },
4704
4712
  },
4705
- },
4706
- )
4713
+ )
4707
4714
  if warn_payload_failures:
4708
4715
  failures.append(
4709
4716
  Failure(
@@ -4712,6 +4719,50 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
4712
4719
  )
4713
4720
  )
4714
4721
 
4722
+ valid_reconciliation_samples = [
4723
+ ("merged_but_open", "fix-needed", "reconciliation-sync"),
4724
+ ("absorbed_but_open", "fix-needed", "reconciliation-sync"),
4725
+ ("parent_drift", "block", "manual-reconciliation"),
4726
+ ("host_signal_drift", "block", "manual-reconciliation"),
4727
+ ]
4728
+ for kind, reconciliation_result_value, fallback_to in valid_reconciliation_samples:
4729
+ sample_failures = []
4730
+ require_closeout_reconciliation_contract(
4731
+ sample_failures,
4732
+ category="daily-execution-cli",
4733
+ context=f"`closeout-{kind}`",
4734
+ payload={
4735
+ "result": "block",
4736
+ "fallback_to": fallback_to,
4737
+ "reconciliation": {
4738
+ "command": "reconciliation",
4739
+ "operation": "audit",
4740
+ "result": reconciliation_result_value,
4741
+ "summary": kind,
4742
+ "missing_inputs": [] if kind != "host_signal_drift" else ["github control plane"],
4743
+ "fallback_to": "manual-reconciliation",
4744
+ "findings": [
4745
+ {
4746
+ "category": "drift",
4747
+ "kind": kind,
4748
+ "severity": reconciliation_result_value,
4749
+ "subject": "closeout sample",
4750
+ "evidence": {},
4751
+ "recommended_action": "reconcile closeout sample",
4752
+ "fallback_to": fallback_to,
4753
+ }
4754
+ ],
4755
+ },
4756
+ },
4757
+ )
4758
+ if sample_failures:
4759
+ failures.append(
4760
+ Failure(
4761
+ "daily-execution-cli",
4762
+ f"`closeout-{kind}` synthetic payload must satisfy closeout reconciliation validation",
4763
+ )
4764
+ )
4765
+
4715
4766
  return failures
4716
4767
 
4717
4768
 
@@ -3748,13 +3748,19 @@ def make_reconciliation_finding(
3748
3748
  subject: str,
3749
3749
  evidence: dict[str, Any],
3750
3750
  recommended_action: str,
3751
+ category: str = "drift",
3752
+ fallback_to: str | None = None,
3751
3753
  ) -> dict[str, Any]:
3754
+ if fallback_to is None:
3755
+ fallback_to = "manual-reconciliation" if severity == "block" else "reconciliation-sync"
3752
3756
  return {
3757
+ "category": category,
3753
3758
  "kind": kind,
3754
3759
  "severity": severity,
3755
3760
  "subject": subject,
3756
3761
  "evidence": evidence,
3757
3762
  "recommended_action": recommended_action,
3763
+ "fallback_to": fallback_to,
3758
3764
  }
3759
3765
 
3760
3766
 
@@ -3824,13 +3830,13 @@ def reconciliation_audit_payload(
3824
3830
  merge_commit_sha = oid
3825
3831
  merge_commit_in_main = contains_merged_commit(target_root, merge_commit_sha)
3826
3832
 
3827
- absorbed_issue = False
3833
+ merged_issue_open = False
3828
3834
  if issue_payload is not None and pr_payload is not None:
3829
3835
  if issue_payload.get("state") == "OPEN" and pr_payload.get("state") == "MERGED" and merge_commit_sha and merge_commit_in_main:
3830
- absorbed_issue = True
3836
+ merged_issue_open = True
3831
3837
  findings.append(
3832
3838
  make_reconciliation_finding(
3833
- kind="absorbed_but_open",
3839
+ kind="merged_but_open",
3834
3840
  severity="fix-needed",
3835
3841
  subject=f"issue #{issue_number}",
3836
3842
  evidence={
@@ -3840,7 +3846,7 @@ def reconciliation_audit_payload(
3840
3846
  "merge_commit": merge_commit_sha,
3841
3847
  "merge_commit_in_main": merge_commit_in_main,
3842
3848
  },
3843
- recommended_action="close the absorbed issue or run reconciliation sync after reviewing the evidence.",
3849
+ recommended_action="close the merged issue or run reconciliation sync after reviewing the evidence.",
3844
3850
  )
3845
3851
  )
3846
3852
 
@@ -3866,7 +3872,7 @@ def reconciliation_audit_payload(
3866
3872
  if child_state == "CLOSED":
3867
3873
  resolved_children.append(child)
3868
3874
  continue
3869
- if child_number == issue_number and absorbed_issue:
3875
+ if child_number == issue_number and merged_issue_open:
3870
3876
  resolved_children.append(child)
3871
3877
  continue
3872
3878
  unresolved_children.append(child)
@@ -3929,7 +3935,7 @@ def reconciliation_audit_payload(
3929
3935
  }
3930
3936
 
3931
3937
  if issue_number is not None:
3932
- expected_done = issue_payload is not None and (issue_payload.get("state") == "CLOSED" or absorbed_issue)
3938
+ expected_done = issue_payload is not None and (issue_payload.get("state") == "CLOSED" or merged_issue_open)
3933
3939
  if issue_item is None:
3934
3940
  project_drift_details.append(
3935
3941
  {
@@ -3994,12 +4000,23 @@ def reconciliation_audit_payload(
3994
4000
  )
3995
4001
 
3996
4002
  if missing_inputs:
4003
+ findings.append(
4004
+ make_reconciliation_finding(
4005
+ kind="host_signal_drift",
4006
+ severity="block",
4007
+ subject="github control plane",
4008
+ evidence={"missing_inputs": missing_inputs},
4009
+ recommended_action="restore readable GitHub issue, PR, project, or repository signals before closeout.",
4010
+ category="drift",
4011
+ fallback_to="manual-reconciliation",
4012
+ )
4013
+ )
3997
4014
  result = "block"
3998
4015
  summary = "reconciliation audit could not complete because required GitHub inputs were missing."
3999
4016
  else:
4000
4017
  result = reconciliation_result(findings)
4001
4018
  summary = (
4002
- "reconciliation audit found no absorbed-but-open, parent-drift, or project-drift findings."
4019
+ "reconciliation audit found no merged-but-open, absorbed-but-open, parent-drift, host-signal-drift, or project-drift findings."
4003
4020
  if result == "pass"
4004
4021
  else "reconciliation audit found GitHub drift that must be reviewed before closeout."
4005
4022
  )
@@ -4037,7 +4054,7 @@ def reconciliation_sync_plan(audit_payload: dict[str, Any]) -> tuple[list[dict[s
4037
4054
  evidence = finding.get("evidence")
4038
4055
  if severity != "fix-needed":
4039
4056
  continue
4040
- if kind == "absorbed_but_open":
4057
+ if kind in {"merged_but_open", "absorbed_but_open"}:
4041
4058
  plan.append(
4042
4059
  {
4043
4060
  "kind": kind,
@@ -92,6 +92,7 @@ CORE_DOCS = (
92
92
  "docs/methodology/templates/pull-request.md",
93
93
  "docs/evidence/extraction-ledger.md",
94
94
  "docs/evidence/landing-map.md",
95
+ "docs/evidence/validations/validation-closeout-reconciliation-blocking-gate.md",
95
96
  "docs/evidence/validations/validation-loom-core-runtime-parity.md",
96
97
  "docs/evidence/validations/validation-syvert-strong-governance-parity.md",
97
98
  "docs/adoption/rationale.md",
@@ -1118,10 +1119,14 @@ def require_reconciliation_payload(
1118
1119
  if not isinstance(finding, dict):
1119
1120
  failures.append(Failure(category, f"{context} reconciliation findings must be JSON objects"))
1120
1121
  continue
1121
- if finding.get("kind") not in {"absorbed_but_open", "parent_drift", "project_drift"}:
1122
+ if finding.get("category") not in {"drift", "gate_failure"}:
1123
+ failures.append(Failure(category, f"{context} reconciliation finding category must stay within the stable taxonomy"))
1124
+ if finding.get("kind") not in {"merged_but_open", "absorbed_but_open", "parent_drift", "project_drift", "host_signal_drift"}:
1122
1125
  failures.append(Failure(category, f"{context} reconciliation finding kind must stay within the stable contract"))
1123
1126
  if finding.get("severity") not in {"warn", "fix-needed", "block"}:
1124
1127
  failures.append(Failure(category, f"{context} reconciliation finding severity must stay within the stable contract"))
1128
+ if finding.get("fallback_to") not in {"reconciliation-sync", "manual-reconciliation", None}:
1129
+ failures.append(Failure(category, f"{context} reconciliation finding fallback_to must stay within the stable contract"))
1125
1130
  if not isinstance(finding.get("subject"), str) or not finding.get("subject"):
1126
1131
  failures.append(Failure(category, f"{context} reconciliation findings must include non-empty `subject`"))
1127
1132
  if not isinstance(finding.get("evidence"), dict):
@@ -4692,18 +4697,20 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
4692
4697
  "summary": "warn",
4693
4698
  "missing_inputs": [],
4694
4699
  "fallback_to": "manual-reconciliation",
4695
- "findings": [
4696
- {
4697
- "kind": "project_drift",
4698
- "severity": "warn",
4699
- "subject": "project 5",
4700
- "evidence": {},
4701
- "recommended_action": "review warning",
4702
- }
4703
- ],
4700
+ "findings": [
4701
+ {
4702
+ "category": "drift",
4703
+ "kind": "project_drift",
4704
+ "severity": "warn",
4705
+ "subject": "project 5",
4706
+ "evidence": {},
4707
+ "recommended_action": "review warning",
4708
+ "fallback_to": "reconciliation-sync",
4709
+ }
4710
+ ],
4711
+ },
4704
4712
  },
4705
- },
4706
- )
4713
+ )
4707
4714
  if warn_payload_failures:
4708
4715
  failures.append(
4709
4716
  Failure(
@@ -4712,6 +4719,50 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
4712
4719
  )
4713
4720
  )
4714
4721
 
4722
+ valid_reconciliation_samples = [
4723
+ ("merged_but_open", "fix-needed", "reconciliation-sync"),
4724
+ ("absorbed_but_open", "fix-needed", "reconciliation-sync"),
4725
+ ("parent_drift", "block", "manual-reconciliation"),
4726
+ ("host_signal_drift", "block", "manual-reconciliation"),
4727
+ ]
4728
+ for kind, reconciliation_result_value, fallback_to in valid_reconciliation_samples:
4729
+ sample_failures = []
4730
+ require_closeout_reconciliation_contract(
4731
+ sample_failures,
4732
+ category="daily-execution-cli",
4733
+ context=f"`closeout-{kind}`",
4734
+ payload={
4735
+ "result": "block",
4736
+ "fallback_to": fallback_to,
4737
+ "reconciliation": {
4738
+ "command": "reconciliation",
4739
+ "operation": "audit",
4740
+ "result": reconciliation_result_value,
4741
+ "summary": kind,
4742
+ "missing_inputs": [] if kind != "host_signal_drift" else ["github control plane"],
4743
+ "fallback_to": "manual-reconciliation",
4744
+ "findings": [
4745
+ {
4746
+ "category": "drift",
4747
+ "kind": kind,
4748
+ "severity": reconciliation_result_value,
4749
+ "subject": "closeout sample",
4750
+ "evidence": {},
4751
+ "recommended_action": "reconcile closeout sample",
4752
+ "fallback_to": fallback_to,
4753
+ }
4754
+ ],
4755
+ },
4756
+ },
4757
+ )
4758
+ if sample_failures:
4759
+ failures.append(
4760
+ Failure(
4761
+ "daily-execution-cli",
4762
+ f"`closeout-{kind}` synthetic payload must satisfy closeout reconciliation validation",
4763
+ )
4764
+ )
4765
+
4715
4766
  return failures
4716
4767
 
4717
4768
 
@@ -3748,13 +3748,19 @@ def make_reconciliation_finding(
3748
3748
  subject: str,
3749
3749
  evidence: dict[str, Any],
3750
3750
  recommended_action: str,
3751
+ category: str = "drift",
3752
+ fallback_to: str | None = None,
3751
3753
  ) -> dict[str, Any]:
3754
+ if fallback_to is None:
3755
+ fallback_to = "manual-reconciliation" if severity == "block" else "reconciliation-sync"
3752
3756
  return {
3757
+ "category": category,
3753
3758
  "kind": kind,
3754
3759
  "severity": severity,
3755
3760
  "subject": subject,
3756
3761
  "evidence": evidence,
3757
3762
  "recommended_action": recommended_action,
3763
+ "fallback_to": fallback_to,
3758
3764
  }
3759
3765
 
3760
3766
 
@@ -3824,13 +3830,13 @@ def reconciliation_audit_payload(
3824
3830
  merge_commit_sha = oid
3825
3831
  merge_commit_in_main = contains_merged_commit(target_root, merge_commit_sha)
3826
3832
 
3827
- absorbed_issue = False
3833
+ merged_issue_open = False
3828
3834
  if issue_payload is not None and pr_payload is not None:
3829
3835
  if issue_payload.get("state") == "OPEN" and pr_payload.get("state") == "MERGED" and merge_commit_sha and merge_commit_in_main:
3830
- absorbed_issue = True
3836
+ merged_issue_open = True
3831
3837
  findings.append(
3832
3838
  make_reconciliation_finding(
3833
- kind="absorbed_but_open",
3839
+ kind="merged_but_open",
3834
3840
  severity="fix-needed",
3835
3841
  subject=f"issue #{issue_number}",
3836
3842
  evidence={
@@ -3840,7 +3846,7 @@ def reconciliation_audit_payload(
3840
3846
  "merge_commit": merge_commit_sha,
3841
3847
  "merge_commit_in_main": merge_commit_in_main,
3842
3848
  },
3843
- recommended_action="close the absorbed issue or run reconciliation sync after reviewing the evidence.",
3849
+ recommended_action="close the merged issue or run reconciliation sync after reviewing the evidence.",
3844
3850
  )
3845
3851
  )
3846
3852
 
@@ -3866,7 +3872,7 @@ def reconciliation_audit_payload(
3866
3872
  if child_state == "CLOSED":
3867
3873
  resolved_children.append(child)
3868
3874
  continue
3869
- if child_number == issue_number and absorbed_issue:
3875
+ if child_number == issue_number and merged_issue_open:
3870
3876
  resolved_children.append(child)
3871
3877
  continue
3872
3878
  unresolved_children.append(child)
@@ -3929,7 +3935,7 @@ def reconciliation_audit_payload(
3929
3935
  }
3930
3936
 
3931
3937
  if issue_number is not None:
3932
- expected_done = issue_payload is not None and (issue_payload.get("state") == "CLOSED" or absorbed_issue)
3938
+ expected_done = issue_payload is not None and (issue_payload.get("state") == "CLOSED" or merged_issue_open)
3933
3939
  if issue_item is None:
3934
3940
  project_drift_details.append(
3935
3941
  {
@@ -3994,12 +4000,23 @@ def reconciliation_audit_payload(
3994
4000
  )
3995
4001
 
3996
4002
  if missing_inputs:
4003
+ findings.append(
4004
+ make_reconciliation_finding(
4005
+ kind="host_signal_drift",
4006
+ severity="block",
4007
+ subject="github control plane",
4008
+ evidence={"missing_inputs": missing_inputs},
4009
+ recommended_action="restore readable GitHub issue, PR, project, or repository signals before closeout.",
4010
+ category="drift",
4011
+ fallback_to="manual-reconciliation",
4012
+ )
4013
+ )
3997
4014
  result = "block"
3998
4015
  summary = "reconciliation audit could not complete because required GitHub inputs were missing."
3999
4016
  else:
4000
4017
  result = reconciliation_result(findings)
4001
4018
  summary = (
4002
- "reconciliation audit found no absorbed-but-open, parent-drift, or project-drift findings."
4019
+ "reconciliation audit found no merged-but-open, absorbed-but-open, parent-drift, host-signal-drift, or project-drift findings."
4003
4020
  if result == "pass"
4004
4021
  else "reconciliation audit found GitHub drift that must be reviewed before closeout."
4005
4022
  )
@@ -4037,7 +4054,7 @@ def reconciliation_sync_plan(audit_payload: dict[str, Any]) -> tuple[list[dict[s
4037
4054
  evidence = finding.get("evidence")
4038
4055
  if severity != "fix-needed":
4039
4056
  continue
4040
- if kind == "absorbed_but_open":
4057
+ if kind in {"merged_but_open", "absorbed_but_open"}:
4041
4058
  plan.append(
4042
4059
  {
4043
4060
  "kind": kind,