ultimate-pi 0.23.0 → 0.25.0

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 (62) hide show
  1. package/.pi/extensions/agt-prompt-guard.ts +20 -6
  2. package/.pi/extensions/harness-ask-user.ts +14 -5
  3. package/.pi/extensions/harness-auto-compact.ts +94 -0
  4. package/.pi/extensions/harness-debate-tools.ts +59 -4
  5. package/.pi/extensions/harness-live-widget.ts +25 -0
  6. package/.pi/extensions/harness-plan-approval.ts +65 -15
  7. package/.pi/extensions/harness-plan-orchestration.ts +140 -0
  8. package/.pi/extensions/harness-run-context.ts +501 -48
  9. package/.pi/extensions/harness-telemetry.ts +1 -0
  10. package/.pi/extensions/harness-web-tools.ts +1 -0
  11. package/.pi/extensions/policy-gate.ts +9 -0
  12. package/.pi/extensions/trace-recorder.ts +1 -0
  13. package/.pi/harness/agents.manifest.json +1 -1
  14. package/.pi/harness/docs/adrs/0056-agent-native-speed-wiring.md +26 -0
  15. package/.pi/harness/env.harness.template +14 -0
  16. package/.pi/harness/specs/harness-posthog-event.schema.json +2 -0
  17. package/.pi/harness/specs/sentrux-signal.schema.json +1 -1
  18. package/.pi/lib/harness-auto-approve.ts +140 -0
  19. package/.pi/lib/harness-auto-compact-policy.ts +85 -0
  20. package/.pi/lib/harness-cocoindex-refresh.ts +82 -2
  21. package/.pi/lib/harness-phase-telemetry.ts +81 -0
  22. package/.pi/lib/harness-phase-worker.ts +23 -0
  23. package/.pi/lib/harness-plan-fsm.ts +162 -0
  24. package/.pi/lib/harness-plan-route.ts +134 -0
  25. package/.pi/lib/harness-posthog.ts +6 -1
  26. package/.pi/lib/harness-remediation.ts +79 -0
  27. package/.pi/lib/harness-repair-brief.ts +2 -2
  28. package/.pi/lib/harness-review-parallel.ts +18 -0
  29. package/.pi/lib/harness-run-context.ts +119 -72
  30. package/.pi/lib/harness-spawn-budget.ts +32 -4
  31. package/.pi/lib/harness-spawn-stall-detector.ts +106 -0
  32. package/.pi/lib/harness-spawn-topology.ts +50 -1
  33. package/.pi/lib/harness-subagent-precheck.ts +41 -0
  34. package/.pi/lib/harness-subagent-progress.ts +119 -0
  35. package/.pi/lib/harness-subagent-timeout.ts +81 -0
  36. package/.pi/lib/harness-subagents-bridge.ts +94 -8
  37. package/.pi/lib/harness-ui-state.ts +5 -0
  38. package/.pi/lib/harness-vcc-settings.ts +36 -0
  39. package/.pi/lib/plan-approval-readiness.ts +9 -5
  40. package/.pi/lib/plan-debate-eligibility-snapshot.ts +90 -0
  41. package/.pi/lib/plan-debate-eligibility.ts +16 -9
  42. package/.pi/lib/plan-debate-focus.ts +23 -11
  43. package/.pi/lib/plan-debate-gate.ts +94 -31
  44. package/.pi/lib/plan-debate-round-status.ts +23 -8
  45. package/.pi/lib/plan-debate-wall-clock.ts +57 -0
  46. package/.pi/lib/plan-headless-ux.ts +598 -0
  47. package/.pi/lib/plan-human-gates.ts +24 -85
  48. package/.pi/lib/plan-messenger.ts +3 -3
  49. package/.pi/lib/plan-review-gate.ts +56 -0
  50. package/.pi/prompts/harness-abort.md +1 -0
  51. package/.pi/prompts/harness-auto.md +1 -1
  52. package/.pi/prompts/harness-clear.md +6 -6
  53. package/.pi/prompts/harness-plan.md +15 -2
  54. package/.pi/prompts/harness-review.md +26 -12
  55. package/.pi/scripts/harness-e2e-workflow.mjs +94 -0
  56. package/.pi/scripts/harness-project-toggle.mjs +1 -1
  57. package/.pi/scripts/harness-sentrux-cli.mjs +26 -1
  58. package/.pi/scripts/harness-sentrux-report.mjs +41 -6
  59. package/CHANGELOG.md +16 -0
  60. package/README.md +2 -2
  61. package/package.json +1 -1
  62. package/vendor/pi-subagents/src/subagents.ts +41 -10
@@ -53,6 +53,7 @@ const FLUSH_MAP: Record<string, HarnessPostHogEventName> = {
53
53
  "harness-eval-verdict": "harness_eval_verdict",
54
54
  "harness-sentrux-signal": "harness_sentrux_signal",
55
55
  "harness-observation": "harness_observation",
56
+ "harness-phase-completed": "harness_phase_completed",
56
57
  };
57
58
 
58
59
  function hashEntry(customType: string, data: unknown): string {
@@ -32,6 +32,7 @@ import {
32
32
  summarizeSearchJson,
33
33
  } from "../lib/harness-web/run-cli.js";
34
34
 
35
+ // @ts-expect-error pi extensions run as ESM
35
36
  const MODULE_URL = import.meta.url;
36
37
 
37
38
  const WEB_SEARCH_GUIDELINES = [
@@ -60,6 +60,7 @@ const PHASE_ORDER: HarnessPhase[] = [
60
60
  "merge",
61
61
  ];
62
62
 
63
+ // @ts-expect-error pi extensions run as ESM
63
64
  const MODULE_URL = import.meta.url;
64
65
 
65
66
  const MUTATING_TOOLS = new Set(["write", "edit"]);
@@ -183,6 +184,10 @@ async function handlePolicyBeforeAgentStart(args: {
183
184
  stateRef.current.updatedAt = stateRef.current.abortedAt;
184
185
  stateRef.current = state;
185
186
  pi.appendEntry("harness-policy-state", stateRef.current);
187
+ pi.events.emit("harness-run-aborted", {
188
+ reason: state.abortReason,
189
+ abortedAt: stateRef.current.abortedAt,
190
+ });
186
191
  return {
187
192
  message: {
188
193
  customType: "harness-policy-aborted",
@@ -435,6 +440,10 @@ export default function policyGate(pi: ExtensionAPI) {
435
440
  stateRef.current.abortedAt = nowIso();
436
441
  stateRef.current.updatedAt = stateRef.current.abortedAt;
437
442
  pi.appendEntry("harness-policy-state", stateRef.current);
443
+ pi.events.emit("harness-run-aborted", {
444
+ reason: stateRef.current.abortReason,
445
+ abortedAt: stateRef.current.abortedAt,
446
+ });
438
447
 
439
448
  const runCtx = getLatestRunContext(ctx.sessionManager.getEntries());
440
449
  if (runCtx) {
@@ -227,6 +227,7 @@ export default function traceRecorder(pi: ExtensionAPI) {
227
227
  plan_id: activeRun.planId,
228
228
  phase: activeRun.phase,
229
229
  started_at: startedAt,
230
+ phase_started_at: startedAt,
230
231
  });
231
232
 
232
233
  const runCtx = getLatestRunContext(entries);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "1.0.0",
3
3
  "package": "ultimate-pi",
4
- "package_version": "0.22.1",
4
+ "package_version": "0.25.0",
5
5
  "generated_at": "2026-05-27T15:57:32.501Z",
6
6
  "policy_sha256": "1a631333f1abed3b411961d3527bcae2d4fcd2f715b09a689b0b83b3ea0f54f3",
7
7
  "agents": {
@@ -0,0 +1,26 @@
1
+ # ADR 0056: Agent-native speed wiring (v0.25.0)
2
+
3
+ Status: Accepted
4
+ Date: 2026-06-06
5
+
6
+ ## Context
7
+
8
+ ADR 0042 documented agent-native orchestration (parallel probes, synthesizer path, FSM). v0.24.0 shipped latency infrastructure but runtime wiring remained meeting-shaped: `harness_debate_open` ignored `parallel_probes`, gates fell back to threaded mode, and parents re-reasoned every turn.
9
+
10
+ ## Decision
11
+
12
+ 1. **parallel_probes end-to-end** — `review_gate_mode` includes `parallel_probes`; eligibility snapshot at `artifacts/plan-debate-eligibility.yaml`; `effectiveMinFocusRounds` caps bus checks to 1; focus coverage parses `review-round-parallel-probes.yaml`.
13
+ 2. **SSOT routing** — `planReviewGateModeForProfile`: fast→consolidated, standard→parallel_probes, light/full→threaded.
14
+ 3. **plan-synthesizer default** — low/med route via `harness_plan_route`; readiness waives separate decompose/hypothesis when all three synthesizer artifacts exist.
15
+ 4. **Auto-approve** — `HARNESS_PLAN_AUTO_APPROVE` with `canAutoApprovePlan` / audit artifact; requires non-interactive or `force`.
16
+ 5. **Plan FSM** — `derivePlanNextAction` + `harness_plan_next_action` tool.
17
+ 6. **Spawn budget enforce** — per-phase caps when `HARNESS_BUDGET_ENFORCE=1` (plan 12, execute 3, evaluate 6).
18
+ 7. **Review parallel default** — evaluator∥adversary on by default unless `HARNESS_REVIEW_PARALLEL=0`, `--quick`, or steer_attempt ≥ 2.
19
+ 8. **Auto-compact 50%** — `harness-auto-compact` extension with hysteresis; subagent compact off by default.
20
+ 9. **Phase worker spike** — `HARNESS_PHASE_WORKER=1` env only; no cross evaluator/adversary resume.
21
+
22
+ ## Consequences
23
+
24
+ - Med-risk plans complete Review Gate in ≤4 debate spawns (validator, parallel evaluator∥adversary, integrator, submit).
25
+ - `HARNESS_REVIEW_PARALLEL=0` remains CI escape hatch.
26
+ - Amend ADR 0030 for 50% harness compact gate.
@@ -52,3 +52,17 @@ VAULT_WIKI_PATH=vault/wiki
52
52
  # --- Sentrux fitness functions ---
53
53
  # Require sentrux check + run signal (or CI stub) in harness-verify
54
54
  HARNESS_SENTRUX_REQUIRED=true
55
+ # HARNESS_SENTRUX_TIMEOUT_MS=300000
56
+ # HARNESS_SUBAGENT_TIMEOUT_PLAN_MS=1800000
57
+ # HARNESS_SUBAGENT_TIMEOUT_EXECUTE_MS=2700000
58
+ # HARNESS_SUBAGENT_TIMEOUT_REVIEW_MS=1200000
59
+ # HARNESS_SUBAGENT_TIMEOUT_DISABLE=0
60
+ # HARNESS_DEBATE_WALL_CLOCK_MS=1200000
61
+ # HARNESS_REVIEW_PARALLEL=0 # unset = parallel evaluator∥adversary default on (med/high, non-quick)
62
+ # HARNESS_PLAN_AUTO_APPROVE=1 # requires HARNESS_NON_INTERACTIVE=1 or =force
63
+ # HARNESS_COMPACT_THRESHOLD_PERCENT=50
64
+ # HARNESS_COMPACT_REARM_PERCENT=40
65
+ # HARNESS_COMPACT_AUTO=true
66
+ # HARNESS_COMPACT_SUBAGENTS=false
67
+ # HARNESS_PHASE_WORKER=1
68
+ # HARNESS_COCOINDEX_REFRESH_DEBOUNCE_MS=300000
@@ -27,8 +27,10 @@
27
27
  "harness_observation",
28
28
  "harness_subagent_spawned",
29
29
  "harness_subagent_completed",
30
+ "harness_subagent_timeout",
30
31
  "harness_subagent_result_wait",
31
32
  "harness_subagent_setup",
33
+ "harness_phase_completed",
32
34
  "harness_blackboard_op"
33
35
  ]
34
36
  },
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "gate_status": {
28
28
  "type": "string",
29
- "enum": ["pass", "degraded", "skipped", "not_installed"]
29
+ "enum": ["pass", "degraded", "skipped", "not_installed", "timeout"]
30
30
  },
31
31
  "quality_signal_summary": {
32
32
  "type": "string"
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Deterministic plan auto-approve when gates pass (HARNESS_PLAN_AUTO_APPROVE).
3
+ */
4
+
5
+ import { mkdir, writeFile } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import { stringify as stringifyYaml } from "yaml";
8
+ import { isHarnessNonInteractive } from "./ask-user/policy.js";
9
+ import type { PlanApprovalReadiness } from "./plan-approval-readiness.js";
10
+ import { loadPlanDebateEligibilitySnapshot } from "./plan-debate-eligibility-snapshot.js";
11
+ import type { PlanDebateGateResult } from "./plan-debate-gate.js";
12
+ import { readTaskClarificationDoc } from "./plan-task-clarification.js";
13
+
14
+ function missingPlanningContextReadinessError(error: string): boolean {
15
+ return (
16
+ error.includes("planning-context.yaml") ||
17
+ error.includes("missing artifacts/planning-context.yaml") ||
18
+ error.includes("missing:planning-reconnaissance")
19
+ );
20
+ }
21
+
22
+ function missingPhase35ReadinessError(error: string): boolean {
23
+ return (
24
+ error.includes("implementation-research.yaml") ||
25
+ error.includes("stack.yaml") ||
26
+ error.includes("Phase 3.5")
27
+ );
28
+ }
29
+
30
+ export const PLAN_APPROVAL_AUDIT_ARTIFACT =
31
+ "artifacts/plan-approval-audit.yaml";
32
+
33
+ export function isHarnessPlanAutoApproveForce(): boolean {
34
+ return (
35
+ process.env.HARNESS_PLAN_AUTO_APPROVE?.trim().toLowerCase() === "force"
36
+ );
37
+ }
38
+
39
+ export function isHarnessPlanAutoApproveEnabled(): boolean {
40
+ const raw = process.env.HARNESS_PLAN_AUTO_APPROVE?.trim().toLowerCase();
41
+ if (!raw || raw === "0" || raw === "false" || raw === "off") return false;
42
+ if (raw === "force") return true;
43
+ return raw === "1" || raw === "true" || raw === "on";
44
+ }
45
+
46
+ export interface AutoApprovePolicyInput {
47
+ projectRoot: string;
48
+ runId: string;
49
+ riskLevel: string;
50
+ readiness: PlanApprovalReadiness;
51
+ debateGate: PlanDebateGateResult;
52
+ dagPass?: boolean;
53
+ }
54
+
55
+ export interface AutoApprovePolicyResult {
56
+ allowed: boolean;
57
+ reasons: string[];
58
+ }
59
+
60
+ export async function canAutoApprovePlan(
61
+ input: AutoApprovePolicyInput,
62
+ ): Promise<AutoApprovePolicyResult> {
63
+ const reasons: string[] = [];
64
+ if (!isHarnessPlanAutoApproveEnabled()) {
65
+ return { allowed: false, reasons: ["HARNESS_PLAN_AUTO_APPROVE not set"] };
66
+ }
67
+ if (!isHarnessPlanAutoApproveForce() && !isHarnessNonInteractive()) {
68
+ reasons.push(
69
+ "interactive session — set HARNESS_NON_INTERACTIVE=1 or HARNESS_PLAN_AUTO_APPROVE=force",
70
+ );
71
+ }
72
+ const risk = String(input.riskLevel ?? "med").toLowerCase();
73
+ const qaSmoke =
74
+ process.env.HARNESS_QA_SMOKE === "1" && isHarnessNonInteractive();
75
+ if (risk === "high" && !qaSmoke)
76
+ reasons.push("high risk requires human approval");
77
+ if (!input.readiness.ok) {
78
+ for (const err of input.readiness.errors) {
79
+ if (
80
+ qaSmoke &&
81
+ risk === "low" &&
82
+ (missingPlanningContextReadinessError(err) ||
83
+ missingPhase35ReadinessError(err))
84
+ ) {
85
+ continue;
86
+ }
87
+ reasons.push(`readiness: ${err}`);
88
+ }
89
+ }
90
+ if (!input.debateGate.ok) {
91
+ reasons.push(...input.debateGate.errors.map((e) => `debate: ${e}`));
92
+ }
93
+ if (input.debateGate.warnings.some((w) => /block/i.test(w))) {
94
+ reasons.push("debate gate warnings include blocker");
95
+ }
96
+ const runDir = join(input.projectRoot, ".pi", "harness", "runs", input.runId);
97
+ const eligibility = await loadPlanDebateEligibilitySnapshot(runDir);
98
+ if (eligibility?.human_required) {
99
+ reasons.push("eligibility human_required=true");
100
+ }
101
+ const clar = await readTaskClarificationDoc(runDir);
102
+ if (clar?.needs_clarification === true) {
103
+ reasons.push("task-clarification needs_clarification");
104
+ }
105
+ if (input.dagPass === false) {
106
+ reasons.push("DAG validation not passed");
107
+ }
108
+ return { allowed: reasons.length === 0, reasons };
109
+ }
110
+
111
+ export async function writePlanApprovalAudit(
112
+ runDir: string,
113
+ doc: Record<string, unknown>,
114
+ ): Promise<void> {
115
+ const abs = join(runDir, PLAN_APPROVAL_AUDIT_ARTIFACT);
116
+ await mkdir(join(runDir, "artifacts"), { recursive: true });
117
+ await writeFile(abs, stringifyYaml(doc), "utf-8");
118
+ }
119
+
120
+ export interface AutoApproveOutcome {
121
+ approved: boolean;
122
+ reasons: string[];
123
+ }
124
+
125
+ /** Returns whether auto-approve was applied (caller skips dialog when true). */
126
+ export async function tryAutoApprovePlan(
127
+ input: AutoApprovePolicyInput,
128
+ ): Promise<AutoApproveOutcome> {
129
+ const policy = await canAutoApprovePlan(input);
130
+ const runDir = join(input.projectRoot, ".pi", "harness", "runs", input.runId);
131
+ await writePlanApprovalAudit(runDir, {
132
+ schema_version: "1.0.0",
133
+ source: policy.allowed ? "auto" : "blocked",
134
+ captured_at: new Date().toISOString(),
135
+ allowed: policy.allowed,
136
+ reasons: policy.reasons,
137
+ risk_level: input.riskLevel,
138
+ });
139
+ return { approved: policy.allowed, reasons: policy.reasons };
140
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Harness 50% auto-compact gate policy (testable without pi runtime).
3
+ */
4
+
5
+ import {
6
+ resolveCompactAuto,
7
+ resolveCompactRearmPercent,
8
+ resolveCompactSubagents,
9
+ resolveCompactThresholdPercent,
10
+ } from "./harness-vcc-settings.js";
11
+
12
+ export interface CompactUsage {
13
+ percent: number | null;
14
+ tokens?: number;
15
+ contextWindow?: number;
16
+ }
17
+
18
+ export interface CompactGateState {
19
+ armed: boolean;
20
+ inFlight: boolean;
21
+ cooldownTurns: number;
22
+ subagentSpawnPending: boolean;
23
+ }
24
+
25
+ export interface CompactGateDecision {
26
+ shouldCompact: boolean;
27
+ reason?: string;
28
+ }
29
+
30
+ export function createCompactGateState(): CompactGateState {
31
+ return {
32
+ armed: true,
33
+ inFlight: false,
34
+ cooldownTurns: 0,
35
+ subagentSpawnPending: false,
36
+ };
37
+ }
38
+
39
+ export function evaluateAutoCompactGate(
40
+ usage: CompactUsage,
41
+ state: CompactGateState,
42
+ opts?: { isSubagent?: boolean },
43
+ ): CompactGateDecision {
44
+ if (!resolveCompactAuto()) {
45
+ return { shouldCompact: false, reason: "HARNESS_COMPACT_AUTO=false" };
46
+ }
47
+ if (opts?.isSubagent && !resolveCompactSubagents()) {
48
+ return { shouldCompact: false, reason: "subagent compact disabled" };
49
+ }
50
+ if (state.subagentSpawnPending) {
51
+ return { shouldCompact: false, reason: "defer until subagent idle" };
52
+ }
53
+ if (state.inFlight) {
54
+ return { shouldCompact: false, reason: "compaction in flight" };
55
+ }
56
+ if (state.cooldownTurns > 0) {
57
+ return { shouldCompact: false, reason: "VCC cancel cooldown" };
58
+ }
59
+ if (!state.armed) {
60
+ const rearm = resolveCompactRearmPercent();
61
+ if (usage.percent != null && usage.percent < rearm) {
62
+ state.armed = true;
63
+ } else {
64
+ return { shouldCompact: false, reason: "hysteresis disarmed" };
65
+ }
66
+ }
67
+ const threshold = resolveCompactThresholdPercent();
68
+ if (usage.percent == null) {
69
+ return { shouldCompact: false, reason: "usage percent null" };
70
+ }
71
+ if (usage.percent < threshold) {
72
+ return { shouldCompact: false, reason: "below threshold" };
73
+ }
74
+ return { shouldCompact: true };
75
+ }
76
+
77
+ export function onSessionCompact(state: CompactGateState): void {
78
+ state.armed = false;
79
+ state.inFlight = false;
80
+ }
81
+
82
+ export function onCompactCancel(state: CompactGateState): void {
83
+ state.inFlight = false;
84
+ state.cooldownTurns = 2;
85
+ }
@@ -4,12 +4,82 @@
4
4
  */
5
5
 
6
6
  import { spawnSync } from "node:child_process";
7
- import { existsSync } from "node:fs";
7
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
 
10
10
  const DEFAULT_TIMEOUT_MS = 120_000;
11
+ const MARKER_REL = join(".cocoindex_code", ".harness-last-index.json");
11
12
 
12
- export function refreshHarnessCocoindexIndex(cwd: string): string | undefined {
13
+ interface IndexMarker {
14
+ indexed_at_ms: number;
15
+ git_head: string | null;
16
+ porcelain_empty: boolean;
17
+ }
18
+
19
+ function readMarker(cwd: string): IndexMarker | null {
20
+ const path = join(cwd, MARKER_REL);
21
+ try {
22
+ return JSON.parse(readFileSync(path, "utf8")) as IndexMarker;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ function writeMarker(cwd: string, marker: IndexMarker): void {
29
+ const path = join(cwd, MARKER_REL);
30
+ writeFileSync(path, `${JSON.stringify(marker, null, 2)}\n`, "utf8");
31
+ }
32
+
33
+ function gitHead(cwd: string): string | null {
34
+ const r = spawnSync("git", ["rev-parse", "HEAD"], {
35
+ cwd,
36
+ encoding: "utf8",
37
+ stdio: "pipe",
38
+ });
39
+ if (r.status !== 0) return null;
40
+ return (r.stdout ?? "").trim() || null;
41
+ }
42
+
43
+ function gitPorcelainEmpty(cwd: string): boolean {
44
+ const r = spawnSync("git", ["status", "--porcelain"], {
45
+ cwd,
46
+ encoding: "utf8",
47
+ stdio: "pipe",
48
+ });
49
+ if (r.status !== 0) return false;
50
+ return !(r.stdout ?? "").trim();
51
+ }
52
+
53
+ function shouldSkipIndex(cwd: string, forceExecuteRefresh: boolean): boolean {
54
+ if (forceExecuteRefresh) return false;
55
+ if (process.env.HARNESS_COCOINDEX_REFRESH === "0") return true;
56
+
57
+ const debounceMs = Number(
58
+ process.env.HARNESS_COCOINDEX_REFRESH_DEBOUNCE_MS ?? 300_000,
59
+ );
60
+ if (!Number.isFinite(debounceMs) || debounceMs <= 0) return false;
61
+
62
+ const marker = readMarker(cwd);
63
+ if (!marker) return false;
64
+
65
+ const age = Date.now() - marker.indexed_at_ms;
66
+ if (age >= debounceMs) return false;
67
+
68
+ const head = gitHead(cwd);
69
+ const porcelainEmpty = gitPorcelainEmpty(cwd);
70
+ if (!porcelainEmpty) return false;
71
+ if (head && marker.git_head && head !== marker.git_head) return false;
72
+
73
+ console.error(
74
+ `harness-cocoindex: skip ccc index (debounced ${Math.round(age / 1000)}s ago, git clean)`,
75
+ );
76
+ return true;
77
+ }
78
+
79
+ export function refreshHarnessCocoindexIndex(
80
+ cwd: string,
81
+ opts?: { forceExecuteRefresh?: boolean },
82
+ ): string | undefined {
13
83
  if (process.env.HARNESS_COCOINDEX_REFRESH === "0") {
14
84
  return undefined;
15
85
  }
@@ -18,6 +88,10 @@ export function refreshHarnessCocoindexIndex(cwd: string): string | undefined {
18
88
  return undefined;
19
89
  }
20
90
 
91
+ if (shouldSkipIndex(cwd, opts?.forceExecuteRefresh === true)) {
92
+ return undefined;
93
+ }
94
+
21
95
  const timeoutMs = Number(
22
96
  process.env.HARNESS_COCOINDEX_REFRESH_TIMEOUT_MS ?? DEFAULT_TIMEOUT_MS,
23
97
  );
@@ -45,5 +119,11 @@ export function refreshHarnessCocoindexIndex(cwd: string): string | undefined {
45
119
  return `${msg} — continuing`;
46
120
  }
47
121
 
122
+ writeMarker(cwd, {
123
+ indexed_at_ms: Date.now(),
124
+ git_head: gitHead(cwd),
125
+ porcelain_empty: gitPorcelainEmpty(cwd),
126
+ });
127
+
48
128
  return undefined;
49
129
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Phase boundary telemetry helpers (harness_phase_completed, phase_started_at).
3
+ */
4
+
5
+ import type { HarnessPhase } from "./harness-run-context.js";
6
+
7
+ const phaseStartedAt = new Map<string, number>();
8
+ const phaseCompletedKeys = new Set<string>();
9
+ const phaseSubagentCounts = new Map<string, number>();
10
+
11
+ function phaseKey(runId: string, phase: HarnessPhase): string {
12
+ return `${runId}:${phase}`;
13
+ }
14
+
15
+ export function recordHarnessPhaseStart(
16
+ runId: string,
17
+ phase: HarnessPhase,
18
+ ): void {
19
+ const key = phaseKey(runId, phase);
20
+ if (!phaseStartedAt.has(key)) {
21
+ phaseStartedAt.set(key, Date.now());
22
+ }
23
+ }
24
+
25
+ export function incrementHarnessPhaseSubagentCount(
26
+ runId: string,
27
+ phase: HarnessPhase,
28
+ delta = 1,
29
+ ): void {
30
+ const key = phaseKey(runId, phase);
31
+ phaseSubagentCounts.set(key, (phaseSubagentCounts.get(key) ?? 0) + delta);
32
+ }
33
+
34
+ export function phaseTerminalArtifact(
35
+ artifactPath: string,
36
+ ): HarnessPhase | null {
37
+ const norm = artifactPath.replace(/\\/g, "/");
38
+ if (norm === "artifacts/task-clarification.yaml") return "plan";
39
+ if (norm === "plan-packet.yaml") return "plan";
40
+ if (norm === "handoff/executor-summary.yaml") return "execute";
41
+ if (norm === "artifacts/review-outcome.yaml") return "evaluate";
42
+ return null;
43
+ }
44
+
45
+ export function buildPhaseCompletedPayload(
46
+ runId: string,
47
+ phase: HarnessPhase,
48
+ ): {
49
+ harness_run_id: string;
50
+ run_id: string;
51
+ harness_phase: HarnessPhase;
52
+ duration_ms: number;
53
+ subagent_count: number;
54
+ } | null {
55
+ const key = phaseKey(runId, phase);
56
+ if (phaseCompletedKeys.has(key)) return null;
57
+
58
+ const started = phaseStartedAt.get(key) ?? Date.now();
59
+ phaseCompletedKeys.add(key);
60
+
61
+ return {
62
+ harness_run_id: runId,
63
+ run_id: runId,
64
+ harness_phase: phase,
65
+ duration_ms: Math.max(0, Date.now() - started),
66
+ subagent_count: phaseSubagentCounts.get(key) ?? 0,
67
+ };
68
+ }
69
+
70
+ export function resetHarnessPhaseTelemetryForTests(): void {
71
+ phaseStartedAt.clear();
72
+ phaseCompletedKeys.clear();
73
+ phaseSubagentCounts.clear();
74
+ }
75
+
76
+ export function getHarnessPhaseSubagentCount(
77
+ runId: string,
78
+ phase: HarnessPhase,
79
+ ): number {
80
+ return phaseSubagentCounts.get(phaseKey(runId, phase)) ?? 0;
81
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Phase worker resume eligibility (HARNESS_PHASE_WORKER=1 spike).
3
+ * Never resume across evaluator ↔ adversary — preserves generator–evaluator isolation.
4
+ */
5
+
6
+ const DEBATE_ISOLATION_PAIRS = new Set([
7
+ "harness/planning/plan-evaluator",
8
+ "harness/planning/plan-adversary",
9
+ ]);
10
+
11
+ export function isHarnessPhaseWorkerEnabled(): boolean {
12
+ return process.env.HARNESS_PHASE_WORKER === "1";
13
+ }
14
+
15
+ export function phaseWorkerResumeEligible(
16
+ priorAgent: string | null,
17
+ nextAgent: string,
18
+ ): boolean {
19
+ if (!isHarnessPhaseWorkerEnabled()) return false;
20
+ if (!priorAgent || priorAgent !== nextAgent) return false;
21
+ if (DEBATE_ISOLATION_PAIRS.has(nextAgent)) return false;
22
+ return true;
23
+ }