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.
Files changed (44) hide show
  1. package/.pi/extensions/agt-kill-switch.ts +7 -1
  2. package/.pi/extensions/harness-plan-approval.ts +9 -1
  3. package/.pi/extensions/harness-run-context.ts +587 -86
  4. package/.pi/extensions/policy-gate.ts +15 -2
  5. package/.pi/harness/agents.manifest.json +3 -3
  6. package/.pi/harness/agents.policy.yaml +82 -3
  7. package/.pi/harness/specs/plan-task-clarification.schema.json +10 -1
  8. package/.pi/lib/agents-policy.mjs +42 -1
  9. package/.pi/lib/agt/build-evaluation-context.ts +3 -1
  10. package/.pi/lib/agt/kill-switch-state.ts +14 -0
  11. package/.pi/lib/agt/legacy-evaluate.ts +3 -1
  12. package/.pi/lib/ask-user/index.ts +2 -0
  13. package/.pi/lib/ask-user/merge-task-clarification.ts +5 -0
  14. package/.pi/lib/ask-user/policy.ts +23 -0
  15. package/.pi/lib/ask-user/presenters/glimpse.ts +8 -1
  16. package/.pi/lib/ask-user/presenters/headless.ts +15 -0
  17. package/.pi/lib/ask-user/presenters/select.ts +11 -2
  18. package/.pi/lib/ask-user/validate-core.mjs +16 -0
  19. package/.pi/lib/harness-artifact-gate.ts +75 -5
  20. package/.pi/lib/harness-repair-brief.ts +30 -4
  21. package/.pi/lib/harness-run-context.ts +842 -17
  22. package/.pi/lib/harness-schema-validate.ts +147 -38
  23. package/.pi/lib/harness-spawn-policy.ts +9 -0
  24. package/.pi/lib/harness-spawn-topology.ts +109 -7
  25. package/.pi/lib/harness-subagent-precheck.ts +21 -0
  26. package/.pi/lib/harness-subagent-submit-pipeline.ts +95 -21
  27. package/.pi/lib/harness-subagent-submit-register.ts +6 -1
  28. package/.pi/lib/harness-subagents-bridge.ts +3 -0
  29. package/.pi/lib/harness-yaml.ts +11 -3
  30. package/.pi/lib/plan-approval/create-plan.ts +2 -6
  31. package/.pi/lib/plan-debate-gate.ts +87 -0
  32. package/.pi/lib/plan-debate-lane.ts +8 -2
  33. package/.pi/lib/plan-human-gates.ts +404 -0
  34. package/.pi/prompts/harness-clear.md +25 -0
  35. package/.pi/prompts/harness-plan.md +6 -0
  36. package/.pi/prompts/harness-review.md +2 -0
  37. package/.pi/prompts/harness-run.md +4 -3
  38. package/.pi/scripts/generate-agents-policy-yaml.mjs +73 -7
  39. package/.pi/scripts/harness-reconcile-run-context.mjs +62 -0
  40. package/.pi/scripts/harness-schema-compile-verify.mjs +29 -0
  41. package/.pi/scripts/harness-verify.mjs +27 -0
  42. package/CHANGELOG.md +13 -0
  43. package/README.md +4 -0
  44. 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
- await mkdir(dirname(absPath), { recursive: true });
125
- await writeYamlFile(absPath, doc);
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,404 @@
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
+ function logPlanHumanGate(payload: {
32
+ runId: string;
33
+ hypothesisId: string;
34
+ location: string;
35
+ message: string;
36
+ data: Record<string, unknown>;
37
+ }): void {
38
+ // #region agent log
39
+ fetch("http://127.0.0.1:7928/ingest/a5d40896-34cb-4f12-97db-df7ada0b22f0", {
40
+ method: "POST",
41
+ headers: {
42
+ "Content-Type": "application/json",
43
+ "X-Debug-Session-Id": "f7763e",
44
+ },
45
+ body: JSON.stringify({
46
+ sessionId: "f7763e",
47
+ runId: payload.runId,
48
+ hypothesisId: payload.hypothesisId,
49
+ location: payload.location,
50
+ message: payload.message,
51
+ data: payload.data,
52
+ timestamp: Date.now(),
53
+ }),
54
+ }).catch(() => {});
55
+ // #endregion
56
+ }
57
+
58
+ type SessionEntryLike = {
59
+ type?: string;
60
+ customType?: string;
61
+ data?: unknown;
62
+ message?: {
63
+ role?: string;
64
+ toolName?: string;
65
+ details?: unknown;
66
+ content?: string | unknown[];
67
+ };
68
+ };
69
+
70
+ function isNonInteractivePlan(): boolean {
71
+ return (
72
+ process.env.HARNESS_PLAN_NONINTERACTIVE === "1" || isHarnessNonInteractive()
73
+ );
74
+ }
75
+
76
+ function askUserCallWasTaskClarification(details: unknown): boolean {
77
+ if (!details || typeof details !== "object") return false;
78
+ const d = details as { cancelled?: boolean; input?: unknown };
79
+ if (d.cancelled) return false;
80
+ const input = d.input as
81
+ | { question?: string; options?: unknown[]; questions?: unknown[] }
82
+ | undefined;
83
+ if (!input) return true;
84
+ return !isPlanApprovalAskUser(input);
85
+ }
86
+
87
+ export function hasTaskClarificationAskUserSincePlanCommand(
88
+ entries: unknown[],
89
+ ): boolean {
90
+ if (isNonInteractivePlan()) return true;
91
+ const since = Math.max(0, indexOfLastPlanCommand(entries));
92
+ for (let i = since; i < entries.length; i++) {
93
+ const entry = entries[i] as SessionEntryLike;
94
+ if (
95
+ entry.type === "custom" &&
96
+ entry.customType === "harness-task-clarification-engagement"
97
+ ) {
98
+ return true;
99
+ }
100
+ if (entry.type !== "message" || entry.message?.role !== "toolResult") {
101
+ continue;
102
+ }
103
+ if (entry.message.toolName !== "ask_user") continue;
104
+ if (askUserCallWasTaskClarification(entry.message.details)) {
105
+ return true;
106
+ }
107
+ }
108
+ return false;
109
+ }
110
+
111
+ export function hasClarificationFollowUpUserMessage(
112
+ entries: unknown[],
113
+ ): boolean {
114
+ const since = Math.max(0, indexOfLastPlanCommand(entries));
115
+ for (let i = since; i < entries.length; i++) {
116
+ const entry = entries[i] as SessionEntryLike;
117
+ if (entry.type !== "message" || entry.message?.role !== "user") continue;
118
+ const content = entry.message.content;
119
+ const text =
120
+ typeof content === "string"
121
+ ? content.trim()
122
+ : Array.isArray(content)
123
+ ? content
124
+ .filter(
125
+ (c): c is { type: string; text?: string } =>
126
+ typeof c === "object" && c !== null && "type" in c,
127
+ )
128
+ .map((c) => c.text ?? "")
129
+ .join("")
130
+ .trim()
131
+ : "";
132
+ if (!text || text.startsWith("/")) continue;
133
+ return true;
134
+ }
135
+ return false;
136
+ }
137
+
138
+ export function isExplicitTaskAcceptance(taskSummary: string): boolean {
139
+ const t = taskSummary.trim();
140
+ if (t.length < 24) return false;
141
+ return EXPLICIT_ACCEPTANCE_RE.test(t);
142
+ }
143
+
144
+ export interface TaskClarificationHumanGateResult {
145
+ ok: boolean;
146
+ errors: string[];
147
+ }
148
+
149
+ export function validateTaskClarificationHumanGate(
150
+ entries: unknown[],
151
+ doc: Record<string, unknown> | null,
152
+ opts?: {
153
+ quick?: boolean;
154
+ taskSummary?: string;
155
+ allowFollowUpMessage?: boolean;
156
+ },
157
+ ): TaskClarificationHumanGateResult {
158
+ const errors: string[] = [];
159
+ const status = String(doc?.status ?? "").toLowerCase();
160
+ if (status !== "ready") {
161
+ return { ok: true, errors };
162
+ }
163
+
164
+ const engagement = doc?.user_engagement as { source?: string } | undefined;
165
+ if (engagement?.source === "ask_user") {
166
+ return { ok: true, errors };
167
+ }
168
+
169
+ if (hasTaskClarificationAskUserSincePlanCommand(entries)) {
170
+ return { ok: true, errors };
171
+ }
172
+
173
+ if (
174
+ opts?.allowFollowUpMessage &&
175
+ hasClarificationFollowUpUserMessage(entries)
176
+ ) {
177
+ return { ok: true, errors };
178
+ }
179
+
180
+ if (opts?.quick && isExplicitTaskAcceptance(opts.taskSummary ?? "")) {
181
+ return { ok: true, errors };
182
+ }
183
+
184
+ errors.push(
185
+ "Phase 0 requires ask_user before task-clarification status: ready. Call ask_user (harness-decisions skill), merge answers, then harness_artifact_ready.",
186
+ );
187
+ return { ok: false, errors };
188
+ }
189
+
190
+ async function fileExists(path: string): Promise<boolean> {
191
+ try {
192
+ await access(path, constants.R_OK);
193
+ return true;
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+
199
+ export interface PlanHumanGateStatus {
200
+ phase0Ready: boolean;
201
+ phase0NeedsAskUser: boolean;
202
+ debateComplete: boolean;
203
+ debateRequired: boolean;
204
+ approvalRequired: boolean;
205
+ approvalRecorded: boolean;
206
+ nextRequiredAction: string | null;
207
+ /** Actionable Review Gate recovery when debateRequired. */
208
+ debateRecoveryHint: string | null;
209
+ }
210
+
211
+ export async function resolvePlanHumanGateStatus(
212
+ projectRoot: string,
213
+ runId: string,
214
+ entries: unknown[],
215
+ opts?: { quick?: boolean; taskSummary?: string; lastOutcome?: string | null },
216
+ ): Promise<PlanHumanGateStatus> {
217
+ const runDir = join(projectRoot, ".pi", "harness", "runs", runId);
218
+ const clar = await isTaskClarificationReady(runDir);
219
+ const clarDoc = clar.ok ? await readTaskClarificationDoc(runDir) : null;
220
+ logPlanHumanGate({
221
+ runId,
222
+ hypothesisId: "H3",
223
+ location: "plan-human-gates.ts:resolvePlanHumanGateStatus:clar",
224
+ message: "Task clarification readiness evaluated",
225
+ data: {
226
+ runDir,
227
+ clarOk: clar.ok,
228
+ clarErrors: clar.errors,
229
+ docStatus: String(clarDoc?.status ?? ""),
230
+ docEngagementSource:
231
+ typeof clarDoc?.user_engagement === "object" &&
232
+ clarDoc?.user_engagement !== null
233
+ ? String(
234
+ (
235
+ clarDoc.user_engagement as {
236
+ source?: string;
237
+ }
238
+ ).source ?? "",
239
+ )
240
+ : "",
241
+ },
242
+ });
243
+ const humanGate = validateTaskClarificationHumanGate(entries, clarDoc, {
244
+ quick: opts?.quick,
245
+ taskSummary: opts?.taskSummary,
246
+ allowFollowUpMessage: opts?.lastOutcome === "needs_clarification",
247
+ });
248
+ logPlanHumanGate({
249
+ runId,
250
+ hypothesisId: "H1-H2",
251
+ location: "plan-human-gates.ts:resolvePlanHumanGateStatus:humanGate",
252
+ message: "Human gate evaluated for phase0 ask_user requirement",
253
+ data: {
254
+ humanGateOk: humanGate.ok,
255
+ humanGateErrors: humanGate.errors,
256
+ allowFollowUpMessage: opts?.lastOutcome === "needs_clarification",
257
+ hasTaskClarificationAskUserSincePlanCommand:
258
+ hasTaskClarificationAskUserSincePlanCommand(entries),
259
+ hasClarificationFollowUpUserMessage:
260
+ hasClarificationFollowUpUserMessage(entries),
261
+ indexOfLastPlanCommand: indexOfLastPlanCommand(entries),
262
+ entriesLen: entries.length,
263
+ },
264
+ });
265
+ const phase0Ready = clar.ok && humanGate.ok;
266
+ const phase0NeedsAskUser = clar.ok && !humanGate.ok;
267
+ const approvalRecorded = hasPlanUserApproval(entries, {
268
+ sincePlanCommand: true,
269
+ });
270
+ const dagPath = join(runDir, "plan-packet.yaml");
271
+ const hasPacket = await fileExists(dagPath);
272
+ const messengerPath = join(runDir, "debate-messenger", "state.json");
273
+ const debateOpened = await fileExists(messengerPath);
274
+
275
+ let debateComplete = true;
276
+ let debateGate = null;
277
+ let readinessOk = false;
278
+ let approvalRequired = false;
279
+
280
+ if (phase0Ready && !approvalRecorded) {
281
+ const readiness = await validatePlanApprovalReadiness(projectRoot, runId, {
282
+ risk_level: String(clarDoc?.risk_level ?? "med"),
283
+ quick: opts?.quick,
284
+ });
285
+ readinessOk = readiness.ok;
286
+ debateGate = await validatePlanDebateGate(projectRoot, runId);
287
+ debateComplete = debateGate.ok;
288
+ approvalRequired = readiness.ok && debateComplete && hasPacket;
289
+ }
290
+
291
+ const debateRequired =
292
+ phase0Ready &&
293
+ !debateComplete &&
294
+ !approvalRecorded &&
295
+ (debateOpened || hasPacket);
296
+
297
+ let debateRecoveryHint: string | null = null;
298
+ let nextRequiredAction: string | null = null;
299
+ if (!phase0Ready) {
300
+ nextRequiredAction = phase0NeedsAskUser
301
+ ? "ask_user (Phase 0 task contract)"
302
+ : "complete artifacts/task-clarification.yaml (Phase 0)";
303
+ } else if (debateRequired && debateGate) {
304
+ debateRecoveryHint = await buildPlanDebateGateRecovery(
305
+ projectRoot,
306
+ runId,
307
+ debateGate,
308
+ );
309
+ nextRequiredAction =
310
+ "Complete Review Gate (debate rounds + harness_debate_consensus) before approve_plan";
311
+ } else if (approvalRequired && !approvalRecorded) {
312
+ nextRequiredAction = "approve_plan then create_plan (Phase 6)";
313
+ }
314
+ logPlanHumanGate({
315
+ runId,
316
+ hypothesisId: "H4",
317
+ location: "plan-human-gates.ts:resolvePlanHumanGateStatus:result",
318
+ message: "Resolved plan human gate status",
319
+ data: {
320
+ phase0Ready,
321
+ phase0NeedsAskUser,
322
+ debateComplete,
323
+ debateRequired,
324
+ approvalRequired,
325
+ approvalRecorded,
326
+ nextRequiredAction,
327
+ },
328
+ });
329
+
330
+ return {
331
+ phase0Ready,
332
+ phase0NeedsAskUser,
333
+ debateComplete,
334
+ debateRequired,
335
+ approvalRequired,
336
+ approvalRecorded,
337
+ nextRequiredAction,
338
+ debateRecoveryHint,
339
+ };
340
+ }
341
+
342
+ export function formatPlanHumanGateBlock(status: PlanHumanGateStatus): string {
343
+ if (!status.nextRequiredAction) return "";
344
+ const lines = [
345
+ "[HarnessPlanGate]",
346
+ `next_required_action=${status.nextRequiredAction}`,
347
+ `phase0_ready=${status.phase0Ready}`,
348
+ `review_gate_complete=${status.debateComplete}`,
349
+ `review_gate_required=${status.debateRequired}`,
350
+ `plan_approval_required=${status.approvalRequired}`,
351
+ `plan_approval_recorded=${status.approvalRecorded}`,
352
+ ];
353
+ if (status.debateRequired) {
354
+ lines.push(
355
+ "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).",
356
+ );
357
+ } else {
358
+ lines.push(
359
+ "Do not spawn planning subagents or end this turn until the required human step completes.",
360
+ );
361
+ }
362
+ if (status.debateRecoveryHint) {
363
+ lines.push("", status.debateRecoveryHint);
364
+ }
365
+ return lines.join("\n");
366
+ }
367
+
368
+ export async function shouldBlockSubagentForMissingPlanApproval(
369
+ projectRoot: string,
370
+ runId: string,
371
+ entries: unknown[],
372
+ phase: string,
373
+ ): Promise<{ block: boolean; reason?: string }> {
374
+ if (phase !== "plan" || isNonInteractivePlan()) return { block: false };
375
+ if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
376
+ return { block: false };
377
+ }
378
+ const status = await resolvePlanHumanGateStatus(projectRoot, runId, entries);
379
+ if (!status.approvalRequired) return { block: false };
380
+ return {
381
+ block: true,
382
+ reason:
383
+ "Plan Review Gate is complete but user approval is missing. Call approve_plan (then create_plan) before further subagent work.",
384
+ };
385
+ }
386
+
387
+ export async function validateTaskClarificationReadyWithHumanGate(
388
+ runDir: string,
389
+ entries: unknown[],
390
+ opts?: { quick?: boolean; taskSummary?: string; lastOutcome?: string | null },
391
+ ): Promise<TaskClarificationReadiness & { humanErrors: string[] }> {
392
+ const doc = await readTaskClarificationDoc(runDir);
393
+ const base = validateTaskClarificationDoc(doc, { requireReady: true });
394
+ const human = validateTaskClarificationHumanGate(entries, doc, {
395
+ quick: opts?.quick,
396
+ taskSummary: opts?.taskSummary,
397
+ allowFollowUpMessage: opts?.lastOutcome === "needs_clarification",
398
+ });
399
+ return {
400
+ ok: base.ok && human.ok,
401
+ errors: [...base.errors, ...human.errors],
402
+ humanErrors: human.errors,
403
+ };
404
+ }
@@ -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
@@ -184,6 +188,8 @@ subagent({ agentScope: "both", agent: "harness/planning/execution-plan-author",
184
188
 
185
189
  Merge `execution_plan` into draft `plan-packet.yaml` (`write_harness_yaml`). Save `artifacts/execution-plan-draft.yaml` the same way.
186
190
 
191
+ The `execution_plan` must make testing expectations explicit: decide whether unit, integration, and e2e/end-to-end tests are applicable for each changed surface based on risk and implementation scope; add work items/done criteria to create or update applicable tests; list relevant verification commands; and record a short rationale when a test level is not applicable. Do not hard-require all three test levels for every change — make the applicability decision visible.
192
+
187
193
  ## Phase 4c — Deterministic quality gate (hard stop)
188
194
 
189
195
  **Practice:** Harness engineering — never trust the model for graph validity.
@@ -75,6 +75,8 @@ Ensure `artifacts/ls-lint-signal.yaml` exists (from `/harness-run` or write from
75
75
 
76
76
  Run project tests if the approved `PlanPacket` or spawn context lists a test command. Capture stdout paths only — do not paste full logs into the next spawn.
77
77
 
78
+ Verify the testing obligation itself: the approved `PlanPacket` or spawn context must show planned applicability decisions for unit, integration, and e2e/end-to-end tests, and executor evidence must show applicable tests were implemented or updated and run. If a test level was not applicable, require a clear rationale tied to risk and changed surface; missing planned or executed applicable testing is a benchmark failure.
79
+
78
80
  Write `artifacts/benchmark-log.yaml` via `write_harness_yaml` when any shell step ran:
79
81
 
80
82
  ```yaml
@@ -52,14 +52,15 @@ Note `violation_count` in run notes (do not block execute on pre-existing violat
52
52
  1. Confirm `[HarnessActivePlan]` / extension reports plan ready.
53
53
  2. Build `HarnessSpawnContext` with `mode: execute`, `plan_packet_path`, `run_dir`, `acceptance_checks` from plan file.
54
54
  3. Include **`critical_path_work_item_ids`** from `execution_plan.schedule_metadata` in spawn task when present — executor should tackle limiting-step items first (Grove).
55
- 4. Spawn (max **1** agent per call):
55
+ 4. Include the plan's testing expectations in the spawn task: the executor must implement or update applicable unit, integration, and e2e/end-to-end tests, run the relevant verification commands, and report command evidence or a rationale for any non-applicable test level in `validation_summary`.
56
+ 5. Spawn (max **1** agent per call):
56
57
 
57
58
  ```
58
59
  subagent({ agentScope: "both", agent: "harness/running/executor", task: "<HarnessSpawnContext + handoff + critical path hint>" })
59
60
  ```
60
61
 
61
- 5. Parse subprocess output JSON (`execution_status`, validations, rollback refs) from tool result text.
62
- 6. Parent persists trace/handoff artifacts under run dir if needed; do not self-review.
62
+ 6. Parse subprocess output JSON (`execution_status`, validations, rollback refs) from tool result text.
63
+ 7. Parent persists trace/handoff artifacts under run dir if needed; do not self-review.
63
64
 
64
65
  ## Post-work — Structural observation (parent)
65
66