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.
- package/.pi/extensions/agt-prompt-guard.ts +20 -6
- package/.pi/extensions/harness-ask-user.ts +14 -5
- package/.pi/extensions/harness-auto-compact.ts +94 -0
- package/.pi/extensions/harness-debate-tools.ts +59 -4
- package/.pi/extensions/harness-live-widget.ts +25 -0
- package/.pi/extensions/harness-plan-approval.ts +65 -15
- package/.pi/extensions/harness-plan-orchestration.ts +140 -0
- package/.pi/extensions/harness-run-context.ts +501 -48
- package/.pi/extensions/harness-telemetry.ts +1 -0
- package/.pi/extensions/harness-web-tools.ts +1 -0
- package/.pi/extensions/policy-gate.ts +9 -0
- package/.pi/extensions/trace-recorder.ts +1 -0
- package/.pi/harness/agents.manifest.json +1 -1
- package/.pi/harness/docs/adrs/0056-agent-native-speed-wiring.md +26 -0
- package/.pi/harness/env.harness.template +14 -0
- package/.pi/harness/specs/harness-posthog-event.schema.json +2 -0
- package/.pi/harness/specs/sentrux-signal.schema.json +1 -1
- package/.pi/lib/harness-auto-approve.ts +140 -0
- package/.pi/lib/harness-auto-compact-policy.ts +85 -0
- package/.pi/lib/harness-cocoindex-refresh.ts +82 -2
- package/.pi/lib/harness-phase-telemetry.ts +81 -0
- package/.pi/lib/harness-phase-worker.ts +23 -0
- package/.pi/lib/harness-plan-fsm.ts +162 -0
- package/.pi/lib/harness-plan-route.ts +134 -0
- package/.pi/lib/harness-posthog.ts +6 -1
- package/.pi/lib/harness-remediation.ts +79 -0
- package/.pi/lib/harness-repair-brief.ts +2 -2
- package/.pi/lib/harness-review-parallel.ts +18 -0
- package/.pi/lib/harness-run-context.ts +119 -72
- package/.pi/lib/harness-spawn-budget.ts +32 -4
- package/.pi/lib/harness-spawn-stall-detector.ts +106 -0
- package/.pi/lib/harness-spawn-topology.ts +50 -1
- package/.pi/lib/harness-subagent-precheck.ts +41 -0
- package/.pi/lib/harness-subagent-progress.ts +119 -0
- package/.pi/lib/harness-subagent-timeout.ts +81 -0
- package/.pi/lib/harness-subagents-bridge.ts +94 -8
- package/.pi/lib/harness-ui-state.ts +5 -0
- package/.pi/lib/harness-vcc-settings.ts +36 -0
- package/.pi/lib/plan-approval-readiness.ts +9 -5
- package/.pi/lib/plan-debate-eligibility-snapshot.ts +90 -0
- package/.pi/lib/plan-debate-eligibility.ts +16 -9
- package/.pi/lib/plan-debate-focus.ts +23 -11
- package/.pi/lib/plan-debate-gate.ts +94 -31
- package/.pi/lib/plan-debate-round-status.ts +23 -8
- package/.pi/lib/plan-debate-wall-clock.ts +57 -0
- package/.pi/lib/plan-headless-ux.ts +598 -0
- package/.pi/lib/plan-human-gates.ts +24 -85
- package/.pi/lib/plan-messenger.ts +3 -3
- package/.pi/lib/plan-review-gate.ts +56 -0
- package/.pi/prompts/harness-abort.md +1 -0
- package/.pi/prompts/harness-auto.md +1 -1
- package/.pi/prompts/harness-clear.md +6 -6
- package/.pi/prompts/harness-plan.md +15 -2
- package/.pi/prompts/harness-review.md +26 -12
- package/.pi/scripts/harness-e2e-workflow.mjs +94 -0
- package/.pi/scripts/harness-project-toggle.mjs +1 -1
- package/.pi/scripts/harness-sentrux-cli.mjs +26 -1
- package/.pi/scripts/harness-sentrux-report.mjs +41 -6
- package/CHANGELOG.md +16 -0
- package/README.md +2 -2
- package/package.json +1 -1
- 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 {
|
|
@@ -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) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.0.0",
|
|
3
3
|
"package": "ultimate-pi",
|
|
4
|
-
"package_version": "0.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
+
}
|