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
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
isPlanPhaseAllowedMutation,
|
|
25
25
|
isPlanPhaseScopedWrite,
|
|
26
26
|
normalizeHarnessPath,
|
|
27
|
+
parseHarnessSlashInput,
|
|
27
28
|
readPlanPacketFromPath,
|
|
28
29
|
saveProjectActiveRun,
|
|
29
30
|
saveRunContextToDisk,
|
|
@@ -152,6 +153,9 @@ async function handlePolicyBeforeAgentStart(args: {
|
|
|
152
153
|
const bootstrapPrompt = isHarnessBootstrapPrompt(userPrompt);
|
|
153
154
|
const abortSignal = hasHarnessAbortSignal(userPrompt);
|
|
154
155
|
|
|
156
|
+
const parsed = parseHarnessSlashInput(userPrompt);
|
|
157
|
+
const isHarnessClear = parsed?.command === "harness-clear";
|
|
158
|
+
|
|
155
159
|
if (bootstrapPrompt) {
|
|
156
160
|
state.phase = "execute";
|
|
157
161
|
state.approvedPlan = true;
|
|
@@ -186,13 +190,22 @@ async function handlePolicyBeforeAgentStart(args: {
|
|
|
186
190
|
content: [
|
|
187
191
|
"Harness run aborted safely.",
|
|
188
192
|
"Mutating tools are now blocked until a new approved plan is attached.",
|
|
189
|
-
'Next step: /harness-plan "<task>"',
|
|
193
|
+
'Next step: /harness-plan "<task>" or /harness-auto "<task>"',
|
|
190
194
|
].join("\n"),
|
|
191
195
|
},
|
|
192
196
|
systemPrompt: `${event.systemPrompt}\n\n[PolicyGate]\nAbort lock active. Mutating tools must remain blocked until a new approved plan is attached.`,
|
|
193
197
|
};
|
|
194
198
|
}
|
|
195
199
|
|
|
200
|
+
if (
|
|
201
|
+
parsed?.command === "harness-plan" ||
|
|
202
|
+
parsed?.command === "harness-auto"
|
|
203
|
+
) {
|
|
204
|
+
stateRef.current.aborted = false;
|
|
205
|
+
stateRef.current.abortReason = null;
|
|
206
|
+
stateRef.current.abortedAt = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
196
209
|
const nextPhase = inferHarnessPhase(entries, userPrompt);
|
|
197
210
|
const planSignal = hasApprovedPlanSignal(userPrompt, entries);
|
|
198
211
|
const transitionBlock = getPolicyTransitionBlock(userPrompt, entries);
|
|
@@ -206,7 +219,7 @@ async function handlePolicyBeforeAgentStart(args: {
|
|
|
206
219
|
};
|
|
207
220
|
}
|
|
208
221
|
|
|
209
|
-
if (nextPhase === "plan") {
|
|
222
|
+
if (nextPhase === "plan" || isHarnessClear) {
|
|
210
223
|
stateRef.current.approvedPlan = false;
|
|
211
224
|
stateRef.current.planId = null;
|
|
212
225
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.0.0",
|
|
3
3
|
"package": "ultimate-pi",
|
|
4
|
-
"package_version": "0.22.
|
|
5
|
-
"generated_at": "2026-05-
|
|
6
|
-
"policy_sha256": "
|
|
4
|
+
"package_version": "0.22.1",
|
|
5
|
+
"generated_at": "2026-05-27T15:57:32.501Z",
|
|
6
|
+
"policy_sha256": "1a631333f1abed3b411961d3527bcae2d4fcd2f715b09a689b0b83b3ea0f54f3",
|
|
7
7
|
"agents": {
|
|
8
8
|
"pi-pi/agent-expert": {
|
|
9
9
|
"path": ".pi/agents/pi-pi/agent-expert.md",
|
|
@@ -9,17 +9,28 @@ kinds:
|
|
|
9
9
|
- grep
|
|
10
10
|
- find
|
|
11
11
|
- ls
|
|
12
|
+
- bash
|
|
13
|
+
- ctx_batch_execute
|
|
14
|
+
- ctx_execute
|
|
15
|
+
- ctx_execute_file
|
|
16
|
+
- ctx_search
|
|
17
|
+
- ctx_fetch_and_index
|
|
12
18
|
extensions: false
|
|
13
19
|
read_only: true
|
|
14
20
|
executor:
|
|
15
21
|
tools:
|
|
16
22
|
- read
|
|
17
|
-
- write
|
|
18
|
-
- edit
|
|
19
|
-
- bash
|
|
20
23
|
- grep
|
|
21
24
|
- find
|
|
22
25
|
- ls
|
|
26
|
+
- bash
|
|
27
|
+
- ctx_batch_execute
|
|
28
|
+
- ctx_execute
|
|
29
|
+
- ctx_execute_file
|
|
30
|
+
- ctx_search
|
|
31
|
+
- ctx_fetch_and_index
|
|
32
|
+
- write
|
|
33
|
+
- edit
|
|
23
34
|
extensions: true
|
|
24
35
|
extension_bundle: executor
|
|
25
36
|
read_only: false
|
|
@@ -29,6 +40,12 @@ kinds:
|
|
|
29
40
|
- grep
|
|
30
41
|
- find
|
|
31
42
|
- ls
|
|
43
|
+
- bash
|
|
44
|
+
- ctx_batch_execute
|
|
45
|
+
- ctx_execute
|
|
46
|
+
- ctx_execute_file
|
|
47
|
+
- ctx_search
|
|
48
|
+
- ctx_fetch_and_index
|
|
32
49
|
extensions: false
|
|
33
50
|
read_only: true
|
|
34
51
|
adversary:
|
|
@@ -37,6 +54,12 @@ kinds:
|
|
|
37
54
|
- grep
|
|
38
55
|
- find
|
|
39
56
|
- ls
|
|
57
|
+
- bash
|
|
58
|
+
- ctx_batch_execute
|
|
59
|
+
- ctx_execute
|
|
60
|
+
- ctx_execute_file
|
|
61
|
+
- ctx_search
|
|
62
|
+
- ctx_fetch_and_index
|
|
40
63
|
extensions: false
|
|
41
64
|
read_only: true
|
|
42
65
|
tie_breaker:
|
|
@@ -45,6 +68,12 @@ kinds:
|
|
|
45
68
|
- grep
|
|
46
69
|
- find
|
|
47
70
|
- ls
|
|
71
|
+
- bash
|
|
72
|
+
- ctx_batch_execute
|
|
73
|
+
- ctx_execute
|
|
74
|
+
- ctx_execute_file
|
|
75
|
+
- ctx_search
|
|
76
|
+
- ctx_fetch_and_index
|
|
48
77
|
extensions: false
|
|
49
78
|
read_only: true
|
|
50
79
|
trace:
|
|
@@ -53,6 +82,12 @@ kinds:
|
|
|
53
82
|
- grep
|
|
54
83
|
- find
|
|
55
84
|
- ls
|
|
85
|
+
- bash
|
|
86
|
+
- ctx_batch_execute
|
|
87
|
+
- ctx_execute
|
|
88
|
+
- ctx_execute_file
|
|
89
|
+
- ctx_search
|
|
90
|
+
- ctx_fetch_and_index
|
|
56
91
|
extensions: false
|
|
57
92
|
read_only: true
|
|
58
93
|
incident:
|
|
@@ -61,6 +96,12 @@ kinds:
|
|
|
61
96
|
- grep
|
|
62
97
|
- find
|
|
63
98
|
- ls
|
|
99
|
+
- bash
|
|
100
|
+
- ctx_batch_execute
|
|
101
|
+
- ctx_execute
|
|
102
|
+
- ctx_execute_file
|
|
103
|
+
- ctx_search
|
|
104
|
+
- ctx_fetch_and_index
|
|
64
105
|
extensions: false
|
|
65
106
|
read_only: true
|
|
66
107
|
other:
|
|
@@ -196,6 +237,16 @@ agents:
|
|
|
196
237
|
submit_tool: submit_execution_plan_brief
|
|
197
238
|
harness/planning/hypothesis-validator:
|
|
198
239
|
kind: planner
|
|
240
|
+
tools_deny:
|
|
241
|
+
- bash
|
|
242
|
+
- grep
|
|
243
|
+
- find
|
|
244
|
+
- ls
|
|
245
|
+
- ctx_batch_execute
|
|
246
|
+
- ctx_execute
|
|
247
|
+
- ctx_execute_file
|
|
248
|
+
- ctx_search
|
|
249
|
+
- ctx_fetch_and_index
|
|
199
250
|
tools_add:
|
|
200
251
|
- submit_hypothesis_validation
|
|
201
252
|
extensions: false
|
|
@@ -212,6 +263,12 @@ agents:
|
|
|
212
263
|
submit_tool: submit_hypothesis_brief
|
|
213
264
|
harness/planning/implementation-researcher:
|
|
214
265
|
kind: planner
|
|
266
|
+
tools_deny:
|
|
267
|
+
- bash
|
|
268
|
+
- find
|
|
269
|
+
- ctx_batch_execute
|
|
270
|
+
- ctx_execute
|
|
271
|
+
- ctx_execute_file
|
|
215
272
|
tools_add:
|
|
216
273
|
- submit_implementation_research
|
|
217
274
|
extensions: false
|
|
@@ -251,6 +308,15 @@ agents:
|
|
|
251
308
|
submit_tool: submit_planning_context
|
|
252
309
|
harness/planning/review-integrator:
|
|
253
310
|
kind: planner
|
|
311
|
+
tools_deny:
|
|
312
|
+
- bash
|
|
313
|
+
- grep
|
|
314
|
+
- find
|
|
315
|
+
- ctx_batch_execute
|
|
316
|
+
- ctx_execute
|
|
317
|
+
- ctx_execute_file
|
|
318
|
+
- ctx_search
|
|
319
|
+
- ctx_fetch_and_index
|
|
254
320
|
tools_add:
|
|
255
321
|
- submit_review_round_draft
|
|
256
322
|
extensions: false
|
|
@@ -259,6 +325,13 @@ agents:
|
|
|
259
325
|
submit_tool: submit_review_round_draft
|
|
260
326
|
harness/planning/sprint-contract-auditor:
|
|
261
327
|
kind: planner
|
|
328
|
+
tools_deny:
|
|
329
|
+
- bash
|
|
330
|
+
- find
|
|
331
|
+
- ctx_batch_execute
|
|
332
|
+
- ctx_execute
|
|
333
|
+
- ctx_execute_file
|
|
334
|
+
- ctx_fetch_and_index
|
|
262
335
|
tools_add:
|
|
263
336
|
- submit_sprint_audit
|
|
264
337
|
extensions: false
|
|
@@ -267,6 +340,12 @@ agents:
|
|
|
267
340
|
submit_tool: submit_sprint_audit
|
|
268
341
|
harness/planning/stack-researcher:
|
|
269
342
|
kind: planner
|
|
343
|
+
tools_deny:
|
|
344
|
+
- bash
|
|
345
|
+
- find
|
|
346
|
+
- ctx_batch_execute
|
|
347
|
+
- ctx_execute
|
|
348
|
+
- ctx_execute_file
|
|
270
349
|
tools_add:
|
|
271
350
|
- submit_stack_brief
|
|
272
351
|
extensions: false
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"schema_version": { "type": "string", "const": "1.0.0" },
|
|
25
25
|
"status": {
|
|
26
26
|
"type": "string",
|
|
27
|
-
"enum": ["draft", "needs_user", "ready"]
|
|
27
|
+
"enum": ["draft", "needs_user", "needs_clarification", "ready"]
|
|
28
28
|
},
|
|
29
29
|
"source_task": { "type": "string", "minLength": 1 },
|
|
30
30
|
"clarified_task": { "type": "string", "minLength": 1 },
|
|
@@ -83,6 +83,15 @@
|
|
|
83
83
|
"summary": { "type": "string" }
|
|
84
84
|
},
|
|
85
85
|
"additionalProperties": true
|
|
86
|
+
},
|
|
87
|
+
"user_engagement": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"properties": {
|
|
90
|
+
"source": { "type": "string", "minLength": 1 },
|
|
91
|
+
"recorded_at": { "type": "string" }
|
|
92
|
+
},
|
|
93
|
+
"required": ["source"],
|
|
94
|
+
"additionalProperties": false
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
}
|
|
@@ -14,6 +14,9 @@ const BUILTIN_DENY_TOOLS = new Set([
|
|
|
14
14
|
"blackboard",
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
|
+
/** Debate inspectors may only run ast-grep via bash (no repo-wide shell exploration). */
|
|
18
|
+
const DEBATE_SG_BASH_ALLOW = /^\s*sg\s+(-p|--pattern)\s+/;
|
|
19
|
+
|
|
17
20
|
const PLANNING_BASH_DENY_PATTERNS = [
|
|
18
21
|
/\bgraphify\s+update\b/i,
|
|
19
22
|
/\bgraphify\s+extract\b/i,
|
|
@@ -29,6 +32,32 @@ const PLANNING_ARTIFACT_JSON_WRITE = /artifacts\/[^\s'"`;]+\.json\b/i;
|
|
|
29
32
|
|
|
30
33
|
const MUTATING_TOOLS = new Set(["write", "edit"]);
|
|
31
34
|
|
|
35
|
+
/** Blind R1: hypothesis-validator may only read task + hypothesis brief. */
|
|
36
|
+
const AGENT_READ_PATH_ALLOW = {
|
|
37
|
+
"harness/planning/hypothesis-validator": [
|
|
38
|
+
"artifacts/task-clarification.yaml",
|
|
39
|
+
"artifacts/hypothesis.yaml",
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function normalizeReadToolPath(toolInput) {
|
|
44
|
+
const raw = String(
|
|
45
|
+
toolInput.path ?? toolInput.file_path ?? toolInput.filePath ?? "",
|
|
46
|
+
).replace(/\\/g, "/");
|
|
47
|
+
if (!raw.trim()) return "";
|
|
48
|
+
const idx = raw.indexOf("artifacts/");
|
|
49
|
+
if (idx >= 0) return raw.slice(idx);
|
|
50
|
+
return raw.replace(/^\.\//, "");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function deniesAgentReadPath(agentId, toolInput) {
|
|
54
|
+
const allowed = AGENT_READ_PATH_ALLOW[agentId];
|
|
55
|
+
if (!allowed) return false;
|
|
56
|
+
const rel = normalizeReadToolPath(toolInput);
|
|
57
|
+
if (!rel) return true;
|
|
58
|
+
return !allowed.includes(rel);
|
|
59
|
+
}
|
|
60
|
+
|
|
32
61
|
const cache = new Map();
|
|
33
62
|
|
|
34
63
|
const EXTENSION_BUNDLE_MODULES = {
|
|
@@ -334,6 +363,10 @@ export function allowsAgentTool(input) {
|
|
|
334
363
|
|
|
335
364
|
if (MUTATING_TOOLS.has(toolName) && spec.readOnly) return false;
|
|
336
365
|
|
|
366
|
+
if (toolName === "read" && deniesAgentReadPath(agentId, toolInput)) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
337
370
|
if (toolName === "ctx_batch_execute" && spec.readOnly) {
|
|
338
371
|
if (deniesReadOnlyBatchExecute(toolInput)) return false;
|
|
339
372
|
}
|
|
@@ -343,7 +376,15 @@ export function allowsAgentTool(input) {
|
|
|
343
376
|
}
|
|
344
377
|
|
|
345
378
|
if (toolName === "bash" && spec.readOnly) {
|
|
346
|
-
if (
|
|
379
|
+
if (
|
|
380
|
+
agentId === "harness/planning/plan-evaluator" ||
|
|
381
|
+
agentId === "harness/planning/plan-adversary"
|
|
382
|
+
) {
|
|
383
|
+
const command = String(toolInput.command ?? "");
|
|
384
|
+
if (!command || !DEBATE_SG_BASH_ALLOW.test(command)) return false;
|
|
385
|
+
} else if (deniesReadOnlyBash(agentId, toolInput)) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
347
388
|
}
|
|
348
389
|
|
|
349
390
|
return true;
|
|
@@ -264,7 +264,9 @@ export async function buildHarnessAgtEvaluationContext(
|
|
|
264
264
|
agentKind,
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
-
const spawnDecision = evaluateSubagentToolCall(input.toolName, agentId
|
|
267
|
+
const spawnDecision = evaluateSubagentToolCall(input.toolName, agentId, {
|
|
268
|
+
isParentOrchestrator,
|
|
269
|
+
});
|
|
268
270
|
|
|
269
271
|
const toolAllowed = allowsAgentTool({
|
|
270
272
|
packageRoot: input.packageRoot,
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
const denyCounts = new Map<string, number>();
|
|
2
|
+
/** Sessions cleared after /harness-plan or /harness-auto starts a fresh plan attempt. */
|
|
3
|
+
const disarmedKillSwitchSessions = new Set<string>();
|
|
4
|
+
|
|
5
|
+
export function disarmHarnessKillSwitch(sessionId: string): void {
|
|
6
|
+
disarmedKillSwitchSessions.add(sessionId);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function armHarnessKillSwitch(sessionId: string): void {
|
|
10
|
+
disarmedKillSwitchSessions.delete(sessionId);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isHarnessKillSwitchDisarmed(sessionId: string): boolean {
|
|
14
|
+
return disarmedKillSwitchSessions.has(sessionId);
|
|
15
|
+
}
|
|
2
16
|
|
|
3
17
|
export function recordHarnessPolicyDeny(sessionId: string): number {
|
|
4
18
|
const n = (denyCounts.get(sessionId) ?? 0) + 1;
|
|
@@ -26,7 +26,9 @@ export async function evaluateLegacyHarnessToolPolicy(
|
|
|
26
26
|
const isSubprocess = process.env.PI_HARNESS_SUBPROCESS === "1";
|
|
27
27
|
const isParent = agentId === "parent-orchestrator";
|
|
28
28
|
|
|
29
|
-
const spawn = evaluateSubagentToolCall(toolName, agentId
|
|
29
|
+
const spawn = evaluateSubagentToolCall(toolName, agentId, {
|
|
30
|
+
isParentOrchestrator: isParent,
|
|
31
|
+
});
|
|
30
32
|
if (spawn.action === "block") {
|
|
31
33
|
return { allowed: false, reason: spawn.reason ?? "spawn policy" };
|
|
32
34
|
}
|
|
@@ -14,11 +14,13 @@ export { formatResultText, toToolDetails } from "./format.js";
|
|
|
14
14
|
export { applyAskUserToTaskClarification } from "./merge-task-clarification.js";
|
|
15
15
|
export {
|
|
16
16
|
assertSubagentCannotAskUser,
|
|
17
|
+
isCursorAgentContext,
|
|
17
18
|
isHarnessNonInteractive,
|
|
18
19
|
isPlanApprovalAskUser,
|
|
19
20
|
nonInteractiveAskUserResult,
|
|
20
21
|
PLAN_APPROVE_OPTION,
|
|
21
22
|
PLAN_CANCEL_OPTION,
|
|
23
|
+
shouldPreferTuiOverGlimpse,
|
|
22
24
|
} from "./policy.js";
|
|
23
25
|
export {
|
|
24
26
|
glimpseHealthCheck,
|
|
@@ -93,6 +93,11 @@ export function applyAskUserToTaskClarification(
|
|
|
93
93
|
if (next.status === "draft" || next.status === "needs_user") {
|
|
94
94
|
next.status = "needs_user";
|
|
95
95
|
}
|
|
96
|
+
next.user_engagement = {
|
|
97
|
+
source: "ask_user",
|
|
98
|
+
recorded_at: new Date().toISOString(),
|
|
99
|
+
};
|
|
100
|
+
next.clarification_rounds = (Number(next.clarification_rounds) || 0) + 1;
|
|
96
101
|
|
|
97
102
|
return next;
|
|
98
103
|
}
|
|
@@ -56,10 +56,33 @@ export function isPlanApprovalAskUser(input: {
|
|
|
56
56
|
export function isHarnessNonInteractive(): boolean {
|
|
57
57
|
return (
|
|
58
58
|
process.env.HARNESS_NON_INTERACTIVE === "1" ||
|
|
59
|
+
process.env.HARNESS_PLAN_NONINTERACTIVE === "1" ||
|
|
60
|
+
process.argv.some((a) => a === "-p" || a === "--print") ||
|
|
59
61
|
process.argv.some((a) => a.includes("non-interactive"))
|
|
60
62
|
);
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/** Prefer headless ask_user UI (print mode, CI, or non-TTY stdin). */
|
|
66
|
+
export function isHeadlessAskUserContext(): boolean {
|
|
67
|
+
return isHarnessNonInteractive() || !process.stdin.isTTY;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Cursor Composer / agent runs set CURSOR_AGENT=1; Glimpse WebView often returns null there. */
|
|
71
|
+
export function isCursorAgentContext(): boolean {
|
|
72
|
+
const v = process.env.CURSOR_AGENT;
|
|
73
|
+
return v === "1" || v === "true";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Prefer terminal TUI over Glimpse when auto-routing (unless HARNESS_ASK_USER_UI=glimpse).
|
|
78
|
+
*/
|
|
79
|
+
export function shouldPreferTuiOverGlimpse(): boolean {
|
|
80
|
+
const forced = process.env.HARNESS_ASK_USER_UI?.toLowerCase();
|
|
81
|
+
if (forced === "glimpse") return false;
|
|
82
|
+
if (forced === "tui" || forced === "headless") return forced === "tui";
|
|
83
|
+
return isCursorAgentContext();
|
|
84
|
+
}
|
|
85
|
+
|
|
63
86
|
export function assertSubagentCannotAskUser(agentType: string | undefined): {
|
|
64
87
|
blocked: boolean;
|
|
65
88
|
reason?: string;
|
|
@@ -95,7 +95,14 @@ export async function runGlimpsePresenter(
|
|
|
95
95
|
unknown
|
|
96
96
|
> | null;
|
|
97
97
|
|
|
98
|
-
const
|
|
98
|
+
const explicitCancel = raw?.__cancelled === true;
|
|
99
|
+
if (raw === null && !explicitCancel) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"glimpse prompt returned null without user cancel — degrade to TUI",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const cancelled = raw === null || explicitCancel;
|
|
99
106
|
const response = parseGlimpseRawResult(raw, cancelled);
|
|
100
107
|
|
|
101
108
|
return {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
mergeQuestionnaireResults,
|
|
6
6
|
questionToFlatParams,
|
|
7
7
|
} from "../core/questionnaire.js";
|
|
8
|
+
import { isHarnessNonInteractive } from "../policy.js";
|
|
8
9
|
import type { DialogResult, ValidatedAskParams } from "../types.js";
|
|
9
10
|
|
|
10
11
|
async function runFlatHeadless(
|
|
@@ -17,6 +18,20 @@ async function runFlatHeadless(
|
|
|
17
18
|
const title = context ? `${context}\n\n${question}` : question;
|
|
18
19
|
const labels = options.map((o) => o.title);
|
|
19
20
|
|
|
21
|
+
if (isHarnessNonInteractive() && labels.length > 0) {
|
|
22
|
+
const preferred =
|
|
23
|
+
labels.find((l) =>
|
|
24
|
+
/^(approve(d)?(\s+plan)?|yes,?\s+proceed|looks\s+good|yes|proceed|continue)$/i.test(
|
|
25
|
+
l.trim(),
|
|
26
|
+
),
|
|
27
|
+
) ?? labels[0];
|
|
28
|
+
return {
|
|
29
|
+
response: { kind: "selection", selections: [preferred] },
|
|
30
|
+
cancelled: false,
|
|
31
|
+
ui_backend: "headless",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
if (labels.length === 0) {
|
|
21
36
|
if (!allowFreeform) {
|
|
22
37
|
return { response: null, cancelled: true, ui_backend: "headless" };
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isHeadlessAskUserContext,
|
|
3
|
+
shouldPreferTuiOverGlimpse,
|
|
4
|
+
} from "../policy.js";
|
|
1
5
|
import type { DialogResult, UiBackend, ValidatedAskParams } from "../types.js";
|
|
2
6
|
import { isGlimpseAvailable, runGlimpsePresenter } from "./glimpse.js";
|
|
3
7
|
import { runHeadlessPresenter } from "./headless.js";
|
|
@@ -10,6 +14,7 @@ export function resolvePresenterChoice(
|
|
|
10
14
|
validated: ValidatedAskParams,
|
|
11
15
|
hasUI: boolean,
|
|
12
16
|
): PresenterChoice {
|
|
17
|
+
if (isHeadlessAskUserContext()) return "headless";
|
|
13
18
|
if (validated.displayMode === "inline") return "tui";
|
|
14
19
|
|
|
15
20
|
const forced = process.env.HARNESS_ASK_USER_UI?.toLowerCase();
|
|
@@ -17,6 +22,11 @@ export function resolvePresenterChoice(
|
|
|
17
22
|
if (forced === "glimpse") return "glimpse";
|
|
18
23
|
if (forced === "headless") return "headless";
|
|
19
24
|
|
|
25
|
+
if (shouldPreferTuiOverGlimpse()) {
|
|
26
|
+
if (hasUI) return "tui";
|
|
27
|
+
return "headless";
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
if (hasUI && isGlimpseAvailable()) return "glimpse";
|
|
21
31
|
if (hasUI) return "tui";
|
|
22
32
|
return "headless";
|
|
@@ -44,13 +54,12 @@ export async function presentAskUser(
|
|
|
44
54
|
validated: ValidatedAskParams,
|
|
45
55
|
ctx: PresenterContext,
|
|
46
56
|
): Promise<DialogResult> {
|
|
47
|
-
|
|
57
|
+
const choice = resolvePresenterChoice(validated, ctx.hasUI);
|
|
48
58
|
|
|
49
59
|
if (choice === "glimpse") {
|
|
50
60
|
try {
|
|
51
61
|
return await runPresenter("glimpse", validated, ctx);
|
|
52
62
|
} catch {
|
|
53
|
-
choice = "tui";
|
|
54
63
|
const outcome = await runPresenter("tui", validated, ctx);
|
|
55
64
|
return { ...outcome, ui_degraded: true };
|
|
56
65
|
}
|
|
@@ -210,6 +210,18 @@ function isGlimpseAvailable() {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
function isCursorAgentContext() {
|
|
214
|
+
const v = process.env.CURSOR_AGENT;
|
|
215
|
+
return v === "1" || v === "true";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function shouldPreferTuiOverGlimpse() {
|
|
219
|
+
const forced = process.env.HARNESS_ASK_USER_UI?.toLowerCase();
|
|
220
|
+
if (forced === "glimpse") return false;
|
|
221
|
+
if (forced === "tui" || forced === "headless") return forced === "tui";
|
|
222
|
+
return isCursorAgentContext();
|
|
223
|
+
}
|
|
224
|
+
|
|
213
225
|
/** @param {import('./types.js').ValidatedAskParams} validated @param {boolean} hasUI */
|
|
214
226
|
export function resolvePresenterChoice(validated, hasUI) {
|
|
215
227
|
if (validated.displayMode === "inline") return "tui";
|
|
@@ -218,6 +230,10 @@ export function resolvePresenterChoice(validated, hasUI) {
|
|
|
218
230
|
if (forced === "tui") return "tui";
|
|
219
231
|
if (forced === "glimpse") return "glimpse";
|
|
220
232
|
if (forced === "headless") return "headless";
|
|
233
|
+
if (shouldPreferTuiOverGlimpse()) {
|
|
234
|
+
if (hasUI) return "tui";
|
|
235
|
+
return "headless";
|
|
236
|
+
}
|
|
221
237
|
if (hasUI && isGlimpseAvailable()) return "glimpse";
|
|
222
238
|
if (hasUI) return "tui";
|
|
223
239
|
return "headless";
|