ultimate-pi 0.19.1 → 0.22.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 (147) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +68 -2
  2. package/.agents/skills/harness-git-commit/SKILL.md +72 -0
  3. package/.agents/skills/harness-governor/SKILL.md +2 -2
  4. package/.agents/skills/harness-ls-lint-setup/SKILL.md +59 -0
  5. package/.agents/skills/harness-plan/SKILL.md +13 -11
  6. package/.agents/skills/harness-review/SKILL.md +1 -1
  7. package/.agents/skills/harness-sentrux-repair/SKILL.md +48 -0
  8. package/.agents/skills/sentrux/SKILL.md +4 -2
  9. package/.agents/skills/wiki-save/SKILL.md +1 -1
  10. package/.pi/PACKAGING.md +6 -0
  11. package/.pi/SYSTEM.md +21 -3
  12. package/.pi/agents/harness/ls-lint-steward.md +49 -0
  13. package/.pi/agents/harness/planning/decompose.md +4 -4
  14. package/.pi/agents/harness/reviewing/evaluator.md +1 -1
  15. package/.pi/agents/harness/running/executor.md +43 -2
  16. package/.pi/agents/harness/sentrux-repair-advisor.md +50 -0
  17. package/.pi/agents/pi-pi/prompt-expert.md +17 -2
  18. package/.pi/auto-commit.json +9 -2
  19. package/.pi/extensions/debate-orchestrator.ts +3 -0
  20. package/.pi/extensions/harness-anchored-edit.ts +139 -0
  21. package/.pi/extensions/harness-ask-user.ts +13 -34
  22. package/.pi/extensions/harness-debate-tools.ts +43 -4
  23. package/.pi/extensions/harness-live-widget.ts +28 -19
  24. package/.pi/extensions/harness-run-context.ts +278 -115
  25. package/.pi/extensions/harness-web-tools.ts +598 -471
  26. package/.pi/extensions/ls-lint-rules-sync.ts +103 -0
  27. package/.pi/extensions/observation-bus.ts +4 -0
  28. package/.pi/extensions/policy-gate.ts +270 -229
  29. package/.pi/extensions/sentrux-rules-sync.ts +2 -0
  30. package/.pi/extensions/soundboard.ts +48 -48
  31. package/.pi/harness/README.md +4 -0
  32. package/.pi/harness/agents.manifest.json +15 -7
  33. package/.pi/harness/agents.policy.yaml +47 -81
  34. package/.pi/harness/docs/adrs/0051-hash-anchored-executor-edits.md +41 -0
  35. package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
  36. package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
  37. package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
  38. package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
  39. package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
  40. package/.pi/harness/docs/adrs/README.md +7 -0
  41. package/.pi/harness/docs/practice-map.md +21 -5
  42. package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
  43. package/.pi/harness/evolution/self-healing-rules.json +16 -0
  44. package/.pi/harness/ls-lint/naming.manifest.json +128 -0
  45. package/.pi/harness/sentrux/architecture.manifest.json +1 -1
  46. package/.pi/harness/specs/auto-commit.schema.json +63 -0
  47. package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
  48. package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
  49. package/.pi/harness/specs/naming-manifest.schema.json +54 -0
  50. package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
  51. package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
  52. package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
  53. package/.pi/harness/specs/sentrux-report.schema.json +119 -0
  54. package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
  55. package/.pi/lib/agents-policy.d.mts +26 -47
  56. package/.pi/lib/agents-policy.mjs +84 -29
  57. package/.pi/lib/agents-policy.ts +1 -0
  58. package/.pi/lib/agt/build-evaluation-context.ts +136 -64
  59. package/.pi/lib/ask-user/constants.mjs +3 -0
  60. package/.pi/lib/ask-user/constants.ts +4 -0
  61. package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
  62. package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
  63. package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
  64. package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
  65. package/.pi/lib/ask-user/dialog.ts +2 -314
  66. package/.pi/lib/ask-user/fallback.ts +2 -78
  67. package/.pi/lib/ask-user/format.ts +85 -0
  68. package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
  69. package/.pi/lib/ask-user/index.ts +114 -0
  70. package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
  71. package/.pi/lib/ask-user/policy.mjs +43 -0
  72. package/.pi/lib/ask-user/policy.ts +104 -0
  73. package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
  74. package/.pi/lib/ask-user/presenters/headless.ts +131 -0
  75. package/.pi/lib/ask-user/presenters/select.ts +60 -0
  76. package/.pi/lib/ask-user/presenters/tui.ts +373 -0
  77. package/.pi/lib/ask-user/presenters/types.ts +13 -0
  78. package/.pi/lib/ask-user/render.ts +40 -9
  79. package/.pi/lib/ask-user/schema.ts +66 -13
  80. package/.pi/lib/ask-user/types.ts +60 -3
  81. package/.pi/lib/ask-user/validate-core.mjs +193 -7
  82. package/.pi/lib/ask-user/validate.ts +53 -34
  83. package/.pi/lib/harness-anchored-edit/.hash_anchors +1721 -0
  84. package/.pi/lib/harness-anchored-edit/anchor-state.ts +320 -0
  85. package/.pi/lib/harness-anchored-edit/apply-anchored-edits.ts +161 -0
  86. package/.pi/lib/harness-anchored-edit/edit-executor.ts +146 -0
  87. package/.pi/lib/harness-anchored-edit/index.ts +9 -0
  88. package/.pi/lib/harness-anchored-edit/line-protocol.ts +38 -0
  89. package/.pi/lib/harness-anchored-edit/package.json +3 -0
  90. package/.pi/lib/harness-anchored-edit/settings.ts +1 -0
  91. package/.pi/lib/harness-anchored-edit/task-id.ts +8 -0
  92. package/.pi/lib/harness-anchored-edit/types.ts +19 -0
  93. package/.pi/lib/harness-artifact-gate.ts +75 -21
  94. package/.pi/lib/harness-auto-commit-config.mjs +321 -0
  95. package/.pi/lib/harness-lens/clients/anchored-edit-autopatch.ts +158 -0
  96. package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
  97. package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
  98. package/.pi/lib/harness-lens/index.ts +246 -96
  99. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
  100. package/.pi/lib/harness-repair-brief.ts +84 -25
  101. package/.pi/lib/harness-run-context.ts +42 -52
  102. package/.pi/lib/harness-sentrux-parse.mjs +272 -0
  103. package/.pi/lib/harness-sentrux-root.mjs +78 -0
  104. package/.pi/lib/harness-slash-completions.ts +116 -0
  105. package/.pi/lib/harness-spawn-topology.ts +121 -87
  106. package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
  107. package/.pi/lib/harness-subagents-bridge.ts +11 -6
  108. package/.pi/lib/harness-ui-state.ts +95 -48
  109. package/.pi/lib/plan-approval/dialog.ts +5 -0
  110. package/.pi/lib/plan-approval/validate.ts +1 -1
  111. package/.pi/lib/plan-approval-readiness.ts +32 -0
  112. package/.pi/lib/plan-debate-gate.ts +154 -114
  113. package/.pi/lib/plan-task-clarification.ts +158 -0
  114. package/.pi/prompts/harness-auto.md +2 -2
  115. package/.pi/prompts/harness-ls-lint-steward.md +43 -0
  116. package/.pi/prompts/harness-plan.md +58 -8
  117. package/.pi/prompts/harness-review.md +40 -6
  118. package/.pi/prompts/harness-run.md +33 -11
  119. package/.pi/prompts/harness-setup.md +72 -3
  120. package/.pi/prompts/harness-steer.md +3 -2
  121. package/.pi/prompts/wiki-save.md +5 -4
  122. package/.pi/scripts/README.md +8 -0
  123. package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
  124. package/.pi/scripts/harness-anchored-edit-smoke.mjs +45 -0
  125. package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
  126. package/.pi/scripts/harness-cli-verify.sh +47 -0
  127. package/.pi/scripts/harness-git-churn.mjs +77 -0
  128. package/.pi/scripts/harness-git-commit.mjs +173 -0
  129. package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
  130. package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
  131. package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
  132. package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
  133. package/.pi/scripts/harness-sentrux-report.mjs +256 -0
  134. package/.pi/scripts/harness-verify.mjs +347 -117
  135. package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
  136. package/.pi/scripts/run-tests.mjs +65 -0
  137. package/.pi/settings.example.json +1 -0
  138. package/.sentrux/rules.toml +1 -1
  139. package/AGENTS.md +1 -0
  140. package/CHANGELOG.md +31 -0
  141. package/README.md +13 -4
  142. package/THIRD_PARTY_NOTICES.md +7 -0
  143. package/package.json +8 -3
  144. package/vendor/pi-subagents/src/agents.ts +5 -0
  145. package/vendor/pi-subagents/src/subagents.ts +22 -3
  146. package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
  147. package/.pi/scripts/release.sh +0 -338
@@ -31,10 +31,26 @@ const MUTATING_TOOLS = new Set(["write", "edit"]);
31
31
 
32
32
  const cache = new Map();
33
33
 
34
+ const EXTENSION_BUNDLE_MODULES = {
35
+ executor: [
36
+ "subagent-governance.ts",
37
+ "harness-anchored-edit.ts",
38
+ "harness-lens.ts",
39
+ ],
40
+ };
41
+
34
42
  export function packageAgentsPolicyPath(packageRoot) {
35
43
  return join(packageRoot, ".pi", "harness", "agents.policy.yaml");
36
44
  }
37
45
 
46
+ /** Absolute paths for subprocess `-e` loads (curated; avoids parent-only extensions). */
47
+ export function resolveExtensionBundlePaths(packageRoot, bundleName) {
48
+ const modules = EXTENSION_BUNDLE_MODULES[bundleName];
49
+ if (!modules) return [];
50
+ const extDir = join(packageRoot, ".pi", "extensions");
51
+ return modules.map((name) => join(extDir, name));
52
+ }
53
+
38
54
  export function projectAgentsPolicyPath(projectRoot) {
39
55
  return join(projectRoot, ".pi", "agents.policy.yaml");
40
56
  }
@@ -52,12 +68,19 @@ function readYamlFile(path) {
52
68
  }
53
69
  }
54
70
 
71
+ function normalizeExtensionBundle(raw) {
72
+ if (typeof raw.extension_bundle !== "string") return undefined;
73
+ const bundle = raw.extension_bundle.trim();
74
+ return bundle.length > 0 ? bundle : undefined;
75
+ }
76
+
55
77
  function normalizeKindEntry(raw) {
56
78
  if (!raw || typeof raw !== "object") return null;
57
79
  const tools = Array.isArray(raw.tools) ? raw.tools.map(String) : [];
58
80
  return {
59
81
  tools,
60
82
  extensions: raw.extensions === false ? false : Boolean(raw.extensions),
83
+ extensionBundle: normalizeExtensionBundle(raw),
61
84
  readOnly: raw.read_only === true,
62
85
  maxTurns:
63
86
  typeof raw.max_turns === "number" && raw.max_turns > 0
@@ -93,6 +116,7 @@ function normalizeAgentEntry(raw) {
93
116
  : raw.extensions === true
94
117
  ? true
95
118
  : undefined,
119
+ extensionBundle: normalizeExtensionBundle(raw),
96
120
  maxTurns:
97
121
  typeof raw.max_turns === "number" && raw.max_turns > 0
98
122
  ? raw.max_turns
@@ -162,10 +186,25 @@ export function resolveEffectiveTools(agentId, merged) {
162
186
  for (const t of entry.toolsDeny ?? []) base.delete(t);
163
187
  for (const t of BUILTIN_DENY_TOOLS) base.delete(t);
164
188
 
189
+ const extensionBundle =
190
+ entry.extensionBundle ?? kind.extensionBundle ?? undefined;
191
+ const extensionsFull =
192
+ !extensionBundle &&
193
+ (entry.extensions === true
194
+ ? true
195
+ : entry.extensions === false
196
+ ? false
197
+ : Boolean(kind.extensions));
198
+ const extensionsOff = !extensionsFull;
199
+
165
200
  return {
166
201
  kind: kindName,
167
202
  effectiveTools: [...base],
168
- extensionsOff: entry.extensions === true ? false : entry.extensions === false ? true : !kind.extensions,
203
+ extensionsOff,
204
+ extensionBundle,
205
+ extensionsFull,
206
+ /** Suppress Pi builtins when harness read/edit register (full extensions or subprocess bundle). */
207
+ noBuiltinTools: extensionsFull || Boolean(extensionBundle),
169
208
  readOnly: kind.readOnly,
170
209
  maxTurns: entry.maxTurns ?? kind.maxTurns,
171
210
  thinking: entry.thinking ?? kind.thinking,
@@ -223,7 +262,45 @@ function isMutatingBash(command) {
223
262
  command,
224
263
  );
225
264
  }
265
+ function deniesReadOnlyBatchExecute(toolInput) {
266
+ const commands = toolInput.commands;
267
+ if (!Array.isArray(commands)) return false;
268
+ for (const c of commands) {
269
+ const cmd =
270
+ typeof c === "string" ? c : String(c?.command ?? c?.code ?? "");
271
+ if (cmd && isMutatingBash(cmd)) return true;
272
+ }
273
+ return false;
274
+ }
275
+
276
+ function deniesReadOnlyExecute(toolInput) {
277
+ const code = String(toolInput.code ?? toolInput.command ?? "");
278
+ return Boolean(code && isMutatingBash(code));
279
+ }
226
280
 
281
+ function deniesReadOnlyBash(agentId, toolInput) {
282
+ const command = String(toolInput.command ?? "");
283
+ if (command && isMutatingBash(command)) return true;
284
+ if (
285
+ isHarnessPlanningAgent(agentId) &&
286
+ command &&
287
+ PLANNING_ARTIFACT_JSON_WRITE.test(command)
288
+ ) {
289
+ return true;
290
+ }
291
+ if (
292
+ isHarnessPlanningAgent(agentId) &&
293
+ command &&
294
+ PLANNING_BASH_DENY_PATTERNS.some((p) => p.test(command))
295
+ ) {
296
+ return true;
297
+ }
298
+ return false;
299
+ }
300
+
301
+ /**
302
+ * Manifest allowlist + subprocess constraints (replaces harness-subagent-policy.ts).
303
+ */
227
304
  /**
228
305
  * Manifest allowlist + subprocess constraints (replaces harness-subagent-policy.ts).
229
306
  */
@@ -258,40 +335,15 @@ export function allowsAgentTool(input) {
258
335
  if (MUTATING_TOOLS.has(toolName) && spec.readOnly) return false;
259
336
 
260
337
  if (toolName === "ctx_batch_execute" && spec.readOnly) {
261
- const commands = toolInput.commands;
262
- if (Array.isArray(commands)) {
263
- for (const c of commands) {
264
- const cmd =
265
- typeof c === "string"
266
- ? c
267
- : String(c?.command ?? c?.code ?? "");
268
- if (cmd && isMutatingBash(cmd)) return false;
269
- }
270
- }
338
+ if (deniesReadOnlyBatchExecute(toolInput)) return false;
271
339
  }
272
340
 
273
341
  if (toolName === "ctx_execute" && spec.readOnly) {
274
- const code = String(toolInput.code ?? toolInput.command ?? "");
275
- if (code && isMutatingBash(code)) return false;
342
+ if (deniesReadOnlyExecute(toolInput)) return false;
276
343
  }
277
344
 
278
345
  if (toolName === "bash" && spec.readOnly) {
279
- const command = String(toolInput.command ?? "");
280
- if (command && isMutatingBash(command)) return false;
281
- if (
282
- isHarnessPlanningAgent(agentId) &&
283
- command &&
284
- PLANNING_ARTIFACT_JSON_WRITE.test(command)
285
- ) {
286
- return false;
287
- }
288
- if (
289
- isHarnessPlanningAgent(agentId) &&
290
- command &&
291
- PLANNING_BASH_DENY_PATTERNS.some((p) => p.test(command))
292
- ) {
293
- return false;
294
- }
346
+ if (deniesReadOnlyBash(agentId, toolInput)) return false;
295
347
  }
296
348
 
297
349
  return true;
@@ -304,6 +356,9 @@ export function applyAgentPolicyToConfig(agent, packageRoot, projectRoot) {
304
356
  ...agent,
305
357
  tools: spec.effectiveTools.length > 0 ? spec.effectiveTools : undefined,
306
358
  extensionsOff: spec.extensionsOff,
359
+ extensionBundle: spec.extensionBundle,
360
+ extensionsFull: spec.extensionsFull,
361
+ noBuiltinTools: spec.noBuiltinTools,
307
362
  maxTurns: spec.maxTurns ?? agent.maxTurns,
308
363
  thinking: spec.thinking ?? agent.thinking,
309
364
  model: spec.model ?? agent.model,
@@ -16,4 +16,5 @@ export {
16
16
  projectAgentsPolicyPath,
17
17
  projectPoliciesDir,
18
18
  resolveEffectiveTools,
19
+ resolveExtensionBundlePaths,
19
20
  } from "./agents-policy.mjs";
@@ -125,6 +125,106 @@ function bashPlanningJsonDenied(command: string, agentType: string): boolean {
125
125
  return PLANNING_ARTIFACT_JSON_WRITE.test(command);
126
126
  }
127
127
 
128
+ function isReadOnlyAgentKind(agentKind: string): boolean {
129
+ return (
130
+ agentKind === "planner" ||
131
+ agentKind === "evaluator" ||
132
+ agentKind === "adversary" ||
133
+ agentKind === "tie_breaker" ||
134
+ agentKind === "trace" ||
135
+ agentKind === "incident" ||
136
+ agentKind === "meta"
137
+ );
138
+ }
139
+
140
+ async function resolvePlanMutation(args: {
141
+ sessionActive: boolean;
142
+ toolName: string;
143
+ toolInput: Record<string, unknown>;
144
+ phase: HarnessPhase;
145
+ runCtx: HarnessRunContext | null;
146
+ projectRoot: string;
147
+ aborted: boolean;
148
+ entries: unknown[];
149
+ sessionId: string;
150
+ }): Promise<{ allowed: boolean; reason?: string }> {
151
+ const mutatingTool = args.toolName === "write" || args.toolName === "edit";
152
+ if (!(args.sessionActive && mutatingTool)) return { allowed: true };
153
+ return isPlanPhaseAllowedMutation(
154
+ args.toolName,
155
+ args.toolInput,
156
+ args.phase,
157
+ args.runCtx,
158
+ args.projectRoot,
159
+ {
160
+ aborted: args.aborted,
161
+ entries: args.entries,
162
+ ownerSessionId: args.runCtx?.owner_pi_session_id,
163
+ currentSessionId: args.sessionId,
164
+ },
165
+ );
166
+ }
167
+
168
+ function resolveContextMode(args: {
169
+ sessionActive: boolean;
170
+ toolName: string;
171
+ toolInput: Record<string, unknown>;
172
+ phase: HarnessPhase;
173
+ aborted: boolean;
174
+ budgetBypass: boolean;
175
+ agentKind: string;
176
+ }): { blocked: boolean; reason?: string } {
177
+ if (!args.sessionActive) return { blocked: false, reason: "" };
178
+ return evaluateContextModeMutation(
179
+ args.toolName,
180
+ args.toolInput,
181
+ args.phase,
182
+ {
183
+ aborted: args.aborted,
184
+ budgetBypass: args.budgetBypass,
185
+ readOnlyAgent: isReadOnlyAgentKind(args.agentKind),
186
+ },
187
+ );
188
+ }
189
+
190
+ function shouldBlockEvalPlanPacketWrite(args: {
191
+ sessionActive: boolean;
192
+ runCtx: HarnessRunContext | null;
193
+ phase: HarnessPhase;
194
+ toolName: string;
195
+ toolInput: Record<string, unknown>;
196
+ }): boolean {
197
+ if (!args.sessionActive || !args.runCtx?.plan_packet_path) return false;
198
+ if (args.phase !== "evaluate" && args.phase !== "adversary") return false;
199
+ if (args.toolName !== "write" && args.toolName !== "edit") return false;
200
+ const target = String(args.toolInput.path ?? args.toolInput.filePath ?? "");
201
+ return target.includes("plan-packet.yaml");
202
+ }
203
+
204
+ function resolveMutatingBashFlags(args: {
205
+ sessionActive: boolean;
206
+ bashCommand: string;
207
+ aborted: boolean;
208
+ phase: HarnessPhase;
209
+ toolName: string;
210
+ }): { mutatingBashPhaseBlock: boolean; abortMutatingBlock: boolean } {
211
+ const isMutating = Boolean(
212
+ args.bashCommand && isMutatingBash(args.bashCommand),
213
+ );
214
+ return {
215
+ mutatingBashPhaseBlock:
216
+ args.sessionActive &&
217
+ isMutating &&
218
+ !args.aborted &&
219
+ args.phase !== "execute" &&
220
+ args.phase !== "merge",
221
+ abortMutatingBlock:
222
+ args.sessionActive &&
223
+ args.aborted &&
224
+ (isMutating || args.toolName === "write" || args.toolName === "edit"),
225
+ };
226
+ }
227
+
128
228
  function harnessSessionActive(entries: unknown[]): boolean {
129
229
  return isHarnessAutoSession(entries);
130
230
  }
@@ -142,35 +242,27 @@ export async function buildHarnessAgtEvaluationContext(
142
242
  input.toolName === "bash" ? String(input.toolInput.command ?? "") : "";
143
243
  const sessionActive = harnessSessionActive(input.entries);
144
244
 
145
- const MUTATING_FILE_TOOLS = new Set(["write", "edit"]);
146
- const planMutation =
147
- sessionActive && MUTATING_FILE_TOOLS.has(input.toolName)
148
- ? await isPlanPhaseAllowedMutation(
149
- input.toolName,
150
- input.toolInput,
151
- phase,
152
- runCtx,
153
- input.projectRoot,
154
- {
155
- aborted: input.policyState.aborted,
156
- entries: input.entries,
157
- ownerSessionId: runCtx?.owner_pi_session_id,
158
- currentSessionId: input.sessionId,
159
- },
160
- )
161
- : { allowed: true };
245
+ const planMutation = await resolvePlanMutation({
246
+ sessionActive,
247
+ toolName: input.toolName,
248
+ toolInput: input.toolInput,
249
+ phase,
250
+ runCtx,
251
+ projectRoot: input.projectRoot,
252
+ aborted: input.policyState.aborted,
253
+ entries: input.entries,
254
+ sessionId: input.sessionId,
255
+ });
162
256
 
163
- const ctxMode = sessionActive
164
- ? evaluateContextModeMutation(input.toolName, input.toolInput, phase, {
165
- aborted: input.policyState.aborted,
166
- budgetBypass: input.policyState.budgetBypass,
167
- readOnlyAgent:
168
- agentKind === "planner" ||
169
- agentKind === "evaluator" ||
170
- agentKind === "adversary" ||
171
- agentKind === "tie_breaker",
172
- })
173
- : { blocked: false, reason: "" };
257
+ const ctxMode = resolveContextMode({
258
+ sessionActive,
259
+ toolName: input.toolName,
260
+ toolInput: input.toolInput,
261
+ phase,
262
+ aborted: input.policyState.aborted,
263
+ budgetBypass: input.policyState.budgetBypass,
264
+ agentKind,
265
+ });
174
266
 
175
267
  const spawnDecision = evaluateSubagentToolCall(input.toolName, agentId);
176
268
 
@@ -184,40 +276,27 @@ export async function buildHarnessAgtEvaluationContext(
184
276
  isParentOrchestrator,
185
277
  });
186
278
 
187
- let evalPlanPacketBlock = false;
188
- if (
189
- sessionActive &&
190
- runCtx?.plan_packet_path &&
191
- (phase === "evaluate" || phase === "adversary") &&
192
- (input.toolName === "write" || input.toolName === "edit")
193
- ) {
194
- const target = String(
195
- input.toolInput.path ?? input.toolInput.filePath ?? "",
196
- );
197
- if (target.includes("plan-packet.yaml")) {
198
- evalPlanPacketBlock = true;
199
- }
200
- }
279
+ const evalPlanPacketBlock = shouldBlockEvalPlanPacketWrite({
280
+ sessionActive,
281
+ runCtx,
282
+ phase,
283
+ toolName: input.toolName,
284
+ toolInput: input.toolInput,
285
+ });
201
286
 
202
287
  const writePath =
203
288
  input.toolName === "write" || input.toolName === "edit"
204
289
  ? extractWritePathFromToolInput(input.toolInput)
205
290
  : null;
206
291
 
207
- const mutatingBashPhaseBlock =
208
- sessionActive &&
209
- Boolean(bashCommand && isMutatingBash(bashCommand)) &&
210
- !input.policyState.aborted &&
211
- phase !== "execute" &&
212
- phase !== "merge";
213
-
214
- const abortMutatingBlock =
215
- sessionActive &&
216
- input.policyState.aborted &&
217
- ((bashCommand && isMutatingBash(bashCommand)) ||
218
- input.toolName === "write" ||
219
- input.toolName === "edit");
220
-
292
+ const { mutatingBashPhaseBlock, abortMutatingBlock } =
293
+ resolveMutatingBashFlags({
294
+ sessionActive,
295
+ bashCommand,
296
+ aborted: input.policyState.aborted,
297
+ phase,
298
+ toolName: input.toolName,
299
+ });
221
300
  return {
222
301
  tool_name: input.toolName,
223
302
  harness_phase: phase,
@@ -249,14 +328,7 @@ export async function buildHarnessAgtEvaluationContext(
249
328
  eval_plan_packet_write_block: evalPlanPacketBlock,
250
329
  is_submit_tool: input.toolName.startsWith("submit_"),
251
330
  is_planning_agent: isHarnessPlanningAgent(agentId),
252
- is_read_only_kind:
253
- agentKind === "planner" ||
254
- agentKind === "evaluator" ||
255
- agentKind === "adversary" ||
256
- agentKind === "tie_breaker" ||
257
- agentKind === "trace" ||
258
- agentKind === "incident" ||
259
- agentKind === "meta",
331
+ is_read_only_kind: isReadOnlyAgentKind(agentKind),
260
332
  is_executor_kind: agentKind === "executor",
261
333
  trust_score: Number(process.env.HARNESS_TRUST_SCORE ?? "1"),
262
334
  delegation_ceiling: Number(process.env.HARNESS_DELEGATION_CEILING ?? "1"),
@@ -0,0 +1,3 @@
1
+ /** @sync .pi/lib/ask-user/constants.ts */
2
+ export const FREEFORM_OPTION_TITLE = "Type something…";
3
+ export const MAX_QUESTIONNAIRE_QUESTIONS = 8;
@@ -0,0 +1,4 @@
1
+ /** Freeform row label in TUI/headless presenters (agents: "custom answer", not this literal). */
2
+ export const FREEFORM_OPTION_TITLE = "Type something…";
3
+
4
+ export const MAX_QUESTIONNAIRE_QUESTIONS = 8;
@@ -0,0 +1,56 @@
1
+ import type { AskResponse, QuestionnaireDetail } from "../types.js";
2
+
3
+ export function parseGlimpseRawResult(
4
+ raw: Record<string, unknown> | null,
5
+ cancelled: boolean,
6
+ ): AskResponse | null {
7
+ if (cancelled || !raw) return null;
8
+
9
+ const kind = raw.kind;
10
+ if (kind === "freeform") {
11
+ return {
12
+ kind: "freeform",
13
+ text: String(raw.text ?? "").trim(),
14
+ additionalComments: pickString(raw.additionalComments),
15
+ };
16
+ }
17
+
18
+ if (kind === "questionnaire") {
19
+ const questionnaireDetails: QuestionnaireDetail[] = Array.isArray(
20
+ raw.questionnaireDetails,
21
+ )
22
+ ? raw.questionnaireDetails.map((d: unknown) => {
23
+ const entry = d as Record<string, unknown>;
24
+ return {
25
+ question: String(entry.question ?? ""),
26
+ answer: String(entry.answer ?? ""),
27
+ kind: entry.kind === "freeform" ? "freeform" : "selection",
28
+ comment: pickString(entry.comment),
29
+ };
30
+ })
31
+ : [];
32
+
33
+ return {
34
+ kind: "questionnaire",
35
+ questionnaireDetails,
36
+ additionalComments: pickString(raw.additionalComments),
37
+ };
38
+ }
39
+
40
+ const selections = Array.isArray(raw.selections)
41
+ ? raw.selections.map(String)
42
+ : raw.selection
43
+ ? [String(raw.selection)]
44
+ : [];
45
+
46
+ return {
47
+ kind: "selection",
48
+ selections,
49
+ comment: pickString(raw.comment),
50
+ additionalComments: pickString(raw.additionalComments),
51
+ };
52
+ }
53
+
54
+ function pickString(raw: unknown): string | undefined {
55
+ return raw ? String(raw) : undefined;
56
+ }
@@ -0,0 +1,58 @@
1
+ import type { ValidatedAskParams } from "../types.js";
2
+ import type { GlimpseAskUserPayload } from "./glimpse-payload.js";
3
+
4
+ export function buildGlimpsePayload(
5
+ validated: ValidatedAskParams,
6
+ sessionName?: string,
7
+ ): GlimpseAskUserPayload {
8
+ const hasQuestions = validated.mode === "questionnaire";
9
+ const hasOptions = validated.options.length > 0;
10
+
11
+ let payloadType: GlimpseAskUserPayload["type"];
12
+ if (hasQuestions) {
13
+ payloadType = "questionnaire";
14
+ } else if (!hasOptions) {
15
+ payloadType = "freeform";
16
+ } else if (validated.allowMultiple) {
17
+ payloadType = "multi-select";
18
+ } else {
19
+ payloadType = "single-select";
20
+ }
21
+
22
+ let question = validated.question;
23
+ let context = validated.context;
24
+ if (!context && validated.question.length > 120) {
25
+ const match = validated.question.match(/^(.+?[.?!])(\s+|$)/);
26
+ if (match && match[0].length < validated.question.length) {
27
+ question = match[1].trim();
28
+ context = validated.question.slice(match[0].length).trim();
29
+ }
30
+ }
31
+
32
+ return {
33
+ type: payloadType,
34
+ question,
35
+ context,
36
+ contextFormat: validated.contextFormat,
37
+ options: validated.options.map((o) => ({
38
+ title: o.title,
39
+ description: o.description,
40
+ recommended: o.recommended,
41
+ })),
42
+ questions: validated.questions.map((q) => ({
43
+ title: q.title,
44
+ description: q.description,
45
+ allowMultiple: q.allowMultiple,
46
+ options: q.options.map((o) => ({
47
+ title: o.title,
48
+ description: o.description,
49
+ recommended: o.recommended,
50
+ })),
51
+ })),
52
+ allowMultiple: validated.allowMultiple,
53
+ allowFreeform: validated.allowFreeform,
54
+ allowComment: validated.allowComment,
55
+ allowSkip: validated.allowSkip,
56
+ sessionName,
57
+ };
58
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Synced with @alexleekt/pi-ask-user-glimpse@0.5.1 shared/ask-user.ts — harness-owned payload contract.
3
+ */
4
+
5
+ export interface GlimpseQuestionOption {
6
+ title: string;
7
+ description?: string;
8
+ recommended?: boolean;
9
+ }
10
+
11
+ export interface GlimpseQuestion {
12
+ title: string;
13
+ description?: string;
14
+ options?: GlimpseQuestionOption[];
15
+ allowMultiple?: boolean;
16
+ }
17
+
18
+ export type GlimpsePayloadType =
19
+ | "single-select"
20
+ | "multi-select"
21
+ | "questionnaire"
22
+ | "freeform";
23
+
24
+ export interface GlimpseAskUserPayload {
25
+ type: GlimpsePayloadType;
26
+ question: string;
27
+ context?: string;
28
+ contextFormat?: "markdown" | "html";
29
+ options: GlimpseQuestionOption[];
30
+ questions?: GlimpseQuestion[];
31
+ allowMultiple: boolean;
32
+ allowFreeform: boolean;
33
+ allowComment: boolean;
34
+ allowSkip?: boolean;
35
+ sessionName?: string;
36
+ }
37
+
38
+ export const GLIMPSE_FREEFORM_OPTION_TITLE = "My answer isn't listed above";
@@ -0,0 +1,74 @@
1
+ import type {
2
+ DialogResult,
3
+ NormalizedQuestion,
4
+ QuestionnaireDetail,
5
+ ValidatedAskParams,
6
+ } from "../types.js";
7
+
8
+ /** Build flat ask params for one questionnaire card (TUI/headless sequential). */
9
+ export function questionToFlatParams(
10
+ parent: ValidatedAskParams,
11
+ q: NormalizedQuestion,
12
+ index: number,
13
+ total: number,
14
+ ): ValidatedAskParams {
15
+ const header =
16
+ total > 1 ? `[${index + 1}/${total}] ${parent.question}` : parent.question;
17
+ return {
18
+ ...parent,
19
+ mode: "flat",
20
+ question: q.title,
21
+ context: [header, q.description].filter(Boolean).join("\n\n") || undefined,
22
+ options: q.options,
23
+ questions: [],
24
+ allowMultiple: q.allowMultiple,
25
+ allowFreeform: q.options.length === 0 ? true : parent.allowFreeform,
26
+ };
27
+ }
28
+
29
+ export function mergeQuestionnaireResults(
30
+ details: QuestionnaireDetail[],
31
+ last?: DialogResult,
32
+ ): DialogResult {
33
+ const additionalComments =
34
+ last?.response?.kind === "freeform"
35
+ ? last.response.text
36
+ : last?.response && "additionalComments" in last.response
37
+ ? last.response.additionalComments
38
+ : undefined;
39
+
40
+ return {
41
+ response: {
42
+ kind: "questionnaire",
43
+ questionnaireDetails: details,
44
+ additionalComments,
45
+ },
46
+ cancelled: false,
47
+ ui_backend: last?.ui_backend ?? "tui",
48
+ ui_degraded: last?.ui_degraded,
49
+ };
50
+ }
51
+
52
+ export function detailFromFlatResult(
53
+ questionLabel: string,
54
+ result: DialogResult,
55
+ ): QuestionnaireDetail | null {
56
+ if (result.cancelled || !result.response) return null;
57
+ const r = result.response;
58
+ if (r.kind === "freeform") {
59
+ return {
60
+ question: questionLabel,
61
+ answer: r.text,
62
+ kind: "freeform",
63
+ };
64
+ }
65
+ if (r.kind === "selection") {
66
+ return {
67
+ question: questionLabel,
68
+ answer: r.selections.join(", "),
69
+ kind: "selection",
70
+ comment: r.comment,
71
+ };
72
+ }
73
+ return null;
74
+ }