@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 +138 -11
- package/package.json +1 -1
- package/roles/planner.md +3 -2
- package/roles/researcher.md +12 -0
- package/roles/roles.json +2 -1
package/git-hat.ts
CHANGED
|
@@ -60,7 +60,7 @@ interface RolesConfig {
|
|
|
60
60
|
defaultRole?: string;
|
|
61
61
|
fileDir?: string;
|
|
62
62
|
caseInsensitive?: boolean;
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|