@hunter-harness/workflow-harness 0.2.46 → 0.2.47
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/README.md +1 -1
- package/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +9 -10
- 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 +347 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +6 -3
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -7
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +6 -3
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +5 -7
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +6 -3
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +164 -17
- package/harness/bundles/general/claude-code/scripts/harness_events.py +30 -1
- package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +269 -0
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +216 -64
- package/harness/bundles/general/claude-code/scripts/harness_headless.py +37 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +9 -11
- 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 +347 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +6 -3
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -7
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +6 -3
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +5 -8
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +6 -3
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +164 -17
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +30 -1
- package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +269 -0
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +216 -64
- package/harness/bundles/general/codebuddy/scripts/harness_headless.py +37 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +9 -11
- 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 +347 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +6 -3
- package/harness/bundles/general/codex/harness-review/SKILL.md +6 -7
- package/harness/bundles/general/codex/harness-run/SKILL.md +6 -3
- package/harness/bundles/general/codex/harness-submit/SKILL.md +5 -8
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +6 -3
- package/harness/bundles/general/codex/scripts/harness_archive.py +164 -17
- package/harness/bundles/general/codex/scripts/harness_events.py +30 -1
- package/harness/bundles/general/codex/scripts/harness_events_sync.py +269 -0
- package/harness/bundles/general/codex/scripts/harness_gate.py +216 -64
- package/harness/bundles/general/codex/scripts/harness_headless.py +37 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +9 -11
- 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 +347 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +6 -3
- package/harness/bundles/general/cursor/harness-review/SKILL.md +6 -7
- package/harness/bundles/general/cursor/harness-run/SKILL.md +6 -3
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +5 -8
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +6 -3
- package/harness/bundles/general/cursor/scripts/harness_archive.py +164 -17
- package/harness/bundles/general/cursor/scripts/harness_events.py +30 -1
- package/harness/bundles/general/cursor/scripts/harness_events_sync.py +269 -0
- package/harness/bundles/general/cursor/scripts/harness_gate.py +216 -64
- package/harness/bundles/general/cursor/scripts/harness_headless.py +37 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +3 -2
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +9 -10
- 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 +347 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +3 -2
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +6 -3
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -7
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +6 -3
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +5 -7
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +6 -3
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +164 -17
- package/harness/bundles/java/claude-code/scripts/harness_events.py +30 -1
- package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +269 -0
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +216 -64
- package/harness/bundles/java/claude-code/scripts/harness_headless.py +37 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +3 -2
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +9 -11
- 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 +347 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +6 -3
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -7
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +6 -3
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +5 -8
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +6 -3
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +164 -17
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +30 -1
- package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +269 -0
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +216 -64
- package/harness/bundles/java/codebuddy/scripts/harness_headless.py +37 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +3 -2
- package/harness/bundles/java/codex/harness-archive/SKILL.md +9 -11
- 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 +347 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-plan/SKILL.md +6 -3
- package/harness/bundles/java/codex/harness-review/SKILL.md +6 -7
- package/harness/bundles/java/codex/harness-run/SKILL.md +6 -3
- package/harness/bundles/java/codex/harness-submit/SKILL.md +5 -8
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +6 -3
- package/harness/bundles/java/codex/scripts/harness_archive.py +164 -17
- package/harness/bundles/java/codex/scripts/harness_events.py +30 -1
- package/harness/bundles/java/codex/scripts/harness_events_sync.py +269 -0
- package/harness/bundles/java/codex/scripts/harness_gate.py +216 -64
- package/harness/bundles/java/codex/scripts/harness_headless.py +37 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +3 -2
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +9 -11
- 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 +347 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +6 -3
- package/harness/bundles/java/cursor/harness-review/SKILL.md +6 -7
- package/harness/bundles/java/cursor/harness-run/SKILL.md +6 -3
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +5 -8
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +6 -3
- package/harness/bundles/java/cursor/scripts/harness_archive.py +164 -17
- package/harness/bundles/java/cursor/scripts/harness_events.py +30 -1
- package/harness/bundles/java/cursor/scripts/harness_events_sync.py +269 -0
- package/harness/bundles/java/cursor/scripts/harness_gate.py +216 -64
- package/harness/bundles/java/cursor/scripts/harness_headless.py +37 -0
- package/harness/manifests/general/claude-code.json +25 -17
- package/harness/manifests/general/codebuddy.json +25 -17
- package/harness/manifests/general/codex.json +25 -17
- package/harness/manifests/general/cursor.json +25 -17
- package/harness/manifests/java/claude-code.json +27 -19
- package/harness/manifests/java/codebuddy.json +27 -19
- package/harness/manifests/java/codex.json +27 -19
- package/harness/manifests/java/cursor.json +27 -19
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -22,6 +22,8 @@ import subprocess
|
|
|
22
22
|
import sys
|
|
23
23
|
import time
|
|
24
24
|
import unicodedata
|
|
25
|
+
import urllib.error
|
|
26
|
+
import urllib.request
|
|
25
27
|
from difflib import SequenceMatcher
|
|
26
28
|
from pathlib import Path
|
|
27
29
|
from typing import Any
|
|
@@ -394,6 +396,187 @@ def project_id(project: Path) -> str:
|
|
|
394
396
|
return slugify(project.name, 80)
|
|
395
397
|
|
|
396
398
|
|
|
399
|
+
def _yaml_scalar(text: str, key: str) -> str | None:
|
|
400
|
+
"""Read a top-level or nested YAML scalar without depending on PyYAML."""
|
|
401
|
+
pattern = rf"(?m)^[ \t]*{re.escape(key)}:\s*['\"]?([^'\"\n#]+)"
|
|
402
|
+
match = re.search(pattern, text)
|
|
403
|
+
if match is None:
|
|
404
|
+
return None
|
|
405
|
+
value = match.group(1).strip()
|
|
406
|
+
return value or None
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def platform_project_id(project: Path) -> str | None:
|
|
410
|
+
"""Platform-bound project id from project.yaml / credentials (not the local slug)."""
|
|
411
|
+
for relative in (
|
|
412
|
+
".harness/credentials.local.yaml",
|
|
413
|
+
".harness/project.yaml",
|
|
414
|
+
):
|
|
415
|
+
path = project / relative
|
|
416
|
+
if not path.is_file():
|
|
417
|
+
continue
|
|
418
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
419
|
+
value = _yaml_scalar(text, "project_id")
|
|
420
|
+
if value and value.lower() not in {"null", "~", "none"}:
|
|
421
|
+
return value
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def resolve_remote_endpoint(project: Path) -> dict[str, str] | None:
|
|
426
|
+
"""P3 remote mode: credentials.local.yaml with server_url+token enables platform ingest.
|
|
427
|
+
|
|
428
|
+
When configured, local index.json / index.sqlite / judge / maintenance-outbox are
|
|
429
|
+
not maintained; entries still land under entries/ for offline grep fallback.
|
|
430
|
+
"""
|
|
431
|
+
creds = project / ".harness" / "credentials.local.yaml"
|
|
432
|
+
if not creds.is_file():
|
|
433
|
+
return None
|
|
434
|
+
text = creds.read_text(encoding="utf-8", errors="ignore")
|
|
435
|
+
server_url = _yaml_scalar(text, "server_url")
|
|
436
|
+
token = _yaml_scalar(text, "token")
|
|
437
|
+
if not server_url or not token:
|
|
438
|
+
return None
|
|
439
|
+
project_bound = platform_project_id(project)
|
|
440
|
+
if not project_bound:
|
|
441
|
+
return None
|
|
442
|
+
return {
|
|
443
|
+
"server_url": server_url.rstrip("/"),
|
|
444
|
+
"token": token,
|
|
445
|
+
"project_id": project_bound,
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def post_knowledge_ingest(
|
|
450
|
+
endpoint: dict[str, str],
|
|
451
|
+
entries: list[dict[str, Any]],
|
|
452
|
+
*,
|
|
453
|
+
timeout: float = 30.0,
|
|
454
|
+
) -> dict[str, Any]:
|
|
455
|
+
"""POST entries to the platform knowledge ingest API (idempotent)."""
|
|
456
|
+
if not entries:
|
|
457
|
+
return {"accepted": 0, "created": 0, "updated": 0, "duplicates": 0, "skipped": True}
|
|
458
|
+
url = (
|
|
459
|
+
f"{endpoint['server_url']}/api/v1/projects/"
|
|
460
|
+
f"{endpoint['project_id']}/knowledge/ingest"
|
|
461
|
+
)
|
|
462
|
+
body = json.dumps(
|
|
463
|
+
{"schema_version": 1, "entries": entries},
|
|
464
|
+
ensure_ascii=False,
|
|
465
|
+
separators=(",", ":"),
|
|
466
|
+
).encode("utf-8")
|
|
467
|
+
request = urllib.request.Request(
|
|
468
|
+
url,
|
|
469
|
+
data=body,
|
|
470
|
+
method="POST",
|
|
471
|
+
headers={
|
|
472
|
+
"Accept": "application/json",
|
|
473
|
+
"Authorization": f"Bearer {endpoint['token']}",
|
|
474
|
+
"Content-Type": "application/json",
|
|
475
|
+
},
|
|
476
|
+
)
|
|
477
|
+
try:
|
|
478
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
479
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
480
|
+
except urllib.error.HTTPError as exc:
|
|
481
|
+
detail = exc.read().decode("utf-8", errors="replace")
|
|
482
|
+
raise RuntimeError(f"knowledge ingest HTTP {exc.code}: {detail}") from exc
|
|
483
|
+
except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
|
|
484
|
+
raise RuntimeError(f"knowledge ingest failed: {exc}") from exc
|
|
485
|
+
if not isinstance(payload, dict):
|
|
486
|
+
raise RuntimeError("knowledge ingest returned a non-object payload")
|
|
487
|
+
return payload
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def query_remote_semantic(
|
|
491
|
+
endpoint: dict[str, str],
|
|
492
|
+
query: str,
|
|
493
|
+
*,
|
|
494
|
+
limit: int = 10,
|
|
495
|
+
timeout: float = 20.0,
|
|
496
|
+
) -> list[dict[str, Any]]:
|
|
497
|
+
"""Query the platform semantic search API; empty list when unreachable."""
|
|
498
|
+
from urllib.parse import urlencode
|
|
499
|
+
|
|
500
|
+
params = urlencode({"q": query, "project_id": endpoint["project_id"]})
|
|
501
|
+
url = f"{endpoint['server_url']}/api/v1/semantic/search?{params}"
|
|
502
|
+
request = urllib.request.Request(
|
|
503
|
+
url,
|
|
504
|
+
method="GET",
|
|
505
|
+
headers={
|
|
506
|
+
"Accept": "application/json",
|
|
507
|
+
"Authorization": f"Bearer {endpoint['token']}",
|
|
508
|
+
},
|
|
509
|
+
)
|
|
510
|
+
try:
|
|
511
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
512
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
513
|
+
except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError):
|
|
514
|
+
return []
|
|
515
|
+
items = payload.get("items") if isinstance(payload, dict) else None
|
|
516
|
+
if not isinstance(items, list):
|
|
517
|
+
return []
|
|
518
|
+
matches: list[dict[str, Any]] = []
|
|
519
|
+
for item in items[:limit]:
|
|
520
|
+
if not isinstance(item, dict):
|
|
521
|
+
continue
|
|
522
|
+
document = item.get("document") if isinstance(item.get("document"), dict) else item
|
|
523
|
+
metadata = document.get("metadata") if isinstance(document.get("metadata"), dict) else {}
|
|
524
|
+
matches.append(
|
|
525
|
+
{
|
|
526
|
+
"id": str(metadata.get("entry_id") or document.get("document_id") or ""),
|
|
527
|
+
"type": str(metadata.get("entry_type") or "decision"),
|
|
528
|
+
"status": str(metadata.get("status") or "active"),
|
|
529
|
+
"title": str(document.get("title") or ""),
|
|
530
|
+
"source": {
|
|
531
|
+
"archive": str(metadata.get("source_archive") or "remote"),
|
|
532
|
+
},
|
|
533
|
+
"scope": {
|
|
534
|
+
"sourceFiles": list(metadata.get("source_files") or []),
|
|
535
|
+
},
|
|
536
|
+
"body": str(document.get("body") or ""),
|
|
537
|
+
"summary": str(document.get("title") or ""),
|
|
538
|
+
}
|
|
539
|
+
)
|
|
540
|
+
return [match for match in matches if match["id"]]
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def grep_entry_files(
|
|
544
|
+
knowledge: Path,
|
|
545
|
+
query: str,
|
|
546
|
+
limit: int = 10,
|
|
547
|
+
statuses: list[str] | None = None,
|
|
548
|
+
) -> list[dict[str, Any]]:
|
|
549
|
+
"""Offline fallback: substring scan over entries/*.json when remote is down."""
|
|
550
|
+
needle = query.strip().lower()
|
|
551
|
+
if not needle:
|
|
552
|
+
return []
|
|
553
|
+
selected = set(statuses or ["active", "candidate"])
|
|
554
|
+
matches: list[dict[str, Any]] = []
|
|
555
|
+
for status in sorted(selected):
|
|
556
|
+
folder = knowledge / "entries" / status
|
|
557
|
+
if not folder.is_dir():
|
|
558
|
+
continue
|
|
559
|
+
for path in sorted(folder.glob("*.json")):
|
|
560
|
+
try:
|
|
561
|
+
entry = read_json(path)
|
|
562
|
+
except (OSError, json.JSONDecodeError):
|
|
563
|
+
continue
|
|
564
|
+
if not isinstance(entry, dict):
|
|
565
|
+
continue
|
|
566
|
+
haystack = " ".join(
|
|
567
|
+
str(entry.get(key) or "")
|
|
568
|
+
for key in ("id", "title", "summary", "body", "type", "status")
|
|
569
|
+
).lower()
|
|
570
|
+
keywords = entry.get("keywords") if isinstance(entry.get("keywords"), list) else []
|
|
571
|
+
haystack += " " + " ".join(str(item) for item in keywords).lower()
|
|
572
|
+
if needle not in haystack:
|
|
573
|
+
continue
|
|
574
|
+
matches.append(entry)
|
|
575
|
+
if len(matches) >= limit:
|
|
576
|
+
return matches
|
|
577
|
+
return matches
|
|
578
|
+
|
|
579
|
+
|
|
397
580
|
def git_head(project: Path) -> str | None:
|
|
398
581
|
result = run_git(project, ["rev-parse", "HEAD"])
|
|
399
582
|
return result.stdout.strip() if result.returncode == 0 else None
|
|
@@ -3110,11 +3293,16 @@ def build_index(
|
|
|
3110
3293
|
# so a repeated ingest is a true no-op (design §3.5, cluster 6, UT-025).
|
|
3111
3294
|
# Also require index.sqlite to exist so a query never sees a stale index.json
|
|
3112
3295
|
# pointing at a missing sqlite (API-009 single ensure-current).
|
|
3296
|
+
# P3 remote mode does not maintain sqlite; the remote pointer index is enough.
|
|
3297
|
+
remote_endpoint = resolve_remote_endpoint(project)
|
|
3113
3298
|
if incremental:
|
|
3114
3299
|
if (
|
|
3115
3300
|
isinstance(old_index, dict)
|
|
3116
3301
|
and old_index.get("inputsHash") == inputs_hash
|
|
3117
|
-
and (
|
|
3302
|
+
and (
|
|
3303
|
+
remote_endpoint is not None
|
|
3304
|
+
or (knowledge / "index.sqlite").exists()
|
|
3305
|
+
)
|
|
3118
3306
|
):
|
|
3119
3307
|
stale_mode = dict(old_index.get("ingestMode", {}))
|
|
3120
3308
|
stale_mode.update(
|
|
@@ -3134,6 +3322,7 @@ def build_index(
|
|
|
3134
3322
|
"archivesChanged": 0,
|
|
3135
3323
|
"archivesRemoved": 0,
|
|
3136
3324
|
"archivesDelta": 0,
|
|
3325
|
+
**({"remote": True} if remote_endpoint is not None else {}),
|
|
3137
3326
|
}
|
|
3138
3327
|
)
|
|
3139
3328
|
result = dict(old_index)
|
|
@@ -3363,6 +3552,40 @@ def build_index(
|
|
|
3363
3552
|
)
|
|
3364
3553
|
|
|
3365
3554
|
reporter.emit("sqlite", 0, len(indexed_entries), force=True)
|
|
3555
|
+
remote = resolve_remote_endpoint(project)
|
|
3556
|
+
if remote is not None:
|
|
3557
|
+
# P3 remote mode: keep entry JSON files for offline grep; skip local
|
|
3558
|
+
# index.json / index.sqlite / views / outbox projections and push to platform.
|
|
3559
|
+
ingest_mode["mode"] = "remote"
|
|
3560
|
+
ingest_mode["remote"] = True
|
|
3561
|
+
ingest_mode["remoteProjectId"] = remote["project_id"]
|
|
3562
|
+
ingest_mode["sqliteRebuild"] = 0
|
|
3563
|
+
ingest_mode["sqliteUpsert"] = 0
|
|
3564
|
+
ingest_mode["sqliteDelete"] = 0
|
|
3565
|
+
ingest_mode["sqliteRowsTouched"] = 0
|
|
3566
|
+
try:
|
|
3567
|
+
remote_result = post_knowledge_ingest(remote, indexed_entries)
|
|
3568
|
+
ingest_mode["remoteIngest"] = remote_result
|
|
3569
|
+
ingest_mode["remoteOk"] = True
|
|
3570
|
+
except RuntimeError as exc:
|
|
3571
|
+
ingest_mode["remoteOk"] = False
|
|
3572
|
+
ingest_mode["remoteError"] = str(exc)
|
|
3573
|
+
index = make_manifest(
|
|
3574
|
+
project,
|
|
3575
|
+
pname,
|
|
3576
|
+
summary_paths,
|
|
3577
|
+
archive_records,
|
|
3578
|
+
indexed_entries,
|
|
3579
|
+
failures,
|
|
3580
|
+
duplicates,
|
|
3581
|
+
ingest_mode,
|
|
3582
|
+
)
|
|
3583
|
+
# Lightweight pointer only — not a local search index.
|
|
3584
|
+
write_json_if_changed(knowledge / "index.json", index)
|
|
3585
|
+
write_ingest_report(knowledge, index, failures, duplicates)
|
|
3586
|
+
reporter.emit("complete", len(indexed_entries), len(indexed_entries), force=True)
|
|
3587
|
+
return index
|
|
3588
|
+
|
|
3366
3589
|
sqlite_dirty_entries = [
|
|
3367
3590
|
entry
|
|
3368
3591
|
for entry in indexed_entries
|
|
@@ -4417,6 +4640,27 @@ def auto_knowledge(
|
|
|
4417
4640
|
audit_limit: int = 10,
|
|
4418
4641
|
) -> dict[str, Any]:
|
|
4419
4642
|
project = project.resolve()
|
|
4643
|
+
remote = resolve_remote_endpoint(project)
|
|
4644
|
+
if remote is not None:
|
|
4645
|
+
sync = sync_status(project, update=True, incremental=incremental)
|
|
4646
|
+
return {
|
|
4647
|
+
"command": "auto",
|
|
4648
|
+
"project": str(project),
|
|
4649
|
+
"generatedAt": now_iso(),
|
|
4650
|
+
"mode": "remote",
|
|
4651
|
+
"remote": {
|
|
4652
|
+
"serverUrl": remote["server_url"],
|
|
4653
|
+
"projectId": remote["project_id"],
|
|
4654
|
+
},
|
|
4655
|
+
"sync": sync,
|
|
4656
|
+
"skipped": {
|
|
4657
|
+
"suggestValidators": True,
|
|
4658
|
+
"verify": True,
|
|
4659
|
+
"audit": True,
|
|
4660
|
+
"judge": True,
|
|
4661
|
+
"reason": "remote mode: candidate review happens on the platform",
|
|
4662
|
+
},
|
|
4663
|
+
}
|
|
4420
4664
|
knowledge = project / ".harness" / "knowledge"
|
|
4421
4665
|
config_summary = ensure_auto_knowledge_config(knowledge)
|
|
4422
4666
|
sync = sync_status(project, update=True, incremental=incremental)
|
|
@@ -4530,6 +4774,46 @@ def query_index(
|
|
|
4530
4774
|
) -> dict[str, Any]:
|
|
4531
4775
|
project = project.resolve()
|
|
4532
4776
|
knowledge = project / ".harness" / "knowledge"
|
|
4777
|
+
remote = resolve_remote_endpoint(project)
|
|
4778
|
+
if remote is not None:
|
|
4779
|
+
# P3: prefer platform semantic search; degrade to local entry grep when offline.
|
|
4780
|
+
entries = query_remote_semantic(remote, query, limit=limit)
|
|
4781
|
+
source = "remote"
|
|
4782
|
+
if not entries:
|
|
4783
|
+
entries = grep_entry_files(knowledge, query, limit=limit, statuses=statuses)
|
|
4784
|
+
source = "offline-grep"
|
|
4785
|
+
context_path = write_context_pack(project, knowledge, query, entries)
|
|
4786
|
+
filters = {
|
|
4787
|
+
"files": file_filters or [],
|
|
4788
|
+
"statuses": statuses or [],
|
|
4789
|
+
"types": types or [],
|
|
4790
|
+
}
|
|
4791
|
+
return {
|
|
4792
|
+
"query": query,
|
|
4793
|
+
"matchCount": len(entries),
|
|
4794
|
+
"contextPack": str(context_path),
|
|
4795
|
+
"filters": filters,
|
|
4796
|
+
"mode": "remote",
|
|
4797
|
+
"source": source,
|
|
4798
|
+
"planInput": {
|
|
4799
|
+
"kind": "harness-knowledge-context-pack",
|
|
4800
|
+
"path": str(context_path),
|
|
4801
|
+
"requiredBefore": "harness-plan",
|
|
4802
|
+
"usage": "Read this context pack before design, planning, code exploration, or implementation.",
|
|
4803
|
+
},
|
|
4804
|
+
"matches": [
|
|
4805
|
+
{
|
|
4806
|
+
"id": entry.get("id"),
|
|
4807
|
+
"type": entry.get("type"),
|
|
4808
|
+
"status": entry.get("status"),
|
|
4809
|
+
"title": entry.get("title"),
|
|
4810
|
+
"sourceArchive": (entry.get("source") or {}).get("archive"),
|
|
4811
|
+
"sourceFiles": (entry.get("scope") or {}).get("sourceFiles") or [],
|
|
4812
|
+
}
|
|
4813
|
+
for entry in entries
|
|
4814
|
+
],
|
|
4815
|
+
}
|
|
4816
|
+
|
|
4533
4817
|
sqlite_path = knowledge / "index.sqlite"
|
|
4534
4818
|
# API-009: one ensure-current. Build the shared snapshot once (inputs_hash
|
|
4535
4819
|
# computed exactly once) and a single build_index call whose no-op fast path
|
|
@@ -4621,6 +4905,45 @@ def sync_status(
|
|
|
4621
4905
|
check: bool = False,
|
|
4622
4906
|
) -> dict[str, Any]:
|
|
4623
4907
|
project = project.resolve()
|
|
4908
|
+
remote = resolve_remote_endpoint(project)
|
|
4909
|
+
if remote is not None:
|
|
4910
|
+
# P3: remote mode — local sqlite/outbox are intentionally not maintained.
|
|
4911
|
+
refreshed: dict[str, Any] | None = None
|
|
4912
|
+
action = "none"
|
|
4913
|
+
if update and not check:
|
|
4914
|
+
refreshed = build_index(
|
|
4915
|
+
project,
|
|
4916
|
+
incremental=incremental,
|
|
4917
|
+
progress=progress,
|
|
4918
|
+
)
|
|
4919
|
+
action = "ingested-remote"
|
|
4920
|
+
remote_ok = True
|
|
4921
|
+
remote_error = None
|
|
4922
|
+
if refreshed is not None:
|
|
4923
|
+
mode = refreshed.get("ingestMode") or {}
|
|
4924
|
+
remote_ok = bool(mode.get("remoteOk", True))
|
|
4925
|
+
remote_error = mode.get("remoteError")
|
|
4926
|
+
return {
|
|
4927
|
+
"ok": remote_ok,
|
|
4928
|
+
"current": remote_ok,
|
|
4929
|
+
"action": action,
|
|
4930
|
+
"reasons": [] if remote_ok else [str(remote_error or "remote ingest failed")],
|
|
4931
|
+
"mode": "remote",
|
|
4932
|
+
"remote": {
|
|
4933
|
+
"serverUrl": remote["server_url"],
|
|
4934
|
+
"projectId": remote["project_id"],
|
|
4935
|
+
"ok": remote_ok,
|
|
4936
|
+
**({} if remote_error is None else {"error": remote_error}),
|
|
4937
|
+
},
|
|
4938
|
+
"maintenance": {
|
|
4939
|
+
"attempted": False,
|
|
4940
|
+
"skipped": True,
|
|
4941
|
+
"skippedReason": "remote mode: judge/outbox run on the platform",
|
|
4942
|
+
"ok": True,
|
|
4943
|
+
},
|
|
4944
|
+
"index": refreshed,
|
|
4945
|
+
}
|
|
4946
|
+
|
|
4624
4947
|
snapshot = build_snapshot(project)
|
|
4625
4948
|
knowledge = snapshot.knowledge
|
|
4626
4949
|
index_path = knowledge / "index.json"
|
|
@@ -6295,6 +6618,16 @@ def maintain_knowledge(project: Path, archive_id: str) -> dict[str, Any]:
|
|
|
6295
6618
|
judge checklist, running->completed (or completed_rules_pending_judge).
|
|
6296
6619
|
Idempotent for completed items. Failure -> failed, attempts+1, retryable."""
|
|
6297
6620
|
project = project.resolve()
|
|
6621
|
+
remote = resolve_remote_endpoint(project)
|
|
6622
|
+
if remote is not None:
|
|
6623
|
+
return {
|
|
6624
|
+
"ok": True,
|
|
6625
|
+
"archiveId": archive_id,
|
|
6626
|
+
"status": "skipped-remote",
|
|
6627
|
+
"skipped": True,
|
|
6628
|
+
"skippedReason": "remote mode: maintenance runs on the platform",
|
|
6629
|
+
"remoteProjectId": remote["project_id"],
|
|
6630
|
+
}
|
|
6298
6631
|
outbox_root = _outbox_root(project)
|
|
6299
6632
|
outbox_root.mkdir(parents=True, exist_ok=True)
|
|
6300
6633
|
|
|
@@ -6379,6 +6712,19 @@ def drain_maintenance_outbox(
|
|
|
6379
6712
|
simply including the stale id in the drain batch is sufficient recovery.
|
|
6380
6713
|
"""
|
|
6381
6714
|
project = project.resolve()
|
|
6715
|
+
remote = resolve_remote_endpoint(project)
|
|
6716
|
+
if remote is not None:
|
|
6717
|
+
return {
|
|
6718
|
+
"ok": True,
|
|
6719
|
+
"project": str(project),
|
|
6720
|
+
"processed": 0,
|
|
6721
|
+
"remaining": 0,
|
|
6722
|
+
"staleRunningRecovered": 0,
|
|
6723
|
+
"results": [],
|
|
6724
|
+
"skipped": True,
|
|
6725
|
+
"skippedReason": "remote mode: maintenance outbox is not used",
|
|
6726
|
+
"mode": "remote",
|
|
6727
|
+
}
|
|
6382
6728
|
root = _outbox_root(project)
|
|
6383
6729
|
now = time.time()
|
|
6384
6730
|
retryable_ids = {
|
|
@@ -3,7 +3,7 @@ name: harness-knowledge-query
|
|
|
3
3
|
description: 根据新需求、设计任务、代码修改请求或问题排查,在规划和编码前查询 .harness/knowledge
|
|
4
4
|
历史上下文。适用场景:query knowledge、查找历史需求、根据归档理解需求、继续之前类似开发、生成需求上下文包。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=25cf57e2e035246a; overlay=none; agent=codex; do not edit -->
|
|
7
7
|
# harness-knowledge-query
|
|
8
8
|
|
|
9
9
|
并行 change 中查询必须传 `--change <change-id>`;成功后以该 change 的 `meta/knowledge-context.json` 为稳定指针。全局 `context-packs/latest.json` 仅向后兼容,不得用于判断多个 active change 的上下文归属。
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: harness-plan
|
|
3
|
-
description:
|
|
3
|
+
description: 将需求转化为设计文档+实施计划+完整测试场景表,必须在编码前完成。仅当用户显式调用 /harness-plan 或明确要求进入
|
|
4
|
+
Harness 规划阶段时使用;不得因用户描述需求就自动触发。
|
|
4
5
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=25cf57e2e035246a; overlay=none; agent=codex; do not edit -->
|
|
6
7
|
# harness-plan — 需求规划
|
|
7
8
|
|
|
8
9
|
## Purpose
|
|
@@ -13,7 +14,9 @@ description: 将需求转化为设计文档+实施计划+完整测试场景表
|
|
|
13
14
|
|
|
14
15
|
## When to Use
|
|
15
16
|
|
|
16
|
-
`/harness-plan
|
|
17
|
+
仅当用户显式调用 `/harness-plan`(或明确说"用 harness 规划这个需求")时执行。用户只是描述需求、提问或讨论方案时,**不得**自动进入本 skill。
|
|
18
|
+
|
|
19
|
+
**单阶段原则**:本 skill 只负责 plan 阶段。plan finalize + verify 完成后必须停止并交还用户,仅提示下一步可执行 `/harness-run`;禁止自动开始编码或调用其他 harness 阶段 skill。
|
|
17
20
|
|
|
18
21
|
## 统一读取协议
|
|
19
22
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: harness-review
|
|
3
3
|
description: 6维度代码审查(架构/安全/规范/兼容/测试/性能),对照项目规则(见
|
|
4
|
-
.harness/context-index.json
|
|
4
|
+
.harness/context-index.json)和测试场景表,在隔离上下文运行。仅当用户显式调用 /harness-review 时使用;不得在
|
|
5
|
+
test 结束后自动接续执行。
|
|
5
6
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
7
|
+
<!-- generated by harness_deploy.py; core=25cf57e2e035246a; overlay=none; agent=codex; do not edit -->
|
|
7
8
|
# harness-review — 代码审查
|
|
8
9
|
|
|
9
10
|
## Purpose
|
|
@@ -12,11 +13,9 @@ description: 6维度代码审查(架构/安全/规范/兼容/测试/性能)
|
|
|
12
13
|
|
|
13
14
|
## When to Use
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
- 合并评审时需要系统性检查
|
|
19
|
-
- 用户说"审查代码"、"review"、"检查代码质量"
|
|
16
|
+
仅当用户显式调用 `/harness-review` 时执行。test 阶段结束后**不自动**进入本阶段;用户口头提到"审查/review"而未调用本 skill 时,先确认是否走 Harness 审查阶段。
|
|
17
|
+
|
|
18
|
+
**单阶段原则**:review 关门(报告 + fixback 产出)后必须停止并交还用户,仅提示下一步可执行 `/harness-run --fixback`(有待修项)或 `/harness-submit`;禁止自动接续执行。
|
|
20
19
|
|
|
21
20
|
## Inputs
|
|
22
21
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: harness-run
|
|
3
|
-
description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR
|
|
3
|
+
description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编译验证),逐变更簇实现计划中的任务。仅当用户显式调用
|
|
4
|
+
/harness-run 时使用;不得因用户提到编码/实现就自动触发,也不得被其他阶段 skill 自动接续。
|
|
4
5
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=25cf57e2e035246a; overlay=none; agent=codex; do not edit -->
|
|
6
7
|
# harness-run — 需求编码
|
|
7
8
|
|
|
8
9
|
## Purpose
|
|
@@ -11,7 +12,9 @@ description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编
|
|
|
11
12
|
|
|
12
13
|
## When to Use
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
仅当用户显式调用 `/harness-run` 时执行;plan 完成后**不自动**进入本阶段。参数:`--subagent` 强制 Subagent-Driven;`--inline` 等同默认;`--fixback` 读最新 review fixback。**默认 Inline,不询问执行模式**。
|
|
16
|
+
|
|
17
|
+
**单阶段原则**:run 关门(步骤 4 的 gate close + context close)完成后必须停止并交还用户,仅提示下一步可执行 `/harness-test`;禁止自动开始执行测试、review 或 submit。
|
|
15
18
|
|
|
16
19
|
## 前置条件
|
|
17
20
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: harness-submit
|
|
3
|
-
description: 最终提交封装:验证→中文 commit→提交/推送;worktree 模式含 --no-ff
|
|
4
|
-
|
|
3
|
+
description: 最终提交封装:验证→中文 commit→提交/推送;worktree 模式含 --no-ff 合并回主分支。仅当用户显式调用
|
|
4
|
+
/harness-submit(或 /harness-merge 重入合并段)时使用;用户口头说'提交/commit/push'时必须先确认,不得自动触发。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=25cf57e2e035246a; overlay=none; agent=codex; do not edit -->
|
|
7
7
|
# harness-submit — 最终提交(含 worktree 合并)
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -12,12 +12,9 @@ description: 最终提交封装:验证→中文 commit→提交/推送;workt
|
|
|
12
12
|
|
|
13
13
|
## When to Use
|
|
14
14
|
|
|
15
|
-
-
|
|
16
|
-
- 用户说「提交代码」「commit」「push」「完成开发」
|
|
17
|
-
- worktree 模式:用户说「合并分支」「merge to main」「合入主分支」「/harness-merge」(别名,从合并阶段重入亦可)
|
|
18
|
-
- review 和 test 已通过或已明确跳过
|
|
15
|
+
仅当用户显式调用 `/harness-submit`(或 `/harness-merge` 从合并段重入)时执行。用户口头说「提交代码」「commit」「push」「完成开发」而未调用本 skill 时,先确认是否走 Harness 提交阶段,**不得自动触发**。前置:review 和 test 已通过或已明确跳过。
|
|
19
16
|
|
|
20
|
-
> **主目录模式**(`worktree.json` requested=false):commit+push
|
|
17
|
+
> **主目录模式**(`worktree.json` requested=false):commit+push 主分支后**停止**,仅提示可执行 `/harness-archive`,不自动归档。**worktree 模式**(requested=true):worktree 内仅本地 commit,随后本 skill 在同一次调用内接续合并流程(这是同一次提交动作的一部分,不属于跨阶段自动推进);**push 只在主分支发生一次**;合并完成后同样**停止**,仅提示 `/harness-archive`。
|
|
21
18
|
|
|
22
19
|
## 前置条件
|
|
23
20
|
|
|
@@ -4,7 +4,7 @@ description: Use when the user asks to synchronize, refresh, or validate Harness
|
|
|
4
4
|
metadata, adapters, knowledge, rules, instruction entrypoints, config origins,
|
|
5
5
|
or CodeGraph status.
|
|
6
6
|
---
|
|
7
|
-
<!-- generated by harness_deploy.py; core=
|
|
7
|
+
<!-- generated by harness_deploy.py; core=25cf57e2e035246a; overlay=none; agent=codex; do not edit -->
|
|
8
8
|
# harness-sync
|
|
9
9
|
|
|
10
10
|
## Purpose
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: harness-test
|
|
3
|
-
description: 测试执行:读取场景表,执行单元测试+API
|
|
3
|
+
description: 测试执行:读取场景表,执行单元测试+API接口测试+数据兼容验证,输出测试报告。仅当用户显式调用 /harness-test
|
|
4
|
+
时使用;不得在 run 结束后自动接续执行。
|
|
4
5
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=25cf57e2e035246a; overlay=none; agent=codex; do not edit -->
|
|
6
7
|
# harness-test — 测试执行
|
|
7
8
|
|
|
8
9
|
## Purpose
|
|
@@ -11,7 +12,9 @@ description: 测试执行:读取场景表,执行单元测试+API接口测试
|
|
|
11
12
|
|
|
12
13
|
## When to Use
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
仅当用户显式调用 `/harness-test` 时执行(已设 `disable-model-invocation`)。run 阶段结束后**不自动**进入本阶段;用户口头提到"跑测试"而未调用本 skill 时,先确认是否走 Harness 测试阶段。
|
|
16
|
+
|
|
17
|
+
**单阶段原则**:test 关门后必须停止并交还用户,仅按风险层级提示下一步(`/harness-review` 或 `/harness-submit`);禁止自动接续执行。
|
|
15
18
|
|
|
16
19
|
使用场景:
|
|
17
20
|
- 完成 `/harness-run` 编码后,验证单元测试 + 接口测试 + 数据兼容
|