ultimate-pi 0.22.0 → 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.
Files changed (78) hide show
  1. package/.agents/skills/harness-context/SKILL.md +3 -3
  2. package/.agents/skills/harness-debate-plan/SKILL.md +2 -2
  3. package/.agents/skills/harness-decisions/SKILL.md +2 -2
  4. package/.agents/skills/harness-eval/SKILL.md +1 -1
  5. package/.agents/skills/harness-git-commit/SKILL.md +1 -1
  6. package/.agents/skills/harness-governor/SKILL.md +5 -5
  7. package/.agents/skills/harness-ls-lint-setup/SKILL.md +2 -2
  8. package/.agents/skills/harness-orchestration/SKILL.md +4 -4
  9. package/.agents/skills/harness-plan/SKILL.md +2 -2
  10. package/.agents/skills/harness-review/SKILL.md +2 -2
  11. package/.agents/skills/harness-sentrux-repair/SKILL.md +1 -1
  12. package/.agents/skills/harness-sentrux-setup/SKILL.md +2 -2
  13. package/.agents/skills/harness-spec/SKILL.md +1 -1
  14. package/.agents/skills/harness-steer/SKILL.md +2 -2
  15. package/.agents/skills/posthog-analyst/SKILL.md +1 -1
  16. package/.agents/skills/sentrux/SKILL.md +4 -4
  17. package/.agents/skills/web-retrieval/SKILL.md +1 -1
  18. package/.pi/agents/harness/ls-lint-steward.md +3 -3
  19. package/.pi/agents/harness/planning/decompose.md +1 -1
  20. package/.pi/agents/harness/planning/execution-plan-author.md +1 -1
  21. package/.pi/agents/harness/planning/hypothesis-validator.md +1 -1
  22. package/.pi/agents/harness/planning/hypothesis.md +1 -1
  23. package/.pi/agents/harness/planning/plan-adversary.md +1 -1
  24. package/.pi/agents/harness/planning/plan-evaluator.md +2 -2
  25. package/.pi/agents/harness/planning/plan-synthesizer.md +2 -2
  26. package/.pi/agents/harness/planning/review-integrator.md +1 -1
  27. package/.pi/agents/harness/planning/sprint-contract-auditor.md +5 -5
  28. package/.pi/agents/harness/running/executor.md +1 -1
  29. package/.pi/agents/harness/sentrux-repair-advisor.md +1 -1
  30. package/.pi/agents/harness/sentrux-steward.md +2 -2
  31. package/.pi/extensions/agt-kill-switch.ts +7 -1
  32. package/.pi/extensions/harness-plan-approval.ts +9 -1
  33. package/.pi/extensions/harness-run-context.ts +529 -84
  34. package/.pi/extensions/policy-gate.ts +15 -2
  35. package/.pi/harness/agents.manifest.json +16 -16
  36. package/.pi/harness/agents.policy.yaml +82 -3
  37. package/.pi/harness/specs/plan-task-clarification.schema.json +10 -1
  38. package/.pi/lib/agents-policy.mjs +42 -1
  39. package/.pi/lib/agt/build-evaluation-context.ts +3 -1
  40. package/.pi/lib/agt/kill-switch-state.ts +14 -0
  41. package/.pi/lib/agt/legacy-evaluate.ts +3 -1
  42. package/.pi/lib/ask-user/index.ts +2 -0
  43. package/.pi/lib/ask-user/merge-task-clarification.ts +5 -0
  44. package/.pi/lib/ask-user/policy.ts +23 -0
  45. package/.pi/lib/ask-user/presenters/glimpse.ts +8 -1
  46. package/.pi/lib/ask-user/presenters/headless.ts +15 -0
  47. package/.pi/lib/ask-user/presenters/select.ts +11 -2
  48. package/.pi/lib/ask-user/validate-core.mjs +16 -0
  49. package/.pi/lib/harness-artifact-gate.ts +75 -5
  50. package/.pi/lib/harness-repair-brief.ts +30 -4
  51. package/.pi/lib/harness-run-context.ts +804 -17
  52. package/.pi/lib/harness-schema-validate.ts +147 -38
  53. package/.pi/lib/harness-spawn-policy.ts +9 -0
  54. package/.pi/lib/harness-spawn-topology.ts +109 -7
  55. package/.pi/lib/harness-subagent-precheck.ts +21 -0
  56. package/.pi/lib/harness-subagent-submit-pipeline.ts +95 -21
  57. package/.pi/lib/harness-subagent-submit-register.ts +6 -1
  58. package/.pi/lib/harness-subagents-bridge.ts +3 -0
  59. package/.pi/lib/harness-yaml.ts +11 -3
  60. package/.pi/lib/plan-approval/create-plan.ts +2 -6
  61. package/.pi/lib/plan-debate-gate.ts +87 -0
  62. package/.pi/lib/plan-debate-lane.ts +8 -2
  63. package/.pi/lib/plan-human-gates.ts +322 -0
  64. package/.pi/prompts/harness-clear.md +25 -0
  65. package/.pi/prompts/harness-plan.md +11 -7
  66. package/.pi/prompts/harness-review.md +5 -5
  67. package/.pi/prompts/harness-run.md +2 -2
  68. package/.pi/prompts/harness-sentrux-steward.md +2 -2
  69. package/.pi/prompts/harness-setup.md +3 -3
  70. package/.pi/prompts/harness-steer.md +5 -5
  71. package/.pi/scripts/generate-agents-policy-yaml.mjs +73 -7
  72. package/.pi/scripts/harness-reconcile-run-context.mjs +62 -0
  73. package/.pi/scripts/harness-schema-compile-verify.mjs +29 -0
  74. package/.pi/scripts/harness-verify.mjs +100 -0
  75. package/AGENTS.md +1 -0
  76. package/CHANGELOG.md +13 -0
  77. package/README.md +4 -0
  78. package/package.json +9 -6
@@ -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.21.0",
5
- "generated_at": "2026-05-27T07:05:16.312Z",
6
- "policy_sha256": "799782453e74a1d2d15a28715c985c1b5dc4566701ddcce475ec4725294437e4",
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",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "harness/ls-lint-steward": {
53
53
  "path": ".pi/agents/harness/ls-lint-steward.md",
54
- "sha256": "e35bf59adf2785e7369a7428793a26691cea43d630e02a9541398cb5a5b1f5dc"
54
+ "sha256": "abbb43da45a2c9c080cd2043384d481a143acaf440e52b657493425218cb951f"
55
55
  },
56
56
  "harness/sentrux-bootstrap": {
57
57
  "path": ".pi/agents/harness/sentrux-bootstrap.md",
@@ -59,11 +59,11 @@
59
59
  },
60
60
  "harness/sentrux-repair-advisor": {
61
61
  "path": ".pi/agents/harness/sentrux-repair-advisor.md",
62
- "sha256": "057618de561d90e597be7a319f3abfa2adaa128d4e92850c50b3e35a447f0371"
62
+ "sha256": "3c9722806338f680db3d165e633a877e087c1485ad74c2b38781d7ba989b48f0"
63
63
  },
64
64
  "harness/sentrux-steward": {
65
65
  "path": ".pi/agents/harness/sentrux-steward.md",
66
- "sha256": "d806cbf2c2e211c6b4c95e35893bc250c8a0fab6fae92190766eab16cd091d51"
66
+ "sha256": "019740bd4313426edaa36f7fa96471088731abe616282a6e5109e9c538ae34eb"
67
67
  },
68
68
  "harness/trace-librarian": {
69
69
  "path": ".pi/agents/harness/trace-librarian.md",
@@ -95,7 +95,7 @@
95
95
  },
96
96
  "harness/running/executor": {
97
97
  "path": ".pi/agents/harness/running/executor.md",
98
- "sha256": "0b602c27f8842af3b1bc702623649de8ba775e77fe59bc156bfabf91b5303d85"
98
+ "sha256": "e8710179def62a9adaa63ba5b05c3f36dee95da6fd751ef34be773bbee65a5c2"
99
99
  },
100
100
  "harness/reviewing/adversary": {
101
101
  "path": ".pi/agents/harness/reviewing/adversary.md",
@@ -111,19 +111,19 @@
111
111
  },
112
112
  "harness/planning/decompose": {
113
113
  "path": ".pi/agents/harness/planning/decompose.md",
114
- "sha256": "ef552be75ff92667e6be90a85768e0804501d00e517b753ca37d174b2561618a"
114
+ "sha256": "35965f8f8eaa19caee72de1b708178f5ce7bba185f25bed28b9b2ff66c51eaed"
115
115
  },
116
116
  "harness/planning/execution-plan-author": {
117
117
  "path": ".pi/agents/harness/planning/execution-plan-author.md",
118
- "sha256": "3b83edca1eb393941e04213c5cabe0e4b180e52df59169ba24904341a369ead5"
118
+ "sha256": "f0251ac5fb423dda3d6b0b4cff1f63a8e5adfa40806b99454a649c1b0fe3adae"
119
119
  },
120
120
  "harness/planning/hypothesis-validator": {
121
121
  "path": ".pi/agents/harness/planning/hypothesis-validator.md",
122
- "sha256": "ee68aa5c04b903320116cfa21cea8f130199fd21e1fd1a8a747830bf53920fdb"
122
+ "sha256": "70d755da14e146755932c2cb3eb9b828ccd6406b4962d61baeba27c38d9f73dc"
123
123
  },
124
124
  "harness/planning/hypothesis": {
125
125
  "path": ".pi/agents/harness/planning/hypothesis.md",
126
- "sha256": "c974f5381aa562589942e8d52b48bdace6663e10caed6bf5f2fb9ce11d84b0bc"
126
+ "sha256": "5dac1020bc1d5a4100150959d52d983a4860210aa9cbda9120214191c2a17f1d"
127
127
  },
128
128
  "harness/planning/implementation-researcher": {
129
129
  "path": ".pi/agents/harness/planning/implementation-researcher.md",
@@ -131,15 +131,15 @@
131
131
  },
132
132
  "harness/planning/plan-adversary": {
133
133
  "path": ".pi/agents/harness/planning/plan-adversary.md",
134
- "sha256": "305cfa6cd0d4e6493a2dad2f01d8cb0b0dddc06df11f871746f6da7124c9d16b"
134
+ "sha256": "0c9abc088ced31705598baff143df992d435df751b01a9faad9d4af94df16c5a"
135
135
  },
136
136
  "harness/planning/plan-evaluator": {
137
137
  "path": ".pi/agents/harness/planning/plan-evaluator.md",
138
- "sha256": "1a6f465f4d400bcf32b9e82a1032ae789354f264af31c8d358b2a0dde7df81bf"
138
+ "sha256": "f85aba0adbbc7e726a51bfe2d6aa857ab39c8240cd08f06493bc1883ff387c5e"
139
139
  },
140
140
  "harness/planning/plan-synthesizer": {
141
141
  "path": ".pi/agents/harness/planning/plan-synthesizer.md",
142
- "sha256": "5bc3ec109179790c196df1328d362c1485cd5ff9295c31c3de93c050330295da"
142
+ "sha256": "3508126385d338b03f583aaa1f5d75f1cd1fcac8559fed52cd7db11ba1205536"
143
143
  },
144
144
  "harness/planning/planning-context": {
145
145
  "path": ".pi/agents/harness/planning/planning-context.md",
@@ -147,11 +147,11 @@
147
147
  },
148
148
  "harness/planning/review-integrator": {
149
149
  "path": ".pi/agents/harness/planning/review-integrator.md",
150
- "sha256": "3f60c41768cad24150718b4a415b9636b0df6892195986a90fc77e2d0a6be537"
150
+ "sha256": "ea810e3ecf50afe16cf70d12ca3107af94f2272434a72acc5f206bdf7ee89699"
151
151
  },
152
152
  "harness/planning/sprint-contract-auditor": {
153
153
  "path": ".pi/agents/harness/planning/sprint-contract-auditor.md",
154
- "sha256": "402c585168c5510b5f22837d2fb157726b928fa59108a8580437ac6ac08d04f5"
154
+ "sha256": "615feafa48481af0a94b3d6ea9903d58b532575447f5482e60c65e6eb780bfc0"
155
155
  },
156
156
  "harness/planning/stack-researcher": {
157
157
  "path": ".pi/agents/harness/planning/stack-researcher.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 (deniesReadOnlyBash(agentId, toolInput)) return false;
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 cancelled = raw === null || raw?.__cancelled === true;
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
- let choice = resolvePresenterChoice(validated, ctx.hasUI);
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";