@gtrabanco/pi-agentic-workflow 0.18.3 → 0.18.4

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.
@@ -2,6 +2,7 @@ import type { LoadedConfig } from "../config/load.js";
2
2
  import type { Catalogue } from "../routing/catalogue.js";
3
3
  import type { Router } from "../routing/dispatch.js";
4
4
  import { dirtyWorktreeWarning, receiptGuard } from "./receipt-guard.js";
5
+ import { mergeGuard } from "./merge-guard.js";
5
6
  import type { ExtensionSurface, InvocationContext, ModelRef, RoutingControls } from "../routing/types.js";
6
7
  import type { HintStore } from "../routing/state.js";
7
8
  /**
@@ -48,12 +49,14 @@ export interface ExtensionHandle<M extends ModelRef = ModelRef> {
48
49
  catalogue: Catalogue;
49
50
  /**
50
51
  * The Pi-free predicates the adapter (`index.ts`) binds to Pi's lifecycle
51
- * events: the inline-receipt block guard and the dirty-worktree notice. They
52
- * travel with the handle so the entry stays a thin translation layer and the
53
- * decisions are unit-tested with no session (issue #182).
52
+ * events: the inline-receipt block guard, the merge guard (owner-only
53
+ * merges, issue #278), and the dirty-worktree notice. They travel with the
54
+ * handle so the entry stays a thin translation layer and the decisions are
55
+ * unit-tested with no session (issues #182, #278).
54
56
  */
55
57
  guards: {
56
58
  receiptGuard: typeof receiptGuard;
59
+ mergeGuard: typeof mergeGuard;
57
60
  dirtyWorktreeWarning: typeof dirtyWorktreeWarning;
58
61
  };
59
62
  }
@@ -2,6 +2,7 @@ import { loadConfig } from "../config/load.js";
2
2
  import { readCatalogue } from "../routing/catalogue.js";
3
3
  import { createRouter } from "../routing/dispatch.js";
4
4
  import { dirtyWorktreeWarning, receiptGuard } from "./receipt-guard.js";
5
+ import { mergeGuard } from "./merge-guard.js";
5
6
  import { ADVANCE_COMMAND, SETTINGS_COMMAND, SETTINGS_COMMAND_ALIAS } from "../routing/types.js";
6
7
  import { registerAdvanceCommand } from "./conductor-command.js";
7
8
  import { createProfileStateStore, stateFilePath } from "../routing/state.js";
@@ -69,5 +70,5 @@ export function createExtension(deps) {
69
70
  description: "Show and configure per-command model routing",
70
71
  handler: settingsHandler,
71
72
  });
72
- return { router, catalogue, guards: { receiptGuard, dirtyWorktreeWarning } };
73
+ return { router, catalogue, guards: { receiptGuard, mergeGuard, dirtyWorktreeWarning } };
73
74
  }
@@ -211,8 +211,16 @@ export default function extension(pi) {
211
211
  // names the script that proves the receipt landed (issue #182). The guard
212
212
  // itself filters to `bash`, so every tool call is checked — nesting it under
213
213
  // one tool name would make the block unreachable for the calls it exists for.
214
+ //
215
+ // Merges are owner-only (issue #278): `gh pr merge`, the `gh api` merge
216
+ // endpoints, and `git merge` are blocked before anything else runs — a
217
+ // review waiver is not merge authorization. Read-only git (`merge-tree`,
218
+ // `merge-base`) and rebases pass; the reason hands the PR URL to the owner.
214
219
  pi.on("tool_call", (event, ctx) => {
215
220
  const command = "command" in event.input && typeof event.input.command === "string" ? event.input.command : undefined;
221
+ const merge = guards.mergeGuard({ toolName: event.toolName, command });
222
+ if (merge.block)
223
+ return merge;
216
224
  const receipt = guards.receiptGuard({ toolName: event.toolName, command });
217
225
  if (receipt.block)
218
226
  return receipt;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The merge guard — owner-only merges, enforced at the `tool_call` gate
3
+ * (issue #278).
4
+ *
5
+ * On 2026-09-29 an agent session ran `gh pr merge` after an owner message that
6
+ * waived only the *review* ("No hace falta una review para esto"), not the
7
+ * merge. Merging is owner-only: prose already says so (`audit-pr` "never
8
+ * merges") and prose demonstrably failed once, so the block lives here, in the
9
+ * same Pi-free layer as the inline-receipt guard (issue #182) — a predicate the
10
+ * shipped adapter binds to `tool_call`, unit-tested with no session.
11
+ *
12
+ * Same philosophy as `receipt-guard`: deterministic over clever. A bash command
13
+ * that merely *mentions* `gh pr merge` (a search for the guard's own text) is
14
+ * blocked like an invocation; the reason tells the agent to search `pr merge`
15
+ * instead. This is a guard, not a wall — it covers what routes through Pi's
16
+ * tool pipeline, exactly like the path-protection guard beside it.
17
+ *
18
+ * Pure on purpose: `index.ts` is the only file that imports Pi values, so this
19
+ * predicate compiles and is tested with no session.
20
+ */
21
+ /** The narrow slice of a Pi `tool_call` the guard reads. */
22
+ export interface ToolCallLike {
23
+ toolName: string;
24
+ command?: string | undefined;
25
+ }
26
+ /** The Pi `tool_call` result shape: `block` plus an actionable `reason`. */
27
+ export interface MergeGuardDecision {
28
+ block: boolean;
29
+ reason?: string;
30
+ }
31
+ /**
32
+ * The `tool_call` gate. Blocks merge shapes on the `bash` tool only — every
33
+ * other tool passes, and every non-merge command passes. Read-only git
34
+ * (`merge-tree`, `merge-base`), rebases, and PR reads (`gh pr view`, checks,
35
+ * comments) are deliberately out of scope; `gh pr comment` receipts remain the
36
+ * receipt guard's domain.
37
+ */
38
+ export declare function mergeGuard({ toolName, command }: ToolCallLike): MergeGuardDecision;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The merge guard — owner-only merges, enforced at the `tool_call` gate
3
+ * (issue #278).
4
+ *
5
+ * On 2026-09-29 an agent session ran `gh pr merge` after an owner message that
6
+ * waived only the *review* ("No hace falta una review para esto"), not the
7
+ * merge. Merging is owner-only: prose already says so (`audit-pr` "never
8
+ * merges") and prose demonstrably failed once, so the block lives here, in the
9
+ * same Pi-free layer as the inline-receipt guard (issue #182) — a predicate the
10
+ * shipped adapter binds to `tool_call`, unit-tested with no session.
11
+ *
12
+ * Same philosophy as `receipt-guard`: deterministic over clever. A bash command
13
+ * that merely *mentions* `gh pr merge` (a search for the guard's own text) is
14
+ * blocked like an invocation; the reason tells the agent to search `pr merge`
15
+ * instead. This is a guard, not a wall — it covers what routes through Pi's
16
+ * tool pipeline, exactly like the path-protection guard beside it.
17
+ *
18
+ * Pure on purpose: `index.ts` is the only file that imports Pi values, so this
19
+ * predicate compiles and is tested with no session.
20
+ */
21
+ /** `gh pr merge` as an invoked command, not as a quoted string elsewhere in it. */
22
+ const GH_PR_MERGE_RE = /\bgh\s+pr\s+merge\b/;
23
+ /** The REST merge endpoint and the GraphQL mutation, both under `gh api`. */
24
+ const GH_API_MERGE_RE = /\bgh\s+api\b[\s\S]*(?:pulls\/\d+\/merge\b|mergePullRequest\b)/;
25
+ /**
26
+ * `git merge <args>` (optionally after `-C <path>` / `-c <name>`), at a word
27
+ * boundary that is deliberately NOT `-`: `git merge-tree` and `git merge-base`
28
+ * are read-only and the reconciliation flow depends on them (issue #275).
29
+ */
30
+ const GIT_MERGE_RE = /(^|[\s;|&()])git\s+(?:-[Cc]\s+\S+\s+)*merge(?![\w-])(?=\s|$)/;
31
+ const GH_MERGE_REASON = "Merging is owner-only (merge guard, issue #278): a merge command from an agent session is blocked — a review waiver is not merge authorization. " +
32
+ "Open or refresh the PR and hand its URL to the owner (`gh pr view <N> --json url -q .url`); if you were searching text, search `pr merge` instead of `gh pr merge`.";
33
+ const GIT_MERGE_REASON = "Merging is owner-only (merge guard, issue #278): `git merge` from an agent session is blocked. " +
34
+ "Hand the branch to the owner for integration, or use the read-only forms (`git merge-tree`, `git merge-base`) and rebases, which stay allowed.";
35
+ /**
36
+ * The `tool_call` gate. Blocks merge shapes on the `bash` tool only — every
37
+ * other tool passes, and every non-merge command passes. Read-only git
38
+ * (`merge-tree`, `merge-base`), rebases, and PR reads (`gh pr view`, checks,
39
+ * comments) are deliberately out of scope; `gh pr comment` receipts remain the
40
+ * receipt guard's domain.
41
+ */
42
+ export function mergeGuard({ toolName, command }) {
43
+ if (toolName !== "bash")
44
+ return { block: false };
45
+ const text = command ?? "";
46
+ if (GH_PR_MERGE_RE.test(text) || GH_API_MERGE_RE.test(text)) {
47
+ return { block: true, reason: GH_MERGE_REASON };
48
+ }
49
+ if (GIT_MERGE_RE.test(text)) {
50
+ return { block: true, reason: GIT_MERGE_REASON };
51
+ }
52
+ return { block: false };
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtrabanco/pi-agentic-workflow",
3
- "version": "0.18.3",
3
+ "version": "0.18.4",
4
4
  "description": "Pi package: canonical agentic-workflow skills, friendly slash commands, and per-command model routing.",
5
5
  "keywords": [
6
6
  "pi-package",