@senomas/pi-git-hat 0.2.7 → 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 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 roleFile = currentRole ? await loadRoleFile(ctx.cwd, currentRole, config) : null;
158
- const usingFile = roleFile ? ` | using ${config.fileDir}/${currentRole}.md` : "";
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}${usingFile}`
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
- // Filter to role-matched branches, excluding current
402
+ // All branches except current
403
403
  const candidates: string[] = [];
404
404
  for (const b of allBranches) {
405
405
  if (b === currentBranch) continue;
406
- if (detectRole(b, config)) candidates.push(b);
406
+ candidates.push(b);
407
407
  }
408
408
 
409
- // Edge case: no other role-matched branches
409
+ // Edge case: no other branches
410
410
  if (candidates.length === 0) {
411
- ctx.ui.notify("No other role-matched branches to rebase onto.", "info");
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("Could not determine latest commits for candidates.", "error");
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 roleFile = currentRole
620
- ? await loadRoleFile(ctx.cwd, currentRole, config)
621
+ const roleSource = currentRole
622
+ ? getRoleFileSource(ctx.cwd, currentRole, config)
621
623
  : null;
622
- const usingFile = roleFile ? ` | using ${config.fileDir}/${currentRole}.md` : "";
624
+ const sourceStr = roleSource ? ` | ${roleSource}` : " | (none)";
623
625
 
624
626
  const roleDisplay = currentRole
625
- ? `${roleIcon(currentRole)} ${currentRole}${usingFile}`
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 nn = extractNN(path.replace("todo/", ""));
969
- if (nn !== null && nn <= (await findMaxNN(ctx.cwd))) {
970
- return {
971
- block: true,
972
- 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.`,
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";
@@ -15,6 +16,42 @@ import type { MergedConfig } from "./types.js";
15
16
  /** Directory containing this extension file. Used to resolve bundled roles/ directory. */
16
17
  export const EXTENSION_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
17
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
+ }
54
+
18
55
  /**
19
56
  * Load a role's .md file from the project's fileDir directory.
20
57
  * Case-insensitive lookup if config.caseInsensitive is true.
package/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "@senomas/pi-git-hat",
3
- "version": "0.2.7",
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": ["pi-package", "git", "workflow", "branching", "roles"],
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": ["./git-hat.ts"]
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
- Every `todo/NN-name.md` must use NN **higher** than the highest NN in both
26
- `todo/` **and** `report/`.
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