@gtrabanco/pi-agentic-workflow 0.37.0 → 4.10.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 (52) hide show
  1. package/dist/config/defaults.js +8 -0
  2. package/dist/config/merge.js +20 -0
  3. package/dist/config/schema.js +103 -1
  4. package/dist/config/types.d.ts +60 -0
  5. package/dist/extension/ci-guard.d.ts +29 -0
  6. package/dist/extension/ci-guard.js +39 -0
  7. package/dist/extension/cost-guard.d.ts +30 -0
  8. package/dist/extension/cost-guard.js +50 -0
  9. package/dist/extension/factory.d.ts +11 -4
  10. package/dist/extension/factory.js +4 -1
  11. package/dist/extension/guard-util.d.ts +26 -0
  12. package/dist/extension/guard-util.js +35 -0
  13. package/dist/extension/history-guard.d.ts +59 -0
  14. package/dist/extension/history-guard.js +132 -0
  15. package/dist/extension/index.js +21 -5
  16. package/dist/extension/merge-guard.d.ts +26 -28
  17. package/dist/extension/merge-guard.js +64 -38
  18. package/package.json +7 -6
  19. package/skills/audit-docs/SKILL.md +1 -1
  20. package/skills/audit-pr/SKILL.md +12 -1
  21. package/skills/discover-repository-state/SKILL.md +1 -1
  22. package/skills/execute-phase/SKILL.md +61 -6
  23. package/skills/fold-findings/SKILL.md +126 -8
  24. package/skills/fold-findings/references/FOLD_PROCESS.md +7 -1
  25. package/skills/init-workspace/SKILL.md +1 -1
  26. package/skills/log-session/SKILL.md +1 -1
  27. package/skills/orchestration-envelope/SKILL.md +1 -1
  28. package/skills/phase-contract/SKILL.md +1 -1
  29. package/skills/pre-execution-review/SKILL.md +1 -1
  30. package/skills/product-audit/SKILL.md +4 -1
  31. package/skills/product-audit/references/AUDIT_DIMENSIONS.md +34 -0
  32. package/skills/product-audit/references/AUDIT_PROCESS.md +5 -1
  33. package/skills/resolve-repository-state/SKILL.md +1 -1
  34. package/skills/review-a11y/SKILL.md +1 -1
  35. package/skills/review-brand/SKILL.md +1 -1
  36. package/skills/review-change/SKILL.md +18 -3
  37. package/skills/review-code/SKILL.md +28 -2
  38. package/skills/review-debt/SKILL.md +8 -2
  39. package/skills/review-design/SKILL.md +1 -1
  40. package/skills/review-implementation/SKILL.md +1 -1
  41. package/skills/review-implementation/references/CLASSIFY.md +25 -0
  42. package/skills/review-perf/SKILL.md +1 -1
  43. package/skills/review-security/SKILL.md +1 -1
  44. package/skills/review-seo/SKILL.md +1 -1
  45. package/skills/review-verify/SKILL.md +19 -2
  46. package/skills/triage-issue/SKILL.md +9 -1
  47. package/skills/triage-issue/references/LABELS.md +22 -0
  48. package/skills/unit-lane/SKILL.md +26 -7
  49. package/skills/unit-lane/references/DESIGN.md +55 -1
  50. package/skills/unit-lane/references/PLAN.md +29 -0
  51. package/skills/verification-contract/SKILL.md +9 -1
  52. package/skills/workflow-status/SKILL.md +1 -1
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The history guard — published history is owner-written, decided by git
3
+ * itself (`git push --dry-run`), not by enumerating spellings.
4
+ *
5
+ * AGENTS.md → Working rules states the way this repository integrates work:
6
+ * short branches off `main`, `main` only advances through a PR, and when `main`
7
+ * moves the conflicted branch is handed back for the owner to integrate. A
8
+ * local `git rebase` is a private rewrite; the moment it is pushed it rewrites
9
+ * commits a reviewer, another session or a CI artifact may already be building
10
+ * on, so the block sits where the rewrite becomes public — on `git push`.
11
+ *
12
+ * The outcome, not the spelling, is what is judged. Every direct `git push`
13
+ * runs first as `git push --dry-run` (the same arguments, plus `--dry-run` —
14
+ * git resolves flags and refspecs itself), and the printed outcome decides: a
15
+ * `(forced update)`, a `[deleted]` ref, or a destination of `main` declines
16
+ * the push. One mechanism covers every force flag spelling and `+`/`:ref`
17
+ * refspec, because the answer comes from git, not from this file. When the
18
+ * dry-run cannot run (offline — where a real push would fail anyway), a simple
19
+ * text check on the push's own segment is the fallback.
20
+ *
21
+ * The guard is config-gated (`guards.history`): off by default for every
22
+ * project, enabled where an operator asks for it. The only way through is an
23
+ * explicit owner instruction in the same turn, carried as the command's first
24
+ * word: `AW_ALLOW=force` — which never enables a push to `main`, because main
25
+ * advances only through a PR.
26
+ *
27
+ * What is deliberately out of scope: payloads hidden inside scripts or
28
+ * interpreter one-liners that never spell `git push` in the command text, and
29
+ * a git alias invoked as `git fp` — the dry-run sees what git is told to push,
30
+ * not what an alias named `fp` expands to. The dry-run sees what git sees; it
31
+ * cannot see what a script builds at runtime, and no text predicate honestly
32
+ * can. The residual risk is documented, not chased with a regex zoo.
33
+ */
34
+ import { spawnSync } from "node:child_process";
35
+ import { explicitAllow, splitSegments } from "./guard-util.js";
36
+ /** A direct `git push` (optionally after `-C <path>` / `-c <name=value>`). */
37
+ const GIT_PUSH_RE = /(?:^|[\s;|&])git\s+(?:-[Cc]\s+\S+\s+)*push(?![\w-])(?=\s|$)/u;
38
+ /** The REST twin of a force push: `gh api` moving a ref with `force`. */
39
+ const GH_API_FORCE_REF_RE = /\bgh\s+api\b[\s\S]*refs\/heads\/[\s\S]*\bforce\b/u;
40
+ /** Offline fallback — the flags that make a push rewrite or delete. */
41
+ const FALLBACK_REWRITE_RE = /(?:^|\s)(?:--force(?:-with-lease(?:=\S*)?|-if-includes)?|--mirror|--all|--branches|--delete)(?=\s|$)/u;
42
+ const FALLBACK_SHORT_RE = /(?:^|\s)-[a-z]*[fd][a-z]*(?=\s|$)/u;
43
+ const FALLBACK_REFSPEC_RE = /(?:^|\s)[+:]\S+|^:\S+/u;
44
+ const FALLBACK_MAIN_RE = /(?:^|[\s:])(?:refs\/heads\/)?main(?=\s|$)/u;
45
+ /** The default runner: git in the session's working directory. */
46
+ export function spawnPushRunner(cwd) {
47
+ return (args) => {
48
+ const result = spawnSync("git", ["push", "--dry-run", ...args], {
49
+ cwd,
50
+ encoding: "utf8",
51
+ stdio: ["ignore", "pipe", "pipe"],
52
+ timeout: 30_000,
53
+ });
54
+ return { status: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
55
+ };
56
+ }
57
+ /** Shell-tokenize the arguments of one push segment (quotes stripped). */
58
+ export function pushArgs(segment) {
59
+ const rest = segment.replace(/^[\s]*git\s+(?:-[Cc]\s+\S+\s+)*push(?![\w-])\s?/u, "");
60
+ return rest
61
+ .split(/\s+/u)
62
+ .filter((token) => token !== "")
63
+ .map((token) => token.replace(/^["'`]|["'`]$/gu, ""));
64
+ }
65
+ /** Read the outcome git printed for one dry-run. */
66
+ export function readPushOutcome(result) {
67
+ if (result.status !== 0)
68
+ return "unknown";
69
+ for (const line of result.stdout.split("\n")) {
70
+ if (/\(forced update\)/u.test(line))
71
+ return "forced";
72
+ if (/\[deleted\]/u.test(line))
73
+ return "deleted";
74
+ const arrow = /->\s*(\S+)\s*$/u.exec(line.trim());
75
+ if (arrow) {
76
+ const ref = arrow[1].replace(/^refs\/heads\//u, "");
77
+ if (ref === "main")
78
+ return "main";
79
+ }
80
+ }
81
+ return "clean";
82
+ }
83
+ /** The offline fallback, scoped to one push segment. */
84
+ function fallbackOutcome(segment) {
85
+ const args = pushArgs(segment);
86
+ const text = args.join(" ");
87
+ if (FALLBACK_REWRITE_RE.test(` ${text} `) || FALLBACK_SHORT_RE.test(` ${text} `) || FALLBACK_REFSPEC_RE.test(` ${text} `)) {
88
+ return "forced";
89
+ }
90
+ if (FALLBACK_MAIN_RE.test(` ${text} `))
91
+ return "main";
92
+ return "unknown";
93
+ }
94
+ /**
95
+ * The `tool_call` gate. `enabled` comes from the config (`guards.history`);
96
+ * the guard is a no-op while disabled. `runner`/`cwd` are injectable so the
97
+ * suite never touches the network.
98
+ */
99
+ export function historyGuard({ toolName, command }, enabled = true, { runner, cwd = process.cwd(), offlineFallback = fallbackOutcome } = {}) {
100
+ if (!enabled || toolName !== "bash")
101
+ return { block: false };
102
+ const text = command ?? "";
103
+ const allow = explicitAllow(text);
104
+ // `gh api` forcing a ref update is a push by other means; the dry-run never
105
+ // sees it, so the text is the only witness.
106
+ if (!allow.has("force") && GH_API_FORCE_REF_RE.test(text)) {
107
+ return { block: true, reason: `${HISTORY_REASON} (blocked: \`gh api\` forcing a ref update)` };
108
+ }
109
+ for (const segment of splitSegments(text)) {
110
+ if (!GIT_PUSH_RE.test(segment))
111
+ continue;
112
+ const args = pushArgs(segment);
113
+ const run = runner ?? spawnPushRunner(cwd);
114
+ let outcome = readPushOutcome(run(args));
115
+ if (outcome === "unknown")
116
+ outcome = offlineFallback(segment);
117
+ if (outcome === "forced" || outcome === "deleted") {
118
+ if (!allow.has("force")) {
119
+ return { block: true, reason: `${HISTORY_REASON} (git reports: ${outcome === "forced" ? "a forced update" : "a ref delete"})` };
120
+ }
121
+ }
122
+ if (outcome === "main") {
123
+ // `force` never enables main: main advances only through a PR.
124
+ return { block: true, reason: `${HISTORY_REASON} (blocked: a push whose destination is \`main\`)` };
125
+ }
126
+ }
127
+ return { block: false };
128
+ }
129
+ export const HISTORY_REASON = "Published history is owner-written (AGENTS.md → Working rules): an agent session does not rewrite, delete or force commits that are already pushed, and never pushes `main` — main advances only through a PR. " +
130
+ "The push was declined by asking git what it would do (`git push --dry-run`). " +
131
+ "Only an explicit owner instruction in this turn (\"Sí, usa force\") enables it, carried as the command's first word: `AW_ALLOW=force` — which never enables a push to `main`. " +
132
+ "A conflict with `main` is resolved by handing the branch back: the owner runs the integration.";
@@ -297,15 +297,31 @@ export default function extension(pi) {
297
297
  // itself filters to `bash`, so every tool call is checked — nesting it under
298
298
  // one tool name would make the block unreachable for the calls it exists for.
299
299
  //
300
- // Merges are owner-only (issue #278): `gh pr merge`, the `gh api` merge
301
- // endpoints, and `git merge` are blocked before anything else runs — a
302
- // review waiver is not merge authorization. Read-only git (`merge-tree`,
303
- // `merge-base`) and rebases pass; the reason hands the PR URL to the owner.
300
+ // Owner-gated tool guards, all config-driven (`guards` in the config files;
301
+ // every guard ships OFF — a project opts in per guard):
302
+ // merge `git merge`, `gh pr merge`, `git pull`'s merge form (issue #278).
303
+ // history pushes decided by git itself (`git push --dry-run`): a forced
304
+ // update, a ref delete or a push to `main` is declined.
305
+ // ci writing workflow definitions and triggering workflow runs.
306
+ // cost commands estimated above the configured token budget.
307
+ // The only way through any of them is the explicit per-command token
308
+ // (`AW_ALLOW=merge|force|ci|cost`) after an owner instruction in the turn.
304
309
  pi.on("tool_call", (event, ctx) => {
305
310
  const command = "command" in event.input && typeof event.input.command === "string" ? event.input.command : undefined;
306
- const merge = guards.mergeGuard({ toolName: event.toolName, command });
311
+ const inputPath = "path" in event.input && typeof event.input.path === "string" ? event.input.path : undefined;
312
+ const { guards: policy } = loadConfig({ agentDir, cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() }).config;
313
+ const merge = guards.mergeGuard({ toolName: event.toolName, command }, policy.merge);
307
314
  if (merge.block)
308
315
  return merge;
316
+ const history = guards.historyGuard({ toolName: event.toolName, command }, policy.history, { cwd: ctx.cwd });
317
+ if (history.block)
318
+ return history;
319
+ const ci = guards.ciGuard({ toolName: event.toolName, command, path: inputPath }, policy.ci);
320
+ if (ci.block)
321
+ return ci;
322
+ const cost = guards.costGuard({ toolName: event.toolName, command }, policy.expensiveTests);
323
+ if (cost.block)
324
+ return cost;
309
325
  const receipt = guards.receiptGuard({ toolName: event.toolName, command });
310
326
  if (receipt.block)
311
327
  return receipt;
@@ -1,38 +1,36 @@
1
1
  /**
2
- * The merge guard — owner-only merges, enforced at the `tool_call` gate
3
- * (issue #278).
2
+ * The merge guard — owner-only merges at the `tool_call` gate (issue #278).
4
3
  *
5
4
  * On 2026-09-29 an agent session ran `gh pr merge` after an owner message that
6
5
  * 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.
6
+ * merge. Merging is owner-only, and the direct routes are self-evident — a
7
+ * command that *is* a merge needs no simulation and no spelling census:
11
8
  *
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.
9
+ * `git merge <ref>` the command is a merge (read-only forms —
10
+ * `merge-tree`, `merge-base`, `merge-file` — stay out).
11
+ * `gh pr merge` the command merges the PR.
12
+ * `gh api` merge the REST endpoint (`pulls/N/merge`) and the GraphQL
13
+ * mutations (`mergePullRequest`, `mergeBranch`,
14
+ * `enablePullRequestAutoMerge`) — the same merge by
15
+ * other syntax, still self-evident.
16
+ * `git pull` fetch + merge by default; only `--rebase`/`--ff-only`
17
+ * (no merge commit) pass.
17
18
  *
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.
19
+ * The guard is config-gated (`guards.merge`): off by default for every
20
+ * project, enabled where an operator asks for it. The only way through is an
21
+ * explicit owner instruction in the same turn, carried as the command's first
22
+ * word: `AW_ALLOW=merge`.
23
+ *
24
+ * What is deliberately out of scope: merges hidden inside scripts or
25
+ * interpreter one-liners. A text predicate that chased every wrapper would
26
+ * grow a regex zoo and still miss the file it never read; the direct routes
27
+ * above are the honest boundary.
20
28
  */
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
- }
29
+ import { type GuardDecision, type ToolCallLike } from "./guard-util.js";
30
+ export declare const MERGE_REASON: string;
31
31
  /**
32
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.
33
+ * other tool passes, and every non-merge command passes. `enabled` comes from
34
+ * the config (`guards.merge`); the guard is a no-op while disabled.
37
35
  */
38
- export declare function mergeGuard({ toolName, command }: ToolCallLike): MergeGuardDecision;
36
+ export declare function mergeGuard({ toolName, command }: ToolCallLike, enabled?: boolean): GuardDecision;
@@ -1,53 +1,79 @@
1
1
  /**
2
- * The merge guard — owner-only merges, enforced at the `tool_call` gate
3
- * (issue #278).
2
+ * The merge guard — owner-only merges at the `tool_call` gate (issue #278).
4
3
  *
5
4
  * On 2026-09-29 an agent session ran `gh pr merge` after an owner message that
6
5
  * 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.
6
+ * merge. Merging is owner-only, and the direct routes are self-evident — a
7
+ * command that *is* a merge needs no simulation and no spelling census:
11
8
  *
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.
9
+ * `git merge <ref>` the command is a merge (read-only forms —
10
+ * `merge-tree`, `merge-base`, `merge-file` — stay out).
11
+ * `gh pr merge` the command merges the PR.
12
+ * `gh api` merge the REST endpoint (`pulls/N/merge`) and the GraphQL
13
+ * mutations (`mergePullRequest`, `mergeBranch`,
14
+ * `enablePullRequestAutoMerge`) — the same merge by
15
+ * other syntax, still self-evident.
16
+ * `git pull` fetch + merge by default; only `--rebase`/`--ff-only`
17
+ * (no merge commit) pass.
17
18
  *
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).
19
+ * The guard is config-gated (`guards.merge`): off by default for every
20
+ * project, enabled where an operator asks for it. The only way through is an
21
+ * explicit owner instruction in the same turn, carried as the command's first
22
+ * word: `AW_ALLOW=merge`.
23
+ *
24
+ * What is deliberately out of scope: merges hidden inside scripts or
25
+ * interpreter one-liners. A text predicate that chased every wrapper would
26
+ * grow a regex zoo and still miss the file it never read; the direct routes
27
+ * above are the honest boundary.
29
28
  */
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.";
29
+ import { explicitAllow, splitSegments } from "./guard-util.js";
30
+ /** `git merge` at a word boundary — `merge-tree`/`merge-base`/`merge-file` stay out. */
31
+ const GIT_MERGE_RE = /(?:^|[\s;|&])git\s+(?:-[Cc]\s+\S+\s+)*merge(?![\w-])(?=\s|$)/u;
32
+ /** `git pull` — merge by default. */
33
+ const GIT_PULL_RE = /(?:^|[\s;|&])git\s+(?:-[Cc]\s+\S+\s+)*pull(?![\w-])(?=\s|$)/u;
34
+ /** pull flags that make it a rebase or a fast-forward-only update: no merge commit. */
35
+ const PULL_NO_MERGE_RE = /(?:^|\s)(?:--rebase(?:=(?!=false)\S*)?|--ff-only|-[a-z]*r[a-z]*)(?=\s|$)/u;
36
+ /** pull flags that force the merge form back on. */
37
+ const PULL_MERGE_RE = /(?:^|\s)(?:--no-rebase|--rebase=false)(?=\s|$)/u;
38
+ const GH_PR_MERGE_RE = /\bgh\s+pr\s+merge\b/u;
39
+ /** The REST endpoint and GraphQL mutations that are a merge by other syntax. */
40
+ const GH_API_MERGE_RE = /\bgh\s+api\b[\s\S]*(?:pulls\/\d+\/merge\b|\b(?:mergePullRequest|mergeBranch|enablePullRequestAutoMerge)\b)/u;
41
+ /** The merge shape one command segment carries, or null. */
42
+ function mergeShape(segment) {
43
+ if (GH_PR_MERGE_RE.test(segment))
44
+ return "`gh pr merge`";
45
+ if (GH_API_MERGE_RE.test(segment))
46
+ return "a `gh api` merge endpoint or mutation";
47
+ if (GIT_MERGE_RE.test(segment))
48
+ return "`git merge`";
49
+ if (GIT_PULL_RE.test(segment)) {
50
+ if (PULL_MERGE_RE.test(segment))
51
+ return "`git pull` in its merge form";
52
+ if (!PULL_NO_MERGE_RE.test(segment)) {
53
+ return "`git pull` in its merge form (use `git pull --rebase`, `--ff-only`, or `git fetch` + ask)";
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ export const 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. " +
59
+ "Only an explicit owner instruction in this turn (\"Sí, mergea la PR\" / \"Mergea rama B en rama A\") enables it, carried as the command's first word: `AW_ALLOW=merge`. " +
60
+ "Otherwise: resolve the files, report, and hand the branch to the owner (`gh pr view <N> --json url -q .url`). " +
61
+ "If you were searching text, search `pr merge` instead of `gh pr merge`.";
35
62
  /**
36
63
  * 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.
64
+ * other tool passes, and every non-merge command passes. `enabled` comes from
65
+ * the config (`guards.merge`); the guard is a no-op while disabled.
41
66
  */
42
- export function mergeGuard({ toolName, command }) {
43
- if (toolName !== "bash")
67
+ export function mergeGuard({ toolName, command }, enabled = true) {
68
+ if (!enabled || toolName !== "bash")
44
69
  return { block: false };
45
70
  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 };
71
+ if (explicitAllow(text).has("merge"))
72
+ return { block: false };
73
+ for (const segment of splitSegments(text)) {
74
+ const shape = mergeShape(segment);
75
+ if (shape)
76
+ return { block: true, reason: `${MERGE_REASON} (blocked: ${shape})` };
51
77
  }
52
78
  return { block: false };
53
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtrabanco/pi-agentic-workflow",
3
- "version": "0.37.0",
3
+ "version": "4.10.0",
4
4
  "description": "Pi package: canonical agentic-workflow skills, friendly slash commands, and per-command model routing.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -43,25 +43,26 @@
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsc",
46
+ "build:deps": "bun run --cwd ../agentic-workflow-schema build",
46
47
  "bundle:skills": "bun scripts/bundle-skills.mjs",
47
48
  "clean:skills": "bun scripts/bundle-skills.mjs --clean",
48
49
  "test": "bun run bundle:skills && tsc && bun test test/*.test.mjs",
49
50
  "test:node": "bun run bundle:skills && tsc && node --test test/*.test.mjs",
50
- "prepublishOnly": "bun run build && bun run bundle:skills && bun run test",
51
+ "prepublishOnly": "bun run build:deps && bun run build && bun run bundle:skills && bun run test",
51
52
  "postpack": "bun run clean:skills",
52
53
  "mutation": "bun run bundle:skills && bun scripts/mutation-check.mjs",
53
- "prepare": "bun run build && bun run bundle:skills"
54
+ "prepare": "bun run build:deps && bun run build && bun run bundle:skills"
54
55
  },
55
56
  "peerDependencies": {
56
57
  "@earendil-works/pi-coding-agent": ">=0.99.1",
57
58
  "@earendil-works/pi-tui": "*"
58
59
  },
59
60
  "dependencies": {
60
- "@gtrabanco/agentic-workflow": "0.5.0",
61
- "@gtrabanco/agentic-workflow-schema": "4.9.0"
61
+ "@gtrabanco/agentic-workflow": "4.10.0",
62
+ "@gtrabanco/agentic-workflow-schema": "4.10.0"
62
63
  },
63
64
  "devDependencies": {
64
65
  "@types/node": "24.13.3",
65
- "typescript": "6.0.3"
66
+ "typescript": "7"
66
67
  }
67
68
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: audit-docs
3
3
  user-invocable: true
4
- version: 2.2.0
4
+ version: 4.10.0
5
5
  argument-hint: "[--fix]"
6
6
  author: "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>"
7
7
  license: MIT
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: audit-pr
3
3
  user-invocable: true
4
- version: 5.6.3
4
+ version: 4.10.0
5
5
  argument-hint: <pr-number> (optional — defaults to the current branch's PR)
6
6
  author: "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>"
7
7
  license: MIT
@@ -50,6 +50,12 @@ independently evaluates only the delivery gates below.
50
50
  this skill and cannot be inherited from docs or an earlier session
51
51
  ✓ No review pass was composed or reconstructed: a missing/stale receipt is a
52
52
  blocker, never a prompt to re-run review axes from this skill
53
+ ✓ **Every AC links a demonstration artifact (S3, S7).** Read the unit doc's
54
+ `## Acceptance criteria` (or `## Specs` S# + `## Acceptance criteria` in
55
+ the re-cut shape). For each AC, verify it links a demonstration:
56
+ a passing test with output, a UI capture, a reproduced command output.
57
+ An AC without a demonstration is a **BLOCKER** — the PR's body must render
58
+ it. A PR whose ACs lack demonstrations never reaches the merge gate.
53
59
  ✓ Closure integrity was evaluated and its result stated explicitly: pass /
54
60
  blocker / n-a (fix-governed PRs are always n-a; never skipped
55
61
  silently)
@@ -77,6 +83,11 @@ first on purpose).
77
83
  everything the SPEC promised is present, traceable, and green. A missing or stale
78
84
  receipt is a blocker routed back to `/review-change`, never re-litigated here.
79
85
 
86
+ **Cost-scaling invariant (S7).** Reviewers dig into code only when evidence is
87
+ absent or contradicts — so review cost scales with the change, not the project.
88
+ The non-technical approver (human layer) validates demonstrations; the merge
89
+ gate (technical layer) validates evidence linkage. Same evidence, two readings.
90
+
80
91
  ## Scope
81
92
 
82
93
  The whole pull request: the branch vs. the default base, **plus** its SPEC and
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: discover-repository-state
3
3
  user-invocable: true
4
- version: 1.2.2
4
+ version: 4.10.0
5
5
  description: >
6
6
  Discover repository evidence and write a frozen Normalized Repository State.
7
7
  Produces verified repository evidence and keeps facts, decisions, planned
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: execute-phase
3
3
  user-invocable: true
4
- version: 6.5.0
4
+ version: 4.10.0
5
5
  argument-hint: <NN-slug> [P<k>|step] [--max-attempts N] [--force]
6
6
  allowed-tools: [Bash, Read, Edit, Write, MultiEdit]
7
7
  author: "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>"
@@ -43,11 +43,16 @@ if bun is not installed) and the
43
43
  ✓ Unit doc read and step selected (explicit P<k> or first unfinished)
44
44
  ✓ Step executed and committed (one commit per step, conventional format)
45
45
  ✓ Evidence rows updated (command → exit/digest → output → verified-by)
46
+ — each row carries the command, observed output (reproduced, not typed by
47
+ hand), and the verifying-by credit. (S5 — claim-evidence loop: mandatory
48
+ reproduction, loop-until-green inside the same turn)
46
49
  ✓ Progress log updated with a dated `YYYY-MM-DD HH:MM` entry that names the
47
50
  phase id (what, commit, next)
48
51
  ✓ Memory boundary ran — the phase's outcome saved under `unit/<NN-slug>/doc`,
49
52
  or the mirror marked pending when the layer is degraded
50
53
  ✓ Diff guard passed (on implement steps) — see `diff-guard` in PREFLIGHT.md
54
+ ✓ Self-audit box (S9 d): every claim the persist introduces was re-derived
55
+ with pasted output before the commit lands.
51
56
  ✓ → Next: block printed as the absolute last output
52
57
  ```
53
58
 
@@ -133,14 +138,51 @@ work. For `implement` steps: write tests-first where they pay (core/domain,
133
138
  orchestration — record the failing red run's command, exit, and failing test
134
139
  names before implementation), then green. UI/adapter glue may test after.
135
140
 
136
- ### 5. Post-step
141
+ ### 5. Claim-evidence loop (S5 — before post-step)
142
+
143
+ Before running post-step, verify every claim the step's repair introduces. This
144
+ is the repairer-owned verification that prevents the unit 79 F50-class failure
145
+ (closed gate-green with an unreproduced figure "170" without re-deriving via
146
+ `git diff --numstat`).
147
+
148
+ 1. **Claim ledger.** Enumerate every claim: each new figure, cross-reference,
149
+ "X is now Y"; in code: each behavior the step declares.
150
+ 2. **Mandatory reproduction.** Re-derive the figure with the command, resolve
151
+ the reference with grep, reproduce the behavior. Paste command + observed
152
+ output into the claim table — never type the expected result by hand.
153
+ 3. **Loop until green.** red → repair → re-run, *inside the same turn*, until
154
+ every claim verifies.
155
+ 4. **Admission rule.** If no command can verify the sentence, it is not a
156
+ claim, it is an opinion, and it does not get written.
157
+
158
+ Output:
159
+
160
+ ```
161
+ | claim | command | output | status |
162
+ |---|---|---|---|
163
+ | <claim> | `<command>` | `<observed output>` | ✓ |
164
+ ```
165
+
166
+ The row shape is published grammar (`evidence-row@1` in
167
+ `@gtrabanco/agentic-workflow-schema`), so a row missing its command, its
168
+ observed output or a ✓/✗ verdict is a shape violation rather than a style nit.
169
+
170
+ **Bounded checkpoint (S13/S14).** The claim-evidence loop is only paid if it is
171
+ cheap: `node scripts/test-route.mjs --check <changed path>` names the minimal
172
+ suite a path needs, so an intermediate verification costs seconds instead of the
173
+ full gate. An unmatched path answers `route: full-suite` — never fewer tests —
174
+ and the full gate stays what the merge gate and CI run.
175
+
176
+ ### 6. Post-step
137
177
 
138
178
  1. **Diff guard** (implement steps only): run `bun scripts/diff-guard.mjs --base
139
179
  <last-step-commit> --unit <unit>` — paste the block verbatim. **BREACH → STOP**
140
180
  (re-triage or record exception). Anti-gaming: **NEVER shrink a diff by deleting
141
181
  comments, blank lines, docs or tests.**
142
182
  2. **Evidence rows**: one row per AC satisfied — `| AC | command | exit/digest |
143
- output (≤2 lines) | verified-by |`. Append to the Evidence section.
183
+ output (≤2 lines, reproduced via command, not typed by hand) | verified-by |`
184
+ — append to the Evidence section. The evidence row is a *reproduced receipt*,
185
+ not an assertion (S5: mandatory reproduction extends to doc artifacts).
144
186
  3. **Progress log**: append a dated entry —
145
187
  `YYYY-MM-DD HH:MM — <what was done> → <commit sha> — next: <what is next>`.
146
188
  4. **Memory boundary** — this step is a phase transition. Follow
@@ -153,7 +195,7 @@ names before implementation), then green. UI/adapter glue may test after.
153
195
  5. **Commit**: `git add <changed files>` then `git commit -m "<type>(<scope>):
154
196
  <step-summary>"` — run it, paste the SHA.
155
197
 
156
- ### 6. Red gate (repair loop)
198
+ ### 7. Red gate (repair loop)
157
199
 
158
200
  After each step: run the project's verification gate (type-check + tests + build).
159
201
 
@@ -173,7 +215,7 @@ After each step: run the project's verification gate (type-check + tests + build
173
215
  · continue atomically → /execute-phase <unit> <P<k>>
174
216
  ```
175
217
 
176
- ### 7. Whole-unit mode
218
+ ### 8. Whole-unit mode
177
219
 
178
220
  When invoked as `<NN-slug>` with no step: loop all remaining steps, one commit
179
221
  each. This is an **explicit operator choice only** — the lane's hand-offs
@@ -200,7 +242,7 @@ PR: <url>
200
242
  · merge gate after REVIEW-PASS → /audit-pr
201
243
  ```
202
244
 
203
- ### 8. Descope guard
245
+ ### 9. Descope guard
204
246
 
205
247
  Before creating **any** issue while executing: classify with the descope test.
206
248
  A descope (moving an acceptance criterion or task out of scope) requires
@@ -221,6 +263,11 @@ These files must be loaded when their topic is active:
221
263
 
222
264
  ## Guardrails
223
265
 
266
+ - The all-stages no-assumption rule binds every step here — **ask-don't-infer**,
267
+ owner `skills/unit-lane/references/DESIGN.md` (`ask-dont-infer@1`): an
268
+ expectation the tree, the docs, or a prior owner decision does not evidence
269
+ is asked, never invented.
270
+
224
271
  **Allowed:**
225
272
  - Only the triaged steps listed by the unit doc's Evidence header
226
273
  - Changes scoped to the acceptance criteria and step tasks
@@ -235,6 +282,14 @@ These files must be loaded when their topic is active:
235
282
  keep assertions at least as strong.
236
283
  - Never execute a step the triage skipped — the catalog is authoritative.
237
284
  - Never edit the unit doc's Acceptance criteria (that is re-triage with user consent).
285
+ - Never write to `docs/LOGS.md` (S9 4b): LOGS.md must be pure code output —
286
+ no model writes it. The interim rule (until feature 41's
287
+ `autoLogSession: mechanical`) is append-only: a wrong past line gets a *new
288
+ correcting line*, never an edit. Machine-authored git-facts entries also
289
+ kill the "hand-tallied figure" class.
290
+ - Self-audit box (S9 d): before committing, re-derive every claim the step's
291
+ persist introduces with pasted output. The turn does not end with unreproduced
292
+ claims.
238
293
  - Never expand scope beyond what Non-goals define — findings discovered during
239
294
  implementation never widen the unit.
240
295
  - Never invent an acceptance criterion — ask the user with concrete options.