@senomas/pi-git-hat 0.2.3 → 0.2.6

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.
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Role file loading for git-hat.
3
+ *
4
+ * Loads role-specific .md files from the project .pi/ directory or
5
+ * the extension-bundled roles/ directory. Also contains the built-in
6
+ * instruction constants used as fallback when no .md file exists.
7
+ */
8
+
9
+ import { readFile, readdir } from "node:fs/promises";
10
+ import { resolve, dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ import type { MergedConfig } from "./types.js";
14
+
15
+ /** Directory containing this extension file. Used to resolve bundled roles/ directory. */
16
+ export const EXTENSION_DIR = dirname(fileURLToPath(import.meta.url));
17
+
18
+ /**
19
+ * Load a role's .md file from the project's fileDir directory.
20
+ * Case-insensitive lookup if config.caseInsensitive is true.
21
+ *
22
+ * Search order:
23
+ * 1. {cwd}/{fileDir}/{role}.md (project-local, highest priority)
24
+ * 2. {extensionDir}/roles/{role}.md (extension-bundled fallback)
25
+ */
26
+ export async function loadRoleFile(
27
+ cwd: string,
28
+ role: string,
29
+ config: MergedConfig,
30
+ ): Promise<string | null> {
31
+ // 1. Project-local: {cwd}/{fileDir}/{role}.md (highest priority)
32
+ const roleDir = resolve(cwd, config.fileDir);
33
+ const exactPath = resolve(roleDir, `${role}.md`);
34
+ try {
35
+ return await readFile(exactPath, "utf8");
36
+ } catch {
37
+ // not found with exact case
38
+ }
39
+
40
+ if (config.caseInsensitive) {
41
+ try {
42
+ const entries = await readdir(roleDir);
43
+ for (const entry of entries) {
44
+ if (entry.toLowerCase() === `${role}.md`) {
45
+ return await readFile(resolve(roleDir, entry), "utf8");
46
+ }
47
+ }
48
+ } catch {
49
+ // roleDir doesn't exist
50
+ }
51
+ }
52
+
53
+ // 2. Extension-bundled: {extensionDir}/roles/{role}.md (fallback)
54
+ const bundledRoleDir = resolve(EXTENSION_DIR, "roles");
55
+ const bundledExact = resolve(bundledRoleDir, `${role}.md`);
56
+ try {
57
+ return await readFile(bundledExact, "utf8");
58
+ } catch {
59
+ // not found with exact case
60
+ }
61
+
62
+ if (config.caseInsensitive) {
63
+ try {
64
+ const entries = await readdir(bundledRoleDir);
65
+ for (const entry of entries) {
66
+ if (entry.toLowerCase() === `${role}.md`) {
67
+ return await readFile(resolve(bundledRoleDir, entry), "utf8");
68
+ }
69
+ }
70
+ } catch {
71
+ // bundledRoleDir doesn't exist
72
+ }
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ // -- Built-in role instructions (fallback when no .md file exists) ---
79
+ // Canonical source: roles/{role}.md in the extension directory.
80
+ // Run `python3 scripts/validate-role-prompts.py` to verify alignment.
81
+
82
+ export const BUILTIN_INSTRUCTIONS: Record<string, string> = {
83
+ planner: `
84
+
85
+ ## Your Role: Planner
86
+
87
+ You are **PLANNER**. Your sole responsibility is to research (just collecting data for others to solved the problem) and create
88
+ \`todo/NN-name.md\` files. That is it. Switching branch does not switch your role.
89
+
90
+ ## What you do
91
+ 1. Research with \`read\` / \`grep\` / \`find\` / \`ls\`
92
+ 2. Create \`todo/NN-name.md\` with sequenced, actionable items
93
+ 3. Present the plan to the user
94
+
95
+ ## What you do NOT do
96
+ - \`edit\`/\`write\` any file outside \`todo/\`
97
+ - Switch branches (the user handles that)
98
+ - Implement anything (that's the implementor)
99
+ - Write reports (that's the implementor)
100
+
101
+ ## File format: \`todo/NN-name.md\`
102
+ - \`- [ ]\` pending, \`- [x]\` done
103
+ - First line after checkbox = **header**
104
+ - Indented lines = **body**
105
+ - Blank lines separate items
106
+
107
+ ## NN sequence rule
108
+ Every \`todo/NN-name.md\` must use NN **higher** than the highest NN in both
109
+ \`todo/\` **and** \`report/\`.
110
+ `,
111
+
112
+ implementor: `
113
+
114
+ ## Your Role: Implementor
115
+
116
+ You are **IMPLEMENTOR**. Your responsibility is to read todo/*.md files, implement and write reports. That is it. Switching branch does not switch your role.
117
+
118
+ ## Report format: \`report/NN-name.md\`
119
+ - Same NN and same headers as the todo
120
+ - Add implementation notes, decisions, and \`\`\`bash results
121
+
122
+ ## How to implement
123
+ 1. Read \`todo/NN-name.md\` (lowest NN first)
124
+ 2. If \`todo/NN-name.detail.md\` exists, read it too
125
+ 3. Implement each \`- [ ]\` item using \`edit\`/\`write\`
126
+ 4. After each item (or batch), update \`report/NN-name.md\`
127
+ 5. When done, move to the next NN
128
+
129
+ ## What you do NOT do
130
+ - Switch branches (the user handles that)
131
+ - Write to \`todo/\`, \`plan/\`, or \`.pi/\` — create reports only; let others verify and mark items done
132
+ - Create todo files (that's the planner's job)
133
+ - Modify or mark items in todo files (that's the reviewer's job after verification)
134
+ `,
135
+
136
+ reviewer: `
137
+
138
+ ## Your Role: Reviewer
139
+
140
+ You are **REVIEWER**. Your sole responsibility is to verify that reports fully cover their corresponding todos, and mark items done in \`todo/\`. That is it. Switching branch does not switch your role.
141
+
142
+ ## What you do
143
+ 1. Read \`todo/NN-name.md\` and \`report/NN-name.md\`
144
+ 2. Match each pending \`- [ ]\` todo item against a section in the report
145
+ 3. **Covered** → mark \`- [x]\` in the todo file
146
+ 4. **Missing** → leave \`- [ ]\`, tell the user
147
+ 5. Also check for orphan reports and stale todos
148
+
149
+ ## What you do NOT do
150
+ - \`edit\`/\`write\` any file outside \`todo/\`
151
+ - Write reports (that's the implementor)
152
+ - Create \`todo/\` or \`plan/\` files (that's the planner)
153
+ - Switch branches (the user handles that)
154
+ - Write to \`.pi/\`
155
+
156
+ ## Key rules
157
+ - **Covered** items get \`- [x]\` in the todo file
158
+ - **Missing** items stay \`- [ ]\` — explain why to the user
159
+ - Read source files and \`review/\` and \`todo/\` as needed to verify
160
+ `,
161
+
162
+ admin: `
163
+
164
+ ## Your Role: Admin
165
+
166
+ You are **ADMIN**. Switching branch does not switch your role.
167
+
168
+ ## What you do
169
+ - Edit \`.pi/\` files — role prompts, roles.json
170
+ - Edit \`README.md\` and \`AGENTS.md\`
171
+ - Explore the codebase (read-only)
172
+
173
+ ## What you do NOT do
174
+ - Edit source code
175
+ - Create or modify \`todo/\`, \`plan/\`, \`report/\` files
176
+ - Switch branches (the user handles that)
177
+ `,
178
+
179
+ researcher: `
180
+
181
+ ## Your Role: Researcher
182
+
183
+ You are **RESEARCHER**. Switching branch does not switch your role.
184
+
185
+ ## What you do
186
+ - Research with \`find\` / \`grep\` / \`ls\` / \`web_search\` / \`read\`
187
+ - Write findings to \`docs/*.md\` and root \`*.md\` files
188
+
189
+ ## What you do NOT do
190
+ - Edit source code
191
+ - Create or modify \`todo/\`, \`plan/\`, \`report/\` files
192
+ - Switch branches (the user handles that)
193
+ `,
194
+ };
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Todo utilities for git-hat.
3
+ *
4
+ * Functions for scanning todo/ files, verifying ancestry of branches,
5
+ * checking master/main ancestry, and the /hat todo handler.
6
+ */
7
+
8
+ import { readFile, readdir } from "node:fs/promises";
9
+ import { execFileSync } from "node:child_process";
10
+ import { resolve } from "node:path";
11
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
+
13
+ import type { MergedConfig, PendingItem, TodoFile } from "./types.js";
14
+ import { detectRole } from "./config.js";
15
+
16
+ /** Extract NN sequence number from a todo/ or report/ filename. */
17
+ export function extractNN(filename: string): number | null {
18
+ const match = filename.match(/^(\d+)-/);
19
+ return match ? parseInt(match[1], 10) : null;
20
+ }
21
+
22
+ /** Scan todo/ and report/ dirs for the highest NN in use. */
23
+ export async function findMaxNN(cwd: string): Promise<number> {
24
+ let max = 0;
25
+ for (const dirName of ["todo", "report"]) {
26
+ try {
27
+ const entries = await readdir(resolve(cwd, dirName));
28
+ for (const entry of entries) {
29
+ const nn = extractNN(entry);
30
+ if (nn !== null && nn > max) max = nn;
31
+ }
32
+ } catch {
33
+ // dir doesn't exist yet
34
+ }
35
+ }
36
+ return max;
37
+ }
38
+
39
+ /** Scan todos and cross-reference with reports. */
40
+ async function scanTodos(cwd: string): Promise<{
41
+ files: TodoFile[];
42
+ totalPending: number;
43
+ totalCovered: number;
44
+ summary: string;
45
+ }> {
46
+ const files: TodoFile[] = [];
47
+ let totalPending = 0;
48
+ let totalCovered = 0;
49
+
50
+ try {
51
+ const entries = await readdir(resolve(cwd, "todo"));
52
+ for (const entry of entries.sort()) {
53
+ if (!entry.endsWith(".md") || entry.endsWith(".detail.md")) continue;
54
+ const content = await readFile(resolve(cwd, "todo", entry), "utf8");
55
+ const pending: PendingItem[] = [];
56
+ for (const [i, line] of content.split("\n").entries()) {
57
+ const m = line.match(/^- \[ \] (.+)$/);
58
+ if (m) pending.push({ lineno: i + 1, header: m[1].trim(), covered: false });
59
+ }
60
+ if (pending.length === 0) continue;
61
+
62
+ let reportHeaders: string[] = [];
63
+ try {
64
+ const reportContent = await readFile(resolve(cwd, "report", entry), "utf8");
65
+ for (const line of reportContent.split("\n")) {
66
+ const m = line.match(/^- (.+)$/);
67
+ if (m && !line.includes("[ ]") && !line.includes("[x]")) {
68
+ reportHeaders.push(m[1].trim().toLowerCase());
69
+ }
70
+ }
71
+ } catch {
72
+ // no matching report
73
+ }
74
+
75
+ const reportExists = reportHeaders.length > 0;
76
+ for (const p of pending) {
77
+ p.covered = reportHeaders.some(
78
+ (h) => p.header.toLowerCase().includes(h) || h.includes(p.header.toLowerCase()),
79
+ );
80
+ if (p.covered) totalCovered++;
81
+ }
82
+ totalPending += pending.length;
83
+ files.push({ file: entry, pending, reportExists });
84
+ }
85
+ } catch {
86
+ // todo/ doesn"t exist
87
+ }
88
+
89
+ const displayLines: string[] = [];
90
+ for (const f of files) {
91
+ const tag = f.reportExists ? "" : " (no report)";
92
+ displayLines.push(`todo/${f.file}${tag}`);
93
+ for (const p of f.pending) {
94
+ displayLines.push(` ${p.covered ? "\u2705" : "\u274C"} line ${p.lineno}: ${p.header}`);
95
+ }
96
+ }
97
+ displayLines.push(
98
+ `\u2500\u2500 ${totalPending - totalCovered} missing \u00B7 ${totalCovered} covered \u00B7 ${totalPending} total \u2500\u2500`,
99
+ );
100
+
101
+ return { files, totalPending, totalCovered, summary: displayLines.join("\n") };
102
+ }
103
+
104
+ /** Ancestry check: verify current branch descends from ancestor role branches. */
105
+ export async function verifyAncestry(config: MergedConfig, cwd: string): Promise<{
106
+ ok: boolean;
107
+ violations: { role: string; branch: string; currentBranch: string }[];
108
+ }> {
109
+ const violations: { role: string; branch: string; currentBranch: string }[] = [];
110
+
111
+ let currentBranch: string;
112
+ try {
113
+ currentBranch = execFileSync("git", ["branch", "--show-current"], {
114
+ cwd,
115
+ encoding: "utf-8",
116
+ }).trim();
117
+ if (!currentBranch) return { ok: true, violations }; // not on a branch (detached)
118
+ } catch {
119
+ return { ok: true, violations }; // not a git repo
120
+ }
121
+
122
+ const role = detectRole(currentBranch, config);
123
+ if (!role) return { ok: true, violations }; // unknown role
124
+
125
+ const ancestorRoles = config.roles[role]?.ancestorRoles;
126
+ if (!ancestorRoles || ancestorRoles.length === 0) return { ok: true, violations }; // no ancestors required
127
+
128
+ // Get all local branches
129
+ let allBranches: string[];
130
+ try {
131
+ const stdout = execFileSync("git", ["branch", "--format", "%(refname:short)"], {
132
+ cwd,
133
+ encoding: "utf-8",
134
+ });
135
+ allBranches = stdout.trim().split("\n").filter(Boolean);
136
+ } catch {
137
+ return { ok: true, violations };
138
+ }
139
+
140
+ for (const ancestorRole of ancestorRoles) {
141
+ const pattern = config.roles[ancestorRole]?.pattern;
142
+ if (!pattern) continue;
143
+
144
+ const regex = new RegExp(pattern);
145
+ const candidates = allBranches.filter((b) => regex.test(b));
146
+
147
+ for (const candidate of candidates) {
148
+ if (candidate === currentBranch) continue; // same branch, trivially ancestor
149
+
150
+ try {
151
+ execFileSync("git", ["merge-base", "--is-ancestor", candidate, currentBranch], {
152
+ cwd,
153
+ stdio: "pipe",
154
+ });
155
+ // exit code 0 = is ancestor, no violation
156
+ } catch {
157
+ // exit code 1 or error = not ancestor, or deleted branch — record violation
158
+ violations.push({ role: ancestorRole, branch: candidate, currentBranch });
159
+ }
160
+ }
161
+ }
162
+
163
+ return { ok: violations.length === 0, violations };
164
+ }
165
+
166
+ /** Check if master/main is an ancestor of the current branch. */
167
+ export async function isMasterOrMainAncestor(cwd: string): Promise<{ ok: boolean; branch: string }> {
168
+ let currentBranch: string;
169
+ try {
170
+ currentBranch = execFileSync("git", ["branch", "--show-current"], {
171
+ cwd,
172
+ encoding: "utf-8",
173
+ }).trim();
174
+ if (!currentBranch) return { ok: true, branch: "master" }; // detached
175
+ } catch {
176
+ return { ok: true, branch: "master" }; // not a git repo
177
+ }
178
+
179
+ // try master first, then main
180
+ for (const candidate of ["master", "main"]) {
181
+ try {
182
+ execFileSync("git", ["rev-parse", "--verify", `refs/heads/${candidate}`], {
183
+ cwd,
184
+ stdio: "pipe",
185
+ });
186
+ // branch exists
187
+ if (currentBranch === candidate) return { ok: true, branch: candidate }; // already on it
188
+ try {
189
+ execFileSync("git", ["merge-base", "--is-ancestor", candidate, currentBranch], {
190
+ cwd,
191
+ stdio: "pipe",
192
+ });
193
+ return { ok: true, branch: candidate }; // is ancestor
194
+ } catch {
195
+ return { ok: false, branch: candidate }; // not ancestor
196
+ }
197
+ } catch {
198
+ continue; // branch doesn't exist locally, try next
199
+ }
200
+ }
201
+
202
+ // neither master nor main exists locally — skip check
203
+ return { ok: true, branch: "master" };
204
+ }
205
+
206
+ /** /hat todo handler: scan todos, notify user, and send follow-up for pending items. */
207
+ export async function handleTodo(
208
+ pi: ExtensionAPI,
209
+ ctx: { cwd: string; ui: { notify: (msg: string, level: string) => void } },
210
+ ): Promise<void> {
211
+ // plan/*.md files are excluded from /hat todo — they store future ideas,
212
+ // not actionable todos. scanTodos() already reads only from the todo/ directory.
213
+ const result = await scanTodos(ctx.cwd);
214
+ if (result.files.length === 0) {
215
+ ctx.ui.notify("\uD83D\uDCED No pending todos found", "info");
216
+ return;
217
+ }
218
+
219
+ const remaining = result.totalPending - result.totalCovered;
220
+
221
+ if (remaining === 0) {
222
+ ctx.ui.notify("\u2714\uFE0F No more task in todo", "info");
223
+ return;
224
+ }
225
+
226
+ ctx.ui.notify(result.summary, "info");
227
+
228
+ if (remaining > 0) {
229
+ const detail = result.files
230
+ .map((f) => {
231
+ const items = f.pending
232
+ .map((p) => ` ${p.covered ? "\u2705" : "\u274C"} ${p.header}`)
233
+ .join("\n");
234
+ return `todo/${f.file}${f.reportExists ? "" : " (no report)"}\n${items}`;
235
+ })
236
+ .join("\n");
237
+ pi.sendUserMessage(
238
+ `Analyze these pending todos against their reports:\n\n${detail}\n\n` +
239
+ `For each \u274C item, check if it"s truly not implemented yet or if the report ` +
240
+ `just uses different wording. If it"s genuinely missing, suggest next steps.`,
241
+ { deliverAs: "followUp" },
242
+ );
243
+ }
244
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Tool enforcement engine — reusable by extensions and packages.
3
+ *
4
+ * Provides types and functions for config-driven tool rule evaluation.
5
+ *
6
+ * Exports:
7
+ * - RegexObj, ToolRule (types)
8
+ * - testRegex() — recursive regex matching (string / any / all)
9
+ * - evaluateToolRules() — first-match-wins rule evaluation
10
+ */
11
+
12
+ // -- Types ---------------------------------------------------------
13
+
14
+ export interface RegexObj {
15
+ type: "any" | "all";
16
+ regex: (string | RegexObj)[];
17
+ }
18
+
19
+ export interface ToolRule {
20
+ type: "allow" | "block" | "confirm";
21
+ regex: string | RegexObj;
22
+ reason?: string;
23
+ }
24
+
25
+ // -- Regex matching ------------------------------------------------
26
+
27
+ /**
28
+ * Recursively test a command against a regex pattern.
29
+ *
30
+ * - **string**: direct `new RegExp(regex).test(command)`, invalid patterns skipped
31
+ * - **`{ type: "any", regex: [...] }`**: true if **any** sub-regex matches (OR)
32
+ * - **`{ type: "all", regex: [...] }`**: true if **all** sub-regexes match (AND)
33
+ *
34
+ * Sub-regexes can themselves be strings or nested `RegexObj` values.
35
+ */
36
+ export function testRegex(regex: string | RegexObj, command: string): boolean {
37
+ if (typeof regex === "string") {
38
+ try {
39
+ return new RegExp(regex).test(command);
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+ // RegexObj
45
+ if (regex.type === "any") {
46
+ return regex.regex.some((r) => testRegex(r, command));
47
+ }
48
+ // type === "all"
49
+ return regex.regex.every((r) => testRegex(r, command));
50
+ }
51
+
52
+ // -- Rule evaluation -----------------------------------------------
53
+
54
+ /**
55
+ * Evaluate a command against an ordered list of tool rules.
56
+ *
57
+ * **First matching rule wins**: if a rule matches and its type is `"allow"`,
58
+ * the command is permitted. If `"block"`, the command is denied with an
59
+ * optional reason. If `"confirm"`, the command requires user confirmation.
60
+ * If no rule matches, the command also requires user confirmation (safe default).
61
+ *
62
+ * @param rules - Array of `ToolRule` objects, or `undefined` (treated as "not configured")
63
+ * @param command - The string to test (bash command, tool name, etc.)
64
+ * @returns
65
+ * - `{ allowed: true }` — command is permitted unconditionally
66
+ * - `{ allowed: false, reason }` — command is denied
67
+ * - `{ allowed: true, confirm: true, reason }` — command requires user confirmation
68
+ */
69
+ export function evaluateToolRules(
70
+ rules: ToolRule[] | undefined,
71
+ command: string,
72
+ ): { allowed: boolean; confirm?: boolean; reason?: string; matched: boolean } {
73
+ if (!rules || rules.length === 0) {
74
+ // No rules defined or empty array = caller should fall back to hardcoded behaviour
75
+ return { allowed: true, matched: false };
76
+ }
77
+ for (const rule of rules) {
78
+ if (testRegex(rule.regex, command)) {
79
+ if (rule.type === "allow") {
80
+ return { allowed: true, matched: true };
81
+ }
82
+ if (rule.type === "confirm") {
83
+ return { allowed: true, confirm: true, reason: rule.reason, matched: true };
84
+ }
85
+ // block
86
+ return { allowed: false, reason: rule.reason, matched: true };
87
+ }
88
+ }
89
+ // No rule matched — caller should continue to next stage
90
+ return { allowed: true, matched: false };
91
+ }
package/lib/types.ts ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Shared type definitions for git-hat extension.
3
+ *
4
+ * Re-exported from git-hat.ts into lib/ to reduce file size.
5
+ */
6
+
7
+ export interface WritablePathEntry {
8
+ /** Directory or file path (relative to project root). Empty string = project root. */
9
+ path: string;
10
+ /** Optional file extension filter, e.g. "md" — if set, only files with this extension are writable. */
11
+ extension?: string;
12
+ }
13
+
14
+ export interface RoleDef {
15
+ pattern: string;
16
+ description?: string;
17
+ ancestorRoles?: string[];
18
+ tool?: ToolRule[];
19
+ /** Tool rules evaluated after default post-tool rules (snake_case key "post-tool" in JSON). */
20
+ postTool?: ToolRule[];
21
+ writablePaths?: WritablePathEntry[];
22
+ }
23
+
24
+ export interface RolesConfig {
25
+ version?: number;
26
+ roles?: Record<string, RoleDef>;
27
+ defaultRole?: string;
28
+ fileDir?: string;
29
+ caseInsensitive?: boolean;
30
+
31
+ default?: {
32
+ /** Tool rules evaluated before role-specific rules (snake_case key "pre-tool" in JSON). */
33
+ preTool?: ToolRule[];
34
+ /** Tool rules evaluated after role-specific rules (snake_case key "post-tool" in JSON). */
35
+ postTool?: ToolRule[];
36
+ };
37
+
38
+ postSwitchLog?: boolean;
39
+ }
40
+
41
+ export interface MergedConfig {
42
+ roles: Record<string, RoleDef>;
43
+ fileDir: string;
44
+ caseInsensitive: boolean;
45
+
46
+ /** Default pre-tool rules loaded from roles.json default.pre-tool */
47
+ preTool?: ToolRule[];
48
+ /** Default post-tool rules loaded from roles.json default.post-tool */
49
+ postTool?: ToolRule[];
50
+
51
+ postSwitchLog: boolean;
52
+ configFile: string;
53
+ }
54
+
55
+ import type { ToolRule } from "./tool-enforcement.js";
56
+
57
+ // -- Re-export for convenience
58
+ export type { ToolRule };
59
+
60
+ export interface SwitchLogEntry {
61
+ ts: string;
62
+ role: string;
63
+ branch: string;
64
+ }
65
+
66
+ export interface BranchHistory {
67
+ [role: string]: string | undefined;
68
+ }
69
+
70
+ export interface BranchEntry {
71
+ branch: string;
72
+ role: string;
73
+ isCurrent: boolean;
74
+ isLastUsed: boolean;
75
+ }
76
+
77
+ export interface PendingItem {
78
+ lineno: number;
79
+ header: string;
80
+ covered: boolean;
81
+ }
82
+
83
+ export interface TodoFile {
84
+ file: string;
85
+ pending: PendingItem[];
86
+ reportExists: boolean;
87
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senomas/pi-git-hat",
3
- "version": "0.2.3",
3
+ "version": "0.2.6",
4
4
  "description": "Pi extension for role-based Git branch workflows — wear different hats by switching branches",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package", "git", "workflow", "branching", "roles"],
@@ -0,0 +1,26 @@
1
+ # Workflow Rules
2
+
3
+ ## Role boundaries
4
+ - Different roles (planner, implementor, reviewer, admin) may be run by different people or on different machines.
5
+ - Do **not** assume another role will act on your suggestions or that the current user manages all roles.
6
+ - Focus only on the work assigned to your current role.
7
+ - If a task requires a different role, report it in the report and stop.
8
+ - **Follow your role strictly to the letter.** Do not deviate from your role's defined write permissions and responsibilities.
9
+
10
+ ## Branch discipline
11
+ - Do NOT switch branches unless the user explicitly asks.
12
+ - Do NOT create new branches on your own.
13
+ - Let the user handle all git branch operations.
14
+ - When the user tells you to "switch to X" or "go to branch X", they mean **you should run `git switch X` yourself** — treat it as an explicit instruction to execute the command.
15
+ - When the user says "rebase X", they mean **run `git rebase X`** — rebase the current branch onto X. Do NOT interpret it as "switch to X".
16
+ - After a successful rebase, **show the commit tree** (`git log --oneline --graph --all --decorate` or `/hatl`) so the user can see the new state.
17
+ - When the user says "commit", they mean **run `git add` and `git commit` yourself** — treat it as an explicit instruction to execute.
18
+
19
+ ## Enforcement
20
+ - If an enforcement rule blocks you, respect it.
21
+ - Do NOT try to sidestep enforcement with scripts, alternative tools, or workarounds.
22
+ - Explain to the user what is blocked and why, and show the command they can run manually to unblock.
23
+
24
+ ## Never suggest branch switching
25
+ - **Never suggest or recommend switching branches.** Not as advice, not as a next step — under any circumstance.
26
+ - Wait for the user to explicitly ask or tell you.