@hunter-harness/workflow-harness 0.2.29 → 0.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +323 -32
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +28 -51
- package/harness/bundles/general/claude-code/harness-sync/reference.md +89 -142
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_change.py +189 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +323 -32
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +30 -51
- package/harness/bundles/general/codebuddy/harness-sync/reference.md +89 -142
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_change.py +189 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +323 -32
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +30 -51
- package/harness/bundles/general/codex/harness-sync/reference.md +89 -142
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_change.py +189 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +323 -32
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +30 -51
- package/harness/bundles/general/cursor/harness-sync/reference.md +89 -142
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_change.py +189 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +323 -32
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +28 -51
- package/harness/bundles/java/claude-code/harness-sync/reference.md +89 -142
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_change.py +189 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +323 -32
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +30 -51
- package/harness/bundles/java/codebuddy/harness-sync/reference.md +89 -142
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_change.py +189 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +323 -32
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +30 -51
- package/harness/bundles/java/codex/harness-sync/reference.md +89 -142
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_change.py +189 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +323 -32
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +30 -51
- package/harness/bundles/java/cursor/harness-sync/reference.md +89 -142
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_change.py +189 -0
- package/harness/manifests/general/claude-code.json +24 -15
- package/harness/manifests/general/codebuddy.json +24 -15
- package/harness/manifests/general/codex.json +24 -15
- package/harness/manifests/general/cursor.json +24 -15
- package/harness/manifests/java/claude-code.json +26 -17
- package/harness/manifests/java/codebuddy.json +26 -17
- package/harness/manifests/java/codex.json +26 -17
- package/harness/manifests/java/cursor.json +26 -17
- package/hunter-workflow-family.json +18 -2
- package/package.json +1 -1
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=370a50726f655b0c; overlay=none; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-archive — 变更归档
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=370a50726f655b0c; overlay=none; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-codebase-map — 代码库地图
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=370a50726f655b0c; overlay=none; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-knowledge-ingest
|
|
25
25
|
|
|
26
26
|
从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
|
package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py
CHANGED
|
@@ -19,6 +19,7 @@ import shutil
|
|
|
19
19
|
import sqlite3
|
|
20
20
|
import subprocess
|
|
21
21
|
import sys
|
|
22
|
+
import time
|
|
22
23
|
from difflib import SequenceMatcher
|
|
23
24
|
from pathlib import Path
|
|
24
25
|
from typing import Any
|
|
@@ -40,6 +41,14 @@ ENTRY_TYPES = {
|
|
|
40
41
|
"api-contract",
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
ENTRY_ID_SCHEMA_VERSION = 2
|
|
45
|
+
ARCHIVE_CACHE_SCHEMA_VERSION = 2
|
|
46
|
+
EXTRACTOR_SCHEMA_VERSION = 2
|
|
47
|
+
_GIT_REPO_CACHE: dict[str, bool] = {}
|
|
48
|
+
_GIT_COMMIT_CACHE: dict[tuple[str, str], bool] = {}
|
|
49
|
+
_GIT_DIFF_CACHE: dict[tuple[str, str, tuple[str, ...]], list[str] | None] = {}
|
|
50
|
+
_GIT_SUBPROCESS_COUNT = 0
|
|
51
|
+
|
|
43
52
|
DEFAULT_AUTO_KNOWLEDGE_CONFIG = {
|
|
44
53
|
"autoPromote": {
|
|
45
54
|
"enabled": True,
|
|
@@ -303,16 +312,28 @@ def git_head(project: Path) -> str | None:
|
|
|
303
312
|
|
|
304
313
|
|
|
305
314
|
def is_git_repo(project: Path) -> bool:
|
|
315
|
+
key = str(project.resolve())
|
|
316
|
+
if key in _GIT_REPO_CACHE:
|
|
317
|
+
return _GIT_REPO_CACHE[key]
|
|
306
318
|
result = run_git(project, ["rev-parse", "--is-inside-work-tree"])
|
|
307
|
-
|
|
319
|
+
value = result.returncode == 0 and result.stdout.strip() == "true"
|
|
320
|
+
_GIT_REPO_CACHE[key] = value
|
|
321
|
+
return value
|
|
308
322
|
|
|
309
323
|
|
|
310
324
|
def git_commit_exists(project: Path, commit: str) -> bool:
|
|
325
|
+
key = (str(project.resolve()), commit)
|
|
326
|
+
if key in _GIT_COMMIT_CACHE:
|
|
327
|
+
return _GIT_COMMIT_CACHE[key]
|
|
311
328
|
result = run_git(project, ["cat-file", "-e", f"{commit}^{{commit}}"])
|
|
312
|
-
|
|
329
|
+
value = result.returncode == 0
|
|
330
|
+
_GIT_COMMIT_CACHE[key] = value
|
|
331
|
+
return value
|
|
313
332
|
|
|
314
333
|
|
|
315
334
|
def run_git(project: Path, args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
335
|
+
global _GIT_SUBPROCESS_COUNT
|
|
336
|
+
_GIT_SUBPROCESS_COUNT += 1
|
|
316
337
|
try:
|
|
317
338
|
return subprocess.run(
|
|
318
339
|
["git", "-C", str(project), *args],
|
|
@@ -741,22 +762,65 @@ def make_entry(
|
|
|
741
762
|
"source commit missing from local git history: " + final_commit[:12]
|
|
742
763
|
)
|
|
743
764
|
elif source_files:
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
765
|
+
diff_key = (
|
|
766
|
+
str(project.resolve()),
|
|
767
|
+
final_commit,
|
|
768
|
+
tuple(sorted(source_files)),
|
|
769
|
+
)
|
|
770
|
+
changed = _GIT_DIFF_CACHE.get(diff_key)
|
|
771
|
+
if changed is None and diff_key not in _GIT_DIFF_CACHE:
|
|
772
|
+
diff = run_git(
|
|
773
|
+
project,
|
|
774
|
+
[
|
|
775
|
+
"diff",
|
|
776
|
+
"--name-only",
|
|
777
|
+
f"{final_commit}..HEAD",
|
|
778
|
+
"--",
|
|
779
|
+
*sorted(source_files),
|
|
780
|
+
],
|
|
781
|
+
)
|
|
782
|
+
changed = (
|
|
783
|
+
[
|
|
784
|
+
line.strip()
|
|
785
|
+
for line in diff.stdout.splitlines()
|
|
786
|
+
if line.strip()
|
|
787
|
+
]
|
|
788
|
+
if diff.returncode == 0
|
|
789
|
+
else None
|
|
790
|
+
)
|
|
791
|
+
_GIT_DIFF_CACHE[diff_key] = changed
|
|
792
|
+
if changed is not None:
|
|
747
793
|
if changed:
|
|
748
794
|
stale_reasons.append(
|
|
749
795
|
"source files changed after source commit: " + ", ".join(changed[:8])
|
|
750
796
|
)
|
|
751
797
|
else:
|
|
752
|
-
|
|
753
|
-
|
|
798
|
+
stale_reasons.append(
|
|
799
|
+
"source commit could not be compared with current HEAD"
|
|
800
|
+
)
|
|
754
801
|
|
|
755
802
|
if stale_reasons:
|
|
756
803
|
status = "stale"
|
|
757
804
|
|
|
758
|
-
|
|
759
|
-
|
|
805
|
+
canonical_body = body.strip()
|
|
806
|
+
identity = json.dumps(
|
|
807
|
+
{
|
|
808
|
+
"schemaVersion": ENTRY_ID_SCHEMA_VERSION,
|
|
809
|
+
"projectId": project_name,
|
|
810
|
+
"sourceIdentity": {
|
|
811
|
+
"changeName": str(summary.get("changeName") or archive),
|
|
812
|
+
},
|
|
813
|
+
"type": entry_type,
|
|
814
|
+
"title": title.strip(),
|
|
815
|
+
"body": canonical_body,
|
|
816
|
+
},
|
|
817
|
+
ensure_ascii=False,
|
|
818
|
+
sort_keys=True,
|
|
819
|
+
separators=(",", ":"),
|
|
820
|
+
)
|
|
821
|
+
entry_id = ".".join(
|
|
822
|
+
[project_name, slugify(archive, 80), entry_type, short_hash(identity)]
|
|
823
|
+
)
|
|
760
824
|
archive_dir = archive_dir_from_summary(summary_path)
|
|
761
825
|
|
|
762
826
|
return {
|
|
@@ -767,7 +831,7 @@ def make_entry(
|
|
|
767
831
|
"status": status,
|
|
768
832
|
"title": title.strip(),
|
|
769
833
|
"summary": first_sentence(body),
|
|
770
|
-
"body":
|
|
834
|
+
"body": canonical_body,
|
|
771
835
|
"keywords": sorted({kw for kw in keywords if kw}),
|
|
772
836
|
"source": {
|
|
773
837
|
"archive": rel_to_project(project, archive_dir),
|
|
@@ -1065,9 +1129,11 @@ def load_entries_from_dir(path: Path) -> list[dict[str, Any]]:
|
|
|
1065
1129
|
|
|
1066
1130
|
|
|
1067
1131
|
def archive_entry_cache_path(project: Path, knowledge: Path, summary_path: Path, summary_hash: str) -> Path:
|
|
1068
|
-
|
|
1069
|
-
cache_id = short_hash(
|
|
1070
|
-
|
|
1132
|
+
del project, summary_path
|
|
1133
|
+
cache_id = short_hash(
|
|
1134
|
+
f"extractor:{EXTRACTOR_SCHEMA_VERSION}|summary:{summary_hash}", 24
|
|
1135
|
+
)
|
|
1136
|
+
filename = f"content-{cache_id}.json"
|
|
1071
1137
|
return knowledge / "cache" / "archive-entries" / filename
|
|
1072
1138
|
|
|
1073
1139
|
|
|
@@ -1085,9 +1151,7 @@ def load_cached_archive_entries(
|
|
|
1085
1151
|
return None
|
|
1086
1152
|
if not isinstance(payload, dict):
|
|
1087
1153
|
return None
|
|
1088
|
-
if payload.get("schemaVersion") !=
|
|
1089
|
-
return None
|
|
1090
|
-
if payload.get("summaryData") != summary_path:
|
|
1154
|
+
if payload.get("schemaVersion") != ARCHIVE_CACHE_SCHEMA_VERSION:
|
|
1091
1155
|
return None
|
|
1092
1156
|
if payload.get("summarySha256") != summary_hash:
|
|
1093
1157
|
return None
|
|
@@ -1112,7 +1176,8 @@ def write_cached_archive_entries(
|
|
|
1112
1176
|
write_json(
|
|
1113
1177
|
cache_path,
|
|
1114
1178
|
{
|
|
1115
|
-
"schemaVersion":
|
|
1179
|
+
"schemaVersion": ARCHIVE_CACHE_SCHEMA_VERSION,
|
|
1180
|
+
"extractorSchemaVersion": EXTRACTOR_SCHEMA_VERSION,
|
|
1116
1181
|
"generatedAt": now_iso(),
|
|
1117
1182
|
"summaryData": summary_path,
|
|
1118
1183
|
"summarySha256": summary_hash,
|
|
@@ -1121,6 +1186,43 @@ def write_cached_archive_entries(
|
|
|
1121
1186
|
)
|
|
1122
1187
|
|
|
1123
1188
|
|
|
1189
|
+
def rebind_cached_archive_entries(
|
|
1190
|
+
project: Path,
|
|
1191
|
+
summary_path: Path,
|
|
1192
|
+
summary_hash: str,
|
|
1193
|
+
entries: list[dict[str, Any]],
|
|
1194
|
+
) -> list[dict[str, Any]]:
|
|
1195
|
+
rebound = json_clone(entries)
|
|
1196
|
+
summary_rel = rel_to_project(project, summary_path)
|
|
1197
|
+
archive_rel = rel_to_project(project, archive_dir_from_summary(summary_path))
|
|
1198
|
+
for entry in rebound:
|
|
1199
|
+
source = entry.setdefault("source", {})
|
|
1200
|
+
source["archive"] = archive_rel
|
|
1201
|
+
source["summaryData"] = summary_rel
|
|
1202
|
+
source["summarySha256"] = summary_hash
|
|
1203
|
+
return rebound
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
def assert_unique_entry_ids(entries: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
|
1207
|
+
unique: dict[str, dict[str, Any]] = {}
|
|
1208
|
+
fingerprints: dict[str, str] = {}
|
|
1209
|
+
for entry in entries:
|
|
1210
|
+
entry_id = str(entry.get("id") or "")
|
|
1211
|
+
if not entry_id:
|
|
1212
|
+
raise ValueError("ENTRY_ID_MISSING: knowledge entry has no id")
|
|
1213
|
+
fingerprint = json.dumps(
|
|
1214
|
+
entry, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
1215
|
+
)
|
|
1216
|
+
previous = fingerprints.get(entry_id)
|
|
1217
|
+
if previous is not None and previous != fingerprint:
|
|
1218
|
+
raise ValueError(
|
|
1219
|
+
f"ENTRY_ID_COLLISION: id {entry_id} maps to different payloads"
|
|
1220
|
+
)
|
|
1221
|
+
fingerprints[entry_id] = fingerprint
|
|
1222
|
+
unique[entry_id] = entry
|
|
1223
|
+
return unique
|
|
1224
|
+
|
|
1225
|
+
|
|
1124
1226
|
def load_preserved_entries(knowledge: Path) -> list[dict[str, Any]]:
|
|
1125
1227
|
active_entries = load_entries_from_dir(knowledge / "entries" / "active")
|
|
1126
1228
|
manually_decided_entries = [
|
|
@@ -2299,6 +2401,45 @@ class KnowledgeSnapshot:
|
|
|
2299
2401
|
self.inputs_hash = inputs_hash
|
|
2300
2402
|
|
|
2301
2403
|
|
|
2404
|
+
class ProgressReporter:
|
|
2405
|
+
def __init__(self, mode: str = "none") -> None:
|
|
2406
|
+
self.mode = mode
|
|
2407
|
+
self.started = time.monotonic()
|
|
2408
|
+
self.last_emit = 0.0
|
|
2409
|
+
|
|
2410
|
+
def emit(
|
|
2411
|
+
self,
|
|
2412
|
+
phase: str,
|
|
2413
|
+
completed: int,
|
|
2414
|
+
total: int,
|
|
2415
|
+
*,
|
|
2416
|
+
force: bool = False,
|
|
2417
|
+
metrics: dict[str, Any] | None = None,
|
|
2418
|
+
) -> None:
|
|
2419
|
+
if self.mode == "none":
|
|
2420
|
+
return
|
|
2421
|
+
now = time.monotonic()
|
|
2422
|
+
if not force and now - self.last_emit < 10.0:
|
|
2423
|
+
return
|
|
2424
|
+
payload = {
|
|
2425
|
+
"phase": phase,
|
|
2426
|
+
"completed": completed,
|
|
2427
|
+
"total": total,
|
|
2428
|
+
"elapsedSeconds": round(now - self.started, 3),
|
|
2429
|
+
"heartbeat": not force,
|
|
2430
|
+
**(metrics or {}),
|
|
2431
|
+
}
|
|
2432
|
+
if self.mode == "jsonl":
|
|
2433
|
+
sys.stderr.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
2434
|
+
else:
|
|
2435
|
+
sys.stderr.write(
|
|
2436
|
+
f"[knowledge] {phase} {completed}/{total} "
|
|
2437
|
+
f"elapsed={payload['elapsedSeconds']}s\n"
|
|
2438
|
+
)
|
|
2439
|
+
sys.stderr.flush()
|
|
2440
|
+
self.last_emit = now
|
|
2441
|
+
|
|
2442
|
+
|
|
2302
2443
|
def build_snapshot(project: Path) -> KnowledgeSnapshot:
|
|
2303
2444
|
"""Load config + archive records + inputs_hash exactly once for one invocation."""
|
|
2304
2445
|
project = project.resolve()
|
|
@@ -2318,7 +2459,11 @@ def build_index(
|
|
|
2318
2459
|
incremental: bool = True,
|
|
2319
2460
|
*,
|
|
2320
2461
|
snapshot: KnowledgeSnapshot | None = None,
|
|
2462
|
+
progress: ProgressReporter | None = None,
|
|
2321
2463
|
) -> dict[str, Any]:
|
|
2464
|
+
global _GIT_SUBPROCESS_COUNT
|
|
2465
|
+
_GIT_SUBPROCESS_COUNT = 0
|
|
2466
|
+
reporter = progress or ProgressReporter("none")
|
|
2322
2467
|
project = project.resolve()
|
|
2323
2468
|
if snapshot is not None:
|
|
2324
2469
|
# Reuse the single-invocation snapshot: no recomputation of
|
|
@@ -2337,6 +2482,7 @@ def build_index(
|
|
|
2337
2482
|
archive_records = archive_summary_records(project, summary_paths)
|
|
2338
2483
|
inputs_hash = compute_inputs_hash(archive_records, config, knowledge)
|
|
2339
2484
|
ensure_knowledge_dirs(knowledge)
|
|
2485
|
+
reporter.emit("snapshot", len(summary_paths), len(summary_paths), force=True)
|
|
2340
2486
|
|
|
2341
2487
|
# No-op fast path: inputs (archive checksums + config + schema) are unchanged.
|
|
2342
2488
|
# Write nothing — entries, sqlite, index and views all stay byte-identical,
|
|
@@ -2401,7 +2547,7 @@ def build_index(
|
|
|
2401
2547
|
"sqliteDelete": 0,
|
|
2402
2548
|
}
|
|
2403
2549
|
|
|
2404
|
-
for summary_path in summary_paths:
|
|
2550
|
+
for summary_index, summary_path in enumerate(summary_paths, start=1):
|
|
2405
2551
|
summary_rel = rel_to_project(project, summary_path)
|
|
2406
2552
|
summary_hash = sha256_file(summary_path)
|
|
2407
2553
|
cache_path = archive_entry_cache_path(project, knowledge, summary_path, summary_hash)
|
|
@@ -2426,6 +2572,9 @@ def build_index(
|
|
|
2426
2572
|
ingest_mode["cacheWrites"] += 1
|
|
2427
2573
|
else:
|
|
2428
2574
|
ingest_mode["archivesReused"] += 1
|
|
2575
|
+
archive_entries = rebind_cached_archive_entries(
|
|
2576
|
+
project, summary_path, summary_hash, archive_entries
|
|
2577
|
+
)
|
|
2429
2578
|
# API-006/RET-40: entries from archives failing the publication
|
|
2430
2579
|
# gate stay quarantined (candidate only, never active/promoted).
|
|
2431
2580
|
archive_rel = rel_to_project(project, archive_dir_from_summary(summary_path))
|
|
@@ -2441,6 +2590,7 @@ def build_index(
|
|
|
2441
2590
|
entry["status"] = "candidate"
|
|
2442
2591
|
ingest_mode["activeAutoDemoted"] += 1
|
|
2443
2592
|
entries.extend(archive_entries)
|
|
2593
|
+
reporter.emit("archive-extract", summary_index, len(summary_paths))
|
|
2444
2594
|
except Exception as exc: # keep one bad archive from blocking the index
|
|
2445
2595
|
failures.append({"path": rel_to_project(project, summary_path), "error": str(exc)})
|
|
2446
2596
|
|
|
@@ -2468,8 +2618,11 @@ def build_index(
|
|
|
2468
2618
|
seen.add(fp)
|
|
2469
2619
|
deduped.append(entry)
|
|
2470
2620
|
|
|
2471
|
-
|
|
2621
|
+
reporter.emit("near-deduplicate", 0, len(deduped), force=True)
|
|
2622
|
+
near_dedupe = dedupe_near_duplicates(deduped, progress=reporter)
|
|
2472
2623
|
ingest_mode["nearDuplicatesMerged"] = near_dedupe["merged"]
|
|
2624
|
+
ingest_mode["similarityCandidates"] = near_dedupe["similarityCandidates"]
|
|
2625
|
+
ingest_mode["exactSimilarityComparisons"] = near_dedupe["exactSimilarityComparisons"]
|
|
2473
2626
|
|
|
2474
2627
|
mark_conflicting_generated_entries(deduped)
|
|
2475
2628
|
mark_degraded_test_evidence(deduped)
|
|
@@ -2479,8 +2632,10 @@ def build_index(
|
|
|
2479
2632
|
auto_promotions = apply_auto_promote_policy(deduped, config)
|
|
2480
2633
|
ingest_mode["confidenceScored"] = len(deduped)
|
|
2481
2634
|
ingest_mode["candidateAutoPromoted"] = len(auto_promotions)
|
|
2635
|
+
assert_unique_entry_ids(deduped)
|
|
2482
2636
|
|
|
2483
2637
|
preserved_ids = {entry["id"] for entry in load_preserved_entries(knowledge)}
|
|
2638
|
+
failed_entry_ids: set[str] = set()
|
|
2484
2639
|
for entry in deduped:
|
|
2485
2640
|
if entry["id"] in preserved_ids:
|
|
2486
2641
|
continue
|
|
@@ -2495,12 +2650,20 @@ def build_index(
|
|
|
2495
2650
|
if isinstance(existing, dict) and existing.get("id") != entry["id"]:
|
|
2496
2651
|
failures.append({"id": entry["id"], "reason": "filename collision",
|
|
2497
2652
|
"path": str(target), "conflictsWith": existing.get("id")})
|
|
2653
|
+
failed_entry_ids.add(entry["id"])
|
|
2498
2654
|
continue
|
|
2499
2655
|
_preserve_confidence_timestamp(target, entry)
|
|
2500
2656
|
if write_json_if_changed(target, entry):
|
|
2501
2657
|
ingest_mode["entriesWritten"] += 1
|
|
2502
2658
|
|
|
2503
|
-
|
|
2659
|
+
persisted_generated = [
|
|
2660
|
+
entry for entry in deduped if entry["id"] not in failed_entry_ids
|
|
2661
|
+
]
|
|
2662
|
+
indexed_entries = combine_generated_with_preserved(
|
|
2663
|
+
knowledge, persisted_generated
|
|
2664
|
+
)
|
|
2665
|
+
indexed_by_id = assert_unique_entry_ids(indexed_entries)
|
|
2666
|
+
indexed_entries = list(indexed_by_id.values())
|
|
2504
2667
|
apply_confidence_scores(indexed_entries, config)
|
|
2505
2668
|
ingest_mode["entriesWritten"] += persist_entry_updates(knowledge, indexed_entries)
|
|
2506
2669
|
auto_demotions = apply_active_lifecycle_policy(knowledge, indexed_entries, config)
|
|
@@ -2510,13 +2673,18 @@ def build_index(
|
|
|
2510
2673
|
ingest_mode["validationFailed"] = validation["failed"]
|
|
2511
2674
|
ingest_mode["validationAutoDemoted"] = validation["autoDemoted"]
|
|
2512
2675
|
if auto_demotions:
|
|
2513
|
-
indexed_entries = combine_generated_with_preserved(
|
|
2676
|
+
indexed_entries = combine_generated_with_preserved(
|
|
2677
|
+
knowledge, persisted_generated
|
|
2678
|
+
)
|
|
2514
2679
|
if validation["checked"]:
|
|
2515
2680
|
indexed_entries = [entry for _, entry in load_entry_files(knowledge)]
|
|
2516
2681
|
apply_confidence_scores(indexed_entries, config)
|
|
2517
2682
|
ingest_mode["entriesWritten"] += persist_entry_updates(knowledge, indexed_entries)
|
|
2518
2683
|
ingest_mode["confidenceScored"] = len(indexed_entries)
|
|
2519
2684
|
ingest_mode["entriesPruned"] = prune_generated_entries(knowledge, indexed_entries)
|
|
2685
|
+
ingest_mode["uniqueEntryIdsWritten"] = len(indexed_entries)
|
|
2686
|
+
ingest_mode["stageWriteOperations"] = ingest_mode["entriesWritten"]
|
|
2687
|
+
ingest_mode["gitSubprocessCount"] = _GIT_SUBPROCESS_COUNT
|
|
2520
2688
|
|
|
2521
2689
|
# Lifecycle policies above may relocate or mutate preserved entries. The
|
|
2522
2690
|
# pre-build hash remains useful for the no-op gate, but the persisted index
|
|
@@ -2528,8 +2696,12 @@ def build_index(
|
|
|
2528
2696
|
load_preserved_entries(knowledge),
|
|
2529
2697
|
)
|
|
2530
2698
|
|
|
2699
|
+
reporter.emit("sqlite", 0, len(indexed_entries), force=True)
|
|
2531
2700
|
sqlite_stats = write_sqlite(knowledge / "index.sqlite", indexed_entries)
|
|
2532
2701
|
ingest_mode.update(sqlite_stats)
|
|
2702
|
+
ingest_mode["sqliteRowsTouched"] = (
|
|
2703
|
+
sqlite_stats.get("sqliteUpsert", 0) + sqlite_stats.get("sqliteDelete", 0)
|
|
2704
|
+
)
|
|
2533
2705
|
index = make_manifest(
|
|
2534
2706
|
project,
|
|
2535
2707
|
pname,
|
|
@@ -2543,6 +2715,8 @@ def build_index(
|
|
|
2543
2715
|
write_json_if_changed(knowledge / "index.json", index)
|
|
2544
2716
|
write_views(knowledge, index, indexed_entries)
|
|
2545
2717
|
write_ingest_report(knowledge, index, failures, duplicates)
|
|
2718
|
+
assert_persistence_invariants(knowledge, index, indexed_entries)
|
|
2719
|
+
reporter.emit("complete", len(indexed_entries), len(indexed_entries), force=True)
|
|
2546
2720
|
return index
|
|
2547
2721
|
|
|
2548
2722
|
|
|
@@ -2797,6 +2971,7 @@ def make_manifest(
|
|
|
2797
2971
|
duplicates: int,
|
|
2798
2972
|
ingest_mode: dict[str, Any] | None = None,
|
|
2799
2973
|
) -> dict[str, Any]:
|
|
2974
|
+
entries = list(assert_unique_entry_ids(entries).values())
|
|
2800
2975
|
stats = {status: 0 for status in ["candidate", "active", "stale", "superseded", "deprecated", "conflicted"]}
|
|
2801
2976
|
by_type = {entry_type: 0 for entry_type in sorted(ENTRY_TYPES)}
|
|
2802
2977
|
for entry in entries:
|
|
@@ -2859,6 +3034,37 @@ def make_manifest(
|
|
|
2859
3034
|
}
|
|
2860
3035
|
|
|
2861
3036
|
|
|
3037
|
+
def assert_persistence_invariants(
|
|
3038
|
+
knowledge: Path,
|
|
3039
|
+
index: dict[str, Any],
|
|
3040
|
+
entries: list[dict[str, Any]],
|
|
3041
|
+
) -> None:
|
|
3042
|
+
expected = set(assert_unique_entry_ids(entries))
|
|
3043
|
+
manifest_ids = [str(item.get("id") or "") for item in index.get("entries", [])]
|
|
3044
|
+
if len(manifest_ids) != len(set(manifest_ids)):
|
|
3045
|
+
raise ValueError("ENTRY_ID_COLLISION: duplicate IDs in knowledge manifest")
|
|
3046
|
+
file_entries = [
|
|
3047
|
+
entry
|
|
3048
|
+
for _, entry in load_entry_files(knowledge)
|
|
3049
|
+
if isinstance(entry, dict) and entry.get("id")
|
|
3050
|
+
]
|
|
3051
|
+
file_ids = [str(entry["id"]) for entry in file_entries]
|
|
3052
|
+
if len(file_ids) != len(set(file_ids)):
|
|
3053
|
+
raise ValueError("ENTRY_ID_COLLISION: duplicate IDs across entry files")
|
|
3054
|
+
connection = sqlite3.connect(knowledge / "index.sqlite")
|
|
3055
|
+
try:
|
|
3056
|
+
sqlite_ids = {
|
|
3057
|
+
str(row[0])
|
|
3058
|
+
for row in connection.execute("select id from entries").fetchall()
|
|
3059
|
+
}
|
|
3060
|
+
finally:
|
|
3061
|
+
connection.close()
|
|
3062
|
+
if set(manifest_ids) != expected or set(file_ids) != expected or sqlite_ids != expected:
|
|
3063
|
+
raise ValueError(
|
|
3064
|
+
"KNOWLEDGE_PERSISTENCE_INVARIANT: manifest, files and SQLite IDs differ"
|
|
3065
|
+
)
|
|
3066
|
+
|
|
3067
|
+
|
|
2862
3068
|
def write_sqlite(path: Path, entries: list[dict[str, Any]]) -> dict[str, Any]:
|
|
2863
3069
|
"""Persist entries to SQLite using a transactional dirty-set.
|
|
2864
3070
|
|
|
@@ -3627,13 +3833,19 @@ def summarize_index(index: dict[str, Any]) -> dict[str, Any]:
|
|
|
3627
3833
|
}
|
|
3628
3834
|
|
|
3629
3835
|
|
|
3630
|
-
def sync_status(
|
|
3836
|
+
def sync_status(
|
|
3837
|
+
project: Path,
|
|
3838
|
+
update: bool = False,
|
|
3839
|
+
incremental: bool = True,
|
|
3840
|
+
*,
|
|
3841
|
+
progress: ProgressReporter | None = None,
|
|
3842
|
+
) -> dict[str, Any]:
|
|
3631
3843
|
project = project.resolve()
|
|
3632
|
-
|
|
3844
|
+
snapshot = build_snapshot(project)
|
|
3845
|
+
knowledge = snapshot.knowledge
|
|
3633
3846
|
index_path = knowledge / "index.json"
|
|
3634
3847
|
sqlite_path = knowledge / "index.sqlite"
|
|
3635
|
-
|
|
3636
|
-
current_records = archive_summary_records(project, summary_paths)
|
|
3848
|
+
current_records = snapshot.archive_records
|
|
3637
3849
|
reasons: list[str] = []
|
|
3638
3850
|
index: dict[str, Any] | None = None
|
|
3639
3851
|
|
|
@@ -3670,14 +3882,19 @@ def sync_status(project: Path, update: bool = False, incremental: bool = True) -
|
|
|
3670
3882
|
# rebuild. The archive checksum checks above already cover archive
|
|
3671
3883
|
# changes; compare the full input fingerprint to catch config/schema
|
|
3672
3884
|
# drift that the per-archive checks would miss.
|
|
3673
|
-
current_inputs_hash =
|
|
3885
|
+
current_inputs_hash = snapshot.inputs_hash
|
|
3674
3886
|
if current_inputs_hash != index.get("inputsHash") and not reasons:
|
|
3675
3887
|
reasons.append("knowledge inputs changed (config or schema)")
|
|
3676
3888
|
|
|
3677
3889
|
action = "none"
|
|
3678
3890
|
refreshed: dict[str, Any] | None = None
|
|
3679
3891
|
if reasons and update:
|
|
3680
|
-
refreshed = build_index(
|
|
3892
|
+
refreshed = build_index(
|
|
3893
|
+
project,
|
|
3894
|
+
incremental=incremental,
|
|
3895
|
+
snapshot=snapshot,
|
|
3896
|
+
progress=progress,
|
|
3897
|
+
)
|
|
3681
3898
|
action = "ingested"
|
|
3682
3899
|
reasons = []
|
|
3683
3900
|
|
|
@@ -4080,6 +4297,8 @@ def supersede_entry(entry: dict[str, Any], newer_id: str, reason: str) -> None:
|
|
|
4080
4297
|
def dedupe_near_duplicates(
|
|
4081
4298
|
entries: list[dict[str, Any]],
|
|
4082
4299
|
threshold: float = NEAR_DUPLICATE_THRESHOLD,
|
|
4300
|
+
*,
|
|
4301
|
+
progress: ProgressReporter | None = None,
|
|
4083
4302
|
) -> dict[str, Any]:
|
|
4084
4303
|
"""Merge near-duplicate entries within the same archive (in-place)."""
|
|
4085
4304
|
by_archive: dict[str, list[dict[str, Any]]] = {}
|
|
@@ -4090,6 +4309,47 @@ def dedupe_near_duplicates(
|
|
|
4090
4309
|
by_archive.setdefault(archive, []).append(entry)
|
|
4091
4310
|
|
|
4092
4311
|
merges: list[dict[str, Any]] = []
|
|
4312
|
+
normalized = {
|
|
4313
|
+
id(entry): entry_compare_text(entry)
|
|
4314
|
+
for entry in entries
|
|
4315
|
+
}
|
|
4316
|
+
similarity_cache: dict[tuple[int, int], float] = {}
|
|
4317
|
+
similarity_candidates = 0
|
|
4318
|
+
exact_comparisons = 0
|
|
4319
|
+
|
|
4320
|
+
def similarity(left: dict[str, Any], right: dict[str, Any]) -> float:
|
|
4321
|
+
nonlocal similarity_candidates, exact_comparisons
|
|
4322
|
+
pair = tuple(sorted((id(left), id(right))))
|
|
4323
|
+
if pair in similarity_cache:
|
|
4324
|
+
return similarity_cache[pair]
|
|
4325
|
+
left_text = normalized[id(left)]
|
|
4326
|
+
right_text = normalized[id(right)]
|
|
4327
|
+
if not left_text or not right_text:
|
|
4328
|
+
similarity_cache[pair] = 0.0
|
|
4329
|
+
return 0.0
|
|
4330
|
+
shorter = min(len(left_text), len(right_text))
|
|
4331
|
+
longer = max(len(left_text), len(right_text))
|
|
4332
|
+
# SequenceMatcher's ratio cannot exceed this length-only upper bound.
|
|
4333
|
+
if longer == 0 or (2 * shorter / (shorter + longer)) < threshold:
|
|
4334
|
+
similarity_cache[pair] = 0.0
|
|
4335
|
+
return 0.0
|
|
4336
|
+
similarity_candidates += 1
|
|
4337
|
+
matcher = SequenceMatcher(None, left_text, right_text)
|
|
4338
|
+
if matcher.real_quick_ratio() < threshold or matcher.quick_ratio() < threshold:
|
|
4339
|
+
similarity_cache[pair] = 0.0
|
|
4340
|
+
return 0.0
|
|
4341
|
+
exact_comparisons += 1
|
|
4342
|
+
value = matcher.ratio()
|
|
4343
|
+
similarity_cache[pair] = value
|
|
4344
|
+
if progress is not None:
|
|
4345
|
+
progress.emit(
|
|
4346
|
+
"near-deduplicate",
|
|
4347
|
+
similarity_candidates,
|
|
4348
|
+
len(entries),
|
|
4349
|
+
metrics={"exactSimilarityComparisons": exact_comparisons},
|
|
4350
|
+
)
|
|
4351
|
+
return value
|
|
4352
|
+
|
|
4093
4353
|
for archive, group in by_archive.items():
|
|
4094
4354
|
remaining = list(group)
|
|
4095
4355
|
while remaining:
|
|
@@ -4104,7 +4364,7 @@ def dedupe_near_duplicates(
|
|
|
4104
4364
|
if other.get("type") != current.get("type"):
|
|
4105
4365
|
still.append(other)
|
|
4106
4366
|
continue
|
|
4107
|
-
if
|
|
4367
|
+
if similarity(current, other) >= threshold:
|
|
4108
4368
|
cluster.append(other)
|
|
4109
4369
|
else:
|
|
4110
4370
|
still.append(other)
|
|
@@ -4128,10 +4388,15 @@ def dedupe_near_duplicates(
|
|
|
4128
4388
|
"keptId": keeper["id"],
|
|
4129
4389
|
"mergedId": absorbed["id"],
|
|
4130
4390
|
"archive": archive,
|
|
4131
|
-
"similarity": round(
|
|
4391
|
+
"similarity": round(similarity(keeper, absorbed), 3),
|
|
4132
4392
|
}
|
|
4133
4393
|
)
|
|
4134
|
-
return {
|
|
4394
|
+
return {
|
|
4395
|
+
"merged": len(merges),
|
|
4396
|
+
"merges": merges,
|
|
4397
|
+
"similarityCandidates": similarity_candidates,
|
|
4398
|
+
"exactSimilarityComparisons": exact_comparisons,
|
|
4399
|
+
}
|
|
4135
4400
|
|
|
4136
4401
|
|
|
4137
4402
|
def relocate_entry_file(
|
|
@@ -4922,6 +5187,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
4922
5187
|
action="store_true",
|
|
4923
5188
|
help="Re-extract every archive instead of reusing the archive entry cache",
|
|
4924
5189
|
)
|
|
5190
|
+
ingest.add_argument(
|
|
5191
|
+
"--progress",
|
|
5192
|
+
choices=["jsonl", "text", "none"],
|
|
5193
|
+
default="none",
|
|
5194
|
+
help="Write phase progress to stderr; stdout remains final JSON",
|
|
5195
|
+
)
|
|
4925
5196
|
|
|
4926
5197
|
sync = sub.add_parser("sync", help="Check whether .harness/knowledge is current")
|
|
4927
5198
|
sync.add_argument("--project", default=".", help="Project root containing .harness/archive")
|
|
@@ -4931,6 +5202,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
4931
5202
|
action="store_true",
|
|
4932
5203
|
help="When used with --update, rebuild without reusing the archive entry cache",
|
|
4933
5204
|
)
|
|
5205
|
+
sync.add_argument(
|
|
5206
|
+
"--json",
|
|
5207
|
+
action="store_true",
|
|
5208
|
+
help="Emit machine-readable JSON (default behavior)",
|
|
5209
|
+
)
|
|
5210
|
+
sync.add_argument(
|
|
5211
|
+
"--progress",
|
|
5212
|
+
choices=["jsonl", "text", "none"],
|
|
5213
|
+
default="none",
|
|
5214
|
+
help="Write phase progress to stderr; stdout remains final JSON",
|
|
5215
|
+
)
|
|
4934
5216
|
|
|
4935
5217
|
auto = sub.add_parser("auto", help="Run the default automated knowledge maintenance workflow")
|
|
4936
5218
|
auto.add_argument("--project", default=".", help="Project root containing .harness/archive")
|
|
@@ -5041,11 +5323,20 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
5041
5323
|
|
|
5042
5324
|
args = parser.parse_args(argv)
|
|
5043
5325
|
if args.command == "ingest":
|
|
5044
|
-
index = build_index(
|
|
5326
|
+
index = build_index(
|
|
5327
|
+
Path(args.project),
|
|
5328
|
+
incremental=not args.no_incremental,
|
|
5329
|
+
progress=ProgressReporter(args.progress),
|
|
5330
|
+
)
|
|
5045
5331
|
print(json.dumps(summarize_index(index), ensure_ascii=False, indent=2))
|
|
5046
5332
|
return 0
|
|
5047
5333
|
if args.command == "sync":
|
|
5048
|
-
result = sync_status(
|
|
5334
|
+
result = sync_status(
|
|
5335
|
+
Path(args.project),
|
|
5336
|
+
args.update,
|
|
5337
|
+
incremental=not args.no_incremental,
|
|
5338
|
+
progress=ProgressReporter(args.progress),
|
|
5339
|
+
)
|
|
5049
5340
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
5050
5341
|
return 0
|
|
5051
5342
|
if args.command == "auto":
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=370a50726f655b0c; overlay=none; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-knowledge-query
|
|
25
25
|
|
|
26
26
|
并行 change 中查询必须传 `--change <change-id>`;成功后以该 change 的 `meta/knowledge-context.json` 为稳定指针。全局 `context-packs/latest.json` 仅向后兼容,不得用于判断多个 active change 的上下文归属。
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=370a50726f655b0c; overlay=none; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-plan — 需求规划
|
|
25
25
|
|
|
26
26
|
## Purpose
|