ultimate-pi 0.22.1 → 0.22.2
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 +529 -84
- 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 +804 -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 +322 -0
- package/.pi/prompts/harness-clear.md +25 -0
- package/.pi/prompts/harness-plan.md +4 -0
- 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 +6 -0
- package/README.md +4 -0
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
laneArtifactPathsForParallelProbesRound,
|
|
20
20
|
laneArtifactPathsForRound,
|
|
21
21
|
} from "./plan-debate-lanes.js";
|
|
22
|
+
import { getPlanDebateRoundStatus } from "./plan-debate-round-status.js";
|
|
22
23
|
import {
|
|
23
24
|
getMessengerRoundState,
|
|
24
25
|
loadMessengerState,
|
|
@@ -331,3 +332,89 @@ export function isReviewRoundArtifactPath(relPath: string): boolean {
|
|
|
331
332
|
norm === CONSOLIDATED_REVIEW_ARTIFACT
|
|
332
333
|
);
|
|
333
334
|
}
|
|
335
|
+
|
|
336
|
+
function roundIndexForFocus(
|
|
337
|
+
focus: PlanDebateFocus,
|
|
338
|
+
required: readonly PlanDebateFocus[],
|
|
339
|
+
): number {
|
|
340
|
+
const idx = required.indexOf(focus);
|
|
341
|
+
return idx >= 0 ? idx + 1 : 1;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Actionable recovery steps when approve_plan is blocked by the debate gate. */
|
|
345
|
+
export async function buildPlanDebateGateRecovery(
|
|
346
|
+
projectRoot: string,
|
|
347
|
+
runId: string,
|
|
348
|
+
gate: PlanDebateGateResult,
|
|
349
|
+
): Promise<string> {
|
|
350
|
+
const runDir = join(projectRoot, ".pi", "harness", "runs", runId);
|
|
351
|
+
const messenger = await loadMessengerState(runDir);
|
|
352
|
+
const required: readonly PlanDebateFocus[] =
|
|
353
|
+
messenger?.required_focuses && messenger.required_focuses.length > 0
|
|
354
|
+
? messenger.required_focuses
|
|
355
|
+
: (["spec", "wbs", "schedule", "quality"] as const);
|
|
356
|
+
const profile =
|
|
357
|
+
messenger?.debate_profile ?? gate.debate_profile ?? "standard";
|
|
358
|
+
const mode = messenger?.review_gate_mode ?? "threaded";
|
|
359
|
+
const coverage = gate.focus_coverage;
|
|
360
|
+
|
|
361
|
+
const lines: string[] = [
|
|
362
|
+
"Review Gate must finish before approve_plan.",
|
|
363
|
+
"",
|
|
364
|
+
"Blocking checks:",
|
|
365
|
+
...gate.errors.map((e) => `- ${e}`),
|
|
366
|
+
"",
|
|
367
|
+
`Debate profile: ${profile}, mode: ${mode}, required focuses: ${required.join(", ")}`,
|
|
368
|
+
"",
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
const needsConsensus = gate.errors.some(
|
|
372
|
+
(e) => e.includes("consensus") || e.includes(".consensus.json"),
|
|
373
|
+
);
|
|
374
|
+
const needsRounds = gate.errors.some(
|
|
375
|
+
(e) =>
|
|
376
|
+
e.includes("review_gate_ready") ||
|
|
377
|
+
e.includes("focus not covered") ||
|
|
378
|
+
e.includes("missing artifacts/") ||
|
|
379
|
+
e.includes("round events"),
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
if (needsRounds) {
|
|
383
|
+
const nextFocus: PlanDebateFocus =
|
|
384
|
+
(coverage?.missing[0] as PlanDebateFocus | undefined) ??
|
|
385
|
+
required[0] ??
|
|
386
|
+
"spec";
|
|
387
|
+
const roundIndex =
|
|
388
|
+
mode === "consolidated" ? 1 : roundIndexForFocus(nextFocus, required);
|
|
389
|
+
const status = await getPlanDebateRoundStatus(runDir, roundIndex, runId, {
|
|
390
|
+
debate_round_focus: mode === "consolidated" ? "all" : nextFocus,
|
|
391
|
+
});
|
|
392
|
+
lines.push(
|
|
393
|
+
`Next round: ${roundIndex} (focus: ${mode === "consolidated" ? "all" : nextFocus})`,
|
|
394
|
+
);
|
|
395
|
+
if (status.missing.length > 0) {
|
|
396
|
+
lines.push("Missing lane artifacts:");
|
|
397
|
+
for (const m of status.missing) {
|
|
398
|
+
lines.push(`- ${m}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (status.next_tool) {
|
|
402
|
+
lines.push(`Next tool: ${status.next_tool}`);
|
|
403
|
+
}
|
|
404
|
+
lines.push(
|
|
405
|
+
"Workflow: complete lane subagents (one per batch) → review-integrator → harness_debate_submit_round → harness_debate_focus_coverage.",
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (needsConsensus) {
|
|
410
|
+
lines.push(
|
|
411
|
+
"When all required focuses are covered and the last round has review_gate_ready: true, call harness_debate_consensus, then approve_plan again.",
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (gate.warnings.length > 0) {
|
|
416
|
+
lines.push("", "Warnings:", ...gate.warnings.map((w) => `- ${w}`));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return lines.join("\n");
|
|
420
|
+
}
|
|
@@ -48,12 +48,14 @@ export async function applyDebateLaneFromDoc(opts: {
|
|
|
48
48
|
lane: DebateLaneKind;
|
|
49
49
|
doc: Record<string, unknown>;
|
|
50
50
|
roundIndex?: number;
|
|
51
|
+
skipArtifactWrite?: boolean;
|
|
51
52
|
}): Promise<ApplyDebateLaneResult> {
|
|
52
53
|
return applyDebateLane({
|
|
53
54
|
runDir: opts.runDir,
|
|
54
55
|
lane: opts.lane,
|
|
55
56
|
content: JSON.stringify(opts.doc),
|
|
56
57
|
roundIndex: opts.roundIndex,
|
|
58
|
+
skipArtifactWrite: opts.skipArtifactWrite,
|
|
57
59
|
});
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -95,6 +97,8 @@ export async function applyDebateLane(opts: {
|
|
|
95
97
|
lane: DebateLaneKind;
|
|
96
98
|
content: string;
|
|
97
99
|
roundIndex?: number;
|
|
100
|
+
/** When true, artifact YAML was already written (e.g. submit pipeline); only messenger side effects run. */
|
|
101
|
+
skipArtifactWrite?: boolean;
|
|
98
102
|
}): Promise<ApplyDebateLaneResult> {
|
|
99
103
|
const errors: string[] = [];
|
|
100
104
|
let doc: Record<string, unknown>;
|
|
@@ -121,8 +125,10 @@ export async function applyDebateLane(opts: {
|
|
|
121
125
|
: (opts.roundIndex ?? 1);
|
|
122
126
|
const relPath = laneArtifactPath(opts.lane, roundIndex);
|
|
123
127
|
const absPath = join(opts.runDir, relPath);
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
if (!opts.skipArtifactWrite) {
|
|
129
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
130
|
+
await writeYamlFile(absPath, doc);
|
|
131
|
+
}
|
|
126
132
|
|
|
127
133
|
let messengerPosted = false;
|
|
128
134
|
let nextStep: string | undefined;
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-in-the-loop gates for /harness-plan — Phase 0 ask_user and Phase 6 approve_plan.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { constants } from "node:fs";
|
|
6
|
+
import { access } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
isHarnessNonInteractive,
|
|
10
|
+
isPlanApprovalAskUser,
|
|
11
|
+
} from "./ask-user/policy.js";
|
|
12
|
+
import {
|
|
13
|
+
hasPlanUserApproval,
|
|
14
|
+
indexOfLastPlanCommand,
|
|
15
|
+
} from "./harness-run-context.js";
|
|
16
|
+
import { validatePlanApprovalReadiness } from "./plan-approval-readiness.js";
|
|
17
|
+
import {
|
|
18
|
+
buildPlanDebateGateRecovery,
|
|
19
|
+
validatePlanDebateGate,
|
|
20
|
+
} from "./plan-debate-gate.js";
|
|
21
|
+
import {
|
|
22
|
+
isTaskClarificationReady,
|
|
23
|
+
readTaskClarificationDoc,
|
|
24
|
+
type TaskClarificationReadiness,
|
|
25
|
+
validateTaskClarificationDoc,
|
|
26
|
+
} from "./plan-task-clarification.js";
|
|
27
|
+
|
|
28
|
+
const EXPLICIT_ACCEPTANCE_RE =
|
|
29
|
+
/\b(acceptance|success criteria|definition of done|done when|must (pass|satisfy)|out of scope|in scope)\b/i;
|
|
30
|
+
|
|
31
|
+
type SessionEntryLike = {
|
|
32
|
+
type?: string;
|
|
33
|
+
customType?: string;
|
|
34
|
+
data?: unknown;
|
|
35
|
+
message?: {
|
|
36
|
+
role?: string;
|
|
37
|
+
toolName?: string;
|
|
38
|
+
details?: unknown;
|
|
39
|
+
content?: string | unknown[];
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function isNonInteractivePlan(): boolean {
|
|
44
|
+
return (
|
|
45
|
+
process.env.HARNESS_PLAN_NONINTERACTIVE === "1" || isHarnessNonInteractive()
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function askUserCallWasTaskClarification(details: unknown): boolean {
|
|
50
|
+
if (!details || typeof details !== "object") return false;
|
|
51
|
+
const d = details as { cancelled?: boolean; input?: unknown };
|
|
52
|
+
if (d.cancelled) return false;
|
|
53
|
+
const input = d.input as
|
|
54
|
+
| { question?: string; options?: unknown[]; questions?: unknown[] }
|
|
55
|
+
| undefined;
|
|
56
|
+
if (!input) return true;
|
|
57
|
+
return !isPlanApprovalAskUser(input);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function hasTaskClarificationAskUserSincePlanCommand(
|
|
61
|
+
entries: unknown[],
|
|
62
|
+
): boolean {
|
|
63
|
+
if (isNonInteractivePlan()) return true;
|
|
64
|
+
const since = Math.max(0, indexOfLastPlanCommand(entries));
|
|
65
|
+
for (let i = since; i < entries.length; i++) {
|
|
66
|
+
const entry = entries[i] as SessionEntryLike;
|
|
67
|
+
if (
|
|
68
|
+
entry.type === "custom" &&
|
|
69
|
+
entry.customType === "harness-task-clarification-engagement"
|
|
70
|
+
) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (entry.type !== "message" || entry.message?.role !== "toolResult") {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (entry.message.toolName !== "ask_user") continue;
|
|
77
|
+
if (askUserCallWasTaskClarification(entry.message.details)) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function hasClarificationFollowUpUserMessage(
|
|
85
|
+
entries: unknown[],
|
|
86
|
+
): boolean {
|
|
87
|
+
const since = Math.max(0, indexOfLastPlanCommand(entries));
|
|
88
|
+
for (let i = since; i < entries.length; i++) {
|
|
89
|
+
const entry = entries[i] as SessionEntryLike;
|
|
90
|
+
if (entry.type !== "message" || entry.message?.role !== "user") continue;
|
|
91
|
+
const content = entry.message.content;
|
|
92
|
+
const text =
|
|
93
|
+
typeof content === "string"
|
|
94
|
+
? content.trim()
|
|
95
|
+
: Array.isArray(content)
|
|
96
|
+
? content
|
|
97
|
+
.filter(
|
|
98
|
+
(c): c is { type: string; text?: string } =>
|
|
99
|
+
typeof c === "object" && c !== null && "type" in c,
|
|
100
|
+
)
|
|
101
|
+
.map((c) => c.text ?? "")
|
|
102
|
+
.join("")
|
|
103
|
+
.trim()
|
|
104
|
+
: "";
|
|
105
|
+
if (!text || text.startsWith("/")) continue;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function isExplicitTaskAcceptance(taskSummary: string): boolean {
|
|
112
|
+
const t = taskSummary.trim();
|
|
113
|
+
if (t.length < 24) return false;
|
|
114
|
+
return EXPLICIT_ACCEPTANCE_RE.test(t);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface TaskClarificationHumanGateResult {
|
|
118
|
+
ok: boolean;
|
|
119
|
+
errors: string[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function validateTaskClarificationHumanGate(
|
|
123
|
+
entries: unknown[],
|
|
124
|
+
doc: Record<string, unknown> | null,
|
|
125
|
+
opts?: {
|
|
126
|
+
quick?: boolean;
|
|
127
|
+
taskSummary?: string;
|
|
128
|
+
allowFollowUpMessage?: boolean;
|
|
129
|
+
},
|
|
130
|
+
): TaskClarificationHumanGateResult {
|
|
131
|
+
const errors: string[] = [];
|
|
132
|
+
const status = String(doc?.status ?? "").toLowerCase();
|
|
133
|
+
if (status !== "ready") {
|
|
134
|
+
return { ok: true, errors };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const engagement = doc?.user_engagement as { source?: string } | undefined;
|
|
138
|
+
if (engagement?.source === "ask_user") {
|
|
139
|
+
return { ok: true, errors };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (hasTaskClarificationAskUserSincePlanCommand(entries)) {
|
|
143
|
+
return { ok: true, errors };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (
|
|
147
|
+
opts?.allowFollowUpMessage &&
|
|
148
|
+
hasClarificationFollowUpUserMessage(entries)
|
|
149
|
+
) {
|
|
150
|
+
return { ok: true, errors };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (opts?.quick && isExplicitTaskAcceptance(opts.taskSummary ?? "")) {
|
|
154
|
+
return { ok: true, errors };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
errors.push(
|
|
158
|
+
"Phase 0 requires ask_user before task-clarification status: ready. Call ask_user (harness-decisions skill), merge answers, then harness_artifact_ready.",
|
|
159
|
+
);
|
|
160
|
+
return { ok: false, errors };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
164
|
+
try {
|
|
165
|
+
await access(path, constants.R_OK);
|
|
166
|
+
return true;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface PlanHumanGateStatus {
|
|
173
|
+
phase0Ready: boolean;
|
|
174
|
+
phase0NeedsAskUser: boolean;
|
|
175
|
+
debateComplete: boolean;
|
|
176
|
+
debateRequired: boolean;
|
|
177
|
+
approvalRequired: boolean;
|
|
178
|
+
approvalRecorded: boolean;
|
|
179
|
+
nextRequiredAction: string | null;
|
|
180
|
+
/** Actionable Review Gate recovery when debateRequired. */
|
|
181
|
+
debateRecoveryHint: string | null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function resolvePlanHumanGateStatus(
|
|
185
|
+
projectRoot: string,
|
|
186
|
+
runId: string,
|
|
187
|
+
entries: unknown[],
|
|
188
|
+
opts?: { quick?: boolean; taskSummary?: string; lastOutcome?: string | null },
|
|
189
|
+
): Promise<PlanHumanGateStatus> {
|
|
190
|
+
const runDir = join(projectRoot, ".pi", "harness", "runs", runId);
|
|
191
|
+
const clar = await isTaskClarificationReady(runDir);
|
|
192
|
+
const clarDoc = clar.ok ? await readTaskClarificationDoc(runDir) : null;
|
|
193
|
+
const humanGate = validateTaskClarificationHumanGate(entries, clarDoc, {
|
|
194
|
+
quick: opts?.quick,
|
|
195
|
+
taskSummary: opts?.taskSummary,
|
|
196
|
+
allowFollowUpMessage: opts?.lastOutcome === "needs_clarification",
|
|
197
|
+
});
|
|
198
|
+
const phase0Ready = clar.ok && humanGate.ok;
|
|
199
|
+
const phase0NeedsAskUser = clar.ok && !humanGate.ok;
|
|
200
|
+
const approvalRecorded = hasPlanUserApproval(entries, {
|
|
201
|
+
sincePlanCommand: true,
|
|
202
|
+
});
|
|
203
|
+
const dagPath = join(runDir, "plan-packet.yaml");
|
|
204
|
+
const hasPacket = await fileExists(dagPath);
|
|
205
|
+
const messengerPath = join(runDir, "debate-messenger", "state.json");
|
|
206
|
+
const debateOpened = await fileExists(messengerPath);
|
|
207
|
+
|
|
208
|
+
let debateComplete = true;
|
|
209
|
+
let debateGate = null;
|
|
210
|
+
let readinessOk = false;
|
|
211
|
+
let approvalRequired = false;
|
|
212
|
+
|
|
213
|
+
if (phase0Ready && !approvalRecorded) {
|
|
214
|
+
const readiness = await validatePlanApprovalReadiness(projectRoot, runId, {
|
|
215
|
+
risk_level: String(clarDoc?.risk_level ?? "med"),
|
|
216
|
+
quick: opts?.quick,
|
|
217
|
+
});
|
|
218
|
+
readinessOk = readiness.ok;
|
|
219
|
+
debateGate = await validatePlanDebateGate(projectRoot, runId);
|
|
220
|
+
debateComplete = debateGate.ok;
|
|
221
|
+
approvalRequired = readiness.ok && debateComplete && hasPacket;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const debateRequired =
|
|
225
|
+
phase0Ready &&
|
|
226
|
+
!debateComplete &&
|
|
227
|
+
!approvalRecorded &&
|
|
228
|
+
(debateOpened || hasPacket);
|
|
229
|
+
|
|
230
|
+
let debateRecoveryHint: string | null = null;
|
|
231
|
+
let nextRequiredAction: string | null = null;
|
|
232
|
+
if (!phase0Ready) {
|
|
233
|
+
nextRequiredAction = phase0NeedsAskUser
|
|
234
|
+
? "ask_user (Phase 0 task contract)"
|
|
235
|
+
: "complete artifacts/task-clarification.yaml (Phase 0)";
|
|
236
|
+
} else if (debateRequired && debateGate) {
|
|
237
|
+
debateRecoveryHint = await buildPlanDebateGateRecovery(
|
|
238
|
+
projectRoot,
|
|
239
|
+
runId,
|
|
240
|
+
debateGate,
|
|
241
|
+
);
|
|
242
|
+
nextRequiredAction =
|
|
243
|
+
"Complete Review Gate (debate rounds + harness_debate_consensus) before approve_plan";
|
|
244
|
+
} else if (approvalRequired && !approvalRecorded) {
|
|
245
|
+
nextRequiredAction = "approve_plan then create_plan (Phase 6)";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
phase0Ready,
|
|
250
|
+
phase0NeedsAskUser,
|
|
251
|
+
debateComplete,
|
|
252
|
+
debateRequired,
|
|
253
|
+
approvalRequired,
|
|
254
|
+
approvalRecorded,
|
|
255
|
+
nextRequiredAction,
|
|
256
|
+
debateRecoveryHint,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function formatPlanHumanGateBlock(status: PlanHumanGateStatus): string {
|
|
261
|
+
if (!status.nextRequiredAction) return "";
|
|
262
|
+
const lines = [
|
|
263
|
+
"[HarnessPlanGate]",
|
|
264
|
+
`next_required_action=${status.nextRequiredAction}`,
|
|
265
|
+
`phase0_ready=${status.phase0Ready}`,
|
|
266
|
+
`review_gate_complete=${status.debateComplete}`,
|
|
267
|
+
`review_gate_required=${status.debateRequired}`,
|
|
268
|
+
`plan_approval_required=${status.approvalRequired}`,
|
|
269
|
+
`plan_approval_recorded=${status.approvalRecorded}`,
|
|
270
|
+
];
|
|
271
|
+
if (status.debateRequired) {
|
|
272
|
+
lines.push(
|
|
273
|
+
"Do not end this turn with prose only — call harness_debate_round_status / harness_debate_focus_coverage and spawn the next debate lane subagent (one per batch).",
|
|
274
|
+
);
|
|
275
|
+
} else {
|
|
276
|
+
lines.push(
|
|
277
|
+
"Do not spawn planning subagents or end this turn until the required human step completes.",
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (status.debateRecoveryHint) {
|
|
281
|
+
lines.push("", status.debateRecoveryHint);
|
|
282
|
+
}
|
|
283
|
+
return lines.join("\n");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function shouldBlockSubagentForMissingPlanApproval(
|
|
287
|
+
projectRoot: string,
|
|
288
|
+
runId: string,
|
|
289
|
+
entries: unknown[],
|
|
290
|
+
phase: string,
|
|
291
|
+
): Promise<{ block: boolean; reason?: string }> {
|
|
292
|
+
if (phase !== "plan" || isNonInteractivePlan()) return { block: false };
|
|
293
|
+
if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
|
|
294
|
+
return { block: false };
|
|
295
|
+
}
|
|
296
|
+
const status = await resolvePlanHumanGateStatus(projectRoot, runId, entries);
|
|
297
|
+
if (!status.approvalRequired) return { block: false };
|
|
298
|
+
return {
|
|
299
|
+
block: true,
|
|
300
|
+
reason:
|
|
301
|
+
"Plan Review Gate is complete but user approval is missing. Call approve_plan (then create_plan) before further subagent work.",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function validateTaskClarificationReadyWithHumanGate(
|
|
306
|
+
runDir: string,
|
|
307
|
+
entries: unknown[],
|
|
308
|
+
opts?: { quick?: boolean; taskSummary?: string; lastOutcome?: string | null },
|
|
309
|
+
): Promise<TaskClarificationReadiness & { humanErrors: string[] }> {
|
|
310
|
+
const doc = await readTaskClarificationDoc(runDir);
|
|
311
|
+
const base = validateTaskClarificationDoc(doc, { requireReady: true });
|
|
312
|
+
const human = validateTaskClarificationHumanGate(entries, doc, {
|
|
313
|
+
quick: opts?.quick,
|
|
314
|
+
taskSummary: opts?.taskSummary,
|
|
315
|
+
allowFollowUpMessage: opts?.lastOutcome === "needs_clarification",
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
ok: base.ok && human.ok,
|
|
319
|
+
errors: [...base.errors, ...human.errors],
|
|
320
|
+
humanErrors: human.errors,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Safely delete historical harness run directories while preserving the active run.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# harness-clear
|
|
6
|
+
|
|
7
|
+
Delete only historical run directories under `.pi/harness/runs/`.
|
|
8
|
+
|
|
9
|
+
## What this does
|
|
10
|
+
|
|
11
|
+
- enumerates delete candidates strictly from `.pi/harness/runs/<run_id>/`
|
|
12
|
+
- always preserves active run ids discovered from session context and active-run pointer
|
|
13
|
+
- asks for one confirmation before any filesystem mutation
|
|
14
|
+
- fails closed: cancel/decline/timeout/error/unavailable confirmation paths delete nothing
|
|
15
|
+
- reports deleted vs protected/skipped counts
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
`/harness-clear`
|
|
20
|
+
|
|
21
|
+
## Safety boundaries
|
|
22
|
+
|
|
23
|
+
- in scope: historical run directories only
|
|
24
|
+
- out of scope: full `.pi/harness/` reset, non-run harness assets, active-run deletion overrides
|
|
25
|
+
- confirmation is mandatory; non-affirmative outcomes are no-op
|
|
@@ -11,6 +11,10 @@ Use the phase order and spawn topology defined in this prompt directly.
|
|
|
11
11
|
|
|
12
12
|
Subagents persist artifacts via scoped **`submit_*`** tools (deterministic YAML under the run dir). Parent uses **`harness_artifact_ready`** to gate phases (no JSON parsing). Parent merges still use **`write_harness_yaml`** for `research-brief.yaml`, `plan-packet.yaml`, `planning-context.yaml`, and integrator patches.
|
|
13
13
|
|
|
14
|
+
### Subagent submit → gate (required)
|
|
15
|
+
|
|
16
|
+
After a subprocess **`submit_*`** succeeds (or the artifact path is on disk and schema-valid), call **`harness_artifact_ready({ paths: ["<that-artifact>"] })` once** before the next phase or spawn. If spawn topology returns **Duplicate spawn blocked**, do **not** re-spawn that agent — call `harness_artifact_ready` on the existing artifact and advance. Never call the same `submit_*` twice with identical content (idempotent noop — end the subprocess turn instead).
|
|
17
|
+
|
|
14
18
|
**Phase 0 is mandatory** before reconnaissance or any planning subagent. `write_harness_yaml` and spawn topology enforce `artifacts/task-clarification.yaml` with `status: ready`.
|
|
15
19
|
|
|
16
20
|
## Allowed subagents
|
|
@@ -13,6 +13,53 @@ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
|
13
13
|
const AGENTS_DIR = join(ROOT, ".pi", "agents");
|
|
14
14
|
const OUT = join(ROOT, ".pi", "harness", "agents.policy.yaml");
|
|
15
15
|
|
|
16
|
+
/** Per-agent tool denials layered on kind defaults (ADR 0049). */
|
|
17
|
+
const AGENT_TOOLS_DENY = {
|
|
18
|
+
"harness/planning/hypothesis-validator": [
|
|
19
|
+
"bash",
|
|
20
|
+
"grep",
|
|
21
|
+
"find",
|
|
22
|
+
"ls",
|
|
23
|
+
"ctx_batch_execute",
|
|
24
|
+
"ctx_execute",
|
|
25
|
+
"ctx_execute_file",
|
|
26
|
+
"ctx_search",
|
|
27
|
+
"ctx_fetch_and_index",
|
|
28
|
+
],
|
|
29
|
+
"harness/planning/implementation-researcher": [
|
|
30
|
+
"bash",
|
|
31
|
+
"find",
|
|
32
|
+
"ctx_batch_execute",
|
|
33
|
+
"ctx_execute",
|
|
34
|
+
"ctx_execute_file",
|
|
35
|
+
],
|
|
36
|
+
"harness/planning/stack-researcher": [
|
|
37
|
+
"bash",
|
|
38
|
+
"find",
|
|
39
|
+
"ctx_batch_execute",
|
|
40
|
+
"ctx_execute",
|
|
41
|
+
"ctx_execute_file",
|
|
42
|
+
],
|
|
43
|
+
"harness/planning/sprint-contract-auditor": [
|
|
44
|
+
"bash",
|
|
45
|
+
"find",
|
|
46
|
+
"ctx_batch_execute",
|
|
47
|
+
"ctx_execute",
|
|
48
|
+
"ctx_execute_file",
|
|
49
|
+
"ctx_fetch_and_index",
|
|
50
|
+
],
|
|
51
|
+
"harness/planning/review-integrator": [
|
|
52
|
+
"bash",
|
|
53
|
+
"grep",
|
|
54
|
+
"find",
|
|
55
|
+
"ctx_batch_execute",
|
|
56
|
+
"ctx_execute",
|
|
57
|
+
"ctx_execute_file",
|
|
58
|
+
"ctx_search",
|
|
59
|
+
"ctx_fetch_and_index",
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
|
|
16
63
|
const SUBMIT_BY_AGENT = {
|
|
17
64
|
"harness/planning/planning-context": ["submit_planning_context"],
|
|
18
65
|
"harness/planning/decompose": ["submit_decomposition_brief", "submit_human_required"],
|
|
@@ -60,14 +107,31 @@ function kindFor(id) {
|
|
|
60
107
|
return "other";
|
|
61
108
|
}
|
|
62
109
|
|
|
110
|
+
const READ_ONLY_BASE_TOOLS = [
|
|
111
|
+
"read",
|
|
112
|
+
"grep",
|
|
113
|
+
"find",
|
|
114
|
+
"ls",
|
|
115
|
+
"bash",
|
|
116
|
+
"ctx_batch_execute",
|
|
117
|
+
"ctx_execute",
|
|
118
|
+
"ctx_execute_file",
|
|
119
|
+
"ctx_search",
|
|
120
|
+
"ctx_fetch_and_index",
|
|
121
|
+
];
|
|
122
|
+
|
|
63
123
|
const KIND_BASE = {
|
|
64
|
-
planner: [
|
|
65
|
-
executor: [
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
124
|
+
planner: [...READ_ONLY_BASE_TOOLS],
|
|
125
|
+
executor: [
|
|
126
|
+
...READ_ONLY_BASE_TOOLS,
|
|
127
|
+
"write",
|
|
128
|
+
"edit",
|
|
129
|
+
],
|
|
130
|
+
evaluator: [...READ_ONLY_BASE_TOOLS],
|
|
131
|
+
adversary: [...READ_ONLY_BASE_TOOLS],
|
|
132
|
+
tie_breaker: [...READ_ONLY_BASE_TOOLS],
|
|
133
|
+
trace: [...READ_ONLY_BASE_TOOLS],
|
|
134
|
+
incident: [...READ_ONLY_BASE_TOOLS],
|
|
71
135
|
other: ["read", "grep", "find", "ls"],
|
|
72
136
|
};
|
|
73
137
|
|
|
@@ -117,6 +181,8 @@ async function main() {
|
|
|
117
181
|
(t) => !base.has(t),
|
|
118
182
|
);
|
|
119
183
|
const entry = { kind };
|
|
184
|
+
const toolsDeny = AGENT_TOOLS_DENY[id];
|
|
185
|
+
if (toolsDeny?.length) entry.tools_deny = toolsDeny;
|
|
120
186
|
if (toolsAdd.length > 0) entry.tools_add = toolsAdd;
|
|
121
187
|
if (fm.extensions === false) entry.extensions = false;
|
|
122
188
|
if (fm.extensions === true) entry.extensions = true;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Reconcile run-context.yaml with on-disk plan + executor handoff (no Pi session).
|
|
4
|
+
* Usage: node .pi/scripts/harness-reconcile-run-context.mjs <run-id>
|
|
5
|
+
*/
|
|
6
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
13
|
+
const runId = process.argv[2];
|
|
14
|
+
if (!runId) {
|
|
15
|
+
console.error("usage: node .pi/scripts/harness-reconcile-run-context.mjs <run-id>");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const dir = mkdtempSync(join(tmpdir(), "harness-reconcile-"));
|
|
20
|
+
const runner = join(dir, "run.mts");
|
|
21
|
+
writeFileSync(
|
|
22
|
+
runner,
|
|
23
|
+
`import {
|
|
24
|
+
reconcileStaleExecuteCompletion,
|
|
25
|
+
reconcileReviewRouting,
|
|
26
|
+
loadRunContextFromDisk,
|
|
27
|
+
saveRunContextToDisk,
|
|
28
|
+
} from ${JSON.stringify(join(root, ".pi/lib/harness-run-context.ts"))};
|
|
29
|
+
|
|
30
|
+
const root = ${JSON.stringify(root)};
|
|
31
|
+
const runId = ${JSON.stringify(runId)};
|
|
32
|
+
const ctx0 = await loadRunContextFromDisk(runId, root);
|
|
33
|
+
if (!ctx0) {
|
|
34
|
+
console.error("run not found:", runId);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
console.log("before", JSON.stringify({
|
|
38
|
+
phase: ctx0.phase,
|
|
39
|
+
step: ctx0.last_completed_step,
|
|
40
|
+
outcome: ctx0.last_outcome,
|
|
41
|
+
next: ctx0.next_recommended_command,
|
|
42
|
+
}));
|
|
43
|
+
let ctx1 = await reconcileStaleExecuteCompletion(root, ctx0, []);
|
|
44
|
+
ctx1 = await reconcileReviewRouting(root, ctx1);
|
|
45
|
+
await saveRunContextToDisk(ctx1, root);
|
|
46
|
+
console.log("after", JSON.stringify({
|
|
47
|
+
phase: ctx1.phase,
|
|
48
|
+
step: ctx1.last_completed_step,
|
|
49
|
+
outcome: ctx1.last_outcome,
|
|
50
|
+
next: ctx1.next_recommended_command,
|
|
51
|
+
}));
|
|
52
|
+
`,
|
|
53
|
+
"utf-8",
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const result = spawnSync("npx", ["-y", "tsx", runner], {
|
|
57
|
+
cwd: root,
|
|
58
|
+
encoding: "utf-8",
|
|
59
|
+
stdio: "inherit",
|
|
60
|
+
});
|
|
61
|
+
rmSync(dir, { recursive: true, force: true });
|
|
62
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Compile every harness JSON Schema (catches unresolved cross-file $ref).
|
|
4
|
+
* Invoked from harness-verify.mjs via `npx tsx`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import {
|
|
10
|
+
listHarnessSpecSchemaFiles,
|
|
11
|
+
verifyHarnessSchemaRefIntegrity,
|
|
12
|
+
verifyHarnessSchemasCompile,
|
|
13
|
+
} from "../lib/harness-schema-validate.ts";
|
|
14
|
+
|
|
15
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
16
|
+
const SPECS = join(ROOT, ".pi", "harness", "specs");
|
|
17
|
+
|
|
18
|
+
const files = await listHarnessSpecSchemaFiles(SPECS);
|
|
19
|
+
const integrity = await verifyHarnessSchemaRefIntegrity(SPECS);
|
|
20
|
+
if (!integrity.ok) {
|
|
21
|
+
console.error(integrity.errors.join("\n"));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const compiled = await verifyHarnessSchemasCompile(SPECS, files);
|
|
25
|
+
if (!compiled.ok) {
|
|
26
|
+
console.error(compiled.errors.join("\n"));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
console.log(`harness-schema-compile-verify: ${files.length} schemas OK`);
|