@senomas/pi-git-hat 0.2.1 → 0.2.3

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/git-hat.ts CHANGED
@@ -60,7 +60,7 @@ interface RolesConfig {
60
60
  defaultRole?: string;
61
61
  fileDir?: string;
62
62
  caseInsensitive?: boolean;
63
- redetectOnInput?: boolean;
63
+
64
64
  postSwitchLog?: boolean;
65
65
  }
66
66
 
@@ -68,7 +68,7 @@ interface MergedConfig {
68
68
  roles: Record<string, RoleDef>;
69
69
  fileDir: string;
70
70
  caseInsensitive: boolean;
71
- redetectOnInput: boolean;
71
+
72
72
  postSwitchLog: boolean;
73
73
  configFile: string;
74
74
  }
@@ -229,7 +229,7 @@ function loadConfig(): MergedConfig {
229
229
  roles: {},
230
230
  fileDir: ".pi",
231
231
  caseInsensitive: true,
232
- redetectOnInput: true,
232
+
233
233
  postSwitchLog: true,
234
234
  };
235
235
 
@@ -252,7 +252,7 @@ function loadConfig(): MergedConfig {
252
252
  }
253
253
  if (raw.fileDir) merged.fileDir = raw.fileDir;
254
254
  if (raw.caseInsensitive !== undefined) merged.caseInsensitive = raw.caseInsensitive;
255
- if (raw.redetectOnInput !== undefined) merged.redetectOnInput = raw.redetectOnInput;
255
+
256
256
  if (raw.postSwitchLog !== undefined) merged.postSwitchLog = raw.postSwitchLog;
257
257
  } catch {
258
258
  // ignore parse errors
@@ -497,6 +497,22 @@ You are **ADMIN**. Switching branch does not switch your role.
497
497
  - Edit \`README.md\` and \`AGENTS.md\`
498
498
  - Explore the codebase (read-only)
499
499
 
500
+ ## What you do NOT do
501
+ - Edit source code
502
+ - Create or modify \`todo/\`, \`plan/\`, \`report/\` files
503
+ - Switch branches (the user handles that)
504
+ `,
505
+
506
+ researcher: `
507
+
508
+ ## Your Role: Researcher
509
+
510
+ You are **RESEARCHER**. Switching branch does not switch your role.
511
+
512
+ ## What you do
513
+ - Research with \`find\` / \`grep\` / \`ls\` / \`web_search\` / \`read\`
514
+ - Write findings to \`docs/*.md\` and root \`*.md\` files
515
+
500
516
  ## What you do NOT do
501
517
  - Edit source code
502
518
  - Create or modify \`todo/\`, \`plan/\`, \`report/\` files
@@ -723,6 +739,7 @@ function roleIcon(role: string): string {
723
739
  if (lower === "implementor") return "\uD83D\uDEE0";
724
740
  if (lower === "reviewer") return "\uD83D\uDD0D";
725
741
  if (lower === "admin") return "\u2699";
742
+ if (lower === "researcher") return "\uD83D\uDD2C";
726
743
  return "\uD83E\uDDE2";
727
744
  }
728
745
 
@@ -1124,9 +1141,114 @@ export default function (pi: ExtensionAPI) {
1124
1141
  },
1125
1142
  });
1126
1143
 
1144
+ // -- /hatr command: rebase onto most recent role-matched branch --
1145
+
1146
+ pi.registerCommand("hatr", {
1147
+ description: "Rebase current branch onto the most recent role-matched branch",
1148
+ handler: async (_args, ctx) => {
1149
+ // Edge case: detached HEAD or non-git directory
1150
+ const branch = await detectBranch();
1151
+ if (!branch) {
1152
+ ctx.ui.notify("Not on a branch (detached HEAD or not a git repo). Aborting.", "warning");
1153
+ return;
1154
+ }
1155
+ const currentBranch = branch;
1156
+
1157
+ // List all local branches
1158
+ let allBranches: string[] = [];
1159
+ try {
1160
+ const result = await pi.exec("git", ["branch", "--format", "%(refname:short)"]);
1161
+ allBranches = result.stdout.trim().split("\n").filter(Boolean);
1162
+ } catch {
1163
+ ctx.ui.notify("Failed to list git branches.", "error");
1164
+ return;
1165
+ }
1166
+
1167
+ // Filter to role-matched branches, excluding current
1168
+ const candidates: string[] = [];
1169
+ for (const b of allBranches) {
1170
+ if (b === currentBranch) continue;
1171
+ if (detectRole(b, config)) candidates.push(b);
1172
+ }
1173
+
1174
+ // Edge case: no other role-matched branches
1175
+ if (candidates.length === 0) {
1176
+ ctx.ui.notify("No other role-matched branches to rebase onto.", "info");
1177
+ return;
1178
+ }
1179
+
1180
+ // Get latest commit info for each candidate
1181
+ interface CandidateInfo {
1182
+ branch: string;
1183
+ timestamp: number;
1184
+ abbrev: string;
1185
+ subject: string;
1186
+ }
1187
+ const infos: CandidateInfo[] = [];
1188
+ for (const b of candidates) {
1189
+ try {
1190
+ const result = await pi.exec("git", [
1191
+ "log", "--format=%ct %h %s", "-1", b,
1192
+ ]);
1193
+ const line = result.stdout.trim();
1194
+ if (!line) continue;
1195
+ const space1 = line.indexOf(" ");
1196
+ const space2 = line.indexOf(" ", space1 + 1);
1197
+ if (space1 === -1 || space2 === -1) continue;
1198
+ const timestamp = parseInt(line.slice(0, space1), 10);
1199
+ const abbrev = line.slice(space1 + 1, space2);
1200
+ const subject = line.slice(space2 + 1);
1201
+ infos.push({ branch: b, timestamp, abbrev, subject });
1202
+ } catch {
1203
+ // skip branches we can't read
1204
+ }
1205
+ }
1206
+
1207
+ if (infos.length === 0) {
1208
+ ctx.ui.notify("Could not determine latest commits for candidates.", "error");
1209
+ return;
1210
+ }
1211
+
1212
+ // Pick the most recent
1213
+ infos.sort((a, b) => b.timestamp - a.timestamp);
1214
+ const target = infos[0];
1215
+
1216
+ // Show and confirm
1217
+ const confirmed = await ctx.ui.confirm(
1218
+ `Rebase ${currentBranch} onto ${target.branch}? (latest commit: ${target.abbrev} ${target.subject})`,
1219
+ );
1220
+ if (!confirmed) {
1221
+ ctx.ui.notify("Rebase cancelled.", "info");
1222
+ return;
1223
+ }
1224
+
1225
+ // Run the rebase
1226
+ try {
1227
+ const result = await pi.exec("git", ["rebase", target.branch]);
1228
+ if (result.code === 0) {
1229
+ ctx.ui.notify(
1230
+ `Rebase onto ${target.branch} succeeded. Updated commit tree:`,
1231
+ "info",
1232
+ );
1233
+ await showGitLog(ctx, 10);
1234
+ } else {
1235
+ ctx.ui.notify(
1236
+ `Rebase onto ${target.branch} failed:\n${result.stderr}\n\nResolve conflicts manually, then run \`git rebase --continue\`.`,
1237
+ "error",
1238
+ );
1239
+ }
1240
+ } catch (e) {
1241
+ ctx.ui.notify(
1242
+ `Rebase onto ${target.branch} failed: ${(e as Error).message}\n\nResolve conflicts manually, then run \`git rebase --continue\`.`,
1243
+ "error",
1244
+ );
1245
+ }
1246
+ },
1247
+ });
1248
+
1127
1249
  // -- Session lifecycle ----------------------------------------
1128
1250
 
1129
- pi.on("session_start", async (event, ctx) => {
1251
+ pi.on("session_start", async (event, ctx) => {"}]}
1130
1252
  cwdAbsolute = ctx.cwd;
1131
1253
 
1132
1254
  // Seed bundled roles/ files to project .pi/ before loading config
@@ -1200,18 +1322,14 @@ export default function (pi: ExtensionAPI) {
1200
1322
  });
1201
1323
 
1202
1324
  pi.on("input", async (event, ctx) => {
1203
- if (config.redetectOnInput) {
1204
- await updateRole(ctx);
1205
- }
1325
+ await updateRole(ctx);
1206
1326
  return { action: "continue" };
1207
1327
  });
1208
1328
 
1209
1329
  // -- Re-detect after every turn (catches git checkout/switch) --
1210
1330
 
1211
1331
  pi.on("turn_end", async (_event, ctx) => {
1212
- if (config.redetectOnInput) {
1213
- await updateRole(ctx);
1214
- }
1332
+ await updateRole(ctx);
1215
1333
  });
1216
1334
 
1217
1335
  // -- System prompt injection ----------------------------------
@@ -1415,6 +1533,15 @@ If the user asks you to make changes, tell them to switch to an appropriate bran
1415
1533
  };
1416
1534
  }
1417
1535
 
1536
+ // Researcher: docs/ and root *.md only
1537
+ if (lowerRole === "researcher") {
1538
+ if (isInside(path, ["docs"]) || (path.endsWith(".md") && !path.includes("/"))) return;
1539
+ return {
1540
+ block: true,
1541
+ reason: `\uD83D\uDD2C Researcher: can only write to docs/ and root *.md. Blocked: ${rawPath}`,
1542
+ };
1543
+ }
1544
+
1418
1545
  // Custom roles: no hard write restrictions (base-enforce handles it)
1419
1546
  return;
1420
1547
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senomas/pi-git-hat",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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"],
package/roles/planner.md CHANGED
@@ -6,10 +6,11 @@ You are **PLANNER**. Your sole responsibility is to research (just collecting da
6
6
  ## What you do
7
7
  1. Research with `read` / `grep` / `find` / `ls`
8
8
  2. Create `todo/NN-name.md` with sequenced, actionable items
9
- 3. Present the plan to the user
9
+ 3. Write to `docs/*.md` files (design docs, research notes, architecture decisions)
10
+ 4. Present the plan to the user
10
11
 
11
12
  ## What you do NOT do
12
- - `edit`/`write` any file outside `todo/`
13
+ - `edit`/`write` any file outside `todo/` or `docs/*.md`
13
14
  - Switch branches (the user handles that)
14
15
  - Implement anything (that's the implementor)
15
16
  - Write reports (that's the implementor)
@@ -0,0 +1,12 @@
1
+ # Researcher
2
+
3
+ You are **RESEARCHER**. Switching branch does not switch your role.
4
+
5
+ ## What you do
6
+ - Research with `find` / `grep` / `ls` / `web_search` / `read`
7
+ - Write findings to `docs/*.md` and root `*.md` files
8
+
9
+ ## What you do NOT do
10
+ - Edit source code
11
+ - Create or modify `todo/`, `plan/`, `report/` files
12
+ - Switch branches (the user handles that)
package/roles/roles.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "planner": { "pattern": "^plan(-|/|$)", "description": "Plan work by creating todo/plan files" },
4
4
  "implementor": { "pattern": "^(implementor$|feature|feat|impl$|work$)(-|/|$)", "description": "Feature/impl/work branches" },
5
5
  "reviewer": { "pattern": "^review(-|/|$)", "description": "Review implementations against todos" },
6
- "admin": { "pattern": "^(main|master)$", "description": "Project configuration and docs" }
6
+ "admin": { "pattern": "^(main|master)$", "description": "Project configuration and docs" },
7
+ "researcher": { "pattern": "^research(-|/|$)", "description": "Research topics and document findings" }
7
8
  }
8
9
  }