@hunter-harness/workflow-harness 0.2.49 → 0.2.51
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 +11 -3
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +6 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +45 -244
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +41 -112
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +9 -4
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +2 -2
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +7 -2
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +8 -3
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +7 -2
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +12 -7
- package/harness/bundles/general/claude-code/harness-sync/reference.md +18 -11
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +7 -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 +769 -191
- package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +90 -12
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +17 -0
- 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 +11 -3
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +6 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +42 -230
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +38 -98
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +9 -4
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +2 -2
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +7 -2
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +8 -3
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +7 -2
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +13 -8
- package/harness/bundles/general/codebuddy/harness-sync/reference.md +18 -11
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +7 -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 +769 -191
- package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +90 -12
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +17 -0
- 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 +11 -3
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +6 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +42 -230
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +38 -98
- package/harness/bundles/general/codex/harness-plan/SKILL.md +9 -4
- package/harness/bundles/general/codex/harness-plan/checklist.md +2 -2
- package/harness/bundles/general/codex/harness-review/SKILL.md +7 -2
- package/harness/bundles/general/codex/harness-run/SKILL.md +8 -3
- package/harness/bundles/general/codex/harness-submit/SKILL.md +7 -2
- package/harness/bundles/general/codex/harness-sync/SKILL.md +13 -8
- package/harness/bundles/general/codex/harness-sync/reference.md +18 -11
- package/harness/bundles/general/codex/harness-test/SKILL.md +7 -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 +769 -191
- package/harness/bundles/general/codex/scripts/harness_events_sync.py +90 -12
- package/harness/bundles/general/codex/scripts/harness_gate.py +17 -0
- 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 +11 -3
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +6 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +42 -230
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +38 -98
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +9 -4
- package/harness/bundles/general/cursor/harness-plan/checklist.md +2 -2
- package/harness/bundles/general/cursor/harness-review/SKILL.md +7 -2
- package/harness/bundles/general/cursor/harness-run/SKILL.md +8 -3
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +7 -2
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +13 -8
- package/harness/bundles/general/cursor/harness-sync/reference.md +18 -11
- package/harness/bundles/general/cursor/harness-test/SKILL.md +7 -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 +769 -191
- package/harness/bundles/general/cursor/scripts/harness_events_sync.py +90 -12
- package/harness/bundles/general/cursor/scripts/harness_gate.py +17 -0
- 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 +7 -2
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +11 -3
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +6 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +45 -244
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +41 -112
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +7 -2
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +9 -4
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +2 -2
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +7 -2
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +8 -3
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +7 -2
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +12 -7
- package/harness/bundles/java/claude-code/harness-sync/reference.md +18 -11
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +7 -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 +769 -191
- package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +90 -12
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +17 -0
- 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 +7 -2
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +11 -3
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +6 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +42 -230
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +38 -98
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +7 -2
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +9 -4
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +2 -2
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +7 -2
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +8 -3
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +7 -2
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +13 -8
- package/harness/bundles/java/codebuddy/harness-sync/reference.md +18 -11
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +7 -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 +769 -191
- package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +90 -12
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +17 -0
- 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 +7 -2
- package/harness/bundles/java/codex/harness-archive/SKILL.md +11 -3
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +6 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +42 -230
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +38 -98
- package/harness/bundles/java/codex/harness-package/SKILL.md +7 -2
- package/harness/bundles/java/codex/harness-plan/SKILL.md +9 -4
- package/harness/bundles/java/codex/harness-plan/checklist.md +2 -2
- package/harness/bundles/java/codex/harness-review/SKILL.md +7 -2
- package/harness/bundles/java/codex/harness-run/SKILL.md +8 -3
- package/harness/bundles/java/codex/harness-submit/SKILL.md +7 -2
- package/harness/bundles/java/codex/harness-sync/SKILL.md +13 -8
- package/harness/bundles/java/codex/harness-sync/reference.md +18 -11
- package/harness/bundles/java/codex/harness-test/SKILL.md +7 -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 +769 -191
- package/harness/bundles/java/codex/scripts/harness_events_sync.py +90 -12
- package/harness/bundles/java/codex/scripts/harness_gate.py +17 -0
- 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 +7 -2
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +11 -3
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +6 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +42 -230
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +38 -98
- package/harness/bundles/java/cursor/harness-package/SKILL.md +7 -2
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +9 -4
- package/harness/bundles/java/cursor/harness-plan/checklist.md +2 -2
- package/harness/bundles/java/cursor/harness-review/SKILL.md +7 -2
- package/harness/bundles/java/cursor/harness-run/SKILL.md +8 -3
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +7 -2
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +13 -8
- package/harness/bundles/java/cursor/harness-sync/reference.md +18 -11
- package/harness/bundles/java/cursor/harness-test/SKILL.md +7 -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 +769 -191
- package/harness/bundles/java/cursor/scripts/harness_events_sync.py +90 -12
- package/harness/bundles/java/cursor/scripts/harness_gate.py +17 -0
- 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 +23 -47
- package/harness/manifests/general/codebuddy.json +23 -47
- package/harness/manifests/general/codex.json +23 -47
- package/harness/manifests/general/cursor.json +23 -47
- package/harness/manifests/java/claude-code.json +25 -49
- package/harness/manifests/java/codebuddy.json +25 -49
- package/harness/manifests/java/codex.json +25 -49
- package/harness/manifests/java/cursor.json +25 -49
- 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.
|
|
@@ -70,6 +70,7 @@ if str(SCRIPTS_DIR) not in sys.path:
|
|
|
70
70
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
71
71
|
|
|
72
72
|
import harness_events as he # noqa: E402
|
|
73
|
+
import harness_events_sync as hes # noqa: E402
|
|
73
74
|
import harness_efficiency as heff # noqa: E402
|
|
74
75
|
import harness_gate as hgate # noqa: E402
|
|
75
76
|
import harness_ledger as hl # noqa: E402
|
|
@@ -1923,38 +1924,12 @@ def worktree_requested(change_dir: Path) -> bool:
|
|
|
1923
1924
|
|
|
1924
1925
|
|
|
1925
1926
|
def check_archive_exact_byte_policy(project_root: Path) -> dict[str, Any]:
|
|
1926
|
-
"""
|
|
1927
|
-
required_rule = ".harness/archive/** -text"
|
|
1928
|
-
attributes_path = project_root / ".gitattributes"
|
|
1929
|
-
rules: list[str] = []
|
|
1930
|
-
if attributes_path.is_file():
|
|
1931
|
-
try:
|
|
1932
|
-
rules = [
|
|
1933
|
-
line.strip()
|
|
1934
|
-
for line in attributes_path.read_text(encoding="utf-8-sig").splitlines()
|
|
1935
|
-
if line.strip() and not line.lstrip().startswith("#")
|
|
1936
|
-
]
|
|
1937
|
-
except OSError:
|
|
1938
|
-
rules = []
|
|
1939
|
-
matching = []
|
|
1940
|
-
for rule in rules:
|
|
1941
|
-
fields = rule.split()
|
|
1942
|
-
if not fields or fields[0] != ".harness/archive/**":
|
|
1943
|
-
continue
|
|
1944
|
-
matching.append(rule)
|
|
1945
|
-
if "-text" in fields[1:] or "binary" in fields[1:]:
|
|
1946
|
-
return {
|
|
1947
|
-
"ok": True,
|
|
1948
|
-
"path": str(attributes_path),
|
|
1949
|
-
"requiredRule": required_rule,
|
|
1950
|
-
"matchedRule": rule,
|
|
1951
|
-
}
|
|
1927
|
+
"""Exact bytes are guaranteed by ZIP entry hashes, independent of Git."""
|
|
1952
1928
|
return {
|
|
1953
|
-
"ok":
|
|
1954
|
-
"
|
|
1955
|
-
"
|
|
1956
|
-
"
|
|
1957
|
-
"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",
|
|
1958
1933
|
}
|
|
1959
1934
|
|
|
1960
1935
|
|
|
@@ -7000,79 +6975,6 @@ def materialize_repository_artifacts(change_dir: Path) -> dict[str, Any]:
|
|
|
7000
6975
|
return result
|
|
7001
6976
|
|
|
7002
6977
|
|
|
7003
|
-
def run_knowledge_poststeps(project_root: Path) -> dict[str, Any]:
|
|
7004
|
-
results: dict[str, Any] = {"ran": False, "steps": [], "warnings": []}
|
|
7005
|
-
if not KNOWLEDGE_SCRIPT.is_file():
|
|
7006
|
-
results["warnings"].append(f"harness_knowledge.py not found: {KNOWLEDGE_SCRIPT}")
|
|
7007
|
-
return results
|
|
7008
|
-
results["ran"] = True
|
|
7009
|
-
for cmd in ("ingest", "dedupe", "auto-supersede", "reverify-stale"):
|
|
7010
|
-
try:
|
|
7011
|
-
proc = subprocess.run(
|
|
7012
|
-
[
|
|
7013
|
-
sys.executable,
|
|
7014
|
-
str(KNOWLEDGE_SCRIPT),
|
|
7015
|
-
cmd,
|
|
7016
|
-
"--project",
|
|
7017
|
-
str(project_root),
|
|
7018
|
-
"--json",
|
|
7019
|
-
],
|
|
7020
|
-
capture_output=True,
|
|
7021
|
-
text=True,
|
|
7022
|
-
encoding="utf-8",
|
|
7023
|
-
errors="replace",
|
|
7024
|
-
timeout=300,
|
|
7025
|
-
check=False,
|
|
7026
|
-
)
|
|
7027
|
-
step = {
|
|
7028
|
-
"command": cmd,
|
|
7029
|
-
"exit_code": proc.returncode,
|
|
7030
|
-
"ok": proc.returncode == 0,
|
|
7031
|
-
}
|
|
7032
|
-
if proc.returncode != 0:
|
|
7033
|
-
step["stderr"] = (proc.stderr or proc.stdout or "")[:500]
|
|
7034
|
-
results["warnings"].append(f"knowledge {cmd} exit {proc.returncode}")
|
|
7035
|
-
results["steps"].append(step)
|
|
7036
|
-
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
7037
|
-
results["steps"].append({"command": cmd, "ok": False, "error": str(exc)})
|
|
7038
|
-
results["warnings"].append(f"knowledge {cmd} failed: {exc}")
|
|
7039
|
-
return results
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
def enqueue_maintenance_outbox(project_root: Path, archive_dir: Path) -> dict[str, Any]:
|
|
7043
|
-
"""§8.2: write a pending maintenance-outbox item instead of synchronously
|
|
7044
|
-
running the four knowledge subprocesses. Never rolls back the archive."""
|
|
7045
|
-
pending_dir = (
|
|
7046
|
-
project_root / ".harness" / "knowledge" / "maintenance-outbox" / "pending"
|
|
7047
|
-
)
|
|
7048
|
-
pending_dir.mkdir(parents=True, exist_ok=True)
|
|
7049
|
-
archive_id = archive_dir.name
|
|
7050
|
-
manifest = archive_dir / "evidence" / "archive-manifest-after.json"
|
|
7051
|
-
manifest_hash = "sha256:" + sha256_file(manifest) if manifest.is_file() else ""
|
|
7052
|
-
try:
|
|
7053
|
-
rel = archive_dir.resolve().relative_to(project_root.resolve()).as_posix()
|
|
7054
|
-
except ValueError:
|
|
7055
|
-
rel = str(archive_dir)
|
|
7056
|
-
item = {
|
|
7057
|
-
"schemaVersion": 1,
|
|
7058
|
-
"archiveId": archive_id,
|
|
7059
|
-
"archivePath": rel,
|
|
7060
|
-
"archiveManifestHash": manifest_hash,
|
|
7061
|
-
"status": "pending",
|
|
7062
|
-
"attempts": 0,
|
|
7063
|
-
"createdAt": now_iso(),
|
|
7064
|
-
"lastError": None,
|
|
7065
|
-
}
|
|
7066
|
-
item_path = pending_dir / f"{archive_id}.json"
|
|
7067
|
-
write_json(item_path, item)
|
|
7068
|
-
return {
|
|
7069
|
-
"queued": True,
|
|
7070
|
-
"outboxPath": str(item_path),
|
|
7071
|
-
"archiveId": archive_id,
|
|
7072
|
-
"status": "pending",
|
|
7073
|
-
}
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
6978
|
def run_service_stop(change_dir: Path) -> dict[str, Any]:
|
|
7077
6979
|
if not SERVICE_SCRIPT.is_file():
|
|
7078
6980
|
return {
|
|
@@ -8284,6 +8186,9 @@ def cmd_finalize(
|
|
|
8284
8186
|
|
|
8285
8187
|
# Publish only after every validator passes. The archive path is never used
|
|
8286
8188
|
# as mutable staging, so a failed attempt cannot poison the next retry.
|
|
8189
|
+
# Capture monitoring run_id from the live change path before it is deleted
|
|
8190
|
+
# so archive finalize can report terminal status on the same platform run.
|
|
8191
|
+
monitoring_run_id = hes.run_id_for(original_change_dir)
|
|
8287
8192
|
try:
|
|
8288
8193
|
archive_root.mkdir(parents=True, exist_ok=True)
|
|
8289
8194
|
shutil.move(str(operation_temp_dir), str(archive_dir))
|
|
@@ -8321,19 +8226,15 @@ def cmd_finalize(
|
|
|
8321
8226
|
warnings.append(f"could not remove archived runtime state: {exc}")
|
|
8322
8227
|
payload["steps"]["delete_runtime_state"] = {"ok": False, "error": str(exc)}
|
|
8323
8228
|
|
|
8324
|
-
# --- 12.
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
except OSError as exc:
|
|
8334
|
-
warnings.append(f"maintenance outbox enqueue failed: {exc}")
|
|
8335
|
-
payload["steps"]["knowledge"] = {"queued": False, "error": str(exc)}
|
|
8336
|
-
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"
|
|
8337
8238
|
|
|
8338
8239
|
service_result = run_service_stop(work_dir)
|
|
8339
8240
|
payload["steps"]["service_stop"] = service_result
|
|
@@ -8361,13 +8262,36 @@ def cmd_finalize(
|
|
|
8361
8262
|
except OSError as exc:
|
|
8362
8263
|
warnings.append(f"could not persist completed archive operation: {exc}")
|
|
8363
8264
|
|
|
8364
|
-
#
|
|
8365
|
-
|
|
8366
|
-
|
|
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
|
+
)
|
|
8367
8271
|
payload["steps"]["archive_push"] = push_result
|
|
8272
|
+
payload["knowledgeMaintenance"] = _knowledge_maintenance_from_archive_push(
|
|
8273
|
+
push_result
|
|
8274
|
+
)
|
|
8368
8275
|
if push_result.get("warning"):
|
|
8369
8276
|
warnings.append(str(push_result["warning"]))
|
|
8370
8277
|
|
|
8278
|
+
# C3: best-effort terminal events-sync so platform run ends (succeeded/failed).
|
|
8279
|
+
# Use the pre-delete change-path run_id + original change_key for continuity.
|
|
8280
|
+
try:
|
|
8281
|
+
monitor = hes.auto_events_sync(
|
|
8282
|
+
project_root,
|
|
8283
|
+
archive_dir,
|
|
8284
|
+
run_id=monitoring_run_id,
|
|
8285
|
+
change_key=change_name,
|
|
8286
|
+
)
|
|
8287
|
+
payload["steps"]["platform_events_sync"] = monitor
|
|
8288
|
+
if monitor.get("warning"):
|
|
8289
|
+
warnings.append(str(monitor["warning"]))
|
|
8290
|
+
except Exception as exc: # noqa: BLE001 — never roll back archive
|
|
8291
|
+
warn = f"events-sync terminal hook failed: {exc}"
|
|
8292
|
+
warnings.append(warn)
|
|
8293
|
+
payload["steps"]["platform_events_sync"] = {"ok": False, "warning": warn}
|
|
8294
|
+
|
|
8371
8295
|
payload["ok"] = True
|
|
8372
8296
|
payload["finalStatus"] = "OK"
|
|
8373
8297
|
payload["warnings"] = warnings
|
|
@@ -8376,80 +8300,597 @@ def cmd_finalize(
|
|
|
8376
8300
|
return 0, payload
|
|
8377
8301
|
|
|
8378
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
|
+
|
|
8379
8421
|
def _remote_credentials_configured(project_root: Path) -> bool:
|
|
8380
|
-
|
|
8381
|
-
|
|
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:
|
|
8382
8442
|
return False
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
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
|
|
8387
8567
|
|
|
8388
8568
|
|
|
8389
8569
|
def collect_archive_core_paths(project_root: Path, archive_dir: Path) -> list[str]:
|
|
8390
|
-
"""
|
|
8391
|
-
root = project_root
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
return
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
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.
|
|
8420
8757
|
"""
|
|
8421
8758
|
project_root = project_root.resolve()
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
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] = {
|
|
8426
8799
|
"schemaVersion": 1,
|
|
8427
8800
|
"generatedAt": now_iso(),
|
|
8801
|
+
"updatedAt": now_iso(),
|
|
8428
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"],
|
|
8429
8808
|
"paths": core_paths,
|
|
8809
|
+
"uploadStatus": "pending",
|
|
8810
|
+
"reasonCode": initial_reason_code,
|
|
8811
|
+
"archiveStatus": None,
|
|
8812
|
+
"knowledgeStatus": None,
|
|
8430
8813
|
}
|
|
8431
|
-
|
|
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
|
+
|
|
8432
8835
|
try:
|
|
8433
8836
|
pending_path.parent.mkdir(parents=True, exist_ok=True)
|
|
8434
|
-
|
|
8837
|
+
persist_receipt(
|
|
8838
|
+
upload_status="pending",
|
|
8839
|
+
reason_code=initial_reason_code,
|
|
8840
|
+
)
|
|
8435
8841
|
except OSError as exc:
|
|
8436
8842
|
return {
|
|
8437
8843
|
"skipped": False,
|
|
8438
8844
|
"ok": False,
|
|
8439
8845
|
"paths": core_paths,
|
|
8440
|
-
"
|
|
8846
|
+
"packagePath": package["packagePath"],
|
|
8847
|
+
"packageSha256": package["packageSha256"],
|
|
8848
|
+
"reasonCode": "ARCHIVE_UPLOAD_RECEIPT_WRITE_FAILED",
|
|
8849
|
+
"warning": f"无法写入归档上传回执:{exc}",
|
|
8441
8850
|
}
|
|
8442
8851
|
|
|
8443
|
-
|
|
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.
|
|
8444
8877
|
command = [
|
|
8445
8878
|
"npx",
|
|
8446
8879
|
"--yes",
|
|
8447
8880
|
"hunter-harness",
|
|
8448
|
-
"
|
|
8881
|
+
"archive",
|
|
8882
|
+
"upload",
|
|
8883
|
+
"--file",
|
|
8884
|
+
str(package["packagePath"]),
|
|
8885
|
+
"--change-key",
|
|
8886
|
+
effective_change_key,
|
|
8449
8887
|
"--yes",
|
|
8450
8888
|
"--non-interactive",
|
|
8451
8889
|
"--json",
|
|
8452
8890
|
]
|
|
8891
|
+
command.extend(["--server-url", str(credentials["serverUrl"])])
|
|
8892
|
+
if credentials["tokenEnv"] is not None:
|
|
8893
|
+
command.extend(["--token-env", str(credentials["tokenEnv"])])
|
|
8453
8894
|
try:
|
|
8454
8895
|
completed = subprocess.run(
|
|
8455
8896
|
command,
|
|
@@ -8461,37 +8902,174 @@ def auto_push_archive_core(project_root: Path, archive_dir: Path) -> dict[str, A
|
|
|
8461
8902
|
shell=False,
|
|
8462
8903
|
)
|
|
8463
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
|
|
8464
8910
|
return {
|
|
8465
|
-
|
|
8466
|
-
"
|
|
8467
|
-
"
|
|
8468
|
-
|
|
8469
|
-
|
|
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
|
+
),
|
|
8470
8948
|
}
|
|
8471
|
-
ok = completed.returncode == 0
|
|
8472
|
-
result: dict[str, Any] = {
|
|
8473
|
-
"skipped": False,
|
|
8474
|
-
"ok": ok,
|
|
8475
|
-
"paths": core_paths,
|
|
8476
|
-
"pending": str(pending_path),
|
|
8477
|
-
"exitCode": completed.returncode,
|
|
8478
|
-
}
|
|
8479
|
-
if not ok:
|
|
8480
|
-
result["warning"] = (
|
|
8481
|
-
"archive auto-push failed (exit "
|
|
8482
|
-
+ str(completed.returncode)
|
|
8483
|
-
+ "); pending receipt kept for retry via `hunter-harness push`"
|
|
8484
|
-
)
|
|
8485
8949
|
if completed.stderr.strip():
|
|
8486
8950
|
result["stderr"] = completed.stderr.strip()[:500]
|
|
8487
|
-
|
|
8951
|
+
return result
|
|
8952
|
+
|
|
8953
|
+
if not isinstance(cli_payload, dict):
|
|
8954
|
+
reason_code = "ARCHIVE_UPLOAD_RECEIPT_INVALID"
|
|
8488
8955
|
try:
|
|
8489
|
-
|
|
8956
|
+
persist_receipt(
|
|
8957
|
+
upload_status="pending",
|
|
8958
|
+
reason_code=reason_code,
|
|
8959
|
+
exit_code=completed.returncode,
|
|
8960
|
+
)
|
|
8490
8961
|
except OSError:
|
|
8491
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"
|
|
8978
|
+
try:
|
|
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
|
+
)
|
|
8990
|
+
except OSError:
|
|
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
|
+
)
|
|
8492
9059
|
return result
|
|
8493
9060
|
|
|
8494
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
|
+
|
|
8495
9073
|
def _merge_runtime_state(state_dir: Path, contract_dir: Path) -> None:
|
|
8496
9074
|
"""Materialize split-v1 dynamic state into the archive contract tree."""
|
|
8497
9075
|
for source in sorted(state_dir.iterdir()):
|