@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.
- package/dist/config/defaults.js +8 -0
- package/dist/config/merge.js +20 -0
- package/dist/config/schema.js +103 -1
- package/dist/config/types.d.ts +60 -0
- package/dist/extension/ci-guard.d.ts +29 -0
- package/dist/extension/ci-guard.js +39 -0
- package/dist/extension/cost-guard.d.ts +30 -0
- package/dist/extension/cost-guard.js +50 -0
- package/dist/extension/factory.d.ts +11 -4
- package/dist/extension/factory.js +4 -1
- package/dist/extension/guard-util.d.ts +26 -0
- package/dist/extension/guard-util.js +35 -0
- package/dist/extension/history-guard.d.ts +59 -0
- package/dist/extension/history-guard.js +132 -0
- package/dist/extension/index.js +21 -5
- package/dist/extension/merge-guard.d.ts +26 -28
- package/dist/extension/merge-guard.js +64 -38
- package/package.json +7 -6
- package/skills/audit-docs/SKILL.md +1 -1
- package/skills/audit-pr/SKILL.md +12 -1
- package/skills/discover-repository-state/SKILL.md +1 -1
- package/skills/execute-phase/SKILL.md +61 -6
- package/skills/fold-findings/SKILL.md +126 -8
- package/skills/fold-findings/references/FOLD_PROCESS.md +7 -1
- package/skills/init-workspace/SKILL.md +1 -1
- package/skills/log-session/SKILL.md +1 -1
- package/skills/orchestration-envelope/SKILL.md +1 -1
- package/skills/phase-contract/SKILL.md +1 -1
- package/skills/pre-execution-review/SKILL.md +1 -1
- package/skills/product-audit/SKILL.md +4 -1
- package/skills/product-audit/references/AUDIT_DIMENSIONS.md +34 -0
- package/skills/product-audit/references/AUDIT_PROCESS.md +5 -1
- package/skills/resolve-repository-state/SKILL.md +1 -1
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-brand/SKILL.md +1 -1
- package/skills/review-change/SKILL.md +18 -3
- package/skills/review-code/SKILL.md +28 -2
- package/skills/review-debt/SKILL.md +8 -2
- package/skills/review-design/SKILL.md +1 -1
- package/skills/review-implementation/SKILL.md +1 -1
- package/skills/review-implementation/references/CLASSIFY.md +25 -0
- package/skills/review-perf/SKILL.md +1 -1
- package/skills/review-security/SKILL.md +1 -1
- package/skills/review-seo/SKILL.md +1 -1
- package/skills/review-verify/SKILL.md +19 -2
- package/skills/triage-issue/SKILL.md +9 -1
- package/skills/triage-issue/references/LABELS.md +22 -0
- package/skills/unit-lane/SKILL.md +26 -7
- package/skills/unit-lane/references/DESIGN.md +55 -1
- package/skills/unit-lane/references/PLAN.md +29 -0
- package/skills/verification-contract/SKILL.md +9 -1
- 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.";
|
package/dist/extension/index.js
CHANGED
|
@@ -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
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
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
|
|
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
|
|
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
|
|
8
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
22
|
-
export
|
|
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.
|
|
34
|
-
* (`merge
|
|
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):
|
|
36
|
+
export declare function mergeGuard({ toolName, command }: ToolCallLike, enabled?: boolean): GuardDecision;
|
|
@@ -1,53 +1,79 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The merge guard — owner-only merges
|
|
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
|
|
8
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
38
|
-
* (`merge
|
|
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 (
|
|
47
|
-
return { block:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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": "
|
|
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": "
|
|
61
|
-
"@gtrabanco/agentic-workflow-schema": "4.
|
|
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": "
|
|
66
|
+
"typescript": "7"
|
|
66
67
|
}
|
|
67
68
|
}
|
package/skills/audit-pr/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: audit-pr
|
|
3
3
|
user-invocable: true
|
|
4
|
-
version:
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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.
|