@arbiterforge/ca-pi 0.17.6 → 0.17.8
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/README.md +1 -1
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +20 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/release.json +36 -36
- package/plugins/ca-pi/hooks/_approvallib.py +32 -64
- package/plugins/ca-pi/hooks/_contextmergelib.py +295 -8
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/resolve-context-conflict.py +1 -1
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +25 -0
- package/plugins/ca-pi/routines/debug/SKILL.md +11 -0
- package/plugins/ca-pi/routines/debug/references/minimal-unresolved.json +58 -0
- package/plugins/ca-pi/routines/debug/references/preparation.md +41 -0
- package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +17 -0
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +25 -0
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +11 -0
- package/plugins/ca-pi/skills/ca-init/SKILL.md +40 -2
- package/plugins/ca-pi/skills/ca-pr/SKILL.md +17 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ and durable project context. You decide. codeArbiter enforces.
|
|
|
10
10
|
<img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude_Code-plugin-d97757">
|
|
11
11
|
<img alt="Codex plugin" src="https://img.shields.io/badge/OpenAI_Codex-plugin-10a37f">
|
|
12
12
|
<img alt="Pi Feature Forge preview" src="https://img.shields.io/badge/ca--pi-Feature_Forge_preview-d97757">
|
|
13
|
-
<img alt="version 2.25.
|
|
13
|
+
<img alt="version 2.25.7" src="https://img.shields.io/badge/version-2.25.7-2b7489">
|
|
14
14
|
<img alt="core lanes" src="https://img.shields.io/badge/core_lanes-18-555">
|
|
15
15
|
<img alt="skills" src="https://img.shields.io/badge/skills-22-555">
|
|
16
16
|
<img alt="agents" src="https://img.shields.io/badge/agents-20-555">
|
package/package.json
CHANGED
|
@@ -4,6 +4,26 @@ All notable changes to `ca-pi` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.17.8] - 2026-10-12
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Restore reviewed legacy code-map metadata updates and explicit per-block Git
|
|
12
|
+
conflict resolution with bounded helpers, preserving native ownership,
|
|
13
|
+
stale-input checks, unselected text and the Git index (#967).
|
|
14
|
+
- Explain debug packet shapes and link the trusted schema and a synthetic
|
|
15
|
+
unresolved example before one-shot validation (#951).
|
|
16
|
+
- Document brownfield host prerequisites before scaffolding, preserving
|
|
17
|
+
containment, native approval and partial-state recovery gates (#950).
|
|
18
|
+
|
|
19
|
+
## [0.17.7] - 2026-10-10
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Carry the shared bounded verification projection and approval preflight checks,
|
|
24
|
+
retaining complete command definitions, source snapshot ordering and full plan
|
|
25
|
+
identity freshness. Pi verification and review authority remain unsupported.
|
|
26
|
+
|
|
7
27
|
## [0.17.6] - 2026-10-10
|
|
8
28
|
|
|
9
29
|
### Fixed
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -6,43 +6,43 @@
|
|
|
6
6
|
"binaries": {
|
|
7
7
|
"darwin/amd64": {
|
|
8
8
|
"file": "ca-artifact-darwin-amd64",
|
|
9
|
-
"sha256": "
|
|
9
|
+
"sha256": "5214e1062e49aa39cd73868cd9792e7e296a865bb6e86230b17c19dda1ba3b71",
|
|
10
10
|
"native_tested": true
|
|
11
11
|
},
|
|
12
12
|
"darwin/arm64": {
|
|
13
13
|
"file": "ca-artifact-darwin-arm64",
|
|
14
|
-
"sha256": "
|
|
14
|
+
"sha256": "dde6deb178a0e725f8a88b1b30fa6980622b8af3b13fb73261aaaea47eb75a38",
|
|
15
15
|
"native_tested": true
|
|
16
16
|
},
|
|
17
17
|
"linux/amd64": {
|
|
18
18
|
"file": "ca-artifact-linux-amd64",
|
|
19
|
-
"sha256": "
|
|
19
|
+
"sha256": "ca2d5e0fecac0c99c45dc7576fff7d56af63dbd85decb4cfc54f81621ac90901",
|
|
20
20
|
"native_tested": true
|
|
21
21
|
},
|
|
22
22
|
"linux/arm64": {
|
|
23
23
|
"file": "ca-artifact-linux-arm64",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "de18df4c499814f6ccfc69efa6bfb6f4bbcd37c2af5dd091727233f1aaf91a39",
|
|
25
25
|
"native_tested": true
|
|
26
26
|
},
|
|
27
27
|
"windows/amd64": {
|
|
28
28
|
"file": "ca-artifact-windows-amd64.exe",
|
|
29
|
-
"sha256": "
|
|
29
|
+
"sha256": "ddab696f02b946400feac527f747a020b76dbb9e5dd444b0b7d464acbcd3888e",
|
|
30
30
|
"native_tested": true
|
|
31
31
|
},
|
|
32
32
|
"windows/arm64": {
|
|
33
33
|
"file": "ca-artifact-windows-arm64.exe",
|
|
34
|
-
"sha256": "
|
|
34
|
+
"sha256": "23689eaa4df71f95bde58b18bc32fab8d4ae709495250ddfd13192fc0427a813",
|
|
35
35
|
"native_tested": true
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"context_qualification": {
|
|
39
39
|
"darwin/amd64": {
|
|
40
|
-
"source_commit": "
|
|
40
|
+
"source_commit": "4c2529e2f3804d38855094e1c11d36178067c826",
|
|
41
41
|
"workflow": ".github/workflows/ci.yml",
|
|
42
|
-
"run_id": "
|
|
43
|
-
"binary_sha256": "
|
|
44
|
-
"observation_sha256": "
|
|
45
|
-
"module_sha256": "
|
|
42
|
+
"run_id": "38219547942",
|
|
43
|
+
"binary_sha256": "5214e1062e49aa39cd73868cd9792e7e296a865bb6e86230b17c19dda1ba3b71",
|
|
44
|
+
"observation_sha256": "fdd1d120d5ad0e4aed101efad7c32aeac561035faf075b70abf3ba5f82a4bc3b",
|
|
45
|
+
"module_sha256": "1aea76bc4dc8b54b368dccc80c3a1133b403b5915018fdcb6018f1a3913fe66c",
|
|
46
46
|
"native_cases": [
|
|
47
47
|
"TestContextBoundedRender",
|
|
48
48
|
"TestContextDocumentContract",
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
]
|
|
54
54
|
},
|
|
55
55
|
"darwin/arm64": {
|
|
56
|
-
"source_commit": "
|
|
56
|
+
"source_commit": "4c2529e2f3804d38855094e1c11d36178067c826",
|
|
57
57
|
"workflow": ".github/workflows/ci.yml",
|
|
58
|
-
"run_id": "
|
|
59
|
-
"binary_sha256": "
|
|
60
|
-
"observation_sha256": "
|
|
61
|
-
"module_sha256": "
|
|
58
|
+
"run_id": "38219547942",
|
|
59
|
+
"binary_sha256": "dde6deb178a0e725f8a88b1b30fa6980622b8af3b13fb73261aaaea47eb75a38",
|
|
60
|
+
"observation_sha256": "18294ead6b8a48804464f06f1bcc8254e0176c0ac0c48486a121aa1a018a309a",
|
|
61
|
+
"module_sha256": "1aea76bc4dc8b54b368dccc80c3a1133b403b5915018fdcb6018f1a3913fe66c",
|
|
62
62
|
"native_cases": [
|
|
63
63
|
"TestContextBoundedRender",
|
|
64
64
|
"TestContextDocumentContract",
|
|
@@ -69,12 +69,12 @@
|
|
|
69
69
|
]
|
|
70
70
|
},
|
|
71
71
|
"linux/amd64": {
|
|
72
|
-
"source_commit": "
|
|
72
|
+
"source_commit": "4c2529e2f3804d38855094e1c11d36178067c826",
|
|
73
73
|
"workflow": ".github/workflows/ci.yml",
|
|
74
|
-
"run_id": "
|
|
75
|
-
"binary_sha256": "
|
|
76
|
-
"observation_sha256": "
|
|
77
|
-
"module_sha256": "
|
|
74
|
+
"run_id": "38219547942",
|
|
75
|
+
"binary_sha256": "ca2d5e0fecac0c99c45dc7576fff7d56af63dbd85decb4cfc54f81621ac90901",
|
|
76
|
+
"observation_sha256": "e4717e28c2ca9a27fc278196310a00a3adff09766d307951ba93de57b262f6ab",
|
|
77
|
+
"module_sha256": "1aea76bc4dc8b54b368dccc80c3a1133b403b5915018fdcb6018f1a3913fe66c",
|
|
78
78
|
"native_cases": [
|
|
79
79
|
"TestContextBoundedRender",
|
|
80
80
|
"TestContextDocumentContract",
|
|
@@ -85,12 +85,12 @@
|
|
|
85
85
|
]
|
|
86
86
|
},
|
|
87
87
|
"linux/arm64": {
|
|
88
|
-
"source_commit": "
|
|
88
|
+
"source_commit": "4c2529e2f3804d38855094e1c11d36178067c826",
|
|
89
89
|
"workflow": ".github/workflows/ci.yml",
|
|
90
|
-
"run_id": "
|
|
91
|
-
"binary_sha256": "
|
|
92
|
-
"observation_sha256": "
|
|
93
|
-
"module_sha256": "
|
|
90
|
+
"run_id": "38219547942",
|
|
91
|
+
"binary_sha256": "de18df4c499814f6ccfc69efa6bfb6f4bbcd37c2af5dd091727233f1aaf91a39",
|
|
92
|
+
"observation_sha256": "03064179c8e677c66c0c90a2d109b139e5b3bb057fa296d5a9da4946ae26c37c",
|
|
93
|
+
"module_sha256": "1aea76bc4dc8b54b368dccc80c3a1133b403b5915018fdcb6018f1a3913fe66c",
|
|
94
94
|
"native_cases": [
|
|
95
95
|
"TestContextBoundedRender",
|
|
96
96
|
"TestContextDocumentContract",
|
|
@@ -101,12 +101,12 @@
|
|
|
101
101
|
]
|
|
102
102
|
},
|
|
103
103
|
"windows/amd64": {
|
|
104
|
-
"source_commit": "
|
|
104
|
+
"source_commit": "4c2529e2f3804d38855094e1c11d36178067c826",
|
|
105
105
|
"workflow": ".github/workflows/ci.yml",
|
|
106
|
-
"run_id": "
|
|
107
|
-
"binary_sha256": "
|
|
108
|
-
"observation_sha256": "
|
|
109
|
-
"module_sha256": "
|
|
106
|
+
"run_id": "38219547942",
|
|
107
|
+
"binary_sha256": "ddab696f02b946400feac527f747a020b76dbb9e5dd444b0b7d464acbcd3888e",
|
|
108
|
+
"observation_sha256": "e31ae79bc15c37dbb157c11e5a3775dd0d314350bbf761a968999ba4ffde2d9c",
|
|
109
|
+
"module_sha256": "1aea76bc4dc8b54b368dccc80c3a1133b403b5915018fdcb6018f1a3913fe66c",
|
|
110
110
|
"native_cases": [
|
|
111
111
|
"TestContextBoundedRender",
|
|
112
112
|
"TestContextDocumentContract",
|
|
@@ -117,12 +117,12 @@
|
|
|
117
117
|
]
|
|
118
118
|
},
|
|
119
119
|
"windows/arm64": {
|
|
120
|
-
"source_commit": "
|
|
120
|
+
"source_commit": "4c2529e2f3804d38855094e1c11d36178067c826",
|
|
121
121
|
"workflow": ".github/workflows/ci.yml",
|
|
122
|
-
"run_id": "
|
|
123
|
-
"binary_sha256": "
|
|
124
|
-
"observation_sha256": "
|
|
125
|
-
"module_sha256": "
|
|
122
|
+
"run_id": "38219547942",
|
|
123
|
+
"binary_sha256": "23689eaa4df71f95bde58b18bc32fab8d4ae709495250ddfd13192fc0427a813",
|
|
124
|
+
"observation_sha256": "e04cb8b45299950247c82557e7aa4c60c4c7b6b9718775b8c3ecaedc1d7bb6fe",
|
|
125
|
+
"module_sha256": "1aea76bc4dc8b54b368dccc80c3a1133b403b5915018fdcb6018f1a3913fe66c",
|
|
126
126
|
"native_cases": [
|
|
127
127
|
"TestContextBoundedRender",
|
|
128
128
|
"TestContextDocumentContract",
|
|
@@ -93,70 +93,44 @@ def _preflight_plan_verification(client: Any, identity: dict[str, Any]) -> dict[
|
|
|
93
93
|
artifact_id = identity["artifact_id"]
|
|
94
94
|
|
|
95
95
|
def same_identity(value: dict[str, Any]) -> None:
|
|
96
|
-
if not isinstance(value, dict) or
|
|
96
|
+
if not isinstance(value, dict) or type(value.get("revision")) is not int:
|
|
97
|
+
raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan verification identity is malformed")
|
|
98
|
+
if any(value.get(key) != identity.get(key) for key in keys):
|
|
97
99
|
raise _artifactlib.ArtifactError("STALE_PLAN_PREFLIGHT", "plan identity changed during verification preflight; reread the complete current plan")
|
|
98
100
|
|
|
99
|
-
def task_records(ids: list[str]):
|
|
100
|
-
for start in range(0, len(ids), 128):
|
|
101
|
-
selected = ids[start:start + 128]
|
|
102
|
-
offset = 0
|
|
103
|
-
for _ in range(len(selected)):
|
|
104
|
-
page = client.call("read-batch", {
|
|
105
|
-
"artifact_id": artifact_id, "symbols": selected,
|
|
106
|
-
"model_sha256": identity["model_sha256"], "offset": offset, "budget": 16384,
|
|
107
|
-
})
|
|
108
|
-
same_identity(page)
|
|
109
|
-
_artifactlib.ArtifactClient._check_page(page, offset, "records")
|
|
110
|
-
records = page["records"]
|
|
111
|
-
end = offset + len(records)
|
|
112
|
-
if (page.get("mode") != "exact" or page.get("context_complete") is not False
|
|
113
|
-
or type(page.get("offset")) is not int or page["offset"] != offset
|
|
114
|
-
or type(page.get("total")) is not int or page["total"] != len(selected)
|
|
115
|
-
or not records or end > len(selected)
|
|
116
|
-
or page["next_offset"] != (end if end < len(selected) else None)):
|
|
117
|
-
raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan task batch count, offset or completeness changed")
|
|
118
|
-
for expected_id, row in zip(selected[offset:end], records):
|
|
119
|
-
if (not isinstance(row, dict) or row.get("id") != expected_id
|
|
120
|
-
or row.get("kind") != "tasks" or row.get("retired") is not False
|
|
121
|
-
or not isinstance(row.get("record"), dict) or row["record"].get("id") != expected_id):
|
|
122
|
-
raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan task batch contains an invalid or unexpected record")
|
|
123
|
-
yield row["record"]
|
|
124
|
-
if page["next_offset"] is None:
|
|
125
|
-
break
|
|
126
|
-
offset = end
|
|
127
|
-
else:
|
|
128
|
-
raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan task batch did not terminate")
|
|
129
|
-
|
|
130
|
-
same_identity(client.call("identity", {"artifact_id": artifact_id}))
|
|
131
101
|
root = _artifactlib._trusted_directory(client.root, "UNSAFE_ROOT")
|
|
132
102
|
diagnostics: list[str] = []
|
|
133
|
-
|
|
103
|
+
last_task_id = ""
|
|
134
104
|
command_count = 0
|
|
135
105
|
offset, total = 0, None
|
|
136
106
|
for _ in range(8193):
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
107
|
+
try:
|
|
108
|
+
page = client.call("plan-verification", {
|
|
109
|
+
"artifact_id": artifact_id, "expected": {key: identity[key] for key in keys},
|
|
110
|
+
"offset": offset, "budget": 16384,
|
|
111
|
+
})
|
|
112
|
+
except _artifactlib.ArtifactError as exc:
|
|
113
|
+
if exc.code == "STALE_CURSOR":
|
|
114
|
+
raise _artifactlib.ArtifactError("STALE_PLAN_PREFLIGHT", "plan identity changed during verification preflight; reread the complete current plan") from exc
|
|
115
|
+
raise
|
|
141
116
|
same_identity(page)
|
|
142
|
-
_artifactlib.ArtifactClient._check_page(page, offset, "
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
117
|
+
_artifactlib.ArtifactClient._check_page(page, offset, "tasks")
|
|
118
|
+
tasks = page["tasks"]
|
|
119
|
+
end = offset + len(tasks)
|
|
120
|
+
if (page.get("context_complete") is not False
|
|
121
|
+
or type(page.get("offset")) is not int or page["offset"] != offset
|
|
122
|
+
or type(page.get("total")) is not int or page["total"] < end
|
|
123
|
+
or total is not None and page["total"] != total
|
|
124
|
+
or not tasks and page["total"] != 0
|
|
125
|
+
or page["next_offset"] != (end if end < page["total"] else None)):
|
|
126
|
+
raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan verification count, offset or completeness changed")
|
|
147
127
|
total = page["total"]
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
or
|
|
152
|
-
raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan
|
|
153
|
-
|
|
154
|
-
if row.get("kind") != "tasks" or row["retired"]:
|
|
155
|
-
continue
|
|
156
|
-
tasks.append(row["id"])
|
|
157
|
-
for task in task_records(tasks):
|
|
158
|
-
if not isinstance(task.get("verification"), list) or not task["verification"]:
|
|
159
|
-
raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan task lacks complete verification definitions")
|
|
128
|
+
for task in tasks:
|
|
129
|
+
if (not isinstance(task, dict) or set(task) != {"id", "verification"}
|
|
130
|
+
or not isinstance(task.get("id"), str) or task["id"] <= last_task_id
|
|
131
|
+
or not isinstance(task.get("verification"), list) or not task["verification"]):
|
|
132
|
+
raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan verification contains an invalid, unordered or incomplete task")
|
|
133
|
+
last_task_id = task["id"]
|
|
160
134
|
for number, definition in enumerate(task["verification"], 1):
|
|
161
135
|
command_count += 1
|
|
162
136
|
try:
|
|
@@ -177,17 +151,11 @@ def _preflight_plan_verification(client: Any, identity: dict[str, Any]) -> dict[
|
|
|
177
151
|
except (_artifactlib.ArtifactError, _artifactauthoritylib.AuthorityError) as exc:
|
|
178
152
|
argv = definition.get("argv") if isinstance(definition, dict) else None
|
|
179
153
|
diagnostics.append(f"{task['id']} verification[{number}] {json.dumps(argv, ensure_ascii=True)}: {exc}")
|
|
180
|
-
|
|
181
|
-
expected = offset + len(page["records"])
|
|
182
|
-
if next_offset is None:
|
|
183
|
-
if expected != total:
|
|
184
|
-
raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan outline ended before every record was inspected")
|
|
154
|
+
if page["next_offset"] is None:
|
|
185
155
|
break
|
|
186
|
-
|
|
187
|
-
raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan outline skipped records")
|
|
188
|
-
offset = next_offset
|
|
156
|
+
offset = end
|
|
189
157
|
else:
|
|
190
|
-
raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan
|
|
158
|
+
raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan verification did not terminate")
|
|
191
159
|
try:
|
|
192
160
|
snapshot = client.call("snapshot", {"artifact_id": artifact_id})
|
|
193
161
|
except _artifactlib.ArtifactError as exc:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# codeArbiter —
|
|
2
|
+
# codeArbiter — bounded legacy code-map recovery and metadata claim refresh.
|
|
3
3
|
#
|
|
4
4
|
# Fixed target and source only. Inspection binds the real merge, all index bytes,
|
|
5
5
|
# conflict rendering and legacy provenance. Application rechecks that identity,
|
|
@@ -9,9 +9,14 @@
|
|
|
9
9
|
# Public API:
|
|
10
10
|
# inspect(root) -> dict read-only recovery binding
|
|
11
11
|
# restore_head(root, expected) -> dict restore exact HEAD blob, never stage
|
|
12
|
+
# inspect_claims(root) -> dict bind existing metadata and sources
|
|
13
|
+
# refresh_claims(root, expected, claims) -> dict selected metadata only
|
|
14
|
+
# inspect_blocks(root) -> dict bind genuine Git conflict blocks
|
|
15
|
+
# resolve_blocks(root, expected, choices) -> dict finite ours/theirs choices
|
|
12
16
|
# main(argv=None) -> int bounded JSON CLI
|
|
13
17
|
|
|
14
18
|
import argparse
|
|
19
|
+
import datetime
|
|
15
20
|
import hashlib
|
|
16
21
|
import json
|
|
17
22
|
import os
|
|
@@ -20,6 +25,7 @@ import re
|
|
|
20
25
|
import stat
|
|
21
26
|
import subprocess
|
|
22
27
|
import sys
|
|
28
|
+
import tempfile
|
|
23
29
|
|
|
24
30
|
from _appendonlyconflictlib import ResolutionError, _validate_conflicted_worktree, _write_bytes_atomic
|
|
25
31
|
from _artifactlib import ArtifactError, _decode, _open_pinned_regular, _trusted_directory
|
|
@@ -32,6 +38,18 @@ TARGET = ".codearbiter/code-map.md"
|
|
|
32
38
|
PROVENANCE = ".codearbiter/.provenance/code-map.json"
|
|
33
39
|
MAX_BYTES = 8 << 20
|
|
34
40
|
OID = r"[0-9a-f]{40}(?:[0-9a-f]{24})?"
|
|
41
|
+
VERSION = r"(?:0|[1-9][0-9]{0,8})\.(?:0|[1-9][0-9]{0,8})\.(?:0|[1-9][0-9]{0,8})"
|
|
42
|
+
MANIFESTS = {
|
|
43
|
+
"ca_version": "plugins/ca/.claude-plugin/plugin.json",
|
|
44
|
+
"codex_version": "plugins/ca-codex/.codex-plugin/plugin.json",
|
|
45
|
+
"pi_version": "plugins/ca-pi/package.json",
|
|
46
|
+
}
|
|
47
|
+
CLAIM_PATTERNS = {
|
|
48
|
+
"ca_version": rb"^- `plugins/ca/` \(v(" + VERSION.encode() + rb")(?=\))",
|
|
49
|
+
"codex_version": rb"^- `plugins/ca-codex/` \(v(" + VERSION.encode() + rb")(?=\))",
|
|
50
|
+
"pi_version": rb"^- `plugins/ca-pi/` \(v(" + VERSION.encode() + rb")(?=[,)])",
|
|
51
|
+
"metadata_date": rb"metadata refreshed on ([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\.)",
|
|
52
|
+
}
|
|
35
53
|
|
|
36
54
|
|
|
37
55
|
def _require(condition, reason):
|
|
@@ -54,13 +72,14 @@ def _git(root, *args):
|
|
|
54
72
|
return result.stdout
|
|
55
73
|
|
|
56
74
|
|
|
57
|
-
def _read(path):
|
|
75
|
+
def _read(path, *, single_link=False):
|
|
58
76
|
_trusted_directory(path.parent, "UNSAFE_CONTEXT_PATH")
|
|
59
77
|
descriptor = _open_pinned_regular(path)
|
|
60
78
|
try:
|
|
61
79
|
info = os.fstat(descriptor)
|
|
62
80
|
_require(stat.S_ISREG(info.st_mode) and not getattr(info, "st_file_attributes", 0) & 0x400
|
|
63
81
|
and info.st_size <= MAX_BYTES, "context evidence must be a bounded regular file")
|
|
82
|
+
_require(not single_link or info.st_nlink == 1, "linked claim evidence is not supported")
|
|
64
83
|
with os.fdopen(descriptor, "rb", closefd=False) as handle:
|
|
65
84
|
raw = handle.read(MAX_BYTES + 1)
|
|
66
85
|
_require(len(raw) <= MAX_BYTES, "context evidence is oversized")
|
|
@@ -75,7 +94,7 @@ def _legacy(raw):
|
|
|
75
94
|
and record["doc"] == "code-map", "legacy code-map provenance required; native ownership is not recoverable here")
|
|
76
95
|
|
|
77
96
|
|
|
78
|
-
def _snapshot(requested_root):
|
|
97
|
+
def _snapshot(requested_root, *, blocks=False):
|
|
79
98
|
root = _trusted_directory(requested_root, "UNSAFE_CONTEXT_ROOT")
|
|
80
99
|
actual = Path(_git(root, "rev-parse", "--show-toplevel").decode().strip()).resolve(strict=True)
|
|
81
100
|
_require(actual == root, "explicit repository root required")
|
|
@@ -103,7 +122,13 @@ def _snapshot(requested_root):
|
|
|
103
122
|
theirs = _git(root, "cat-file", "blob", stages[2][1])
|
|
104
123
|
# Reuse the existing single-conflict renderer check. Unsupported renderings
|
|
105
124
|
# (including a non-empty diff3 base) refuse rather than discarding human text.
|
|
106
|
-
|
|
125
|
+
payload = ours
|
|
126
|
+
rendered = None
|
|
127
|
+
if blocks:
|
|
128
|
+
payload, rendered = _git_conflict_blocks(root, current,
|
|
129
|
+
_git(root, "cat-file", "blob", stages[0][1]), ours, theirs)
|
|
130
|
+
else:
|
|
131
|
+
_validate_conflicted_worktree(current, b"", ours, theirs)
|
|
107
132
|
provenance = _read(root / PROVENANCE)
|
|
108
133
|
_legacy(provenance)
|
|
109
134
|
for revision in (base, head, incoming):
|
|
@@ -116,9 +141,17 @@ def _snapshot(requested_root):
|
|
|
116
141
|
binding = {"root": str(root), "git_dir": str(git_dir), "head": head, "merge_head": incoming,
|
|
117
142
|
"stages": stages, "index_sha256": _hash(_read(index)),
|
|
118
143
|
"worktree_sha256": _hash(current), "provenance_sha256": _hash(provenance)}
|
|
144
|
+
if blocks:
|
|
145
|
+
binding["resolution"] = {"operation": "resolve-blocks", "rendering": rendered,
|
|
146
|
+
"blocks": len(payload[0])}
|
|
119
147
|
digest = _hash(json.dumps(binding, sort_keys=True, separators=(",", ":")).encode())
|
|
120
|
-
|
|
121
|
-
|
|
148
|
+
result = {"target": TARGET, "head": head, "merge_head": incoming,
|
|
149
|
+
"head_sha256": _hash(ours), "binding": digest}
|
|
150
|
+
if blocks:
|
|
151
|
+
result.update(blocks=len(payload[0]), rendering=rendered,
|
|
152
|
+
sides=[{"ours_sha256": _hash(part["ours"]),
|
|
153
|
+
"theirs_sha256": _hash(part["theirs"])} for part in payload[0]])
|
|
154
|
+
return root, index, current, payload, result
|
|
122
155
|
|
|
123
156
|
|
|
124
157
|
def inspect(root):
|
|
@@ -143,16 +176,270 @@ def restore_head(root, expected):
|
|
|
143
176
|
return {**fresh, "status": "restored", "staged": False}
|
|
144
177
|
|
|
145
178
|
|
|
179
|
+
def _claim_matches(raw):
|
|
180
|
+
text = raw.decode("utf-8", "strict")
|
|
181
|
+
_require(re.match(r"^# Code map(?:[ \t][^\r\n]*)?\r?\n", text) is not None
|
|
182
|
+
and "\0" not in text and re.search(r"^(?:<{7,}|={7,}|>{7,}|\|{7,})", text, re.M) is None,
|
|
183
|
+
"a resolved UTF-8 code map is required")
|
|
184
|
+
fields = {}
|
|
185
|
+
for field, pattern in CLAIM_PATTERNS.items():
|
|
186
|
+
matches = list(re.finditer(pattern, raw, re.M))
|
|
187
|
+
_require(len(matches) <= 1, "ambiguous metadata claim")
|
|
188
|
+
if matches:
|
|
189
|
+
fields[field] = matches[0]
|
|
190
|
+
return fields
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _claim_snapshot(requested_root):
|
|
194
|
+
root = _trusted_directory(requested_root, "UNSAFE_CONTEXT_ROOT")
|
|
195
|
+
actual = Path(_git(root, "rev-parse", "--show-toplevel").decode().strip()).resolve(strict=True)
|
|
196
|
+
_require(actual == root, "explicit repository root required")
|
|
197
|
+
git_dir = _trusted_directory(
|
|
198
|
+
_git(root, "rev-parse", "--absolute-git-dir").decode().strip(), "UNSAFE_GIT_DIRECTORY")
|
|
199
|
+
index = Path(_git(root, "rev-parse", "--path-format=absolute", "--git-path", "index").decode().strip())
|
|
200
|
+
head = _git(root, "rev-parse", "--verify", "HEAD^{commit}").decode().strip()
|
|
201
|
+
_require(re.fullmatch(OID, head), "current Git head required")
|
|
202
|
+
_require(not _git(root, "ls-files", "--unmerged", "--", TARGET, PROVENANCE),
|
|
203
|
+
"resolve context and provenance conflicts before claim refresh")
|
|
204
|
+
tracked = _git(root, "ls-files", "--stage", "-z", "--", TARGET, PROVENANCE).split(b"\0")
|
|
205
|
+
indexed = {}
|
|
206
|
+
for raw in tracked:
|
|
207
|
+
if raw:
|
|
208
|
+
matched = re.fullmatch(r"100644 (" + OID + r") 0\t(.+)", raw.decode())
|
|
209
|
+
_require(matched is not None and matched[2] in {TARGET, PROVENANCE},
|
|
210
|
+
"regular tracked legacy context required")
|
|
211
|
+
indexed[matched[2]] = matched[1]
|
|
212
|
+
_require(set(indexed) == {TARGET, PROVENANCE}, "tracked document and provenance required")
|
|
213
|
+
current = _read(root / TARGET, single_link=True)
|
|
214
|
+
fields = _claim_matches(current)
|
|
215
|
+
provenance = _read(root / PROVENANCE, single_link=True)
|
|
216
|
+
_legacy(provenance)
|
|
217
|
+
_legacy(_git(root, "cat-file", "blob", head + ":" + PROVENANCE))
|
|
218
|
+
_legacy(_git(root, "cat-file", "blob", indexed[PROVENANCE]))
|
|
219
|
+
merge = None
|
|
220
|
+
if os.path.lexists(git_dir / "MERGE_HEAD"):
|
|
221
|
+
merge = _read(git_dir / "MERGE_HEAD").decode().strip()
|
|
222
|
+
_require(re.fullmatch(OID, merge), "one current merge head required")
|
|
223
|
+
_legacy(_git(root, "cat-file", "blob", merge + ":" + PROVENANCE))
|
|
224
|
+
for relative in (".codearbiter/.artifacts", ".codearbiter/.artifacts/transactions"):
|
|
225
|
+
if os.path.lexists(root / relative):
|
|
226
|
+
_trusted_directory(root / relative, "UNSAFE_CONTEXT_JOURNAL")
|
|
227
|
+
_require(_committed_context_journal(root, "code-map", TARGET) is False,
|
|
228
|
+
"native or uncertain context ownership requires its writer")
|
|
229
|
+
sources = {}
|
|
230
|
+
for field, relative in MANIFESTS.items():
|
|
231
|
+
raw = _read(root / relative, single_link=True)
|
|
232
|
+
manifest = _decode(raw)
|
|
233
|
+
version = manifest.get("version") if isinstance(manifest, dict) else None
|
|
234
|
+
_require(isinstance(version, str) and re.fullmatch(VERSION, version),
|
|
235
|
+
"literal current source manifest version required")
|
|
236
|
+
sources[field] = {"version": version, "sha256": _hash(raw)}
|
|
237
|
+
binding = {"root": str(root), "git_dir": str(git_dir), "head": head, "merge_head": merge,
|
|
238
|
+
"index_sha256": _hash(_read(index)), "target_sha256": _hash(current),
|
|
239
|
+
"provenance_sha256": _hash(provenance), "sources": sources,
|
|
240
|
+
"claims": {field: matched[1].decode("ascii") for field, matched in fields.items()},
|
|
241
|
+
"inspection_date": datetime.date.today().isoformat()}
|
|
242
|
+
digest = _hash(json.dumps(binding, sort_keys=True, separators=(",", ":")).encode())
|
|
243
|
+
return root, index, current, sources, {**binding, "target": TARGET, "binding": digest}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def inspect_claims(root):
|
|
247
|
+
return _claim_snapshot(root)[-1]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _selected_claim_bytes(current, sources, inspection_date, claims):
|
|
251
|
+
_require(type(claims) is list and 1 <= len(claims) <= 4, "select one to four existing metadata claims")
|
|
252
|
+
matches = _claim_matches(current)
|
|
253
|
+
edits = []
|
|
254
|
+
seen = set()
|
|
255
|
+
for claim in claims:
|
|
256
|
+
_require(type(claim) is dict and set(claim) == {"field", "before", "after"}, "typed metadata claims required")
|
|
257
|
+
field, before, after = claim["field"], claim["before"], claim["after"]
|
|
258
|
+
_require(type(field) is str and field in matches and field not in seen
|
|
259
|
+
and type(before) is str and type(after) is str and before != after,
|
|
260
|
+
"select each existing metadata claim once")
|
|
261
|
+
seen.add(field)
|
|
262
|
+
matched = matches[field]
|
|
263
|
+
_require(matched[1].decode() == before, "exact claim preimage required")
|
|
264
|
+
if field == "metadata_date":
|
|
265
|
+
datetime.date.fromisoformat(before)
|
|
266
|
+
_require(after == inspection_date, "refresh date must be the actual inspection date")
|
|
267
|
+
else:
|
|
268
|
+
_require(re.fullmatch(VERSION, after) and after == sources[field]["version"],
|
|
269
|
+
"claim must match the current source manifest")
|
|
270
|
+
edits.append((*matched.span(1), after.encode("ascii")))
|
|
271
|
+
for start, end, replacement in sorted(edits, reverse=True):
|
|
272
|
+
current = current[:start] + replacement + current[end:]
|
|
273
|
+
return current
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def refresh_claims(root, expected, claims):
|
|
277
|
+
_require(isinstance(expected, str) and re.fullmatch(r"[0-9a-f]{64}", expected), "exact inspection binding required")
|
|
278
|
+
root, index, current, sources, initial = _claim_snapshot(root)
|
|
279
|
+
_require(initial["binding"] == expected, "stale claim binding; inspect again")
|
|
280
|
+
_selected_claim_bytes(current, sources, initial["inspection_date"], claims)
|
|
281
|
+
lock_path = Path(str(index) + ".lock")
|
|
282
|
+
descriptor = os.open(lock_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
283
|
+
try:
|
|
284
|
+
root, _index, before, sources, fresh = _claim_snapshot(root)
|
|
285
|
+
_require(fresh["binding"] == expected, "stale claim binding; inspect again")
|
|
286
|
+
proposed = _selected_claim_bytes(before, sources, fresh["inspection_date"], claims)
|
|
287
|
+
_write_bytes_atomic(root / TARGET, proposed, expected=before)
|
|
288
|
+
finally:
|
|
289
|
+
os.close(descriptor)
|
|
290
|
+
lock_path.unlink()
|
|
291
|
+
return {**fresh, "status": "refreshed", "staged": False, "after_sha256": _hash(proposed)}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _marker(line):
|
|
295
|
+
matched = re.fullmatch(rb"([<|=>]{7,32})(?: ([^\r\n]{1,256}))?\r?\n", line)
|
|
296
|
+
if matched is None or len(set(matched[1])) != 1:
|
|
297
|
+
return None
|
|
298
|
+
return matched[1][:1], len(matched[1]), matched[2] or b""
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _parse_conflict_blocks(raw):
|
|
302
|
+
text = raw.decode("utf-8", "strict")
|
|
303
|
+
_require("\0" not in text, "UTF-8 text conflict required")
|
|
304
|
+
lines = raw.splitlines(keepends=True)
|
|
305
|
+
pieces, labels, marker_size = [], None, None
|
|
306
|
+
cursor = 0
|
|
307
|
+
prefix = []
|
|
308
|
+
while cursor < len(lines):
|
|
309
|
+
marker = _marker(lines[cursor])
|
|
310
|
+
if marker is None:
|
|
311
|
+
prefix.append(lines[cursor])
|
|
312
|
+
cursor += 1
|
|
313
|
+
continue
|
|
314
|
+
_require(marker[0] == b"<" and marker[2] == b"HEAD", "recognized Git HEAD conflict required")
|
|
315
|
+
size = marker[1]
|
|
316
|
+
side_labels = [marker[2], b"base", None]
|
|
317
|
+
part = {"prefix": b"".join(prefix), "ours": b"", "base": None, "theirs": b""}
|
|
318
|
+
prefix = []
|
|
319
|
+
cursor += 1
|
|
320
|
+
side, content = "ours", []
|
|
321
|
+
while cursor < len(lines):
|
|
322
|
+
marker = _marker(lines[cursor])
|
|
323
|
+
if marker is None:
|
|
324
|
+
content.append(lines[cursor])
|
|
325
|
+
cursor += 1
|
|
326
|
+
continue
|
|
327
|
+
_require(marker[1] == size, "consistent conflict marker size required")
|
|
328
|
+
if marker[0] == b"|" and side == "ours":
|
|
329
|
+
_require(marker[2], "Git base label required")
|
|
330
|
+
part[side] = b"".join(content)
|
|
331
|
+
side_labels[1] = marker[2]
|
|
332
|
+
side, content = "base", []
|
|
333
|
+
elif marker[0] == b"=" and side in {"ours", "base"}:
|
|
334
|
+
_require(not marker[2], "plain Git separator required")
|
|
335
|
+
part[side] = b"".join(content)
|
|
336
|
+
side, content = "theirs", []
|
|
337
|
+
elif marker[0] == b">" and side == "theirs":
|
|
338
|
+
_require(marker[2], "Git incoming label required")
|
|
339
|
+
part[side] = b"".join(content)
|
|
340
|
+
side_labels[2] = marker[2]
|
|
341
|
+
cursor += 1
|
|
342
|
+
break
|
|
343
|
+
else:
|
|
344
|
+
raise ResolutionError("ambiguous or nested conflict markers")
|
|
345
|
+
cursor += 1
|
|
346
|
+
_require(side_labels[2] is not None, "complete conflict block required")
|
|
347
|
+
if labels is None:
|
|
348
|
+
labels, marker_size = side_labels, size
|
|
349
|
+
_require(side_labels == labels and size == marker_size, "one consistent Git rendering required")
|
|
350
|
+
pieces.append(part)
|
|
351
|
+
_require(len(pieces) <= 32, "at most 32 conflict blocks supported")
|
|
352
|
+
_require(pieces, "Git conflict blocks required")
|
|
353
|
+
return (pieces, b"".join(prefix)), labels, marker_size
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _git_conflict_blocks(root, current, base, ours, theirs):
|
|
357
|
+
payload, labels, size = _parse_conflict_blocks(current)
|
|
358
|
+
modes = (("diff3", "--diff3"), ("zdiff3", "--zdiff3")) if payload[0][0]["base"] is not None else (("merge", None),)
|
|
359
|
+
env = root_bound_git_env()
|
|
360
|
+
env.update(GIT_OPTIONAL_LOCKS="0", GIT_TERMINAL_PROMPT="0", GIT_NO_REPLACE_OBJECTS="1")
|
|
361
|
+
# Git itself must reproduce all hunk bodies and all unconflicted text from
|
|
362
|
+
# the real three stages. Labels are only the current bounded marker labels;
|
|
363
|
+
# no caller content, custom merge driver or arbitrary revision is executed.
|
|
364
|
+
with tempfile.TemporaryDirectory(prefix="codearbiter-context-merge-") as temporary:
|
|
365
|
+
paths = [Path(temporary) / name for name in ("ours", "base", "theirs")]
|
|
366
|
+
for path, raw in zip(paths, (ours, base, theirs)):
|
|
367
|
+
path.write_bytes(raw)
|
|
368
|
+
for name, flag in modes:
|
|
369
|
+
args = [git_executable(), "--no-pager", "--no-lazy-fetch", "-c", "core.fsmonitor=false",
|
|
370
|
+
"-c", "core.untrackedCache=false", "-c", "merge.conflictStyle=merge",
|
|
371
|
+
"merge-file", "--stdout", "--marker-size=" + str(size)]
|
|
372
|
+
if flag:
|
|
373
|
+
args.append(flag)
|
|
374
|
+
for label in labels:
|
|
375
|
+
args.extend(("-L", label.decode("utf-8", "strict")))
|
|
376
|
+
result = subprocess.run([*args, *(str(path) for path in paths)], cwd=root, env=env,
|
|
377
|
+
capture_output=True, shell=False, timeout=30)
|
|
378
|
+
_require(len(result.stdout) <= MAX_BYTES, "Git conflict rendering is oversized")
|
|
379
|
+
if 1 <= result.returncode <= 127 and result.stdout.replace(b"\r\n", b"\n") == current.replace(b"\r\n", b"\n"):
|
|
380
|
+
return payload, name
|
|
381
|
+
raise ResolutionError("worktree is not Git's current three-stage rendering")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def inspect_blocks(root):
|
|
385
|
+
return _snapshot(root, blocks=True)[-1]
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _choose_blocks(payload, choices):
|
|
389
|
+
parts, suffix = payload
|
|
390
|
+
_require(type(choices) is list and len(choices) == len(parts)
|
|
391
|
+
and all(type(choice) is str and choice in {"ours", "theirs"} for choice in choices),
|
|
392
|
+
"one explicit ours/theirs choice per conflict block required")
|
|
393
|
+
return b"".join(part["prefix"] + part[choice] for part, choice in zip(parts, choices)) + suffix
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def resolve_blocks(root, expected, choices):
|
|
397
|
+
_require(isinstance(expected, str) and re.fullmatch(r"[0-9a-f]{64}", expected), "exact inspection binding required")
|
|
398
|
+
root, index, _before, payload, initial = _snapshot(root, blocks=True)
|
|
399
|
+
_require(initial["binding"] == expected, "stale conflict binding; inspect again")
|
|
400
|
+
_choose_blocks(payload, choices)
|
|
401
|
+
lock_path = Path(str(index) + ".lock")
|
|
402
|
+
descriptor = os.open(lock_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
403
|
+
try:
|
|
404
|
+
root, _index, before, payload, fresh = _snapshot(root, blocks=True)
|
|
405
|
+
_require(fresh["binding"] == expected, "stale conflict binding; inspect again")
|
|
406
|
+
proposed = _choose_blocks(payload, choices)
|
|
407
|
+
_write_bytes_atomic(root / TARGET, proposed, expected=before)
|
|
408
|
+
finally:
|
|
409
|
+
os.close(descriptor)
|
|
410
|
+
lock_path.unlink()
|
|
411
|
+
return {**fresh, "status": "resolved", "staged": False, "after_sha256": _hash(proposed)}
|
|
412
|
+
|
|
413
|
+
|
|
146
414
|
def main(argv=None):
|
|
147
|
-
parser = argparse.ArgumentParser(description="
|
|
415
|
+
parser = argparse.ArgumentParser(description="Bounded fixed-target legacy code-map recovery and metadata refresh.")
|
|
148
416
|
parser.add_argument("--root", required=True)
|
|
149
417
|
operations = parser.add_subparsers(dest="operation", required=True)
|
|
150
418
|
operations.add_parser("inspect")
|
|
151
419
|
restore = operations.add_parser("restore-head")
|
|
152
420
|
restore.add_argument("--expected", required=True)
|
|
421
|
+
operations.add_parser("inspect-claims")
|
|
422
|
+
refresh = operations.add_parser("refresh-claims")
|
|
423
|
+
refresh.add_argument("--expected", required=True)
|
|
424
|
+
refresh.add_argument("--claims", required=True)
|
|
425
|
+
operations.add_parser("inspect-blocks")
|
|
426
|
+
resolve = operations.add_parser("resolve-blocks")
|
|
427
|
+
resolve.add_argument("--expected", required=True)
|
|
428
|
+
resolve.add_argument("--choices", required=True)
|
|
153
429
|
args = parser.parse_args(argv)
|
|
154
430
|
try:
|
|
155
|
-
|
|
431
|
+
if args.operation == "inspect":
|
|
432
|
+
result = inspect(args.root)
|
|
433
|
+
elif args.operation == "restore-head":
|
|
434
|
+
result = restore_head(args.root, args.expected)
|
|
435
|
+
elif args.operation == "inspect-claims":
|
|
436
|
+
result = inspect_claims(args.root)
|
|
437
|
+
elif args.operation == "refresh-claims":
|
|
438
|
+
result = refresh_claims(args.root, args.expected, _decode(args.claims.encode("utf-8")))
|
|
439
|
+
elif args.operation == "inspect-blocks":
|
|
440
|
+
result = inspect_blocks(args.root)
|
|
441
|
+
else:
|
|
442
|
+
result = resolve_blocks(args.root, args.expected, _decode(args.choices.encode("utf-8")))
|
|
156
443
|
except (OSError, ValueError, RuntimeError, ArtifactError, subprocess.SubprocessError):
|
|
157
444
|
print("context conflict recovery refused: absent, changed, unsafe or unsupported conflict evidence", file=sys.stderr)
|
|
158
445
|
return 1
|
|
@@ -11,7 +11,7 @@ import hostapi # noqa: E402
|
|
|
11
11
|
class PiHost(hostapi.Host):
|
|
12
12
|
name = "pi"
|
|
13
13
|
adapter_name = "@arbiterforge/ca-pi"
|
|
14
|
-
adapter_version = "0.17.
|
|
14
|
+
adapter_version = "0.17.8"
|
|
15
15
|
update_target = "ca-pi"
|
|
16
16
|
update_tag_prefix = "ca-pi-v"
|
|
17
17
|
update_command = "pi update npm:@arbiterforge/ca-pi"
|
|
@@ -313,7 +313,7 @@ class Host:
|
|
|
313
313
|
|
|
314
314
|
name = "claude"
|
|
315
315
|
adapter_name = "ca"
|
|
316
|
-
adapter_version = "2.25.
|
|
316
|
+
adapter_version = "2.25.7"
|
|
317
317
|
|
|
318
318
|
# Update-notifier descriptor. Each independently versioned host overrides
|
|
319
319
|
# these three values in its per-plugin _host.py. Keeping the target,
|
|
@@ -114,6 +114,31 @@ Run an **incremental re-scout for each affected document**, scoped to its select
|
|
|
114
114
|
- **Claim still holds → silently re-baseline only that document's provenance record.** Call `_provenancelib.rebaseline(doc_provenance, selected_current_hashes)` where `selected_current_hashes` contains current hashes only for the selected staged paths whose claims in that document were inspected and still hold. Do not re-baseline a path whose claim changed or was not inspected. Stage only `.codearbiter/.provenance/<doc>.json` by explicit path so the re-baselined record rides THIS commit (ADR-0008 ride-along pattern — nothing is surfaced to the user). After staging, re-run the secrets scan from `tech-stack.md` over the newly-staged path(s) — any file staged after Phase 4 must still pass the automated secrets scan before the commit proceeds. A missing current hash (deletion or rename) cannot be silently re-baselined; surface it in Phase 6 for review.
|
|
115
115
|
- **Claim changed → route to the existing Phase 6 diff-review.** Propose only the affected doc or code-map edit required to reflect its changed claim. The user reviews that edit in the normal diff, and Phase 7 stages approved paths explicitly; do not silently rewrite or add unrelated documentation.
|
|
116
116
|
|
|
117
|
+
For reviewed legacy code-map metadata, use the shipped fixed-target helper at
|
|
118
|
+
`<plugin-root>/hooks/resolve-context-conflict.py`. Run `--root "<project-root>"
|
|
119
|
+
inspect-claims`, then `--root "<project-root>" refresh-claims --expected <binding>
|
|
120
|
+
--claims <json-array>` with one to four reviewed records containing exactly
|
|
121
|
+
`field`, `before`, and `after`. The supported existing fields are `ca_version`,
|
|
122
|
+
`codex_version`, `pi_version`, and `metadata_date`. Use the inspection's exact
|
|
123
|
+
current claim as `before`; a version's `after` must match its fixed source
|
|
124
|
+
manifest, and the date must match `inspection_date`. Select the interpreter once
|
|
125
|
+
by presence and use it for both calls; never retry a failed operation under
|
|
126
|
+
another interpreter.
|
|
127
|
+
|
|
128
|
+
This operation requires resolved, tracked legacy schema-1 document/provenance
|
|
129
|
+
and no native or uncertain journal ownership. It binds document, provenance,
|
|
130
|
+
Git/index, source manifest bytes and date, preserves all unselected bytes, and
|
|
131
|
+
does not stage, rebaseline, adopt, enroll or create a native receipt. Inspect the
|
|
132
|
+
worktree diff for `.codearbiter/code-map.md`, selectively stage that explicit
|
|
133
|
+
path, and re-run the normal secrets scan over the newly staged Markdown before
|
|
134
|
+
Phase 6. Treat it as a heal-proposed doc/code-map edit in Phase 6 review, never as
|
|
135
|
+
a silent rebaseline, and retain its explicit inclusion in Phase 7. Provenance
|
|
136
|
+
rebaseline remains separately scoped by Phase 5.5. Unsupported
|
|
137
|
+
claims or refused preimages stop the edit; do not substitute a direct write or
|
|
138
|
+
native adoption. A codeArbiter contributor may exercise the tested and
|
|
139
|
+
independently reviewed canonical helper under `core/pysrc/` with live enforcement
|
|
140
|
+
intact, never a copied script, installed-cache edit or hook/trust change.
|
|
141
|
+
|
|
117
142
|
## Phase 6 — Diff review · gate: BLOCK
|
|
118
143
|
|
|
119
144
|
Read the complete staged diff (`git diff --cached`). Flag as blocking:
|
|
@@ -241,6 +241,17 @@ resume condition when applicable. Distinguish an executed check from a proposed
|
|
|
241
241
|
command and a recommended handoff from a completed operation. Keep the actual
|
|
242
242
|
diagnosis-only or repair-requested caller scope visible throughout.
|
|
243
243
|
|
|
244
|
+
Before assembling a handoff or invoking its validator, read the installed
|
|
245
|
+
packet preparation guide at `<plugin-root>/routines/debug/references/preparation.md`,
|
|
246
|
+
field reference at `<plugin-root>/routines/debug/references/handoff.md`, and
|
|
247
|
+
JSON Schema at `<plugin-root>/routines/debug/references/debug-handoff.schema.json`.
|
|
248
|
+
Use the linked minimal unresolved example to learn the closed field shapes;
|
|
249
|
+
its synthetic observations are never evidence for the current case. In
|
|
250
|
+
particular, `case_id` must match `DBG-[A-Za-z0-9][A-Za-z0-9_-]{2,63}`.
|
|
251
|
+
Resolve these references from the loaded trusted package. If they are absent,
|
|
252
|
+
stop validated transfer and report the missing resource; do not guess a shape
|
|
253
|
+
or use the one-shot validator to discover fields by trial and error.
|
|
254
|
+
|
|
244
255
|
For a handoff, assemble the transient `codearbiter.debug-handoff/1.0.0` packet
|
|
245
256
|
from the observations actually in hand. Preserve symptom, expectation and its
|
|
246
257
|
citations, reproduction state and limitations, evidence observations and
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"protocol": "codearbiter.debug-handoff/1.0.0",
|
|
3
|
+
"case_id": "DBG-example-unresolved",
|
|
4
|
+
"request": {
|
|
5
|
+
"intent": "diagnosis_only",
|
|
6
|
+
"request_ref": "example:synthetic-request",
|
|
7
|
+
"scope": "Synthetic documentation example only.",
|
|
8
|
+
"restrictions": ["No repair or external operations."]
|
|
9
|
+
},
|
|
10
|
+
"snapshots": [],
|
|
11
|
+
"symptom": {
|
|
12
|
+
"observed": "An example operator reports a timeout without an accessible trace.",
|
|
13
|
+
"expected": null,
|
|
14
|
+
"expected_evidence_ids": []
|
|
15
|
+
},
|
|
16
|
+
"reproduction": {
|
|
17
|
+
"status": "not_yet_reproduced",
|
|
18
|
+
"recipe": null,
|
|
19
|
+
"evidence_ids": [],
|
|
20
|
+
"limitations": "No runtime was exercised; this is a synthetic example."
|
|
21
|
+
},
|
|
22
|
+
"hypotheses": [],
|
|
23
|
+
"checks": [],
|
|
24
|
+
"evidence": [
|
|
25
|
+
{
|
|
26
|
+
"id": "E-01",
|
|
27
|
+
"kind": "user_report",
|
|
28
|
+
"snapshot_id": null,
|
|
29
|
+
"locator": "example:synthetic-request",
|
|
30
|
+
"observation": "The example operator reports a timeout.",
|
|
31
|
+
"observed_at": null,
|
|
32
|
+
"check_id": null,
|
|
33
|
+
"coverage": "unknown",
|
|
34
|
+
"limitations": "Synthetic report; no independent execution evidence."
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"disposition": {
|
|
38
|
+
"kind": "unresolved",
|
|
39
|
+
"rationale": "The failure trace is unavailable; no cause is established.",
|
|
40
|
+
"evidence_ids": ["E-01"],
|
|
41
|
+
"cause_ids": [],
|
|
42
|
+
"blockers": [
|
|
43
|
+
{
|
|
44
|
+
"kind": "evidence",
|
|
45
|
+
"missing": "The trace from the reported failing attempt.",
|
|
46
|
+
"owner": null,
|
|
47
|
+
"resume_condition": "A sanitized trace bound to that attempt is available."
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"regression": null,
|
|
52
|
+
"next_step": {
|
|
53
|
+
"target": "evidence",
|
|
54
|
+
"owner": null,
|
|
55
|
+
"action": "Identify the trace owner and obtain the missing observation.",
|
|
56
|
+
"completion_evidence": "A sanitized trace bound to the reported failing attempt."
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Prepare a debug handoff before validation
|
|
2
|
+
|
|
3
|
+
Read the installed field reference at `<plugin-root>/routines/debug/references/handoff.md`
|
|
4
|
+
and JSON Schema at `<plugin-root>/routines/debug/references/debug-handoff.schema.json`
|
|
5
|
+
before assembling the packet. Both are generated from the validator's finite
|
|
6
|
+
model. Every declared field is required, including nullable fields; objects
|
|
7
|
+
reject extra fields. Use `null` for an unknown nullable value and the declared
|
|
8
|
+
empty array where allowed. Do not use an empty string in place of unknown text.
|
|
9
|
+
|
|
10
|
+
The minimal unresolved example at `<plugin-root>/routines/debug/references/minimal-unresolved.json`
|
|
11
|
+
is a synthetic shape example with no runtime proof or repair authority. Read it
|
|
12
|
+
as data. Build the current case from actual observations and their limitations;
|
|
13
|
+
do not submit the example as a real diagnosis or copy its evidence claims.
|
|
14
|
+
|
|
15
|
+
These scalar shapes often cause preventable rejection:
|
|
16
|
+
|
|
17
|
+
| Field | Required shape |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `case_id` | `DBG-[A-Za-z0-9][A-Za-z0-9_-]{2,63}`, matched in full |
|
|
20
|
+
| Snapshot, evidence, hypothesis, check IDs | `S-`, `E-`, `H-`, `C-` followed by 2–4 ASCII digits |
|
|
21
|
+
| `snapshot.commit` | `null` or 40/64 lowercase hexadecimal digits |
|
|
22
|
+
| `snapshot.fingerprint` | `null` or `sha256:` followed by 64 lowercase hexadecimal digits |
|
|
23
|
+
| `snapshot.fingerprint_recipe` | `null` or `[a-z][a-z0-9-]{0,63}/[1-9][0-9]{0,3}` |
|
|
24
|
+
| `recipe.preconditions` | One nonblank string, at most 1,000 characters |
|
|
25
|
+
| `check.exit_code` | `null` or a JSON integer, never a boolean |
|
|
26
|
+
|
|
27
|
+
Choose enums from the reference exactly. For example, hypothesis status is
|
|
28
|
+
`supported`, `refuted`, or `inconclusive`; reproduction status is `reproduced`,
|
|
29
|
+
`intermittent`, `not_yet_reproduced`, `unsafe_to_replay`, or `unavailable`.
|
|
30
|
+
Cross-record IDs must resolve to records in this packet. A shape-valid packet
|
|
31
|
+
can still fail the validator's semantic rules. Schema inspection proves
|
|
32
|
+
neither the diagnosis nor the caller's authority.
|
|
33
|
+
|
|
34
|
+
Measure the finished candidate's UTF-8 byte count before the first invocation.
|
|
35
|
+
The limit is 65,536 bytes. Prepare and, if needed, compact the candidate before
|
|
36
|
+
validation, retaining all evidence required by its claims. Then invoke the
|
|
37
|
+
installed helper once as the debug owner directs. A rejected packet stays
|
|
38
|
+
rejected. Keep the bounded error code and field path; do not rewrite and
|
|
39
|
+
resubmit it, retry through another interpreter, or use an alternate validator
|
|
40
|
+
as an acceptance path. Existing logs and observations remain evidence inputs;
|
|
41
|
+
rejection does not authorize an unvalidated repair handoff.
|
|
@@ -168,6 +168,23 @@ cache edit or hook/trust change is permitted. Native context writer/adoption
|
|
|
168
168
|
qualification remains unchanged. Re-run the current-head review and CI
|
|
169
169
|
requirements after integration; previous green evidence does not bind new code.
|
|
170
170
|
|
|
171
|
+
For an explicitly selected per-block resolution, use the same shipped helper
|
|
172
|
+
with `--root "<project-root>" inspect-blocks`, then `--root "<project-root>"
|
|
173
|
+
resolve-blocks --expected <binding> --choices <json-array>`. Review each block
|
|
174
|
+
and supply exactly one `"ours"` or `"theirs"` choice in its displayed order. The
|
|
175
|
+
inspection returns the block count and side hashes; read the conflict document
|
|
176
|
+
to review the actual text. This is a separate operation: `restore-head` keeps
|
|
177
|
+
its original single-block exact-HEAD contract.
|
|
178
|
+
|
|
179
|
+
The explicit resolver binds the actual current merge and all three index
|
|
180
|
+
stages, and requires Git to reproduce the complete worktree rendering in merge,
|
|
181
|
+
diff3 or zdiff3 form. It supports at most 32 blocks and preserves every
|
|
182
|
+
unconflicted byte, including incoming rows. Changed or foreign worktree text,
|
|
183
|
+
native/uncertain ownership, malformed choices and unsupported renderings refuse
|
|
184
|
+
without staging or changing other files. It leaves every index entry untouched;
|
|
185
|
+
review the result and stage through the existing lane. The same interpreter,
|
|
186
|
+
contributor-source, native-qualification and no-bypass requirements above apply.
|
|
187
|
+
|
|
171
188
|
### Open-PR procedure
|
|
172
189
|
|
|
173
190
|
Run this procedure only for the selected open-PR or merge-via-PR action, after the
|
|
@@ -114,6 +114,31 @@ Run an **incremental re-scout for each affected document**, scoped to its select
|
|
|
114
114
|
- **Claim still holds → silently re-baseline only that document's provenance record.** Call `_provenancelib.rebaseline(doc_provenance, selected_current_hashes)` where `selected_current_hashes` contains current hashes only for the selected staged paths whose claims in that document were inspected and still hold. Do not re-baseline a path whose claim changed or was not inspected. Stage only `.codearbiter/.provenance/<doc>.json` by explicit path so the re-baselined record rides THIS commit (ADR-0008 ride-along pattern — nothing is surfaced to the user). After staging, re-run the secrets scan from `tech-stack.md` over the newly-staged path(s) — any file staged after Phase 4 must still pass the automated secrets scan before the commit proceeds. A missing current hash (deletion or rename) cannot be silently re-baselined; surface it in Phase 6 for review.
|
|
115
115
|
- **Claim changed → route to the existing Phase 6 diff-review.** Propose only the affected doc or code-map edit required to reflect its changed claim. The user reviews that edit in the normal diff, and Phase 7 stages approved paths explicitly; do not silently rewrite or add unrelated documentation.
|
|
116
116
|
|
|
117
|
+
For reviewed legacy code-map metadata, use the shipped fixed-target helper at
|
|
118
|
+
`<plugin-root>/hooks/resolve-context-conflict.py`. Run `--root "<project-root>"
|
|
119
|
+
inspect-claims`, then `--root "<project-root>" refresh-claims --expected <binding>
|
|
120
|
+
--claims <json-array>` with one to four reviewed records containing exactly
|
|
121
|
+
`field`, `before`, and `after`. The supported existing fields are `ca_version`,
|
|
122
|
+
`codex_version`, `pi_version`, and `metadata_date`. Use the inspection's exact
|
|
123
|
+
current claim as `before`; a version's `after` must match its fixed source
|
|
124
|
+
manifest, and the date must match `inspection_date`. Select the interpreter once
|
|
125
|
+
by presence and use it for both calls; never retry a failed operation under
|
|
126
|
+
another interpreter.
|
|
127
|
+
|
|
128
|
+
This operation requires resolved, tracked legacy schema-1 document/provenance
|
|
129
|
+
and no native or uncertain journal ownership. It binds document, provenance,
|
|
130
|
+
Git/index, source manifest bytes and date, preserves all unselected bytes, and
|
|
131
|
+
does not stage, rebaseline, adopt, enroll or create a native receipt. Inspect the
|
|
132
|
+
worktree diff for `.codearbiter/code-map.md`, selectively stage that explicit
|
|
133
|
+
path, and re-run the normal secrets scan over the newly staged Markdown before
|
|
134
|
+
Phase 6. Treat it as a heal-proposed doc/code-map edit in Phase 6 review, never as
|
|
135
|
+
a silent rebaseline, and retain its explicit inclusion in Phase 7. Provenance
|
|
136
|
+
rebaseline remains separately scoped by Phase 5.5. Unsupported
|
|
137
|
+
claims or refused preimages stop the edit; do not substitute a direct write or
|
|
138
|
+
native adoption. A codeArbiter contributor may exercise the tested and
|
|
139
|
+
independently reviewed canonical helper under `core/pysrc/` with live enforcement
|
|
140
|
+
intact, never a copied script, installed-cache edit or hook/trust change.
|
|
141
|
+
|
|
117
142
|
## Phase 6 — Diff review · gate: BLOCK
|
|
118
143
|
|
|
119
144
|
Read the complete staged diff (`git diff --cached`). Flag as blocking:
|
|
@@ -241,6 +241,17 @@ resume condition when applicable. Distinguish an executed check from a proposed
|
|
|
241
241
|
command and a recommended handoff from a completed operation. Keep the actual
|
|
242
242
|
diagnosis-only or repair-requested caller scope visible throughout.
|
|
243
243
|
|
|
244
|
+
Before assembling a handoff or invoking its validator, read the installed
|
|
245
|
+
packet preparation guide at `<plugin-root>/routines/debug/references/preparation.md`,
|
|
246
|
+
field reference at `<plugin-root>/routines/debug/references/handoff.md`, and
|
|
247
|
+
JSON Schema at `<plugin-root>/routines/debug/references/debug-handoff.schema.json`.
|
|
248
|
+
Use the linked minimal unresolved example to learn the closed field shapes;
|
|
249
|
+
its synthetic observations are never evidence for the current case. In
|
|
250
|
+
particular, `case_id` must match `DBG-[A-Za-z0-9][A-Za-z0-9_-]{2,63}`.
|
|
251
|
+
Resolve these references from the loaded trusted package. If they are absent,
|
|
252
|
+
stop validated transfer and report the missing resource; do not guess a shape
|
|
253
|
+
or use the one-shot validator to discover fields by trial and error.
|
|
254
|
+
|
|
244
255
|
For a handoff, assemble the transient `codearbiter.debug-handoff/1.0.0` packet
|
|
245
256
|
from the observations actually in hand. Preserve symptom, expectation and its
|
|
246
257
|
citations, reproduction state and limitations, evidence observations and
|
|
@@ -27,12 +27,50 @@ An initialized refresh is routed through `/ca-status drift` and the existing
|
|
|
27
27
|
`context-check` owner after an explicit scoped or full selection; `--brownfield`
|
|
28
28
|
keeps its initial-population lock.
|
|
29
29
|
|
|
30
|
+
### Brownfield host prerequisites
|
|
31
|
+
|
|
32
|
+
For repositories with existing source, every scaffold and populator step in
|
|
33
|
+
the Procedure below is conditional on observing these prerequisites first.
|
|
34
|
+
This applies to both `--brownfield` and the default auto-detection route.
|
|
35
|
+
When qualification is unavailable or unknown, stop here and issue the handoff
|
|
36
|
+
described below. Do not execute the later Procedure steps or enter brownfield
|
|
37
|
+
population through the When NOT to use section.
|
|
38
|
+
|
|
39
|
+
Scaffolding enables governance and may install Git hooks before population.
|
|
40
|
+
It does not qualify the host for context extraction. Before selecting
|
|
41
|
+
`--brownfield` or automatically routing existing source to brownfield
|
|
42
|
+
population, inspect the installed
|
|
43
|
+
context-creation containment profile in
|
|
44
|
+
`<plugin-root>/routines/context-creation/SKILL.md`, under that exact heading.
|
|
45
|
+
The active host needs an observed context-specific child profile with enforced
|
|
46
|
+
read-only containment and report delivery, plus the existing native context
|
|
47
|
+
writer and approval support. Ordinary unrestricted collaboration agents on
|
|
48
|
+
Codex desktop/Windows do not satisfy that requirement. The repository-only
|
|
49
|
+
Claude preparation fixture is neither a shipped consumer route nor proof of
|
|
50
|
+
a qualified host.
|
|
51
|
+
|
|
52
|
+
When these prerequisites are unavailable or unknown, report the missing
|
|
53
|
+
capability before invoking the scaffolder. Keep a fresh repository unactivated
|
|
54
|
+
and retain every byte of any existing partial state. Name the loaded package,
|
|
55
|
+
actual host limitation, selected population route and next missing prerequisite
|
|
56
|
+
in the handoff; leave unobserved qualification and prior operation identities
|
|
57
|
+
unknown. Resume on a host only after fresh containment and writer qualification,
|
|
58
|
+
using the existing interrupted-population recovery procedure. No supported
|
|
59
|
+
destination is implied by this guidance.
|
|
60
|
+
|
|
61
|
+
This is a workflow preflight. The low-level scaffolder has no native scout
|
|
62
|
+
capability attestation input and does not enforce or prove this assessment.
|
|
63
|
+
Its successful exit, `--check` inventory or enabled stub never establishes
|
|
64
|
+
population readiness. Do not fabricate a profile, receipt, context document
|
|
65
|
+
or initialization sentinel to pass the later gates.
|
|
66
|
+
|
|
30
67
|
The two flags are mutually exclusive and neither may combine with `--check`. `--stage N` may
|
|
31
|
-
accompany one only while `.codearbiter/CONTEXT.md` is absent:
|
|
68
|
+
accompany one only while `.codearbiter/CONTEXT.md` is absent: after any required brownfield preflight,
|
|
69
|
+
scaffold at that stage, then enter the
|
|
32
70
|
selected workflow. When an uninitialized stub already exists, skip the refusing scaffolder and enter
|
|
33
71
|
the selected workflow directly. An initialized marker or source-shape mismatch retains the selected
|
|
34
72
|
legacy workflow's BLOCK. Without an explicit strategy, continue with the unchanged auto-detection
|
|
35
|
-
procedure below.
|
|
73
|
+
procedure below only after any required brownfield preflight.
|
|
36
74
|
<!-- catalog-command-modes:end -->
|
|
37
75
|
|
|
38
76
|
## Procedure
|
|
@@ -168,6 +168,23 @@ cache edit or hook/trust change is permitted. Native context writer/adoption
|
|
|
168
168
|
qualification remains unchanged. Re-run the current-head review and CI
|
|
169
169
|
requirements after integration; previous green evidence does not bind new code.
|
|
170
170
|
|
|
171
|
+
For an explicitly selected per-block resolution, use the same shipped helper
|
|
172
|
+
with `--root "<project-root>" inspect-blocks`, then `--root "<project-root>"
|
|
173
|
+
resolve-blocks --expected <binding> --choices <json-array>`. Review each block
|
|
174
|
+
and supply exactly one `"ours"` or `"theirs"` choice in its displayed order. The
|
|
175
|
+
inspection returns the block count and side hashes; read the conflict document
|
|
176
|
+
to review the actual text. This is a separate operation: `restore-head` keeps
|
|
177
|
+
its original single-block exact-HEAD contract.
|
|
178
|
+
|
|
179
|
+
The explicit resolver binds the actual current merge and all three index
|
|
180
|
+
stages, and requires Git to reproduce the complete worktree rendering in merge,
|
|
181
|
+
diff3 or zdiff3 form. It supports at most 32 blocks and preserves every
|
|
182
|
+
unconflicted byte, including incoming rows. Changed or foreign worktree text,
|
|
183
|
+
native/uncertain ownership, malformed choices and unsupported renderings refuse
|
|
184
|
+
without staging or changing other files. It leaves every index entry untouched;
|
|
185
|
+
review the result and stage through the existing lane. The same interpreter,
|
|
186
|
+
contributor-source, native-qualification and no-bypass requirements above apply.
|
|
187
|
+
|
|
171
188
|
### Open-PR procedure
|
|
172
189
|
|
|
173
190
|
Run this procedure only for the selected open-PR or merge-via-PR action, after the
|