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
|
@@ -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
|
-
// ──
|
|
804
|
-
//
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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 ===
|
|
816
|
-
if (sliceEntry)
|
|
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;
|