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
@@ -59,7 +59,6 @@ const PHASE_ORDER: HarnessPhase[] = [
59
59
  "merge",
60
60
  ];
61
61
 
62
- // @ts-expect-error pi extensions run as ESM
63
62
  const MODULE_URL = import.meta.url;
64
63
 
65
64
  const MUTATING_TOOLS = new Set(["write", "edit"]);
@@ -127,260 +126,302 @@ function getLatestPolicyStateFull(ctx: {
127
126
  return defaultState();
128
127
  }
129
128
 
130
- export default function policyGate(pi: ExtensionAPI) {
131
- if (!isHarnessProjectEnabled()) return;
132
- let state = defaultState();
133
-
134
- const appendPolicyState = (next: PolicyState): void => {
135
- state = next;
136
- pi.appendEntry("harness-policy-state", state);
137
- };
138
-
139
- pi.on("session_start", async (_event, ctx) => {
140
- state = getLatestPolicyStateFull(ctx);
141
- const booted = await bootstrapHarnessSubprocessFromEnv(pi, ctx);
142
- if (booted) {
143
- state = getLatestPolicyStateFull(ctx);
144
- }
145
- });
146
-
147
- pi.on("before_agent_start", async (event, ctx) => {
148
- const userPrompt = userVisiblePromptSlice(event.prompt);
149
- await bootstrapHarnessSubprocessFromEnv(pi, ctx);
150
- const entries = ctx.sessionManager.getEntries();
151
- state = getLatestPolicyStateFull(ctx);
152
- const bootstrapPrompt = isHarnessBootstrapPrompt(userPrompt);
153
- const abortSignal = hasHarnessAbortSignal(userPrompt);
154
-
155
- // /harness-setup instructions mention `harness-plan` (e.g. gh label text). That
156
- // substring must not force inferPhase() to "plan" or bootstrap stays blocked.
157
- if (bootstrapPrompt) {
158
- state.phase = "execute";
159
- state.approvedPlan = true;
160
- state.planId = null;
161
- state.budgetBypass = true;
162
- state.aborted = false;
163
- state.abortReason = null;
164
- state.abortedAt = null;
165
- state.updatedAt = nowIso();
166
- pi.appendEntry("harness-policy-state", state);
167
- return {
168
- systemPrompt: `${event.systemPrompt}\n\n[PolicyGate]\nPhase=${state.phase}; ApprovedPlan=${state.approvedPlan}; PlanId=${state.planId ?? "none"}; Aborted=${state.aborted}; Bootstrap=harness-setup.`,
169
- };
170
- }
171
-
172
- if (abortSignal) {
173
- state.phase = "plan";
174
- state.approvedPlan = false;
175
- state.planId = null;
176
- state.budgetBypass = false;
177
- state.aborted = true;
178
- state.abortReason = "harness-abort command";
179
- state.abortedAt = nowIso();
180
- state.updatedAt = state.abortedAt;
181
- pi.appendEntry("harness-policy-state", state);
182
- return {
183
- message: {
184
- customType: "harness-policy-aborted",
185
- display: true,
186
- content: [
187
- "Harness run aborted safely.",
188
- "Mutating tools are now blocked until a new approved plan is attached.",
189
- 'Next step: /harness-plan "<task>"',
190
- ].join("\n"),
191
- },
192
- systemPrompt: `${event.systemPrompt}\n\n[PolicyGate]\nAbort lock active. Mutating tools must remain blocked until a new approved plan is attached.`,
193
- };
194
- }
195
-
196
- const nextPhase = inferHarnessPhase(entries, userPrompt);
197
- const planSignal = hasApprovedPlanSignal(userPrompt, entries);
198
-
199
- const transitionBlock = getPolicyTransitionBlock(userPrompt, entries);
200
- if (transitionBlock.blocked) {
201
- return {
202
- message: {
203
- customType: "harness-policy-violation",
204
- display: true,
205
- content:
206
- transitionBlock.message ?? "Policy gate blocked this command.",
207
- },
208
- };
209
- }
210
-
211
- if (nextPhase === "plan") {
212
- state.approvedPlan = false;
213
- state.planId = null;
214
- }
215
-
216
- if (nextPhase === "execute" && !state.approvedPlan && !planSignal) {
217
- const runCtx = getLatestRunContext(entries);
218
- if (runCtx?.plan_ready) {
219
- state.approvedPlan = true;
220
- state.planId = runCtx.plan_id ?? state.planId;
221
- }
222
- }
129
+ async function handlePolicySessionStart(
130
+ pi: ExtensionAPI,
131
+ stateRef: { current: PolicyState },
132
+ ctx: any,
133
+ ): Promise<void> {
134
+ stateRef.current = getLatestPolicyStateFull(ctx);
135
+ const booted = await bootstrapHarnessSubprocessFromEnv(pi, ctx);
136
+ if (booted) {
137
+ stateRef.current = getLatestPolicyStateFull(ctx);
138
+ }
139
+ }
223
140
 
224
- if (planSignal) {
225
- state.approvedPlan = true;
226
- const planMatch = userPrompt.match(
227
- /plan[_-]?id["'\s:=]+([A-Za-z0-9._:-]+)/i,
228
- );
229
- state.planId = planMatch?.[1] ?? state.planId;
230
- state.aborted = false;
231
- state.abortReason = null;
232
- state.abortedAt = null;
233
- }
234
- state.budgetBypass = bootstrapPrompt;
235
- state.phase = nextPhase;
141
+ async function handlePolicyBeforeAgentStart(args: {
142
+ pi: ExtensionAPI;
143
+ stateRef: { current: PolicyState };
144
+ event: any;
145
+ ctx: any;
146
+ }) {
147
+ const { pi, stateRef, event, ctx } = args;
148
+ const userPrompt = userVisiblePromptSlice(event.prompt);
149
+ await bootstrapHarnessSubprocessFromEnv(pi, ctx);
150
+ const entries = ctx.sessionManager.getEntries();
151
+ const state = getLatestPolicyStateFull(ctx);
152
+ const bootstrapPrompt = isHarnessBootstrapPrompt(userPrompt);
153
+ const abortSignal = hasHarnessAbortSignal(userPrompt);
154
+
155
+ if (bootstrapPrompt) {
156
+ state.phase = "execute";
157
+ state.approvedPlan = true;
158
+ stateRef.current.planId = null;
159
+ state.budgetBypass = true;
160
+ state.aborted = false;
161
+ state.abortReason = null;
162
+ state.abortedAt = null;
236
163
  state.updatedAt = nowIso();
237
- pi.appendEntry("harness-policy-state", state);
238
-
239
- const planPhaseHint =
240
- state.phase === "plan"
241
- ? "\nPlan phase: scouts (parallel) → decompose → hypothesis (sequential) → implementation-researcher + stack-researcher (parallel) → execution-plan-author → validate-plan-dag → debate eligibility + Review Gate → approve_plan → create_plan (YAML plan-packet.yaml). Post-execute: /harness-review."
242
- : "";
164
+ stateRef.current = state;
165
+ pi.appendEntry("harness-policy-state", stateRef.current);
166
+ return {
167
+ systemPrompt: `${event.systemPrompt}\n\n[PolicyGate]\nPhase=${state.phase}; ApprovedPlan=${state.approvedPlan}; PlanId=${state.planId ?? "none"}; Aborted=${state.aborted}; Bootstrap=harness-setup.`,
168
+ };
169
+ }
243
170
 
171
+ if (abortSignal) {
172
+ stateRef.current.phase = "plan";
173
+ stateRef.current.approvedPlan = false;
174
+ stateRef.current.planId = null;
175
+ stateRef.current.budgetBypass = false;
176
+ stateRef.current.aborted = true;
177
+ state.abortReason = "harness-abort command";
178
+ stateRef.current.abortedAt = nowIso();
179
+ stateRef.current.updatedAt = stateRef.current.abortedAt;
180
+ stateRef.current = state;
181
+ pi.appendEntry("harness-policy-state", stateRef.current);
244
182
  return {
245
- systemPrompt: `${event.systemPrompt}\n\n[PolicyGate]\nPhase=${state.phase}; ApprovedPlan=${state.approvedPlan}; PlanId=${state.planId ?? "none"}; Aborted=${state.aborted}.${planPhaseHint}`,
183
+ message: {
184
+ customType: "harness-policy-aborted",
185
+ display: true,
186
+ content: [
187
+ "Harness run aborted safely.",
188
+ "Mutating tools are now blocked until a new approved plan is attached.",
189
+ 'Next step: /harness-plan "<task>"',
190
+ ].join("\n"),
191
+ },
192
+ systemPrompt: `${event.systemPrompt}\n\n[PolicyGate]\nAbort lock active. Mutating tools must remain blocked until a new approved plan is attached.`,
246
193
  };
247
- });
194
+ }
248
195
 
249
- pi.on("tool_call", async (event, ctx) => {
250
- state = getLatestPolicyStateFull(ctx);
251
- const entries = ctx.sessionManager.getEntries();
252
- const projectRoot = process.cwd();
253
- const sessionId = ctx.sessionManager.getSessionId();
254
-
255
- if (isHarnessAgtPolicyEnabled()) {
256
- return evaluateAgtHarnessToolCall({
257
- moduleUrl: MODULE_URL,
258
- toolName: event.toolName,
259
- toolInput: event.input as Record<string, unknown>,
260
- policyState: state,
261
- entries,
262
- sessionId,
263
- projectRoot,
264
- });
265
- }
196
+ const nextPhase = inferHarnessPhase(entries, userPrompt);
197
+ const planSignal = hasApprovedPlanSignal(userPrompt, entries);
198
+ const transitionBlock = getPolicyTransitionBlock(userPrompt, entries);
199
+ if (transitionBlock.blocked) {
200
+ return {
201
+ message: {
202
+ customType: "harness-policy-violation",
203
+ display: true,
204
+ content: transitionBlock.message ?? "Policy gate blocked this command.",
205
+ },
206
+ };
207
+ }
266
208
 
209
+ if (nextPhase === "plan") {
210
+ stateRef.current.approvedPlan = false;
211
+ stateRef.current.planId = null;
212
+ }
213
+ if (nextPhase === "execute" && !state.approvedPlan && !planSignal) {
267
214
  const runCtx = getLatestRunContext(entries);
268
-
269
- if (MUTATING_TOOLS.has(event.toolName)) {
270
- const decision = await isPlanPhaseAllowedMutation(
271
- event.toolName,
272
- event.input as Record<string, unknown>,
273
- state.phase,
274
- runCtx,
275
- projectRoot,
276
- {
277
- aborted: state.aborted,
278
- entries,
279
- ownerSessionId: runCtx?.owner_pi_session_id,
280
- currentSessionId: sessionId,
281
- },
282
- );
283
- if (!decision.allowed) {
284
- return { block: true, reason: decision.reason };
285
- }
286
- return undefined;
215
+ if (runCtx?.plan_ready) {
216
+ state.approvedPlan = true;
217
+ state.planId = runCtx.plan_id ?? state.planId;
287
218
  }
219
+ }
220
+ if (planSignal) {
221
+ state.approvedPlan = true;
222
+ const planMatch = userPrompt.match(
223
+ /plan[_-]?id["'\s:=]+([A-Za-z0-9._:-]+)/i,
224
+ );
225
+ state.planId = planMatch?.[1] ?? state.planId;
226
+ state.aborted = false;
227
+ state.abortReason = null;
228
+ state.abortedAt = null;
229
+ }
230
+ state.budgetBypass = bootstrapPrompt;
231
+ state.phase = nextPhase;
232
+ state.updatedAt = nowIso();
233
+ stateRef.current = state;
234
+ pi.appendEntry("harness-policy-state", stateRef.current);
235
+
236
+ const planPhaseHint =
237
+ state.phase === "plan"
238
+ ? "\nPlan phase: scouts (parallel) → decompose → hypothesis (sequential) → implementation-researcher + stack-researcher (parallel) → execution-plan-author → validate-plan-dag → debate eligibility + Review Gate → approve_plan → create_plan (YAML plan-packet.yaml). Post-execute: /harness-review."
239
+ : "";
240
+ return {
241
+ systemPrompt: `${event.systemPrompt}\n\n[PolicyGate]\nPhase=${state.phase}; ApprovedPlan=${state.approvedPlan}; PlanId=${state.planId ?? "none"}; Aborted=${state.aborted}.${planPhaseHint}`,
242
+ };
243
+ }
288
244
 
289
- if (event.toolName === "bash") {
290
- const command = String(event.input.command ?? "");
291
- const { isMutatingBash } = await import(
292
- "../lib/harness-context-mode-policy.js"
293
- );
294
- if (!isMutatingBash(command)) return undefined;
295
- if (state.aborted) {
296
- return {
297
- block: true,
298
- reason:
299
- "policy-gate: mutating bash command blocked because harness-abort lock is active. Attach a new approved plan first.",
300
- };
301
- }
302
- if (state.phase !== "execute" && state.phase !== "merge") {
303
- return {
304
- block: true,
305
- reason: `policy-gate: mutating bash command blocked in phase '${state.phase}'.`,
306
- };
307
- }
308
- }
245
+ async function handlePolicyToolCall(args: {
246
+ stateRef: { current: PolicyState };
247
+ event: any;
248
+ ctx: any;
249
+ }) {
250
+ const state = getLatestPolicyStateFull(args.ctx);
251
+ args.stateRef.current = state;
252
+ const entries = args.ctx.sessionManager.getEntries();
253
+ const projectRoot = process.cwd();
254
+ const sessionId = args.ctx.sessionManager.getSessionId();
255
+
256
+ if (isHarnessAgtPolicyEnabled()) {
257
+ return evaluateAgtHarnessToolCall({
258
+ moduleUrl: MODULE_URL,
259
+ toolName: args.event.toolName,
260
+ toolInput: args.event.input as Record<string, unknown>,
261
+ policyState: state,
262
+ entries,
263
+ sessionId,
264
+ projectRoot,
265
+ });
266
+ }
309
267
 
310
- const { evaluateContextModeMutation } = await import(
311
- "../lib/harness-context-mode-policy.js"
312
- );
313
- const ctxDecision = evaluateContextModeMutation(
314
- event.toolName,
315
- event.input as Record<string, unknown>,
268
+ const runCtx = getLatestRunContext(entries);
269
+ if (MUTATING_TOOLS.has(args.event.toolName)) {
270
+ const decision = await isPlanPhaseAllowedMutation(
271
+ args.event.toolName,
272
+ args.event.input as Record<string, unknown>,
316
273
  state.phase,
274
+ runCtx,
275
+ projectRoot,
317
276
  {
318
277
  aborted: state.aborted,
319
- budgetBypass: state.budgetBypass,
278
+ entries,
279
+ ownerSessionId: runCtx?.owner_pi_session_id,
280
+ currentSessionId: sessionId,
320
281
  },
321
282
  );
322
- if (ctxDecision.blocked) {
323
- return { block: true, reason: ctxDecision.reason };
283
+ if (!decision.allowed) return { block: true, reason: decision.reason };
284
+ return undefined;
285
+ }
286
+
287
+ if (args.event.toolName === "bash") {
288
+ const command = String(args.event.input.command ?? "");
289
+ const { isMutatingBash } = await import(
290
+ "../lib/harness-context-mode-policy.js"
291
+ );
292
+ if (!isMutatingBash(command)) return undefined;
293
+ if (state.aborted) {
294
+ return {
295
+ block: true,
296
+ reason:
297
+ "policy-gate: mutating bash command blocked because harness-abort lock is active. Attach a new approved plan first.",
298
+ };
299
+ }
300
+ if (state.phase !== "execute" && state.phase !== "merge") {
301
+ return {
302
+ block: true,
303
+ reason: `policy-gate: mutating bash command blocked in phase '${state.phase}'.`,
304
+ };
324
305
  }
306
+ }
325
307
 
326
- return undefined;
327
- });
308
+ const { evaluateContextModeMutation } = await import(
309
+ "../lib/harness-context-mode-policy.js"
310
+ );
311
+ const ctxDecision = evaluateContextModeMutation(
312
+ args.event.toolName,
313
+ args.event.input as Record<string, unknown>,
314
+ state.phase,
315
+ { aborted: state.aborted, budgetBypass: state.budgetBypass },
316
+ );
317
+ if (ctxDecision.blocked) return { block: true, reason: ctxDecision.reason };
318
+ return undefined;
319
+ }
328
320
 
329
- pi.on("tool_result", async (event, ctx) => {
330
- if (event.isError) return;
331
- if (event.toolName !== "write" && event.toolName !== "edit") return;
321
+ async function handlePolicyToolResult(args: {
322
+ pi: ExtensionAPI;
323
+ stateRef: { current: PolicyState };
324
+ event: any;
325
+ ctx: any;
326
+ appendPolicyState: (next: PolicyState) => void;
327
+ }): Promise<void> {
328
+ const { pi, stateRef, event, ctx, appendPolicyState } = args;
329
+ if (event.isError) return;
330
+ if (event.toolName !== "write" && event.toolName !== "edit") return;
331
+
332
+ const entries = ctx.sessionManager.getEntries();
333
+ const state = getLatestPolicyStateFull(ctx);
334
+ stateRef.current = state;
335
+ const projectRoot = process.cwd();
336
+ const runCtx = getLatestRunContext(entries);
337
+ if (!runCtx) return;
338
+
339
+ const target = extractWritePathFromToolInput(
340
+ event.input as Record<string, unknown>,
341
+ );
342
+ if (!target) return;
343
+ const scoped = await isPlanPhaseScopedWrite(target, runCtx, projectRoot);
344
+ if (!scoped) return;
345
+
346
+ const planPath = normalizeHarnessPath(target, projectRoot);
347
+ const packet = await readPlanPacketFromPath(planPath);
348
+ const validation = validatePlanPacket(packet);
349
+ if (!validation.valid || !packet?.plan_id) return;
350
+ if (!isHarnessAutoSession(entries)) return;
351
+
352
+ state.phase = "execute";
353
+ state.approvedPlan = true;
354
+ state.planId = packet.plan_id;
355
+ state.aborted = false;
356
+ state.abortReason = null;
357
+ state.abortedAt = null;
358
+ state.updatedAt = nowIso();
359
+ stateRef.current = state;
360
+ appendPolicyState(state);
361
+
362
+ runCtx.plan_ready = true;
363
+ runCtx.plan_id = packet.plan_id;
364
+ runCtx.phase = "execute";
365
+ runCtx.updated_at = nowIso();
366
+ pi.appendEntry("harness-run-context", runCtx);
367
+ void saveRunContextToDisk(runCtx);
368
+ void saveProjectActiveRun(runCtx);
369
+ }
332
370
 
333
- const entries = ctx.sessionManager.getEntries();
334
- state = getLatestPolicyStateFull(ctx);
335
- const projectRoot = process.cwd();
336
- const runCtx = getLatestRunContext(entries);
337
- if (!runCtx) return;
371
+ export default function policyGate(pi: ExtensionAPI) {
372
+ if (!isHarnessProjectEnabled()) return;
373
+ const stateRef: { current: PolicyState } = { current: defaultState() };
338
374
 
339
- const target = extractWritePathFromToolInput(
340
- event.input as Record<string, unknown>,
341
- );
342
- if (!target) return;
343
- const scoped = await isPlanPhaseScopedWrite(target, runCtx, projectRoot);
344
- if (!scoped) return;
375
+ const appendPolicyState = (next: PolicyState): void => {
376
+ stateRef.current = next;
377
+ pi.appendEntry("harness-policy-state", stateRef.current);
378
+ };
345
379
 
346
- const planPath = normalizeHarnessPath(target, projectRoot);
347
- const packet = await readPlanPacketFromPath(planPath);
348
- const validation = validatePlanPacket(packet);
349
- if (!validation.valid || !packet?.plan_id) return;
380
+ pi.on("session_start", async (_event, ctx) => {
381
+ await handlePolicySessionStart(pi, stateRef, ctx);
382
+ });
350
383
 
351
- if (isHarnessAutoSession(entries)) {
352
- state.phase = "execute";
353
- state.approvedPlan = true;
354
- state.planId = packet.plan_id;
355
- state.aborted = false;
356
- state.abortReason = null;
357
- state.abortedAt = null;
358
- state.updatedAt = nowIso();
359
- appendPolicyState(state);
360
-
361
- runCtx.plan_ready = true;
362
- runCtx.plan_id = packet.plan_id;
363
- runCtx.phase = "execute";
364
- runCtx.updated_at = nowIso();
365
- pi.appendEntry("harness-run-context", runCtx);
366
- void saveRunContextToDisk(runCtx);
367
- void saveProjectActiveRun(runCtx);
368
- }
384
+ pi.on("before_agent_start", async (event, ctx) =>
385
+ handlePolicyBeforeAgentStart({
386
+ pi,
387
+ stateRef,
388
+ event,
389
+ ctx,
390
+ }),
391
+ );
392
+
393
+ pi.on("tool_call", async (event, ctx) =>
394
+ handlePolicyToolCall({
395
+ stateRef,
396
+ event,
397
+ ctx,
398
+ }),
399
+ );
400
+
401
+ pi.on("tool_result", async (event, ctx) => {
402
+ await handlePolicyToolResult({
403
+ pi,
404
+ stateRef,
405
+ event,
406
+ ctx,
407
+ appendPolicyState,
408
+ });
369
409
  });
370
410
 
371
411
  pi.registerCommand("harness-abort", {
372
412
  description: "Safely abort current harness run and reset to plan phase",
373
413
  handler: async (args, ctx) => {
374
414
  const reason = args.trim();
375
- state.phase = "plan";
376
- state.approvedPlan = false;
377
- state.planId = null;
378
- state.budgetBypass = false;
379
- state.aborted = true;
380
- state.abortReason = reason.length > 0 ? reason : "manual abort";
381
- state.abortedAt = nowIso();
382
- state.updatedAt = state.abortedAt;
383
- pi.appendEntry("harness-policy-state", state);
415
+ stateRef.current.phase = "plan";
416
+ stateRef.current.approvedPlan = false;
417
+ stateRef.current.planId = null;
418
+ stateRef.current.budgetBypass = false;
419
+ stateRef.current.aborted = true;
420
+ stateRef.current.abortReason =
421
+ reason.length > 0 ? reason : "manual abort";
422
+ stateRef.current.abortedAt = nowIso();
423
+ stateRef.current.updatedAt = stateRef.current.abortedAt;
424
+ pi.appendEntry("harness-policy-state", stateRef.current);
384
425
 
385
426
  const runCtx = getLatestRunContext(ctx.sessionManager.getEntries());
386
427
  if (runCtx) {
@@ -391,7 +432,7 @@ export default function policyGate(pi: ExtensionAPI) {
391
432
  runCtx.next_recommended_command = runCtx.task_summary
392
433
  ? `/harness-plan "${runCtx.task_summary}"`
393
434
  : '/harness-plan "<task>"';
394
- runCtx.updated_at = state.abortedAt ?? nowIso();
435
+ runCtx.updated_at = stateRef.current.abortedAt ?? nowIso();
395
436
  pi.appendEntry("harness-run-context", runCtx);
396
437
  void saveRunContextToDisk(runCtx);
397
438
  void saveProjectActiveRun(runCtx);
@@ -401,8 +442,8 @@ export default function policyGate(pi: ExtensionAPI) {
401
442
  "Harness run aborted safely.",
402
443
  " phase: plan",
403
444
  " approvedPlan: false",
404
- ` abortReason: ${state.abortReason}`,
405
- ` abortedAt: ${state.abortedAt}`,
445
+ ` abortReason: ${stateRef.current.abortReason}`,
446
+ ` abortedAt: ${stateRef.current.abortedAt}`,
406
447
  "Mutating tools are now blocked until a new approved plan is attached.",
407
448
  'Next command: /harness-plan "<task>"',
408
449
  ];
@@ -6,6 +6,7 @@ import { spawn } from "node:child_process";
6
6
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
7
  import { resolveHarnessScript } from "../lib/harness-paths.js";
8
8
  import { isHarnessProjectEnabled } from "../lib/harness-project-config.js";
9
+ import { completeStrictFlag } from "../lib/harness-slash-completions.js";
9
10
 
10
11
  function resolveSyncScript(): string {
11
12
  return resolveHarnessScript(
@@ -81,6 +82,7 @@ export default function sentruxRulesSync(pi: ExtensionAPI) {
81
82
  pi.registerCommand("harness-sentrux-sync", {
82
83
  description:
83
84
  "Regenerate .sentrux/rules.toml from harness architecture manifest",
85
+ getArgumentCompletions: completeStrictFlag,
84
86
  handler: async (_args, ctx) => {
85
87
  const strict = _args.includes("--strict");
86
88
  const { code, output } = await runSync(
@@ -387,6 +387,53 @@ function readAgentEndOutcome(event: unknown): {
387
387
  return { status: "completed" };
388
388
  }
389
389
 
390
+ function registerSoundsCommand(pi: ExtensionAPI): void {
391
+ pi.registerCommand("sounds", {
392
+ description: "Show pi-sounds status: loaded sounds, player",
393
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
394
+ const soundCtx = await getSoundContext();
395
+ if (!soundCtx) {
396
+ const msg = "pi-sounds: no .pi/sounds/project-sounds.json found";
397
+ if (ctx.hasUI) ctx.ui.notify(msg, "warning");
398
+ else
399
+ pi.sendMessage({
400
+ customType: "pi-sounds",
401
+ content: msg,
402
+ display: true,
403
+ });
404
+ return;
405
+ }
406
+
407
+ const player = await getPlayer();
408
+ const playerName = player ? player.cmd : "none";
409
+ const total = SOUND_CATEGORIES.reduce(
410
+ (sum, c) => sum + soundCtx.soundsByCategory[c].length,
411
+ 0,
412
+ );
413
+ const lines = [
414
+ `pi-sounds status:`,
415
+ ` player: ${playerName}`,
416
+ ` sounds: ${total} files in ${soundCtx.soundsDirectory}`,
417
+ ` randomize: ${soundCtx.randomizeSounds ? "yes" : "no"}`,
418
+ ` categories:`,
419
+ ];
420
+ for (const cat of SOUND_CATEGORIES) {
421
+ const files = soundCtx.soundsByCategory[cat];
422
+ lines.push(
423
+ ` ${cat}: ${files.length > 0 ? files.map((f) => basename(f)).join(", ") : "(none)"}`,
424
+ );
425
+ }
426
+ if (ctx.hasUI) ctx.ui.notify(lines.join("\n"), "info");
427
+ else
428
+ pi.sendMessage({
429
+ customType: "pi-sounds",
430
+ content: lines.join("\n"),
431
+ display: true,
432
+ });
433
+ },
434
+ });
435
+ }
436
+
390
437
  // ── Extension ─────────────────────────────────────────────────────
391
438
 
392
439
  export default function piSoundsExtension(pi: ExtensionAPI): void {
@@ -482,52 +529,5 @@ export default function piSoundsExtension(pi: ExtensionAPI): void {
482
529
  hadErrorInTurn = false;
483
530
  });
484
531
 
485
- // ── Command ────────────────────────────────────────────────────
486
-
487
- pi.registerCommand("sounds", {
488
- description: "Show pi-sounds status: loaded sounds, player",
489
- handler: async (_args: string, ctx: ExtensionCommandContext) => {
490
- const soundCtx = await getSoundContext();
491
- if (!soundCtx) {
492
- const msg = "pi-sounds: no .pi/sounds/project-sounds.json found";
493
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
494
- else
495
- pi.sendMessage({
496
- customType: "pi-sounds",
497
- content: msg,
498
- display: true,
499
- });
500
- return;
501
- }
502
-
503
- const player = await getPlayer();
504
- const playerName = player ? player.cmd : "none";
505
- const total = SOUND_CATEGORIES.reduce(
506
- (sum, c) => sum + soundCtx.soundsByCategory[c].length,
507
- 0,
508
- );
509
-
510
- const lines = [
511
- `pi-sounds status:`,
512
- ` player: ${playerName}`,
513
- ` sounds: ${total} files in ${soundCtx.soundsDirectory}`,
514
- ` randomize: ${soundCtx.randomizeSounds ? "yes" : "no"}`,
515
- ` categories:`,
516
- ];
517
- for (const cat of SOUND_CATEGORIES) {
518
- const files = soundCtx.soundsByCategory[cat];
519
- lines.push(
520
- ` ${cat}: ${files.length > 0 ? files.map((f) => basename(f)).join(", ") : "(none)"}`,
521
- );
522
- }
523
-
524
- if (ctx.hasUI) ctx.ui.notify(lines.join("\n"), "info");
525
- else
526
- pi.sendMessage({
527
- customType: "pi-sounds",
528
- content: lines.join("\n"),
529
- display: true,
530
- });
531
- },
532
- });
532
+ registerSoundsCommand(pi);
533
533
  }