gsd-pi 2.3.7 → 2.3.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.3.7",
3
+ "version": "2.3.8",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -60,6 +60,8 @@ import { execSync } from "node:child_process";
60
60
  import {
61
61
  autoCommitCurrentBranch,
62
62
  ensureSliceBranch,
63
+ getCurrentBranch,
64
+ getSliceBranchName,
63
65
  switchToMain,
64
66
  mergeSliceToMain,
65
67
  } from "./worktree.ts";
@@ -800,39 +802,53 @@ async function dispatchNextUnit(
800
802
  return;
801
803
  }
802
804
 
803
- // ── Post-completion merge: merge the slice branch after complete-slice finishes ──
804
- // The complete-slice unit writes the summary, UAT, marks roadmap [x], and commits.
805
- // Now we switch to main and squash-merge the slice branch.
806
- if (currentUnit?.type === "complete-slice") {
807
- try {
808
- const [completedMid, completedSid] = currentUnit.id.split("/");
809
- // Look up actual slice title from roadmap (on current branch, before switching)
810
- const roadmapFile = resolveMilestoneFile(basePath, completedMid!, "ROADMAP");
805
+ // ── General merge guard: merge completed slice branches before advancing ──
806
+ // If we're on a gsd/MID/SID branch and that slice is done (roadmap [x]),
807
+ // merge to main before dispatching the next unit. This handles:
808
+ // - Normal complete-slice → merge → reassess flow
809
+ // - LLM writes summary during task execution, skipping complete-slice
810
+ // - Doctor post-hook marks everything done, skipping complete-slice
811
+ // - complete-milestone runs on a slice branch (last slice bypass)
812
+ {
813
+ const currentBranch = getCurrentBranch(basePath);
814
+ const branchMatch = currentBranch.match(/^gsd\/(M\d+)\/(S\d+)$/);
815
+ if (branchMatch) {
816
+ const branchMid = branchMatch[1]!;
817
+ const branchSid = branchMatch[2]!;
818
+ // Check if this slice is marked done in the roadmap
819
+ const roadmapFile = resolveMilestoneFile(basePath, branchMid, "ROADMAP");
811
820
  const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
812
- let sliceTitleForMerge = completedSid!;
813
821
  if (roadmapContent) {
814
822
  const roadmap = parseRoadmap(roadmapContent);
815
- const sliceEntry = roadmap.slices.find(s => s.id === completedSid);
816
- if (sliceEntry) sliceTitleForMerge = sliceEntry.title;
823
+ const sliceEntry = roadmap.slices.find(s => s.id === branchSid);
824
+ if (sliceEntry?.done) {
825
+ try {
826
+ const sliceTitleForMerge = sliceEntry.title || branchSid;
827
+ switchToMain(basePath);
828
+ const mergeResult = mergeSliceToMain(
829
+ basePath, branchMid, branchSid, sliceTitleForMerge,
830
+ );
831
+ ctx.ui.notify(
832
+ `Merged ${mergeResult.branch} → main.`,
833
+ "info",
834
+ );
835
+ // Re-derive state from main so downstream logic sees merged state
836
+ state = await deriveState(basePath);
837
+ mid = state.activeMilestone?.id;
838
+ midTitle = state.activeMilestone?.title;
839
+ } catch (error) {
840
+ const message = error instanceof Error ? error.message : String(error);
841
+ ctx.ui.notify(
842
+ `Slice merge failed: ${message}`,
843
+ "error",
844
+ );
845
+ // Re-derive state so dispatch can figure out what to do
846
+ state = await deriveState(basePath);
847
+ mid = state.activeMilestone?.id;
848
+ midTitle = state.activeMilestone?.title;
849
+ }
850
+ }
817
851
  }
818
- switchToMain(basePath);
819
- const mergeResult = mergeSliceToMain(
820
- basePath, completedMid!, completedSid!, sliceTitleForMerge,
821
- );
822
- ctx.ui.notify(
823
- `Merged ${mergeResult.branch} → main.`,
824
- "info",
825
- );
826
- } catch (error) {
827
- const message = error instanceof Error ? error.message : String(error);
828
- ctx.ui.notify(
829
- `Slice merge failed: ${message}`,
830
- "error",
831
- );
832
- // Re-derive state so dispatch can figure out what to do
833
- state = await deriveState(basePath);
834
- mid = state.activeMilestone?.id;
835
- midTitle = state.activeMilestone?.title;
836
852
  }
837
853
  }
838
854
 
@@ -22,7 +22,7 @@ import type {
22
22
  ExtensionAPI,
23
23
  ExtensionContext,
24
24
  } from "@mariozechner/pi-coding-agent";
25
- import { createBashTool } from "@mariozechner/pi-coding-agent";
25
+ import { createBashTool, createWriteTool, createReadTool, createEditTool } from "@mariozechner/pi-coding-agent";
26
26
 
27
27
  import { registerGSDCommand } from "./commands.js";
28
28
  import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
@@ -102,6 +102,59 @@ export default function (pi: ExtensionAPI) {
102
102
  };
103
103
  pi.registerTool(dynamicBash as any);
104
104
 
105
+ // ── Dynamic-cwd file tools (write, read, edit) ────────────────────────
106
+ // The built-in file tools capture cwd at startup. When process.chdir()
107
+ // moves us into a worktree, relative paths still resolve against the
108
+ // original launch directory. These replacements delegate to freshly-
109
+ // created tools on each call so that process.cwd() is read dynamically.
110
+ const baseWrite = createWriteTool(process.cwd());
111
+ const dynamicWrite = {
112
+ ...baseWrite,
113
+ execute: async (
114
+ toolCallId: string,
115
+ params: { path: string; content: string },
116
+ signal?: AbortSignal,
117
+ onUpdate?: any,
118
+ ctx?: any,
119
+ ) => {
120
+ const fresh = createWriteTool(process.cwd());
121
+ return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
122
+ },
123
+ };
124
+ pi.registerTool(dynamicWrite as any);
125
+
126
+ const baseRead = createReadTool(process.cwd());
127
+ const dynamicRead = {
128
+ ...baseRead,
129
+ execute: async (
130
+ toolCallId: string,
131
+ params: { path: string; offset?: number; limit?: number },
132
+ signal?: AbortSignal,
133
+ onUpdate?: any,
134
+ ctx?: any,
135
+ ) => {
136
+ const fresh = createReadTool(process.cwd());
137
+ return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
138
+ },
139
+ };
140
+ pi.registerTool(dynamicRead as any);
141
+
142
+ const baseEdit = createEditTool(process.cwd());
143
+ const dynamicEdit = {
144
+ ...baseEdit,
145
+ execute: async (
146
+ toolCallId: string,
147
+ params: { path: string; oldText: string; newText: string },
148
+ signal?: AbortSignal,
149
+ onUpdate?: any,
150
+ ctx?: any,
151
+ ) => {
152
+ const fresh = createEditTool(process.cwd());
153
+ return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
154
+ },
155
+ };
156
+ pi.registerTool(dynamicEdit as any);
157
+
105
158
  // ── session_start: render branded GSD header + remote channel status ──
106
159
  pi.on("session_start", async (_event, ctx) => {
107
160
  const theme = ctx.ui.theme;