ultimate-pi 0.22.1 → 0.23.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-kill-switch.ts +7 -1
- package/.pi/extensions/harness-plan-approval.ts +9 -1
- package/.pi/extensions/harness-run-context.ts +587 -86
- package/.pi/extensions/policy-gate.ts +15 -2
- package/.pi/harness/agents.manifest.json +3 -3
- package/.pi/harness/agents.policy.yaml +82 -3
- package/.pi/harness/specs/plan-task-clarification.schema.json +10 -1
- package/.pi/lib/agents-policy.mjs +42 -1
- package/.pi/lib/agt/build-evaluation-context.ts +3 -1
- package/.pi/lib/agt/kill-switch-state.ts +14 -0
- package/.pi/lib/agt/legacy-evaluate.ts +3 -1
- package/.pi/lib/ask-user/index.ts +2 -0
- package/.pi/lib/ask-user/merge-task-clarification.ts +5 -0
- package/.pi/lib/ask-user/policy.ts +23 -0
- package/.pi/lib/ask-user/presenters/glimpse.ts +8 -1
- package/.pi/lib/ask-user/presenters/headless.ts +15 -0
- package/.pi/lib/ask-user/presenters/select.ts +11 -2
- package/.pi/lib/ask-user/validate-core.mjs +16 -0
- package/.pi/lib/harness-artifact-gate.ts +75 -5
- package/.pi/lib/harness-repair-brief.ts +30 -4
- package/.pi/lib/harness-run-context.ts +842 -17
- package/.pi/lib/harness-schema-validate.ts +147 -38
- package/.pi/lib/harness-spawn-policy.ts +9 -0
- package/.pi/lib/harness-spawn-topology.ts +109 -7
- package/.pi/lib/harness-subagent-precheck.ts +21 -0
- package/.pi/lib/harness-subagent-submit-pipeline.ts +95 -21
- package/.pi/lib/harness-subagent-submit-register.ts +6 -1
- package/.pi/lib/harness-subagents-bridge.ts +3 -0
- package/.pi/lib/harness-yaml.ts +11 -3
- package/.pi/lib/plan-approval/create-plan.ts +2 -6
- package/.pi/lib/plan-debate-gate.ts +87 -0
- package/.pi/lib/plan-debate-lane.ts +8 -2
- package/.pi/lib/plan-human-gates.ts +404 -0
- package/.pi/prompts/harness-clear.md +25 -0
- package/.pi/prompts/harness-plan.md +6 -0
- package/.pi/prompts/harness-review.md +2 -0
- package/.pi/prompts/harness-run.md +4 -3
- package/.pi/scripts/generate-agents-policy-yaml.mjs +73 -7
- package/.pi/scripts/harness-reconcile-run-context.mjs +62 -0
- package/.pi/scripts/harness-schema-compile-verify.mjs +29 -0
- package/.pi/scripts/harness-verify.mjs +27 -0
- package/CHANGELOG.md +13 -0
- package/README.md +4 -0
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
readdir,
|
|
11
11
|
readFile,
|
|
12
12
|
rename,
|
|
13
|
+
rm,
|
|
13
14
|
stat,
|
|
14
15
|
writeFile,
|
|
15
16
|
} from "node:fs/promises";
|
|
@@ -17,14 +18,25 @@ import { basename, dirname, join } from "node:path";
|
|
|
17
18
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
19
|
import { Type } from "@sinclair/typebox";
|
|
19
20
|
import { allowsAgentTool } from "../lib/agents-policy.mjs";
|
|
21
|
+
import {
|
|
22
|
+
disarmHarnessKillSwitch,
|
|
23
|
+
resetHarnessPolicyDenyCount,
|
|
24
|
+
} from "../lib/agt/kill-switch-state.js";
|
|
25
|
+
import { runAskUser } from "../lib/ask-user/index.js";
|
|
20
26
|
import { claimHarnessGovernanceLoad } from "../lib/extension-load-guard.js";
|
|
21
27
|
import { getHarnessPackageRoot } from "../lib/harness-paths.js";
|
|
22
28
|
import {
|
|
29
|
+
blockingHarnessAutoCommandReason,
|
|
30
|
+
blockingReviewCommandReason,
|
|
31
|
+
blockingRunCommandReason,
|
|
32
|
+
blockingSteerCommandReason,
|
|
33
|
+
buildHarnessClearManifest,
|
|
23
34
|
canonicalPlanPath,
|
|
24
35
|
claimRunOwnership,
|
|
25
36
|
createFreshRunContext,
|
|
26
37
|
criticalPathWorkItemIdsFromPlanPacket,
|
|
27
38
|
driftGateActive,
|
|
39
|
+
ensureReviewOutcomeFromEval,
|
|
28
40
|
evaluateCrossSessionResume,
|
|
29
41
|
extractWritePathFromToolInput,
|
|
30
42
|
formatActivePlanBlock,
|
|
@@ -36,6 +48,7 @@ import {
|
|
|
36
48
|
getPolicyTransitionBlock,
|
|
37
49
|
type HarnessRunContext,
|
|
38
50
|
type HarnessTurnEntry,
|
|
51
|
+
harnessAutoTasksDiffer,
|
|
39
52
|
hasHarnessAbortSignal,
|
|
40
53
|
hasPlanUserApproval,
|
|
41
54
|
inferHarnessPhase,
|
|
@@ -51,6 +64,7 @@ import {
|
|
|
51
64
|
normalizeHarnessPath,
|
|
52
65
|
nowIso,
|
|
53
66
|
type PlanPacketSummary,
|
|
67
|
+
parseArgFlag,
|
|
54
68
|
parseHarnessSlashInput,
|
|
55
69
|
parseHarnessUseRunArgs,
|
|
56
70
|
parsePlanApprovalFromMessage,
|
|
@@ -58,14 +72,24 @@ import {
|
|
|
58
72
|
readExecutorHandoffFromRun,
|
|
59
73
|
readPlanPacketFromPath,
|
|
60
74
|
readReviewOutcomeFromRun,
|
|
75
|
+
reconcileReviewRouting,
|
|
76
|
+
reconcileStaleExecuteCompletion,
|
|
77
|
+
refreshRunContextProgress,
|
|
78
|
+
relPathUnderActiveRun,
|
|
79
|
+
resetRunContextForHarnessAuto,
|
|
61
80
|
resolveArgsForCommand,
|
|
62
81
|
resolveCompletionStatuses,
|
|
82
|
+
resolveHarnessRunPostAgentState,
|
|
83
|
+
resolveHarnessRunWriteTarget,
|
|
84
|
+
resolveRemediationClassForRun,
|
|
63
85
|
saveProjectActiveRun,
|
|
64
86
|
saveRunContextToDisk,
|
|
65
87
|
sessionHasResumePromptForRun,
|
|
66
88
|
shouldAutoClaimHarnessRun,
|
|
67
89
|
shouldReuseHarnessRunId,
|
|
68
90
|
steerMaxAttemptsFromEnv,
|
|
91
|
+
syncPlanLastOutcomeFromTaskClarification,
|
|
92
|
+
syncPlanReadyFromDisk,
|
|
69
93
|
userVisiblePromptSlice,
|
|
70
94
|
validatePlanOverridePath,
|
|
71
95
|
validatePlanPacket,
|
|
@@ -80,6 +104,11 @@ import {
|
|
|
80
104
|
} from "../lib/harness-yaml.js";
|
|
81
105
|
import { isReviewRoundArtifactPath } from "../lib/plan-debate-gate.js";
|
|
82
106
|
import { isReviewRoundYamlWriteAllowed } from "../lib/plan-debate-write-guard.js";
|
|
107
|
+
import {
|
|
108
|
+
formatPlanHumanGateBlock,
|
|
109
|
+
resolvePlanHumanGateStatus,
|
|
110
|
+
validateTaskClarificationHumanGate,
|
|
111
|
+
} from "../lib/plan-human-gates.js";
|
|
83
112
|
import {
|
|
84
113
|
assertTaskClarificationReadyForPlanWrite,
|
|
85
114
|
readTaskClarificationDoc,
|
|
@@ -102,8 +131,20 @@ function getEntries(ctx: {
|
|
|
102
131
|
|
|
103
132
|
function persistContext(pi: ExtensionAPI, ctx: HarnessRunContext): void {
|
|
104
133
|
pi.appendEntry("harness-run-context", ctx);
|
|
105
|
-
void saveRunContextToDisk(ctx)
|
|
106
|
-
|
|
134
|
+
void saveRunContextToDisk(ctx).catch((err) => {
|
|
135
|
+
pi.appendEntry("harness-run-context-disk-error", {
|
|
136
|
+
run_id: ctx.run_id,
|
|
137
|
+
error: err instanceof Error ? err.message : String(err),
|
|
138
|
+
recorded_at: nowIso(),
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
void saveProjectActiveRun(ctx).catch((err) => {
|
|
142
|
+
pi.appendEntry("harness-run-context-disk-error", {
|
|
143
|
+
run_id: ctx.run_id,
|
|
144
|
+
error: err instanceof Error ? err.message : String(err),
|
|
145
|
+
recorded_at: nowIso(),
|
|
146
|
+
});
|
|
147
|
+
});
|
|
107
148
|
pi.events.emit("harness-run-context:updated", { run_id: ctx.run_id });
|
|
108
149
|
}
|
|
109
150
|
|
|
@@ -215,7 +256,8 @@ export async function archivePlanRevisionArtifacts(input: {
|
|
|
215
256
|
return { archiveDir, moved };
|
|
216
257
|
}
|
|
217
258
|
|
|
218
|
-
|
|
259
|
+
/** Exported for tests — avoid archiving on every /harness-plan continue. */
|
|
260
|
+
export function shouldArchiveForPlanRevise(input: {
|
|
219
261
|
command: string;
|
|
220
262
|
mode: "create" | "revise" | null;
|
|
221
263
|
runCtx: HarnessRunContext;
|
|
@@ -226,15 +268,20 @@ function shouldArchiveForPlanRevise(input: {
|
|
|
226
268
|
return false;
|
|
227
269
|
}
|
|
228
270
|
if (input.mode !== "revise") return false;
|
|
229
|
-
const next = (input.runCtx.next_recommended_command ?? "").toLowerCase();
|
|
230
271
|
const prompt = input.userPrompt.toLowerCase();
|
|
231
|
-
|
|
232
|
-
input.reviewOutcome?.remediation_class === "plan_gap" ||
|
|
233
|
-
next.includes("/harness-plan") ||
|
|
234
|
-
next.includes("revise") ||
|
|
272
|
+
const explicitRevise =
|
|
235
273
|
prompt.includes("--mode revise") ||
|
|
236
274
|
prompt.includes("--mode=revise") ||
|
|
237
|
-
prompt.includes("mode: revise")
|
|
275
|
+
prompt.includes("mode: revise") ||
|
|
276
|
+
/\b(revise\s+(the\s+)?plan|reset\s+plan|start\s+over\s+on\s+the\s+plan)\b/.test(
|
|
277
|
+
prompt,
|
|
278
|
+
);
|
|
279
|
+
if (explicitRevise) return true;
|
|
280
|
+
if (input.reviewOutcome?.remediation_class !== "plan_gap") return false;
|
|
281
|
+
return (
|
|
282
|
+
prompt.includes("plan_gap") ||
|
|
283
|
+
prompt.includes("remediation_class") ||
|
|
284
|
+
/\brevise\s+per\s+review\b/.test(prompt)
|
|
238
285
|
);
|
|
239
286
|
}
|
|
240
287
|
|
|
@@ -341,13 +388,22 @@ async function hydrateFromDisk(
|
|
|
341
388
|
entries: unknown[],
|
|
342
389
|
): Promise<HarnessRunContext | null> {
|
|
343
390
|
const fromSession = getLatestRunContext(entries);
|
|
344
|
-
if (fromSession)
|
|
391
|
+
if (fromSession) {
|
|
392
|
+
return reconcileStaleExecuteCompletion(projectRoot, fromSession, entries);
|
|
393
|
+
}
|
|
345
394
|
|
|
346
395
|
const pointer = await loadProjectActiveRun(projectRoot);
|
|
347
396
|
if (!pointer || isStaleActiveRunPointer(pointer, projectRoot)) return null;
|
|
348
397
|
|
|
349
398
|
const disk = await loadRunContextFromDisk(pointer.run_id, projectRoot);
|
|
350
|
-
if (disk)
|
|
399
|
+
if (disk) {
|
|
400
|
+
const clar = await syncPlanLastOutcomeFromTaskClarification(
|
|
401
|
+
projectRoot,
|
|
402
|
+
disk,
|
|
403
|
+
);
|
|
404
|
+
const planSynced = await syncPlanReadyFromDisk(projectRoot, clar, entries);
|
|
405
|
+
return reconcileStaleExecuteCompletion(projectRoot, planSynced, entries);
|
|
406
|
+
}
|
|
351
407
|
|
|
352
408
|
return {
|
|
353
409
|
schema_version: "1.0.0",
|
|
@@ -476,10 +532,13 @@ function startFreshPlanAttempt(input: {
|
|
|
476
532
|
activeCtx: HarnessRunContext;
|
|
477
533
|
command: string;
|
|
478
534
|
turn: HarnessTurnEntry | null;
|
|
535
|
+
sessionId: string;
|
|
479
536
|
}): void {
|
|
480
537
|
input.activeCtx.plan_ready = false;
|
|
481
538
|
input.activeCtx.phase = "plan";
|
|
482
539
|
input.activeCtx.status = "active";
|
|
540
|
+
disarmHarnessKillSwitch(input.sessionId);
|
|
541
|
+
resetHarnessPolicyDenyCount(input.sessionId);
|
|
483
542
|
input.pi.appendEntry("harness-plan-attempt", {
|
|
484
543
|
run_id: input.activeCtx.run_id,
|
|
485
544
|
command: input.command,
|
|
@@ -584,6 +643,159 @@ type ActiveContextAccess = {
|
|
|
584
643
|
set(ctx: HarnessRunContext | null): void;
|
|
585
644
|
};
|
|
586
645
|
|
|
646
|
+
const HARNESS_CLEAR_CONFIRM_OPTION = "Delete historical runs";
|
|
647
|
+
|
|
648
|
+
function isHarnessClearConfirmed(response: unknown): boolean {
|
|
649
|
+
if (!response || typeof response !== "object") return false;
|
|
650
|
+
const payload = response as {
|
|
651
|
+
kind?: string;
|
|
652
|
+
selections?: unknown;
|
|
653
|
+
};
|
|
654
|
+
if (payload.kind !== "selection" || !Array.isArray(payload.selections)) {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
return (
|
|
658
|
+
payload.selections.length === 1 &&
|
|
659
|
+
payload.selections[0] === HARNESS_CLEAR_CONFIRM_OPTION
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function registerHarnessClearCommand(
|
|
664
|
+
pi: ExtensionAPI,
|
|
665
|
+
active: ActiveContextAccess,
|
|
666
|
+
): void {
|
|
667
|
+
pi.registerCommand("harness-clear", {
|
|
668
|
+
description:
|
|
669
|
+
"Delete historical harness runs under .pi/harness/runs while preserving the active run",
|
|
670
|
+
handler: async (_args, ctx) => {
|
|
671
|
+
const entries = getEntries(ctx);
|
|
672
|
+
const projectRoot = process.cwd();
|
|
673
|
+
const latest = active.get() ?? getLatestRunContext(entries);
|
|
674
|
+
const pointer = await loadProjectActiveRun(projectRoot);
|
|
675
|
+
const protectedRunIds = new Set<string>();
|
|
676
|
+
if (latest?.run_id) protectedRunIds.add(latest.run_id);
|
|
677
|
+
if (pointer?.run_id) protectedRunIds.add(pointer.run_id);
|
|
678
|
+
const manifest = await buildHarnessClearManifest(
|
|
679
|
+
projectRoot,
|
|
680
|
+
protectedRunIds,
|
|
681
|
+
);
|
|
682
|
+
if (manifest.candidates.length === 0) {
|
|
683
|
+
const message = [
|
|
684
|
+
"/harness-clear: no historical run directories eligible for deletion.",
|
|
685
|
+
` protected: ${manifest.protected_run_ids.join(", ") || "(none)"}`,
|
|
686
|
+
` skipped: ${manifest.skipped.length}`,
|
|
687
|
+
].join("\n");
|
|
688
|
+
if (ctx.hasUI) ctx.ui.notify(message, "info");
|
|
689
|
+
else
|
|
690
|
+
pi.sendMessage({
|
|
691
|
+
customType: "harness-clear-result",
|
|
692
|
+
content: message,
|
|
693
|
+
display: true,
|
|
694
|
+
});
|
|
695
|
+
pi.appendEntry("harness-clear-result", {
|
|
696
|
+
approved: false,
|
|
697
|
+
deleted: 0,
|
|
698
|
+
protected: manifest.protected_run_ids,
|
|
699
|
+
skipped: manifest.skipped,
|
|
700
|
+
recorded_at: nowIso(),
|
|
701
|
+
});
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const ask = await runAskUser(
|
|
705
|
+
{
|
|
706
|
+
question: `Delete ${manifest.candidates.length} historical harness run directories?`,
|
|
707
|
+
context: [
|
|
708
|
+
"Scope: .pi/harness/runs/<run_id> only (historical runs).",
|
|
709
|
+
`Preserved active run ids: ${manifest.protected_run_ids.join(", ") || "(none)"}`,
|
|
710
|
+
`Candidates: ${manifest.candidates.map((item) => item.run_id).join(", ")}`,
|
|
711
|
+
].join("\n"),
|
|
712
|
+
options: [HARNESS_CLEAR_CONFIRM_OPTION, "Cancel"],
|
|
713
|
+
allowSkip: true,
|
|
714
|
+
},
|
|
715
|
+
{ ui: ctx.ui, hasUI: ctx.hasUI },
|
|
716
|
+
);
|
|
717
|
+
if ("error" in ask) {
|
|
718
|
+
const message = [
|
|
719
|
+
"/harness-clear: confirmation unavailable; no files deleted (fail-closed).",
|
|
720
|
+
` reason: ${ask.error}`,
|
|
721
|
+
].join("\n");
|
|
722
|
+
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
723
|
+
else
|
|
724
|
+
pi.sendMessage({
|
|
725
|
+
customType: "harness-clear-result",
|
|
726
|
+
content: message,
|
|
727
|
+
display: true,
|
|
728
|
+
});
|
|
729
|
+
pi.appendEntry("harness-clear-result", {
|
|
730
|
+
approved: false,
|
|
731
|
+
deleted: 0,
|
|
732
|
+
protected: manifest.protected_run_ids,
|
|
733
|
+
skipped: manifest.skipped,
|
|
734
|
+
ask_error: ask.error,
|
|
735
|
+
recorded_at: nowIso(),
|
|
736
|
+
});
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const confirmed =
|
|
740
|
+
!ask.details.cancelled && isHarnessClearConfirmed(ask.details.response);
|
|
741
|
+
if (!confirmed) {
|
|
742
|
+
const message = [
|
|
743
|
+
"/harness-clear: cancelled; no files deleted.",
|
|
744
|
+
` candidates: ${manifest.candidates.length}`,
|
|
745
|
+
].join("\n");
|
|
746
|
+
if (ctx.hasUI) ctx.ui.notify(message, "info");
|
|
747
|
+
else
|
|
748
|
+
pi.sendMessage({
|
|
749
|
+
customType: "harness-clear-result",
|
|
750
|
+
content: message,
|
|
751
|
+
display: true,
|
|
752
|
+
});
|
|
753
|
+
pi.appendEntry("harness-clear-result", {
|
|
754
|
+
approved: false,
|
|
755
|
+
deleted: 0,
|
|
756
|
+
protected: manifest.protected_run_ids,
|
|
757
|
+
skipped: manifest.skipped,
|
|
758
|
+
recorded_at: nowIso(),
|
|
759
|
+
});
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
let deleted = 0;
|
|
763
|
+
const failed: Array<{ run_id: string; reason: string }> = [];
|
|
764
|
+
for (const candidate of manifest.candidates) {
|
|
765
|
+
try {
|
|
766
|
+
await rm(candidate.canonical_path, { recursive: true, force: true });
|
|
767
|
+
deleted += 1;
|
|
768
|
+
} catch (err) {
|
|
769
|
+
failed.push({
|
|
770
|
+
run_id: candidate.run_id,
|
|
771
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const message = [
|
|
776
|
+
"/harness-clear complete.",
|
|
777
|
+
` deleted: ${deleted}`,
|
|
778
|
+
` protected: ${manifest.protected_run_ids.length}`,
|
|
779
|
+
` skipped: ${manifest.skipped.length + failed.length}`,
|
|
780
|
+
].join("\n");
|
|
781
|
+
if (ctx.hasUI) ctx.ui.notify(message, "info");
|
|
782
|
+
else
|
|
783
|
+
pi.sendMessage({
|
|
784
|
+
customType: "harness-clear-result",
|
|
785
|
+
content: message,
|
|
786
|
+
display: true,
|
|
787
|
+
});
|
|
788
|
+
pi.appendEntry("harness-clear-result", {
|
|
789
|
+
approved: true,
|
|
790
|
+
deleted,
|
|
791
|
+
protected: manifest.protected_run_ids,
|
|
792
|
+
skipped: [...manifest.skipped, ...failed],
|
|
793
|
+
recorded_at: nowIso(),
|
|
794
|
+
});
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
587
799
|
function registerHarnessRunStatusCommand(
|
|
588
800
|
pi: ExtensionAPI,
|
|
589
801
|
active: ActiveContextAccess,
|
|
@@ -603,6 +815,13 @@ function registerHarnessRunStatusCommand(
|
|
|
603
815
|
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
604
816
|
return;
|
|
605
817
|
}
|
|
818
|
+
ctxState = await refreshRunContextProgress(
|
|
819
|
+
projectRoot,
|
|
820
|
+
ctxState,
|
|
821
|
+
entries,
|
|
822
|
+
);
|
|
823
|
+
active.set(ctxState);
|
|
824
|
+
persistContext(pi, ctxState);
|
|
606
825
|
let summary: PlanPacketSummary | null = null;
|
|
607
826
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
608
827
|
const entry = entries[i] as SessionEntryLike;
|
|
@@ -926,6 +1145,13 @@ async function archivePlanRevisionIfNeeded(input: {
|
|
|
926
1145
|
reason: "review_plan_gap_revise",
|
|
927
1146
|
});
|
|
928
1147
|
if (reset.moved.length === 0) return;
|
|
1148
|
+
input.activeCtx.plan_ready = false;
|
|
1149
|
+
const synced = await syncPlanLastOutcomeFromTaskClarification(
|
|
1150
|
+
input.projectRoot,
|
|
1151
|
+
input.activeCtx,
|
|
1152
|
+
);
|
|
1153
|
+
Object.assign(input.activeCtx, synced);
|
|
1154
|
+
persistContext(input.pi, input.activeCtx);
|
|
929
1155
|
input.pi.appendEntry("harness-plan-revision-reset", {
|
|
930
1156
|
run_id: input.activeCtx.run_id,
|
|
931
1157
|
archive_dir: reset.archiveDir,
|
|
@@ -989,18 +1215,27 @@ async function updatePlanReadinessAfterAgent(input: {
|
|
|
989
1215
|
)
|
|
990
1216
|
return;
|
|
991
1217
|
if (!input.activeCtx.plan_packet_path) return;
|
|
992
|
-
const
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
input.activeCtx
|
|
999
|
-
if (
|
|
1000
|
-
|
|
1001
|
-
|
|
1218
|
+
const beforeReady = input.activeCtx.plan_ready;
|
|
1219
|
+
const synced = await syncPlanReadyFromDisk(
|
|
1220
|
+
process.cwd(),
|
|
1221
|
+
input.activeCtx,
|
|
1222
|
+
input.entries,
|
|
1223
|
+
);
|
|
1224
|
+
Object.assign(input.activeCtx, synced);
|
|
1225
|
+
if (!beforeReady && synced.plan_ready && synced.plan_packet_path) {
|
|
1226
|
+
const packet = await readPlanPacketFromPath(synced.plan_packet_path);
|
|
1227
|
+
if (packet?.plan_id) {
|
|
1228
|
+
syncPolicyFromPlan(input.pi, input.entries, packet.plan_id, "plan", true);
|
|
1229
|
+
const summary = planPacketSummary(packet, synced.plan_packet_path);
|
|
1230
|
+
input.pi.appendEntry("harness-plan-packet", summary);
|
|
1231
|
+
}
|
|
1232
|
+
} else if (
|
|
1233
|
+
synced.plan_packet_path &&
|
|
1234
|
+
!synced.plan_ready &&
|
|
1235
|
+
synced.last_outcome === "pending_approval"
|
|
1236
|
+
) {
|
|
1002
1237
|
const msg =
|
|
1003
|
-
"
|
|
1238
|
+
"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.";
|
|
1004
1239
|
if (input.ctx.hasUI) input.ctx.ui.notify(msg, "warning");
|
|
1005
1240
|
else
|
|
1006
1241
|
input.pi.sendMessage({
|
|
@@ -1008,17 +1243,8 @@ async function updatePlanReadinessAfterAgent(input: {
|
|
|
1008
1243
|
content: msg,
|
|
1009
1244
|
display: true,
|
|
1010
1245
|
});
|
|
1011
|
-
} else if (input.activeCtx.plan_ready && packet?.plan_id) {
|
|
1012
|
-
input.activeCtx.plan_id = packet.plan_id;
|
|
1013
|
-
syncPolicyFromPlan(input.pi, input.entries, packet.plan_id, "plan", true);
|
|
1014
|
-
const summary = planPacketSummary(packet, input.activeCtx.plan_packet_path);
|
|
1015
|
-
input.pi.appendEntry("harness-plan-packet", summary);
|
|
1016
|
-
input.activeCtx.last_completed_step = "plan";
|
|
1017
|
-
input.activeCtx.last_outcome = summary.plan_status;
|
|
1018
|
-
} else if (!validation.valid) {
|
|
1019
|
-
input.activeCtx.last_outcome = "needs_clarification";
|
|
1020
|
-
input.activeCtx.last_completed_step = "plan";
|
|
1021
1246
|
}
|
|
1247
|
+
persistContext(input.pi, input.activeCtx);
|
|
1022
1248
|
}
|
|
1023
1249
|
|
|
1024
1250
|
function registerPlanApprovalCapture(
|
|
@@ -1029,15 +1255,63 @@ function registerPlanApprovalCapture(
|
|
|
1029
1255
|
if (event.isError) return;
|
|
1030
1256
|
if (event.toolName !== "ask_user" && event.toolName !== "approve_plan")
|
|
1031
1257
|
return;
|
|
1258
|
+
const entries = getEntries(ctx);
|
|
1259
|
+
const runCtx = getLatestRunContext(entries) ?? active.get();
|
|
1260
|
+
if (!runCtx) return;
|
|
1261
|
+
if (event.toolName === "ask_user") {
|
|
1262
|
+
const details = event.details as { cancelled?: boolean; input?: unknown };
|
|
1263
|
+
if (details?.cancelled) {
|
|
1264
|
+
// Ignore cancels from later planning forks (e.g. debate profile choice):
|
|
1265
|
+
// only treat cancel as Phase-0 clarification failure when clarification
|
|
1266
|
+
// is not already locked ready.
|
|
1267
|
+
const runRoot = join(
|
|
1268
|
+
process.cwd(),
|
|
1269
|
+
".pi",
|
|
1270
|
+
"harness",
|
|
1271
|
+
"runs",
|
|
1272
|
+
runCtx.run_id ?? "",
|
|
1273
|
+
);
|
|
1274
|
+
const clarDoc = runCtx.run_id
|
|
1275
|
+
? await readTaskClarificationDoc(runRoot)
|
|
1276
|
+
: null;
|
|
1277
|
+
const clarReady =
|
|
1278
|
+
String(clarDoc?.status ?? "").toLowerCase() === "ready";
|
|
1279
|
+
if (!clarReady) {
|
|
1280
|
+
const synced = await syncPlanLastOutcomeFromTaskClarification(
|
|
1281
|
+
process.cwd(),
|
|
1282
|
+
runCtx,
|
|
1283
|
+
);
|
|
1284
|
+
Object.assign(runCtx, synced);
|
|
1285
|
+
persistContext(pi, runCtx);
|
|
1286
|
+
}
|
|
1287
|
+
} else if (
|
|
1288
|
+
!isPlanApprovalAskUser(
|
|
1289
|
+
(details?.input ?? {}) as {
|
|
1290
|
+
question?: string;
|
|
1291
|
+
options?: unknown[];
|
|
1292
|
+
questions?: unknown[];
|
|
1293
|
+
},
|
|
1294
|
+
)
|
|
1295
|
+
) {
|
|
1296
|
+
pi.appendEntry("harness-task-clarification-engagement", {
|
|
1297
|
+
run_id: runCtx.run_id,
|
|
1298
|
+
recorded_at: nowIso(),
|
|
1299
|
+
source: "ask_user",
|
|
1300
|
+
});
|
|
1301
|
+
const synced = await syncPlanLastOutcomeFromTaskClarification(
|
|
1302
|
+
process.cwd(),
|
|
1303
|
+
runCtx,
|
|
1304
|
+
);
|
|
1305
|
+
Object.assign(runCtx, synced);
|
|
1306
|
+
persistContext(pi, runCtx);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1032
1309
|
const approval = parsePlanApprovalFromMessage({
|
|
1033
1310
|
toolName: event.toolName,
|
|
1034
1311
|
details: event.details,
|
|
1035
1312
|
content: event.content,
|
|
1036
1313
|
});
|
|
1037
1314
|
if (!approval) return;
|
|
1038
|
-
const entries = getEntries(ctx);
|
|
1039
|
-
const runCtx = getLatestRunContext(entries) ?? active.get();
|
|
1040
|
-
if (!runCtx) return;
|
|
1041
1315
|
pi.appendEntry("harness-plan-approval", {
|
|
1042
1316
|
plan_id: approval.plan_id ?? runCtx.plan_id,
|
|
1043
1317
|
approved_at: approval.approved_at,
|
|
@@ -1046,6 +1320,36 @@ function registerPlanApprovalCapture(
|
|
|
1046
1320
|
});
|
|
1047
1321
|
}
|
|
1048
1322
|
|
|
1323
|
+
function registerExecutorHandoffReconcile(
|
|
1324
|
+
pi: ExtensionAPI,
|
|
1325
|
+
active: ActiveContextAccess,
|
|
1326
|
+
): void {
|
|
1327
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
1328
|
+
if (event.isError || event.toolName !== "submit_executor_handoff") return;
|
|
1329
|
+
const entries = getEntries(ctx);
|
|
1330
|
+
const runCtx = getLatestRunContext(entries) ?? active.get();
|
|
1331
|
+
if (!runCtx?.run_id) return;
|
|
1332
|
+
const projectRoot = process.cwd();
|
|
1333
|
+
const refreshed = await refreshRunContextProgress(
|
|
1334
|
+
projectRoot,
|
|
1335
|
+
runCtx,
|
|
1336
|
+
entries,
|
|
1337
|
+
);
|
|
1338
|
+
Object.assign(runCtx, refreshed);
|
|
1339
|
+
active.set(runCtx);
|
|
1340
|
+
persistContext(pi, runCtx);
|
|
1341
|
+
if (refreshed.last_completed_step === "execute") {
|
|
1342
|
+
const notify = `Execute finished (${refreshed.last_outcome ?? "done"}). Next: ${refreshed.next_recommended_command ?? "/harness-review"}`;
|
|
1343
|
+
pi.appendEntry("harness-step-handoff", {
|
|
1344
|
+
next_command: refreshed.next_recommended_command,
|
|
1345
|
+
execution_status: refreshed.last_outcome,
|
|
1346
|
+
phase: refreshed.phase,
|
|
1347
|
+
});
|
|
1348
|
+
if (ctx.hasUI) ctx.ui.notify(notify, "info");
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1049
1353
|
async function guardToolCall(input: {
|
|
1050
1354
|
event: { toolName: string; input: unknown };
|
|
1051
1355
|
ctx: { sessionManager: { getEntries(): unknown[] } };
|
|
@@ -1165,18 +1469,41 @@ async function resolveCommandRunContext(input: {
|
|
|
1165
1469
|
input.command === "harness-auto" ||
|
|
1166
1470
|
(!activeCtx && input.command !== "harness-abort")
|
|
1167
1471
|
) {
|
|
1472
|
+
const task = extractTaskSummary(input.args, input.userPrompt);
|
|
1168
1473
|
if (
|
|
1169
|
-
|
|
1170
|
-
|
|
1474
|
+
input.command === "harness-auto" &&
|
|
1475
|
+
activeCtx &&
|
|
1476
|
+
task &&
|
|
1477
|
+
harnessAutoTasksDiffer(activeCtx, task)
|
|
1171
1478
|
) {
|
|
1479
|
+
activeCtx.status = "aborted";
|
|
1480
|
+
activeCtx.plan_ready = false;
|
|
1481
|
+
activeCtx.last_outcome = "abandoned";
|
|
1482
|
+
activeCtx.last_completed_step = "abort";
|
|
1483
|
+
persistContext(input.pi, activeCtx);
|
|
1484
|
+
activeCtx = null;
|
|
1485
|
+
}
|
|
1486
|
+
const reuseRun =
|
|
1487
|
+
activeCtx &&
|
|
1488
|
+
shouldReuseHarnessRunId(input.userPrompt, activeCtx, input.command);
|
|
1489
|
+
if (!activeCtx || !reuseRun) {
|
|
1490
|
+
if (activeCtx?.status === "active") {
|
|
1491
|
+
activeCtx.status = "aborted";
|
|
1492
|
+
activeCtx.plan_ready = false;
|
|
1493
|
+
activeCtx.last_outcome = "abandoned";
|
|
1494
|
+
activeCtx.last_completed_step = "abort";
|
|
1495
|
+
persistContext(input.pi, activeCtx);
|
|
1496
|
+
}
|
|
1172
1497
|
activeCtx = createFreshRunContext(
|
|
1173
1498
|
input.sessionId,
|
|
1174
1499
|
input.projectRoot,
|
|
1175
|
-
|
|
1500
|
+
task,
|
|
1176
1501
|
);
|
|
1502
|
+
} else if (input.command === "harness-auto") {
|
|
1503
|
+
activeCtx = resetRunContextForHarnessAuto(activeCtx);
|
|
1504
|
+
if (task) activeCtx.task_summary = task;
|
|
1177
1505
|
}
|
|
1178
1506
|
if (input.command === "harness-plan") {
|
|
1179
|
-
const task = extractTaskSummary(input.args, input.userPrompt);
|
|
1180
1507
|
if (task) activeCtx.task_summary = task;
|
|
1181
1508
|
}
|
|
1182
1509
|
startFreshPlanAttempt({
|
|
@@ -1184,6 +1511,7 @@ async function resolveCommandRunContext(input: {
|
|
|
1184
1511
|
activeCtx,
|
|
1185
1512
|
command: input.command,
|
|
1186
1513
|
turn: input.turn,
|
|
1514
|
+
sessionId: input.sessionId,
|
|
1187
1515
|
});
|
|
1188
1516
|
} else if (
|
|
1189
1517
|
activeCtx &&
|
|
@@ -1297,7 +1625,7 @@ async function handlePreResolvedHarnessCommand(args: {
|
|
|
1297
1625
|
handled: true,
|
|
1298
1626
|
};
|
|
1299
1627
|
}
|
|
1300
|
-
if (command === "harness-run-status") {
|
|
1628
|
+
if (command === "harness-run-status" || command === "harness-clear") {
|
|
1301
1629
|
return { activeCtx, response: undefined, handled: true };
|
|
1302
1630
|
}
|
|
1303
1631
|
if (
|
|
@@ -1317,21 +1645,6 @@ async function handlePreResolvedHarnessCommand(args: {
|
|
|
1317
1645
|
return { activeCtx, response: null, handled: false };
|
|
1318
1646
|
}
|
|
1319
1647
|
|
|
1320
|
-
function blockingRunCommandReason(
|
|
1321
|
-
command: string,
|
|
1322
|
-
activeCtx: HarnessRunContext,
|
|
1323
|
-
): string | null {
|
|
1324
|
-
if (command !== "harness-run") return null;
|
|
1325
|
-
if (!activeCtx.plan_ready) return "Plan not ready. Run /harness-plan first.";
|
|
1326
|
-
if (
|
|
1327
|
-
activeCtx.last_completed_step === "execute" &&
|
|
1328
|
-
activeCtx.last_outcome === "completed"
|
|
1329
|
-
) {
|
|
1330
|
-
return "Execute already completed for this run. Next: /harness-review (same session), or /harness-abort to replan.";
|
|
1331
|
-
}
|
|
1332
|
-
return null;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
1648
|
async function handleBeforeAgentStart(input: {
|
|
1336
1649
|
pi: ExtensionAPI;
|
|
1337
1650
|
event: any;
|
|
@@ -1371,12 +1684,21 @@ async function handleBeforeAgentStart(input: {
|
|
|
1371
1684
|
"plan";
|
|
1372
1685
|
const driftActive = driftGateActive(entries);
|
|
1373
1686
|
if (!parsed && needsClarificationFollowUp(activeCtx) && activeCtx) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1687
|
+
const synced = await syncPlanLastOutcomeFromTaskClarification(
|
|
1688
|
+
projectRoot,
|
|
1376
1689
|
activeCtx,
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1690
|
+
);
|
|
1691
|
+
if (synced.last_outcome !== "needs_clarification") {
|
|
1692
|
+
input.active.set(synced);
|
|
1693
|
+
persistContext(input.pi, synced);
|
|
1694
|
+
} else {
|
|
1695
|
+
return maybeHandleClarificationFollowUp({
|
|
1696
|
+
pi: input.pi,
|
|
1697
|
+
activeCtx,
|
|
1698
|
+
entries,
|
|
1699
|
+
systemPrompt: input.event.systemPrompt,
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1380
1702
|
}
|
|
1381
1703
|
if (!parsed) return undefined;
|
|
1382
1704
|
const { command, args } = parsed;
|
|
@@ -1433,8 +1755,40 @@ async function handleBeforeAgentStart(input: {
|
|
|
1433
1755
|
return blockRunContextMessage(check.reason ?? "Invalid --plan override");
|
|
1434
1756
|
activeCtx.plan_packet_path = resolved.planPath;
|
|
1435
1757
|
}
|
|
1436
|
-
|
|
1758
|
+
let planSynced = await reconcileStaleExecuteCompletion(
|
|
1759
|
+
projectRoot,
|
|
1760
|
+
activeCtx,
|
|
1761
|
+
entries,
|
|
1762
|
+
);
|
|
1763
|
+
planSynced = await reconcileReviewRouting(projectRoot, planSynced);
|
|
1764
|
+
Object.assign(activeCtx, planSynced);
|
|
1765
|
+
persistContext(input.pi, activeCtx);
|
|
1766
|
+
const autoBlockReason = await blockingHarnessAutoCommandReason(
|
|
1767
|
+
command,
|
|
1768
|
+
activeCtx,
|
|
1769
|
+
args,
|
|
1770
|
+
userPrompt,
|
|
1771
|
+
);
|
|
1772
|
+
if (autoBlockReason) return blockRunContextMessage(autoBlockReason);
|
|
1773
|
+
const runBlockReason = await blockingRunCommandReason(
|
|
1774
|
+
command,
|
|
1775
|
+
activeCtx,
|
|
1776
|
+
projectRoot,
|
|
1777
|
+
entries,
|
|
1778
|
+
);
|
|
1437
1779
|
if (runBlockReason) return blockRunContextMessage(runBlockReason);
|
|
1780
|
+
const reviewBlockReason = await blockingReviewCommandReason(
|
|
1781
|
+
command,
|
|
1782
|
+
activeCtx,
|
|
1783
|
+
projectRoot,
|
|
1784
|
+
);
|
|
1785
|
+
if (reviewBlockReason) return blockRunContextMessage(reviewBlockReason);
|
|
1786
|
+
const steerBlockReason = await blockingSteerCommandReason(
|
|
1787
|
+
command,
|
|
1788
|
+
activeCtx,
|
|
1789
|
+
projectRoot,
|
|
1790
|
+
);
|
|
1791
|
+
if (steerBlockReason) return blockRunContextMessage(steerBlockReason);
|
|
1438
1792
|
const { planSummary, planPacketForSpawn } =
|
|
1439
1793
|
await readPlanSpawnState(activeCtx);
|
|
1440
1794
|
const { activePlanBlock, planMode, contextSpawnOpts } =
|
|
@@ -1452,10 +1806,34 @@ async function handleBeforeAgentStart(input: {
|
|
|
1452
1806
|
projectRoot,
|
|
1453
1807
|
userPrompt,
|
|
1454
1808
|
});
|
|
1809
|
+
const syncedCtx = await syncPlanLastOutcomeFromTaskClarification(
|
|
1810
|
+
projectRoot,
|
|
1811
|
+
activeCtx,
|
|
1812
|
+
);
|
|
1813
|
+
Object.assign(activeCtx, syncedCtx);
|
|
1455
1814
|
input.active.set(activeCtx);
|
|
1456
1815
|
persistContext(input.pi, activeCtx);
|
|
1816
|
+
if (command === "harness-plan" || command === "harness-auto") {
|
|
1817
|
+
syncPolicyFromRunContext(input.pi, entries, activeCtx);
|
|
1818
|
+
}
|
|
1819
|
+
let gateBlock = "";
|
|
1820
|
+
if (command === "harness-plan" || command === "harness-auto") {
|
|
1821
|
+
const quick = parseArgFlag(args, "--quick") != null;
|
|
1822
|
+
const gateStatus = await resolvePlanHumanGateStatus(
|
|
1823
|
+
projectRoot,
|
|
1824
|
+
activeCtx.run_id,
|
|
1825
|
+
entries,
|
|
1826
|
+
{
|
|
1827
|
+
quick,
|
|
1828
|
+
taskSummary: activeCtx.task_summary ?? undefined,
|
|
1829
|
+
lastOutcome: activeCtx.last_outcome ?? undefined,
|
|
1830
|
+
},
|
|
1831
|
+
);
|
|
1832
|
+
gateBlock = formatPlanHumanGateBlock(gateStatus);
|
|
1833
|
+
}
|
|
1834
|
+
const gateSuffix = gateBlock ? `\n\n${gateBlock}` : "";
|
|
1457
1835
|
return {
|
|
1458
|
-
systemPrompt: `${input.event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx, contextSpawnOpts)}${activePlanBlock ? `\n\n${activePlanBlock}` : ""}`,
|
|
1836
|
+
systemPrompt: `${input.event.systemPrompt}\n\n${formatPlanContextBlock(activeCtx, contextSpawnOpts)}${activePlanBlock ? `\n\n${activePlanBlock}` : ""}${gateSuffix}`,
|
|
1459
1837
|
};
|
|
1460
1838
|
}
|
|
1461
1839
|
|
|
@@ -1468,6 +1846,13 @@ async function handleAgentEnd(input: {
|
|
|
1468
1846
|
const entries = getEntries(input.ctx);
|
|
1469
1847
|
const activeCtx = input.active.get() ?? getLatestRunContext(entries);
|
|
1470
1848
|
if (!activeCtx) return;
|
|
1849
|
+
let reconciledOnEnd = await reconcileStaleExecuteCompletion(
|
|
1850
|
+
projectRoot,
|
|
1851
|
+
activeCtx,
|
|
1852
|
+
entries,
|
|
1853
|
+
);
|
|
1854
|
+
reconciledOnEnd = await reconcileReviewRouting(projectRoot, reconciledOnEnd);
|
|
1855
|
+
Object.assign(activeCtx, reconciledOnEnd);
|
|
1471
1856
|
input.active.set(activeCtx);
|
|
1472
1857
|
const parsed = latestParsedHarnessCommand(entries);
|
|
1473
1858
|
if (!parsed && !needsClarificationFollowUp(activeCtx)) return;
|
|
@@ -1482,13 +1867,23 @@ async function handleAgentEnd(input: {
|
|
|
1482
1867
|
parsed,
|
|
1483
1868
|
activeCtx,
|
|
1484
1869
|
});
|
|
1870
|
+
if (
|
|
1871
|
+
parsed?.command === "harness-plan" ||
|
|
1872
|
+
parsed?.command === "harness-auto"
|
|
1873
|
+
) {
|
|
1874
|
+
const synced = await syncPlanLastOutcomeFromTaskClarification(
|
|
1875
|
+
projectRoot,
|
|
1876
|
+
activeCtx,
|
|
1877
|
+
);
|
|
1878
|
+
Object.assign(activeCtx, synced);
|
|
1879
|
+
persistContext(input.pi, activeCtx);
|
|
1880
|
+
}
|
|
1485
1881
|
const statuses = await resolveCompletionStatuses(
|
|
1486
1882
|
entries,
|
|
1487
1883
|
activeCtx.run_id,
|
|
1488
1884
|
projectRoot,
|
|
1489
1885
|
);
|
|
1490
|
-
if (parsed?.command === "harness-run") {
|
|
1491
|
-
activeCtx.last_completed_step = "execute";
|
|
1886
|
+
if (parsed?.command === "harness-run" || parsed?.command === "harness-auto") {
|
|
1492
1887
|
let execStatus = statuses.executionStatus;
|
|
1493
1888
|
if (!execStatus) {
|
|
1494
1889
|
const handoff = await readExecutorHandoffFromRun(
|
|
@@ -1497,8 +1892,11 @@ async function handleAgentEnd(input: {
|
|
|
1497
1892
|
);
|
|
1498
1893
|
execStatus = handoff?.execution_status ?? null;
|
|
1499
1894
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1895
|
+
const runPost = resolveHarnessRunPostAgentState(
|
|
1896
|
+
execStatus,
|
|
1897
|
+
activeCtx.plan_ready,
|
|
1898
|
+
);
|
|
1899
|
+
Object.assign(activeCtx, runPost);
|
|
1502
1900
|
}
|
|
1503
1901
|
if (parsed?.command === "harness-steer") {
|
|
1504
1902
|
activeCtx.last_completed_step = "steer";
|
|
@@ -1521,7 +1919,14 @@ async function handleAgentEnd(input: {
|
|
|
1521
1919
|
activeCtx.last_completed_step = "adversary";
|
|
1522
1920
|
} else if (statuses.evalStatus) activeCtx.phase = "evaluate";
|
|
1523
1921
|
}
|
|
1524
|
-
|
|
1922
|
+
if (
|
|
1923
|
+
["harness-eval", "harness-review", "harness-critic"].includes(
|
|
1924
|
+
parsed?.command ?? "",
|
|
1925
|
+
)
|
|
1926
|
+
) {
|
|
1927
|
+
await ensureReviewOutcomeFromEval(activeCtx.run_id, projectRoot);
|
|
1928
|
+
}
|
|
1929
|
+
const remediationClass = await resolveRemediationClassForRun(
|
|
1525
1930
|
activeCtx.run_id,
|
|
1526
1931
|
projectRoot,
|
|
1527
1932
|
);
|
|
@@ -1537,7 +1942,7 @@ async function handleAgentEnd(input: {
|
|
|
1537
1942
|
evalStatus: statuses.evalStatus,
|
|
1538
1943
|
adversaryComplete: statuses.adversaryComplete,
|
|
1539
1944
|
aborted: activeCtx.status === "aborted",
|
|
1540
|
-
remediationClass
|
|
1945
|
+
remediationClass,
|
|
1541
1946
|
steerAttempt: activeCtx.steer_attempt ?? 0,
|
|
1542
1947
|
steerMaxAttempts: activeCtx.steer_max_attempts ?? steerMaxAttemptsFromEnv(),
|
|
1543
1948
|
reviewComplete,
|
|
@@ -1545,7 +1950,7 @@ async function handleAgentEnd(input: {
|
|
|
1545
1950
|
activeCtx.next_recommended_command = next;
|
|
1546
1951
|
activeCtx.updated_at = new Date().toISOString();
|
|
1547
1952
|
if (
|
|
1548
|
-
parsed?.command === "harness-run" &&
|
|
1953
|
+
(parsed?.command === "harness-run" || parsed?.command === "harness-auto") &&
|
|
1549
1954
|
activeCtx.last_outcome === "completed"
|
|
1550
1955
|
) {
|
|
1551
1956
|
syncPolicyFromRunContext(input.pi, entries, activeCtx);
|
|
@@ -1590,7 +1995,7 @@ function registerHarnessRunContextTool1(
|
|
|
1590
1995
|
parameters: Type.Object({
|
|
1591
1996
|
path: Type.String({
|
|
1592
1997
|
description:
|
|
1593
|
-
"
|
|
1998
|
+
"Run-relative path (preferred): artifacts/decomposition.yaml, research-brief.yaml, plan-packet.yaml. The active run id is applied automatically — do not prefix with .pi/harness/runs/.",
|
|
1594
1999
|
}),
|
|
1595
2000
|
content: Type.String({
|
|
1596
2001
|
description:
|
|
@@ -1640,21 +2045,32 @@ function registerHarnessRunContextTool1(
|
|
|
1640
2045
|
};
|
|
1641
2046
|
}
|
|
1642
2047
|
const projectRoot = process.cwd();
|
|
1643
|
-
const
|
|
1644
|
-
|
|
2048
|
+
const resolved = resolveHarnessRunWriteTarget(
|
|
2049
|
+
pathArg,
|
|
2050
|
+
runCtx,
|
|
2051
|
+
projectRoot,
|
|
2052
|
+
);
|
|
2053
|
+
const absPath =
|
|
2054
|
+
resolved?.absPath ?? normalizeHarnessPath(pathArg, projectRoot);
|
|
2055
|
+
const scoped =
|
|
2056
|
+
resolved != null ||
|
|
2057
|
+
(await isPlanPhaseScopedWrite(absPath, runCtx, projectRoot));
|
|
1645
2058
|
if (!scoped) {
|
|
1646
2059
|
return {
|
|
1647
2060
|
content: [
|
|
1648
2061
|
{
|
|
1649
2062
|
type: "text",
|
|
1650
|
-
text: `Path not allowed: ${pathArg}.
|
|
2063
|
+
text: `Path not allowed: ${pathArg}. Use a run-relative path like artifacts/decomposition.yaml or research-brief.yaml (active run ${runCtx.run_id} is applied automatically). Full paths under .pi/harness/runs/${runCtx.run_id}/ are also accepted.`,
|
|
1651
2064
|
},
|
|
1652
2065
|
],
|
|
1653
|
-
details: { path: pathArg },
|
|
2066
|
+
details: { path: pathArg, run_id: runCtx.run_id },
|
|
1654
2067
|
isError: true,
|
|
1655
2068
|
};
|
|
1656
2069
|
}
|
|
1657
|
-
const relForGate =
|
|
2070
|
+
const relForGate =
|
|
2071
|
+
resolved?.relUnderRun ??
|
|
2072
|
+
(await relPathUnderActiveRun(absPath, runCtx, projectRoot)) ??
|
|
2073
|
+
pathArg.replace(/\\/g, "/");
|
|
1658
2074
|
const subagentOnly = new Set([
|
|
1659
2075
|
"artifacts/eval-verdict.yaml",
|
|
1660
2076
|
"artifacts/adversary-report.yaml",
|
|
@@ -1721,12 +2137,67 @@ function registerHarnessRunContextTool1(
|
|
|
1721
2137
|
doc = parseStructuredDocument(content, pathArg);
|
|
1722
2138
|
} catch (err) {
|
|
1723
2139
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2140
|
+
const hint =
|
|
2141
|
+
msg.includes("not valid YAML") || msg.includes("JSON parse")
|
|
2142
|
+
? " Pass a fenced ```yaml block, raw YAML object, or JSON object — not prose or a partial fragment."
|
|
2143
|
+
: "";
|
|
1724
2144
|
return {
|
|
1725
|
-
content: [
|
|
1726
|
-
|
|
2145
|
+
content: [
|
|
2146
|
+
{
|
|
2147
|
+
type: "text",
|
|
2148
|
+
text: `${relForGate}: ${msg}${hint}`,
|
|
2149
|
+
},
|
|
2150
|
+
],
|
|
2151
|
+
details: { path: relForGate, run_id: runCtx.run_id },
|
|
1727
2152
|
isError: true,
|
|
1728
2153
|
};
|
|
1729
2154
|
}
|
|
2155
|
+
const docRecord = doc as Record<string, unknown>;
|
|
2156
|
+
if (relForGate === TASK_CLARIFICATION_ARTIFACT) {
|
|
2157
|
+
const humanGate = validateTaskClarificationHumanGate(
|
|
2158
|
+
entries,
|
|
2159
|
+
docRecord,
|
|
2160
|
+
{
|
|
2161
|
+
quick:
|
|
2162
|
+
parseArgFlag(
|
|
2163
|
+
getLatestHarnessTurn(entries)?.args ?? "",
|
|
2164
|
+
"--quick",
|
|
2165
|
+
) != null,
|
|
2166
|
+
taskSummary: runCtx.task_summary ?? undefined,
|
|
2167
|
+
allowFollowUpMessage: runCtx.last_outcome === "needs_clarification",
|
|
2168
|
+
},
|
|
2169
|
+
);
|
|
2170
|
+
if (!humanGate.ok) {
|
|
2171
|
+
return {
|
|
2172
|
+
content: [
|
|
2173
|
+
{
|
|
2174
|
+
type: "text",
|
|
2175
|
+
text: humanGate.errors.join("\n"),
|
|
2176
|
+
},
|
|
2177
|
+
],
|
|
2178
|
+
details: { path: pathArg },
|
|
2179
|
+
isError: true,
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
if (relForGate === "artifacts/plan-phase-status.yaml") {
|
|
2184
|
+
const planStatus = String(docRecord.plan_status ?? "").toLowerCase();
|
|
2185
|
+
if (
|
|
2186
|
+
planStatus === "ready" &&
|
|
2187
|
+
!hasPlanUserApproval(entries, { sincePlanCommand: true })
|
|
2188
|
+
) {
|
|
2189
|
+
return {
|
|
2190
|
+
content: [
|
|
2191
|
+
{
|
|
2192
|
+
type: "text",
|
|
2193
|
+
text: "Blocked: plan_status ready requires approve_plan (then create_plan) before marking the plan phase complete.",
|
|
2194
|
+
},
|
|
2195
|
+
],
|
|
2196
|
+
details: { path: pathArg },
|
|
2197
|
+
isError: true,
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
1730
2201
|
await mkdir(dirname(absPath), { recursive: true });
|
|
1731
2202
|
await writeYamlFile(absPath, doc);
|
|
1732
2203
|
if (relForGate === TASK_CLARIFICATION_ARTIFACT) {
|
|
@@ -1743,10 +2214,10 @@ function registerHarnessRunContextTool1(
|
|
|
1743
2214
|
content: [
|
|
1744
2215
|
{
|
|
1745
2216
|
type: "text",
|
|
1746
|
-
text: `Wrote ${
|
|
2217
|
+
text: `Wrote ${relForGate} as canonical YAML.`,
|
|
1747
2218
|
},
|
|
1748
2219
|
],
|
|
1749
|
-
details: { path: absPath },
|
|
2220
|
+
details: { path: absPath, rel: relForGate, run_id: runCtx.run_id },
|
|
1750
2221
|
};
|
|
1751
2222
|
},
|
|
1752
2223
|
});
|
|
@@ -1812,17 +2283,25 @@ function registerHarnessRunContextTool2(
|
|
|
1812
2283
|
};
|
|
1813
2284
|
}
|
|
1814
2285
|
const projectRoot = process.cwd();
|
|
1815
|
-
const
|
|
1816
|
-
|
|
2286
|
+
const resolved = resolveHarnessRunWriteTarget(
|
|
2287
|
+
pathArg,
|
|
2288
|
+
runCtx,
|
|
2289
|
+
projectRoot,
|
|
2290
|
+
);
|
|
2291
|
+
const absPath =
|
|
2292
|
+
resolved?.absPath ?? normalizeHarnessPath(pathArg, projectRoot);
|
|
2293
|
+
const scoped =
|
|
2294
|
+
resolved != null ||
|
|
2295
|
+
(await isPlanPhaseScopedWrite(absPath, runCtx, projectRoot));
|
|
1817
2296
|
if (!scoped) {
|
|
1818
2297
|
return {
|
|
1819
2298
|
content: [
|
|
1820
2299
|
{
|
|
1821
2300
|
type: "text",
|
|
1822
|
-
text: `Path not allowed: ${pathArg}.`,
|
|
2301
|
+
text: `Path not allowed: ${pathArg}. Use run-relative paths like artifacts/decomposition.yaml (active run ${runCtx.run_id}).`,
|
|
1823
2302
|
},
|
|
1824
2303
|
],
|
|
1825
|
-
details: { path: pathArg },
|
|
2304
|
+
details: { path: pathArg, run_id: runCtx.run_id },
|
|
1826
2305
|
isError: true,
|
|
1827
2306
|
};
|
|
1828
2307
|
}
|
|
@@ -1833,7 +2312,10 @@ function registerHarnessRunContextTool2(
|
|
|
1833
2312
|
"runs",
|
|
1834
2313
|
runCtx.run_id,
|
|
1835
2314
|
);
|
|
1836
|
-
const relMerge =
|
|
2315
|
+
const relMerge =
|
|
2316
|
+
resolved?.relUnderRun ??
|
|
2317
|
+
(await relPathUnderActiveRun(absPath, runCtx, projectRoot)) ??
|
|
2318
|
+
pathArg.replace(/\\/g, "/");
|
|
1837
2319
|
const clarMerge = await assertTaskClarificationReadyForPlanWrite(
|
|
1838
2320
|
runRoot,
|
|
1839
2321
|
relMerge,
|
|
@@ -2044,7 +2526,18 @@ function registerHarnessRunContextTool4(
|
|
|
2044
2526
|
const { validateHarnessArtifactPaths } = await import(
|
|
2045
2527
|
"../lib/harness-artifact-gate.js"
|
|
2046
2528
|
);
|
|
2047
|
-
const
|
|
2529
|
+
const turn = getLatestHarnessTurn(entries);
|
|
2530
|
+
const gate = await validateHarnessArtifactPaths(
|
|
2531
|
+
runRoot,
|
|
2532
|
+
paths,
|
|
2533
|
+
specsDir,
|
|
2534
|
+
{
|
|
2535
|
+
entries,
|
|
2536
|
+
quick: turn ? parseArgFlag(turn.args, "--quick") != null : false,
|
|
2537
|
+
taskSummary: runCtx.task_summary ?? undefined,
|
|
2538
|
+
lastOutcome: runCtx.last_outcome ?? undefined,
|
|
2539
|
+
},
|
|
2540
|
+
);
|
|
2048
2541
|
if (
|
|
2049
2542
|
gate.ok &&
|
|
2050
2543
|
paths.some((p) => p.replace(/\\/g, "/") === TASK_CLARIFICATION_ARTIFACT)
|
|
@@ -2053,8 +2546,13 @@ function registerHarnessRunContextTool4(
|
|
|
2053
2546
|
const clarified = String(clarDoc?.clarified_task ?? "").trim();
|
|
2054
2547
|
if (clarified && runCtx.task_summary !== clarified) {
|
|
2055
2548
|
runCtx.task_summary = clarified;
|
|
2056
|
-
persistContext(pi, runCtx);
|
|
2057
2549
|
}
|
|
2550
|
+
const synced = await syncPlanLastOutcomeFromTaskClarification(
|
|
2551
|
+
projectRoot,
|
|
2552
|
+
runCtx,
|
|
2553
|
+
);
|
|
2554
|
+
Object.assign(runCtx, synced);
|
|
2555
|
+
persistContext(pi, runCtx);
|
|
2058
2556
|
}
|
|
2059
2557
|
const text = gate.ok
|
|
2060
2558
|
? `All ${gate.present.length} artifact(s) present and valid.`
|
|
@@ -2136,8 +2634,11 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
2136
2634
|
});
|
|
2137
2635
|
|
|
2138
2636
|
registerPlanApprovalCapture(pi, activeAccess);
|
|
2637
|
+
registerExecutorHandoffReconcile(pi, activeAccess);
|
|
2139
2638
|
registerHarnessToolCallGuards(pi, activeAccess);
|
|
2140
2639
|
registerHarnessRunStatusCommand(pi, activeAccess);
|
|
2640
|
+
|
|
2641
|
+
registerHarnessClearCommand(pi, activeAccess);
|
|
2141
2642
|
registerHarnessNewRunCommand(pi, activeAccess);
|
|
2142
2643
|
|
|
2143
2644
|
registerHarnessPlanCommitCommand(pi, activeAccess);
|