@hunter-harness/workflow-harness 0.2.77 → 0.2.79
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/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +17 -2
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +16 -6
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/general/claude-code/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +459 -46
- package/harness/bundles/general/claude-code/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/general/claude-code/scripts/harness_context.py +4 -0
- package/harness/bundles/general/claude-code/scripts/harness_fixback.py +102 -3
- package/harness/bundles/general/claude-code/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +4 -0
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +82 -4
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +17 -2
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +15 -5
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/general/codebuddy/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +459 -46
- package/harness/bundles/general/codebuddy/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +4 -0
- package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +102 -3
- package/harness/bundles/general/codebuddy/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +4 -0
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +82 -4
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +17 -2
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +15 -5
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/general/codex/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +459 -46
- package/harness/bundles/general/codex/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/general/codex/scripts/harness_context.py +4 -0
- package/harness/bundles/general/codex/scripts/harness_fixback.py +102 -3
- package/harness/bundles/general/codex/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +4 -0
- package/harness/bundles/general/codex/scripts/harness_runtime.py +82 -4
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +17 -2
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +15 -5
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/general/cursor/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +459 -46
- package/harness/bundles/general/cursor/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/general/cursor/scripts/harness_context.py +4 -0
- package/harness/bundles/general/cursor/scripts/harness_fixback.py +102 -3
- package/harness/bundles/general/cursor/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +4 -0
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +82 -4
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +17 -2
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +16 -6
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/java/claude-code/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +459 -46
- package/harness/bundles/java/claude-code/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/java/claude-code/scripts/harness_context.py +4 -0
- package/harness/bundles/java/claude-code/scripts/harness_fixback.py +102 -3
- package/harness/bundles/java/claude-code/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +4 -0
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +82 -4
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +17 -2
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +15 -5
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/java/codebuddy/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +459 -46
- package/harness/bundles/java/codebuddy/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +4 -0
- package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +102 -3
- package/harness/bundles/java/codebuddy/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +4 -0
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +82 -4
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +17 -2
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +15 -5
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/java/codex/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +459 -46
- package/harness/bundles/java/codex/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/java/codex/scripts/harness_context.py +4 -0
- package/harness/bundles/java/codex/scripts/harness_fixback.py +102 -3
- package/harness/bundles/java/codex/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +4 -0
- package/harness/bundles/java/codex/scripts/harness_runtime.py +82 -4
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +17 -2
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +15 -5
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/java/cursor/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +459 -46
- package/harness/bundles/java/cursor/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/java/cursor/scripts/harness_context.py +4 -0
- package/harness/bundles/java/cursor/scripts/harness_fixback.py +102 -3
- package/harness/bundles/java/cursor/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +4 -0
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +82 -4
- package/harness/manifests/general/claude-code.json +27 -23
- package/harness/manifests/general/codebuddy.json +27 -23
- package/harness/manifests/general/codex.json +27 -23
- package/harness/manifests/general/cursor.json +27 -23
- package/harness/manifests/java/claude-code.json +29 -25
- package/harness/manifests/java/codebuddy.json +29 -25
- package/harness/manifests/java/codex.json +29 -25
- package/harness/manifests/java/cursor.json +29 -25
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
|
@@ -91,6 +91,7 @@ import harness_events as he # noqa: E402
|
|
|
91
91
|
import harness_events_sync as hes # noqa: E402
|
|
92
92
|
import harness_efficiency as heff # noqa: E402
|
|
93
93
|
import harness_gate as hgate # noqa: E402
|
|
94
|
+
import harness_knowledge_candidates as hkc # noqa: E402
|
|
94
95
|
import harness_ledger as hl # noqa: E402
|
|
95
96
|
import harness_paths as hp # noqa: E402
|
|
96
97
|
import harness_phase as hphase # noqa: E402
|
|
@@ -258,9 +259,13 @@ def append_event(
|
|
|
258
259
|
def generate_manifest(root: Path, output_path: Path) -> dict[str, Any]:
|
|
259
260
|
"""Build path/size/sha256 manifest; exclude the output file itself."""
|
|
260
261
|
root = root.resolve()
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
# Compare by relative path, not by resolve() per file: on Windows every
|
|
263
|
+
# resolve() is a realpath syscall, and this loop runs over the whole tree.
|
|
264
|
+
exclude_rel: str | None = None
|
|
265
|
+
try:
|
|
266
|
+
exclude_rel = output_path.resolve().relative_to(root).as_posix()
|
|
267
|
+
except (OSError, ValueError):
|
|
268
|
+
exclude_rel = None
|
|
264
269
|
|
|
265
270
|
files: list[dict[str, Any]] = []
|
|
266
271
|
total_bytes = 0
|
|
@@ -269,9 +274,9 @@ def generate_manifest(root: Path, output_path: Path) -> dict[str, Any]:
|
|
|
269
274
|
continue
|
|
270
275
|
if path.name.endswith(".lock"):
|
|
271
276
|
continue
|
|
272
|
-
if exclude is not None and path.resolve() == exclude:
|
|
273
|
-
continue
|
|
274
277
|
rel = path.relative_to(root).as_posix()
|
|
278
|
+
if exclude_rel is not None and rel == exclude_rel:
|
|
279
|
+
continue
|
|
275
280
|
size = path.stat().st_size
|
|
276
281
|
total_bytes += size
|
|
277
282
|
files.append(
|
|
@@ -2112,6 +2117,56 @@ def check_archive_exact_byte_policy(project_root: Path) -> dict[str, Any]:
|
|
|
2112
2117
|
}
|
|
2113
2118
|
|
|
2114
2119
|
|
|
2120
|
+
SENSITIVE_SCAN_POLICY_ENV = "HUNTER_HARNESS_SENSITIVE_SCAN"
|
|
2121
|
+
DEFAULT_SENSITIVE_SCAN_POLICY = "warn"
|
|
2122
|
+
|
|
2123
|
+
|
|
2124
|
+
def resolve_sensitive_scan_policy(env: dict[str, str] | None = None) -> str:
|
|
2125
|
+
"""Publication-gate policy: ``off`` | ``warn`` | ``block`` (default warn).
|
|
2126
|
+
|
|
2127
|
+
Mirrors resolveSensitiveScanPolicy() in packages/core so the archive gate
|
|
2128
|
+
and the push gate answer to the same switch.
|
|
2129
|
+
"""
|
|
2130
|
+
source = os.environ if env is None else env
|
|
2131
|
+
raw = str(source.get(SENSITIVE_SCAN_POLICY_ENV) or "").strip().lower()
|
|
2132
|
+
if raw in {"off", "false", "0", "disabled"}:
|
|
2133
|
+
return "off"
|
|
2134
|
+
if raw in {"warn", "advisory"}:
|
|
2135
|
+
return "warn"
|
|
2136
|
+
if raw in {"block", "true", "1", "enforce"}:
|
|
2137
|
+
return "block"
|
|
2138
|
+
return DEFAULT_SENSITIVE_SCAN_POLICY
|
|
2139
|
+
|
|
2140
|
+
|
|
2141
|
+
def _advisory_sensitive_gate(result: dict[str, Any], policy: str) -> dict[str, Any]:
|
|
2142
|
+
"""Downgrade a failed gate to advisory under a non-blocking policy.
|
|
2143
|
+
|
|
2144
|
+
The facts survive verbatim under ``advisory`` so the archive report still
|
|
2145
|
+
names every plaintext file; only the veto is dropped. A fail-closed gate
|
|
2146
|
+
with no proportionate setting gets worked around rather than respected.
|
|
2147
|
+
"""
|
|
2148
|
+
if result.get("ok") or policy == "block":
|
|
2149
|
+
return result
|
|
2150
|
+
return {
|
|
2151
|
+
"ok": True,
|
|
2152
|
+
"reasonCode": "SENSITIVE_EVIDENCE_ADVISORY",
|
|
2153
|
+
"policy": policy,
|
|
2154
|
+
"receiptPath": result.get("receiptPath"),
|
|
2155
|
+
"unresolvedFailures": [],
|
|
2156
|
+
"advisory": {
|
|
2157
|
+
key: value
|
|
2158
|
+
for key, value in result.items()
|
|
2159
|
+
if key not in {"ok", "policy"}
|
|
2160
|
+
},
|
|
2161
|
+
"nextAction": (
|
|
2162
|
+
"Findings are advisory under "
|
|
2163
|
+
f"{SENSITIVE_SCAN_POLICY_ENV}={policy}; set it to `block` to veto "
|
|
2164
|
+
"publication, or quarantine with `harness_runtime.py "
|
|
2165
|
+
"quarantine-evidence`."
|
|
2166
|
+
),
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
|
|
2115
2170
|
def validate_sensitive_evidence_publication_gate(
|
|
2116
2171
|
change_dir: Path,
|
|
2117
2172
|
*,
|
|
@@ -2128,24 +2183,42 @@ def validate_sensitive_evidence_publication_gate(
|
|
|
2128
2183
|
"""
|
|
2129
2184
|
change_dir = change_dir.expanduser().resolve()
|
|
2130
2185
|
copy_root = (copy_root or change_dir).expanduser().resolve()
|
|
2186
|
+
policy = resolve_sensitive_scan_policy()
|
|
2131
2187
|
receipt_path = hruntime.sensitive_evidence_receipt_path(change_dir)
|
|
2132
|
-
|
|
2133
|
-
|
|
2188
|
+
if policy == "off":
|
|
2189
|
+
# Skipping the scan also skips a full-tree byte read of the change dir.
|
|
2134
2190
|
return {
|
|
2135
|
-
"ok":
|
|
2136
|
-
"reasonCode": "
|
|
2191
|
+
"ok": True,
|
|
2192
|
+
"reasonCode": "SECRET_SCAN_DISABLED",
|
|
2193
|
+
"policy": policy,
|
|
2137
2194
|
"receiptPath": str(receipt_path),
|
|
2138
|
-
"unresolvedFailures":
|
|
2139
|
-
"nextAction": "Quarantine the original bytes before archive publication.",
|
|
2195
|
+
"unresolvedFailures": [],
|
|
2140
2196
|
}
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2197
|
+
candidates = hruntime.sensitive_evidence_candidates(change_dir)
|
|
2198
|
+
if candidates:
|
|
2199
|
+
return _advisory_sensitive_gate(
|
|
2200
|
+
{
|
|
2144
2201
|
"ok": False,
|
|
2145
|
-
"reasonCode": "
|
|
2202
|
+
"reasonCode": "SENSITIVE_EVIDENCE_UNQUARANTINED",
|
|
2146
2203
|
"receiptPath": str(receipt_path),
|
|
2147
|
-
"unresolvedFailures":
|
|
2148
|
-
|
|
2204
|
+
"unresolvedFailures": candidates,
|
|
2205
|
+
"nextAction": (
|
|
2206
|
+
"Quarantine the original bytes before archive publication."
|
|
2207
|
+
),
|
|
2208
|
+
},
|
|
2209
|
+
policy,
|
|
2210
|
+
)
|
|
2211
|
+
if receipt_override is None and not receipt_path.is_file():
|
|
2212
|
+
if require_receipt:
|
|
2213
|
+
return _advisory_sensitive_gate(
|
|
2214
|
+
{
|
|
2215
|
+
"ok": False,
|
|
2216
|
+
"reasonCode": "SECRET_SCAN_RECEIPT_MISSING",
|
|
2217
|
+
"receiptPath": str(receipt_path),
|
|
2218
|
+
"unresolvedFailures": [],
|
|
2219
|
+
},
|
|
2220
|
+
policy,
|
|
2221
|
+
)
|
|
2149
2222
|
return {
|
|
2150
2223
|
"ok": True,
|
|
2151
2224
|
"reasonCode": "SECRET_SCAN_NOT_APPLICABLE",
|
|
@@ -2157,21 +2230,27 @@ def validate_sensitive_evidence_publication_gate(
|
|
|
2157
2230
|
try:
|
|
2158
2231
|
receipt = read_json(receipt_path)
|
|
2159
2232
|
except (OSError, json.JSONDecodeError, TypeError) as exc:
|
|
2160
|
-
return
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2233
|
+
return _advisory_sensitive_gate(
|
|
2234
|
+
{
|
|
2235
|
+
"ok": False,
|
|
2236
|
+
"reasonCode": "SECRET_SCAN_RECEIPT_INVALID",
|
|
2237
|
+
"receiptPath": str(receipt_path),
|
|
2238
|
+
"error": str(exc),
|
|
2239
|
+
},
|
|
2240
|
+
policy,
|
|
2241
|
+
)
|
|
2166
2242
|
else:
|
|
2167
2243
|
receipt = receipt_override
|
|
2168
2244
|
if not isinstance(receipt, dict):
|
|
2169
|
-
return
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2245
|
+
return _advisory_sensitive_gate(
|
|
2246
|
+
{
|
|
2247
|
+
"ok": False,
|
|
2248
|
+
"reasonCode": "SECRET_SCAN_RECEIPT_INVALID",
|
|
2249
|
+
"receiptPath": str(receipt_path),
|
|
2250
|
+
"error": "receipt must be an object",
|
|
2251
|
+
},
|
|
2252
|
+
policy,
|
|
2253
|
+
)
|
|
2175
2254
|
issues: list[dict[str, Any]] = []
|
|
2176
2255
|
if receipt.get("schemaVersion") != 1:
|
|
2177
2256
|
issues.append({"code": "SECRET_SCAN_SCHEMA_INVALID"})
|
|
@@ -2231,14 +2310,19 @@ def validate_sensitive_evidence_publication_gate(
|
|
|
2231
2310
|
"code": "SENSITIVE_EVIDENCE_SOURCE_REMAINS",
|
|
2232
2311
|
"sourcePath": source_raw,
|
|
2233
2312
|
})
|
|
2234
|
-
return
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2313
|
+
return _advisory_sensitive_gate(
|
|
2314
|
+
{
|
|
2315
|
+
"ok": not issues,
|
|
2316
|
+
"reasonCode": (
|
|
2317
|
+
"SECRET_SCAN_GATE_SATISFIED" if not issues else "SECRET_SCAN_GATE_BLOCKED"
|
|
2318
|
+
),
|
|
2319
|
+
"receiptPath": str(receipt_path),
|
|
2320
|
+
"receipt": receipt,
|
|
2321
|
+
"issues": issues,
|
|
2322
|
+
"treeDigest": actual_digest,
|
|
2323
|
+
},
|
|
2324
|
+
policy,
|
|
2325
|
+
)
|
|
2242
2326
|
|
|
2243
2327
|
|
|
2244
2328
|
# ---------------------------------------------------------------------------
|
|
@@ -4173,6 +4257,33 @@ def write_archive_meta(work_dir: Path, summary: dict[str, Any]) -> Path:
|
|
|
4173
4257
|
return out
|
|
4174
4258
|
|
|
4175
4259
|
|
|
4260
|
+
def write_knowledge_candidates(work_dir: Path, summary: dict[str, Any]) -> Path:
|
|
4261
|
+
"""Generate candidates/knowledge.json from summary-data (single ownership).
|
|
4262
|
+
|
|
4263
|
+
Mirrors write_archive_meta: derived from the same summary, written before the
|
|
4264
|
+
after-manifest so its bytes are covered. The archive directory name is the
|
|
4265
|
+
archive id, matching write_archive_meta's `archive-id` field.
|
|
4266
|
+
"""
|
|
4267
|
+
archive_id = work_dir.name
|
|
4268
|
+
candidates = hkc.build_knowledge_candidates(
|
|
4269
|
+
summary,
|
|
4270
|
+
change_key=str(summary.get("changeName") or archive_id),
|
|
4271
|
+
archive_id=archive_id,
|
|
4272
|
+
# The archive schema version identifies the producing format; there is
|
|
4273
|
+
# no separate harness version constant to borrow here.
|
|
4274
|
+
producer_version=SCHEMA_VERSION,
|
|
4275
|
+
created_at=now_iso(),
|
|
4276
|
+
)
|
|
4277
|
+
out = work_dir / "candidates" / "knowledge.json"
|
|
4278
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
4279
|
+
out.write_text(
|
|
4280
|
+
hkc.render_knowledge_candidates_json(candidates),
|
|
4281
|
+
encoding="utf-8",
|
|
4282
|
+
newline="\n",
|
|
4283
|
+
)
|
|
4284
|
+
return out
|
|
4285
|
+
|
|
4286
|
+
|
|
4176
4287
|
|
|
4177
4288
|
|
|
4178
4289
|
def _changed_files_from_git(
|
|
@@ -8269,6 +8380,21 @@ def cmd_finalize(
|
|
|
8269
8380
|
warnings.append(f"archive-meta write failed: {exc}")
|
|
8270
8381
|
payload["steps"]["archive_meta"] = {"ok": False, "error": str(exc)}
|
|
8271
8382
|
|
|
8383
|
+
# --- 8b. knowledge candidates (also before the after-manifest) ---
|
|
8384
|
+
# Soft-fail like archive-meta: an archive must never be rolled back because
|
|
8385
|
+
# knowledge extraction found nothing. An empty array is a valid outcome.
|
|
8386
|
+
try:
|
|
8387
|
+
summary = read_json(summary_path)
|
|
8388
|
+
candidates_path = write_knowledge_candidates(work_dir, summary)
|
|
8389
|
+
payload["steps"]["knowledge_candidates"] = {
|
|
8390
|
+
"ok": True,
|
|
8391
|
+
"path": str(candidates_path),
|
|
8392
|
+
"count": len(read_json(candidates_path)),
|
|
8393
|
+
}
|
|
8394
|
+
except Exception as exc: # noqa: BLE001 — candidates soft-fail
|
|
8395
|
+
warnings.append(f"knowledge candidates write failed: {exc}")
|
|
8396
|
+
payload["steps"]["knowledge_candidates"] = {"ok": False, "error": str(exc)}
|
|
8397
|
+
|
|
8272
8398
|
# --- 9/10. final summary stats, then LAST manifest (IA-7) ---
|
|
8273
8399
|
# Post-manifest rewrites of covered bytes are forbidden. We update the
|
|
8274
8400
|
# summary first, regenerate after-manifest last, then verify on-disk hashes.
|
|
@@ -8296,14 +8422,16 @@ def cmd_finalize(
|
|
|
8296
8422
|
if before_in_archive.is_file():
|
|
8297
8423
|
before_manifest = read_json(before_in_archive)
|
|
8298
8424
|
compare_result = compare_manifests(before_manifest, after_manifest)
|
|
8299
|
-
coverage = verify_manifest_byte_coverage(work_dir, after_manifest)
|
|
8300
8425
|
# Embed compare stats into summary AFTER manifest → must exclude those paths.
|
|
8426
|
+
# Only one coverage pass runs: an earlier build verified here as well,
|
|
8427
|
+
# but that result was overwritten a few lines below by the pass that
|
|
8428
|
+
# excludes summary-data.json, so it only cost a full extra tree hash.
|
|
8301
8429
|
summary = read_json(summary_path)
|
|
8302
8430
|
summary["archiveManifest"] = {
|
|
8303
8431
|
"movedFiles": compare_result.get("movedFiles", 0),
|
|
8304
8432
|
"generatedFiles": compare_result.get("generatedFiles", 0),
|
|
8305
8433
|
"totalArchiveFiles": compare_result.get("totalArchiveFiles", 0),
|
|
8306
|
-
"checksumStatus":
|
|
8434
|
+
"checksumStatus": "PENDING",
|
|
8307
8435
|
"exclusionReasons": {
|
|
8308
8436
|
"reports/final/summary-data.json": (
|
|
8309
8437
|
"archiveManifest stats written after coverage snapshot"
|
|
@@ -8577,6 +8705,7 @@ def cmd_finalize(
|
|
|
8577
8705
|
"archiveId",
|
|
8578
8706
|
"archiveStatus",
|
|
8579
8707
|
"knowledgeStatus",
|
|
8708
|
+
"knowledgeCandidateCount",
|
|
8580
8709
|
"uploadStatus",
|
|
8581
8710
|
"packageSha256",
|
|
8582
8711
|
"manifestSha256",
|
|
@@ -8885,6 +9014,12 @@ def _archive_core_file_specs(
|
|
|
8885
9014
|
file_specs.append(
|
|
8886
9015
|
(source, f"{folder}/{relative}", role, "text/markdown")
|
|
8887
9016
|
)
|
|
9017
|
+
add_if_file(
|
|
9018
|
+
archive / "candidates" / "knowledge.json",
|
|
9019
|
+
"candidates/knowledge.json",
|
|
9020
|
+
"knowledge_candidates",
|
|
9021
|
+
"application/json",
|
|
9022
|
+
)
|
|
8888
9023
|
add_if_file(
|
|
8889
9024
|
archive / "meta" / "archive-meta.md",
|
|
8890
9025
|
"archive-meta.md",
|
|
@@ -8913,6 +9048,8 @@ def _archive_source_identity(project_root: Path) -> dict[str, str | None]:
|
|
|
8913
9048
|
["git", *args],
|
|
8914
9049
|
cwd=str(project_root),
|
|
8915
9050
|
text=True,
|
|
9051
|
+
encoding="utf-8",
|
|
9052
|
+
errors="replace",
|
|
8916
9053
|
stdout=subprocess.PIPE,
|
|
8917
9054
|
stderr=subprocess.DEVNULL,
|
|
8918
9055
|
timeout=10,
|
|
@@ -9002,14 +9139,50 @@ def resolve_npx_launcher() -> list[str]:
|
|
|
9002
9139
|
return [node, str(npx_cli)]
|
|
9003
9140
|
|
|
9004
9141
|
|
|
9142
|
+
def _installed_cli_entry(project_root: Path) -> Path | None:
|
|
9143
|
+
"""Find an installed hunter-harness entry script, walking up from the project.
|
|
9144
|
+
|
|
9145
|
+
npx costs a full npm resolution (and a registry round-trip when the package
|
|
9146
|
+
is not a local dependency) on every call, and archive publishes it twice.
|
|
9147
|
+
Running the entry script under node directly skips all of that.
|
|
9148
|
+
"""
|
|
9149
|
+
seen: set[Path] = set()
|
|
9150
|
+
for base in (project_root, *project_root.parents):
|
|
9151
|
+
if base in seen:
|
|
9152
|
+
continue
|
|
9153
|
+
seen.add(base)
|
|
9154
|
+
candidate = base / "node_modules" / "hunter-harness" / "dist" / "bin.js"
|
|
9155
|
+
if candidate.is_file():
|
|
9156
|
+
return candidate
|
|
9157
|
+
return None
|
|
9158
|
+
|
|
9159
|
+
|
|
9160
|
+
def resolve_hunter_cli_command(project_root: Path) -> list[str]:
|
|
9161
|
+
"""Command prefix that runs the hunter-harness CLI, cheapest route first."""
|
|
9162
|
+
node = shutil.which("node")
|
|
9163
|
+
if node:
|
|
9164
|
+
entry = _installed_cli_entry(project_root)
|
|
9165
|
+
if entry is not None:
|
|
9166
|
+
return [node, str(entry)]
|
|
9167
|
+
return [*resolve_npx_launcher(), "--yes", "hunter-harness"]
|
|
9168
|
+
|
|
9169
|
+
|
|
9005
9170
|
def build_archive_package(
|
|
9006
9171
|
project_root: Path,
|
|
9007
9172
|
archive_dir: Path,
|
|
9008
9173
|
change_key: str,
|
|
9009
9174
|
*,
|
|
9010
9175
|
output_path: Path | None = None,
|
|
9176
|
+
extra_entries: dict[str, tuple[str, str, bytes]] | None = None,
|
|
9011
9177
|
) -> dict[str, Any]:
|
|
9012
|
-
"""Build the deterministic core-v1 ZIP uploaded to hunter-platform.
|
|
9178
|
+
"""Build the deterministic core-v1 ZIP uploaded to hunter-platform.
|
|
9179
|
+
|
|
9180
|
+
``extra_entries`` maps a package path to ``(role, media_type, bytes)`` for
|
|
9181
|
+
content that is *not* on disk in the archive. Republishing an older archive
|
|
9182
|
+
uses it to supply knowledge candidates that predate the candidates step,
|
|
9183
|
+
without writing into a sealed archive directory whose manifest already
|
|
9184
|
+
covers every byte in it.
|
|
9185
|
+
"""
|
|
9013
9186
|
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,159}", change_key) is None:
|
|
9014
9187
|
raise ValueError("change_key must be a portable path segment")
|
|
9015
9188
|
project_root, archive_dir, file_specs = _archive_core_file_specs(
|
|
@@ -9019,8 +9192,29 @@ def build_archive_package(
|
|
|
9019
9192
|
if not any(spec[1] == "reports/final/summary-data.json" for spec in file_specs):
|
|
9020
9193
|
raise ValueError("archive final summary-data.json is required")
|
|
9021
9194
|
|
|
9195
|
+
on_disk_paths = {spec[1] for spec in file_specs}
|
|
9196
|
+
injected = {
|
|
9197
|
+
package_path: value
|
|
9198
|
+
for package_path, value in (extra_entries or {}).items()
|
|
9199
|
+
if package_path not in on_disk_paths
|
|
9200
|
+
}
|
|
9201
|
+
|
|
9022
9202
|
entries: list[dict[str, Any]] = []
|
|
9023
9203
|
content_by_path: dict[str, bytes] = {}
|
|
9204
|
+
for package_path, (role, media_type, raw) in sorted(injected.items()):
|
|
9205
|
+
raw.decode("utf-8")
|
|
9206
|
+
if media_type == "application/json":
|
|
9207
|
+
raw = _archive_remote_json_bytes(raw)
|
|
9208
|
+
content_by_path[package_path] = raw
|
|
9209
|
+
entries.append(
|
|
9210
|
+
{
|
|
9211
|
+
"path": package_path,
|
|
9212
|
+
"role": role,
|
|
9213
|
+
"media_type": media_type,
|
|
9214
|
+
"content_sha256": "sha256:" + hashlib.sha256(raw).hexdigest(),
|
|
9215
|
+
"size_bytes": len(raw),
|
|
9216
|
+
}
|
|
9217
|
+
)
|
|
9024
9218
|
for source, package_path, role, media_type in sorted(file_specs, key=lambda item: item[1]):
|
|
9025
9219
|
source = _require_archive_project_path(
|
|
9026
9220
|
project_root,
|
|
@@ -9044,6 +9238,7 @@ def build_archive_package(
|
|
|
9044
9238
|
}
|
|
9045
9239
|
)
|
|
9046
9240
|
|
|
9241
|
+
entries.sort(key=lambda item: str(item["path"]))
|
|
9047
9242
|
manifest = {
|
|
9048
9243
|
"schema_version": 1,
|
|
9049
9244
|
"profile": "core-v1",
|
|
@@ -9117,6 +9312,17 @@ def build_archive_package(
|
|
|
9117
9312
|
temporary.unlink(missing_ok=True)
|
|
9118
9313
|
|
|
9119
9314
|
package_raw = output_path.read_bytes()
|
|
9315
|
+
# An upload can report knowledge_status=ready and still add nothing to the
|
|
9316
|
+
# platform when the package carries zero candidates. Counting them here is
|
|
9317
|
+
# what separates "server did not index" from "there was nothing to index".
|
|
9318
|
+
candidate_count = 0
|
|
9319
|
+
candidate_bytes = content_by_path.get("candidates/knowledge.json")
|
|
9320
|
+
if candidate_bytes is not None:
|
|
9321
|
+
try:
|
|
9322
|
+
parsed = json.loads(candidate_bytes)
|
|
9323
|
+
candidate_count = len(parsed) if isinstance(parsed, list) else 0
|
|
9324
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
9325
|
+
candidate_count = 0
|
|
9120
9326
|
return {
|
|
9121
9327
|
"schemaVersion": 1,
|
|
9122
9328
|
"profile": "core-v1",
|
|
@@ -9127,6 +9333,7 @@ def build_archive_package(
|
|
|
9127
9333
|
"fileCount": len(entries),
|
|
9128
9334
|
"sizeBytes": len(package_raw),
|
|
9129
9335
|
"paths": [entry["path"] for entry in entries],
|
|
9336
|
+
"knowledgeCandidateCount": candidate_count,
|
|
9130
9337
|
}
|
|
9131
9338
|
|
|
9132
9339
|
|
|
@@ -9191,7 +9398,7 @@ def auto_push_managed_snapshot(project_root: Path) -> dict[str, Any]:
|
|
|
9191
9398
|
"missingCredentials": credentials["missing"],
|
|
9192
9399
|
}
|
|
9193
9400
|
try:
|
|
9194
|
-
launcher =
|
|
9401
|
+
launcher = resolve_hunter_cli_command(project_root)
|
|
9195
9402
|
except OSError as exc:
|
|
9196
9403
|
return {
|
|
9197
9404
|
"ok": False,
|
|
@@ -9204,8 +9411,6 @@ def auto_push_managed_snapshot(project_root: Path) -> dict[str, Any]:
|
|
|
9204
9411
|
# 规则、架构地图与指令入口。
|
|
9205
9412
|
command = [
|
|
9206
9413
|
*launcher,
|
|
9207
|
-
"--yes",
|
|
9208
|
-
"hunter-harness",
|
|
9209
9414
|
"harness-push",
|
|
9210
9415
|
"--scope",
|
|
9211
9416
|
"config,rules,architecture,instructions,branch_files",
|
|
@@ -9282,6 +9487,7 @@ def auto_push_archive_core(
|
|
|
9282
9487
|
archive_dir: Path,
|
|
9283
9488
|
*,
|
|
9284
9489
|
change_key: str | None = None,
|
|
9490
|
+
extra_entries: dict[str, tuple[str, str, bytes]] | None = None,
|
|
9285
9491
|
) -> dict[str, Any]:
|
|
9286
9492
|
"""Build and upload one deterministic core ZIP after finalize.
|
|
9287
9493
|
|
|
@@ -9296,6 +9502,7 @@ def auto_push_archive_core(
|
|
|
9296
9502
|
project_root,
|
|
9297
9503
|
archive_dir,
|
|
9298
9504
|
effective_change_key,
|
|
9505
|
+
extra_entries=extra_entries,
|
|
9299
9506
|
)
|
|
9300
9507
|
except (OSError, ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
9301
9508
|
return {
|
|
@@ -9345,6 +9552,7 @@ def auto_push_archive_core(
|
|
|
9345
9552
|
"packageSha256": package["packageSha256"],
|
|
9346
9553
|
"manifestSha256": package["manifestSha256"],
|
|
9347
9554
|
"fileCount": package["fileCount"],
|
|
9555
|
+
"knowledgeCandidateCount": package["knowledgeCandidateCount"],
|
|
9348
9556
|
"paths": core_paths,
|
|
9349
9557
|
"uploadStatus": "pending",
|
|
9350
9558
|
"reasonCode": initial_reason_code,
|
|
@@ -9399,6 +9607,7 @@ def auto_push_archive_core(
|
|
|
9399
9607
|
"manifestSha256": package["manifestSha256"],
|
|
9400
9608
|
"fileCount": package["fileCount"],
|
|
9401
9609
|
"sizeBytes": package["sizeBytes"],
|
|
9610
|
+
"knowledgeCandidateCount": package["knowledgeCandidateCount"],
|
|
9402
9611
|
"pending": str(pending_path),
|
|
9403
9612
|
}
|
|
9404
9613
|
|
|
@@ -9415,7 +9624,7 @@ def auto_push_archive_core(
|
|
|
9415
9624
|
# The published CLI owns credentials, project binding, durable receipt checks,
|
|
9416
9625
|
# and retries. Generic `push` is intentionally not used for archive bytes.
|
|
9417
9626
|
try:
|
|
9418
|
-
launcher =
|
|
9627
|
+
launcher = resolve_hunter_cli_command(project_root)
|
|
9419
9628
|
except OSError as exc:
|
|
9420
9629
|
reason_code = "ARCHIVE_UPLOAD_DEFERRED"
|
|
9421
9630
|
try:
|
|
@@ -9429,8 +9638,6 @@ def auto_push_archive_core(
|
|
|
9429
9638
|
}
|
|
9430
9639
|
command = [
|
|
9431
9640
|
*launcher,
|
|
9432
|
-
"--yes",
|
|
9433
|
-
"hunter-harness",
|
|
9434
9641
|
"archive",
|
|
9435
9642
|
"upload",
|
|
9436
9643
|
"--file",
|
|
@@ -9449,6 +9656,10 @@ def auto_push_archive_core(
|
|
|
9449
9656
|
command,
|
|
9450
9657
|
cwd=str(project_root),
|
|
9451
9658
|
text=True,
|
|
9659
|
+
# 不指定 encoding 时中文 Windows 按 cp936 解码 UTF-8,
|
|
9660
|
+
# CLI 的中文回执会被损坏,随后的 json.loads 也可能失败。
|
|
9661
|
+
encoding="utf-8",
|
|
9662
|
+
errors="replace",
|
|
9452
9663
|
stdout=subprocess.PIPE,
|
|
9453
9664
|
stderr=subprocess.PIPE,
|
|
9454
9665
|
timeout=180,
|
|
@@ -10021,6 +10232,186 @@ def cmd_repair_cli(args: argparse.Namespace) -> int:
|
|
|
10021
10232
|
return code
|
|
10022
10233
|
|
|
10023
10234
|
|
|
10235
|
+
def resolve_archive_dir_for_change(
|
|
10236
|
+
project_root: Path,
|
|
10237
|
+
change_key: str,
|
|
10238
|
+
) -> tuple[Path | None, list[str]]:
|
|
10239
|
+
"""Locate the sealed archive directory for a change key.
|
|
10240
|
+
|
|
10241
|
+
Directories are named ``<YYYY-MM-DD>-<change-key>``; older layouts use the
|
|
10242
|
+
bare key. Returns the newest match plus every candidate name so an ambiguous
|
|
10243
|
+
key is reported rather than silently resolved.
|
|
10244
|
+
"""
|
|
10245
|
+
archive_root = project_root / ".harness" / "archive"
|
|
10246
|
+
if not archive_root.is_dir():
|
|
10247
|
+
return None, []
|
|
10248
|
+
matches = sorted(
|
|
10249
|
+
entry.name
|
|
10250
|
+
for entry in archive_root.iterdir()
|
|
10251
|
+
if entry.is_dir()
|
|
10252
|
+
and (entry.name == change_key or entry.name.endswith(f"-{change_key}"))
|
|
10253
|
+
)
|
|
10254
|
+
if not matches:
|
|
10255
|
+
return None, []
|
|
10256
|
+
return archive_root / matches[-1], matches
|
|
10257
|
+
|
|
10258
|
+
|
|
10259
|
+
def _republish_knowledge_entry(archive_dir: Path) -> dict[str, tuple[str, str, bytes]]:
|
|
10260
|
+
"""Knowledge candidates for archives sealed before the candidates step.
|
|
10261
|
+
|
|
10262
|
+
Generated in memory only. The archive directory is sealed: its after-manifest
|
|
10263
|
+
covers every byte in it, so writing a new file there would break byte
|
|
10264
|
+
coverage for every later reader.
|
|
10265
|
+
"""
|
|
10266
|
+
if (archive_dir / "candidates" / "knowledge.json").is_file():
|
|
10267
|
+
return {}
|
|
10268
|
+
summary_path = archive_dir / "reports" / "final" / "summary-data.json"
|
|
10269
|
+
summary = read_json(summary_path)
|
|
10270
|
+
candidates = hkc.build_knowledge_candidates(
|
|
10271
|
+
summary,
|
|
10272
|
+
change_key=str(summary.get("changeName") or archive_dir.name),
|
|
10273
|
+
archive_id=archive_dir.name,
|
|
10274
|
+
producer_version=SCHEMA_VERSION,
|
|
10275
|
+
created_at=now_iso(),
|
|
10276
|
+
)
|
|
10277
|
+
if not candidates:
|
|
10278
|
+
return {}
|
|
10279
|
+
return {
|
|
10280
|
+
"candidates/knowledge.json": (
|
|
10281
|
+
"knowledge_candidates",
|
|
10282
|
+
"application/json",
|
|
10283
|
+
hkc.render_knowledge_candidates_json(candidates).encode("utf-8"),
|
|
10284
|
+
)
|
|
10285
|
+
}
|
|
10286
|
+
|
|
10287
|
+
|
|
10288
|
+
def cmd_republish(
|
|
10289
|
+
*,
|
|
10290
|
+
change_key: str,
|
|
10291
|
+
archive_dir: Path | None,
|
|
10292
|
+
project_root: Path | None,
|
|
10293
|
+
dry_run: bool,
|
|
10294
|
+
) -> tuple[int, dict[str, Any]]:
|
|
10295
|
+
"""Rebuild and re-upload the core ZIP for an already sealed archive.
|
|
10296
|
+
|
|
10297
|
+
The normal publish path deletes the ZIP and its receipt once the server
|
|
10298
|
+
reports durable+ready, and archives produced before the outbox existed never
|
|
10299
|
+
had one, so nothing is left to retry from. Rebuilding from the sealed
|
|
10300
|
+
directory is the only route that can re-deliver an archive, and it is also
|
|
10301
|
+
how archives sealed before knowledge candidates existed get them.
|
|
10302
|
+
"""
|
|
10303
|
+
payload: dict[str, Any] = {
|
|
10304
|
+
"ok": False,
|
|
10305
|
+
"action": "republish",
|
|
10306
|
+
"changeKey": change_key,
|
|
10307
|
+
"dryRun": dry_run,
|
|
10308
|
+
}
|
|
10309
|
+
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,159}", change_key) is None:
|
|
10310
|
+
payload["error"] = "change key must be a portable path segment"
|
|
10311
|
+
payload["reasonCode"] = "ARCHIVE_CHANGE_KEY_INVALID"
|
|
10312
|
+
return 2, payload
|
|
10313
|
+
|
|
10314
|
+
root = (project_root or find_project_root(archive_dir or Path.cwd())).resolve()
|
|
10315
|
+
payload["projectRoot"] = str(root)
|
|
10316
|
+
matches: list[str] = []
|
|
10317
|
+
if archive_dir is None:
|
|
10318
|
+
archive_dir, matches = resolve_archive_dir_for_change(root, change_key)
|
|
10319
|
+
payload["matchedArchives"] = matches
|
|
10320
|
+
if archive_dir is None or not archive_dir.is_dir():
|
|
10321
|
+
payload["error"] = (
|
|
10322
|
+
f"no archive directory for change key {change_key} under "
|
|
10323
|
+
f"{root / '.harness' / 'archive'}"
|
|
10324
|
+
)
|
|
10325
|
+
payload["reasonCode"] = "ARCHIVE_DIR_NOT_FOUND"
|
|
10326
|
+
return 1, payload
|
|
10327
|
+
archive_dir = archive_dir.resolve()
|
|
10328
|
+
payload["archiveDir"] = str(archive_dir)
|
|
10329
|
+
if len(matches) > 1:
|
|
10330
|
+
payload["warning"] = (
|
|
10331
|
+
"多个归档目录匹配该 change key,已选用最新的一个:"
|
|
10332
|
+
+ ", ".join(matches)
|
|
10333
|
+
)
|
|
10334
|
+
|
|
10335
|
+
summary_path = archive_dir / "reports" / "final" / "summary-data.json"
|
|
10336
|
+
if not summary_path.is_file():
|
|
10337
|
+
payload["error"] = f"archive final summary-data.json missing: {summary_path}"
|
|
10338
|
+
payload["reasonCode"] = "ARCHIVE_SUMMARY_MISSING"
|
|
10339
|
+
return 1, payload
|
|
10340
|
+
|
|
10341
|
+
try:
|
|
10342
|
+
extra_entries = _republish_knowledge_entry(archive_dir)
|
|
10343
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
10344
|
+
# Knowledge is additive: never fail a re-delivery over it.
|
|
10345
|
+
payload.setdefault("warnings", []).append(
|
|
10346
|
+
f"knowledge candidate regeneration skipped: {exc}"
|
|
10347
|
+
)
|
|
10348
|
+
extra_entries = {}
|
|
10349
|
+
payload["knowledgeCandidatesInjected"] = "candidates/knowledge.json" in extra_entries
|
|
10350
|
+
|
|
10351
|
+
if dry_run:
|
|
10352
|
+
# The package builder refuses to write outside the project, so the
|
|
10353
|
+
# preview lands beside the real packages under a distinct name and is
|
|
10354
|
+
# removed again; it must never be mistaken for a pending upload.
|
|
10355
|
+
preview_path = (
|
|
10356
|
+
root / ".harness" / "state" / "local" / "archive-packages"
|
|
10357
|
+
/ f"{change_key}.preview.zip"
|
|
10358
|
+
)
|
|
10359
|
+
try:
|
|
10360
|
+
package = build_archive_package(
|
|
10361
|
+
root,
|
|
10362
|
+
archive_dir,
|
|
10363
|
+
change_key,
|
|
10364
|
+
output_path=preview_path,
|
|
10365
|
+
extra_entries=extra_entries,
|
|
10366
|
+
)
|
|
10367
|
+
except (OSError, ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
10368
|
+
payload["error"] = f"archive package build failed: {exc}"
|
|
10369
|
+
payload["reasonCode"] = "ARCHIVE_PACKAGE_BUILD_FAILED"
|
|
10370
|
+
return 1, payload
|
|
10371
|
+
finally:
|
|
10372
|
+
try:
|
|
10373
|
+
preview_path.unlink(missing_ok=True)
|
|
10374
|
+
except OSError:
|
|
10375
|
+
pass
|
|
10376
|
+
payload.update(
|
|
10377
|
+
{
|
|
10378
|
+
"ok": True,
|
|
10379
|
+
"reasonCode": "ARCHIVE_REPUBLISH_PREVIEW",
|
|
10380
|
+
"fileCount": package["fileCount"],
|
|
10381
|
+
"sizeBytes": package["sizeBytes"],
|
|
10382
|
+
"knowledgeCandidateCount": package["knowledgeCandidateCount"],
|
|
10383
|
+
"packageSha256": package["packageSha256"],
|
|
10384
|
+
"manifestSha256": package["manifestSha256"],
|
|
10385
|
+
"files": package["paths"],
|
|
10386
|
+
}
|
|
10387
|
+
)
|
|
10388
|
+
return 0, payload
|
|
10389
|
+
|
|
10390
|
+
result = auto_push_archive_core(
|
|
10391
|
+
root,
|
|
10392
|
+
archive_dir,
|
|
10393
|
+
change_key=change_key,
|
|
10394
|
+
extra_entries=extra_entries,
|
|
10395
|
+
)
|
|
10396
|
+
payload["push"] = result
|
|
10397
|
+
payload["ok"] = bool(result.get("ok"))
|
|
10398
|
+
payload["reasonCode"] = result.get("reasonCode")
|
|
10399
|
+
if result.get("warning"):
|
|
10400
|
+
payload.setdefault("warnings", []).append(str(result["warning"]))
|
|
10401
|
+
return 0 if payload["ok"] else 1, payload
|
|
10402
|
+
|
|
10403
|
+
|
|
10404
|
+
def cmd_republish_cli(args: argparse.Namespace) -> int:
|
|
10405
|
+
code, payload = cmd_republish(
|
|
10406
|
+
change_key=str(args.change).strip(),
|
|
10407
|
+
archive_dir=resolve_path(args.archive_dir) if args.archive_dir else None,
|
|
10408
|
+
project_root=resolve_path(args.project) if args.project else None,
|
|
10409
|
+
dry_run=bool(args.dry_run),
|
|
10410
|
+
)
|
|
10411
|
+
emit_json(payload)
|
|
10412
|
+
return code
|
|
10413
|
+
|
|
10414
|
+
|
|
10024
10415
|
def build_parser() -> argparse.ArgumentParser:
|
|
10025
10416
|
parser = argparse.ArgumentParser(
|
|
10026
10417
|
prog="harness_archive.py",
|
|
@@ -10180,6 +10571,28 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
10180
10571
|
p_repair.add_argument("--json", action="store_true", default=True)
|
|
10181
10572
|
p_repair.set_defaults(func=cmd_repair_cli)
|
|
10182
10573
|
|
|
10574
|
+
p_republish = sub.add_parser(
|
|
10575
|
+
"republish",
|
|
10576
|
+
help=(
|
|
10577
|
+
"rebuild and re-upload the core ZIP for an already sealed archive "
|
|
10578
|
+
"(the normal path deletes the ZIP once the server confirms it)"
|
|
10579
|
+
),
|
|
10580
|
+
)
|
|
10581
|
+
p_republish.add_argument("--change", required=True, help="change key")
|
|
10582
|
+
p_republish.add_argument(
|
|
10583
|
+
"--archive-dir",
|
|
10584
|
+
default=None,
|
|
10585
|
+
help="explicit archive directory; resolved from --change when omitted",
|
|
10586
|
+
)
|
|
10587
|
+
p_republish.add_argument("--project", default=None)
|
|
10588
|
+
p_republish.add_argument(
|
|
10589
|
+
"--dry-run",
|
|
10590
|
+
action="store_true",
|
|
10591
|
+
help="build the package and report its contents without uploading",
|
|
10592
|
+
)
|
|
10593
|
+
p_republish.add_argument("--json", action="store_true", default=True)
|
|
10594
|
+
p_republish.set_defaults(func=cmd_republish_cli)
|
|
10595
|
+
|
|
10183
10596
|
return parser
|
|
10184
10597
|
|
|
10185
10598
|
|