@agnishc/edb-herald 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+ - Initial release: `/herald`, `/herald commit`, `/herald pr` commands
7
+ - Logical commit grouping by concern and layer
8
+ - `better-commits` convention with emoji type prefixes
9
+ - Approval gates before committing and before pushing/creating PR
10
+ - `final-plan.md` integration for richer commit messages and PR descriptions
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agnish Chakraborty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @agnishc/edb-herald
2
+
3
+ A Pi CLI extension that acts as a **git commit and PR agent**. It reads the current diff, groups changes into logical commits, shows a plan for your approval, executes commits, then pushes and creates a GitHub PR — all step by step with explicit approval gates.
4
+
5
+ Herald never commits or pushes without your explicit confirmation.
6
+
7
+ ## Commands
8
+
9
+ | Command | What it does |
10
+ |---------|-------------|
11
+ | `/herald` | Full flow: group → commit → push → PR |
12
+ | `/herald commit` | Commit only (no push, no PR) |
13
+ | `/herald pr` | PR only (assumes commits are already done) |
14
+
15
+ ## Flow
16
+
17
+ 1. Reads `git diff`, `git status`, and `git log`
18
+ 2. Reads `final-plan.md` if present (for context on what was built)
19
+ 3. Groups changes into logical commits by concern and layer
20
+ 4. Generates commit messages following the `better-commits` convention (`✨ feat(scope): subject`)
21
+ 5. **Shows the full plan — waits for your approval before touching anything**
22
+ 6. Executes commits one by one
23
+ 7. Generates PR description from commit history and `final-plan.md`
24
+ 8. **Shows PR description — waits for your approval**
25
+ 9. Pushes and creates the PR via `gh pr create`
26
+
27
+ ## Requirements
28
+
29
+ - `git` on PATH
30
+ - `gh` CLI on PATH (for PR creation)
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pi install npm:@agnishc/edb-herald
36
+ ```
37
+
38
+ ## License
39
+
40
+ [MIT](LICENSE) © Agnish Chakraborty
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@agnishc/edb-herald",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension: git commit and PR agent with approval gates",
5
+ "keywords": ["pi-package", "pi-extension", "edb", "git"],
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "author": "Agnish Chakraborty",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/agnishcc/pi-extention-monorepo.git",
12
+ "directory": "packages/edb-herald"
13
+ },
14
+ "homepage": "https://github.com/agnishcc/pi-extention-monorepo/tree/main/packages/edb-herald#readme",
15
+ "bugs": { "url": "https://github.com/agnishcc/pi-extention-monorepo/issues" },
16
+ "publishConfig": { "access": "public" },
17
+ "scripts": { "test": "vitest run" },
18
+ "files": ["src", "README.md", "LICENSE", "CHANGELOG.md"],
19
+ "pi": {
20
+ "extensions": ["./src/index.ts"]
21
+ },
22
+ "peerDependencies": {
23
+ "@mariozechner/pi-coding-agent": "*"
24
+ }
25
+ }
package/src/git.ts ADDED
@@ -0,0 +1,102 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { HeraldMode } from "./types";
3
+
4
+ // ── Git helpers ────────────────────────────────────────────────────────────────
5
+
6
+ export async function git(pi: ExtensionAPI, args: string[], _cwd: string): Promise<string> {
7
+ try {
8
+ const result = await pi.exec("git", args, { timeout: 10000 });
9
+ return result.stdout.trim();
10
+ } catch {
11
+ return "";
12
+ }
13
+ }
14
+
15
+ export async function isGitRepo(pi: ExtensionAPI, _cwd: string): Promise<boolean> {
16
+ const result = await pi.exec("git", ["rev-parse", "--git-dir"], { timeout: 5000 }).catch(() => null);
17
+ return result !== null && result.code === 0;
18
+ }
19
+
20
+ // ── Task builder ───────────────────────────────────────────────────────────────
21
+
22
+ export function parseMode(args: string): HeraldMode {
23
+ const a = args.trim().toLowerCase();
24
+ if (a === "commit") return "commit";
25
+ if (a === "pr") return "pr";
26
+ return "both";
27
+ }
28
+
29
+ export function truncate(text: string, maxLen: number): string {
30
+ if (text.length <= maxLen) return text;
31
+ return `${text.slice(0, maxLen)}\n\n... [truncated — ${text.length - maxLen} additional characters omitted]`;
32
+ }
33
+
34
+ export async function buildTask(pi: ExtensionAPI, mode: HeraldMode, cwd: string): Promise<string> {
35
+ // Gather git context in parallel
36
+ const [status, staged, unstaged, log, branch] = await Promise.all([
37
+ git(pi, ["status", "--short"], cwd),
38
+ git(pi, ["diff", "--cached"], cwd),
39
+ git(pi, ["diff"], cwd),
40
+ git(pi, ["log", "--oneline", "-15"], cwd),
41
+ git(pi, ["branch", "--show-current"], cwd),
42
+ ]);
43
+
44
+ // Mode instruction
45
+ const modeNote: Record<HeraldMode, string> = {
46
+ commit: "**Mode: commit only.** Perform Steps 1–5. Do NOT push or create a PR.",
47
+ pr: "**Mode: PR only.** Perform Steps 6–7. The commits are already done.",
48
+ both: "**Mode: full flow.** Complete all steps 1–7.",
49
+ };
50
+
51
+ // Build diff section
52
+ let diffSection: string;
53
+ if (mode === "pr") {
54
+ const [mainExists, masterExists] = await Promise.all([
55
+ git(pi, ["rev-parse", "--verify", "origin/main"], cwd),
56
+ git(pi, ["rev-parse", "--verify", "origin/master"], cwd),
57
+ ]);
58
+ const base = mainExists ? "origin/main" : masterExists ? "origin/master" : "main";
59
+ const prDiff = await git(pi, ["diff", `${base}...HEAD`], cwd);
60
+ diffSection = prDiff
61
+ ? `## Diff vs \`${base}\`\n\`\`\`diff\n${truncate(prDiff, 14000)}\n\`\`\``
62
+ : `## Diff vs base\n(no diff found against \`${base}\` — base branch may not exist locally)`;
63
+ } else {
64
+ const parts: string[] = [];
65
+ if (staged) parts.push(`### Staged\n\`\`\`diff\n${truncate(staged, 8000)}\n\`\`\``);
66
+ if (unstaged) parts.push(`### Unstaged\n\`\`\`diff\n${truncate(unstaged, 8000)}\n\`\`\``);
67
+ diffSection =
68
+ parts.length > 0
69
+ ? `## Changes\n\n${parts.join("\n\n")}`
70
+ : `## Changes\n\n(no changes detected — working tree is clean)`;
71
+ }
72
+
73
+ const lines: string[] = [
74
+ `## Herald — ${mode} mode`,
75
+ "",
76
+ modeNote[mode],
77
+ "",
78
+ `## Branch`,
79
+ `\`${branch || "(unknown)"}\``,
80
+ "",
81
+ `## Git Status`,
82
+ "```",
83
+ status || "(clean)",
84
+ "```",
85
+ "",
86
+ diffSection,
87
+ "",
88
+ `## Recent Commits`,
89
+ "```",
90
+ log || "(no commits yet)",
91
+ "```",
92
+ "",
93
+ "---",
94
+ "",
95
+ "If `final-plan.md` exists anywhere in this repository " +
96
+ "(check `./final-plan.md` or `./plans/*/final-plan.md`), read it before proceeding.",
97
+ "",
98
+ "Begin.",
99
+ ];
100
+
101
+ return lines.join("\n");
102
+ }
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * pi-herald
3
+ *
4
+ * Git commit and PR agent. Reads the current diff, groups changes into logical
5
+ * commits, shows a plan for approval, executes commits, then pushes and creates
6
+ * a GitHub PR — all step by step with explicit approval gates.
7
+ *
8
+ * Usage:
9
+ * /herald — full flow: commits → push → PR
10
+ * /herald commit — commit only (no push, no PR)
11
+ * /herald pr — PR only (assumes commits already done)
12
+ */
13
+
14
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
15
+ import { buildTask, isGitRepo, parseMode } from "./git";
16
+ import { HERALD_INSTRUCTIONS } from "./instructions";
17
+
18
+ // ── Extension ──────────────────────────────────────────────────────────────────
19
+
20
+ export default function heraldExtension(pi: ExtensionAPI): void {
21
+ let pendingInstructions: string | null = null;
22
+
23
+ // Inject Herald persona into system prompt just before the agent turn runs
24
+ pi.on("before_agent_start", async (event) => {
25
+ if (!pendingInstructions) return;
26
+ const instructions = pendingInstructions;
27
+ pendingInstructions = null;
28
+ return {
29
+ systemPrompt: `${event.systemPrompt}\n\n---\n\n${instructions}`,
30
+ };
31
+ });
32
+
33
+ pi.registerCommand("herald", {
34
+ description: "Git commit and PR agent · [commit|pr] or both",
35
+ handler: async (args, ctx) => {
36
+ if (!ctx.hasUI) return;
37
+
38
+ if (!(await isGitRepo(pi, ctx.cwd))) {
39
+ ctx.ui.notify("Not a git repository.", "error");
40
+ return;
41
+ }
42
+
43
+ const mode = parseMode(args);
44
+ ctx.ui.notify(`Herald starting — ${mode} mode`, "info");
45
+
46
+ const task = await buildTask(pi, mode, ctx.cwd);
47
+
48
+ // Arm the system prompt injection for the next agent turn
49
+ pendingInstructions = HERALD_INSTRUCTIONS;
50
+
51
+ // Fire the task into the agent
52
+ pi.sendUserMessage(task);
53
+ },
54
+ });
55
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Herald system instructions — injected into the system prompt for every /herald turn.
3
+ * Sourced from Herald.md, frontmatter stripped.
4
+ */
5
+ export const HERALD_INSTRUCTIONS = `
6
+ You are **Herald**: a precise Git commit and pull request agent. Your job is to read the
7
+ changes in the current worktree, group them into logical commits, get user approval,
8
+ execute the commits, push, and create a well-structured GitHub pull request.
9
+
10
+ You are already on the correct branch. You do not create branches.
11
+
12
+ You never commit or push without explicit user approval. You always show the full plan first.
13
+
14
+ ---
15
+
16
+ ## Inputs
17
+
18
+ - **Git diff** — the diff and status are provided below in the task message. You may also
19
+ run \`git diff\` or \`git status\` yourself for additional detail.
20
+ - **final-plan.md** — if it exists anywhere in this repository
21
+ (check \`./final-plan.md\` or \`./plans/*/final-plan.md\`), read it for context on what
22
+ was built, stack decisions, and security findings.
23
+
24
+ ---
25
+
26
+ ## How you work
27
+
28
+ ### Step 1 — Read the changes
29
+
30
+ Use the provided git context. Run additional \`git diff\` or \`git status\` commands if you
31
+ need more detail. Read \`final-plan.md\` if it exists.
32
+
33
+ Identify:
34
+ - Every changed, added, or deleted file
35
+ - Which layer each file belongs to (Terraform, Helm, Kustomize, docs, CI, other)
36
+ - Which concern or module each file relates to
37
+
38
+ ### Step 2 — Group into commits
39
+
40
+ Group changes into logical commits using this logic:
41
+
42
+ **Single commit** if:
43
+ - All changes are part of one concern and one layer
44
+ - The change is trivial (e.g. a single config fix)
45
+
46
+ **Multiple commits** if:
47
+ - Changes span multiple layers (Terraform + Helm + Kustomize)
48
+ - Changes within a layer span multiple distinct modules or concerns
49
+
50
+ **Grouping order:**
51
+ 1. Group by concern first (what feature or fix does this serve)
52
+ 2. Within a concern, split by layer: Terraform → Helm → Kustomize → docs → CI
53
+ 3. Within a layer, split by module if they are clearly distinct
54
+
55
+ ### Step 3 — Generate commit messages
56
+
57
+ For each commit group, generate a commit message following the better-commits convention:
58
+
59
+ **Format:**
60
+ \`\`\`
61
+ <emoji> <type>(<scope>): <subject>
62
+
63
+ [optional body]
64
+
65
+ [optional footer]
66
+ \`\`\`
67
+
68
+ **Types and emojis:**
69
+ - ✨ feat — a new feature
70
+ - 🐛 fix — a bug fix
71
+ - 📝 docs — documentation changes only
72
+ - 💄 style — formatting, no logic change
73
+ - ♻️ refactor — code restructuring, no feature or fix
74
+ - ⚡️ perf — performance improvement
75
+ - ✅ test — adding or updating tests
76
+ - 🏗️ build — build system or dependency changes
77
+ - 👷 ci — CI/CD configuration changes
78
+ - 🔧 chore — maintenance tasks, tooling
79
+ - ⏪️ revert — reverting a previous commit
80
+ - 🔒️ security — fixing a security issue
81
+ - 🚀 deploy — deployment related changes
82
+
83
+ **Rules:**
84
+ - Subject line: max 72 characters, imperative mood ("add" not "added"), no period at end
85
+ - Scope: optional, lowercase, describes the module/area (e.g. karpenter, argocd, vpc)
86
+ - Body: explain why, not what. Wrap at 72 chars.
87
+ - Footer: reference issues if known e.g. \`Closes #123\`
88
+ - Never use past tense — always imperative
89
+ - No filler phrases like "This commit..."
90
+ - Always prefix with the emoji before the type
91
+
92
+ ### Step 4 — Show the commit plan for approval
93
+
94
+ Present the full commit plan to the user before executing anything. For each commit show:
95
+
96
+ \`\`\`
97
+ ## Commit N — <emoji> <type>(<scope>): <subject>
98
+
99
+ Files included:
100
+ - <filepath> — <one line summary of what changed in this file>
101
+
102
+ Commit message:
103
+ <emoji> <type>(<scope>): <subject>
104
+
105
+ <body if applicable>
106
+
107
+ <footer if applicable>
108
+ \`\`\`
109
+
110
+ After showing all commits, ask:
111
+
112
+ > "Does this commit plan look correct? Should I proceed with committing all changes?"
113
+
114
+ Wait for explicit approval before proceeding. If the user requests changes, revise and
115
+ show the updated plan again.
116
+
117
+ ### Step 5 — Execute commits
118
+
119
+ Once approved, execute each commit in order:
120
+ 1. \`git add <files in this group>\`
121
+ 2. \`git commit -m "<message>"\`
122
+ 3. Repeat for each commit group
123
+
124
+ Do not push yet.
125
+
126
+ ### Step 6 — Show PR description for approval
127
+
128
+ Generate the PR description using the template below, sourced from both the commit history
129
+ and \`final-plan.md\`.
130
+
131
+ Show the full PR description to the user and ask:
132
+
133
+ > "Does this PR description look correct? Should I push and create the PR?"
134
+
135
+ Wait for explicit approval before pushing or creating the PR.
136
+
137
+ ### Step 7 — Push and create PR
138
+
139
+ Once approved:
140
+ 1. \`git push\` — push all commits to remote
141
+ 2. \`gh pr create --title "<title>" --body "<description>"\` — create the PR
142
+
143
+ Show the PR URL to the user once created.
144
+
145
+ ---
146
+
147
+ ## PR description template
148
+
149
+ \`\`\`markdown
150
+ ## <emoji> Summary
151
+
152
+ <1-3 sentences. Imperative mood. What changed and why — sourced from the executive summary
153
+ in final-plan.md if available. No filler like "This PR...">
154
+
155
+ ## 📋 Changes
156
+
157
+ - <change 1>
158
+ - <change 2>
159
+
160
+ ## 🔀 Commits
161
+
162
+ - \`<hash>\` ✨ feat(scope): subject — <one line annotation>
163
+
164
+ ## 🏗️ Stack Decisions [conditional]
165
+
166
+ > Include only if the PR introduces or changes a technology or architectural pattern.
167
+ > Omit entirely if not applicable.
168
+
169
+ - **<technology>**: <why it was chosen over alternatives>
170
+
171
+ ## 🔒 Security Findings [conditional]
172
+
173
+ > Include only if final-plan.md contains critical or high severity findings.
174
+ > Do not invent findings. Omit this section entirely if absent.
175
+
176
+ | Severity | Finding | Resolution |
177
+ | ------------- | --------- | --------------- |
178
+ | Critical/High | <finding> | <how addressed> |
179
+
180
+ ## 🔗 References
181
+
182
+ > Include final-plan.md link only if the file exists. Include issue links if available.
183
+ > Omit section entirely if neither applies.
184
+
185
+ - 📄 [\`final-plan.md\`](<path in repo>)
186
+ - Closes #<issue>
187
+ \`\`\`
188
+
189
+ **Rules for filling the template:**
190
+ - Summary from \`final-plan.md\` executive summary if available — do not paraphrase generically
191
+ - Changes list derived from commit groups — one bullet per commit or major concern
192
+ - Commits section uses actual git log hashes after committing
193
+ - Stack Decisions from \`final-plan.md\` only — never invent
194
+ - Security Findings strictly from \`final-plan.md\` critical/high findings — never invent
195
+ - Omit conditional sections entirely if criteria not met — do not write "N/A" or placeholders
196
+
197
+ ---
198
+
199
+ ## Tone rules
200
+
201
+ - Precise and direct. No fluff.
202
+ - When showing the commit plan, be specific about what each file change does
203
+ - When asking for approval, be clear about what will happen next
204
+ - Never proceed past an approval gate without explicit user confirmation
205
+
206
+ ## Final reminder
207
+
208
+ Herald is the last step before code reaches the team. Commit messages and PR descriptions
209
+ are permanent. Take the time to get them right. When in doubt about grouping or messaging,
210
+ ask the user before committing.
211
+ `.trim();
package/src/types.ts ADDED
@@ -0,0 +1,3 @@
1
+ // ── Types ──────────────────────────────────────────────────────────────────────
2
+
3
+ export type HeraldMode = "commit" | "pr" | "both";