@hunter-harness/workflow-harness 0.2.50 → 0.2.52
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/README.md +4 -4
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +6 -6
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +42 -258
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +38 -120
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +4 -4
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +2 -2
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +7 -7
- package/harness/bundles/general/claude-code/harness-sync/reference.md +18 -11
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +5 -7
- package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +17 -11
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +748 -200
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +0 -1
- package/harness/bundles/general/claude-code/scripts/harness_state.py +3 -5
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/README.md +4 -4
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +6 -6
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +39 -244
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +35 -106
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +4 -4
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +2 -2
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +8 -8
- package/harness/bundles/general/codebuddy/harness-sync/reference.md +18 -11
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +5 -7
- package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +17 -11
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +748 -200
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +0 -1
- package/harness/bundles/general/codebuddy/scripts/harness_state.py +3 -5
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/README.md +4 -4
- package/harness/bundles/general/codex/harness-archive/SKILL.md +6 -6
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +39 -244
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +35 -106
- package/harness/bundles/general/codex/harness-plan/SKILL.md +4 -4
- package/harness/bundles/general/codex/harness-plan/checklist.md +2 -2
- package/harness/bundles/general/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-run/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-sync/SKILL.md +8 -8
- package/harness/bundles/general/codex/harness-sync/reference.md +18 -11
- package/harness/bundles/general/codex/harness-test/SKILL.md +2 -2
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +5 -7
- package/harness/bundles/general/codex/protocols/state-layout-protocol.md +17 -11
- package/harness/bundles/general/codex/scripts/harness_archive.py +748 -200
- package/harness/bundles/general/codex/scripts/harness_profile.py +0 -1
- package/harness/bundles/general/codex/scripts/harness_state.py +3 -5
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/README.md +4 -4
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +6 -6
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +39 -244
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +35 -106
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +4 -4
- package/harness/bundles/general/cursor/harness-plan/checklist.md +2 -2
- package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-run/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +8 -8
- package/harness/bundles/general/cursor/harness-sync/reference.md +18 -11
- package/harness/bundles/general/cursor/harness-test/SKILL.md +2 -2
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +5 -7
- package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +17 -11
- package/harness/bundles/general/cursor/scripts/harness_archive.py +748 -200
- package/harness/bundles/general/cursor/scripts/harness_profile.py +0 -1
- package/harness/bundles/general/cursor/scripts/harness_state.py +3 -5
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/README.md +4 -4
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +6 -6
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +42 -258
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +38 -120
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +2 -2
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +7 -7
- package/harness/bundles/java/claude-code/harness-sync/reference.md +18 -11
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +5 -7
- package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +17 -11
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +748 -200
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +0 -1
- package/harness/bundles/java/claude-code/scripts/harness_state.py +3 -5
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/README.md +4 -4
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +6 -6
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +39 -244
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +35 -106
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +2 -2
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +8 -8
- package/harness/bundles/java/codebuddy/harness-sync/reference.md +18 -11
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +5 -7
- package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +17 -11
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +748 -200
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +0 -1
- package/harness/bundles/java/codebuddy/scripts/harness_state.py +3 -5
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/README.md +4 -4
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-archive/SKILL.md +6 -6
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +39 -244
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +35 -106
- package/harness/bundles/java/codex/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/codex/harness-plan/checklist.md +2 -2
- package/harness/bundles/java/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-run/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-sync/SKILL.md +8 -8
- package/harness/bundles/java/codex/harness-sync/reference.md +18 -11
- package/harness/bundles/java/codex/harness-test/SKILL.md +2 -2
- package/harness/bundles/java/codex/protocols/archive-report-protocol.md +5 -7
- package/harness/bundles/java/codex/protocols/state-layout-protocol.md +17 -11
- package/harness/bundles/java/codex/scripts/harness_archive.py +748 -200
- package/harness/bundles/java/codex/scripts/harness_profile.py +0 -1
- package/harness/bundles/java/codex/scripts/harness_state.py +3 -5
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/README.md +4 -4
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +6 -6
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +39 -244
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +35 -106
- package/harness/bundles/java/cursor/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/cursor/harness-plan/checklist.md +2 -2
- package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-run/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +8 -8
- package/harness/bundles/java/cursor/harness-sync/reference.md +18 -11
- package/harness/bundles/java/cursor/harness-test/SKILL.md +2 -2
- package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +5 -7
- package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +17 -11
- package/harness/bundles/java/cursor/scripts/harness_archive.py +748 -200
- package/harness/bundles/java/cursor/scripts/harness_profile.py +0 -1
- package/harness/bundles/java/cursor/scripts/harness_state.py +3 -5
- package/harness/manifests/general/claude-code.json +21 -45
- package/harness/manifests/general/codebuddy.json +21 -45
- package/harness/manifests/general/codex.json +21 -45
- package/harness/manifests/general/cursor.json +21 -45
- package/harness/manifests/java/claude-code.json +23 -47
- package/harness/manifests/java/codebuddy.json +23 -47
- package/harness/manifests/java/codex.json +23 -47
- package/harness/manifests/java/cursor.json +23 -47
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/design.md +0 -842
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/reference.md +0 -413
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -7263
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -275
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/design.md +0 -842
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/reference.md +0 -413
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -7263
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -275
- package/harness/bundles/general/codex/harness-knowledge-ingest/design.md +0 -842
- package/harness/bundles/general/codex/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/harness/bundles/general/codex/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/harness/bundles/general/codex/harness-knowledge-ingest/reference.md +0 -413
- package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -7263
- package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -275
- package/harness/bundles/general/cursor/harness-knowledge-ingest/design.md +0 -842
- package/harness/bundles/general/cursor/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/harness/bundles/general/cursor/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/harness/bundles/general/cursor/harness-knowledge-ingest/reference.md +0 -413
- package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -7263
- package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -275
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/design.md +0 -842
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/reference.md +0 -413
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -7263
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -275
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/design.md +0 -842
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/reference.md +0 -413
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -7263
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -275
- package/harness/bundles/java/codex/harness-knowledge-ingest/design.md +0 -842
- package/harness/bundles/java/codex/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/harness/bundles/java/codex/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/harness/bundles/java/codex/harness-knowledge-ingest/reference.md +0 -413
- package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -7263
- package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -275
- package/harness/bundles/java/cursor/harness-knowledge-ingest/design.md +0 -842
- package/harness/bundles/java/cursor/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/harness/bundles/java/cursor/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/harness/bundles/java/cursor/harness-knowledge-ingest/reference.md +0 -413
- package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -7263
- package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -275
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
Subcommands:
|
|
5
5
|
status — pre-archive gate checks (read-only)
|
|
6
6
|
finalize — single-process archive: manifest → move → collect → render →
|
|
7
|
-
validate → after-manifest →
|
|
7
|
+
validate → after-manifest → publish → ZIP upload/service
|
|
8
8
|
replay — read-only re-collect + validate for historical archives
|
|
9
9
|
|
|
10
10
|
Python 3.10+, stdlib only. UTF-8 without BOM. Windows path safe.
|
|
11
|
-
Depends on P0-1 harness_events.py
|
|
11
|
+
Depends on P0-1 harness_events.py. Knowledge ingest is owned by Hunter Platform.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
@@ -21,11 +21,14 @@ import json
|
|
|
21
21
|
import os
|
|
22
22
|
import re
|
|
23
23
|
import shutil
|
|
24
|
+
import stat
|
|
24
25
|
import subprocess
|
|
25
26
|
import sys
|
|
26
27
|
import tarfile
|
|
27
28
|
import tempfile
|
|
28
29
|
import uuid
|
|
30
|
+
import zipfile
|
|
31
|
+
from collections.abc import Mapping
|
|
29
32
|
from pathlib import Path
|
|
30
33
|
from typing import Any
|
|
31
34
|
|
|
@@ -42,9 +45,6 @@ RENDER_SCRIPT = SKILLS_ROOT / "harness-archive" / "templates" / "render-summary.
|
|
|
42
45
|
SUMMARY_TEMPLATE = (
|
|
43
46
|
SKILLS_ROOT / "harness-archive" / "templates" / "summary-data-template.json"
|
|
44
47
|
)
|
|
45
|
-
KNOWLEDGE_SCRIPT = (
|
|
46
|
-
SKILLS_ROOT / "harness-knowledge-ingest" / "scripts" / "harness_knowledge.py"
|
|
47
|
-
)
|
|
48
48
|
SERVICE_SCRIPT = SCRIPTS_DIR / "harness_service.py"
|
|
49
49
|
|
|
50
50
|
# Manifest compare must ignore self-mutating log files appended during finalize.
|
|
@@ -1924,38 +1924,12 @@ def worktree_requested(change_dir: Path) -> bool:
|
|
|
1924
1924
|
|
|
1925
1925
|
|
|
1926
1926
|
def check_archive_exact_byte_policy(project_root: Path) -> dict[str, Any]:
|
|
1927
|
-
"""
|
|
1928
|
-
required_rule = ".harness/archive/** -text"
|
|
1929
|
-
attributes_path = project_root / ".gitattributes"
|
|
1930
|
-
rules: list[str] = []
|
|
1931
|
-
if attributes_path.is_file():
|
|
1932
|
-
try:
|
|
1933
|
-
rules = [
|
|
1934
|
-
line.strip()
|
|
1935
|
-
for line in attributes_path.read_text(encoding="utf-8-sig").splitlines()
|
|
1936
|
-
if line.strip() and not line.lstrip().startswith("#")
|
|
1937
|
-
]
|
|
1938
|
-
except OSError:
|
|
1939
|
-
rules = []
|
|
1940
|
-
matching = []
|
|
1941
|
-
for rule in rules:
|
|
1942
|
-
fields = rule.split()
|
|
1943
|
-
if not fields or fields[0] != ".harness/archive/**":
|
|
1944
|
-
continue
|
|
1945
|
-
matching.append(rule)
|
|
1946
|
-
if "-text" in fields[1:] or "binary" in fields[1:]:
|
|
1947
|
-
return {
|
|
1948
|
-
"ok": True,
|
|
1949
|
-
"path": str(attributes_path),
|
|
1950
|
-
"requiredRule": required_rule,
|
|
1951
|
-
"matchedRule": rule,
|
|
1952
|
-
}
|
|
1927
|
+
"""Exact bytes are guaranteed by ZIP entry hashes, independent of Git."""
|
|
1953
1928
|
return {
|
|
1954
|
-
"ok":
|
|
1955
|
-
"
|
|
1956
|
-
"
|
|
1957
|
-
"
|
|
1958
|
-
"remediation": f"add this exact-byte rule to .gitattributes: {required_rule}",
|
|
1929
|
+
"ok": True,
|
|
1930
|
+
"mode": "zip-content-hash",
|
|
1931
|
+
"gitattributesRequired": False,
|
|
1932
|
+
"note": "archive bytes are verified by manifest and package SHA-256",
|
|
1959
1933
|
}
|
|
1960
1934
|
|
|
1961
1935
|
|
|
@@ -7001,79 +6975,6 @@ def materialize_repository_artifacts(change_dir: Path) -> dict[str, Any]:
|
|
|
7001
6975
|
return result
|
|
7002
6976
|
|
|
7003
6977
|
|
|
7004
|
-
def run_knowledge_poststeps(project_root: Path) -> dict[str, Any]:
|
|
7005
|
-
results: dict[str, Any] = {"ran": False, "steps": [], "warnings": []}
|
|
7006
|
-
if not KNOWLEDGE_SCRIPT.is_file():
|
|
7007
|
-
results["warnings"].append(f"harness_knowledge.py not found: {KNOWLEDGE_SCRIPT}")
|
|
7008
|
-
return results
|
|
7009
|
-
results["ran"] = True
|
|
7010
|
-
for cmd in ("ingest", "dedupe", "auto-supersede", "reverify-stale"):
|
|
7011
|
-
try:
|
|
7012
|
-
proc = subprocess.run(
|
|
7013
|
-
[
|
|
7014
|
-
sys.executable,
|
|
7015
|
-
str(KNOWLEDGE_SCRIPT),
|
|
7016
|
-
cmd,
|
|
7017
|
-
"--project",
|
|
7018
|
-
str(project_root),
|
|
7019
|
-
"--json",
|
|
7020
|
-
],
|
|
7021
|
-
capture_output=True,
|
|
7022
|
-
text=True,
|
|
7023
|
-
encoding="utf-8",
|
|
7024
|
-
errors="replace",
|
|
7025
|
-
timeout=300,
|
|
7026
|
-
check=False,
|
|
7027
|
-
)
|
|
7028
|
-
step = {
|
|
7029
|
-
"command": cmd,
|
|
7030
|
-
"exit_code": proc.returncode,
|
|
7031
|
-
"ok": proc.returncode == 0,
|
|
7032
|
-
}
|
|
7033
|
-
if proc.returncode != 0:
|
|
7034
|
-
step["stderr"] = (proc.stderr or proc.stdout or "")[:500]
|
|
7035
|
-
results["warnings"].append(f"knowledge {cmd} exit {proc.returncode}")
|
|
7036
|
-
results["steps"].append(step)
|
|
7037
|
-
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
7038
|
-
results["steps"].append({"command": cmd, "ok": False, "error": str(exc)})
|
|
7039
|
-
results["warnings"].append(f"knowledge {cmd} failed: {exc}")
|
|
7040
|
-
return results
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
def enqueue_maintenance_outbox(project_root: Path, archive_dir: Path) -> dict[str, Any]:
|
|
7044
|
-
"""§8.2: write a pending maintenance-outbox item instead of synchronously
|
|
7045
|
-
running the four knowledge subprocesses. Never rolls back the archive."""
|
|
7046
|
-
pending_dir = (
|
|
7047
|
-
project_root / ".harness" / "knowledge" / "maintenance-outbox" / "pending"
|
|
7048
|
-
)
|
|
7049
|
-
pending_dir.mkdir(parents=True, exist_ok=True)
|
|
7050
|
-
archive_id = archive_dir.name
|
|
7051
|
-
manifest = archive_dir / "evidence" / "archive-manifest-after.json"
|
|
7052
|
-
manifest_hash = "sha256:" + sha256_file(manifest) if manifest.is_file() else ""
|
|
7053
|
-
try:
|
|
7054
|
-
rel = archive_dir.resolve().relative_to(project_root.resolve()).as_posix()
|
|
7055
|
-
except ValueError:
|
|
7056
|
-
rel = str(archive_dir)
|
|
7057
|
-
item = {
|
|
7058
|
-
"schemaVersion": 1,
|
|
7059
|
-
"archiveId": archive_id,
|
|
7060
|
-
"archivePath": rel,
|
|
7061
|
-
"archiveManifestHash": manifest_hash,
|
|
7062
|
-
"status": "pending",
|
|
7063
|
-
"attempts": 0,
|
|
7064
|
-
"createdAt": now_iso(),
|
|
7065
|
-
"lastError": None,
|
|
7066
|
-
}
|
|
7067
|
-
item_path = pending_dir / f"{archive_id}.json"
|
|
7068
|
-
write_json(item_path, item)
|
|
7069
|
-
return {
|
|
7070
|
-
"queued": True,
|
|
7071
|
-
"outboxPath": str(item_path),
|
|
7072
|
-
"archiveId": archive_id,
|
|
7073
|
-
"status": "pending",
|
|
7074
|
-
}
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
6978
|
def run_service_stop(change_dir: Path) -> dict[str, Any]:
|
|
7078
6979
|
if not SERVICE_SCRIPT.is_file():
|
|
7079
6980
|
return {
|
|
@@ -8325,19 +8226,15 @@ def cmd_finalize(
|
|
|
8325
8226
|
warnings.append(f"could not remove archived runtime state: {exc}")
|
|
8326
8227
|
payload["steps"]["delete_runtime_state"] = {"ok": False, "error": str(exc)}
|
|
8327
8228
|
|
|
8328
|
-
# --- 12.
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
except OSError as exc:
|
|
8338
|
-
warnings.append(f"maintenance outbox enqueue failed: {exc}")
|
|
8339
|
-
payload["steps"]["knowledge"] = {"queued": False, "error": str(exc)}
|
|
8340
|
-
payload["knowledgeMaintenance"] = "NOT_QUEUED"
|
|
8229
|
+
# --- 12. remote knowledge ownership + service ---
|
|
8230
|
+
# Knowledge ingest is server-owned. The local archive never builds or keeps
|
|
8231
|
+
# a searchable knowledge index; upload status below is the ingest receipt.
|
|
8232
|
+
payload["steps"]["knowledge"] = {
|
|
8233
|
+
"mode": "remote-after-archive-upload",
|
|
8234
|
+
"localIngest": False,
|
|
8235
|
+
"legacySkipIngestFlag": bool(skip_ingest),
|
|
8236
|
+
}
|
|
8237
|
+
payload["knowledgeMaintenance"] = "REMOTE_PENDING"
|
|
8341
8238
|
|
|
8342
8239
|
service_result = run_service_stop(work_dir)
|
|
8343
8240
|
payload["steps"]["service_stop"] = service_result
|
|
@@ -8365,10 +8262,16 @@ def cmd_finalize(
|
|
|
8365
8262
|
except OSError as exc:
|
|
8366
8263
|
warnings.append(f"could not persist completed archive operation: {exc}")
|
|
8367
8264
|
|
|
8368
|
-
#
|
|
8369
|
-
|
|
8370
|
-
|
|
8265
|
+
# When remote credentials exist, upload one deterministic core-only ZIP.
|
|
8266
|
+
push_result = auto_push_archive_core(
|
|
8267
|
+
project_root,
|
|
8268
|
+
archive_dir,
|
|
8269
|
+
change_key=change_name,
|
|
8270
|
+
)
|
|
8371
8271
|
payload["steps"]["archive_push"] = push_result
|
|
8272
|
+
payload["knowledgeMaintenance"] = _knowledge_maintenance_from_archive_push(
|
|
8273
|
+
push_result
|
|
8274
|
+
)
|
|
8372
8275
|
if push_result.get("warning"):
|
|
8373
8276
|
warnings.append(str(push_result["warning"]))
|
|
8374
8277
|
|
|
@@ -8397,89 +8300,597 @@ def cmd_finalize(
|
|
|
8397
8300
|
return 0, payload
|
|
8398
8301
|
|
|
8399
8302
|
|
|
8303
|
+
def _yaml_scalar(raw: str) -> str | None:
|
|
8304
|
+
"""Decode the small scalar subset used by Harness-owned YAML files."""
|
|
8305
|
+
value = raw.strip()
|
|
8306
|
+
if not value:
|
|
8307
|
+
return None
|
|
8308
|
+
quote: str | None = None
|
|
8309
|
+
escaped = False
|
|
8310
|
+
end = len(value)
|
|
8311
|
+
for index, character in enumerate(value):
|
|
8312
|
+
if escaped:
|
|
8313
|
+
escaped = False
|
|
8314
|
+
continue
|
|
8315
|
+
if quote == '"' and character == "\\":
|
|
8316
|
+
escaped = True
|
|
8317
|
+
continue
|
|
8318
|
+
if character in {"'", '"'}:
|
|
8319
|
+
if quote is None:
|
|
8320
|
+
quote = character
|
|
8321
|
+
elif quote == character:
|
|
8322
|
+
quote = None
|
|
8323
|
+
continue
|
|
8324
|
+
if character == "#" and quote is None:
|
|
8325
|
+
end = index
|
|
8326
|
+
break
|
|
8327
|
+
value = value[:end].strip()
|
|
8328
|
+
if value.lower() in {"", "null", "none", "~"}:
|
|
8329
|
+
return None
|
|
8330
|
+
if len(value) >= 2 and value[0] == value[-1] == '"':
|
|
8331
|
+
try:
|
|
8332
|
+
decoded = json.loads(value)
|
|
8333
|
+
except json.JSONDecodeError:
|
|
8334
|
+
return None
|
|
8335
|
+
return decoded.strip() if isinstance(decoded, str) and decoded.strip() else None
|
|
8336
|
+
if len(value) >= 2 and value[0] == value[-1] == "'":
|
|
8337
|
+
decoded = value[1:-1].replace("''", "'").strip()
|
|
8338
|
+
return decoded or None
|
|
8339
|
+
return value
|
|
8340
|
+
|
|
8341
|
+
|
|
8342
|
+
def _yaml_mapping_value(
|
|
8343
|
+
text: str,
|
|
8344
|
+
key: str,
|
|
8345
|
+
*,
|
|
8346
|
+
section: str | None = None,
|
|
8347
|
+
) -> str | None:
|
|
8348
|
+
"""Read one root or one-level nested scalar without loading arbitrary YAML."""
|
|
8349
|
+
inside_section = section is None
|
|
8350
|
+
for line in text.splitlines():
|
|
8351
|
+
if not line.strip() or line.lstrip().startswith("#"):
|
|
8352
|
+
continue
|
|
8353
|
+
match = re.match(
|
|
8354
|
+
r"^(?P<indent> *)(?P<key>[A-Za-z_][A-Za-z0-9_-]*):(?P<raw>.*)$",
|
|
8355
|
+
line,
|
|
8356
|
+
)
|
|
8357
|
+
if match is None:
|
|
8358
|
+
continue
|
|
8359
|
+
indent = len(match.group("indent"))
|
|
8360
|
+
name = match.group("key")
|
|
8361
|
+
if section is None:
|
|
8362
|
+
if indent == 0 and name == key:
|
|
8363
|
+
return _yaml_scalar(match.group("raw"))
|
|
8364
|
+
continue
|
|
8365
|
+
if section is not None and indent == 0:
|
|
8366
|
+
inside_section = name == section
|
|
8367
|
+
continue
|
|
8368
|
+
if inside_section and name == key and indent > 0:
|
|
8369
|
+
return _yaml_scalar(match.group("raw"))
|
|
8370
|
+
return None
|
|
8371
|
+
|
|
8372
|
+
|
|
8373
|
+
def _read_optional_text(path: Path) -> str:
|
|
8374
|
+
try:
|
|
8375
|
+
return path.read_text(encoding="utf-8-sig")
|
|
8376
|
+
except OSError:
|
|
8377
|
+
return ""
|
|
8378
|
+
|
|
8379
|
+
|
|
8380
|
+
def _resolve_archive_remote_credentials(
|
|
8381
|
+
project_root: Path,
|
|
8382
|
+
environment: Mapping[str, str],
|
|
8383
|
+
) -> dict[str, Any]:
|
|
8384
|
+
"""Mirror CLI auth precedence without returning or persisting the token."""
|
|
8385
|
+
project_text = _read_optional_text(project_root / ".harness" / "project.yaml")
|
|
8386
|
+
local_text = _read_optional_text(
|
|
8387
|
+
project_root / ".harness" / "credentials.local.yaml"
|
|
8388
|
+
)
|
|
8389
|
+
project_url = _yaml_mapping_value(project_text, "url", section="server")
|
|
8390
|
+
token_env = _yaml_mapping_value(project_text, "token_env", section="server")
|
|
8391
|
+
local_url = _yaml_mapping_value(local_text, "server_url")
|
|
8392
|
+
local_token = _yaml_mapping_value(local_text, "token")
|
|
8393
|
+
server_url = local_url or project_url
|
|
8394
|
+
valid_token_env = (
|
|
8395
|
+
token_env
|
|
8396
|
+
if token_env is not None
|
|
8397
|
+
and re.fullmatch(r"[A-Z_][A-Z0-9_]*", token_env) is not None
|
|
8398
|
+
else None
|
|
8399
|
+
)
|
|
8400
|
+
env_token = (
|
|
8401
|
+
environment.get(valid_token_env, "").strip()
|
|
8402
|
+
if valid_token_env is not None
|
|
8403
|
+
else ""
|
|
8404
|
+
)
|
|
8405
|
+
token_available = bool(env_token or local_token)
|
|
8406
|
+
url_available = bool(
|
|
8407
|
+
server_url
|
|
8408
|
+
and re.fullmatch(r"https://[^\s]+", server_url, flags=re.IGNORECASE)
|
|
8409
|
+
)
|
|
8410
|
+
return {
|
|
8411
|
+
"configured": bool(url_available and token_available),
|
|
8412
|
+
"serverUrl": server_url if url_available else None,
|
|
8413
|
+
"tokenEnv": valid_token_env,
|
|
8414
|
+
"missing": [
|
|
8415
|
+
*([] if url_available else ["url"]),
|
|
8416
|
+
*([] if token_available else ["token"]),
|
|
8417
|
+
],
|
|
8418
|
+
}
|
|
8419
|
+
|
|
8420
|
+
|
|
8400
8421
|
def _remote_credentials_configured(project_root: Path) -> bool:
|
|
8401
|
-
|
|
8402
|
-
|
|
8422
|
+
"""Compatibility predicate backed by the same auth sources as the CLI."""
|
|
8423
|
+
return bool(
|
|
8424
|
+
_resolve_archive_remote_credentials(project_root.resolve(), os.environ)[
|
|
8425
|
+
"configured"
|
|
8426
|
+
]
|
|
8427
|
+
)
|
|
8428
|
+
|
|
8429
|
+
|
|
8430
|
+
_FILE_ATTRIBUTE_REPARSE_POINT = getattr(
|
|
8431
|
+
stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400
|
|
8432
|
+
)
|
|
8433
|
+
|
|
8434
|
+
|
|
8435
|
+
def _archive_path_is_link_or_reparse(path: Path) -> bool:
|
|
8436
|
+
"""Detect symlinks and Windows junction/reparse points without following them."""
|
|
8437
|
+
try:
|
|
8438
|
+
if path.is_symlink():
|
|
8439
|
+
return True
|
|
8440
|
+
attributes = getattr(os.lstat(path), "st_file_attributes", 0)
|
|
8441
|
+
except OSError:
|
|
8403
8442
|
return False
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8443
|
+
return bool(attributes & _FILE_ATTRIBUTE_REPARSE_POINT)
|
|
8444
|
+
|
|
8445
|
+
|
|
8446
|
+
def _require_archive_project_path(
|
|
8447
|
+
project_root: Path,
|
|
8448
|
+
candidate: Path,
|
|
8449
|
+
*,
|
|
8450
|
+
label: str,
|
|
8451
|
+
must_exist: bool,
|
|
8452
|
+
) -> Path:
|
|
8453
|
+
"""Return a canonical project-contained path after rejecting link components."""
|
|
8454
|
+
lexical_root = Path(os.path.abspath(os.fspath(project_root)))
|
|
8455
|
+
lexical_candidate = Path(os.path.abspath(os.fspath(candidate)))
|
|
8456
|
+
try:
|
|
8457
|
+
relative = lexical_candidate.relative_to(lexical_root)
|
|
8458
|
+
except ValueError as exc:
|
|
8459
|
+
raise ValueError(f"{label} is outside project root: {candidate}") from exc
|
|
8460
|
+
if not relative.parts:
|
|
8461
|
+
raise ValueError(f"{label} must be strictly inside project root")
|
|
8462
|
+
|
|
8463
|
+
current = lexical_root
|
|
8464
|
+
for part in relative.parts:
|
|
8465
|
+
current = current / part
|
|
8466
|
+
if _archive_path_is_link_or_reparse(current):
|
|
8467
|
+
raise ValueError(f"{label} contains a link or reparse point: {current}")
|
|
8468
|
+
|
|
8469
|
+
try:
|
|
8470
|
+
canonical_root = lexical_root.resolve(strict=True)
|
|
8471
|
+
canonical_candidate = lexical_candidate.resolve(strict=must_exist)
|
|
8472
|
+
except OSError as exc:
|
|
8473
|
+
raise ValueError(f"{label} does not exist or cannot be resolved: {candidate}") from exc
|
|
8474
|
+
try:
|
|
8475
|
+
canonical_relative = canonical_candidate.relative_to(canonical_root)
|
|
8476
|
+
except ValueError as exc:
|
|
8477
|
+
raise ValueError(f"{label} resolves outside project root: {candidate}") from exc
|
|
8478
|
+
if not canonical_relative.parts:
|
|
8479
|
+
raise ValueError(f"{label} must be strictly inside project root")
|
|
8480
|
+
return canonical_candidate
|
|
8481
|
+
|
|
8482
|
+
|
|
8483
|
+
def _archive_core_file_specs(
|
|
8484
|
+
project_root: Path,
|
|
8485
|
+
archive_dir: Path,
|
|
8486
|
+
) -> tuple[Path, Path, list[tuple[Path, str, str, str]]]:
|
|
8487
|
+
root = Path(os.path.abspath(os.fspath(project_root))).resolve(strict=True)
|
|
8488
|
+
archive = _require_archive_project_path(
|
|
8489
|
+
project_root,
|
|
8490
|
+
archive_dir,
|
|
8491
|
+
label="archive dir",
|
|
8492
|
+
must_exist=True,
|
|
8493
|
+
)
|
|
8494
|
+
if not archive.is_dir():
|
|
8495
|
+
raise ValueError(f"archive dir not found: {archive}")
|
|
8496
|
+
|
|
8497
|
+
file_specs: list[tuple[Path, str, str, str]] = []
|
|
8498
|
+
|
|
8499
|
+
def add_if_file(
|
|
8500
|
+
candidate: Path,
|
|
8501
|
+
package_path: str,
|
|
8502
|
+
role: str,
|
|
8503
|
+
media_type: str,
|
|
8504
|
+
) -> None:
|
|
8505
|
+
safe = _require_archive_project_path(
|
|
8506
|
+
root,
|
|
8507
|
+
candidate,
|
|
8508
|
+
label=f"archive input {package_path}",
|
|
8509
|
+
must_exist=False,
|
|
8510
|
+
)
|
|
8511
|
+
if safe.is_file():
|
|
8512
|
+
file_specs.append((safe, package_path, role, media_type))
|
|
8513
|
+
|
|
8514
|
+
add_if_file(
|
|
8515
|
+
archive / "reports" / "final" / "summary-data.json",
|
|
8516
|
+
"reports/final/summary-data.json",
|
|
8517
|
+
"summary",
|
|
8518
|
+
"application/json",
|
|
8519
|
+
)
|
|
8520
|
+
for folder, role in (("spec", "spec"), ("plans", "plan")):
|
|
8521
|
+
base = _require_archive_project_path(
|
|
8522
|
+
root,
|
|
8523
|
+
archive / folder,
|
|
8524
|
+
label=f"archive input {folder}",
|
|
8525
|
+
must_exist=False,
|
|
8526
|
+
)
|
|
8527
|
+
if not base.is_dir():
|
|
8528
|
+
continue
|
|
8529
|
+
for directory, directory_names, file_names in os.walk(
|
|
8530
|
+
base, topdown=True, followlinks=False
|
|
8531
|
+
):
|
|
8532
|
+
current = Path(directory)
|
|
8533
|
+
directory_names.sort()
|
|
8534
|
+
file_names.sort()
|
|
8535
|
+
for name in directory_names:
|
|
8536
|
+
_require_archive_project_path(
|
|
8537
|
+
root,
|
|
8538
|
+
current / name,
|
|
8539
|
+
label=f"archive input {folder}/{name}",
|
|
8540
|
+
must_exist=True,
|
|
8541
|
+
)
|
|
8542
|
+
for name in file_names:
|
|
8543
|
+
source = _require_archive_project_path(
|
|
8544
|
+
root,
|
|
8545
|
+
current / name,
|
|
8546
|
+
label=f"archive input {folder}/{name}",
|
|
8547
|
+
must_exist=True,
|
|
8548
|
+
)
|
|
8549
|
+
if source.is_file() and source.suffix.lower() == ".md":
|
|
8550
|
+
relative = source.relative_to(base).as_posix()
|
|
8551
|
+
file_specs.append(
|
|
8552
|
+
(source, f"{folder}/{relative}", role, "text/markdown")
|
|
8553
|
+
)
|
|
8554
|
+
add_if_file(
|
|
8555
|
+
archive / "meta" / "archive-meta.md",
|
|
8556
|
+
"archive-meta.md",
|
|
8557
|
+
"archive_meta",
|
|
8558
|
+
"text/markdown",
|
|
8559
|
+
)
|
|
8560
|
+
add_if_file(
|
|
8561
|
+
archive / "meta" / "change-context.json",
|
|
8562
|
+
"change-context.json",
|
|
8563
|
+
"change_context",
|
|
8564
|
+
"application/json",
|
|
8565
|
+
)
|
|
8566
|
+
return root, archive, file_specs
|
|
8408
8567
|
|
|
8409
8568
|
|
|
8410
8569
|
def collect_archive_core_paths(project_root: Path, archive_dir: Path) -> list[str]:
|
|
8411
|
-
"""
|
|
8412
|
-
root = project_root
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8570
|
+
"""Return only durable core files; diagnostics and local knowledge stay local."""
|
|
8571
|
+
root, _, file_specs = _archive_core_file_specs(project_root, archive_dir)
|
|
8572
|
+
return [source.relative_to(root).as_posix() for source, *_ in file_specs]
|
|
8573
|
+
|
|
8574
|
+
|
|
8575
|
+
def _archive_source_identity(project_root: Path) -> dict[str, str | None]:
|
|
8576
|
+
def git_value(*args: str) -> str | None:
|
|
8577
|
+
try:
|
|
8578
|
+
completed = subprocess.run(
|
|
8579
|
+
["git", *args],
|
|
8580
|
+
cwd=str(project_root),
|
|
8581
|
+
text=True,
|
|
8582
|
+
stdout=subprocess.PIPE,
|
|
8583
|
+
stderr=subprocess.DEVNULL,
|
|
8584
|
+
timeout=10,
|
|
8585
|
+
shell=False,
|
|
8586
|
+
)
|
|
8587
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
8588
|
+
return None
|
|
8589
|
+
value = completed.stdout.strip()
|
|
8590
|
+
return value if completed.returncode == 0 and value else None
|
|
8591
|
+
|
|
8592
|
+
return {
|
|
8593
|
+
"commit": git_value("rev-parse", "HEAD"),
|
|
8594
|
+
"tree": git_value("rev-parse", "HEAD^{tree}"),
|
|
8595
|
+
}
|
|
8596
|
+
|
|
8597
|
+
|
|
8598
|
+
def _archive_created_at(summary_path: Path) -> str:
|
|
8599
|
+
try:
|
|
8600
|
+
summary = read_json(summary_path)
|
|
8601
|
+
except (OSError, json.JSONDecodeError):
|
|
8602
|
+
return "1980-01-01T00:00:00.000Z"
|
|
8603
|
+
for key in ("archivedAt", "archived_at", "generatedAt", "generated_at"):
|
|
8604
|
+
value = summary.get(key)
|
|
8605
|
+
if isinstance(value, str) and value.strip():
|
|
8606
|
+
return value.strip()
|
|
8607
|
+
return "1980-01-01T00:00:00.000Z"
|
|
8608
|
+
|
|
8609
|
+
|
|
8610
|
+
def _deterministic_zip_info(path: str) -> zipfile.ZipInfo:
|
|
8611
|
+
info = zipfile.ZipInfo(path, date_time=(1980, 1, 1, 0, 0, 0))
|
|
8612
|
+
info.compress_type = zipfile.ZIP_DEFLATED
|
|
8613
|
+
info.create_system = 3
|
|
8614
|
+
info.external_attr = (0o100644 & 0xFFFF) << 16
|
|
8615
|
+
return info
|
|
8616
|
+
|
|
8617
|
+
|
|
8618
|
+
def build_archive_package(
|
|
8619
|
+
project_root: Path,
|
|
8620
|
+
archive_dir: Path,
|
|
8621
|
+
change_key: str,
|
|
8622
|
+
*,
|
|
8623
|
+
output_path: Path | None = None,
|
|
8624
|
+
) -> dict[str, Any]:
|
|
8625
|
+
"""Build the deterministic core-v1 ZIP uploaded to hunter-platform."""
|
|
8626
|
+
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,159}", change_key) is None:
|
|
8627
|
+
raise ValueError("change_key must be a portable path segment")
|
|
8628
|
+
project_root, archive_dir, file_specs = _archive_core_file_specs(
|
|
8629
|
+
project_root, archive_dir
|
|
8630
|
+
)
|
|
8631
|
+
summary_path = archive_dir / "reports" / "final" / "summary-data.json"
|
|
8632
|
+
if not any(spec[1] == "reports/final/summary-data.json" for spec in file_specs):
|
|
8633
|
+
raise ValueError("archive final summary-data.json is required")
|
|
8634
|
+
|
|
8635
|
+
entries: list[dict[str, Any]] = []
|
|
8636
|
+
content_by_path: dict[str, bytes] = {}
|
|
8637
|
+
for source, package_path, role, media_type in sorted(file_specs, key=lambda item: item[1]):
|
|
8638
|
+
source = _require_archive_project_path(
|
|
8639
|
+
project_root,
|
|
8640
|
+
source,
|
|
8641
|
+
label=f"archive input {package_path}",
|
|
8642
|
+
must_exist=True,
|
|
8643
|
+
)
|
|
8644
|
+
raw = source.read_bytes()
|
|
8645
|
+
# All semantic archive inputs are text and must round-trip as UTF-8.
|
|
8646
|
+
raw.decode("utf-8")
|
|
8647
|
+
if media_type == "application/json":
|
|
8648
|
+
json.loads(raw)
|
|
8649
|
+
content_by_path[package_path] = raw
|
|
8650
|
+
entries.append(
|
|
8651
|
+
{
|
|
8652
|
+
"path": package_path,
|
|
8653
|
+
"role": role,
|
|
8654
|
+
"media_type": media_type,
|
|
8655
|
+
"content_sha256": "sha256:" + hashlib.sha256(raw).hexdigest(),
|
|
8656
|
+
"size_bytes": len(raw),
|
|
8657
|
+
}
|
|
8658
|
+
)
|
|
8659
|
+
|
|
8660
|
+
manifest = {
|
|
8661
|
+
"schema_version": 1,
|
|
8662
|
+
"profile": "core-v1",
|
|
8663
|
+
"change_key": change_key,
|
|
8664
|
+
"created_at": _archive_created_at(summary_path),
|
|
8665
|
+
"source": _archive_source_identity(project_root),
|
|
8666
|
+
"files": entries,
|
|
8667
|
+
}
|
|
8668
|
+
manifest_bytes = (
|
|
8669
|
+
json.dumps(
|
|
8670
|
+
manifest,
|
|
8671
|
+
ensure_ascii=False,
|
|
8672
|
+
sort_keys=True,
|
|
8673
|
+
separators=(",", ":"),
|
|
8674
|
+
)
|
|
8675
|
+
+ "\n"
|
|
8676
|
+
).encode("utf-8")
|
|
8677
|
+
|
|
8678
|
+
if output_path is None:
|
|
8679
|
+
output_path = (
|
|
8680
|
+
project_root
|
|
8681
|
+
/ ".harness"
|
|
8682
|
+
/ "state"
|
|
8683
|
+
/ "local"
|
|
8684
|
+
/ "archive-packages"
|
|
8685
|
+
/ f"{change_key}.zip"
|
|
8686
|
+
)
|
|
8687
|
+
output_path = _require_archive_project_path(
|
|
8688
|
+
project_root,
|
|
8689
|
+
output_path,
|
|
8690
|
+
label="archive package output",
|
|
8691
|
+
must_exist=False,
|
|
8692
|
+
)
|
|
8693
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
8694
|
+
output_path = _require_archive_project_path(
|
|
8695
|
+
project_root,
|
|
8696
|
+
output_path,
|
|
8697
|
+
label="archive package output",
|
|
8698
|
+
must_exist=False,
|
|
8699
|
+
)
|
|
8700
|
+
temporary = output_path.with_name(output_path.name + f".{uuid.uuid4().hex}.tmp")
|
|
8701
|
+
try:
|
|
8702
|
+
with zipfile.ZipFile(
|
|
8703
|
+
temporary,
|
|
8704
|
+
"w",
|
|
8705
|
+
compression=zipfile.ZIP_DEFLATED,
|
|
8706
|
+
compresslevel=9,
|
|
8707
|
+
strict_timestamps=True,
|
|
8708
|
+
) as package:
|
|
8709
|
+
package.writestr(
|
|
8710
|
+
_deterministic_zip_info("archive-manifest.json"),
|
|
8711
|
+
manifest_bytes,
|
|
8712
|
+
compress_type=zipfile.ZIP_DEFLATED,
|
|
8713
|
+
compresslevel=9,
|
|
8714
|
+
)
|
|
8715
|
+
for path in sorted(content_by_path):
|
|
8716
|
+
package.writestr(
|
|
8717
|
+
_deterministic_zip_info(path),
|
|
8718
|
+
content_by_path[path],
|
|
8719
|
+
compress_type=zipfile.ZIP_DEFLATED,
|
|
8720
|
+
compresslevel=9,
|
|
8721
|
+
)
|
|
8722
|
+
output_path = _require_archive_project_path(
|
|
8723
|
+
project_root,
|
|
8724
|
+
output_path,
|
|
8725
|
+
label="archive package output",
|
|
8726
|
+
must_exist=False,
|
|
8727
|
+
)
|
|
8728
|
+
os.replace(temporary, output_path)
|
|
8729
|
+
finally:
|
|
8730
|
+
temporary.unlink(missing_ok=True)
|
|
8731
|
+
|
|
8732
|
+
package_raw = output_path.read_bytes()
|
|
8733
|
+
return {
|
|
8734
|
+
"schemaVersion": 1,
|
|
8735
|
+
"profile": "core-v1",
|
|
8736
|
+
"changeKey": change_key,
|
|
8737
|
+
"packagePath": str(output_path),
|
|
8738
|
+
"packageSha256": "sha256:" + hashlib.sha256(package_raw).hexdigest(),
|
|
8739
|
+
"manifestSha256": "sha256:" + hashlib.sha256(manifest_bytes).hexdigest(),
|
|
8740
|
+
"fileCount": len(entries),
|
|
8741
|
+
"sizeBytes": len(package_raw),
|
|
8742
|
+
"paths": [entry["path"] for entry in entries],
|
|
8743
|
+
}
|
|
8744
|
+
|
|
8745
|
+
|
|
8746
|
+
def auto_push_archive_core(
|
|
8747
|
+
project_root: Path,
|
|
8748
|
+
archive_dir: Path,
|
|
8749
|
+
*,
|
|
8750
|
+
change_key: str | None = None,
|
|
8751
|
+
) -> dict[str, Any]:
|
|
8752
|
+
"""Build and upload one deterministic core ZIP after finalize.
|
|
8753
|
+
|
|
8754
|
+
The ZIP and its per-change receipt are always created before auth is
|
|
8755
|
+
evaluated. Failures become warnings so archive success is never rolled
|
|
8756
|
+
back by missing credentials or a remote outage.
|
|
8450
8757
|
"""
|
|
8451
8758
|
project_root = project_root.resolve()
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8759
|
+
effective_change_key = change_key or archive_dir.name
|
|
8760
|
+
try:
|
|
8761
|
+
package = build_archive_package(
|
|
8762
|
+
project_root,
|
|
8763
|
+
archive_dir,
|
|
8764
|
+
effective_change_key,
|
|
8765
|
+
)
|
|
8766
|
+
except (OSError, ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
8767
|
+
return {
|
|
8768
|
+
"skipped": False,
|
|
8769
|
+
"ok": False,
|
|
8770
|
+
"reasonCode": "ARCHIVE_PACKAGE_BUILD_FAILED",
|
|
8771
|
+
"warning": f"核心归档 ZIP 生成失败:{exc}",
|
|
8772
|
+
}
|
|
8773
|
+
core_paths = list(package["paths"])
|
|
8774
|
+
package_path = Path(str(package["packagePath"]))
|
|
8775
|
+
try:
|
|
8776
|
+
pending_path = _require_archive_project_path(
|
|
8777
|
+
project_root,
|
|
8778
|
+
package_path.with_suffix(".upload.json"),
|
|
8779
|
+
label="archive upload receipt",
|
|
8780
|
+
must_exist=False,
|
|
8781
|
+
)
|
|
8782
|
+
except (OSError, ValueError) as exc:
|
|
8783
|
+
return {
|
|
8784
|
+
"skipped": False,
|
|
8785
|
+
"ok": False,
|
|
8786
|
+
"paths": core_paths,
|
|
8787
|
+
"packagePath": package["packagePath"],
|
|
8788
|
+
"packageSha256": package["packageSha256"],
|
|
8789
|
+
"reasonCode": "ARCHIVE_UPLOAD_RECEIPT_PATH_UNSAFE",
|
|
8790
|
+
"warning": f"归档上传回执路径不安全;已保留 ZIP:{exc}",
|
|
8791
|
+
}
|
|
8792
|
+
credentials = _resolve_archive_remote_credentials(project_root, os.environ)
|
|
8793
|
+
initial_reason_code = (
|
|
8794
|
+
"ARCHIVE_UPLOAD_PENDING"
|
|
8795
|
+
if credentials["configured"]
|
|
8796
|
+
else "ARCHIVE_UPLOAD_CREDENTIALS_MISSING"
|
|
8797
|
+
)
|
|
8798
|
+
pending: dict[str, Any] = {
|
|
8456
8799
|
"schemaVersion": 1,
|
|
8457
8800
|
"generatedAt": now_iso(),
|
|
8801
|
+
"updatedAt": now_iso(),
|
|
8458
8802
|
"archiveDir": str(archive_dir),
|
|
8803
|
+
"changeKey": effective_change_key,
|
|
8804
|
+
"packagePath": package["packagePath"],
|
|
8805
|
+
"packageSha256": package["packageSha256"],
|
|
8806
|
+
"manifestSha256": package["manifestSha256"],
|
|
8807
|
+
"fileCount": package["fileCount"],
|
|
8459
8808
|
"paths": core_paths,
|
|
8809
|
+
"uploadStatus": "pending",
|
|
8810
|
+
"reasonCode": initial_reason_code,
|
|
8811
|
+
"archiveStatus": None,
|
|
8812
|
+
"knowledgeStatus": None,
|
|
8460
8813
|
}
|
|
8461
|
-
|
|
8814
|
+
|
|
8815
|
+
def persist_receipt(
|
|
8816
|
+
*,
|
|
8817
|
+
upload_status: str,
|
|
8818
|
+
reason_code: str | None,
|
|
8819
|
+
archive_status: str | None = None,
|
|
8820
|
+
knowledge_status: str | None = None,
|
|
8821
|
+
exit_code: int | None = None,
|
|
8822
|
+
) -> None:
|
|
8823
|
+
pending.update(
|
|
8824
|
+
{
|
|
8825
|
+
"updatedAt": now_iso(),
|
|
8826
|
+
"uploadStatus": upload_status,
|
|
8827
|
+
"reasonCode": reason_code,
|
|
8828
|
+
"archiveStatus": archive_status,
|
|
8829
|
+
"knowledgeStatus": knowledge_status,
|
|
8830
|
+
"exitCode": exit_code,
|
|
8831
|
+
}
|
|
8832
|
+
)
|
|
8833
|
+
write_json(pending_path, pending)
|
|
8834
|
+
|
|
8462
8835
|
try:
|
|
8463
8836
|
pending_path.parent.mkdir(parents=True, exist_ok=True)
|
|
8464
|
-
|
|
8837
|
+
persist_receipt(
|
|
8838
|
+
upload_status="pending",
|
|
8839
|
+
reason_code=initial_reason_code,
|
|
8840
|
+
)
|
|
8465
8841
|
except OSError as exc:
|
|
8466
8842
|
return {
|
|
8467
8843
|
"skipped": False,
|
|
8468
8844
|
"ok": False,
|
|
8469
8845
|
"paths": core_paths,
|
|
8470
|
-
"
|
|
8846
|
+
"packagePath": package["packagePath"],
|
|
8847
|
+
"packageSha256": package["packageSha256"],
|
|
8848
|
+
"reasonCode": "ARCHIVE_UPLOAD_RECEIPT_WRITE_FAILED",
|
|
8849
|
+
"warning": f"无法写入归档上传回执:{exc}",
|
|
8471
8850
|
}
|
|
8472
8851
|
|
|
8473
|
-
|
|
8852
|
+
base_result: dict[str, Any] = {
|
|
8853
|
+
"skipped": False,
|
|
8854
|
+
"ok": False,
|
|
8855
|
+
"uploadStatus": "pending",
|
|
8856
|
+
"paths": core_paths,
|
|
8857
|
+
"packagePath": package["packagePath"],
|
|
8858
|
+
"packageSha256": package["packageSha256"],
|
|
8859
|
+
"manifestSha256": package["manifestSha256"],
|
|
8860
|
+
"fileCount": package["fileCount"],
|
|
8861
|
+
"sizeBytes": package["sizeBytes"],
|
|
8862
|
+
"pending": str(pending_path),
|
|
8863
|
+
}
|
|
8864
|
+
|
|
8865
|
+
if not credentials["configured"]:
|
|
8866
|
+
reason_code = "ARCHIVE_UPLOAD_CREDENTIALS_MISSING"
|
|
8867
|
+
return {
|
|
8868
|
+
**base_result,
|
|
8869
|
+
"skipped": True,
|
|
8870
|
+
"reasonCode": reason_code,
|
|
8871
|
+
"missingCredentials": credentials["missing"],
|
|
8872
|
+
"reason": "未配置可用的远端地址或 API Token;已保留 ZIP 等待重试。",
|
|
8873
|
+
}
|
|
8874
|
+
|
|
8875
|
+
# The published CLI owns credentials, project binding, durable receipt checks,
|
|
8876
|
+
# and retries. Generic `push` is intentionally not used for archive bytes.
|
|
8474
8877
|
command = [
|
|
8475
8878
|
"npx",
|
|
8476
8879
|
"--yes",
|
|
8477
8880
|
"hunter-harness",
|
|
8478
|
-
"
|
|
8881
|
+
"archive",
|
|
8882
|
+
"upload",
|
|
8883
|
+
"--file",
|
|
8884
|
+
str(package["packagePath"]),
|
|
8885
|
+
"--change-key",
|
|
8886
|
+
effective_change_key,
|
|
8479
8887
|
"--yes",
|
|
8480
8888
|
"--non-interactive",
|
|
8481
8889
|
"--json",
|
|
8482
8890
|
]
|
|
8891
|
+
command.extend(["--server-url", str(credentials["serverUrl"])])
|
|
8892
|
+
if credentials["tokenEnv"] is not None:
|
|
8893
|
+
command.extend(["--token-env", str(credentials["tokenEnv"])])
|
|
8483
8894
|
try:
|
|
8484
8895
|
completed = subprocess.run(
|
|
8485
8896
|
command,
|
|
@@ -8491,37 +8902,174 @@ def auto_push_archive_core(project_root: Path, archive_dir: Path) -> dict[str, A
|
|
|
8491
8902
|
shell=False,
|
|
8492
8903
|
)
|
|
8493
8904
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
8905
|
+
reason_code = "ARCHIVE_UPLOAD_DEFERRED"
|
|
8906
|
+
try:
|
|
8907
|
+
persist_receipt(upload_status="pending", reason_code=reason_code)
|
|
8908
|
+
except OSError:
|
|
8909
|
+
pass
|
|
8494
8910
|
return {
|
|
8495
|
-
|
|
8496
|
-
"
|
|
8497
|
-
"
|
|
8498
|
-
|
|
8499
|
-
|
|
8911
|
+
**base_result,
|
|
8912
|
+
"reasonCode": reason_code,
|
|
8913
|
+
"warning": f"归档自动上传已延后:{exc}",
|
|
8914
|
+
}
|
|
8915
|
+
|
|
8916
|
+
try:
|
|
8917
|
+
cli_payload = json.loads(completed.stdout)
|
|
8918
|
+
except (json.JSONDecodeError, TypeError):
|
|
8919
|
+
cli_payload = None
|
|
8920
|
+
if completed.returncode != 0:
|
|
8921
|
+
reason_code = "ARCHIVE_UPLOAD_COMMAND_FAILED"
|
|
8922
|
+
if isinstance(cli_payload, dict):
|
|
8923
|
+
errors = cli_payload.get("errors")
|
|
8924
|
+
if isinstance(errors, list) and errors and isinstance(errors[0], dict):
|
|
8925
|
+
cli_code = errors[0].get("code")
|
|
8926
|
+
if (
|
|
8927
|
+
isinstance(cli_code, str)
|
|
8928
|
+
and re.fullmatch(r"[A-Z][A-Z0-9_]{0,79}", cli_code.strip())
|
|
8929
|
+
is not None
|
|
8930
|
+
):
|
|
8931
|
+
reason_code = cli_code.strip()
|
|
8932
|
+
try:
|
|
8933
|
+
persist_receipt(
|
|
8934
|
+
upload_status="pending",
|
|
8935
|
+
reason_code=reason_code,
|
|
8936
|
+
exit_code=completed.returncode,
|
|
8937
|
+
)
|
|
8938
|
+
except OSError:
|
|
8939
|
+
pass
|
|
8940
|
+
result = {
|
|
8941
|
+
**base_result,
|
|
8942
|
+
"exitCode": completed.returncode,
|
|
8943
|
+
"reasonCode": reason_code,
|
|
8944
|
+
"warning": (
|
|
8945
|
+
f"归档自动上传失败(退出码 {completed.returncode});"
|
|
8946
|
+
"已保留 ZIP 与回执,可重试同一个包。"
|
|
8947
|
+
),
|
|
8500
8948
|
}
|
|
8501
|
-
ok = completed.returncode == 0
|
|
8502
|
-
result: dict[str, Any] = {
|
|
8503
|
-
"skipped": False,
|
|
8504
|
-
"ok": ok,
|
|
8505
|
-
"paths": core_paths,
|
|
8506
|
-
"pending": str(pending_path),
|
|
8507
|
-
"exitCode": completed.returncode,
|
|
8508
|
-
}
|
|
8509
|
-
if not ok:
|
|
8510
|
-
result["warning"] = (
|
|
8511
|
-
"archive auto-push failed (exit "
|
|
8512
|
-
+ str(completed.returncode)
|
|
8513
|
-
+ "); pending receipt kept for retry via `hunter-harness push`"
|
|
8514
|
-
)
|
|
8515
8949
|
if completed.stderr.strip():
|
|
8516
8950
|
result["stderr"] = completed.stderr.strip()[:500]
|
|
8517
|
-
|
|
8951
|
+
return result
|
|
8952
|
+
|
|
8953
|
+
if not isinstance(cli_payload, dict):
|
|
8954
|
+
reason_code = "ARCHIVE_UPLOAD_RECEIPT_INVALID"
|
|
8955
|
+
try:
|
|
8956
|
+
persist_receipt(
|
|
8957
|
+
upload_status="pending",
|
|
8958
|
+
reason_code=reason_code,
|
|
8959
|
+
exit_code=completed.returncode,
|
|
8960
|
+
)
|
|
8961
|
+
except OSError:
|
|
8962
|
+
pass
|
|
8963
|
+
return {
|
|
8964
|
+
**base_result,
|
|
8965
|
+
"exitCode": completed.returncode,
|
|
8966
|
+
"reasonCode": reason_code,
|
|
8967
|
+
"warning": "归档上传命令未返回可核验的 JSON 收据;已保留 ZIP 与回执。",
|
|
8968
|
+
}
|
|
8969
|
+
|
|
8970
|
+
archive_status = cli_payload.get("archive_status")
|
|
8971
|
+
knowledge_status = cli_payload.get("knowledge_status")
|
|
8972
|
+
if archive_status != "durable" or knowledge_status not in {
|
|
8973
|
+
"indexing",
|
|
8974
|
+
"ready",
|
|
8975
|
+
"failed",
|
|
8976
|
+
}:
|
|
8977
|
+
reason_code = "ARCHIVE_UPLOAD_RECEIPT_INVALID"
|
|
8518
8978
|
try:
|
|
8519
|
-
|
|
8979
|
+
persist_receipt(
|
|
8980
|
+
upload_status="pending",
|
|
8981
|
+
reason_code=reason_code,
|
|
8982
|
+
archive_status=(
|
|
8983
|
+
archive_status if isinstance(archive_status, str) else None
|
|
8984
|
+
),
|
|
8985
|
+
knowledge_status=(
|
|
8986
|
+
knowledge_status if isinstance(knowledge_status, str) else None
|
|
8987
|
+
),
|
|
8988
|
+
exit_code=completed.returncode,
|
|
8989
|
+
)
|
|
8520
8990
|
except OSError:
|
|
8521
8991
|
pass
|
|
8992
|
+
return {
|
|
8993
|
+
**base_result,
|
|
8994
|
+
"exitCode": completed.returncode,
|
|
8995
|
+
"reasonCode": reason_code,
|
|
8996
|
+
"warning": "归档上传收据缺少有效的耐久或知识状态;已保留 ZIP 与回执。",
|
|
8997
|
+
}
|
|
8998
|
+
|
|
8999
|
+
fully_ready = archive_status == "durable" and knowledge_status == "ready"
|
|
9000
|
+
if fully_ready:
|
|
9001
|
+
upload_status = "ready"
|
|
9002
|
+
reason_code = None
|
|
9003
|
+
elif knowledge_status == "failed":
|
|
9004
|
+
upload_status = "failed"
|
|
9005
|
+
reason_code = "ARCHIVE_KNOWLEDGE_INDEX_FAILED"
|
|
9006
|
+
else:
|
|
9007
|
+
upload_status = "pending"
|
|
9008
|
+
reason_code = "ARCHIVE_KNOWLEDGE_INDEXING"
|
|
9009
|
+
try:
|
|
9010
|
+
persist_receipt(
|
|
9011
|
+
upload_status=upload_status,
|
|
9012
|
+
reason_code=reason_code,
|
|
9013
|
+
archive_status=archive_status,
|
|
9014
|
+
knowledge_status=knowledge_status,
|
|
9015
|
+
exit_code=completed.returncode,
|
|
9016
|
+
)
|
|
9017
|
+
except OSError as exc:
|
|
9018
|
+
return {
|
|
9019
|
+
**base_result,
|
|
9020
|
+
"archiveId": cli_payload.get("archive_id"),
|
|
9021
|
+
"archiveStatus": archive_status,
|
|
9022
|
+
"knowledgeStatus": knowledge_status,
|
|
9023
|
+
"reasonCode": "ARCHIVE_UPLOAD_RECEIPT_WRITE_FAILED",
|
|
9024
|
+
"warning": f"服务端已返回状态,但本地上传回执更新失败:{exc}",
|
|
9025
|
+
}
|
|
9026
|
+
|
|
9027
|
+
result = {
|
|
9028
|
+
**base_result,
|
|
9029
|
+
"ok": True,
|
|
9030
|
+
"uploadStatus": upload_status,
|
|
9031
|
+
"exitCode": completed.returncode,
|
|
9032
|
+
"archiveId": cli_payload.get("archive_id"),
|
|
9033
|
+
"archiveStatus": archive_status,
|
|
9034
|
+
"knowledgeStatus": knowledge_status,
|
|
9035
|
+
"reasonCode": reason_code,
|
|
9036
|
+
}
|
|
9037
|
+
if fully_ready:
|
|
9038
|
+
cleanup_warnings: list[str] = []
|
|
9039
|
+
for path in (package_path, pending_path):
|
|
9040
|
+
try:
|
|
9041
|
+
path.unlink(missing_ok=True)
|
|
9042
|
+
except OSError as exc:
|
|
9043
|
+
cleanup_warnings.append(f"{path.name}: {exc}")
|
|
9044
|
+
if cleanup_warnings:
|
|
9045
|
+
result["warning"] = (
|
|
9046
|
+
"远端归档与知识均已就绪,但本地重试文件清理不完整:"
|
|
9047
|
+
+ "; ".join(cleanup_warnings)
|
|
9048
|
+
)
|
|
9049
|
+
elif knowledge_status == "failed":
|
|
9050
|
+
result["warning"] = (
|
|
9051
|
+
"归档原包已在服务端持久化,但知识索引失败;"
|
|
9052
|
+
"已保留 ZIP 与上传回执,可重试同一个 ZIP。"
|
|
9053
|
+
)
|
|
9054
|
+
else:
|
|
9055
|
+
result["warning"] = (
|
|
9056
|
+
"归档原包已在服务端持久化,知识索引仍在进行;"
|
|
9057
|
+
"已保留 ZIP 与上传回执,可枚举后重试。"
|
|
9058
|
+
)
|
|
8522
9059
|
return result
|
|
8523
9060
|
|
|
8524
9061
|
|
|
9062
|
+
def _knowledge_maintenance_from_archive_push(push_result: dict[str, Any]) -> str:
|
|
9063
|
+
if (
|
|
9064
|
+
push_result.get("archiveStatus") == "durable"
|
|
9065
|
+
and push_result.get("knowledgeStatus") == "ready"
|
|
9066
|
+
):
|
|
9067
|
+
return "REMOTE_READY"
|
|
9068
|
+
if push_result.get("knowledgeStatus") == "failed":
|
|
9069
|
+
return "REMOTE_INDEX_FAILED"
|
|
9070
|
+
return "REMOTE_PENDING"
|
|
9071
|
+
|
|
9072
|
+
|
|
8525
9073
|
def _merge_runtime_state(state_dir: Path, contract_dir: Path) -> None:
|
|
8526
9074
|
"""Materialize split-v1 dynamic state into the archive contract tree."""
|
|
8527
9075
|
for source in sorted(state_dir.iterdir()):
|