@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.
Files changed (155) hide show
  1. package/README.md +1 -1
  2. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  3. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +9 -10
  4. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +6 -3
  9. package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -7
  10. package/harness/bundles/general/claude-code/harness-run/SKILL.md +6 -3
  11. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +5 -7
  12. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-test/SKILL.md +6 -3
  14. package/harness/bundles/general/claude-code/scripts/harness_archive.py +164 -17
  15. package/harness/bundles/general/claude-code/scripts/harness_events.py +30 -1
  16. package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +269 -0
  17. package/harness/bundles/general/claude-code/scripts/harness_gate.py +216 -64
  18. package/harness/bundles/general/claude-code/scripts/harness_headless.py +37 -0
  19. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  20. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +9 -11
  21. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  22. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  23. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  24. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +6 -3
  26. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -7
  27. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +6 -3
  28. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +5 -8
  29. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +6 -3
  31. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +164 -17
  32. package/harness/bundles/general/codebuddy/scripts/harness_events.py +30 -1
  33. package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +269 -0
  34. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +216 -64
  35. package/harness/bundles/general/codebuddy/scripts/harness_headless.py +37 -0
  36. package/harness/bundles/general/codex/.harness-build.json +1 -1
  37. package/harness/bundles/general/codex/harness-archive/SKILL.md +9 -11
  38. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  39. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  40. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  41. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  42. package/harness/bundles/general/codex/harness-plan/SKILL.md +6 -3
  43. package/harness/bundles/general/codex/harness-review/SKILL.md +6 -7
  44. package/harness/bundles/general/codex/harness-run/SKILL.md +6 -3
  45. package/harness/bundles/general/codex/harness-submit/SKILL.md +5 -8
  46. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-test/SKILL.md +6 -3
  48. package/harness/bundles/general/codex/scripts/harness_archive.py +164 -17
  49. package/harness/bundles/general/codex/scripts/harness_events.py +30 -1
  50. package/harness/bundles/general/codex/scripts/harness_events_sync.py +269 -0
  51. package/harness/bundles/general/codex/scripts/harness_gate.py +216 -64
  52. package/harness/bundles/general/codex/scripts/harness_headless.py +37 -0
  53. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  54. package/harness/bundles/general/cursor/harness-archive/SKILL.md +9 -11
  55. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  56. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  57. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  58. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  59. package/harness/bundles/general/cursor/harness-plan/SKILL.md +6 -3
  60. package/harness/bundles/general/cursor/harness-review/SKILL.md +6 -7
  61. package/harness/bundles/general/cursor/harness-run/SKILL.md +6 -3
  62. package/harness/bundles/general/cursor/harness-submit/SKILL.md +5 -8
  63. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  64. package/harness/bundles/general/cursor/harness-test/SKILL.md +6 -3
  65. package/harness/bundles/general/cursor/scripts/harness_archive.py +164 -17
  66. package/harness/bundles/general/cursor/scripts/harness_events.py +30 -1
  67. package/harness/bundles/general/cursor/scripts/harness_events_sync.py +269 -0
  68. package/harness/bundles/general/cursor/scripts/harness_gate.py +216 -64
  69. package/harness/bundles/general/cursor/scripts/harness_headless.py +37 -0
  70. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  71. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +3 -2
  72. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +9 -10
  73. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  74. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  75. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  76. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  77. package/harness/bundles/java/claude-code/harness-package/SKILL.md +3 -2
  78. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +6 -3
  79. package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -7
  80. package/harness/bundles/java/claude-code/harness-run/SKILL.md +6 -3
  81. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +5 -7
  82. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  83. package/harness/bundles/java/claude-code/harness-test/SKILL.md +6 -3
  84. package/harness/bundles/java/claude-code/scripts/harness_archive.py +164 -17
  85. package/harness/bundles/java/claude-code/scripts/harness_events.py +30 -1
  86. package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +269 -0
  87. package/harness/bundles/java/claude-code/scripts/harness_gate.py +216 -64
  88. package/harness/bundles/java/claude-code/scripts/harness_headless.py +37 -0
  89. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  90. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +3 -2
  91. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +9 -11
  92. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  93. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  94. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  95. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  96. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +2 -2
  97. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +6 -3
  98. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -7
  99. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +6 -3
  100. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +5 -8
  101. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  102. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +6 -3
  103. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +164 -17
  104. package/harness/bundles/java/codebuddy/scripts/harness_events.py +30 -1
  105. package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +269 -0
  106. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +216 -64
  107. package/harness/bundles/java/codebuddy/scripts/harness_headless.py +37 -0
  108. package/harness/bundles/java/codex/.harness-build.json +1 -1
  109. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +3 -2
  110. package/harness/bundles/java/codex/harness-archive/SKILL.md +9 -11
  111. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  112. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  113. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  114. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  115. package/harness/bundles/java/codex/harness-package/SKILL.md +2 -2
  116. package/harness/bundles/java/codex/harness-plan/SKILL.md +6 -3
  117. package/harness/bundles/java/codex/harness-review/SKILL.md +6 -7
  118. package/harness/bundles/java/codex/harness-run/SKILL.md +6 -3
  119. package/harness/bundles/java/codex/harness-submit/SKILL.md +5 -8
  120. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  121. package/harness/bundles/java/codex/harness-test/SKILL.md +6 -3
  122. package/harness/bundles/java/codex/scripts/harness_archive.py +164 -17
  123. package/harness/bundles/java/codex/scripts/harness_events.py +30 -1
  124. package/harness/bundles/java/codex/scripts/harness_events_sync.py +269 -0
  125. package/harness/bundles/java/codex/scripts/harness_gate.py +216 -64
  126. package/harness/bundles/java/codex/scripts/harness_headless.py +37 -0
  127. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  128. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +3 -2
  129. package/harness/bundles/java/cursor/harness-archive/SKILL.md +9 -11
  130. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  131. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  132. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  133. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  134. package/harness/bundles/java/cursor/harness-package/SKILL.md +2 -2
  135. package/harness/bundles/java/cursor/harness-plan/SKILL.md +6 -3
  136. package/harness/bundles/java/cursor/harness-review/SKILL.md +6 -7
  137. package/harness/bundles/java/cursor/harness-run/SKILL.md +6 -3
  138. package/harness/bundles/java/cursor/harness-submit/SKILL.md +5 -8
  139. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  140. package/harness/bundles/java/cursor/harness-test/SKILL.md +6 -3
  141. package/harness/bundles/java/cursor/scripts/harness_archive.py +164 -17
  142. package/harness/bundles/java/cursor/scripts/harness_events.py +30 -1
  143. package/harness/bundles/java/cursor/scripts/harness_events_sync.py +269 -0
  144. package/harness/bundles/java/cursor/scripts/harness_gate.py +216 -64
  145. package/harness/bundles/java/cursor/scripts/harness_headless.py +37 -0
  146. package/harness/manifests/general/claude-code.json +25 -17
  147. package/harness/manifests/general/codebuddy.json +25 -17
  148. package/harness/manifests/general/codex.json +25 -17
  149. package/harness/manifests/general/cursor.json +25 -17
  150. package/harness/manifests/java/claude-code.json +27 -19
  151. package/harness/manifests/java/codebuddy.json +27 -19
  152. package/harness/manifests/java/codex.json +27 -19
  153. package/harness/manifests/java/cursor.json +27 -19
  154. package/hunter-workflow-family.json +4 -4
  155. 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 (knowledge / "index.sqlite").exists()
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 = {
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=769659e013ca1779; overlay=java; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=a4143be109944c07; overlay=java; 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 的上下文归属。
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: harness-package
3
- description: "增量模块打包:拉取最新→编译验证→mvn package→汇总 jar/war。使用场景:打包、package、发版准备"
3
+ description: "增量模块打包:拉取最新→编译验证→mvn package→汇总 jar/war。仅当用户显式调用 /harness-package 时使用,不得自动触发。"
4
+ disable-model-invocation: true
4
5
  argument-hint: "变更名或留空自动检测"
5
6
  effort: medium
6
7
  allowed-tools: [Bash(powershell.exe:*), Read, Write, Edit, Glob, Grep]
@@ -20,7 +21,7 @@ disallowed-tools:
20
21
  - Bash(awk *)
21
22
  - Bash(curl *)
22
23
  ---
23
- <!-- generated by harness_deploy.py; core=769659e013ca1779; overlay=java; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=a4143be109944c07; overlay=java; agent=claude-code; do not edit -->
24
25
  # harness-package — 增量模块打包(Java overlay)
25
26
 
26
27
  ## Purpose
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: harness-plan
3
- description: "将需求转化为设计文档+实施计划+完整测试场景表,必须在编码前完成。使用场景:需求分析、feature plan、技术方案设计、实现方案规划"
3
+ description: "将需求转化为设计文档+实施计划+完整测试场景表,必须在编码前完成。仅当用户显式调用 /harness-plan 或明确要求进入 Harness 规划阶段时使用;不得因用户描述需求就自动触发。"
4
+ disable-model-invocation: true
4
5
  argument-hint: "需求描述 | --adversarial"
5
6
  effort: medium
6
7
  allowed-tools: [Read, Glob, Grep, Edit, Write, Agent, Bash(powershell.exe:*)]
@@ -20,7 +21,7 @@ disallowed-tools:
20
21
  - Bash(awk *)
21
22
  - Bash(curl *)
22
23
  ---
23
- <!-- generated by harness_deploy.py; core=769659e013ca1779; overlay=java; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=a4143be109944c07; overlay=java; agent=claude-code; do not edit -->
24
25
  # harness-plan — 需求规划
25
26
 
26
27
  ## Purpose
@@ -31,7 +32,9 @@ disallowed-tools:
31
32
 
32
33
  ## When to Use
33
34
 
34
- `/harness-plan`、新功能设计、技术方案、测试场景表规划。
35
+ 仅当用户显式调用 `/harness-plan`(或明确说"用 harness 规划这个需求")时执行。用户只是描述需求、提问或讨论方案时,**不得**自动进入本 skill。
36
+
37
+ **单阶段原则**:本 skill 只负责 plan 阶段。plan finalize + verify 完成后必须停止并交还用户,仅提示下一步可执行 `/harness-run`;禁止自动开始编码或调用其他 harness 阶段 skill。
35
38
 
36
39
  ## 统一读取协议
37
40
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: harness-review
3
- description: "6维度代码审查(架构/安全/规范/兼容/测试/性能),对照项目规则(见 .harness/context-index.json)和测试场景表,在隔离上下文运行。使用场景:代码审查、提交前检查、合并评审"
3
+ description: "6维度代码审查(架构/安全/规范/兼容/测试/性能),对照项目规则(见 .harness/context-index.json)和测试场景表,在隔离上下文运行。仅当用户显式调用 /harness-review 时使用;不得在 test 结束后自动接续执行。"
4
+ disable-model-invocation: true
4
5
  argument-hint: "变更名或留空自动检测"
5
6
  effort: high
6
7
  allowed-tools: [Read, Write, Edit, Glob, Grep, Agent, Bash(powershell.exe:*)]
@@ -21,7 +22,7 @@ disallowed-tools:
21
22
  - Bash(curl *)
22
23
  - Bash(codegraph *)
23
24
  ---
24
- <!-- generated by harness_deploy.py; core=769659e013ca1779; overlay=java; agent=claude-code; do not edit -->
25
+ <!-- generated by harness_deploy.py; core=a4143be109944c07; overlay=java; agent=claude-code; do not edit -->
25
26
  # harness-review — 代码审查
26
27
 
27
28
  ## Purpose
@@ -30,11 +31,9 @@ disallowed-tools:
30
31
 
31
32
  ## When to Use
32
33
 
33
- 使用此 Skill 当:
34
- - 代码变更完成后需要审查
35
- - 提交前需要质量检查
36
- - 合并评审时需要系统性检查
37
- - 用户说"审查代码"、"review"、"检查代码质量"
34
+ 仅当用户显式调用 `/harness-review` 时执行。test 阶段结束后**不自动**进入本阶段;用户口头提到"审查/review"而未调用本 skill 时,先确认是否走 Harness 审查阶段。
35
+
36
+ **单阶段原则**:review 关门(报告 + fixback 产出)后必须停止并交还用户,仅提示下一步可执行 `/harness-run --fixback`(有待修项)或 `/harness-submit`;禁止自动接续执行。
38
37
 
39
38
  ## Inputs
40
39
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: harness-run
3
- description: "按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编译验证),逐变更簇实现计划中的任务。使用场景:开始编码、实现功能、写代码、TDD 编码"
3
+ description: "按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编译验证),逐变更簇实现计划中的任务。仅当用户显式调用 /harness-run 时使用;不得因用户提到编码/实现就自动触发,也不得被其他阶段 skill 自动接续。"
4
+ disable-model-invocation: true
4
5
  argument-hint: "变更名 | --subagent | --inline | --fixback | 留空自动检测"
5
6
  effort: medium
6
7
  allowed-tools: [Read, Edit, Write, Glob, Grep, Bash(powershell.exe:*)]
@@ -21,7 +22,7 @@ disallowed-tools:
21
22
  - Bash(curl *)
22
23
  - Bash(codegraph *)
23
24
  ---
24
- <!-- generated by harness_deploy.py; core=769659e013ca1779; overlay=java; agent=claude-code; do not edit -->
25
+ <!-- generated by harness_deploy.py; core=a4143be109944c07; overlay=java; agent=claude-code; do not edit -->
25
26
  # harness-run — 需求编码
26
27
 
27
28
  ## Purpose
@@ -30,7 +31,9 @@ disallowed-tools:
30
31
 
31
32
  ## When to Use
32
33
 
33
- 触发语:"开始编码""实现功能""写代码""TDD编码"。参数:`--subagent` 强制 Subagent-Driven;`--inline` 等同默认;`--fixback` 读最新 review fixback。**默认 Inline,不询问执行模式**。
34
+ 仅当用户显式调用 `/harness-run` 时执行;plan 完成后**不自动**进入本阶段。参数:`--subagent` 强制 Subagent-Driven;`--inline` 等同默认;`--fixback` 读最新 review fixback。**默认 Inline,不询问执行模式**。
35
+
36
+ **单阶段原则**:run 关门(步骤 4 的 gate close + context close)完成后必须停止并交还用户,仅提示下一步可执行 `/harness-test`;禁止自动开始执行测试、review 或 submit。
34
37
 
35
38
  ## 前置条件
36
39
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: harness-submit
3
- description: "最终提交封装:验证→中文 commit→提交/推送;worktree 模式含 --no-ff 合并回主分支。使用场景:提交代码、commitpush、合并分支、merge to main、完成开发"
3
+ description: "最终提交封装:验证→中文 commit→提交/推送;worktree 模式含 --no-ff 合并回主分支。仅当用户显式调用 /harness-submit(或 /harness-merge 重入合并段)时使用;用户口头说'提交/commit/push'时必须先确认,不得自动触发。"
4
+ disable-model-invocation: true
4
5
  argument-hint: "变更名或留空自动检测"
5
6
  effort: medium
6
7
  allowed-tools: [Bash(powershell.exe:*), Read, Write, Edit, Glob, Grep]
@@ -20,7 +21,7 @@ disallowed-tools:
20
21
  - Bash(awk *)
21
22
  - Bash(curl *)
22
23
  ---
23
- <!-- generated by harness_deploy.py; core=769659e013ca1779; overlay=java; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=a4143be109944c07; overlay=java; agent=claude-code; do not edit -->
24
25
  # harness-submit — 最终提交(含 worktree 合并)
25
26
 
26
27
  ## Purpose
@@ -29,12 +30,9 @@ disallowed-tools:
29
30
 
30
31
  ## When to Use
31
32
 
32
- - 代码开发完成,准备提交
33
- - 用户说「提交代码」「commit」「push」「完成开发」
34
- - worktree 模式:用户说「合并分支」「merge to main」「合入主分支」「/harness-merge」(别名,从合并阶段重入亦可)
35
- - review 和 test 已通过或已明确跳过
33
+ 仅当用户显式调用 `/harness-submit`(或 `/harness-merge` 从合并段重入)时执行。用户口头说「提交代码」「commit」「push」「完成开发」而未调用本 skill 时,先确认是否走 Harness 提交阶段,**不得自动触发**。前置:review 和 test 已通过或已明确跳过。
36
34
 
37
- > **主目录模式**(`worktree.json` requested=false):commit+push 主分支后直接 `/harness-archive`。**worktree 模式**(requested=true):worktree 内仅本地 commit,随后本 skill 自动执行合并流程;**push 只在主分支发生一次**。
35
+ > **主目录模式**(`worktree.json` requested=false):commit+push 主分支后**停止**,仅提示可执行 `/harness-archive`,不自动归档。**worktree 模式**(requested=true):worktree 内仅本地 commit,随后本 skill 在同一次调用内接续合并流程(这是同一次提交动作的一部分,不属于跨阶段自动推进);**push 只在主分支发生一次**;合并完成后同样**停止**,仅提示 `/harness-archive`。
38
36
 
39
37
  ## 前置条件
40
38
 
@@ -19,7 +19,7 @@ disallowed-tools:
19
19
  - Bash(awk *)
20
20
  - Bash(curl *)
21
21
  ---
22
- <!-- generated by harness_deploy.py; core=769659e013ca1779; overlay=java; agent=claude-code; do not edit -->
22
+ <!-- generated by harness_deploy.py; core=a4143be109944c07; overlay=java; agent=claude-code; do not edit -->
23
23
  # harness-sync
24
24
 
25
25
  ## Purpose
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: harness-test
3
- description: "测试执行:读取场景表,执行单元测试+API接口测试+数据兼容验证,输出测试报告。当用户说'跑测试/验证/跑用例/接口测试/单元测试'时使用"
3
+ description: "测试执行:读取场景表,执行单元测试+API接口测试+数据兼容验证,输出测试报告。仅当用户显式调用 /harness-test 时使用;不得在 run 结束后自动接续执行。"
4
+ disable-model-invocation: true
4
5
  argument-hint: "变更名或留空自动检测"
5
6
  effort: medium
6
7
  allowed-tools: [Read, Glob, Grep, Write, Edit, Agent, Bash(powershell.exe:*)]
@@ -22,7 +23,7 @@ disallowed-tools:
22
23
  - Bash(node *)
23
24
  - Bash(codegraph *)
24
25
  ---
25
- <!-- generated by harness_deploy.py; core=769659e013ca1779; overlay=java; agent=claude-code; do not edit -->
26
+ <!-- generated by harness_deploy.py; core=a4143be109944c07; overlay=java; agent=claude-code; do not edit -->
26
27
  # harness-test — 测试执行
27
28
 
28
29
  ## Purpose
@@ -31,7 +32,9 @@ disallowed-tools:
31
32
 
32
33
  ## When to Use
33
34
 
34
- 当用户明确要求运行测试时触发。典型触发语:"跑测试""验证""跑用例""接口测试""单元测试"。属于自动调用型 skill(未设 `disable-model-invocation`),默认经 `/harness-test` 显式调用。
35
+ 仅当用户显式调用 `/harness-test` 时执行(已设 `disable-model-invocation`)。run 阶段结束后**不自动**进入本阶段;用户口头提到"跑测试"而未调用本 skill 时,先确认是否走 Harness 测试阶段。
36
+
37
+ **单阶段原则**:test 关门后必须停止并交还用户,仅按风险层级提示下一步(`/harness-review` 或 `/harness-submit`);禁止自动接续执行。
35
38
 
36
39
  使用场景:
37
40
  - 完成 `/harness-run` 编码后,验证单元测试 + 接口测试 + 数据兼容