@gtrabanco/pi-agentic-workflow 0.10.7 → 0.11.1
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/extension/factory.d.ts +11 -0
- package/dist/extension/factory.js +2 -1
- package/dist/extension/index.js +19 -2
- package/dist/extension/receipt-guard.d.ts +54 -0
- package/dist/extension/receipt-guard.js +88 -0
- package/package.json +1 -1
- package/skills/audit-pr/SKILL.md +6 -4
- package/skills/audit-pr/references/01_MERGE_GATES.md +3 -0
- package/skills/audit-pr/references/03_AUDIT_PROCESS.md +22 -34
- package/skills/log-session/SKILL.md +25 -19
- package/skills/review-change/SKILL.md +8 -6
- package/skills/review-change/references/PERSIST_AND_DECIDE.md +24 -39
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { dirtyWorktreeWarning, receiptGuard } from "./receipt-guard.js";
|
|
4
5
|
import type { ExtensionSurface, InvocationContext, ModelRef, RoutingControls } from "../routing/types.js";
|
|
5
6
|
import type { HintStore } from "../routing/state.js";
|
|
6
7
|
/**
|
|
@@ -43,5 +44,15 @@ export interface ExtensionDeps<M extends ModelRef = ModelRef> {
|
|
|
43
44
|
export interface ExtensionHandle<M extends ModelRef = ModelRef> {
|
|
44
45
|
router: Router<M>;
|
|
45
46
|
catalogue: Catalogue;
|
|
47
|
+
/**
|
|
48
|
+
* The Pi-free predicates the adapter (`index.ts`) binds to Pi's lifecycle
|
|
49
|
+
* events: the inline-receipt block guard and the dirty-worktree notice. They
|
|
50
|
+
* travel with the handle so the entry stays a thin translation layer and the
|
|
51
|
+
* decisions are unit-tested with no session (issue #182).
|
|
52
|
+
*/
|
|
53
|
+
guards: {
|
|
54
|
+
receiptGuard: typeof receiptGuard;
|
|
55
|
+
dirtyWorktreeWarning: typeof dirtyWorktreeWarning;
|
|
56
|
+
};
|
|
46
57
|
}
|
|
47
58
|
export declare function createExtension<M extends ModelRef = ModelRef>(deps: ExtensionDeps<M>): ExtensionHandle<M>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { loadConfig } from "../config/load.js";
|
|
2
2
|
import { readCatalogue } from "../routing/catalogue.js";
|
|
3
3
|
import { createRouter } from "../routing/dispatch.js";
|
|
4
|
+
import { dirtyWorktreeWarning, receiptGuard } from "./receipt-guard.js";
|
|
4
5
|
import { SETTINGS_COMMAND, SETTINGS_COMMAND_ALIAS } from "../routing/types.js";
|
|
5
6
|
export function createExtension(deps) {
|
|
6
7
|
const { registrar, surface, skillsDir, agentDir, hint, settings } = deps;
|
|
@@ -56,5 +57,5 @@ export function createExtension(deps) {
|
|
|
56
57
|
description: "Show and configure per-command model routing",
|
|
57
58
|
handler: settingsHandler,
|
|
58
59
|
});
|
|
59
|
-
return { router, catalogue };
|
|
60
|
+
return { router, catalogue, guards: { receiptGuard, dirtyWorktreeWarning } };
|
|
60
61
|
}
|
package/dist/extension/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { loadConfig } from "../config/load.js";
|
|
5
5
|
import { createExtension } from "./factory.js";
|
|
6
|
+
import { readGitStatusBounded } from "./receipt-guard.js";
|
|
6
7
|
import { createPickerComponent, PICKER_MAX_VISIBLE, pagedSelect } from "../settings/picker.js";
|
|
7
8
|
import { createHintStore, stateFilePath } from "../routing/state.js";
|
|
8
9
|
import { runSettingsConsole } from "../settings/console.js";
|
|
@@ -97,7 +98,7 @@ function toRegistrar(pi) {
|
|
|
97
98
|
export default function extension(pi) {
|
|
98
99
|
const agentDir = getAgentDir();
|
|
99
100
|
const hint = createHintStore({ path: stateFilePath(agentDir) });
|
|
100
|
-
const { router } = createExtension({
|
|
101
|
+
const { router, guards } = createExtension({
|
|
101
102
|
registrar: toRegistrar(pi),
|
|
102
103
|
// Resolved per call: the router must never hold a session-bound object
|
|
103
104
|
// between turns, because Pi can hand a new session to the same extension.
|
|
@@ -126,7 +127,23 @@ export default function extension(pi) {
|
|
|
126
127
|
});
|
|
127
128
|
pi.on("model_select", (event) => router.noteModelSelect(event.model));
|
|
128
129
|
pi.on("thinking_level_select", (event) => router.noteThinkingLevelSelect(event.level));
|
|
129
|
-
|
|
130
|
+
// The inline-receipt path is impossible: a `gh pr comment` carrying a
|
|
131
|
+
// REVIEW-PASS / merge-ready marker is blocked before it runs, and the reason
|
|
132
|
+
// names the script that proves the receipt landed (issue #182).
|
|
133
|
+
pi.on("tool_call", (event) => {
|
|
134
|
+
const command = "command" in event.input && typeof event.input.command === "string" ? event.input.command : undefined;
|
|
135
|
+
return guards.receiptGuard({ toolName: event.toolName, command });
|
|
136
|
+
});
|
|
137
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
138
|
+
void router.settle(toInvocationContext(ctx));
|
|
139
|
+
// Terminal hygiene is said out loud once the turn is over; a clean tree
|
|
140
|
+
// stays silent. The probe is time-bounded (2000 ms) and swallows timeout,
|
|
141
|
+
// spawn error and non-zero status, so an unresponsive git can never park
|
|
142
|
+
// the settled turn — best-effort by construction (issue #182 F4).
|
|
143
|
+
const warning = guards.dirtyWorktreeWarning(readGitStatusBounded(ctx.cwd));
|
|
144
|
+
if (warning)
|
|
145
|
+
ctx.ui.notify(warning, "warning");
|
|
146
|
+
});
|
|
130
147
|
}
|
|
131
148
|
// Exported so the settings console (P4) names the same command without relisting it.
|
|
132
149
|
export { SETTINGS_COMMAND };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inline-receipt guard and the dirty-worktree notice — the two Pi-free
|
|
3
|
+
* decisions the extension's lifecycle adapter binds (issue #182).
|
|
4
|
+
*
|
|
5
|
+
* `review-change` and `audit-pr` used to post their PR receipts through a bash
|
|
6
|
+
* heredoc: a model assembled the marker inline, ran `gh pr comment --body "…"`,
|
|
7
|
+
* re-read the thread and retried. Nothing proved the receipt landed, so a review
|
|
8
|
+
* could print `REVIEW-PASS` while the PR had no current marker. Two scripts now
|
|
9
|
+
* own that mechanically — `scripts/review-receipt.mjs emit` and
|
|
10
|
+
* `scripts/audit-pr-gate.mjs comment` — each posting the body from standard input,
|
|
11
|
+
* re-reading the PR and exiting 0 only when the marker names the reviewed head.
|
|
12
|
+
* This guard blocks the inline path before it runs, so the unverifiable route is
|
|
13
|
+
* impossible rather than discouraged.
|
|
14
|
+
*
|
|
15
|
+
* Pure on purpose: `index.ts` is the only file that imports Pi values, so these
|
|
16
|
+
* predicates compile and are tested with no session.
|
|
17
|
+
*/
|
|
18
|
+
/** The narrow slice of a Pi `tool_call` the guard reads. */
|
|
19
|
+
export interface ToolCallLike {
|
|
20
|
+
toolName: string;
|
|
21
|
+
command?: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
/** The Pi `tool_call` result shape: `block` plus an actionable `reason`. */
|
|
24
|
+
export interface ReceiptGuardDecision {
|
|
25
|
+
block: boolean;
|
|
26
|
+
reason?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The `tool_call` gate. Blocks exactly one shape: a bash command that invokes
|
|
30
|
+
* `gh pr comment` and carries a receipt marker as inline text. A `--body-file`
|
|
31
|
+
* invocation is the scripts' own path and is allowed; a command that merely
|
|
32
|
+
* mentions a marker (editing the skill docs) is allowed; every non-bash tool is
|
|
33
|
+
* allowed.
|
|
34
|
+
*/
|
|
35
|
+
export declare function receiptGuard({ toolName, command }: ToolCallLike): ReceiptGuardDecision;
|
|
36
|
+
/**
|
|
37
|
+
* The `agent_settled` notice. `porcelain` is `git status --porcelain` output;
|
|
38
|
+
* an empty (or whitespace-only) string stays silent, and a non-empty one yields
|
|
39
|
+
* one concise warning naming what is dirty. Decision only — the adapter runs
|
|
40
|
+
* git and delivers the message.
|
|
41
|
+
*/
|
|
42
|
+
export declare function dirtyWorktreeWarning(porcelain: string): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* How long the settled-turn `git status` probe may run before it is abandoned.
|
|
45
|
+
* The probe is best-effort hygiene, not work: an unresponsive git must never
|
|
46
|
+
* park the turn the way the old unbounded `spawnSync` could (issue #182 F4).
|
|
47
|
+
*/
|
|
48
|
+
export declare const GIT_STATUS_TIMEOUT_MS = 2000;
|
|
49
|
+
/**
|
|
50
|
+
* `git status --porcelain` for the settled-turn notice, time-bounded. A timeout,
|
|
51
|
+
* a spawn error and a non-zero exit all read as a clean worktree — the notice is
|
|
52
|
+
* best-effort by construction and must never fail a turn.
|
|
53
|
+
*/
|
|
54
|
+
export declare function readGitStatusBounded(cwd: string): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inline-receipt guard and the dirty-worktree notice — the two Pi-free
|
|
3
|
+
* decisions the extension's lifecycle adapter binds (issue #182).
|
|
4
|
+
*
|
|
5
|
+
* `review-change` and `audit-pr` used to post their PR receipts through a bash
|
|
6
|
+
* heredoc: a model assembled the marker inline, ran `gh pr comment --body "…"`,
|
|
7
|
+
* re-read the thread and retried. Nothing proved the receipt landed, so a review
|
|
8
|
+
* could print `REVIEW-PASS` while the PR had no current marker. Two scripts now
|
|
9
|
+
* own that mechanically — `scripts/review-receipt.mjs emit` and
|
|
10
|
+
* `scripts/audit-pr-gate.mjs comment` — each posting the body from standard input,
|
|
11
|
+
* re-reading the PR and exiting 0 only when the marker names the reviewed head.
|
|
12
|
+
* This guard blocks the inline path before it runs, so the unverifiable route is
|
|
13
|
+
* impossible rather than discouraged.
|
|
14
|
+
*
|
|
15
|
+
* Pure on purpose: `index.ts` is the only file that imports Pi values, so these
|
|
16
|
+
* predicates compile and are tested with no session.
|
|
17
|
+
*/
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
/** `gh pr comment` as an invoked command, not as a quoted string elsewhere in it. */
|
|
20
|
+
const GH_PR_COMMENT_RE = /\bgh\s+pr\s+comment\b/;
|
|
21
|
+
const REVIEW_MARKER = "review-change:pass";
|
|
22
|
+
const AUDIT_MARKER = "audit-pr:merge-ready";
|
|
23
|
+
const BODY_FILE_FLAG = "--body-file";
|
|
24
|
+
const REVIEW_REASON = "`gh pr comment` with an inline `review-change:pass` body is blocked: inline receipt bodies are unverifiable, so nothing proves the marker landed on the PR. " +
|
|
25
|
+
"Use `bun scripts/review-receipt.mjs emit --pr <N> --head <40-hex> …` instead — it posts the receipt, re-reads the PR, and exits 0 only when the marker names the reviewed head.";
|
|
26
|
+
const AUDIT_REASON = "`gh pr comment` with an inline `audit-pr:merge-ready` body is blocked: inline receipt bodies are unverifiable, so nothing proves the marker landed on the PR. " +
|
|
27
|
+
"Use `bun scripts/audit-pr-gate.mjs comment --pr <N> --head <40-hex> …` instead — it posts the comment idempotently and confirms the marker names the audited head.";
|
|
28
|
+
/**
|
|
29
|
+
* The `tool_call` gate. Blocks exactly one shape: a bash command that invokes
|
|
30
|
+
* `gh pr comment` and carries a receipt marker as inline text. A `--body-file`
|
|
31
|
+
* invocation is the scripts' own path and is allowed; a command that merely
|
|
32
|
+
* mentions a marker (editing the skill docs) is allowed; every non-bash tool is
|
|
33
|
+
* allowed.
|
|
34
|
+
*/
|
|
35
|
+
export function receiptGuard({ toolName, command }) {
|
|
36
|
+
if (toolName !== "bash")
|
|
37
|
+
return { block: false };
|
|
38
|
+
const text = command ?? "";
|
|
39
|
+
if (!GH_PR_COMMENT_RE.test(text))
|
|
40
|
+
return { block: false };
|
|
41
|
+
if (text.includes(BODY_FILE_FLAG))
|
|
42
|
+
return { block: false };
|
|
43
|
+
if (text.includes(REVIEW_MARKER))
|
|
44
|
+
return { block: true, reason: REVIEW_REASON };
|
|
45
|
+
if (text.includes(AUDIT_MARKER))
|
|
46
|
+
return { block: true, reason: AUDIT_REASON };
|
|
47
|
+
return { block: false };
|
|
48
|
+
}
|
|
49
|
+
/** How many porcelain lines the warning quotes before summarising the rest. */
|
|
50
|
+
const WARNING_PREVIEW = 5;
|
|
51
|
+
/**
|
|
52
|
+
* The `agent_settled` notice. `porcelain` is `git status --porcelain` output;
|
|
53
|
+
* an empty (or whitespace-only) string stays silent, and a non-empty one yields
|
|
54
|
+
* one concise warning naming what is dirty. Decision only — the adapter runs
|
|
55
|
+
* git and delivers the message.
|
|
56
|
+
*/
|
|
57
|
+
export function dirtyWorktreeWarning(porcelain) {
|
|
58
|
+
const lines = String(porcelain ?? "")
|
|
59
|
+
.split("\n")
|
|
60
|
+
.map((line) => line.trim())
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
if (lines.length === 0)
|
|
63
|
+
return undefined;
|
|
64
|
+
const preview = lines.slice(0, WARNING_PREVIEW).join(", ");
|
|
65
|
+
const rest = lines.length > WARNING_PREVIEW ? `, +${lines.length - WARNING_PREVIEW} more` : "";
|
|
66
|
+
return `Uncommitted changes in the worktree (${lines.length}): ${preview}${rest} — commit or stash before this change is reviewed.`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* How long the settled-turn `git status` probe may run before it is abandoned.
|
|
70
|
+
* The probe is best-effort hygiene, not work: an unresponsive git must never
|
|
71
|
+
* park the turn the way the old unbounded `spawnSync` could (issue #182 F4).
|
|
72
|
+
*/
|
|
73
|
+
export const GIT_STATUS_TIMEOUT_MS = 2000;
|
|
74
|
+
/**
|
|
75
|
+
* `git status --porcelain` for the settled-turn notice, time-bounded. A timeout,
|
|
76
|
+
* a spawn error and a non-zero exit all read as a clean worktree — the notice is
|
|
77
|
+
* best-effort by construction and must never fail a turn.
|
|
78
|
+
*/
|
|
79
|
+
export function readGitStatusBounded(cwd) {
|
|
80
|
+
const result = spawnSync("git", ["status", "--porcelain"], {
|
|
81
|
+
cwd,
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
84
|
+
});
|
|
85
|
+
if (result.error || result.status !== 0)
|
|
86
|
+
return "";
|
|
87
|
+
return result.stdout ?? "";
|
|
88
|
+
}
|
package/package.json
CHANGED
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: 5.1
|
|
4
|
+
version: 5.2.1
|
|
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
|
|
@@ -34,6 +34,8 @@ independently evaluates only the delivery gates below.
|
|
|
34
34
|
matching `review-change:pass` marker fetched together; absent or any SHA
|
|
35
35
|
mismatch → blocker routed to `/review-change`, current → its scope/axes/
|
|
36
36
|
acceptance coverage/manual checks acknowledged without re-review
|
|
37
|
+
✓ Terminal hygiene was read from state, not assumed: tree clean, branch pushed,
|
|
38
|
+
PR not a draft (`bun scripts/audit-pr-gate.mjs hygiene --pr <N> --apply`)
|
|
37
39
|
✓ Upstream lineage was re-verified (current plan receipt + parent spec receipt,
|
|
38
40
|
digests recomputed), every obligation row is `verified`/`n/a`, and no planning
|
|
39
41
|
finding is open — stale or missing lineage is a blocker, never a formality
|
|
@@ -41,9 +43,9 @@ independently evaluates only the delivery gates below.
|
|
|
41
43
|
✓ The PR's FULL URL is printed in the verdict header (the user may be juggling
|
|
42
44
|
several projects and agents without a CI monitor — the link in the chat is
|
|
43
45
|
the contract, never "PR #N" alone)
|
|
44
|
-
✓ MERGE-READY verdict? Then the
|
|
45
|
-
|
|
46
|
-
never a commit-message tag. BLOCKED → no comment
|
|
46
|
+
✓ MERGE-READY verdict? Then the comment was POSTED by
|
|
47
|
+
`bun scripts/audit-pr-gate.mjs comment` (re-reads, exits non-zero unless the newest marker
|
|
48
|
+
names the head) — a comment, never a commit-message tag. BLOCKED → no comment
|
|
47
49
|
✓ Nothing was edited, refactored, or merged; merge authorization is outside
|
|
48
50
|
this skill and cannot be inherited from docs or an earlier session
|
|
49
51
|
✓ No review pass was composed or reconstructed: a missing/stale receipt is a
|
|
@@ -16,6 +16,9 @@ A gate that can't be confirmed is a **blocker**, not a pass — never assume gre
|
|
|
16
16
|
| **Closure integrity** | The governing **feature** SPEC's capability closure was taken and recorded — `design-feature` was actually run, not bypassed. Fix-governed PRs: `n/a` (no closure block by design). | A present `Capability closure` block has a blank row, or a resolved non-`n/a` row with no matching acceptance criterion. |
|
|
17
17
|
| **Scope integrity (descope)** | An issue born during this unit that maps to an unmet SPEC acceptance criterion or phase task has a matching, user-approved, dated `## Amendments` entry — descoped scope was recorded, not silently exported. Detection is two-path: a slug/issue-number text match, **or** an issue linked from an `## Amendments` row (`#89`) — either is sufficient to enumerate the issue, so a descoped issue with a generic title/body is not invisible to the gate. | An issue born since branch divergence that references this unit (by either detection path) maps to an unmet criterion/task with no matching `## Amendments` entry, or an `## Amendments` row that is undated, unapproved, or unlinked to an issue. |
|
|
18
18
|
| **Architectural invariants** | The **receipt** records the invariant result review-change took against the optional project invariant document; this gate confirms that result is explicit (`pass | blocker | n-a`) and does not reclassify it (AC 13). No document → `n/a: no project invariants declared`, not a blocker. | The receipt's invariant result is absent/ambiguous, or a recorded `blocker`/`violates`/`introduces`/`changes` lacks the cited decision the review surfaced. |
|
|
19
|
+
| **Hygiene — tree clean** | `git status --porcelain` is empty at the audited head: every artifact is committed, so nothing rides along to the next conversation (the field failure of issue #182). | Any modified or untracked path — the blocker names each one. |
|
|
20
|
+
| **Hygiene — branch pushed** | The branch is level with its remote, so the PR head the audit read is the head that was reviewed. | Local commits the forge has not seen. |
|
|
21
|
+
| **Hygiene — PR ready** | The PR is not a draft. This is the one hygiene gate with a mechanical repair, so `audit-pr` flips it (`gh pr ready`) instead of reporting it. | Still a draft after the repair attempt. |
|
|
19
22
|
|
|
20
23
|
> The review evidence is the receipt, never a re-review: verify the newest
|
|
21
24
|
> `review-change:pass` marker's `sha` against the PR's head (Step 1). A current
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
## Process
|
|
2
2
|
|
|
3
3
|
1. **Gather** — Step 0: project contract, PR, SPEC + artifacts, CI status.
|
|
4
|
-
2. **Consume the review receipt** — Step 1:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
manual checks; continue to the gates.
|
|
4
|
+
2. **Consume the review receipt** — Step 1: run the receipt verifier, which
|
|
5
|
+
fetches `headRefOid` + comments and applies the currency rule itself:
|
|
6
|
+
`bun scripts/review-receipt.mjs verify --pr <N>` — exit `0` current, `3`
|
|
7
|
+
absent, `4` stale. Its `headRefOid` is the current head SHA: a marker for any
|
|
8
|
+
other SHA is stale, never preserved from a local diff.
|
|
9
|
+
- **current (0)** → acknowledge scope/axes, acceptance coverage, invariant
|
|
10
|
+
result, manual checks; continue to the gates.
|
|
12
11
|
- **absent / stale** → **BLOCKER** (no review evidence at the head), routed to
|
|
13
12
|
`/review-change`; never re-review from here.
|
|
14
|
-
3. **Walk the contract** — evaluate every gate above against evidence
|
|
15
|
-
|
|
13
|
+
3. **Walk the contract** — evaluate every gate above against evidence, recording
|
|
14
|
+
pass / blocker / n-a with the artifact that proves it. The three
|
|
15
|
+
terminal-hygiene gates (`tree-clean`, `branch-pushed`, `pr-ready`) are **read
|
|
16
|
+
from state, never asserted** — hygiene previously had no owner at merge time:
|
|
17
|
+
`bun scripts/audit-pr-gate.mjs hygiene --pr <N> --apply` (exit `0` clean, `2`
|
|
18
|
+
blocked). `--apply` performs the only mechanical repair there is (`gh pr
|
|
19
|
+
ready`, so a draft is flipped rather than reported); a dirty tree names its
|
|
20
|
+
paths and an unpushed branch its commit count — those stay the author's to fix.
|
|
21
|
+
Fold the three results into the gate map you decide on in step 5.
|
|
16
22
|
4. **Confirm deferrals are real** — for anything postponed (an unchecked task, a
|
|
17
23
|
review finding, a known issue), verify a tracked issue + trigger exists. A
|
|
18
24
|
deferral with no destination is a blocker, not a pass.
|
|
@@ -40,30 +46,12 @@
|
|
|
40
46
|
re-appended; a genuinely new blocker gets the next `Fn` id.
|
|
41
47
|
7. **Post the MERGE-READY comment on the PR (MERGE-READY only).** The verdict
|
|
42
48
|
must be visible on the PR itself — as a **comment**, never in a commit
|
|
43
|
-
message
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<!-- audit-pr:merge-ready sha=<head SHA> -->
|
|
50
|
-
## ✅ audit-pr: MERGE-READY
|
|
51
|
-
|
|
52
|
-
- **Audited head:** `<head SHA>` · CI: <green|local-gate-green>
|
|
53
|
-
- **Review receipt:** `REVIEW-PASS` at `<head SHA>` (consumed, not re-reviewed)
|
|
54
|
-
- **Date:** <YYYY-MM-DD>
|
|
55
|
-
- **Before merge, a human should still verify:**
|
|
56
|
-
- <manual-verification item — or "nothing">
|
|
57
|
-
|
|
58
|
-
Any commit after `<head SHA>` voids this verdict — re-run `audit-pr`.
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
**Idempotent:** first check the existing comments
|
|
62
|
-
(`gh pr view <N> --json comments`) for the `<!-- audit-pr:merge-ready -->`
|
|
63
|
-
marker — same SHA already commented → skip (say so); older SHA → post the
|
|
64
|
-
new comment (the newest marker wins). Never post a comment for a BLOCKED
|
|
65
|
-
verdict — blockers go in the chat report only, so the PR page never shows
|
|
66
|
-
a stale green flag.
|
|
49
|
+
message. Run `bun scripts/audit-pr-gate.mjs comment --pr <N> --head <head
|
|
50
|
+
SHA> --gates-json <gate map>`: it builds the fixed body, posts once, re-reads,
|
|
51
|
+
and **exits non-zero unless the newest `<!-- audit-pr:merge-ready sha=… -->`
|
|
52
|
+
marker names the head**. **Idempotent by marker** — same SHA skips, an older
|
|
53
|
+
SHA re-posts (newest wins) — and it **refuses to post for a BLOCKED verdict**,
|
|
54
|
+
so the page never shows a stale green flag.
|
|
67
55
|
8. **Report** — the verdict block below, always headed by the PR's full URL.
|
|
68
56
|
In an active `ship-roadmap --fullauto` AUDIT stage, return the verdict to the
|
|
69
57
|
conductor; never run its merge wrapper from this skill.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: log-session
|
|
3
3
|
user-invocable: true
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
argument-hint: "[note to prepend to the entry]"
|
|
6
6
|
author: "Gabriel Trabanco <gtrabanco@users.noreply.github.com>"
|
|
7
7
|
license: MIT
|
|
@@ -29,6 +29,7 @@ judgment. It must never reach for an expensive model.
|
|
|
29
29
|
|
|
30
30
|
```
|
|
31
31
|
✓ The entry was APPENDED to docs/LOGS.md (file edited, not just drafted) with accurate git facts
|
|
32
|
+
✓ `bun scripts/session-close.mjs close` RAN and exited 0 (or 2, with the leftover paths named) — the entry is committed, not left riding along
|
|
32
33
|
✓ No past entry was edited
|
|
33
34
|
✓ Artifact language: explicit user instruction > the project's declared docs language > English. The CONVERSATION language never decides — a Spanish prompt still produces English PRs/issues/commits/SPECs unless one of the first two says otherwise
|
|
34
35
|
✓ The closing `→ Next:` block is printed as the ABSOLUTE last output
|
|
@@ -93,27 +94,32 @@ the HEAD sha and start time at session open.
|
|
|
93
94
|
review must chase (fix #157's F14). Write the actual state: "PR #N open,
|
|
94
95
|
awaiting the human merge".
|
|
95
96
|
|
|
96
|
-
4. **
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
- **Commits:** <n> (`<short-sha>…<short-sha>`)
|
|
103
|
-
- **Files:** <comma-separated paths, or a count if many>
|
|
104
|
-
- **Summary:** <what this session did>
|
|
105
|
-
- **Decisions:** <key choices + why; omit the line if none>
|
|
106
|
-
- **Next:** <the concrete next step>
|
|
97
|
+
4. **Render the entry** — the mechanical facts (timestamp, branch, commit count
|
|
98
|
+
and range, files touched) are computed for you:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
bun scripts/session-close.mjs render --summary "<what this session did>" \
|
|
102
|
+
--decisions "<key choices + why>" --next "<the concrete next step>"
|
|
107
103
|
```
|
|
108
104
|
|
|
109
|
-
|
|
105
|
+
It writes nothing: append its output to the **end** of `docs/LOGS.md`
|
|
106
|
+
(append-only, chronological) with your editing tool. Omit `--decisions` /
|
|
107
|
+
`--next` when there is nothing real to record — the script drops the line
|
|
108
|
+
rather than emitting an empty one. A user-supplied note is prepended to the
|
|
109
|
+
Summary.
|
|
110
|
+
|
|
111
|
+
5. **Close the session** — commit the entry and prove the tree is clean:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
bun scripts/session-close.mjs close
|
|
115
|
+
```
|
|
110
116
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
clean
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
It refuses unless `docs/LOGS.md` was **appended to** (a rewritten or truncated
|
|
118
|
+
log is not a session entry), commits that one file as
|
|
119
|
+
`docs(log): <the appended summary>`, and reports every path still uncommitted:
|
|
120
|
+
exit `0` clean, `2` the named leftovers are still dirty, `1` refused. Do not
|
|
121
|
+
open a PR just for a log entry, and never `git add -A` here — a concurrent
|
|
122
|
+
conversation's work must not be swept into a log commit.
|
|
117
123
|
|
|
118
124
|
## Guardrails
|
|
119
125
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: review-change
|
|
3
3
|
user-invocable: true
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.6.0
|
|
5
5
|
argument-hint: <path-or-glob> [--adversarial N] [--synthesize]
|
|
6
6
|
author: "Gabriel Trabanco <gtrabanco@users.noreply.github.com>"
|
|
7
7
|
license: MIT
|
|
@@ -24,14 +24,16 @@ Load and verify the **canonical** [Turn contract](.claude/skills/orchestration-e
|
|
|
24
24
|
For a final PR review, the turn is incomplete until this additional box passes:
|
|
25
25
|
|
|
26
26
|
```text
|
|
27
|
-
✓ Decision: REVIEW-PASS + PR exists →
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
✓ Decision: REVIEW-PASS + PR exists →
|
|
28
|
+
`bun scripts/review-receipt.mjs emit --pr <N> --head "$(git rev-parse HEAD)" …`
|
|
29
|
+
RUN and exited 0 — the script posts the SHA-bound comment, re-reads the PR
|
|
30
|
+
comments, and exits non-zero unless the newest exact-HEAD
|
|
31
|
+
`review-change:pass` marker is confirmed, so its exit code IS this box
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
The receipt closeout is a precondition of the report, not a follow-up: do not
|
|
33
|
-
print the fixed report block
|
|
34
|
-
|
|
35
|
+
print the fixed report block on a non-zero exit. A clean report without a
|
|
36
|
+
current receipt must not recommend `/audit-pr`.
|
|
35
37
|
|
|
36
38
|
For `REVIEW-FAIL` or `NEEDS-DECISION`, list every open finding ID in the closing
|
|
37
39
|
recommendation, joined with ` + `; the review must never hand off only the first
|
|
@@ -45,48 +45,33 @@ exact command tokens.
|
|
|
45
45
|
`REVIEW-PASS` with a PR, the receipt comment (step 12) — it never runs a
|
|
46
46
|
fold, executes a phase, or edits source (see the review-end turn boundary in
|
|
47
47
|
the SKILL.md Turn contract).
|
|
48
|
-
12. **Close out the final-review receipt before reporting.**
|
|
49
|
-
`Decision` from step 7 and persist step 11. Then
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
The reviewed head SHA is the value frozen by `git rev-parse HEAD` in the
|
|
60
|
-
review process. Resolve the PR identity immediately before this action with
|
|
61
|
-
`gh pr view --json number,headRefOid`. No PR → take the documented pre-PR
|
|
62
|
-
path. With a PR, its `headRefOid` **must equal the reviewed head SHA** before
|
|
63
|
-
querying or posting comments. A mismatch means the candidate changed during
|
|
64
|
-
review: do not post a receipt and re-run `/review-change` at the PR head.
|
|
65
|
-
|
|
66
|
-
For `REVIEW-PASS` with a PR, write the body below to a **temporary** Markdown
|
|
67
|
-
file (e.g. `$TMPDIR/review-receipt.md`), then run
|
|
68
|
-
`gh pr comment <N> --body-file <path>` — never inline `--body`, never commit
|
|
69
|
-
the file into the branch. Before posting, run `gh pr view <N> --json comments`
|
|
70
|
-
and inspect the newest matching marker. Same SHA → skip the post; older or
|
|
71
|
-
absent SHA → post. After posting, run the same comment query again and confirm
|
|
72
|
-
the newest marker equals the reviewed head SHA. If that confirmation fails,
|
|
73
|
-
retry the receipt action; do not print a `REVIEW-PASS` report or recommend
|
|
74
|
-
`/audit-pr` while the receipt is not current.
|
|
48
|
+
12. **Close out the final-review receipt before reporting.** Derive the
|
|
49
|
+
`Decision` from step 7 and persist step 11 first. Then — **only on
|
|
50
|
+
`Decision: REVIEW-PASS` and when the PR exists** — run the receipt emitter,
|
|
51
|
+
which is a precondition of the report, not a follow-up:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
bun scripts/review-receipt.mjs emit --pr <N> \
|
|
55
|
+
--head "$(git rev-parse HEAD)" --scope "<reviewed surface>" \
|
|
56
|
+
--axes "<axes run and skipped>" --coverage "<criterion-to-evidence>" \
|
|
57
|
+
--invariants pass|n/a --proposals <n> --manual "<items or none>"
|
|
58
|
+
```
|
|
75
59
|
|
|
76
|
-
|
|
60
|
+
The script is the contract: it refuses a PR whose `headRefOid` differs from
|
|
61
|
+
the reviewed head, posts the one idempotent SHA-bound comment (skipping an
|
|
62
|
+
identical one), re-reads the comments, and **exits non-zero unless the newest
|
|
63
|
+
`<!-- review-change:pass sha=… contract=v1 -->` marker names the reviewed
|
|
64
|
+
head**. Never hand-assemble the body and never post it inline; the fixed body
|
|
65
|
+
lives in the script and its suite. A non-zero exit means no receipt landed —
|
|
66
|
+
fix that before printing anything in step 13, and never recommend
|
|
67
|
+
`/audit-pr` while the receipt is not current.
|
|
77
68
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
69
|
+
`REVIEW-FAIL` and `NEEDS-DECISION` post **no** passing receipt: record the
|
|
70
|
+
status and continue to step 13. No PR → the documented pre-PR path, and the
|
|
71
|
+
receipt box reads `n/a: no PR`.
|
|
81
72
|
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
- Acceptance coverage: concise criterion-to-evidence summary
|
|
85
|
-
- Architectural invariants: pass | n/a
|
|
86
|
-
- Current-unit findings open: 0
|
|
87
|
-
- Future-capability proposals: <count; no issues created>
|
|
88
|
-
- Manual verification: <items or none>
|
|
89
|
-
```
|
|
73
|
+
Read-only check, any time: `bun scripts/review-receipt.mjs verify --pr <N>`
|
|
74
|
+
prints `current | absent | stale` and exits `0 | 3 | 4`.
|
|
90
75
|
|
|
91
76
|
13. **Report block — Return exactly this structure** after step 12 succeeds
|
|
92
77
|
(fixed chat-report block; this is not the end of the turn):
|