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.
- package/.agents/skills/harness-decisions/SKILL.md +68 -2
- package/.agents/skills/harness-git-commit/SKILL.md +72 -0
- package/.agents/skills/harness-governor/SKILL.md +2 -2
- package/.agents/skills/harness-ls-lint-setup/SKILL.md +59 -0
- package/.agents/skills/harness-plan/SKILL.md +13 -11
- package/.agents/skills/harness-review/SKILL.md +1 -1
- package/.agents/skills/harness-sentrux-repair/SKILL.md +48 -0
- package/.agents/skills/sentrux/SKILL.md +4 -2
- package/.agents/skills/wiki-save/SKILL.md +1 -1
- package/.pi/PACKAGING.md +6 -0
- package/.pi/SYSTEM.md +21 -3
- package/.pi/agents/harness/ls-lint-steward.md +49 -0
- package/.pi/agents/harness/planning/decompose.md +4 -4
- package/.pi/agents/harness/reviewing/evaluator.md +1 -1
- package/.pi/agents/harness/running/executor.md +43 -2
- package/.pi/agents/harness/sentrux-repair-advisor.md +50 -0
- package/.pi/agents/pi-pi/prompt-expert.md +17 -2
- package/.pi/auto-commit.json +9 -2
- package/.pi/extensions/debate-orchestrator.ts +3 -0
- package/.pi/extensions/harness-anchored-edit.ts +139 -0
- package/.pi/extensions/harness-ask-user.ts +13 -34
- package/.pi/extensions/harness-debate-tools.ts +43 -4
- package/.pi/extensions/harness-live-widget.ts +28 -19
- package/.pi/extensions/harness-run-context.ts +278 -115
- package/.pi/extensions/harness-web-tools.ts +598 -471
- package/.pi/extensions/ls-lint-rules-sync.ts +103 -0
- package/.pi/extensions/observation-bus.ts +4 -0
- package/.pi/extensions/policy-gate.ts +270 -229
- package/.pi/extensions/sentrux-rules-sync.ts +2 -0
- package/.pi/extensions/soundboard.ts +48 -48
- package/.pi/harness/README.md +4 -0
- package/.pi/harness/agents.manifest.json +15 -7
- package/.pi/harness/agents.policy.yaml +47 -81
- package/.pi/harness/docs/adrs/0051-hash-anchored-executor-edits.md +41 -0
- package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
- package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
- package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
- package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
- package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
- package/.pi/harness/docs/adrs/README.md +7 -0
- package/.pi/harness/docs/practice-map.md +21 -5
- package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
- package/.pi/harness/evolution/self-healing-rules.json +16 -0
- package/.pi/harness/ls-lint/naming.manifest.json +128 -0
- package/.pi/harness/sentrux/architecture.manifest.json +1 -1
- package/.pi/harness/specs/auto-commit.schema.json +63 -0
- package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
- package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
- package/.pi/harness/specs/naming-manifest.schema.json +54 -0
- package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
- package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
- package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
- package/.pi/harness/specs/sentrux-report.schema.json +119 -0
- package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
- package/.pi/lib/agents-policy.d.mts +26 -47
- package/.pi/lib/agents-policy.mjs +84 -29
- package/.pi/lib/agents-policy.ts +1 -0
- package/.pi/lib/agt/build-evaluation-context.ts +136 -64
- package/.pi/lib/ask-user/constants.mjs +3 -0
- package/.pi/lib/ask-user/constants.ts +4 -0
- package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
- package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
- package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
- package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
- package/.pi/lib/ask-user/dialog.ts +2 -314
- package/.pi/lib/ask-user/fallback.ts +2 -78
- package/.pi/lib/ask-user/format.ts +85 -0
- package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
- package/.pi/lib/ask-user/index.ts +114 -0
- package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
- package/.pi/lib/ask-user/policy.mjs +43 -0
- package/.pi/lib/ask-user/policy.ts +104 -0
- package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
- package/.pi/lib/ask-user/presenters/headless.ts +131 -0
- package/.pi/lib/ask-user/presenters/select.ts +60 -0
- package/.pi/lib/ask-user/presenters/tui.ts +373 -0
- package/.pi/lib/ask-user/presenters/types.ts +13 -0
- package/.pi/lib/ask-user/render.ts +40 -9
- package/.pi/lib/ask-user/schema.ts +66 -13
- package/.pi/lib/ask-user/types.ts +60 -3
- package/.pi/lib/ask-user/validate-core.mjs +193 -7
- package/.pi/lib/ask-user/validate.ts +53 -34
- package/.pi/lib/harness-anchored-edit/.hash_anchors +1721 -0
- package/.pi/lib/harness-anchored-edit/anchor-state.ts +320 -0
- package/.pi/lib/harness-anchored-edit/apply-anchored-edits.ts +161 -0
- package/.pi/lib/harness-anchored-edit/edit-executor.ts +146 -0
- package/.pi/lib/harness-anchored-edit/index.ts +9 -0
- package/.pi/lib/harness-anchored-edit/line-protocol.ts +38 -0
- package/.pi/lib/harness-anchored-edit/package.json +3 -0
- package/.pi/lib/harness-anchored-edit/settings.ts +1 -0
- package/.pi/lib/harness-anchored-edit/task-id.ts +8 -0
- package/.pi/lib/harness-anchored-edit/types.ts +19 -0
- package/.pi/lib/harness-artifact-gate.ts +75 -21
- package/.pi/lib/harness-auto-commit-config.mjs +321 -0
- package/.pi/lib/harness-lens/clients/anchored-edit-autopatch.ts +158 -0
- package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
- package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
- package/.pi/lib/harness-lens/index.ts +246 -96
- package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
- package/.pi/lib/harness-repair-brief.ts +84 -25
- package/.pi/lib/harness-run-context.ts +42 -52
- package/.pi/lib/harness-sentrux-parse.mjs +272 -0
- package/.pi/lib/harness-sentrux-root.mjs +78 -0
- package/.pi/lib/harness-slash-completions.ts +116 -0
- package/.pi/lib/harness-spawn-topology.ts +121 -87
- package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
- package/.pi/lib/harness-subagents-bridge.ts +11 -6
- package/.pi/lib/harness-ui-state.ts +95 -48
- package/.pi/lib/plan-approval/dialog.ts +5 -0
- package/.pi/lib/plan-approval/validate.ts +1 -1
- package/.pi/lib/plan-approval-readiness.ts +32 -0
- package/.pi/lib/plan-debate-gate.ts +154 -114
- package/.pi/lib/plan-task-clarification.ts +158 -0
- package/.pi/prompts/harness-auto.md +2 -2
- package/.pi/prompts/harness-ls-lint-steward.md +43 -0
- package/.pi/prompts/harness-plan.md +58 -8
- package/.pi/prompts/harness-review.md +40 -6
- package/.pi/prompts/harness-run.md +33 -11
- package/.pi/prompts/harness-setup.md +72 -3
- package/.pi/prompts/harness-steer.md +3 -2
- package/.pi/prompts/wiki-save.md +5 -4
- package/.pi/scripts/README.md +8 -0
- package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
- package/.pi/scripts/harness-anchored-edit-smoke.mjs +45 -0
- package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
- package/.pi/scripts/harness-cli-verify.sh +47 -0
- package/.pi/scripts/harness-git-churn.mjs +77 -0
- package/.pi/scripts/harness-git-commit.mjs +173 -0
- package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
- package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
- package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
- package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
- package/.pi/scripts/harness-sentrux-report.mjs +256 -0
- package/.pi/scripts/harness-verify.mjs +347 -117
- package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
- package/.pi/scripts/run-tests.mjs +65 -0
- package/.pi/settings.example.json +1 -0
- package/.sentrux/rules.toml +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +31 -0
- package/README.md +13 -4
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/package.json +8 -3
- package/vendor/pi-subagents/src/agents.ts +5 -0
- package/vendor/pi-subagents/src/subagents.ts +22 -3
- package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
- 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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
state.phase
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
event.
|
|
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
|
-
|
|
278
|
+
entries,
|
|
279
|
+
ownerSessionId: runCtx?.owner_pi_session_id,
|
|
280
|
+
currentSessionId: sessionId,
|
|
320
281
|
},
|
|
321
282
|
);
|
|
322
|
-
if (
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
);
|
|
342
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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 =
|
|
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: ${
|
|
405
|
-
` 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
|
-
|
|
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
|
}
|