ultimate-pi 0.24.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-auto-compact.ts +94 -0
- package/.pi/extensions/harness-debate-tools.ts +26 -2
- package/.pi/extensions/harness-live-widget.ts +19 -2
- package/.pi/extensions/harness-plan-approval.ts +62 -19
- package/.pi/extensions/harness-plan-orchestration.ts +140 -0
- package/.pi/extensions/harness-run-context.ts +457 -48
- package/.pi/extensions/harness-web-tools.ts +1 -0
- package/.pi/extensions/policy-gate.ts +9 -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 +7 -1
- package/.pi/lib/harness-auto-approve.ts +140 -0
- package/.pi/lib/harness-auto-compact-policy.ts +85 -0
- package/.pi/lib/harness-phase-telemetry.ts +7 -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 +4 -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-topology.ts +36 -1
- package/.pi/lib/harness-subagent-precheck.ts +3 -2
- package/.pi/lib/harness-subagent-progress.ts +8 -5
- package/.pi/lib/harness-subagents-bridge.ts +14 -12
- 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 +12 -7
- package/.pi/lib/plan-debate-focus.ts +23 -11
- package/.pi/lib/plan-debate-gate.ts +71 -29
- package/.pi/lib/plan-debate-round-status.ts +23 -8
- 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 +2 -2
- package/.pi/scripts/harness-project-toggle.mjs +1 -1
- package/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
resetHarnessPolicyDenyCount,
|
|
24
24
|
} from "../lib/agt/kill-switch-state.js";
|
|
25
25
|
import { runAskUser } from "../lib/ask-user/index.js";
|
|
26
|
+
import { isHarnessNonInteractive } from "../lib/ask-user/policy.js";
|
|
26
27
|
import { claimHarnessGovernanceLoad } from "../lib/extension-load-guard.js";
|
|
27
28
|
import { getHarnessPackageRoot } from "../lib/harness-paths.js";
|
|
28
29
|
import {
|
|
@@ -40,10 +41,12 @@ import {
|
|
|
40
41
|
claimRunOwnership,
|
|
41
42
|
createFreshRunContext,
|
|
42
43
|
criticalPathWorkItemIdsFromPlanPacket,
|
|
44
|
+
deleteProjectActiveRun,
|
|
43
45
|
driftGateActive,
|
|
44
46
|
ensureReviewOutcomeFromEval,
|
|
45
47
|
evaluateCrossSessionResume,
|
|
46
48
|
extractWritePathFromToolInput,
|
|
49
|
+
findActiveRunOwnershipConflict,
|
|
47
50
|
formatActivePlanBlock,
|
|
48
51
|
formatCrossSessionResumeMessage,
|
|
49
52
|
formatPlanContextBlock,
|
|
@@ -56,6 +59,7 @@ import {
|
|
|
56
59
|
harnessAutoTasksDiffer,
|
|
57
60
|
hasHarnessAbortSignal,
|
|
58
61
|
hasPlanUserApproval,
|
|
62
|
+
indexOfLastPlanCommand,
|
|
59
63
|
inferHarnessPhase,
|
|
60
64
|
isAmendPlanAllowed,
|
|
61
65
|
isHarnessBootstrapPrompt,
|
|
@@ -80,6 +84,7 @@ import {
|
|
|
80
84
|
reconcileReviewRouting,
|
|
81
85
|
reconcileStaleExecuteCompletion,
|
|
82
86
|
refreshRunContextProgress,
|
|
87
|
+
releaseForeignQaRunOwnership,
|
|
83
88
|
relPathUnderActiveRun,
|
|
84
89
|
resetRunContextForHarnessAuto,
|
|
85
90
|
resolveArgsForCommand,
|
|
@@ -109,6 +114,14 @@ import {
|
|
|
109
114
|
} from "../lib/harness-yaml.js";
|
|
110
115
|
import { isReviewRoundArtifactPath } from "../lib/plan-debate-gate.js";
|
|
111
116
|
import { isReviewRoundYamlWriteAllowed } from "../lib/plan-debate-write-guard.js";
|
|
117
|
+
import {
|
|
118
|
+
endHeadlessHarnessPrintSession,
|
|
119
|
+
maybeForceHeadlessPlanProgress,
|
|
120
|
+
maybeHeadlessQaAutoExecuteSmoke,
|
|
121
|
+
seedHeadlessTaskClarificationIfNeeded,
|
|
122
|
+
shouldEndHeadlessHarnessPrintSession,
|
|
123
|
+
tryHeadlessAutoPlanFinalize,
|
|
124
|
+
} from "../lib/plan-headless-ux.js";
|
|
112
125
|
import {
|
|
113
126
|
formatPlanHumanGateBlock,
|
|
114
127
|
resolvePlanHumanGateStatus,
|
|
@@ -154,6 +167,15 @@ function persistContext(pi: ExtensionAPI, ctx: HarnessRunContext): void {
|
|
|
154
167
|
pi.events.emit("harness-run-context:updated", { run_id: ctx.run_id });
|
|
155
168
|
}
|
|
156
169
|
|
|
170
|
+
function notifyHarnessHandoff(
|
|
171
|
+
ctx: { hasUI: boolean; ui: { notify(message: string, type?: string): void } },
|
|
172
|
+
message: string,
|
|
173
|
+
level: "info" | "warning" = "info",
|
|
174
|
+
): void {
|
|
175
|
+
if (ctx.hasUI) ctx.ui.notify(message, level);
|
|
176
|
+
// Headless (-p/json): appendEntry records handoff; never inject user-visible messages.
|
|
177
|
+
}
|
|
178
|
+
|
|
157
179
|
const PLAN_REVISION_ARTIFACT_FILES = new Set([
|
|
158
180
|
"planning-context.yaml",
|
|
159
181
|
"decomposition.yaml",
|
|
@@ -480,7 +502,7 @@ async function applyAbortSignal(input: {
|
|
|
480
502
|
entries: unknown[];
|
|
481
503
|
userPrompt: string;
|
|
482
504
|
}): Promise<HarnessRunContext | null> {
|
|
483
|
-
if (!input.userPrompt
|
|
505
|
+
if (!hasHarnessAbortSignal(input.userPrompt)) {
|
|
484
506
|
return input.activeCtx;
|
|
485
507
|
}
|
|
486
508
|
const nextCtx =
|
|
@@ -498,6 +520,43 @@ async function applyAbortSignal(input: {
|
|
|
498
520
|
return nextCtx;
|
|
499
521
|
}
|
|
500
522
|
|
|
523
|
+
function appendAbortPolicyState(
|
|
524
|
+
pi: ExtensionAPI,
|
|
525
|
+
reason: string,
|
|
526
|
+
abortedAt: string,
|
|
527
|
+
): void {
|
|
528
|
+
pi.appendEntry("harness-policy-state", {
|
|
529
|
+
phase: "plan",
|
|
530
|
+
approvedPlan: false,
|
|
531
|
+
planId: null,
|
|
532
|
+
budgetBypass: false,
|
|
533
|
+
aborted: true,
|
|
534
|
+
abortReason: reason,
|
|
535
|
+
abortedAt,
|
|
536
|
+
updatedAt: abortedAt,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function abortActiveRunContext(input: {
|
|
541
|
+
pi: ExtensionAPI;
|
|
542
|
+
activeCtx: HarnessRunContext;
|
|
543
|
+
reason: string;
|
|
544
|
+
}): HarnessRunContext {
|
|
545
|
+
const abortedAt = nowIso();
|
|
546
|
+
input.activeCtx.phase = "plan";
|
|
547
|
+
input.activeCtx.status = "aborted";
|
|
548
|
+
input.activeCtx.plan_ready = false;
|
|
549
|
+
input.activeCtx.last_outcome = "aborted";
|
|
550
|
+
input.activeCtx.last_completed_step = "abort";
|
|
551
|
+
input.activeCtx.next_recommended_command = input.activeCtx.task_summary
|
|
552
|
+
? `/harness-plan "${input.activeCtx.task_summary}"`
|
|
553
|
+
: '/harness-plan "<task>"';
|
|
554
|
+
input.activeCtx.updated_at = abortedAt;
|
|
555
|
+
appendAbortPolicyState(input.pi, input.reason, abortedAt);
|
|
556
|
+
persistContext(input.pi, input.activeCtx);
|
|
557
|
+
return input.activeCtx;
|
|
558
|
+
}
|
|
559
|
+
|
|
501
560
|
async function maybeHandleClarificationFollowUp(input: {
|
|
502
561
|
pi: ExtensionAPI;
|
|
503
562
|
activeCtx: HarnessRunContext;
|
|
@@ -558,7 +617,7 @@ function contextPrompt(systemPrompt: string, activeCtx: HarnessRunContext) {
|
|
|
558
617
|
};
|
|
559
618
|
}
|
|
560
619
|
|
|
561
|
-
function createNewRunContextForCommand(input: {
|
|
620
|
+
async function createNewRunContextForCommand(input: {
|
|
562
621
|
pi: ExtensionAPI;
|
|
563
622
|
activeCtx: HarnessRunContext | null;
|
|
564
623
|
sessionId: string;
|
|
@@ -567,6 +626,18 @@ function createNewRunContextForCommand(input: {
|
|
|
567
626
|
userPrompt: string;
|
|
568
627
|
systemPrompt: string;
|
|
569
628
|
}) {
|
|
629
|
+
const ownershipConflict = await findActiveRunOwnershipConflict(
|
|
630
|
+
input.projectRoot,
|
|
631
|
+
input.sessionId,
|
|
632
|
+
);
|
|
633
|
+
if (ownershipConflict) {
|
|
634
|
+
return {
|
|
635
|
+
activeCtx: input.activeCtx,
|
|
636
|
+
response: blockRunContextMessage(
|
|
637
|
+
`Another Pi session (${ownershipConflict.ownerPiSessionId}) owns active run ${ownershipConflict.runId}. Finish or abort that run before /harness-new-run.`,
|
|
638
|
+
),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
570
641
|
if (input.activeCtx?.status === "active") {
|
|
571
642
|
input.activeCtx.status = "aborted";
|
|
572
643
|
input.activeCtx.plan_ready = false;
|
|
@@ -649,7 +720,7 @@ type ActiveContextAccess = {
|
|
|
649
720
|
set(ctx: HarnessRunContext | null): void;
|
|
650
721
|
};
|
|
651
722
|
|
|
652
|
-
const HARNESS_CLEAR_CONFIRM_OPTION = "Delete
|
|
723
|
+
const HARNESS_CLEAR_CONFIRM_OPTION = "Delete all harness runs";
|
|
653
724
|
|
|
654
725
|
function isHarnessClearConfirmed(response: unknown): boolean {
|
|
655
726
|
if (!response || typeof response !== "object") return false;
|
|
@@ -672,23 +743,23 @@ function registerHarnessClearCommand(
|
|
|
672
743
|
): void {
|
|
673
744
|
pi.registerCommand("harness-clear", {
|
|
674
745
|
description:
|
|
675
|
-
"Delete
|
|
746
|
+
"Delete all harness runs under .pi/harness/runs, including the active run",
|
|
676
747
|
handler: async (_args, ctx) => {
|
|
677
748
|
const entries = getEntries(ctx);
|
|
678
749
|
const projectRoot = process.cwd();
|
|
679
750
|
const latest = active.get() ?? getLatestRunContext(entries);
|
|
680
751
|
const pointer = await loadProjectActiveRun(projectRoot);
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
752
|
+
const activeRunIds = [
|
|
753
|
+
...new Set(
|
|
754
|
+
[latest?.run_id, pointer?.run_id].filter(Boolean) as string[],
|
|
755
|
+
),
|
|
756
|
+
].sort();
|
|
757
|
+
const manifest = await buildHarnessClearManifest(projectRoot);
|
|
758
|
+
const hasTargets =
|
|
759
|
+
manifest.candidates.length > 0 || activeRunIds.length > 0;
|
|
760
|
+
if (!hasTargets) {
|
|
689
761
|
const message = [
|
|
690
|
-
"/harness-clear: no
|
|
691
|
-
` protected: ${manifest.protected_run_ids.join(", ") || "(none)"}`,
|
|
762
|
+
"/harness-clear: no harness runs found.",
|
|
692
763
|
` skipped: ${manifest.skipped.length}`,
|
|
693
764
|
].join("\n");
|
|
694
765
|
if (ctx.hasUI) ctx.ui.notify(message, "info");
|
|
@@ -700,8 +771,10 @@ function registerHarnessClearCommand(
|
|
|
700
771
|
});
|
|
701
772
|
pi.appendEntry("harness-clear-result", {
|
|
702
773
|
approved: false,
|
|
774
|
+
cleared_all: false,
|
|
703
775
|
deleted: 0,
|
|
704
|
-
|
|
776
|
+
active_cleared: false,
|
|
777
|
+
active_run_ids: activeRunIds,
|
|
705
778
|
skipped: manifest.skipped,
|
|
706
779
|
recorded_at: nowIso(),
|
|
707
780
|
});
|
|
@@ -709,11 +782,12 @@ function registerHarnessClearCommand(
|
|
|
709
782
|
}
|
|
710
783
|
const ask = await runAskUser(
|
|
711
784
|
{
|
|
712
|
-
question: `Delete ${manifest.candidates.length}
|
|
785
|
+
question: `Delete all ${manifest.candidates.length} harness run directories, including the current run?`,
|
|
713
786
|
context: [
|
|
714
|
-
"Scope: .pi/harness/runs/<run_id>
|
|
715
|
-
|
|
716
|
-
`
|
|
787
|
+
"Scope: .pi/harness/runs/<run_id> directories plus .pi/harness/active-run.json.",
|
|
788
|
+
"The in-session active run context will also be cleared.",
|
|
789
|
+
`Active run ids: ${activeRunIds.join(", ") || "(none)"}`,
|
|
790
|
+
`Candidates: ${manifest.candidates.map((item) => item.run_id).join(", ") || "(none)"}`,
|
|
717
791
|
].join("\n"),
|
|
718
792
|
options: [HARNESS_CLEAR_CONFIRM_OPTION, "Cancel"],
|
|
719
793
|
allowSkip: true,
|
|
@@ -734,8 +808,10 @@ function registerHarnessClearCommand(
|
|
|
734
808
|
});
|
|
735
809
|
pi.appendEntry("harness-clear-result", {
|
|
736
810
|
approved: false,
|
|
811
|
+
cleared_all: false,
|
|
737
812
|
deleted: 0,
|
|
738
|
-
|
|
813
|
+
active_cleared: false,
|
|
814
|
+
active_run_ids: activeRunIds,
|
|
739
815
|
skipped: manifest.skipped,
|
|
740
816
|
ask_error: ask.error,
|
|
741
817
|
recorded_at: nowIso(),
|
|
@@ -758,8 +834,10 @@ function registerHarnessClearCommand(
|
|
|
758
834
|
});
|
|
759
835
|
pi.appendEntry("harness-clear-result", {
|
|
760
836
|
approved: false,
|
|
837
|
+
cleared_all: false,
|
|
761
838
|
deleted: 0,
|
|
762
|
-
|
|
839
|
+
active_cleared: false,
|
|
840
|
+
active_run_ids: activeRunIds,
|
|
763
841
|
skipped: manifest.skipped,
|
|
764
842
|
recorded_at: nowIso(),
|
|
765
843
|
});
|
|
@@ -778,10 +856,13 @@ function registerHarnessClearCommand(
|
|
|
778
856
|
});
|
|
779
857
|
}
|
|
780
858
|
}
|
|
859
|
+
const activePointerDeleted = await deleteProjectActiveRun(projectRoot);
|
|
860
|
+
active.set(null);
|
|
781
861
|
const message = [
|
|
782
862
|
"/harness-clear complete.",
|
|
783
863
|
` deleted: ${deleted}`,
|
|
784
|
-
`
|
|
864
|
+
` active_cleared: true`,
|
|
865
|
+
` active_pointer_deleted: ${activePointerDeleted}`,
|
|
785
866
|
` skipped: ${manifest.skipped.length + failed.length}`,
|
|
786
867
|
].join("\n");
|
|
787
868
|
if (ctx.hasUI) ctx.ui.notify(message, "info");
|
|
@@ -793,11 +874,18 @@ function registerHarnessClearCommand(
|
|
|
793
874
|
});
|
|
794
875
|
pi.appendEntry("harness-clear-result", {
|
|
795
876
|
approved: true,
|
|
877
|
+
cleared_all: failed.length === 0,
|
|
796
878
|
deleted,
|
|
797
|
-
|
|
879
|
+
active_cleared: true,
|
|
880
|
+
active_pointer_deleted: activePointerDeleted,
|
|
881
|
+
active_run_ids: activeRunIds,
|
|
798
882
|
skipped: [...manifest.skipped, ...failed],
|
|
799
883
|
recorded_at: nowIso(),
|
|
800
884
|
});
|
|
885
|
+
pi.events.emit("harness-runs-cleared", {
|
|
886
|
+
deleted,
|
|
887
|
+
projectRoot,
|
|
888
|
+
});
|
|
801
889
|
},
|
|
802
890
|
});
|
|
803
891
|
}
|
|
@@ -1199,13 +1287,7 @@ function handleAgentEndAbort(input: {
|
|
|
1199
1287
|
: '/harness-plan "<task>"';
|
|
1200
1288
|
persistContext(input.pi, input.activeCtx);
|
|
1201
1289
|
const msg = `Harness aborted. Next: ${input.activeCtx.next_recommended_command}`;
|
|
1202
|
-
|
|
1203
|
-
else
|
|
1204
|
-
input.pi.sendMessage({
|
|
1205
|
-
customType: "harness-step-handoff",
|
|
1206
|
-
content: msg,
|
|
1207
|
-
display: true,
|
|
1208
|
-
});
|
|
1290
|
+
notifyHarnessHandoff(input.ctx, msg, "warning");
|
|
1209
1291
|
}
|
|
1210
1292
|
|
|
1211
1293
|
async function updatePlanReadinessAfterAgent(input: {
|
|
@@ -1242,13 +1324,7 @@ async function updatePlanReadinessAfterAgent(input: {
|
|
|
1242
1324
|
) {
|
|
1243
1325
|
const msg =
|
|
1244
1326
|
"A draft plan-packet.yaml is on disk, but user approval was not recorded. Complete Review Gate (debate rounds + harness_debate_consensus), then call approve_plan; use create_plan only after Approve.";
|
|
1245
|
-
|
|
1246
|
-
else
|
|
1247
|
-
input.pi.sendMessage({
|
|
1248
|
-
customType: "harness-plan-packet",
|
|
1249
|
-
content: msg,
|
|
1250
|
-
display: true,
|
|
1251
|
-
});
|
|
1327
|
+
notifyHarnessHandoff(input.ctx, msg, "warning");
|
|
1252
1328
|
}
|
|
1253
1329
|
persistContext(input.pi, input.activeCtx);
|
|
1254
1330
|
}
|
|
@@ -1326,6 +1402,16 @@ function registerPlanApprovalCapture(
|
|
|
1326
1402
|
});
|
|
1327
1403
|
}
|
|
1328
1404
|
|
|
1405
|
+
function registerHeadlessPlanProgressWatcher(
|
|
1406
|
+
pi: ExtensionAPI,
|
|
1407
|
+
active: ActiveContextAccess,
|
|
1408
|
+
): void {
|
|
1409
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
1410
|
+
if (event.isError) return;
|
|
1411
|
+
await handlePlanToolResultForHeadlessProgress({ pi, ctx, active });
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1329
1415
|
function registerExecutorHandoffReconcile(
|
|
1330
1416
|
pi: ExtensionAPI,
|
|
1331
1417
|
active: ActiveContextAccess,
|
|
@@ -1489,10 +1575,46 @@ async function resolveCommandRunContext(input: {
|
|
|
1489
1575
|
persistContext(input.pi, activeCtx);
|
|
1490
1576
|
activeCtx = null;
|
|
1491
1577
|
}
|
|
1578
|
+
if (
|
|
1579
|
+
activeCtx &&
|
|
1580
|
+
(input.command === "harness-plan" || input.command === "harness-auto") &&
|
|
1581
|
+
activeCtx.owner_pi_session_id !== input.sessionId
|
|
1582
|
+
) {
|
|
1583
|
+
const foreignRunConflict = await findActiveRunOwnershipConflict(
|
|
1584
|
+
input.projectRoot,
|
|
1585
|
+
input.sessionId,
|
|
1586
|
+
);
|
|
1587
|
+
if (foreignRunConflict) {
|
|
1588
|
+
return {
|
|
1589
|
+
activeCtx,
|
|
1590
|
+
resolved,
|
|
1591
|
+
response: blockRunContextMessage(
|
|
1592
|
+
`Another Pi session (${foreignRunConflict.ownerPiSessionId}) owns active run ${foreignRunConflict.runId}. Finish or abort that run before starting a new plan.`,
|
|
1593
|
+
),
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
activeCtx = null;
|
|
1597
|
+
}
|
|
1492
1598
|
const reuseRun =
|
|
1493
1599
|
activeCtx &&
|
|
1494
1600
|
shouldReuseHarnessRunId(input.userPrompt, activeCtx, input.command);
|
|
1495
1601
|
if (!activeCtx || !reuseRun) {
|
|
1602
|
+
if (process.env.HARNESS_QA_SMOKE === "1") {
|
|
1603
|
+
await releaseForeignQaRunOwnership(input.projectRoot, input.sessionId);
|
|
1604
|
+
}
|
|
1605
|
+
const ownershipConflict = await findActiveRunOwnershipConflict(
|
|
1606
|
+
input.projectRoot,
|
|
1607
|
+
input.sessionId,
|
|
1608
|
+
);
|
|
1609
|
+
if (ownershipConflict) {
|
|
1610
|
+
return {
|
|
1611
|
+
activeCtx,
|
|
1612
|
+
resolved,
|
|
1613
|
+
response: blockRunContextMessage(
|
|
1614
|
+
`Another Pi session (${ownershipConflict.ownerPiSessionId}) owns active run ${ownershipConflict.runId}. Finish or abort that run before starting a new plan.`,
|
|
1615
|
+
),
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1496
1618
|
if (activeCtx?.status === "active") {
|
|
1497
1619
|
activeCtx.status = "aborted";
|
|
1498
1620
|
activeCtx.plan_ready = false;
|
|
@@ -1508,6 +1630,11 @@ async function resolveCommandRunContext(input: {
|
|
|
1508
1630
|
} else if (input.command === "harness-auto") {
|
|
1509
1631
|
activeCtx = resetRunContextForHarnessAuto(activeCtx);
|
|
1510
1632
|
if (task) activeCtx.task_summary = task;
|
|
1633
|
+
} else if (
|
|
1634
|
+
input.command === "harness-plan" &&
|
|
1635
|
+
activeCtx.status === "aborted"
|
|
1636
|
+
) {
|
|
1637
|
+
activeCtx = resetRunContextForHarnessAuto(activeCtx);
|
|
1511
1638
|
}
|
|
1512
1639
|
if (input.command === "harness-plan") {
|
|
1513
1640
|
if (task) activeCtx.task_summary = task;
|
|
@@ -1600,8 +1727,44 @@ async function handlePreResolvedHarnessCommand(args: {
|
|
|
1600
1727
|
};
|
|
1601
1728
|
}
|
|
1602
1729
|
}
|
|
1730
|
+
if (command === "harness-abort") {
|
|
1731
|
+
if (!activeCtx) {
|
|
1732
|
+
if (process.env.HARNESS_QA_SMOKE === "1") {
|
|
1733
|
+
const released = await releaseForeignQaRunOwnership(
|
|
1734
|
+
projectRoot,
|
|
1735
|
+
sessionId,
|
|
1736
|
+
);
|
|
1737
|
+
if (released) {
|
|
1738
|
+
return {
|
|
1739
|
+
activeCtx: null,
|
|
1740
|
+
response: blockRunContextMessage(
|
|
1741
|
+
'Stale QA harness run released from disk. Next: /harness-plan "<task>"',
|
|
1742
|
+
),
|
|
1743
|
+
handled: true,
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return {
|
|
1748
|
+
activeCtx,
|
|
1749
|
+
response: blockRunContextMessage(
|
|
1750
|
+
'No active harness run to abort. Next: /harness-plan "<task>"',
|
|
1751
|
+
),
|
|
1752
|
+
handled: true,
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
const reason = parsedArgs.trim() || "manual abort";
|
|
1756
|
+
const aborted = abortActiveRunContext({ pi, activeCtx, reason });
|
|
1757
|
+
return {
|
|
1758
|
+
activeCtx: aborted,
|
|
1759
|
+
response: blockRunContextMessage(
|
|
1760
|
+
`Harness aborted. Mutating tools are blocked until a new approved plan is attached. Next: ${aborted.next_recommended_command}`,
|
|
1761
|
+
),
|
|
1762
|
+
handled: true,
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1603
1766
|
if (command === "harness-new-run") {
|
|
1604
|
-
const next = createNewRunContextForCommand({
|
|
1767
|
+
const next = await createNewRunContextForCommand({
|
|
1605
1768
|
pi,
|
|
1606
1769
|
activeCtx,
|
|
1607
1770
|
sessionId,
|
|
@@ -1708,6 +1871,8 @@ async function handleBeforeAgentStart(input: {
|
|
|
1708
1871
|
}
|
|
1709
1872
|
if (!parsed) return undefined;
|
|
1710
1873
|
const { command, args } = parsed;
|
|
1874
|
+
const planQuick = parseArgFlag(args, "--quick") != null;
|
|
1875
|
+
const planRisk = parseArgFlag(args, "--risk") ?? "med";
|
|
1711
1876
|
const preResolved = await handlePreResolvedHarnessCommand({
|
|
1712
1877
|
pi: input.pi,
|
|
1713
1878
|
activeCtx,
|
|
@@ -1742,6 +1907,19 @@ async function handleBeforeAgentStart(input: {
|
|
|
1742
1907
|
return blockRunContextMessage(
|
|
1743
1908
|
'No active harness run. Run /harness-plan "<task>" first, or /harness-use-run <run-id> for recovery.',
|
|
1744
1909
|
);
|
|
1910
|
+
if (
|
|
1911
|
+
isHarnessNonInteractive() &&
|
|
1912
|
+
(await shouldEndHeadlessHarnessPrintSession({
|
|
1913
|
+
command,
|
|
1914
|
+
runCtx: activeCtx,
|
|
1915
|
+
projectRoot,
|
|
1916
|
+
}))
|
|
1917
|
+
) {
|
|
1918
|
+
endHeadlessHarnessPrintSession(input.ctx);
|
|
1919
|
+
return {
|
|
1920
|
+
systemPrompt: `${input.event.systemPrompt}\n\n[Harness] Headless session complete; ending.`,
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1745
1923
|
activeCtx.phase = policyPhase;
|
|
1746
1924
|
activeCtx.updated_at = new Date().toISOString();
|
|
1747
1925
|
activeCtx.pi_session_id = sessionId;
|
|
@@ -1817,6 +1995,26 @@ async function handleBeforeAgentStart(input: {
|
|
|
1817
1995
|
activeCtx,
|
|
1818
1996
|
);
|
|
1819
1997
|
Object.assign(activeCtx, syncedCtx);
|
|
1998
|
+
if (command === "harness-plan" || command === "harness-auto") {
|
|
1999
|
+
const runDir = join(
|
|
2000
|
+
projectRoot,
|
|
2001
|
+
".pi",
|
|
2002
|
+
"harness",
|
|
2003
|
+
"runs",
|
|
2004
|
+
activeCtx.run_id,
|
|
2005
|
+
);
|
|
2006
|
+
await seedHeadlessTaskClarificationIfNeeded({
|
|
2007
|
+
runDir,
|
|
2008
|
+
taskSummary: activeCtx.task_summary ?? "",
|
|
2009
|
+
riskLevel: planRisk,
|
|
2010
|
+
quick: planQuick,
|
|
2011
|
+
});
|
|
2012
|
+
const resynced = await syncPlanLastOutcomeFromTaskClarification(
|
|
2013
|
+
projectRoot,
|
|
2014
|
+
activeCtx,
|
|
2015
|
+
);
|
|
2016
|
+
Object.assign(activeCtx, resynced);
|
|
2017
|
+
}
|
|
1820
2018
|
input.active.set(activeCtx);
|
|
1821
2019
|
persistContext(input.pi, activeCtx);
|
|
1822
2020
|
if (command === "harness-plan" || command === "harness-auto") {
|
|
@@ -1824,13 +2022,12 @@ async function handleBeforeAgentStart(input: {
|
|
|
1824
2022
|
}
|
|
1825
2023
|
let gateBlock = "";
|
|
1826
2024
|
if (command === "harness-plan" || command === "harness-auto") {
|
|
1827
|
-
const quick = parseArgFlag(args, "--quick") != null;
|
|
1828
2025
|
const gateStatus = await resolvePlanHumanGateStatus(
|
|
1829
2026
|
projectRoot,
|
|
1830
2027
|
activeCtx.run_id,
|
|
1831
2028
|
entries,
|
|
1832
2029
|
{
|
|
1833
|
-
quick,
|
|
2030
|
+
quick: planQuick,
|
|
1834
2031
|
taskSummary: activeCtx.task_summary ?? undefined,
|
|
1835
2032
|
lastOutcome: activeCtx.last_outcome ?? undefined,
|
|
1836
2033
|
},
|
|
@@ -1843,6 +2040,138 @@ async function handleBeforeAgentStart(input: {
|
|
|
1843
2040
|
};
|
|
1844
2041
|
}
|
|
1845
2042
|
|
|
2043
|
+
async function applyHeadlessPlanFinalizeAndQaSmoke(input: {
|
|
2044
|
+
pi: ExtensionAPI;
|
|
2045
|
+
ctx: any;
|
|
2046
|
+
active: ActiveContextAccess;
|
|
2047
|
+
command: string;
|
|
2048
|
+
args: string;
|
|
2049
|
+
activeCtx: HarnessRunContext;
|
|
2050
|
+
entries: unknown[];
|
|
2051
|
+
}): Promise<void> {
|
|
2052
|
+
const projectRoot = process.cwd();
|
|
2053
|
+
const planQuick = parseArgFlag(input.args, "--quick") != null;
|
|
2054
|
+
const planRisk = parseArgFlag(input.args, "--risk") ?? "med";
|
|
2055
|
+
const outcome = await tryHeadlessAutoPlanFinalize({
|
|
2056
|
+
projectRoot,
|
|
2057
|
+
runCtx: input.activeCtx,
|
|
2058
|
+
taskSummary: input.activeCtx.task_summary ?? "",
|
|
2059
|
+
entries: input.entries,
|
|
2060
|
+
riskLevel: planRisk,
|
|
2061
|
+
quick: planQuick,
|
|
2062
|
+
deps: {
|
|
2063
|
+
appendEntry: (type, data) => input.pi.appendEntry(type, data),
|
|
2064
|
+
getEntries: () => getEntries(input.ctx),
|
|
2065
|
+
getSubagentEntries: () => getEntries(input.ctx),
|
|
2066
|
+
onPlanCommitted: (updated, packet, planPath) => {
|
|
2067
|
+
input.pi.appendEntry("harness-run-context", updated);
|
|
2068
|
+
input.pi.appendEntry(
|
|
2069
|
+
"harness-plan-packet",
|
|
2070
|
+
planPacketSummary(packet, planPath, "ready"),
|
|
2071
|
+
);
|
|
2072
|
+
},
|
|
2073
|
+
},
|
|
2074
|
+
});
|
|
2075
|
+
if (
|
|
2076
|
+
outcome.progress.seeded_clarification ||
|
|
2077
|
+
outcome.progress.seeded_planning_context ||
|
|
2078
|
+
outcome.progress.patched_review_gate ||
|
|
2079
|
+
outcome.progress.wrote_consensus_bypass
|
|
2080
|
+
) {
|
|
2081
|
+
input.pi.appendEntry("harness-headless-plan-progress", {
|
|
2082
|
+
run_id: input.activeCtx.run_id,
|
|
2083
|
+
...outcome.progress,
|
|
2084
|
+
recorded_at: nowIso(),
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
if (outcome.finalized) {
|
|
2088
|
+
const synced = await syncPlanReadyFromDisk(
|
|
2089
|
+
projectRoot,
|
|
2090
|
+
input.activeCtx,
|
|
2091
|
+
input.entries,
|
|
2092
|
+
);
|
|
2093
|
+
Object.assign(input.activeCtx, synced);
|
|
2094
|
+
persistContext(input.pi, input.activeCtx);
|
|
2095
|
+
input.active.set(input.activeCtx);
|
|
2096
|
+
input.pi.appendEntry("harness-headless-plan-finalized", {
|
|
2097
|
+
run_id: input.activeCtx.run_id,
|
|
2098
|
+
source: "headless_auto",
|
|
2099
|
+
recorded_at: nowIso(),
|
|
2100
|
+
});
|
|
2101
|
+
input.activeCtx.next_recommended_command = "/harness-run";
|
|
2102
|
+
persistContext(input.pi, input.activeCtx);
|
|
2103
|
+
if (input.command === "harness-auto") {
|
|
2104
|
+
await maybeHeadlessQaAutoExecuteSmoke({
|
|
2105
|
+
projectRoot,
|
|
2106
|
+
runCtx: input.activeCtx,
|
|
2107
|
+
command: input.command,
|
|
2108
|
+
});
|
|
2109
|
+
persistContext(input.pi, input.activeCtx);
|
|
2110
|
+
}
|
|
2111
|
+
if (
|
|
2112
|
+
await shouldEndHeadlessHarnessPrintSession({
|
|
2113
|
+
command: input.command,
|
|
2114
|
+
runCtx: input.activeCtx,
|
|
2115
|
+
projectRoot,
|
|
2116
|
+
})
|
|
2117
|
+
) {
|
|
2118
|
+
endHeadlessHarnessPrintSession(input.ctx);
|
|
2119
|
+
}
|
|
2120
|
+
} else if (outcome.reason && outcome.progress.force_reason) {
|
|
2121
|
+
input.pi.appendEntry("harness-headless-plan-progress", {
|
|
2122
|
+
run_id: input.activeCtx.run_id,
|
|
2123
|
+
finalize_blocked: outcome.reason,
|
|
2124
|
+
recorded_at: nowIso(),
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
async function handleHeadlessPlanProgressCheck(input: {
|
|
2130
|
+
pi: ExtensionAPI;
|
|
2131
|
+
ctx: any;
|
|
2132
|
+
active: ActiveContextAccess;
|
|
2133
|
+
}): Promise<void> {
|
|
2134
|
+
const entries = getEntries(input.ctx);
|
|
2135
|
+
const turn = getLatestHarnessTurn(entries);
|
|
2136
|
+
if (
|
|
2137
|
+
!turn ||
|
|
2138
|
+
(turn.command !== "harness-plan" && turn.command !== "harness-auto")
|
|
2139
|
+
) {
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
const activeCtx = input.active.get() ?? getLatestRunContext(entries);
|
|
2143
|
+
if (!activeCtx?.run_id || activeCtx.plan_ready) return;
|
|
2144
|
+
await applyHeadlessPlanFinalizeAndQaSmoke({
|
|
2145
|
+
pi: input.pi,
|
|
2146
|
+
ctx: input.ctx,
|
|
2147
|
+
active: input.active,
|
|
2148
|
+
command: turn.command,
|
|
2149
|
+
args: turn.args,
|
|
2150
|
+
activeCtx,
|
|
2151
|
+
entries,
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
async function handleTurnStart(input: {
|
|
2156
|
+
pi: ExtensionAPI;
|
|
2157
|
+
ctx: any;
|
|
2158
|
+
active: ActiveContextAccess;
|
|
2159
|
+
}): Promise<void> {
|
|
2160
|
+
await handleHeadlessPlanProgressCheck(input);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
async function handlePlanToolResultForHeadlessProgress(input: {
|
|
2164
|
+
pi: ExtensionAPI;
|
|
2165
|
+
ctx: any;
|
|
2166
|
+
active: ActiveContextAccess;
|
|
2167
|
+
}): Promise<void> {
|
|
2168
|
+
const entries = getEntries(input.ctx);
|
|
2169
|
+
const since = Math.max(0, indexOfLastPlanCommand(entries));
|
|
2170
|
+
const sinceEntries = entries.length - since;
|
|
2171
|
+
if (sinceEntries > 0 && sinceEntries % 12 !== 0) return;
|
|
2172
|
+
await handleHeadlessPlanProgressCheck(input);
|
|
2173
|
+
}
|
|
2174
|
+
|
|
1846
2175
|
async function handleAgentEnd(input: {
|
|
1847
2176
|
pi: ExtensionAPI;
|
|
1848
2177
|
ctx: any;
|
|
@@ -1877,6 +2206,29 @@ async function handleAgentEnd(input: {
|
|
|
1877
2206
|
parsed?.command === "harness-plan" ||
|
|
1878
2207
|
parsed?.command === "harness-auto"
|
|
1879
2208
|
) {
|
|
2209
|
+
const planArgs = parsed.args ?? "";
|
|
2210
|
+
const quick = parseArgFlag(planArgs, "--quick") != null;
|
|
2211
|
+
const risk = parseArgFlag(planArgs, "--risk") ?? "med";
|
|
2212
|
+
const forced = await maybeForceHeadlessPlanProgress({
|
|
2213
|
+
projectRoot,
|
|
2214
|
+
runId: activeCtx.run_id,
|
|
2215
|
+
taskSummary: activeCtx.task_summary ?? "",
|
|
2216
|
+
entries,
|
|
2217
|
+
riskLevel: risk,
|
|
2218
|
+
quick,
|
|
2219
|
+
});
|
|
2220
|
+
if (
|
|
2221
|
+
forced.seeded_clarification ||
|
|
2222
|
+
forced.seeded_planning_context ||
|
|
2223
|
+
forced.patched_review_gate ||
|
|
2224
|
+
forced.wrote_consensus_bypass
|
|
2225
|
+
) {
|
|
2226
|
+
input.pi.appendEntry("harness-headless-plan-progress", {
|
|
2227
|
+
run_id: activeCtx.run_id,
|
|
2228
|
+
...forced,
|
|
2229
|
+
recorded_at: nowIso(),
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
1880
2232
|
const synced = await syncPlanLastOutcomeFromTaskClarification(
|
|
1881
2233
|
projectRoot,
|
|
1882
2234
|
activeCtx,
|
|
@@ -1884,6 +2236,41 @@ async function handleAgentEnd(input: {
|
|
|
1884
2236
|
Object.assign(activeCtx, synced);
|
|
1885
2237
|
persistContext(input.pi, activeCtx);
|
|
1886
2238
|
}
|
|
2239
|
+
if (
|
|
2240
|
+
parsed?.command === "harness-plan" ||
|
|
2241
|
+
parsed?.command === "harness-auto"
|
|
2242
|
+
) {
|
|
2243
|
+
if (!activeCtx.plan_ready) {
|
|
2244
|
+
await applyHeadlessPlanFinalizeAndQaSmoke({
|
|
2245
|
+
pi: input.pi,
|
|
2246
|
+
ctx: input.ctx,
|
|
2247
|
+
active: input.active,
|
|
2248
|
+
command: parsed.command,
|
|
2249
|
+
args: parsed.args ?? "",
|
|
2250
|
+
activeCtx,
|
|
2251
|
+
entries,
|
|
2252
|
+
});
|
|
2253
|
+
} else if (
|
|
2254
|
+
parsed.command === "harness-auto" &&
|
|
2255
|
+
process.env.HARNESS_QA_SMOKE === "1"
|
|
2256
|
+
) {
|
|
2257
|
+
await maybeHeadlessQaAutoExecuteSmoke({
|
|
2258
|
+
projectRoot,
|
|
2259
|
+
runCtx: activeCtx,
|
|
2260
|
+
command: parsed.command,
|
|
2261
|
+
});
|
|
2262
|
+
persistContext(input.pi, activeCtx);
|
|
2263
|
+
if (
|
|
2264
|
+
await shouldEndHeadlessHarnessPrintSession({
|
|
2265
|
+
command: parsed.command,
|
|
2266
|
+
runCtx: activeCtx,
|
|
2267
|
+
projectRoot,
|
|
2268
|
+
})
|
|
2269
|
+
) {
|
|
2270
|
+
endHeadlessHarnessPrintSession(input.ctx);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
1887
2274
|
const statuses = await resolveCompletionStatuses(
|
|
1888
2275
|
entries,
|
|
1889
2276
|
activeCtx.run_id,
|
|
@@ -1970,14 +2357,17 @@ async function handleAgentEnd(input: {
|
|
|
1970
2357
|
phase: activeCtx.phase,
|
|
1971
2358
|
});
|
|
1972
2359
|
if (next && parsed) {
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2360
|
+
notifyHarnessHandoff(input.ctx, `Next: ${next}`);
|
|
2361
|
+
}
|
|
2362
|
+
if (
|
|
2363
|
+
parsed &&
|
|
2364
|
+
(await shouldEndHeadlessHarnessPrintSession({
|
|
2365
|
+
command: parsed.command,
|
|
2366
|
+
runCtx: activeCtx,
|
|
2367
|
+
projectRoot,
|
|
2368
|
+
}))
|
|
2369
|
+
) {
|
|
2370
|
+
endHeadlessHarnessPrintSession(input.ctx);
|
|
1981
2371
|
}
|
|
1982
2372
|
}
|
|
1983
2373
|
|
|
@@ -2643,6 +3033,20 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
2643
3033
|
},
|
|
2644
3034
|
};
|
|
2645
3035
|
|
|
3036
|
+
pi.events.on("harness-run-aborted", (payload: unknown) => {
|
|
3037
|
+
const reason =
|
|
3038
|
+
typeof (payload as { reason?: unknown })?.reason === "string"
|
|
3039
|
+
? (payload as { reason: string }).reason || "manual abort"
|
|
3040
|
+
: "manual abort";
|
|
3041
|
+
if (activeCtx) {
|
|
3042
|
+
abortActiveRunContext({ pi, activeCtx, reason });
|
|
3043
|
+
}
|
|
3044
|
+
});
|
|
3045
|
+
|
|
3046
|
+
pi.events.on("harness-runs-cleared", () => {
|
|
3047
|
+
activeCtx = null;
|
|
3048
|
+
});
|
|
3049
|
+
|
|
2646
3050
|
pi.on("session_start", async (_event, ctx) => {
|
|
2647
3051
|
const entries = getEntries(ctx);
|
|
2648
3052
|
activeCtx = hydrateFromSession(entries);
|
|
@@ -2673,11 +3077,16 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
2673
3077
|
handleBeforeAgentStart({ pi, event, ctx, active: activeAccess }),
|
|
2674
3078
|
);
|
|
2675
3079
|
|
|
3080
|
+
pi.on("turn_start", async (_event, ctx) => {
|
|
3081
|
+
await handleTurnStart({ pi, ctx, active: activeAccess });
|
|
3082
|
+
});
|
|
3083
|
+
|
|
2676
3084
|
pi.on("agent_end", async (_event, ctx) => {
|
|
2677
3085
|
await handleAgentEnd({ pi, ctx, active: activeAccess });
|
|
2678
3086
|
});
|
|
2679
3087
|
|
|
2680
3088
|
registerPlanApprovalCapture(pi, activeAccess);
|
|
3089
|
+
registerHeadlessPlanProgressWatcher(pi, activeAccess);
|
|
2681
3090
|
registerExecutorHandoffReconcile(pi, activeAccess);
|
|
2682
3091
|
registerHarnessToolCallGuards(pi, activeAccess);
|
|
2683
3092
|
registerHarnessRunStatusCommand(pi, activeAccess);
|