@opengsd/gsd-pi 1.4.0-dev.5fc22c6f → 1.4.0-dev.c18009cd
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/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +58 -4
- package/dist/resources/extensions/gsd/artifact-verification.js +17 -5
- package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
- package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
- package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
- package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
- package/dist/resources/extensions/gsd/auto.js +10 -3
- package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +174 -16
- package/dist/resources/extensions/gsd/gsd-db.js +52 -0
- package/dist/resources/extensions/gsd/migrate-external.js +13 -4
- package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
- package/dist/resources/extensions/gsd/repo-identity.js +16 -0
- package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
- package/dist/resources/extensions/gsd/unit-registry.js +27 -2
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/integrations/hermes/CONTEXT.md +47 -0
- package/integrations/hermes/README.md +21 -0
- package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
- package/integrations/hermes/docs/setup.md +4 -0
- package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
- package/integrations/hermes/open_gsd_hermes/config.py +2 -0
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
- package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
- package/integrations/hermes/plugin.yaml +4 -0
- package/integrations/hermes/tests/test_config.py +6 -0
- package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
- package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
- package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
- package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
- package/integrations/hermes/tests/test_milestone_client.py +447 -0
- package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
- package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/session-manager.js +1 -1
- package/packages/daemon/dist/session-manager.js.map +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/server.d.ts +18 -5
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +46 -5
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +1 -1
- package/packages/mcp-server/dist/session-manager.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +64 -3
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +212 -39
- package/src/resources/extensions/gsd/artifact-verification.ts +16 -4
- package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
- package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
- package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
- package/src/resources/extensions/gsd/auto.ts +9 -3
- package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +230 -21
- package/src/resources/extensions/gsd/gsd-db.ts +72 -0
- package/src/resources/extensions/gsd/migrate-external.ts +14 -4
- package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
- package/src/resources/extensions/gsd/repo-identity.ts +17 -0
- package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +335 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
- package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
- package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
- package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
- package/src/resources/extensions/gsd/unit-registry.ts +34 -2
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_ssgManifest.js +0 -0
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
import test from "node:test";
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
_runMilestoneMergeOnceWithStashRestore,
|
|
9
|
+
_runMilestoneMergeWithStashRestore,
|
|
10
|
+
} from "../auto/closeout.js";
|
|
8
11
|
import type { IterationContext } from "../auto/types.js";
|
|
9
12
|
import { MergeConflictError } from "../git-service.js";
|
|
10
13
|
import type {
|
|
@@ -58,6 +61,30 @@ function buildIc(opts: {
|
|
|
58
61
|
},
|
|
59
62
|
};
|
|
60
63
|
|
|
64
|
+
const plainExitMilestone = (exitOpts: { merge: boolean }) => {
|
|
65
|
+
log.mergeCalls += 1;
|
|
66
|
+
if (opts.mergeBehavior === "succeed") {
|
|
67
|
+
return { ok: true, merged: exitOpts.merge, codeFilesChanged: false } as const;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
opts.mergeBehavior();
|
|
71
|
+
return { ok: true, merged: exitOpts.merge, codeFilesChanged: false } as const;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// Mirror Lifecycle's typed-result wrapping of MergeConflictError
|
|
74
|
+
// and other thrown values per worktree-lifecycle.exitMilestone.
|
|
75
|
+
const isMergeConflict =
|
|
76
|
+
err !== null &&
|
|
77
|
+
typeof err === "object" &&
|
|
78
|
+
err !== undefined &&
|
|
79
|
+
(err as { name?: string }).name === "MergeConflictError";
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
reason: isMergeConflict ? "merge-conflict" : "teardown-failed",
|
|
83
|
+
cause: err,
|
|
84
|
+
} as const;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
61
88
|
const deps = {
|
|
62
89
|
preflightCleanRoot: () => {
|
|
63
90
|
log.preflightCalls += 1;
|
|
@@ -76,28 +103,72 @@ function buildIc(opts: {
|
|
|
76
103
|
},
|
|
77
104
|
},
|
|
78
105
|
lifecycle: {
|
|
79
|
-
exitMilestone: (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
exitMilestone: (
|
|
107
|
+
_mid: string,
|
|
108
|
+
exitOpts: {
|
|
109
|
+
merge: boolean;
|
|
110
|
+
guardedMerge?: {
|
|
111
|
+
projectRoot: string;
|
|
112
|
+
preflightCleanRoot: (
|
|
113
|
+
basePath: string,
|
|
114
|
+
milestoneId: string,
|
|
115
|
+
notify: (message: string, level: "info" | "warning" | "error") => void,
|
|
116
|
+
) => PreflightResult;
|
|
117
|
+
postflightPopStash: (
|
|
118
|
+
basePath: string,
|
|
119
|
+
milestoneId: string,
|
|
120
|
+
stashMarker: string | undefined,
|
|
121
|
+
notify: (message: string, level: "info" | "warning" | "error") => void,
|
|
122
|
+
) => PostflightResult;
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
exitCtx?: { notify: (message: string, level: "info" | "warning" | "error") => void },
|
|
126
|
+
) => {
|
|
127
|
+
const notify = exitCtx?.notify ?? (() => {});
|
|
128
|
+
const guarded = exitOpts.guardedMerge;
|
|
129
|
+
if (exitOpts.merge && guarded) {
|
|
130
|
+
const preflight = guarded.preflightCleanRoot(guarded.projectRoot, _mid, notify);
|
|
131
|
+
if (preflight.blocked) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
reason: preflight.blockedReason?.startsWith("unmerged-conflicts")
|
|
135
|
+
? "preflight-unmerged-conflicts"
|
|
136
|
+
: "preflight-dirty-overlap",
|
|
137
|
+
} as const;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const mergeResult = plainExitMilestone({ merge: true });
|
|
141
|
+
const postflight = preflight.stashPushed
|
|
142
|
+
? guarded.postflightPopStash(
|
|
143
|
+
guarded.projectRoot,
|
|
144
|
+
_mid,
|
|
145
|
+
preflight.stashMarker,
|
|
146
|
+
notify,
|
|
147
|
+
)
|
|
148
|
+
: undefined;
|
|
149
|
+
|
|
150
|
+
if (!mergeResult.ok) {
|
|
151
|
+
if (mergeResult.reason === "merge-conflict") {
|
|
152
|
+
return { ...mergeResult, postflight } as const;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
reason: "merge-failed",
|
|
157
|
+
cause: mergeResult.cause,
|
|
158
|
+
postflight,
|
|
159
|
+
} as const;
|
|
160
|
+
}
|
|
161
|
+
if (postflight?.needsManualRecovery) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
reason: "postflight-stash-restore-failed",
|
|
165
|
+
postflight,
|
|
166
|
+
} as const;
|
|
167
|
+
}
|
|
168
|
+
return mergeResult;
|
|
100
169
|
}
|
|
170
|
+
|
|
171
|
+
return plainExitMilestone(exitOpts);
|
|
101
172
|
},
|
|
102
173
|
},
|
|
103
174
|
stopAuto: async (
|
|
@@ -155,6 +226,14 @@ const PREFLIGHT_UNMERGED: PreflightResult = {
|
|
|
155
226
|
summary: "Working tree has unresolved Git conflicts before milestone M002 merge.",
|
|
156
227
|
};
|
|
157
228
|
|
|
229
|
+
const PREFLIGHT_UNMERGED_EVAL_FAILED: PreflightResult = {
|
|
230
|
+
stashPushed: false,
|
|
231
|
+
blocked: true,
|
|
232
|
+
blockedReason: "unmerged-conflicts-eval-failed",
|
|
233
|
+
conflictedPaths: ["todo.js"],
|
|
234
|
+
summary: "Unable to fully evaluate unresolved Git conflicts before milestone M002 merge.",
|
|
235
|
+
};
|
|
236
|
+
|
|
158
237
|
const POP_OK: PostflightResult = {
|
|
159
238
|
restored: true,
|
|
160
239
|
needsManualRecovery: false,
|
|
@@ -344,6 +423,52 @@ test("unmerged conflicts: preflight stops before merge and postflight restore",
|
|
|
344
423
|
);
|
|
345
424
|
});
|
|
346
425
|
|
|
426
|
+
test("unmerged conflict evaluation failure stops as preflight-unmerged-conflicts", async () => {
|
|
427
|
+
const { ic, log } = buildIc({
|
|
428
|
+
preflightResult: PREFLIGHT_UNMERGED_EVAL_FAILED,
|
|
429
|
+
mergeBehavior: "succeed",
|
|
430
|
+
postflightResult: POP_OK,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const result = await _runMilestoneMergeWithStashRestore(ic, "M002");
|
|
434
|
+
|
|
435
|
+
assert.deepEqual(result, {
|
|
436
|
+
action: "break",
|
|
437
|
+
reason: "preflight-unmerged-conflicts",
|
|
438
|
+
});
|
|
439
|
+
assert.equal(
|
|
440
|
+
log.mergeCalls,
|
|
441
|
+
0,
|
|
442
|
+
"failed conflict evaluation must not start milestone merge",
|
|
443
|
+
);
|
|
444
|
+
assert.equal(log.postflightCalls, 0, "blocked preflight must not attempt stash restore");
|
|
445
|
+
assert.equal(log.stopAutoCalls.length, 1);
|
|
446
|
+
assert.match(
|
|
447
|
+
log.stopAutoCalls[0] ?? "",
|
|
448
|
+
/Pre-merge unresolved Git conflicts block milestone M002/,
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("already-merged milestone skips guarded merge to terminate duplicate closeout loops", async () => {
|
|
453
|
+
const { ic, log } = buildIc({
|
|
454
|
+
preflightResult: STASH_PUSHED,
|
|
455
|
+
mergeBehavior: "succeed",
|
|
456
|
+
postflightResult: POP_OK,
|
|
457
|
+
});
|
|
458
|
+
log.milestoneMergedInPhases = true;
|
|
459
|
+
|
|
460
|
+
const result = await _runMilestoneMergeOnceWithStashRestore(ic, "M002");
|
|
461
|
+
|
|
462
|
+
assert.equal(result, null);
|
|
463
|
+
assert.equal(log.preflightCalls, 0, "already-merged guard must not re-trigger preflight");
|
|
464
|
+
assert.equal(log.mergeCalls, 0, "already-merged guard must not re-trigger merge");
|
|
465
|
+
assert.equal(
|
|
466
|
+
log.postflightCalls,
|
|
467
|
+
0,
|
|
468
|
+
"already-merged guard must not re-trigger stash restore",
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
|
|
347
472
|
test("merge succeeds but stash pop needs manual recovery -> postflight-stash-restore-failed break", async () => {
|
|
348
473
|
const { ic, log } = buildIc({
|
|
349
474
|
preflightResult: STASH_PUSHED,
|
|
@@ -358,6 +483,11 @@ test("merge succeeds but stash pop needs manual recovery -> postflight-stash-res
|
|
|
358
483
|
reason: "postflight-stash-restore-failed",
|
|
359
484
|
});
|
|
360
485
|
assert.equal(log.postflightCalls, 1);
|
|
486
|
+
assert.equal(
|
|
487
|
+
log.milestoneMergedInPhases,
|
|
488
|
+
true,
|
|
489
|
+
"successful merge must set the flag before postflight recovery stops auto-mode",
|
|
490
|
+
);
|
|
361
491
|
assert.equal(log.stopAutoCalls.length, 1);
|
|
362
492
|
assert.match(
|
|
363
493
|
log.stopAutoCalls[0] ?? "",
|
|
@@ -13,7 +13,7 @@ test("runMilestoneMergeTransaction delegates to the supplied merge runner", () =
|
|
|
13
13
|
const calls: unknown[][] = [];
|
|
14
14
|
const result = runMilestoneMergeTransaction(
|
|
15
15
|
{
|
|
16
|
-
|
|
16
|
+
mergeMilestone: (basePath, milestoneId, roadmapContent) => {
|
|
17
17
|
calls.push([basePath, milestoneId, roadmapContent]);
|
|
18
18
|
return { pushed: true, codeFilesChanged: true, commitMessage: "merge M001" };
|
|
19
19
|
},
|
|
@@ -82,13 +82,57 @@ const ic = {
|
|
|
82
82
|
return { ok: true, needsManualRecovery: false };
|
|
83
83
|
},
|
|
84
84
|
lifecycle: {
|
|
85
|
-
exitMilestone
|
|
85
|
+
// Mirror WorktreeLifecycle.exitMilestone's guarded-merge branch
|
|
86
|
+
// (preflight -> inner merge -> postflight). Since #1169 the closeout
|
|
87
|
+
// routes the guard through the lifecycle verb, so a non-conflict inner
|
|
88
|
+
// failure ("teardown-failed") is mapped to "merge-failed" here rather
|
|
89
|
+
// than by the caller.
|
|
90
|
+
exitMilestone(
|
|
91
|
+
milestoneId: string,
|
|
92
|
+
exitOpts: {
|
|
93
|
+
merge: boolean;
|
|
94
|
+
guardedMerge?: {
|
|
95
|
+
projectRoot: string;
|
|
96
|
+
preflightCleanRoot: (
|
|
97
|
+
basePath: string,
|
|
98
|
+
milestoneId: string,
|
|
99
|
+
notify: (message: string, level: "info" | "warning" | "error") => void,
|
|
100
|
+
) => { blocked?: boolean; blockedReason?: string; stashPushed: boolean; stashMarker?: string };
|
|
101
|
+
postflightPopStash: (
|
|
102
|
+
basePath: string,
|
|
103
|
+
milestoneId: string,
|
|
104
|
+
stashMarker: string | undefined,
|
|
105
|
+
notify: (message: string, level: "info" | "warning" | "error") => void,
|
|
106
|
+
) => { needsManualRecovery?: boolean };
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
exitCtx?: { notify: (message: string, level: "info" | "warning" | "error") => void },
|
|
110
|
+
) {
|
|
111
|
+
const notify = exitCtx?.notify ?? (() => {});
|
|
112
|
+
const guarded = exitOpts.guardedMerge;
|
|
113
|
+
if (exitOpts.merge && guarded) {
|
|
114
|
+
const preflight = guarded.preflightCleanRoot(guarded.projectRoot, milestoneId, notify);
|
|
115
|
+
if (preflight.blocked) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
reason: preflight.blockedReason?.startsWith("unmerged-conflicts")
|
|
119
|
+
? "preflight-unmerged-conflicts"
|
|
120
|
+
: "preflight-dirty-overlap",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// Inner merge fails with a non-conflict error. The lifecycle still
|
|
124
|
+
// runs postflight stash restore on the failed-merge path, then maps
|
|
125
|
+
// the non-conflict failure to "merge-failed" (a MergeConflictError
|
|
126
|
+
// would instead map to "merge-conflict").
|
|
127
|
+
calls.push("merge");
|
|
128
|
+
const cause = new Error("remote rejected push");
|
|
129
|
+
if (preflight.stashPushed) {
|
|
130
|
+
guarded.postflightPopStash(guarded.projectRoot, milestoneId, preflight.stashMarker, notify);
|
|
131
|
+
}
|
|
132
|
+
return { ok: false, reason: "merge-failed", cause };
|
|
133
|
+
}
|
|
86
134
|
calls.push("merge");
|
|
87
|
-
return {
|
|
88
|
-
ok: false,
|
|
89
|
-
reason: "teardown-failed",
|
|
90
|
-
cause: new Error("remote rejected push"),
|
|
91
|
-
};
|
|
135
|
+
return { ok: false, reason: "teardown-failed", cause: new Error("remote rejected push") };
|
|
92
136
|
},
|
|
93
137
|
},
|
|
94
138
|
async stopAuto(_ctx: unknown, _pi: unknown, reason?: string) {
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
import test from "node:test";
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
|
-
import { closeSync, ftruncateSync, openSync, statSync
|
|
6
|
+
import { closeSync, ftruncateSync, openSync, statSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
|
-
import { captureRootDirtySnapshot } from "../root-write-leak-guard.ts";
|
|
10
|
-
import { cleanup, createFile,
|
|
9
|
+
import { captureRootDirtySnapshot, detectRootWriteLeak } from "../root-write-leak-guard.ts";
|
|
10
|
+
import { cleanup, createFile, makeTempRepo } from "./test-utils.ts";
|
|
11
11
|
|
|
12
12
|
test("captureRootDirtySnapshot ignores unignored GSD root runtime directories", () => {
|
|
13
13
|
const base = makeTempRepo("gsd-root-dirty-runtime-");
|
|
@@ -34,11 +34,7 @@ test("captureRootDirtySnapshot does not read dirty files larger than Node's Buff
|
|
|
34
34
|
const relPath = "large.bin";
|
|
35
35
|
const absPath = join(base, relPath);
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
git(base, "add", relPath);
|
|
39
|
-
git(base, "commit", "-m", "track large fixture");
|
|
40
|
-
|
|
41
|
-
const fd = openSync(absPath, "r+");
|
|
37
|
+
const fd = openSync(absPath, "w");
|
|
42
38
|
try {
|
|
43
39
|
ftruncateSync(fd, 2_200 * 1024 * 1024);
|
|
44
40
|
} finally {
|
|
@@ -51,9 +47,35 @@ test("captureRootDirtySnapshot does not read dirty files larger than Node's Buff
|
|
|
51
47
|
const snapshot = captureRootDirtySnapshot(base);
|
|
52
48
|
const entry = snapshot.get(relPath);
|
|
53
49
|
|
|
54
|
-
assert.equal(entry?.status, "
|
|
50
|
+
assert.equal(entry?.status, "??");
|
|
55
51
|
assert.match(entry?.fingerprint ?? "", /^large:\d+:/);
|
|
56
52
|
} finally {
|
|
57
53
|
cleanup(base);
|
|
58
54
|
}
|
|
59
55
|
});
|
|
56
|
+
|
|
57
|
+
test("detectRootWriteLeak reports changed baseline untracked root files", () => {
|
|
58
|
+
const base = makeTempRepo("gsd-root-dirty-baseline-");
|
|
59
|
+
try {
|
|
60
|
+
const relPath = "notes/todo.md";
|
|
61
|
+
createFile(base, relPath, "before\n");
|
|
62
|
+
const before = captureRootDirtySnapshot(base);
|
|
63
|
+
|
|
64
|
+
createFile(base, relPath, "after\n");
|
|
65
|
+
const leak = detectRootWriteLeak({
|
|
66
|
+
rootPath: base,
|
|
67
|
+
worktreePath: join(base, ".gsd", "worktrees", "M001"),
|
|
68
|
+
unitType: "execute-task",
|
|
69
|
+
unitId: "M001/S01/T01",
|
|
70
|
+
before,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
assert.ok(leak, "changed baseline untracked file should be reported as a leak");
|
|
74
|
+
assert.deepEqual(
|
|
75
|
+
leak.files.map((file) => file.path),
|
|
76
|
+
[relPath],
|
|
77
|
+
);
|
|
78
|
+
} finally {
|
|
79
|
+
cleanup(base);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Regression tests for ScheduleWakeup tool behavior.
|
|
3
|
+
|
|
4
|
+
import test, { mock } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
|
|
7
|
+
import { autoSession } from "../auto-runtime-state.ts";
|
|
8
|
+
import {
|
|
9
|
+
registerScheduleWakeupTool,
|
|
10
|
+
_resetInteractiveWakeupsForTest,
|
|
11
|
+
} from "../bootstrap/schedule-wakeup-tool.ts";
|
|
12
|
+
|
|
13
|
+
test("ScheduleWakeup arms an interactive wakeup when auto-mode is inactive", async () => {
|
|
14
|
+
autoSession.reset();
|
|
15
|
+
_resetInteractiveWakeupsForTest();
|
|
16
|
+
mock.timers.enable({ apis: ["setTimeout"] });
|
|
17
|
+
|
|
18
|
+
const sent: Array<{ message: unknown; options: unknown }> = [];
|
|
19
|
+
let tool: any;
|
|
20
|
+
const pi = {
|
|
21
|
+
registerTool(registered: any) {
|
|
22
|
+
tool = registered;
|
|
23
|
+
},
|
|
24
|
+
sendMessage(message: unknown, options: unknown) {
|
|
25
|
+
sent.push({ message, options });
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
registerScheduleWakeupTool(pi as any);
|
|
31
|
+
assert.ok(tool, "ScheduleWakeup tool should be registered");
|
|
32
|
+
|
|
33
|
+
const result = await tool.execute(
|
|
34
|
+
"call-1",
|
|
35
|
+
{
|
|
36
|
+
delaySeconds: 1,
|
|
37
|
+
prompt: "Check the external job and report back.",
|
|
38
|
+
reason: "poll external job",
|
|
39
|
+
},
|
|
40
|
+
undefined,
|
|
41
|
+
undefined,
|
|
42
|
+
{ cwd: process.cwd() },
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
assert.equal(result.isError, undefined);
|
|
46
|
+
assert.match(result.content[0].text, /Wakeup scheduled for 1s/);
|
|
47
|
+
assert.equal(sent.length, 0, "wakeup should not dispatch synchronously");
|
|
48
|
+
|
|
49
|
+
mock.timers.tick(999);
|
|
50
|
+
assert.equal(sent.length, 0, "wakeup should wait for the configured delay");
|
|
51
|
+
|
|
52
|
+
mock.timers.tick(1);
|
|
53
|
+
assert.equal(sent.length, 1);
|
|
54
|
+
assert.deepEqual(sent[0].options, { triggerTurn: true });
|
|
55
|
+
assert.deepEqual(sent[0].message, {
|
|
56
|
+
customType: "gsd-schedule-wakeup",
|
|
57
|
+
content: "Check the external job and report back.",
|
|
58
|
+
display: true,
|
|
59
|
+
details: {
|
|
60
|
+
delaySeconds: 1,
|
|
61
|
+
reason: "poll external job",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
} finally {
|
|
65
|
+
mock.timers.reset();
|
|
66
|
+
_resetInteractiveWakeupsForTest();
|
|
67
|
+
autoSession.reset();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("ScheduleWakeup re-arm cancels the prior interactive timer instead of stacking", async () => {
|
|
72
|
+
autoSession.reset();
|
|
73
|
+
_resetInteractiveWakeupsForTest();
|
|
74
|
+
mock.timers.enable({ apis: ["setTimeout"] });
|
|
75
|
+
|
|
76
|
+
const sent: Array<{ message: any; options: unknown }> = [];
|
|
77
|
+
let tool: any;
|
|
78
|
+
const pi = {
|
|
79
|
+
registerTool(registered: any) {
|
|
80
|
+
tool = registered;
|
|
81
|
+
},
|
|
82
|
+
sendMessage(message: unknown, options: unknown) {
|
|
83
|
+
sent.push({ message, options } as { message: any; options: unknown });
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
registerScheduleWakeupTool(pi as any);
|
|
89
|
+
assert.ok(tool, "ScheduleWakeup tool should be registered");
|
|
90
|
+
|
|
91
|
+
const ctx = { cwd: process.cwd() };
|
|
92
|
+
await tool.execute(
|
|
93
|
+
"call-1",
|
|
94
|
+
{ delaySeconds: 5, prompt: "stale prompt", reason: "first arm" },
|
|
95
|
+
undefined,
|
|
96
|
+
undefined,
|
|
97
|
+
ctx,
|
|
98
|
+
);
|
|
99
|
+
await tool.execute(
|
|
100
|
+
"call-2",
|
|
101
|
+
{ delaySeconds: 5, prompt: "fresh prompt", reason: "re-arm" },
|
|
102
|
+
undefined,
|
|
103
|
+
undefined,
|
|
104
|
+
ctx,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
mock.timers.tick(5000);
|
|
108
|
+
|
|
109
|
+
assert.equal(sent.length, 1, "re-arming must not stack overlapping wakeups");
|
|
110
|
+
assert.equal(sent[0].message.content, "fresh prompt");
|
|
111
|
+
} finally {
|
|
112
|
+
mock.timers.reset();
|
|
113
|
+
_resetInteractiveWakeupsForTest();
|
|
114
|
+
autoSession.reset();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("ScheduleWakeup keeps interactive wakeups isolated per session base path", async () => {
|
|
119
|
+
autoSession.reset();
|
|
120
|
+
_resetInteractiveWakeupsForTest();
|
|
121
|
+
mock.timers.enable({ apis: ["setTimeout"] });
|
|
122
|
+
|
|
123
|
+
const sent: Array<{ message: any; options: unknown }> = [];
|
|
124
|
+
let tool: any;
|
|
125
|
+
const pi = {
|
|
126
|
+
registerTool(registered: any) {
|
|
127
|
+
tool = registered;
|
|
128
|
+
},
|
|
129
|
+
sendMessage(message: unknown, options: unknown) {
|
|
130
|
+
sent.push({ message, options } as { message: any; options: unknown });
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
registerScheduleWakeupTool(pi as any);
|
|
136
|
+
assert.ok(tool, "ScheduleWakeup tool should be registered");
|
|
137
|
+
|
|
138
|
+
// Two concurrent interactive sessions (different cwds) arm wakeups. One
|
|
139
|
+
// session re-arming must not cancel the other session's pending wakeup.
|
|
140
|
+
await tool.execute(
|
|
141
|
+
"call-a",
|
|
142
|
+
{ delaySeconds: 5, prompt: "session A", reason: "poll A" },
|
|
143
|
+
undefined,
|
|
144
|
+
undefined,
|
|
145
|
+
{ cwd: "/tmp" },
|
|
146
|
+
);
|
|
147
|
+
await tool.execute(
|
|
148
|
+
"call-b",
|
|
149
|
+
{ delaySeconds: 5, prompt: "session B", reason: "poll B" },
|
|
150
|
+
undefined,
|
|
151
|
+
undefined,
|
|
152
|
+
{ cwd: process.cwd() },
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
mock.timers.tick(5000);
|
|
156
|
+
|
|
157
|
+
assert.equal(sent.length, 2, "distinct sessions must each keep their wakeup");
|
|
158
|
+
assert.deepEqual(
|
|
159
|
+
sent.map((s) => s.message.content).sort(),
|
|
160
|
+
["session A", "session B"],
|
|
161
|
+
);
|
|
162
|
+
} finally {
|
|
163
|
+
mock.timers.reset();
|
|
164
|
+
_resetInteractiveWakeupsForTest();
|
|
165
|
+
autoSession.reset();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
@@ -312,7 +312,7 @@ describe("#2945 Bug 3: mergeAndExit must teardown worktree after successful merg
|
|
|
312
312
|
} as unknown as AutoSession;
|
|
313
313
|
|
|
314
314
|
const deps = {
|
|
315
|
-
|
|
315
|
+
mergeMilestone: () => ({ pushed: false, codeFilesChanged: true }),
|
|
316
316
|
worktreeProjection: new WorktreeStateProjection(),
|
|
317
317
|
gitServiceFactory: () => ({}) as unknown as ReturnType<
|
|
318
318
|
import("../worktree-lifecycle.js").WorktreeLifecycleDeps["gitServiceFactory"]
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import test from "node:test";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
10
12
|
|
|
11
13
|
import {
|
|
12
14
|
KNOWN_UNIT_TYPES,
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
SECTION_CLOSE_GATE_UNIT_TYPES,
|
|
16
18
|
getUnitDescriptor,
|
|
17
19
|
getUnitPhaseChain,
|
|
20
|
+
getUnitPromptTemplate,
|
|
18
21
|
} from "../unit-registry.ts";
|
|
19
22
|
import {
|
|
20
23
|
AUTO_UNIT_SCOPED_TOOLS,
|
|
@@ -96,6 +99,37 @@ const EXPECTED_PHASE_CHAINS: Record<string, string[] | undefined> = {
|
|
|
96
99
|
"no-such-unit": undefined,
|
|
97
100
|
};
|
|
98
101
|
|
|
102
|
+
const EXPECTED_DIRECT_PROMPT_TEMPLATES: Record<string, string> = {
|
|
103
|
+
"research-milestone": "research-milestone",
|
|
104
|
+
"plan-milestone": "plan-milestone",
|
|
105
|
+
"validate-milestone": "validate-milestone",
|
|
106
|
+
"complete-milestone": "complete-milestone",
|
|
107
|
+
"research-slice": "research-slice",
|
|
108
|
+
"plan-slice": "plan-slice",
|
|
109
|
+
"refine-slice": "refine-slice",
|
|
110
|
+
"replan-slice": "replan-slice",
|
|
111
|
+
"complete-slice": "complete-slice",
|
|
112
|
+
"reassess-roadmap": "reassess-roadmap",
|
|
113
|
+
"execute-task": "execute-task",
|
|
114
|
+
"reactive-execute": "reactive-execute",
|
|
115
|
+
"run-uat": "run-uat",
|
|
116
|
+
"gate-evaluate": "gate-evaluate",
|
|
117
|
+
"rewrite-docs": "rewrite-docs",
|
|
118
|
+
"workflow-preferences": "guided-workflow-preferences",
|
|
119
|
+
"discuss-project": "guided-discuss-project",
|
|
120
|
+
"discuss-requirements": "guided-discuss-requirements",
|
|
121
|
+
"research-decision": "guided-research-decision",
|
|
122
|
+
"research-project": "guided-research-project",
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const EXPECTED_UNDECLARED_PROMPT_TEMPLATE_TYPES = [
|
|
126
|
+
"discuss-milestone",
|
|
127
|
+
"discuss-slice",
|
|
128
|
+
"execute-task-simple",
|
|
129
|
+
"triage-captures",
|
|
130
|
+
"quick-task",
|
|
131
|
+
];
|
|
132
|
+
|
|
99
133
|
// ─── Derived-view parity ──────────────────────────────────────────────────
|
|
100
134
|
|
|
101
135
|
test("KNOWN_UNIT_TYPES derives exactly the pre-registry list, in order", () => {
|
|
@@ -134,6 +168,28 @@ test("phaseChainForUnit matches the pre-registry switch for every known input",
|
|
|
134
168
|
}
|
|
135
169
|
});
|
|
136
170
|
|
|
171
|
+
test("direct prompt-template associations live on the Unit Registry", () => {
|
|
172
|
+
for (const [unitType, promptTemplate] of Object.entries(EXPECTED_DIRECT_PROMPT_TEMPLATES)) {
|
|
173
|
+
assert.equal(
|
|
174
|
+
getUnitPromptTemplate(unitType),
|
|
175
|
+
promptTemplate,
|
|
176
|
+
`prompt template for ${unitType}`,
|
|
177
|
+
);
|
|
178
|
+
assert.ok(
|
|
179
|
+
existsSync(join(process.cwd(), "src/resources/extensions/gsd/prompts", `${promptTemplate}.md`)),
|
|
180
|
+
`prompt template file must exist for ${unitType}: ${promptTemplate}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const unitType of EXPECTED_UNDECLARED_PROMPT_TEMPLATE_TYPES) {
|
|
185
|
+
assert.equal(
|
|
186
|
+
getUnitPromptTemplate(unitType),
|
|
187
|
+
undefined,
|
|
188
|
+
`${unitType} prompt association is conditional or not verified yet`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
137
193
|
test("AUTO_UNIT_SCOPED_TOOLS mirrors each contract's allowed tools", () => {
|
|
138
194
|
for (const [unitType, contract] of Object.entries(UNIT_TOOL_CONTRACTS)) {
|
|
139
195
|
assert.deepEqual(AUTO_UNIT_SCOPED_TOOLS[unitType], contract.allowedGsdTools);
|
|
@@ -107,7 +107,7 @@ function makeDeps(
|
|
|
107
107
|
// real `getAutoWorktreePath` / `createAutoWorktree` / etc. and the
|
|
108
108
|
// success/existing/failure branches won't fire as expected.
|
|
109
109
|
const deps: LegacyTestDeps = {
|
|
110
|
-
|
|
110
|
+
mergeMilestone: () => ({ pushed: false, codeFilesChanged: true }),
|
|
111
111
|
worktreeProjection: new WorktreeStateProjection(),
|
|
112
112
|
// ADR-016 phase 2 / C4 (#5627): GitServiceImpl constructor → factory.
|
|
113
113
|
gitServiceFactory: () => ({}) as unknown as ReturnType<
|
|
@@ -283,7 +283,7 @@ describe("worktree journal events", () => {
|
|
|
283
283
|
|
|
284
284
|
const s = makeSession({ basePath: wt, originalBasePath: tmp });
|
|
285
285
|
const deps = makeDeps({
|
|
286
|
-
|
|
286
|
+
mergeMilestone: () => ({ pushed: false, codeFilesChanged: true }),
|
|
287
287
|
});
|
|
288
288
|
process.chdir(wt);
|
|
289
289
|
|
|
@@ -304,7 +304,7 @@ describe("worktree journal events", () => {
|
|
|
304
304
|
const wt = setupMergeWorktree(tmp, "M001");
|
|
305
305
|
const s = makeSession({ basePath: wt, originalBasePath: tmp });
|
|
306
306
|
const deps = makeDeps({
|
|
307
|
-
|
|
307
|
+
mergeMilestone: () => { throw new Error("conflict in main"); },
|
|
308
308
|
});
|
|
309
309
|
// Since #4380, mergeAndExit re-throws all errors after emitting the journal
|
|
310
310
|
// event and restoring state. Lifecycle now wraps that throw in a typed
|
|
@@ -347,7 +347,7 @@ describe("worktree journal events", () => {
|
|
|
347
347
|
const s = makeSession({ basePath: wt, originalBasePath: tmp });
|
|
348
348
|
let attempt = 0;
|
|
349
349
|
const deps = makeDeps({
|
|
350
|
-
|
|
350
|
+
mergeMilestone: () => {
|
|
351
351
|
attempt += 1;
|
|
352
352
|
throw new MergeConflictError(
|
|
353
353
|
attempt === 1 ? ["src/a.ts"] : ["src/b.ts", "src/c.ts"],
|
|
@@ -46,7 +46,7 @@ function makeDeps(
|
|
|
46
46
|
): LegacyTestDeps & { calls: CallLog[] } {
|
|
47
47
|
const calls: CallLog[] = [];
|
|
48
48
|
// ADR-016 phase 2 / C-track close-out: WorktreeLifecycleDeps is now a
|
|
49
|
-
// 3-field bag (gitServiceFactory, worktreeProjection,
|
|
49
|
+
// 3-field bag (gitServiceFactory, worktreeProjection, mergeMilestone).
|
|
50
50
|
// Tests still pass legacy override hooks via `LegacyTestDeps` — Lifecycle
|
|
51
51
|
// ignores the extras structurally and reads them through the C1-healing
|
|
52
52
|
// primitive-override pattern when stubs are needed.
|
|
@@ -73,7 +73,7 @@ function makeDeps(
|
|
|
73
73
|
},
|
|
74
74
|
autoWorktreeBranch: (mid: string) => `milestone/${mid}`,
|
|
75
75
|
teardownAutoWorktree: () => {},
|
|
76
|
-
|
|
76
|
+
mergeMilestone: () => ({ pushed: false, codeFilesChanged: true }),
|
|
77
77
|
...overrides,
|
|
78
78
|
};
|
|
79
79
|
return deps;
|