@hunter-harness/workflow-harness 0.2.37 → 0.2.39
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 +10 -7
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +24 -4
- 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 +283 -28
- 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 +2 -2
- package/harness/bundles/general/claude-code/harness-review/reference.md +20 -5
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +446 -20
- package/harness/bundles/general/claude-code/scripts/harness_events.py +273 -21
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +56 -32
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +197 -3
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +126 -2
- package/harness/bundles/general/claude-code/scripts/harness_report_model.py +28 -6
- package/harness/bundles/general/claude-code/scripts/harness_review.py +118 -17
- package/harness/bundles/general/claude-code/scripts/harness_service.py +219 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +10 -7
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +24 -4
- 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 +283 -28
- 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 +2 -2
- package/harness/bundles/general/codebuddy/harness-review/reference.md +20 -5
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +446 -20
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +273 -21
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +56 -32
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +197 -3
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +126 -2
- package/harness/bundles/general/codebuddy/scripts/harness_report_model.py +28 -6
- package/harness/bundles/general/codebuddy/scripts/harness_review.py +118 -17
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +219 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +10 -7
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +24 -4
- 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 +283 -28
- 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 +2 -2
- package/harness/bundles/general/codex/harness-review/reference.md +20 -5
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +446 -20
- package/harness/bundles/general/codex/scripts/harness_events.py +273 -21
- package/harness/bundles/general/codex/scripts/harness_gate.py +56 -32
- package/harness/bundles/general/codex/scripts/harness_integration.py +197 -3
- package/harness/bundles/general/codex/scripts/harness_ledger.py +126 -2
- package/harness/bundles/general/codex/scripts/harness_report_model.py +28 -6
- package/harness/bundles/general/codex/scripts/harness_review.py +118 -17
- package/harness/bundles/general/codex/scripts/harness_service.py +219 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +10 -7
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +24 -4
- 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 +283 -28
- 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 +2 -2
- package/harness/bundles/general/cursor/harness-review/reference.md +20 -5
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +446 -20
- package/harness/bundles/general/cursor/scripts/harness_events.py +273 -21
- package/harness/bundles/general/cursor/scripts/harness_gate.py +56 -32
- package/harness/bundles/general/cursor/scripts/harness_integration.py +197 -3
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +126 -2
- package/harness/bundles/general/cursor/scripts/harness_report_model.py +28 -6
- package/harness/bundles/general/cursor/scripts/harness_review.py +118 -17
- package/harness/bundles/general/cursor/scripts/harness_service.py +219 -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 +10 -7
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +24 -4
- 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 +283 -28
- 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 +2 -2
- package/harness/bundles/java/claude-code/harness-review/reference.md +20 -5
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +446 -20
- package/harness/bundles/java/claude-code/scripts/harness_events.py +273 -21
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +56 -32
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +197 -3
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +126 -2
- package/harness/bundles/java/claude-code/scripts/harness_report_model.py +28 -6
- package/harness/bundles/java/claude-code/scripts/harness_review.py +118 -17
- package/harness/bundles/java/claude-code/scripts/harness_service.py +219 -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 +10 -7
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +24 -4
- 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 +283 -28
- 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 +2 -2
- package/harness/bundles/java/codebuddy/harness-review/reference.md +20 -5
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +446 -20
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +273 -21
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +56 -32
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +197 -3
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +126 -2
- package/harness/bundles/java/codebuddy/scripts/harness_report_model.py +28 -6
- package/harness/bundles/java/codebuddy/scripts/harness_review.py +118 -17
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +219 -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 +10 -7
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +24 -4
- 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 +283 -28
- 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 +2 -2
- package/harness/bundles/java/codex/harness-review/reference.md +20 -5
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +446 -20
- package/harness/bundles/java/codex/scripts/harness_events.py +273 -21
- package/harness/bundles/java/codex/scripts/harness_gate.py +56 -32
- package/harness/bundles/java/codex/scripts/harness_integration.py +197 -3
- package/harness/bundles/java/codex/scripts/harness_ledger.py +126 -2
- package/harness/bundles/java/codex/scripts/harness_report_model.py +28 -6
- package/harness/bundles/java/codex/scripts/harness_review.py +118 -17
- package/harness/bundles/java/codex/scripts/harness_service.py +219 -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 +10 -7
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +24 -4
- 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 +283 -28
- 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 +2 -2
- package/harness/bundles/java/cursor/harness-review/reference.md +20 -5
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +446 -20
- package/harness/bundles/java/cursor/scripts/harness_events.py +273 -21
- package/harness/bundles/java/cursor/scripts/harness_gate.py +56 -32
- package/harness/bundles/java/cursor/scripts/harness_integration.py +197 -3
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +126 -2
- package/harness/bundles/java/cursor/scripts/harness_report_model.py +28 -6
- package/harness/bundles/java/cursor/scripts/harness_review.py +118 -17
- package/harness/bundles/java/cursor/scripts/harness_service.py +219 -0
- package/harness/manifests/general/claude-code.json +24 -24
- package/harness/manifests/general/codebuddy.json +24 -24
- package/harness/manifests/general/codex.json +24 -24
- package/harness/manifests/general/cursor.json +24 -24
- package/harness/manifests/java/claude-code.json +26 -26
- package/harness/manifests/java/codebuddy.json +26 -26
- package/harness/manifests/java/codex.json +26 -26
- package/harness/manifests/java/cursor.json +26 -26
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@ import hashlib
|
|
|
19
19
|
import json
|
|
20
20
|
import os
|
|
21
21
|
import re
|
|
22
|
+
import subprocess
|
|
22
23
|
import sys
|
|
23
24
|
import uuid
|
|
24
25
|
from pathlib import Path
|
|
@@ -351,43 +352,121 @@ def degradation_matrix(
|
|
|
351
352
|
return {"fallback": "advisory", "status": "ADVISORY"}
|
|
352
353
|
|
|
353
354
|
|
|
355
|
+
def _canonical_root(value: str | Path) -> str:
|
|
356
|
+
"""Canonicalize a root for an identity comparison across Windows worktrees."""
|
|
357
|
+
return os.path.normcase(str(Path(value).expanduser().resolve()))
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def codegraph_worktree_id(execution_root: Path) -> str | None:
|
|
361
|
+
"""Return a stable id for a linked worktree, or ``None`` for the main checkout."""
|
|
362
|
+
root = Path(execution_root).resolve()
|
|
363
|
+
try:
|
|
364
|
+
git_dir = subprocess.run(
|
|
365
|
+
["git", "rev-parse", "--git-dir"],
|
|
366
|
+
cwd=str(root),
|
|
367
|
+
capture_output=True,
|
|
368
|
+
text=True,
|
|
369
|
+
encoding="utf-8",
|
|
370
|
+
errors="replace",
|
|
371
|
+
check=False,
|
|
372
|
+
)
|
|
373
|
+
common_dir = subprocess.run(
|
|
374
|
+
["git", "rev-parse", "--git-common-dir"],
|
|
375
|
+
cwd=str(root),
|
|
376
|
+
capture_output=True,
|
|
377
|
+
text=True,
|
|
378
|
+
encoding="utf-8",
|
|
379
|
+
errors="replace",
|
|
380
|
+
check=False,
|
|
381
|
+
)
|
|
382
|
+
except OSError:
|
|
383
|
+
return None
|
|
384
|
+
if git_dir.returncode != 0 or common_dir.returncode != 0:
|
|
385
|
+
return None
|
|
386
|
+
git_dir_path = Path(git_dir.stdout.strip())
|
|
387
|
+
common_dir_path = Path(common_dir.stdout.strip())
|
|
388
|
+
if not git_dir_path.is_absolute():
|
|
389
|
+
git_dir_path = (root / git_dir_path).resolve()
|
|
390
|
+
if not common_dir_path.is_absolute():
|
|
391
|
+
common_dir_path = (root / common_dir_path).resolve()
|
|
392
|
+
if _canonical_root(git_dir_path) == _canonical_root(common_dir_path):
|
|
393
|
+
return None
|
|
394
|
+
return "sha256:" + hashlib.sha256(
|
|
395
|
+
_canonical_root(root).encode("utf-8")
|
|
396
|
+
).hexdigest()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def codegraph_expected_identity(
|
|
400
|
+
execution_root: Path,
|
|
401
|
+
*,
|
|
402
|
+
expected_repository_id: str,
|
|
403
|
+
expected_head: str,
|
|
404
|
+
) -> dict[str, str | None]:
|
|
405
|
+
"""Build the identity contract supplied with every CodeGraph query."""
|
|
406
|
+
root = Path(execution_root).resolve()
|
|
407
|
+
return {
|
|
408
|
+
"rootPath": str(root),
|
|
409
|
+
"worktreeId": codegraph_worktree_id(root),
|
|
410
|
+
"repositoryId": expected_repository_id,
|
|
411
|
+
"head": expected_head,
|
|
412
|
+
"indexSnapshotAt": None,
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
354
416
|
def validate_codegraph_identity(
|
|
355
417
|
*,
|
|
356
418
|
response: dict[str, Any],
|
|
357
419
|
expected_repository_id: str,
|
|
358
420
|
expected_head: str,
|
|
421
|
+
expected_root: str | Path,
|
|
422
|
+
expected_worktree_id: str | None,
|
|
359
423
|
) -> dict[str, Any]:
|
|
360
|
-
"""
|
|
424
|
+
"""Validate CodeGraph evidence before Review adopts its source snippets.
|
|
361
425
|
|
|
362
|
-
|
|
426
|
+
A repository id is shared by the main checkout and linked worktrees, so it
|
|
427
|
+
cannot by itself prevent main-checkout evidence from being used for feature
|
|
428
|
+
worktree review. Root and worktree identity are therefore mandatory.
|
|
363
429
|
"""
|
|
364
430
|
repo_id = response.get("repositoryId")
|
|
365
|
-
indexed_head = response.get("indexedHead")
|
|
366
|
-
indexed_at = response.get("indexedAt")
|
|
431
|
+
indexed_head = response.get("head", response.get("indexedHead"))
|
|
432
|
+
indexed_at = response.get("indexSnapshotAt", response.get("indexedAt"))
|
|
433
|
+
root_path = response.get("rootPath", response.get("indexedRoot"))
|
|
434
|
+
worktree_id = response.get("worktreeId")
|
|
435
|
+
expected_root_path = _canonical_root(expected_root)
|
|
436
|
+
actual_root_path = (
|
|
437
|
+
_canonical_root(root_path) if isinstance(root_path, str) and root_path.strip() else None
|
|
438
|
+
)
|
|
439
|
+
actual = {
|
|
440
|
+
"rootPath": root_path,
|
|
441
|
+
"worktreeId": worktree_id,
|
|
442
|
+
"repositoryId": repo_id,
|
|
443
|
+
"head": indexed_head,
|
|
444
|
+
"indexSnapshotAt": indexed_at,
|
|
445
|
+
}
|
|
446
|
+
expected = {
|
|
447
|
+
"rootPath": str(Path(expected_root).resolve()),
|
|
448
|
+
"worktreeId": expected_worktree_id,
|
|
449
|
+
"repositoryId": expected_repository_id,
|
|
450
|
+
"head": expected_head,
|
|
451
|
+
"indexSnapshotAt": None,
|
|
452
|
+
}
|
|
367
453
|
if (
|
|
368
454
|
repo_id == expected_repository_id
|
|
369
455
|
and indexed_head == expected_head
|
|
456
|
+
and actual_root_path == expected_root_path
|
|
457
|
+
and worktree_id == expected_worktree_id
|
|
370
458
|
and indexed_at
|
|
371
459
|
):
|
|
372
460
|
return {
|
|
373
461
|
"ok": True,
|
|
374
462
|
"code": "IDENTITY_OK",
|
|
375
|
-
"
|
|
376
|
-
"indexedHead": indexed_head,
|
|
377
|
-
"indexedAt": indexed_at,
|
|
463
|
+
"evidence": actual,
|
|
378
464
|
}
|
|
379
465
|
return {
|
|
380
466
|
"ok": False,
|
|
381
|
-
"code": "
|
|
382
|
-
"expected":
|
|
383
|
-
|
|
384
|
-
"indexedHead": expected_head,
|
|
385
|
-
},
|
|
386
|
-
"actual": {
|
|
387
|
-
"repositoryId": repo_id,
|
|
388
|
-
"indexedHead": indexed_head,
|
|
389
|
-
"indexedAt": indexed_at,
|
|
390
|
-
},
|
|
467
|
+
"code": "IDENTITY_MISMATCH",
|
|
468
|
+
"expected": expected,
|
|
469
|
+
"actual": actual,
|
|
391
470
|
"fallback": "grep-glob-read",
|
|
392
471
|
}
|
|
393
472
|
|
|
@@ -399,6 +478,20 @@ def cmd_validate_findings(args: argparse.Namespace) -> int:
|
|
|
399
478
|
return _emit(payload, as_json=True)
|
|
400
479
|
|
|
401
480
|
|
|
481
|
+
def cmd_validate_codegraph_identity(args: argparse.Namespace) -> int:
|
|
482
|
+
response = _read_json(Path(args.input))
|
|
483
|
+
if not isinstance(response, dict):
|
|
484
|
+
raise ValueError("CodeGraph response must be an object")
|
|
485
|
+
payload = validate_codegraph_identity(
|
|
486
|
+
response=response,
|
|
487
|
+
expected_repository_id=args.repository_id,
|
|
488
|
+
expected_head=args.head,
|
|
489
|
+
expected_root=args.execution_root,
|
|
490
|
+
expected_worktree_id=args.worktree_id,
|
|
491
|
+
)
|
|
492
|
+
return _emit(payload, as_json=True)
|
|
493
|
+
|
|
494
|
+
|
|
402
495
|
def cmd_write_findings(args: argparse.Namespace) -> int:
|
|
403
496
|
doc = _read_json(Path(args.input))
|
|
404
497
|
return _emit(write_findings(Path(args.change_dir), doc), as_json=True)
|
|
@@ -421,6 +514,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
421
514
|
p_validate.add_argument("--input", required=True)
|
|
422
515
|
p_validate.set_defaults(func=cmd_validate_findings)
|
|
423
516
|
|
|
517
|
+
p_codegraph = sub.add_parser("validate-codegraph-identity")
|
|
518
|
+
p_codegraph.add_argument("--input", required=True)
|
|
519
|
+
p_codegraph.add_argument("--execution-root", required=True)
|
|
520
|
+
p_codegraph.add_argument("--repository-id", required=True)
|
|
521
|
+
p_codegraph.add_argument("--head", required=True)
|
|
522
|
+
p_codegraph.add_argument("--worktree-id")
|
|
523
|
+
p_codegraph.set_defaults(func=cmd_validate_codegraph_identity)
|
|
524
|
+
|
|
424
525
|
p_findings = sub.add_parser("write-findings")
|
|
425
526
|
p_findings.add_argument("--change-dir", required=True)
|
|
426
527
|
p_findings.add_argument("--input", required=True)
|
|
@@ -726,6 +726,173 @@ def write_session(change_dir: Path, session: dict[str, Any]) -> Path:
|
|
|
726
726
|
return path
|
|
727
727
|
|
|
728
728
|
|
|
729
|
+
def _same_path(left: Any, right: Path | None) -> bool:
|
|
730
|
+
if right is None or not isinstance(left, str) or not left.strip():
|
|
731
|
+
return False
|
|
732
|
+
try:
|
|
733
|
+
return Path(left).resolve() == right.resolve()
|
|
734
|
+
except OSError:
|
|
735
|
+
return False
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def session_is_owned(
|
|
739
|
+
session: dict[str, Any],
|
|
740
|
+
*,
|
|
741
|
+
change_id: str | None = None,
|
|
742
|
+
execution_root: Path | None = None,
|
|
743
|
+
worktree_root: Path | None = None,
|
|
744
|
+
) -> bool:
|
|
745
|
+
"""Return true for an AI session owned by the supplied change/root filters.
|
|
746
|
+
|
|
747
|
+
``change_id`` is required when provided. Root filters match if the session's
|
|
748
|
+
``executionRoot`` or ``worktreeRoot`` equals any supplied root (OR).
|
|
749
|
+
"""
|
|
750
|
+
if session.get("startedBy") != "AI":
|
|
751
|
+
return False
|
|
752
|
+
if change_id is not None and session.get("changeId") != change_id:
|
|
753
|
+
return False
|
|
754
|
+
root_matches: list[bool] = []
|
|
755
|
+
if execution_root is not None:
|
|
756
|
+
root_matches.append(_same_path(session.get("executionRoot"), execution_root))
|
|
757
|
+
root_matches.append(_same_path(session.get("worktreeRoot"), execution_root))
|
|
758
|
+
if worktree_root is not None:
|
|
759
|
+
root_matches.append(_same_path(session.get("worktreeRoot"), worktree_root))
|
|
760
|
+
root_matches.append(_same_path(session.get("executionRoot"), worktree_root))
|
|
761
|
+
if root_matches and not any(root_matches):
|
|
762
|
+
return False
|
|
763
|
+
return True
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def find_owned_sessions(
|
|
767
|
+
project_root: Path,
|
|
768
|
+
*,
|
|
769
|
+
change_id: str | None = None,
|
|
770
|
+
execution_root: Path | None = None,
|
|
771
|
+
worktree_root: Path | None = None,
|
|
772
|
+
) -> dict[str, list[dict[str, Any]]]:
|
|
773
|
+
"""Discover persisted sessions and classify them without stopping anything."""
|
|
774
|
+
project = resolve_path(project_root)
|
|
775
|
+
candidate_dirs: list[Path] = []
|
|
776
|
+
if change_id:
|
|
777
|
+
candidate_dirs.extend(
|
|
778
|
+
[
|
|
779
|
+
project / ".harness" / "changes" / change_id,
|
|
780
|
+
project / ".harness" / "state" / "changes" / change_id,
|
|
781
|
+
]
|
|
782
|
+
)
|
|
783
|
+
else:
|
|
784
|
+
for root in (
|
|
785
|
+
project / ".harness" / "changes",
|
|
786
|
+
project / ".harness" / "state" / "changes",
|
|
787
|
+
):
|
|
788
|
+
if root.is_dir():
|
|
789
|
+
candidate_dirs.extend(path for path in root.iterdir() if path.is_dir())
|
|
790
|
+
|
|
791
|
+
owned: list[dict[str, Any]] = []
|
|
792
|
+
reported: list[dict[str, Any]] = []
|
|
793
|
+
seen: set[Path] = set()
|
|
794
|
+
for change_dir in candidate_dirs:
|
|
795
|
+
path = session_path(change_dir)
|
|
796
|
+
try:
|
|
797
|
+
path = path.resolve()
|
|
798
|
+
except OSError:
|
|
799
|
+
continue
|
|
800
|
+
if path in seen or not path.is_file():
|
|
801
|
+
continue
|
|
802
|
+
seen.add(path)
|
|
803
|
+
try:
|
|
804
|
+
session = load_session(change_dir)
|
|
805
|
+
except SessionCorrupt as exc:
|
|
806
|
+
reported.append(
|
|
807
|
+
{
|
|
808
|
+
"sessionPath": str(path),
|
|
809
|
+
"reason": "session-corrupt",
|
|
810
|
+
"detail": str(exc),
|
|
811
|
+
}
|
|
812
|
+
)
|
|
813
|
+
continue
|
|
814
|
+
if session is None:
|
|
815
|
+
continue
|
|
816
|
+
item = {"changeDir": str(change_dir.resolve()), "session": session}
|
|
817
|
+
if session_is_owned(
|
|
818
|
+
session,
|
|
819
|
+
change_id=change_id,
|
|
820
|
+
execution_root=execution_root,
|
|
821
|
+
worktree_root=worktree_root,
|
|
822
|
+
):
|
|
823
|
+
owned.append(item)
|
|
824
|
+
else:
|
|
825
|
+
reported.append(
|
|
826
|
+
{
|
|
827
|
+
"sessionPath": str(path),
|
|
828
|
+
"reason": "not-owned",
|
|
829
|
+
"startedBy": session.get("startedBy"),
|
|
830
|
+
"pid": session.get("pid"),
|
|
831
|
+
"leasedPort": session.get("leasedPort"),
|
|
832
|
+
}
|
|
833
|
+
)
|
|
834
|
+
return {"owned": owned, "reported": reported}
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def stop_owned_sessions(
|
|
838
|
+
project_root: Path,
|
|
839
|
+
*,
|
|
840
|
+
change_id: str | None = None,
|
|
841
|
+
execution_root: Path | None = None,
|
|
842
|
+
worktree_root: Path | None = None,
|
|
843
|
+
) -> dict[str, Any]:
|
|
844
|
+
"""Stop only discovered AI sessions whose persisted ownership still matches."""
|
|
845
|
+
discovered = find_owned_sessions(
|
|
846
|
+
project_root,
|
|
847
|
+
change_id=change_id,
|
|
848
|
+
execution_root=execution_root,
|
|
849
|
+
worktree_root=worktree_root,
|
|
850
|
+
)
|
|
851
|
+
stopped: list[dict[str, Any]] = []
|
|
852
|
+
blocked: list[dict[str, Any]] = []
|
|
853
|
+
ports_still_in_use: list[int] = []
|
|
854
|
+
for item in discovered["owned"]:
|
|
855
|
+
change_dir = Path(item["changeDir"])
|
|
856
|
+
session = item["session"]
|
|
857
|
+
if not session_is_owned(
|
|
858
|
+
session,
|
|
859
|
+
change_id=change_id,
|
|
860
|
+
execution_root=execution_root,
|
|
861
|
+
worktree_root=worktree_root,
|
|
862
|
+
):
|
|
863
|
+
blocked.append(
|
|
864
|
+
{"sessionPath": str(session_path(change_dir)), "reason": "ownership-changed"}
|
|
865
|
+
)
|
|
866
|
+
continue
|
|
867
|
+
result = stop_ai_session(change_dir, session, require_identity=True)
|
|
868
|
+
result["sessionPath"] = str(session_path(change_dir))
|
|
869
|
+
if result.get("action") == "needs-user-decision":
|
|
870
|
+
blocked.append(result)
|
|
871
|
+
continue
|
|
872
|
+
pid = session.get("pid")
|
|
873
|
+
if isinstance(pid, int) and is_pid_alive(pid):
|
|
874
|
+
blocked.append(
|
|
875
|
+
{
|
|
876
|
+
"sessionPath": str(session_path(change_dir)),
|
|
877
|
+
"reason": "owned-process-still-alive",
|
|
878
|
+
"pid": pid,
|
|
879
|
+
}
|
|
880
|
+
)
|
|
881
|
+
continue
|
|
882
|
+
stopped.append(result)
|
|
883
|
+
port = session.get("leasedPort")
|
|
884
|
+
if isinstance(port, int) and is_port_in_use(port):
|
|
885
|
+
ports_still_in_use.append(port)
|
|
886
|
+
return {
|
|
887
|
+
"ok": not blocked,
|
|
888
|
+
"ownedSessions": len(discovered["owned"]),
|
|
889
|
+
"stopped": stopped,
|
|
890
|
+
"blocked": blocked,
|
|
891
|
+
"reported": discovered["reported"],
|
|
892
|
+
"portsStillInUse": ports_still_in_use,
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
|
|
729
896
|
# ---------------------------------------------------------------------------
|
|
730
897
|
# Start / stop helpers
|
|
731
898
|
# ---------------------------------------------------------------------------
|
|
@@ -924,6 +1091,10 @@ def build_session(
|
|
|
924
1091
|
command: str,
|
|
925
1092
|
service_start: dict[str, Any],
|
|
926
1093
|
started_at: str | None = None,
|
|
1094
|
+
worktree_root: Path | None = None,
|
|
1095
|
+
execution_root: Path | None = None,
|
|
1096
|
+
change_id: str | None = None,
|
|
1097
|
+
attempt_id: str | None = None,
|
|
927
1098
|
) -> dict[str, Any]:
|
|
928
1099
|
profile_name = service_start.get("profile") or "local-dev"
|
|
929
1100
|
overlay = service_start.get("overlayPath") or ""
|
|
@@ -941,6 +1112,14 @@ def build_session(
|
|
|
941
1112
|
if isinstance(service_start.get("leasedPort"), int):
|
|
942
1113
|
session["leasedPort"] = service_start["leasedPort"]
|
|
943
1114
|
session["leaseOwner"] = service_start.get("leaseOwner")
|
|
1115
|
+
if worktree_root is not None:
|
|
1116
|
+
session["worktreeRoot"] = str(worktree_root.resolve())
|
|
1117
|
+
if execution_root is not None:
|
|
1118
|
+
session["executionRoot"] = str(execution_root.resolve())
|
|
1119
|
+
if change_id:
|
|
1120
|
+
session["changeId"] = change_id
|
|
1121
|
+
if attempt_id:
|
|
1122
|
+
session["attemptId"] = attempt_id
|
|
944
1123
|
return session
|
|
945
1124
|
|
|
946
1125
|
|
|
@@ -1031,6 +1210,15 @@ def cmd_ensure(args: argparse.Namespace) -> int:
|
|
|
1031
1210
|
# 持久 profile 只保存模板;runtime overlay/profile 注入到 session,不写回持久 profile。
|
|
1032
1211
|
# 含 worktree/change 陈旧持久值时拒绝(修复输入端陈旧 profile)。
|
|
1033
1212
|
change_name = getattr(args, "change_name", None) or change_dir.name
|
|
1213
|
+
worktree_root_raw = getattr(args, "worktree_root", None)
|
|
1214
|
+
execution_root_raw = getattr(args, "execution_root", None)
|
|
1215
|
+
attempt_id = str(getattr(args, "attempt_id", None) or "").strip() or None
|
|
1216
|
+
worktree_root = (
|
|
1217
|
+
resolve_path(worktree_root_raw) if worktree_root_raw else project
|
|
1218
|
+
)
|
|
1219
|
+
execution_root = (
|
|
1220
|
+
resolve_path(execution_root_raw) if execution_root_raw else None
|
|
1221
|
+
)
|
|
1034
1222
|
overlay = getattr(args, "overlay", None)
|
|
1035
1223
|
leased_port = getattr(args, "leased_port", None)
|
|
1036
1224
|
lease_owner = str(getattr(args, "lease_owner", None) or "").strip() or None
|
|
@@ -1135,6 +1323,10 @@ def cmd_ensure(args: argparse.Namespace) -> int:
|
|
|
1135
1323
|
as_json=as_json,
|
|
1136
1324
|
action="restarted",
|
|
1137
1325
|
previousPid=pid,
|
|
1326
|
+
worktree_root=worktree_root,
|
|
1327
|
+
execution_root=execution_root,
|
|
1328
|
+
change_id=change_name,
|
|
1329
|
+
attempt_id=attempt_id,
|
|
1138
1330
|
)
|
|
1139
1331
|
|
|
1140
1332
|
# pid dead → stale session; clear and fall through
|
|
@@ -1161,6 +1353,10 @@ def cmd_ensure(args: argparse.Namespace) -> int:
|
|
|
1161
1353
|
files=files,
|
|
1162
1354
|
as_json=as_json,
|
|
1163
1355
|
action="started",
|
|
1356
|
+
worktree_root=worktree_root,
|
|
1357
|
+
execution_root=execution_root,
|
|
1358
|
+
change_id=change_name,
|
|
1359
|
+
attempt_id=attempt_id,
|
|
1164
1360
|
)
|
|
1165
1361
|
|
|
1166
1362
|
|
|
@@ -1173,6 +1369,10 @@ def _start_and_record(
|
|
|
1173
1369
|
files: list[str],
|
|
1174
1370
|
as_json: bool,
|
|
1175
1371
|
action: str,
|
|
1372
|
+
worktree_root: Path | None = None,
|
|
1373
|
+
execution_root: Path | None = None,
|
|
1374
|
+
change_id: str | None = None,
|
|
1375
|
+
attempt_id: str | None = None,
|
|
1176
1376
|
**extra: Any,
|
|
1177
1377
|
) -> int:
|
|
1178
1378
|
if not files:
|
|
@@ -1224,6 +1424,10 @@ def _start_and_record(
|
|
|
1224
1424
|
command=command,
|
|
1225
1425
|
service_start=service_start,
|
|
1226
1426
|
started_at=started_at,
|
|
1427
|
+
worktree_root=worktree_root,
|
|
1428
|
+
execution_root=execution_root,
|
|
1429
|
+
change_id=change_id,
|
|
1430
|
+
attempt_id=attempt_id,
|
|
1227
1431
|
)
|
|
1228
1432
|
write_session(change_dir, session)
|
|
1229
1433
|
|
|
@@ -1423,6 +1627,21 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1423
1627
|
default=None,
|
|
1424
1628
|
help="run id that owns --leased-port",
|
|
1425
1629
|
)
|
|
1630
|
+
p_ensure.add_argument(
|
|
1631
|
+
"--worktree-root",
|
|
1632
|
+
default=None,
|
|
1633
|
+
help="worktree that owns the service (default: --project)",
|
|
1634
|
+
)
|
|
1635
|
+
p_ensure.add_argument(
|
|
1636
|
+
"--execution-root",
|
|
1637
|
+
default=None,
|
|
1638
|
+
help="ephemeral execution worktree that owns the service",
|
|
1639
|
+
)
|
|
1640
|
+
p_ensure.add_argument(
|
|
1641
|
+
"--attempt-id",
|
|
1642
|
+
default=None,
|
|
1643
|
+
help="phase or transaction attempt that started the service",
|
|
1644
|
+
)
|
|
1426
1645
|
p_ensure.add_argument("--json", action="store_true")
|
|
1427
1646
|
p_ensure.set_defaults(func=cmd_ensure)
|
|
1428
1647
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: harness-apidoc
|
|
3
3
|
description: 从 Java 后端代码生成结构化前端 API 文档(Controller/VO/ErrorCode)。使用场景:接口文档、前端接口文档、API 文档生成
|
|
4
4
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
5
|
+
<!-- generated by harness_deploy.py; core=339633bdc97d2146; overlay=java; agent=codex; do not edit -->
|
|
6
6
|
# harness-apidoc — 前端接口文档(Java overlay)
|
|
7
7
|
|
|
8
8
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-archive
|
|
|
3
3
|
description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到
|
|
4
4
|
.harness/archive/,含归档元数据和可视化最终总结。使用场景:归档、完成归档、收尾、打包产出物
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=339633bdc97d2146; overlay=java; agent=codex; do not edit -->
|
|
7
7
|
# harness-archive — 变更归档
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -26,7 +26,7 @@ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档
|
|
|
26
26
|
自动调用边界:
|
|
27
27
|
|
|
28
28
|
- 归档涉及移动/删除原变更目录,支持被其他 skill 调用,但调用前必须确保前置条件已满足;也支持用户显式 `/harness-archive` 调用。
|
|
29
|
-
-
|
|
29
|
+
- 默认归档前确认是阻断检查点。若 `harness_archive.py auto-gate` 返回 `ARCHIVE_AUTO_GATE_SATISFIED`(Submit 或 Merge 已终态,archive-boundary snapshot 存在且全部归档门禁满足),后续 Archive 可无 AskQuestion 自动运行;其他情况仍须确认。
|
|
30
30
|
|
|
31
31
|
## 前置条件
|
|
32
32
|
|
|
@@ -89,9 +89,12 @@ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档
|
|
|
89
89
|
- **Read `checklist.md`** — 归档前检查项
|
|
90
90
|
- 发现多个未归档变更 → 让用户选择或终止
|
|
91
91
|
|
|
92
|
-
### Phase 2:确认归档
|
|
92
|
+
### Phase 2:确认归档 / 自动门禁
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
先运行 `harness_archive.py auto-gate --change-dir ".harness/changes/<change-name>" --intent <intent> --json`。
|
|
95
|
+
|
|
96
|
+
- `autoArchiveAllowed=true`:记录 gate receipt,直接进入 Phase 3;无需 AskQuestion。
|
|
97
|
+
- 否则:blocking user confirmation 让用户确认归档操作。**用户拒绝 → 终止流程,不执行任何操作。**
|
|
95
98
|
|
|
96
99
|
- **Read `reference.md`** — 确认对话框的内容格式
|
|
97
100
|
|
|
@@ -126,9 +129,9 @@ blocking user confirmation 让用户确认归档操作。**用户拒绝 → 终
|
|
|
126
129
|
|
|
127
130
|
扫描排除 `.harness/archive/`;多个未归档变更 → 让用户选择或终止,不批量归档。
|
|
128
131
|
|
|
129
|
-
###
|
|
132
|
+
### 二、归档前确认或自动门禁
|
|
130
133
|
|
|
131
|
-
blocking user confirmation
|
|
134
|
+
`auto-gate` 只有在 Submit/Merge 终态、`meta/state-snapshot.json` 含 archive-boundary base、且 `status.archivable=true` 时才允许无确认执行。未满足时 blocking user confirmation;用户拒绝 → 终止,不执行任何操作。
|
|
132
135
|
|
|
133
136
|
### 三、文件移动只用内置工具或 PowerShell
|
|
134
137
|
|
|
@@ -201,7 +204,7 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 fi
|
|
|
201
204
|
|
|
202
205
|
## 交互白名单
|
|
203
206
|
|
|
204
|
-
|
|
207
|
+
仅当 `auto-gate` 未满足时,允许归档确认(Phase 2 blocking user confirmation);拒绝 → 终止,不执行任何移动。
|
|
205
208
|
|
|
206
209
|
## 执行日志
|
|
207
210
|
|
|
@@ -69,7 +69,7 @@ const risks = list(current.findings).length
|
|
|
69
69
|
const statusTone = (raw) => {
|
|
70
70
|
const status = String(raw || "UNKNOWN").toUpperCase();
|
|
71
71
|
if (/FAIL|ERROR|BLOCKED/.test(status)) return "danger";
|
|
72
|
-
if (/WARN|CONDITIONAL|PARTIAL|NOT_RUN|UNKNOWN|SKIP/.test(status)) return "warning";
|
|
72
|
+
if (/WARN|CONDITIONAL|PARTIAL|NOT_RUN|UNKNOWN|SKIP|EVIDENCE_MISSING/.test(status)) return "warning";
|
|
73
73
|
if (/ADVISORY|REUSED|NOT_APPLICABLE/.test(status)) return "neutral";
|
|
74
74
|
return "success";
|
|
75
75
|
};
|
|
@@ -77,7 +77,7 @@ const STATUS_LABELS = {
|
|
|
77
77
|
OK: "通过", PASS: "通过", PASSED: "通过", CONDITIONAL_OK: "有条件通过",
|
|
78
78
|
WARN: "警告", ADVISORY: "建议", FAIL: "失败", FAILED: "失败", ERROR: "错误",
|
|
79
79
|
BLOCKED: "阻塞", NOT_RUN: "未运行", SKIPPED: "已跳过",
|
|
80
|
-
NOT_APPLICABLE: "不适用", UNKNOWN: "未知"
|
|
80
|
+
NOT_APPLICABLE: "不适用", UNKNOWN: "未知", EVIDENCE_MISSING: "证据缺失"
|
|
81
81
|
};
|
|
82
82
|
const pill = (raw) => {
|
|
83
83
|
const status = String(raw || "UNKNOWN").toUpperCase();
|
|
@@ -115,7 +115,17 @@ const verificationStatus = (value) => {
|
|
|
115
115
|
};
|
|
116
116
|
const verificationNote = (key, raw) => {
|
|
117
117
|
const value = record(raw);
|
|
118
|
-
if (key === "unitTests")
|
|
118
|
+
if (key === "unitTests") {
|
|
119
|
+
const unique = numeric(value.uniqueTestCount);
|
|
120
|
+
const uniqueLabel = unique === null ? "唯一用例未记录" : `唯一 ${unique} 项`;
|
|
121
|
+
return `${value.passRate || `${value.run ?? 0} 次执行`} · ${uniqueLabel} · 重跑 ${value.rerunCount ?? 0}`;
|
|
122
|
+
}
|
|
123
|
+
if (key === "dbCompatibility") {
|
|
124
|
+
const evidence = record(verification.dbCompatibilityEvidence);
|
|
125
|
+
return evidence.status === "EVIDENCE_MISSING"
|
|
126
|
+
? "缺少可验证的类型化账本/收据"
|
|
127
|
+
: evidence.reason || evidence.evidenceHash || "类型化兼容性证据";
|
|
128
|
+
}
|
|
119
129
|
if (key === "apiTests") return `${value.passRate || `${value.passed ?? 0}/${value.total ?? 0}`} · 阻塞 ${value.blocked ?? 0}`;
|
|
120
130
|
if (key === "browserE2E") return `${value.passed ?? 0}/${value.total ?? 0} 通过 · ${value.failed ?? 0} 失败 · 重试 ${value.retries ?? 0}`;
|
|
121
131
|
if (value.checks !== undefined) return `${value.checks} 项检查`;
|
|
@@ -135,6 +145,8 @@ const groups = [
|
|
|
135
145
|
const statuses = values.map(verificationStatus);
|
|
136
146
|
const groupStatus = statuses.some((status) => /FAIL|ERROR|BLOCKED/.test(String(status)))
|
|
137
147
|
? "FAIL"
|
|
148
|
+
: statuses.some((status) => /EVIDENCE_MISSING/.test(String(status)))
|
|
149
|
+
? "EVIDENCE_MISSING"
|
|
138
150
|
: statuses.some((status) => /WARN|CONDITIONAL|PARTIAL/.test(String(status)))
|
|
139
151
|
? "WARN"
|
|
140
152
|
: statuses.length && statuses.every((status) => /NOT_APPLICABLE/.test(String(status)))
|
|
@@ -147,6 +159,13 @@ const groups = [
|
|
|
147
159
|
});
|
|
148
160
|
const passedGroups = groups.filter((group) => group.status === "OK").length;
|
|
149
161
|
const productCommit = identity.productCommit || data.productCommit || data.finalCommit;
|
|
162
|
+
const identityChain = [
|
|
163
|
+
["检查点", identity.checkpointCommit || data.checkpointCommit],
|
|
164
|
+
["产品", productCommit],
|
|
165
|
+
["功能尖端", identity.featureTip || data.featureTip],
|
|
166
|
+
["合并", identity.mergeCommit || data.mergeCommit || identity.featureMergeHash],
|
|
167
|
+
["发布尖端", identity.releaseTip || data.releaseTip || identity.releaseTipHash]
|
|
168
|
+
].filter(([, value]) => value).map(([label, value]) => `${label} ${shortHash(value)}`).join(" → ") || "N/A";
|
|
150
169
|
const wallClock = timing.workflowWallClockMs ?? record(data.durations).totalMs;
|
|
151
170
|
|
|
152
171
|
const groupHtml = groups.map((group) => `
|
|
@@ -201,7 +220,7 @@ const html = `<!doctype html>
|
|
|
201
220
|
${recordOnly ? '<div class="record-only">归档意图:仅记录 · 未请求发布</div>' : ""}
|
|
202
221
|
</section>
|
|
203
222
|
<section class="metrics">
|
|
204
|
-
<article class="metric"><small
|
|
223
|
+
<article class="metric"><small>产品提交链</small><strong><code title="${esc(identityChain)}">${esc(identityChain)}</code></strong></article>
|
|
205
224
|
<article class="metric"><small>验证概览</small><strong>${passedGroups}/${groups.length} 组通过</strong><small>按后端 / Geo / 前端 / 浏览器 / API</small></article>
|
|
206
225
|
<article class="metric"><small>风险与动作</small><strong>${risks.length} / ${actions.length}</strong><small>当前风险 / 人工动作</small></article>
|
|
207
226
|
<article class="metric"><small>全流程耗时</small><strong>${esc(duration(wallClock))}</strong><small>活动 ${esc(duration(timing.stageActiveExecutionMs))}</small></article>
|
|
@@ -234,6 +253,7 @@ const html = `<!doctype html>
|
|
|
234
253
|
<details><summary>命令证据(${commands.length})</summary><div class="table-wrap"><table><thead><tr><th>阶段</th><th>命令</th><th>结果</th></tr></thead><tbody>${commandHtml}</tbody></table></div></details>
|
|
235
254
|
<details><summary>技术元数据</summary><div><dl>
|
|
236
255
|
<dt>产品提交</dt><dd><code>${esc(productCommit || "N/A")}</code></dd>
|
|
256
|
+
<dt>检查点 → 产品 → 功能尖端 → 合并 → 发布尖端</dt><dd><code>${esc(identityChain)}</code></dd>
|
|
237
257
|
<dt>产品树哈希</dt><dd><code>${esc(identity.productTreeHash || data.productTreeHash || "N/A")}</code></dd>
|
|
238
258
|
<dt>环境哈希</dt><dd><code>${esc(identity.environmentHash || data.environmentHash || "N/A")}</code></dd>
|
|
239
259
|
<dt>基线提交</dt><dd><code>${esc(identity.baseCommit || data.baseCommit || "N/A")}</code></dd>
|
|
@@ -3,7 +3,7 @@ name: harness-codebase-map
|
|
|
3
3
|
description: 分析现有代码库并生成 .harness/codebase/map/
|
|
4
4
|
下的结构化代码库地图。用于陌生项目理解、重大改造前梳理、harness-sync 提示 map 过期后的人工刷新。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=339633bdc97d2146; overlay=java; agent=codex; do not edit -->
|
|
7
7
|
# harness-codebase-map — 代码库地图
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-knowledge-ingest
|
|
|
3
3
|
description: 从 .harness/archive 归档整理、同步和维护项目知识索引。适用场景:ingest knowledge、sync
|
|
4
4
|
knowledge、rebuild knowledge index、promote knowledge、确认知识条目、检查知识库是否过期。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=339633bdc97d2146; overlay=java; agent=codex; do not edit -->
|
|
7
7
|
# harness-knowledge-ingest
|
|
8
8
|
|
|
9
9
|
从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
|