@senomas/pi-git-hat 0.2.6 → 0.2.9
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 +39 -21
- package/lib/role-file.ts +38 -1
- package/package.json +11 -3
- package/roles/planner.md +2 -2
package/git-hat.ts
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
47
47
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
48
48
|
import { readFile, readdir, mkdir, copyFile } from "node:fs/promises";
|
|
49
|
-
import { existsSync } from "node:fs";
|
|
49
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
50
50
|
import { resolve } from "node:path";
|
|
51
51
|
import {
|
|
52
52
|
Container,
|
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
import type { MergedConfig } from "./lib/types.js";
|
|
63
63
|
import { loadConfig, detectRole } from "./lib/config.js";
|
|
64
64
|
import { recordBranchUsage, listBranchesByRole } from "./lib/branch-history.js";
|
|
65
|
-
import { loadRoleFile, BUILTIN_INSTRUCTIONS, EXTENSION_DIR } from "./lib/role-file.js";
|
|
65
|
+
import { getRoleFileSource, loadRoleFile, BUILTIN_INSTRUCTIONS, EXTENSION_DIR } from "./lib/role-file.js";
|
|
66
66
|
import { normalisePath, stripCdPrefix, isInside, isWritablePath } from "./lib/paths.js";
|
|
67
67
|
import { extractNN, findMaxNN, verifyAncestry, isMasterOrMainAncestor, handleTodo } from "./lib/todo-utils.js";
|
|
68
68
|
import { roleIcon, showGitLog, showGitStatus, colorizeLog } from "./lib/git-ui.js";
|
|
@@ -154,11 +154,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
154
154
|
// /hat info: show role status
|
|
155
155
|
if (sub === "info") {
|
|
156
156
|
const desc = currentRole ? config.roles[currentRole]?.description ?? "" : "";
|
|
157
|
-
const
|
|
158
|
-
const
|
|
157
|
+
const roleSource = currentRole ? getRoleFileSource(ctx.cwd, currentRole, config) : null;
|
|
158
|
+
const sourceStr = roleSource ? ` | ${roleSource}` : " | (none)";
|
|
159
159
|
|
|
160
160
|
const roleDisplay = currentRole
|
|
161
|
-
? `${roleIcon(currentRole)} ${currentRole}${
|
|
161
|
+
? `${roleIcon(currentRole)} ${currentRole}${sourceStr}`
|
|
162
162
|
: "\u2753 No matching role (read-only mode)";
|
|
163
163
|
|
|
164
164
|
const defaultInfo = [];
|
|
@@ -399,16 +399,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
399
399
|
return;
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
-
//
|
|
402
|
+
// All branches except current
|
|
403
403
|
const candidates: string[] = [];
|
|
404
404
|
for (const b of allBranches) {
|
|
405
405
|
if (b === currentBranch) continue;
|
|
406
|
-
|
|
406
|
+
candidates.push(b);
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
-
// Edge case: no other
|
|
409
|
+
// Edge case: no other branches
|
|
410
410
|
if (candidates.length === 0) {
|
|
411
|
-
ctx.ui.notify("No other
|
|
411
|
+
ctx.ui.notify("No other branches to rebase onto.", "info");
|
|
412
412
|
return;
|
|
413
413
|
}
|
|
414
414
|
|
|
@@ -423,7 +423,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
423
423
|
for (const b of candidates) {
|
|
424
424
|
try {
|
|
425
425
|
const result = await pi.exec("git", [
|
|
426
|
-
"log", "--format=%ct %h %s", "-1", b,
|
|
426
|
+
"log", "--format=%ct %h %s", "-1", b, "--",
|
|
427
427
|
]);
|
|
428
428
|
const line = result.stdout.trim();
|
|
429
429
|
if (!line) continue;
|
|
@@ -440,7 +440,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
440
440
|
}
|
|
441
441
|
|
|
442
442
|
if (infos.length === 0) {
|
|
443
|
-
ctx.ui.notify("
|
|
443
|
+
ctx.ui.notify("No other branches to rebase onto.", "info");
|
|
444
444
|
return;
|
|
445
445
|
}
|
|
446
446
|
|
|
@@ -615,14 +615,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
615
615
|
await updateRole(ctx);
|
|
616
616
|
|
|
617
617
|
// Print startup info (same as /hat info, plus config file path)
|
|
618
|
+
const pkg = JSON.parse(readFileSync(resolve(EXTENSION_DIR, "package.json"), "utf-8"));
|
|
619
|
+
const version = pkg.version ?? "unknown";
|
|
618
620
|
const desc = currentRole ? config.roles[currentRole]?.description ?? "" : "";
|
|
619
|
-
const
|
|
620
|
-
?
|
|
621
|
+
const roleSource = currentRole
|
|
622
|
+
? getRoleFileSource(ctx.cwd, currentRole, config)
|
|
621
623
|
: null;
|
|
622
|
-
const
|
|
624
|
+
const sourceStr = roleSource ? ` | ${roleSource}` : " | (none)";
|
|
623
625
|
|
|
624
626
|
const roleDisplay = currentRole
|
|
625
|
-
? `${roleIcon(currentRole)} ${currentRole}${
|
|
627
|
+
? `${roleIcon(currentRole)} ${currentRole}${sourceStr}`
|
|
626
628
|
: "\u2753 No matching role (read-only mode)";
|
|
627
629
|
|
|
628
630
|
const defaultInfo = [];
|
|
@@ -633,6 +635,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
633
635
|
ctx.ui.notify(
|
|
634
636
|
`${roleDisplay}\n` +
|
|
635
637
|
`branch: ${currentBranch ?? "not a git repo"}\n` +
|
|
638
|
+
`hat: v${version}\n` +
|
|
636
639
|
`${desc ? `desc: ${desc}\n` : ""}` +
|
|
637
640
|
`${defaultLine}` +
|
|
638
641
|
`roles: ${config.configFile}`,
|
|
@@ -965,12 +968,27 @@ If the user asks you to make changes, tell them to switch to an appropriate bran
|
|
|
965
968
|
|
|
966
969
|
// Planner NN sequence validation (architectural guard — always applies)
|
|
967
970
|
if (lowerRole === "planner" && path.startsWith("todo/")) {
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
971
|
+
const fullPath = resolve(ctx.cwd, path);
|
|
972
|
+
|
|
973
|
+
if (existsSync(fullPath)) {
|
|
974
|
+
// File exists — block editing if any items are already marked done (`- [x]`)
|
|
975
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
976
|
+
if (/^- \[x\]/im.test(content)) {
|
|
977
|
+
return {
|
|
978
|
+
block: true,
|
|
979
|
+
reason: `\uD83D\uDCCB Planner: cannot edit todo/${path.replace("todo/", "")} — it contains completed items (- [x]). Create a new todo file instead.`,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
// File exists with no completed items: editing is allowed.
|
|
983
|
+
} else {
|
|
984
|
+
// File does not exist — enforce NN > maxNN for new files.
|
|
985
|
+
const nn = extractNN(path.replace("todo/", ""));
|
|
986
|
+
if (nn !== null && nn <= (await findMaxNN(ctx.cwd))) {
|
|
987
|
+
return {
|
|
988
|
+
block: true,
|
|
989
|
+
reason: `\uD83D\uDCCB Planner: NN must be > max in todo/ and report/. You used ${String(nn).padStart(2, "0")}. Use ${String((await findMaxNN(ctx.cwd)) + 1).padStart(2, "0")} or higher.`,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
974
992
|
}
|
|
975
993
|
}
|
|
976
994
|
|
package/lib/role-file.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* instruction constants used as fallback when no .md file exists.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
9
10
|
import { readFile, readdir } from "node:fs/promises";
|
|
10
11
|
import { resolve, dirname } from "node:path";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
@@ -13,7 +14,43 @@ import { fileURLToPath } from "node:url";
|
|
|
13
14
|
import type { MergedConfig } from "./types.js";
|
|
14
15
|
|
|
15
16
|
/** Directory containing this extension file. Used to resolve bundled roles/ directory. */
|
|
16
|
-
export const EXTENSION_DIR = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
export const EXTENSION_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Determine the source of a role file without loading its content.
|
|
21
|
+
* Returns a human-readable string describing where the file comes from,
|
|
22
|
+
* or null if no source is found.
|
|
23
|
+
*
|
|
24
|
+
* Search order (same as loadRoleFile):
|
|
25
|
+
* 1. {cwd}/{fileDir}/{role}.md (project-local)
|
|
26
|
+
* 2. {extensionDir}/roles/{role}.md (extension-bundled)
|
|
27
|
+
* 3. built-in instructions (hardcoded in BUILTIN_INSTRUCTIONS)
|
|
28
|
+
*/
|
|
29
|
+
export function getRoleFileSource(
|
|
30
|
+
cwd: string,
|
|
31
|
+
role: string,
|
|
32
|
+
config: MergedConfig,
|
|
33
|
+
): string | null {
|
|
34
|
+
// 1. Project-local: {cwd}/{fileDir}/{role}.md
|
|
35
|
+
const roleDir = resolve(cwd, config.fileDir);
|
|
36
|
+
if (existsSync(resolve(roleDir, `${role}.md`))) {
|
|
37
|
+
return `${config.fileDir}/${role}.md`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Extension-bundled: {extensionDir}/roles/{role}.md
|
|
41
|
+
const bundledRoleDir = resolve(EXTENSION_DIR, "roles");
|
|
42
|
+
if (existsSync(resolve(bundledRoleDir, `${role}.md`))) {
|
|
43
|
+
return `roles/${role}.md (bundled)`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 3. Built-in instructions
|
|
47
|
+
const lower = role.toLowerCase();
|
|
48
|
+
if (BUILTIN_INSTRUCTIONS[lower]) {
|
|
49
|
+
return `built-in instructions`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
17
54
|
|
|
18
55
|
/**
|
|
19
56
|
* Load a role's .md file from the project's fileDir directory.
|
package/package.json
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@senomas/pi-git-hat",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Pi extension for role-based Git branch workflows — wear different hats by switching branches",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"keywords": [
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"git",
|
|
9
|
+
"workflow",
|
|
10
|
+
"branching",
|
|
11
|
+
"roles"
|
|
12
|
+
],
|
|
7
13
|
"license": "MIT",
|
|
8
14
|
"files": [
|
|
9
15
|
"git-hat.ts",
|
|
@@ -14,7 +20,9 @@
|
|
|
14
20
|
"README.md"
|
|
15
21
|
],
|
|
16
22
|
"pi": {
|
|
17
|
-
"extensions": [
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./git-hat.ts"
|
|
25
|
+
]
|
|
18
26
|
},
|
|
19
27
|
"peerDependencies": {
|
|
20
28
|
"@earendil-works/pi-coding-agent": "*",
|
package/roles/planner.md
CHANGED
|
@@ -22,5 +22,5 @@ You are **PLANNER**. Your sole responsibility is to research (just collecting da
|
|
|
22
22
|
- Blank lines separate items
|
|
23
23
|
|
|
24
24
|
## NN sequence rule
|
|
25
|
-
|
|
26
|
-
`todo
|
|
25
|
+
- When creating new `todo/NN-name.md` file, must use NN **higher** than the highest NN in both `todo/` **and** `report/`.
|
|
26
|
+
- You are allowed to edit/remove entry from `todo/NN-name/md` only if that entry is un-done
|