gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216
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/dist/bundled-resource-path.d.ts +8 -0
- package/dist/bundled-resource-path.js +14 -0
- package/dist/headless-query.js +6 -6
- package/dist/resources/extensions/gsd/auto/session.js +27 -32
- package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
- package/dist/resources/extensions/gsd/auto-loop.js +956 -0
- package/dist/resources/extensions/gsd/auto-observability.js +4 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
- package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
- package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
- package/dist/resources/extensions/gsd/auto-start.js +330 -309
- package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
- package/dist/resources/extensions/gsd/auto-timers.js +3 -4
- package/dist/resources/extensions/gsd/auto-verification.js +35 -73
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
- package/dist/resources/extensions/gsd/auto.js +283 -1013
- package/dist/resources/extensions/gsd/captures.js +10 -4
- package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
- package/dist/resources/extensions/gsd/git-service.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +296 -151
- package/dist/resources/extensions/gsd/index.js +92 -228
- package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
- package/dist/resources/extensions/gsd/progress-score.js +61 -156
- package/dist/resources/extensions/gsd/quick.js +98 -122
- package/dist/resources/extensions/gsd/session-lock.js +13 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/undo.js +43 -48
- package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
- package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
- package/dist/resources/extensions/gsd/verification-gate.js +6 -35
- package/dist/resources/extensions/gsd/worktree-command.js +30 -24
- package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
- package/dist/resources/extensions/gsd/worktree.js +7 -44
- package/dist/tool-bootstrap.js +59 -11
- package/dist/worktree-cli.js +7 -7
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +735 -2588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/src/models.generated.ts +1039 -2892
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +47 -30
- package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
- package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
- package/src/resources/extensions/gsd/auto-observability.ts +4 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
- package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
- package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
- package/src/resources/extensions/gsd/auto-start.ts +440 -354
- package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
- package/src/resources/extensions/gsd/auto-timers.ts +3 -4
- package/src/resources/extensions/gsd/auto-verification.ts +76 -90
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
- package/src/resources/extensions/gsd/auto.ts +515 -1199
- package/src/resources/extensions/gsd/captures.ts +10 -4
- package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
- package/src/resources/extensions/gsd/git-service.ts +8 -1
- package/src/resources/extensions/gsd/gitignore.ts +4 -2
- package/src/resources/extensions/gsd/gsd-db.ts +375 -180
- package/src/resources/extensions/gsd/index.ts +104 -263
- package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
- package/src/resources/extensions/gsd/progress-score.ts +65 -200
- package/src/resources/extensions/gsd/quick.ts +121 -125
- package/src/resources/extensions/gsd/session-lock.ts +11 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
- package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
- package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
- package/src/resources/extensions/gsd/types.ts +90 -81
- package/src/resources/extensions/gsd/undo.ts +42 -46
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
- package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
- package/src/resources/extensions/gsd/verification-gate.ts +6 -39
- package/src/resources/extensions/gsd/worktree-command.ts +36 -24
- package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
- package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
- package/src/resources/extensions/gsd/worktree.ts +7 -44
- package/dist/resources/extensions/gsd/auto-constants.js +0 -5
- package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
- package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
- package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
- package/src/resources/extensions/gsd/auto-constants.ts +0 -6
- package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
- package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
- package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
- package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
|
@@ -7,54 +7,52 @@ import { deriveState } from "./state.js";
|
|
|
7
7
|
import { invalidateAllCaches } from "./cache.js";
|
|
8
8
|
import { gsdRoot, resolveTasksDir, resolveSlicePath, buildTaskFileName } from "./paths.js";
|
|
9
9
|
import { sendDesktopNotification } from "./notifications.js";
|
|
10
|
-
import { parseUnitId } from "./unit-id.js";
|
|
11
10
|
/**
|
|
12
|
-
* Undo the last completed unit: revert git commits,
|
|
11
|
+
* Undo the last completed unit: revert git commits,
|
|
13
12
|
* delete summary artifacts, and uncheck the task in PLAN.
|
|
13
|
+
* deriveState() handles re-derivation after revert.
|
|
14
14
|
*/
|
|
15
15
|
export async function handleUndo(args, ctx, _pi, basePath) {
|
|
16
16
|
const force = args.includes("--force");
|
|
17
|
-
//
|
|
18
|
-
const
|
|
19
|
-
if (!existsSync(
|
|
20
|
-
ctx.ui.notify("Nothing to undo — no
|
|
17
|
+
// Find the last GSD-related commit from git activity logs
|
|
18
|
+
const activityDir = join(gsdRoot(basePath), "activity");
|
|
19
|
+
if (!existsSync(activityDir)) {
|
|
20
|
+
ctx.ui.notify("Nothing to undo — no activity logs found.", "info");
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
// Parse activity logs to find the most recent unit
|
|
24
|
+
const files = readdirSync(activityDir)
|
|
25
|
+
.filter(f => f.endsWith(".jsonl"))
|
|
26
|
+
.sort()
|
|
27
|
+
.reverse();
|
|
28
|
+
if (files.length === 0) {
|
|
29
|
+
ctx.ui.notify("Nothing to undo — no activity logs found.", "info");
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
// Extract unit type and ID from the most recent activity log filename
|
|
33
|
+
// Format: <seq>-<unitType>-<unitId>.jsonl
|
|
34
|
+
const match = files[0].match(/^\d+-(.+?)-(.+)\.jsonl$/);
|
|
35
|
+
if (!match) {
|
|
36
|
+
ctx.ui.notify("Nothing to undo — could not parse latest activity log.", "warning");
|
|
33
37
|
return;
|
|
34
38
|
}
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const sepIdx = lastKey.indexOf("/");
|
|
38
|
-
const unitType = sepIdx >= 0 ? lastKey.slice(0, sepIdx) : lastKey;
|
|
39
|
-
const unitId = sepIdx >= 0 ? lastKey.slice(sepIdx + 1) : lastKey;
|
|
39
|
+
const unitType = match[1];
|
|
40
|
+
const unitId = match[2].replace(/-/g, "/");
|
|
40
41
|
if (!force) {
|
|
41
42
|
ctx.ui.notify(`Will undo: ${unitType} (${unitId})\n` +
|
|
42
43
|
`This will:\n` +
|
|
43
|
-
` - Remove from completed-units.json\n` +
|
|
44
44
|
` - Delete summary artifacts\n` +
|
|
45
45
|
` - Uncheck task in PLAN (if execute-task)\n` +
|
|
46
46
|
` - Attempt to revert associated git commits\n\n` +
|
|
47
47
|
`Run /gsd undo --force to confirm.`, "warning");
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
writeFileSync(completedKeysFile, JSON.stringify(keys), "utf-8");
|
|
53
|
-
// 3. Delete summary artifact
|
|
54
|
-
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
50
|
+
// 1. Delete summary artifact
|
|
51
|
+
const parts = unitId.split("/");
|
|
55
52
|
let summaryRemoved = false;
|
|
56
|
-
if (
|
|
53
|
+
if (parts.length === 3) {
|
|
57
54
|
// Task-level: M001/S01/T01
|
|
55
|
+
const [mid, sid, tid] = parts;
|
|
58
56
|
const tasksDir = resolveTasksDir(basePath, mid, sid);
|
|
59
57
|
if (tasksDir) {
|
|
60
58
|
const summaryFile = join(tasksDir, buildTaskFileName(tid, "SUMMARY"));
|
|
@@ -64,11 +62,11 @@ export async function handleUndo(args, ctx, _pi, basePath) {
|
|
|
64
62
|
}
|
|
65
63
|
}
|
|
66
64
|
}
|
|
67
|
-
else if (
|
|
65
|
+
else if (parts.length === 2) {
|
|
68
66
|
// Slice-level: M001/S01
|
|
67
|
+
const [mid, sid] = parts;
|
|
69
68
|
const slicePath = resolveSlicePath(basePath, mid, sid);
|
|
70
69
|
if (slicePath) {
|
|
71
|
-
// Try common summary filenames
|
|
72
70
|
for (const suffix of ["SUMMARY", "COMPLETE"]) {
|
|
73
71
|
const candidates = findFileWithPrefix(slicePath, sid, suffix);
|
|
74
72
|
for (const f of candidates) {
|
|
@@ -78,43 +76,40 @@ export async function handleUndo(args, ctx, _pi, basePath) {
|
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
78
|
}
|
|
81
|
-
//
|
|
79
|
+
// 2. Uncheck task in PLAN if execute-task
|
|
82
80
|
let planUpdated = false;
|
|
83
|
-
if (unitType === "execute-task" &&
|
|
81
|
+
if (unitType === "execute-task" && parts.length === 3) {
|
|
82
|
+
const [mid, sid, tid] = parts;
|
|
84
83
|
planUpdated = uncheckTaskInPlan(basePath, mid, sid, tid);
|
|
85
84
|
}
|
|
86
|
-
//
|
|
85
|
+
// 3. Try to revert git commits from activity log
|
|
87
86
|
let commitsReverted = 0;
|
|
88
|
-
const activityDir = join(gsdRoot(basePath), "activity");
|
|
89
87
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
const commits = findCommitsForUnit(activityDir, unitType, unitId);
|
|
89
|
+
if (commits.length > 0) {
|
|
90
|
+
for (const sha of commits.reverse()) {
|
|
91
|
+
try {
|
|
92
|
+
nativeRevertCommit(basePath, sha);
|
|
93
|
+
commitsReverted++;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Revert conflict or already reverted — skip
|
|
94
97
|
try {
|
|
95
|
-
|
|
96
|
-
commitsReverted++;
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
// Revert conflict or already reverted — skip
|
|
100
|
-
try {
|
|
101
|
-
nativeRevertAbort(basePath);
|
|
102
|
-
}
|
|
103
|
-
catch { /* no-op */ }
|
|
104
|
-
break;
|
|
98
|
+
nativeRevertAbort(basePath);
|
|
105
99
|
}
|
|
100
|
+
catch { /* no-op */ }
|
|
101
|
+
break;
|
|
106
102
|
}
|
|
107
103
|
}
|
|
108
104
|
}
|
|
109
105
|
}
|
|
110
106
|
finally {
|
|
111
|
-
//
|
|
107
|
+
// 4. Re-derive state — always invalidate caches even if git operations fail
|
|
112
108
|
invalidateAllCaches();
|
|
113
109
|
await deriveState(basePath);
|
|
114
110
|
}
|
|
115
111
|
// Build result message
|
|
116
112
|
const results = [`Undone: ${unitType} (${unitId})`];
|
|
117
|
-
results.push(` - Removed from completed-units.json`);
|
|
118
113
|
if (summaryRemoved)
|
|
119
114
|
results.push(` - Deleted summary artifact`);
|
|
120
115
|
if (planUpdated)
|
|
@@ -1,25 +1,18 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { gsdRoot, relSliceFile, relTaskFile, resolveSliceFile, resolveTaskFile, } from "./paths.js";
|
|
4
4
|
import { loadFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
|
|
5
|
-
import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
|
|
6
|
-
import { parseUnitId } from "./unit-id.js";
|
|
7
|
-
function isAutoUnitRuntimeRecord(data) {
|
|
8
|
-
return (typeof data === "object" &&
|
|
9
|
-
data !== null &&
|
|
10
|
-
data.version === 1 &&
|
|
11
|
-
typeof data.unitType === "string" &&
|
|
12
|
-
typeof data.unitId === "string");
|
|
13
|
-
}
|
|
14
5
|
function runtimeDir(basePath) {
|
|
15
6
|
return join(gsdRoot(basePath), "runtime", "units");
|
|
16
7
|
}
|
|
17
8
|
function runtimePath(basePath, unitType, unitId) {
|
|
18
|
-
const sanitizedUnitType = unitType.replace(/[
|
|
19
|
-
const sanitizedUnitId = unitId.replace(/[
|
|
9
|
+
const sanitizedUnitType = unitType.replace(/[\/]/g, "-");
|
|
10
|
+
const sanitizedUnitId = unitId.replace(/[\/]/g, "-");
|
|
20
11
|
return join(runtimeDir(basePath), `${sanitizedUnitType}-${sanitizedUnitId}.json`);
|
|
21
12
|
}
|
|
22
13
|
export function writeUnitRuntimeRecord(basePath, unitType, unitId, startedAt, updates = {}) {
|
|
14
|
+
const dir = runtimeDir(basePath);
|
|
15
|
+
mkdirSync(dir, { recursive: true });
|
|
23
16
|
const path = runtimePath(basePath, unitType, unitId);
|
|
24
17
|
const prev = readUnitRuntimeRecord(basePath, unitType, unitId);
|
|
25
18
|
const next = {
|
|
@@ -39,11 +32,19 @@ export function writeUnitRuntimeRecord(basePath, unitType, unitId, startedAt, up
|
|
|
39
32
|
recoveryAttempts: updates.recoveryAttempts ?? prev?.recoveryAttempts ?? 0,
|
|
40
33
|
lastRecoveryReason: updates.lastRecoveryReason ?? prev?.lastRecoveryReason,
|
|
41
34
|
};
|
|
42
|
-
|
|
35
|
+
writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
43
36
|
return next;
|
|
44
37
|
}
|
|
45
38
|
export function readUnitRuntimeRecord(basePath, unitType, unitId) {
|
|
46
|
-
|
|
39
|
+
const path = runtimePath(basePath, unitType, unitId);
|
|
40
|
+
if (!existsSync(path))
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
47
48
|
}
|
|
48
49
|
export function clearUnitRuntimeRecord(basePath, unitType, unitId) {
|
|
49
50
|
const path = runtimePath(basePath, unitType, unitId);
|
|
@@ -74,7 +75,7 @@ export function listUnitRuntimeRecords(basePath) {
|
|
|
74
75
|
return results;
|
|
75
76
|
}
|
|
76
77
|
export async function inspectExecuteTaskDurability(basePath, unitId) {
|
|
77
|
-
const
|
|
78
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
78
79
|
if (!mid || !sid || !tid)
|
|
79
80
|
return null;
|
|
80
81
|
const planAbs = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
@@ -31,7 +31,6 @@ export function writeVerificationJSON(result, tasksDir, taskId, unitId, retryAtt
|
|
|
31
31
|
exitCode: check.exitCode,
|
|
32
32
|
durationMs: check.durationMs,
|
|
33
33
|
verdict: check.exitCode === 0 ? "pass" : "fail",
|
|
34
|
-
blocking: check.blocking,
|
|
35
34
|
})),
|
|
36
35
|
...(retryAttempt !== undefined ? { retryAttempt } : {}),
|
|
37
36
|
...(maxRetries !== undefined ? { maxRetries } : {}),
|
|
@@ -28,12 +28,11 @@ const PACKAGE_SCRIPT_KEYS = ["typecheck", "lint", "test"];
|
|
|
28
28
|
* 4. None found
|
|
29
29
|
*/
|
|
30
30
|
export function discoverCommands(options) {
|
|
31
|
-
// 1. Preference commands
|
|
31
|
+
// 1. Preference commands
|
|
32
32
|
if (options.preferenceCommands && options.preferenceCommands.length > 0) {
|
|
33
33
|
const filtered = options.preferenceCommands
|
|
34
34
|
.map(c => c.trim())
|
|
35
|
-
.filter(Boolean)
|
|
36
|
-
.filter(c => isLikelyCommand(c));
|
|
35
|
+
.filter(Boolean);
|
|
37
36
|
if (filtered.length > 0) {
|
|
38
37
|
return { commands: filtered, source: "preference" };
|
|
39
38
|
}
|
|
@@ -89,9 +88,7 @@ const MAX_FAILURE_CONTEXT_CHARS = 10_000;
|
|
|
89
88
|
* Returns an empty string when all checks pass or the checks array is empty.
|
|
90
89
|
*/
|
|
91
90
|
export function formatFailureContext(result) {
|
|
92
|
-
|
|
93
|
-
// should not be injected into retry prompts to avoid noise pollution.
|
|
94
|
-
const failures = result.checks.filter((c) => c.exitCode !== 0 && c.blocking);
|
|
91
|
+
const failures = result.checks.filter((c) => c.exitCode !== 0);
|
|
95
92
|
if (failures.length === 0)
|
|
96
93
|
return "";
|
|
97
94
|
const blocks = [];
|
|
@@ -187,19 +184,13 @@ function sanitizeCommand(cmd) {
|
|
|
187
184
|
return null;
|
|
188
185
|
return cmd;
|
|
189
186
|
}
|
|
190
|
-
/** Error codes from spawnSync that indicate infrastructure/OS-level failures
|
|
191
|
-
* rather than the command itself failing. These are transient — the agent
|
|
192
|
-
* cannot fix them, so they should not trigger auto-fix retries. */
|
|
193
|
-
const INFRA_ERROR_CODES = new Set(["ETIMEDOUT", "ENOENT", "ENOMEM", "EMFILE", "ENFILE", "EAGAIN"]);
|
|
194
187
|
/**
|
|
195
188
|
* Run the verification gate: discover commands, execute each via spawnSync,
|
|
196
189
|
* and return a structured result.
|
|
197
190
|
*
|
|
198
191
|
* - All commands run sequentially regardless of individual pass/fail.
|
|
199
|
-
* - `passed` is true when every
|
|
192
|
+
* - `passed` is true when every command exits 0 (or no commands are discovered).
|
|
200
193
|
* - stdout/stderr per command are truncated to 10 KB.
|
|
201
|
-
* - Spawn/infra errors (ETIMEDOUT, ENOENT, etc.) are tagged with `infraError: true`
|
|
202
|
-
* so the retry logic can distinguish "the OS couldn't run this" from "the tests failed".
|
|
203
194
|
*/
|
|
204
195
|
export function runVerificationGate(options) {
|
|
205
196
|
const timestamp = Date.now();
|
|
@@ -216,9 +207,6 @@ export function runVerificationGate(options) {
|
|
|
216
207
|
timestamp,
|
|
217
208
|
};
|
|
218
209
|
}
|
|
219
|
-
// Commands from preference and task-plan sources are blocking;
|
|
220
|
-
// package-json discovered commands are advisory (non-blocking).
|
|
221
|
-
const blocking = source === "preference" || source === "task-plan";
|
|
222
210
|
const checks = [];
|
|
223
211
|
for (const command of commands) {
|
|
224
212
|
const start = Date.now();
|
|
@@ -233,22 +221,9 @@ export function runVerificationGate(options) {
|
|
|
233
221
|
let exitCode;
|
|
234
222
|
let stderr;
|
|
235
223
|
if (result.error) {
|
|
236
|
-
//
|
|
237
|
-
// Tag with infraError so the retry logic can skip auto-fix attempts.
|
|
238
|
-
const errCode = result.error.code;
|
|
239
|
-
const isInfra = !!errCode && INFRA_ERROR_CODES.has(errCode);
|
|
224
|
+
// Command not found or spawn failure
|
|
240
225
|
exitCode = 127;
|
|
241
226
|
stderr = truncate((result.stderr || "") + "\n" + result.error.message, MAX_OUTPUT_BYTES);
|
|
242
|
-
checks.push({
|
|
243
|
-
command,
|
|
244
|
-
exitCode,
|
|
245
|
-
stdout: truncate(result.stdout, MAX_OUTPUT_BYTES),
|
|
246
|
-
stderr,
|
|
247
|
-
durationMs,
|
|
248
|
-
blocking,
|
|
249
|
-
...(isInfra ? { infraError: true } : {}),
|
|
250
|
-
});
|
|
251
|
-
continue;
|
|
252
227
|
}
|
|
253
228
|
else {
|
|
254
229
|
// status is null when killed by signal — treat as failure
|
|
@@ -261,14 +236,10 @@ export function runVerificationGate(options) {
|
|
|
261
236
|
stdout: truncate(result.stdout, MAX_OUTPUT_BYTES),
|
|
262
237
|
stderr,
|
|
263
238
|
durationMs,
|
|
264
|
-
blocking,
|
|
265
239
|
});
|
|
266
240
|
}
|
|
267
|
-
// Gate passes if all blocking checks pass (non-blocking failures are advisory)
|
|
268
|
-
const blockingChecks = checks.filter(c => c.blocking);
|
|
269
|
-
const passed = blockingChecks.length === 0 || blockingChecks.every(c => c.exitCode === 0);
|
|
270
241
|
return {
|
|
271
|
-
passed,
|
|
242
|
+
passed: checks.every(c => c.exitCode === 0),
|
|
272
243
|
checks,
|
|
273
244
|
discoverySource: source,
|
|
274
245
|
timestamp,
|
|
@@ -19,34 +19,21 @@ import { inferCommitType } from "./git-service.js";
|
|
|
19
19
|
import { existsSync, realpathSync, readdirSync, rmSync, unlinkSync } from "node:fs";
|
|
20
20
|
import { nativeMergeAbort } from "./native-git-bridge.js";
|
|
21
21
|
import { join, sep } from "node:path";
|
|
22
|
-
import { getErrorMessage } from "./error-utils.js";
|
|
23
22
|
/**
|
|
24
23
|
* Tracks the original project root so we can switch back.
|
|
25
24
|
* Set when we first chdir into a worktree, cleared on return.
|
|
26
25
|
*/
|
|
27
26
|
let originalCwd = null;
|
|
28
|
-
function ensureWorktreeStateInitialized() {
|
|
29
|
-
if (originalCwd)
|
|
30
|
-
return;
|
|
31
|
-
const cwd = process.cwd();
|
|
32
|
-
const marker = `${sep}.gsd${sep}worktrees${sep}`;
|
|
33
|
-
const markerIdx = cwd.indexOf(marker);
|
|
34
|
-
if (markerIdx !== -1) {
|
|
35
|
-
originalCwd = cwd.slice(0, markerIdx);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
27
|
/** Get the original project root if currently in a worktree, or null. */
|
|
39
28
|
export function getWorktreeOriginalCwd() {
|
|
40
|
-
ensureWorktreeStateInitialized();
|
|
41
29
|
return originalCwd;
|
|
42
30
|
}
|
|
43
31
|
/** Get the name of the active worktree, or null if not in one. */
|
|
44
32
|
export function getActiveWorktreeName() {
|
|
45
|
-
ensureWorktreeStateInitialized();
|
|
46
33
|
if (!originalCwd)
|
|
47
34
|
return null;
|
|
48
35
|
const cwd = process.cwd();
|
|
49
|
-
const wtDir = join(
|
|
36
|
+
const wtDir = join(originalCwd, ".gsd", "worktrees");
|
|
50
37
|
if (!cwd.startsWith(wtDir))
|
|
51
38
|
return null;
|
|
52
39
|
const rel = cwd.slice(wtDir.length + 1);
|
|
@@ -94,8 +81,7 @@ function worktreeCompletions(prefix) {
|
|
|
94
81
|
}
|
|
95
82
|
return [];
|
|
96
83
|
}
|
|
97
|
-
|
|
98
|
-
ensureWorktreeStateInitialized();
|
|
84
|
+
async function worktreeHandler(args, ctx, pi, alias) {
|
|
99
85
|
const trimmed = (typeof args === "string" ? args : "").trim();
|
|
100
86
|
const basePath = process.cwd();
|
|
101
87
|
if (trimmed === "") {
|
|
@@ -199,11 +185,21 @@ export async function handleWorktreeCommand(args, ctx, pi, alias) {
|
|
|
199
185
|
await handleCreate(basePath, nameOnly, ctx);
|
|
200
186
|
}
|
|
201
187
|
}
|
|
188
|
+
export async function handleWorktreeCommand(args, ctx, pi, alias) {
|
|
189
|
+
await worktreeHandler(args, ctx, pi, alias);
|
|
190
|
+
}
|
|
202
191
|
export function registerWorktreeCommand(pi) {
|
|
203
192
|
// Restore worktree state after /reload.
|
|
204
193
|
// The module-level originalCwd resets to null when extensions are re-loaded,
|
|
205
194
|
// but process.cwd() is still inside the worktree. Detect this and recover.
|
|
206
|
-
|
|
195
|
+
if (!originalCwd) {
|
|
196
|
+
const cwd = process.cwd();
|
|
197
|
+
const marker = `${sep}.gsd${sep}worktrees${sep}`;
|
|
198
|
+
const markerIdx = cwd.indexOf(marker);
|
|
199
|
+
if (markerIdx !== -1) {
|
|
200
|
+
originalCwd = cwd.slice(0, markerIdx);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
207
203
|
pi.registerCommand("worktree", {
|
|
208
204
|
description: "Git worktrees (also /wt): /worktree <name> | list | merge | remove",
|
|
209
205
|
getArgumentCompletions: worktreeCompletions,
|
|
@@ -317,7 +313,7 @@ async function handleCreate(basePath, name, ctx) {
|
|
|
317
313
|
].filter(Boolean).join("\n"), "info");
|
|
318
314
|
}
|
|
319
315
|
catch (error) {
|
|
320
|
-
const msg =
|
|
316
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
321
317
|
ctx.ui.notify(`Failed to create worktree: ${msg}`, "error");
|
|
322
318
|
}
|
|
323
319
|
}
|
|
@@ -351,7 +347,7 @@ async function handleSwitch(basePath, name, ctx) {
|
|
|
351
347
|
].filter(Boolean).join("\n"), "info");
|
|
352
348
|
}
|
|
353
349
|
catch (error) {
|
|
354
|
-
const msg =
|
|
350
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
355
351
|
ctx.ui.notify(`Failed to switch to worktree: ${msg}`, "error");
|
|
356
352
|
}
|
|
357
353
|
}
|
|
@@ -442,7 +438,7 @@ async function handleList(basePath, ctx) {
|
|
|
442
438
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
443
439
|
}
|
|
444
440
|
catch (error) {
|
|
445
|
-
const msg =
|
|
441
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
446
442
|
ctx.ui.notify(`Failed to list worktrees: ${msg}`, "error");
|
|
447
443
|
}
|
|
448
444
|
}
|
|
@@ -533,6 +529,16 @@ async function handleMerge(basePath, name, ctx, pi, targetBranch) {
|
|
|
533
529
|
// Try a direct squash-merge first. Only fall back to LLM on conflict.
|
|
534
530
|
const commitType = inferCommitType(name);
|
|
535
531
|
const commitMessage = `${commitType}(${name}): merge worktree ${name}`;
|
|
532
|
+
// Reconcile worktree DB into main DB before squash merge
|
|
533
|
+
const wtDbPath = join(worktreePath(basePath, name), ".gsd", "gsd.db");
|
|
534
|
+
const mainDbPath = join(basePath, ".gsd", "gsd.db");
|
|
535
|
+
if (existsSync(wtDbPath) && existsSync(mainDbPath)) {
|
|
536
|
+
try {
|
|
537
|
+
const { reconcileWorktreeDb } = await import("./gsd-db.js");
|
|
538
|
+
reconcileWorktreeDb(mainDbPath, wtDbPath);
|
|
539
|
+
}
|
|
540
|
+
catch { /* non-fatal */ }
|
|
541
|
+
}
|
|
536
542
|
try {
|
|
537
543
|
mergeWorktreeToMain(basePath, name, commitMessage);
|
|
538
544
|
ctx.ui.notify([
|
|
@@ -544,7 +550,7 @@ async function handleMerge(basePath, name, ctx, pi, targetBranch) {
|
|
|
544
550
|
return;
|
|
545
551
|
}
|
|
546
552
|
catch (mergeErr) {
|
|
547
|
-
const mergeMsg =
|
|
553
|
+
const mergeMsg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
548
554
|
const isConflict = /conflict/i.test(mergeMsg);
|
|
549
555
|
if (isConflict) {
|
|
550
556
|
// Abort the failed merge so the working tree is clean for LLM retry
|
|
@@ -588,7 +594,7 @@ async function handleMerge(basePath, name, ctx, pi, targetBranch) {
|
|
|
588
594
|
ctx.ui.notify(`${CLR.ok("✓")} Merge helper started for ${CLR.name(name)} ${CLR.muted(`(${codeChanges} code + ${gsdChanges} GSD artifact change${totalChanges === 1 ? "" : "s"})`)}`, "info");
|
|
589
595
|
}
|
|
590
596
|
catch (error) {
|
|
591
|
-
const msg =
|
|
597
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
592
598
|
ctx.ui.notify(`Failed to start merge: ${msg}`, "error");
|
|
593
599
|
}
|
|
594
600
|
}
|
|
@@ -622,7 +628,7 @@ async function handleRemove(basePath, name, ctx) {
|
|
|
622
628
|
ctx.ui.notify(`${CLR.ok("✓")} Worktree ${CLR.name(name)} removed ${CLR.muted("(branch deleted)")}.`, "info");
|
|
623
629
|
}
|
|
624
630
|
catch (error) {
|
|
625
|
-
const msg =
|
|
631
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
626
632
|
ctx.ui.notify(`Failed to remove worktree: ${msg}`, "error");
|
|
627
633
|
}
|
|
628
634
|
}
|
|
@@ -670,7 +676,7 @@ async function handleRemoveAll(basePath, ctx) {
|
|
|
670
676
|
ctx.ui.notify(lines.join("\n"), failed.length > 0 ? "warning" : "info");
|
|
671
677
|
}
|
|
672
678
|
catch (error) {
|
|
673
|
-
const msg =
|
|
679
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
674
680
|
ctx.ui.notify(`Failed to remove worktrees: ${msg}`, "error");
|
|
675
681
|
}
|
|
676
682
|
}
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
18
18
|
import { join, resolve, sep } from "node:path";
|
|
19
|
-
import { gsdRoot } from "./paths.js";
|
|
20
19
|
import { GSDError, GSD_PARSE_ERROR, GSD_STALE_STATE, GSD_LOCK_HELD, GSD_GIT_ERROR, GSD_MERGE_CONFLICT } from "./errors.js";
|
|
21
20
|
import { nativeBranchDelete, nativeBranchExists, nativeBranchForceReset, nativeCommit, nativeDetectMainBranch, nativeDiffContent, nativeDiffNameStatus, nativeDiffNumstat, nativeGetCurrentBranch, nativeLogOneline, nativeMergeSquash, nativeWorktreeAdd, nativeWorktreeList, nativeWorktreePrune, nativeWorktreeRemove, } from "./native-git-bridge.js";
|
|
22
21
|
// ─── Path Helpers ──────────────────────────────────────────────────────────
|
|
@@ -56,7 +55,7 @@ export function resolveGitDir(basePath) {
|
|
|
56
55
|
return join(basePath, ".git");
|
|
57
56
|
}
|
|
58
57
|
export function worktreesDir(basePath) {
|
|
59
|
-
return join(
|
|
58
|
+
return join(basePath, ".gsd", "worktrees");
|
|
60
59
|
}
|
|
61
60
|
export function worktreePath(basePath, name) {
|
|
62
61
|
return join(worktreesDir(basePath), name);
|
|
@@ -133,7 +132,7 @@ export function listWorktrees(basePath) {
|
|
|
133
132
|
const seenRoots = new Set();
|
|
134
133
|
const worktreeRoots = baseVariants
|
|
135
134
|
.map(baseVariant => {
|
|
136
|
-
const path = join(
|
|
135
|
+
const path = join(baseVariant, ".gsd", "worktrees");
|
|
137
136
|
return {
|
|
138
137
|
normalized: normalizePathForComparison(path),
|
|
139
138
|
};
|