gsd-pi 2.82.0-dev.2841a1e44 → 2.82.0-dev.9d5798940
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/partial-builder.js +2 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +13 -6
- package/dist/resources/extensions/gsd/auto-post-unit.js +69 -8
- package/dist/resources/extensions/gsd/auto-recovery.js +31 -1
- package/dist/resources/extensions/gsd/auto-start.js +7 -3
- package/dist/resources/extensions/gsd/auto-worktree.js +96 -0
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +4 -1
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +13 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +17 -1
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +2 -2
- package/dist/resources/extensions/gsd/export-html.js +27 -425
- package/dist/resources/extensions/gsd/milestone-actions.js +11 -4
- package/dist/resources/extensions/gsd/native-git-bridge.js +8 -3
- package/dist/resources/extensions/gsd/state-reconciliation/drift/merge-state.js +6 -1
- package/dist/resources/extensions/gsd/tools/plan-slice.js +2 -1
- package/dist/resources/extensions/gsd/unit-context-manifest.js +7 -8
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +28 -7
- package/dist/resources/extensions/shared/html-shell.js +388 -0
- package/dist/resources/extensions/visual-brief/page-contract.js +2 -0
- package/dist/resources/extensions/visual-brief/prompts.js +29 -0
- 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 +11 -11
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- 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/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +4 -7
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +4 -7
- 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 +4 -5
- 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 +2 -5
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -7
- 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 +4 -7
- 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 +4 -5
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -5
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/chunks/4266.js +2 -0
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +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/dist/web/standalone/.next/static/chunks/app/layout-8c10ec293ae0f1d5.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-6a95bc41e0f7ec89.js → webpack-9a4db269f9ed63ad.js} +1 -1
- package/dist/web/standalone/.next/static/css/746ee28c929d1880.css +1 -0
- package/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/partial-builder.ts +2 -1
- package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +19 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -6
- package/src/resources/extensions/gsd/auto-post-unit.ts +76 -6
- package/src/resources/extensions/gsd/auto-recovery.ts +29 -0
- package/src/resources/extensions/gsd/auto-start.ts +7 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +104 -0
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +6 -1
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +16 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +17 -1
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +3 -3
- package/src/resources/extensions/gsd/export-html.ts +27 -427
- package/src/resources/extensions/gsd/milestone-actions.ts +10 -4
- package/src/resources/extensions/gsd/native-git-bridge.ts +8 -3
- package/src/resources/extensions/gsd/state-reconciliation/drift/merge-state.ts +8 -1
- package/src/resources/extensions/gsd/tests/auto-deterministic-error-classification-4973.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +12 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +15 -1
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +69 -1
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +57 -2
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +55 -1
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/post-unit-git-failure.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +46 -2
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +65 -7
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +64 -12
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +31 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +12 -9
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +34 -7
- package/src/resources/extensions/shared/html-shell.ts +412 -0
- package/src/resources/extensions/visual-brief/page-contract.ts +2 -0
- package/src/resources/extensions/visual-brief/prompts.ts +37 -1
- package/src/resources/extensions/visual-brief/tests/visual-brief.test.ts +40 -0
- package/dist/web/standalone/.next/server/chunks/5822.js +0 -2
- package/dist/web/standalone/.next/static/chunks/app/layout-a16c7a7ecdf0c2cf.js +0 -1
- package/dist/web/standalone/.next/static/css/0262768ec1b89d34.css +0 -1
- package/dist/web/standalone/.next/static/css/de70bee13400563f.css +0 -1
- package/dist/web/standalone/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/747892c23ea88013-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/8d697b304b401681-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/9610d9e46709d722-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
- /package/dist/web/standalone/.next/static/{Qgr2B_MRhPxC0z8fwv4vT → BdZQhe8yKl6bdKLiXVEzh}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{Qgr2B_MRhPxC0z8fwv4vT → BdZQhe8yKl6bdKLiXVEzh}/_ssgManifest.js +0 -0
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
type SkillsPolicy,
|
|
15
15
|
type UnitContextManifest,
|
|
16
16
|
} from "../unit-context-manifest.ts";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
ALLOWED_PLANNING_DISPATCH_AGENTS,
|
|
19
|
+
shouldBlockPlanningUnit,
|
|
20
|
+
} from "../bootstrap/write-gate.ts";
|
|
18
21
|
import {
|
|
19
22
|
getRequiredWorkflowToolsForAutoUnit,
|
|
20
23
|
getRequiredWorkflowToolsForGuidedUnit,
|
|
@@ -217,7 +220,7 @@ test("#4934: every manifest declares a tools policy", () => {
|
|
|
217
220
|
});
|
|
218
221
|
|
|
219
222
|
test("#4934: tools.mode is one of the declared policies", () => {
|
|
220
|
-
const validModes = new Set(["all", "read-only", "planning", "planning-dispatch", "docs"]);
|
|
223
|
+
const validModes = new Set(["all", "read-only", "planning", "planning-dispatch", "docs", "verification"]);
|
|
221
224
|
for (const [unitType, manifest] of Object.entries(UNIT_MANIFESTS)) {
|
|
222
225
|
const mode = (manifest as { tools: { mode: string } }).tools.mode;
|
|
223
226
|
assert.ok(
|
|
@@ -227,28 +230,83 @@ test("#4934: tools.mode is one of the declared policies", () => {
|
|
|
227
230
|
}
|
|
228
231
|
});
|
|
229
232
|
|
|
230
|
-
test('#4934: only
|
|
231
|
-
const allowedAllUnits = new Set(["execute-task", "reactive-execute"]);
|
|
233
|
+
test('#4934: only execution units and complete-milestone may use tools.mode "all"', () => {
|
|
234
|
+
const allowedAllUnits = new Set(["execute-task", "reactive-execute", "complete-milestone"]);
|
|
232
235
|
for (const [unitType, manifest] of Object.entries(UNIT_MANIFESTS)) {
|
|
233
236
|
const mode = (manifest as { tools: { mode: string } }).tools.mode;
|
|
234
237
|
if (mode === "all") {
|
|
235
238
|
assert.ok(
|
|
236
239
|
allowedAllUnits.has(unitType),
|
|
237
|
-
`manifest "${unitType}" declares tools.mode = "all" but is not
|
|
238
|
-
'Only execute-task
|
|
240
|
+
`manifest "${unitType}" declares tools.mode = "all" but is not explicitly allowed. ` +
|
|
241
|
+
'Only execute-task, reactive-execute, and complete-milestone should have full source write access; ' +
|
|
239
242
|
'planning/discuss/research units must use "planning" or "planning-dispatch" (or "docs" for rewrite-docs).',
|
|
240
243
|
);
|
|
241
244
|
}
|
|
242
245
|
}
|
|
243
246
|
});
|
|
244
247
|
|
|
248
|
+
test("#5453: complete-milestone uses all tools so bash verification is not planning-dispatch blocked", () => {
|
|
249
|
+
const manifest = UNIT_MANIFESTS["complete-milestone"];
|
|
250
|
+
|
|
251
|
+
assert.strictEqual(manifest.tools.mode, "all");
|
|
252
|
+
assert.deepEqual(resolveSubagentPermissionContract("complete-milestone"), {
|
|
253
|
+
allowed: true,
|
|
254
|
+
allowedSubagents: ["*"],
|
|
255
|
+
toolsMode: "all",
|
|
256
|
+
});
|
|
257
|
+
// Runtime gate-level regression: these verification commands were blocked
|
|
258
|
+
// under planning-dispatch in #5453; complete-milestone must bypass that gate.
|
|
259
|
+
for (const cmd of ["git diff --name-only HEAD~1", "git log -n1 --oneline"]) {
|
|
260
|
+
const result = shouldBlockPlanningUnit(
|
|
261
|
+
"bash",
|
|
262
|
+
cmd,
|
|
263
|
+
process.cwd(),
|
|
264
|
+
"complete-milestone",
|
|
265
|
+
manifest.tools,
|
|
266
|
+
);
|
|
267
|
+
assert.strictEqual(
|
|
268
|
+
result.block,
|
|
269
|
+
false,
|
|
270
|
+
`shouldBlockPlanningUnit must not block ${cmd} for complete-milestone: ${result.reason}`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("#5843: run-uat uses verification tools policy so build/test commands can run", () => {
|
|
276
|
+
const manifest = UNIT_MANIFESTS["run-uat"];
|
|
277
|
+
|
|
278
|
+
assert.strictEqual(manifest.tools.mode, "verification");
|
|
279
|
+
|
|
280
|
+
const buildResult = shouldBlockPlanningUnit(
|
|
281
|
+
"bash",
|
|
282
|
+
"npm run build 2>&1",
|
|
283
|
+
process.cwd(),
|
|
284
|
+
"run-uat",
|
|
285
|
+
manifest.tools,
|
|
286
|
+
);
|
|
287
|
+
assert.strictEqual(
|
|
288
|
+
buildResult.block,
|
|
289
|
+
false,
|
|
290
|
+
`run-uat must allow build verification commands: ${buildResult.reason}`,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const sourceWriteResult = shouldBlockPlanningUnit(
|
|
294
|
+
"edit",
|
|
295
|
+
"src/main.ts",
|
|
296
|
+
process.cwd(),
|
|
297
|
+
"run-uat",
|
|
298
|
+
manifest.tools,
|
|
299
|
+
);
|
|
300
|
+
assert.strictEqual(sourceWriteResult.block, true);
|
|
301
|
+
assert.match(sourceWriteResult.reason!, /tools-policy "verification"/);
|
|
302
|
+
});
|
|
303
|
+
|
|
245
304
|
test('planning-dispatch mode is reserved for slice-level decomposition and completion units', () => {
|
|
246
305
|
const allowedDispatchUnits = new Set([
|
|
247
306
|
"plan-slice",
|
|
248
307
|
"research-slice",
|
|
249
308
|
"refine-slice",
|
|
250
309
|
"complete-slice",
|
|
251
|
-
"complete-milestone",
|
|
252
310
|
"gate-evaluate",
|
|
253
311
|
// Deep planning mode: research-project orchestrates 4 parallel research
|
|
254
312
|
// subagents (stack/features/architecture/pitfalls). Subagent dispatch is
|
|
@@ -30,11 +30,13 @@ function initGitRepoIn(base: string, isolation: "worktree" | "branch" | "none"):
|
|
|
30
30
|
}
|
|
31
31
|
import {
|
|
32
32
|
WorktreeLifecycle,
|
|
33
|
+
resetRecentWorktreeMergeFailuresForTest,
|
|
33
34
|
type WorktreeLifecycleDeps,
|
|
34
35
|
type NotifyCtx,
|
|
35
36
|
} from "../worktree-lifecycle.js";
|
|
36
37
|
import { WorktreeStateProjection } from "../worktree-state-projection.js";
|
|
37
38
|
import { type TaskCommitContext } from "../worktree.js";
|
|
39
|
+
import { MergeConflictError } from "../git-service.js";
|
|
38
40
|
|
|
39
41
|
// ADR-016 phase 2 / C-track retired all worktree-manager + cache + prefs
|
|
40
42
|
// fields from `WorktreeLifecycleDeps`. Tests still pass them as overrides
|
|
@@ -141,6 +143,20 @@ function readJournalEntries(basePath: string): JournalEntry[] {
|
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
function setupMergeWorktree(basePath: string, milestoneId: string): string {
|
|
147
|
+
initGitRepoIn(basePath, "worktree");
|
|
148
|
+
execFileSync("git", ["checkout", "-b", `milestone/${milestoneId}`], { cwd: basePath, stdio: "pipe" });
|
|
149
|
+
execFileSync("git", ["checkout", "main"], { cwd: basePath, stdio: "pipe" });
|
|
150
|
+
const wt = join(basePath, ".gsd", "worktrees", milestoneId);
|
|
151
|
+
execFileSync("git", ["worktree", "add", wt, `milestone/${milestoneId}`], { cwd: basePath, stdio: "pipe" });
|
|
152
|
+
mkdirSync(join(basePath, ".gsd", "milestones", milestoneId), { recursive: true });
|
|
153
|
+
writeFileSync(
|
|
154
|
+
join(basePath, ".gsd", "milestones", milestoneId, `${milestoneId}-ROADMAP.md`),
|
|
155
|
+
`# ${milestoneId}\n- [x] S01: Slice one\n`,
|
|
156
|
+
);
|
|
157
|
+
return wt;
|
|
158
|
+
}
|
|
159
|
+
|
|
144
160
|
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
145
161
|
|
|
146
162
|
describe("worktree journal events", () => {
|
|
@@ -148,6 +164,7 @@ describe("worktree journal events", () => {
|
|
|
148
164
|
const originalCwd = process.cwd();
|
|
149
165
|
|
|
150
166
|
beforeEach(() => {
|
|
167
|
+
resetRecentWorktreeMergeFailuresForTest();
|
|
151
168
|
// realpathSync to match what `auto-worktree.ts` returns from
|
|
152
169
|
// `resolveWorktreeProjectRoot` (macOS resolves `/var` → `/private/var`).
|
|
153
170
|
tmp = realpathSync(mkdtempSync(join(tmpdir(), "wt-journal-")));
|
|
@@ -284,17 +301,7 @@ describe("worktree journal events", () => {
|
|
|
284
301
|
});
|
|
285
302
|
|
|
286
303
|
test("mergeAndExit emits worktree-merge-failed on error", () => {
|
|
287
|
-
|
|
288
|
-
execFileSync("git", ["checkout", "-b", "milestone/M001"], { cwd: tmp, stdio: "pipe" });
|
|
289
|
-
execFileSync("git", ["checkout", "main"], { cwd: tmp, stdio: "pipe" });
|
|
290
|
-
const wt = join(tmp, ".gsd", "worktrees", "M001");
|
|
291
|
-
execFileSync("git", ["worktree", "add", wt, "milestone/M001"], { cwd: tmp, stdio: "pipe" });
|
|
292
|
-
mkdirSync(join(tmp, ".gsd", "milestones", "M001"), { recursive: true });
|
|
293
|
-
writeFileSync(
|
|
294
|
-
join(tmp, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
|
|
295
|
-
"# M001\n- [x] S01: Slice one\n",
|
|
296
|
-
);
|
|
297
|
-
|
|
304
|
+
const wt = setupMergeWorktree(tmp, "M001");
|
|
298
305
|
const s = makeSession({ basePath: wt, originalBasePath: tmp });
|
|
299
306
|
const deps = makeDeps({
|
|
300
307
|
mergeMilestoneToMain: () => { throw new Error("conflict in main"); },
|
|
@@ -318,11 +325,56 @@ describe("worktree journal events", () => {
|
|
|
318
325
|
);
|
|
319
326
|
}
|
|
320
327
|
|
|
328
|
+
new WorktreeLifecycle(s, deps).exitMilestone(
|
|
329
|
+
"M001",
|
|
330
|
+
{ merge: true },
|
|
331
|
+
makeNotifyCtx(),
|
|
332
|
+
);
|
|
333
|
+
|
|
321
334
|
const entries = readJournalEntries(tmp);
|
|
322
|
-
const
|
|
335
|
+
const failures = entries.filter(e => e.eventType === "worktree-merge-failed");
|
|
336
|
+
const failed = failures[0];
|
|
323
337
|
assert.ok(failed, "worktree-merge-failed event should be emitted");
|
|
324
338
|
assert.equal(failed!.data?.milestoneId, "M001");
|
|
325
339
|
assert.equal(failed!.data?.error, "conflict in main");
|
|
340
|
+
assert.equal(failures.length, 1, "duplicate merge failures are journaled once");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("merge failure dedupe uses stable conflict category and expires", (t) => {
|
|
344
|
+
let now = 1_000_000;
|
|
345
|
+
t.mock.method(Date, "now", () => now);
|
|
346
|
+
const wt = setupMergeWorktree(tmp, "M001");
|
|
347
|
+
const s = makeSession({ basePath: wt, originalBasePath: tmp });
|
|
348
|
+
let attempt = 0;
|
|
349
|
+
const deps = makeDeps({
|
|
350
|
+
mergeMilestoneToMain: () => {
|
|
351
|
+
attempt += 1;
|
|
352
|
+
throw new MergeConflictError(
|
|
353
|
+
attempt === 1 ? ["src/a.ts"] : ["src/b.ts", "src/c.ts"],
|
|
354
|
+
"squash",
|
|
355
|
+
"milestone/M001",
|
|
356
|
+
"main",
|
|
357
|
+
);
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
const lifecycle = new WorktreeLifecycle(s, deps);
|
|
361
|
+
|
|
362
|
+
lifecycle.exitMilestone("M001", { merge: true }, makeNotifyCtx());
|
|
363
|
+
lifecycle.exitMilestone("M001", { merge: true }, makeNotifyCtx());
|
|
364
|
+
|
|
365
|
+
let failures = readJournalEntries(tmp).filter(e => e.eventType === "worktree-merge-failed");
|
|
366
|
+
assert.equal(failures.length, 1, "variable conflict filenames should not bypass dedupe");
|
|
367
|
+
assert.match(
|
|
368
|
+
String(failures[0]!.data?.error),
|
|
369
|
+
/src\/a\.ts/,
|
|
370
|
+
"journal payload keeps the original error message",
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
now += 60_001;
|
|
374
|
+
lifecycle.exitMilestone("M001", { merge: true }, makeNotifyCtx());
|
|
375
|
+
|
|
376
|
+
failures = readJournalEntries(tmp).filter(e => e.eventType === "worktree-merge-failed");
|
|
377
|
+
assert.equal(failures.length, 2, "same merge failure is journaled again after dedupe expiry");
|
|
326
378
|
});
|
|
327
379
|
|
|
328
380
|
test("journal entries have valid flowId, seq, and ts fields", () => {
|
|
@@ -26,6 +26,7 @@ const PLANNING_DISPATCH_REVIEW: ToolsPolicy = {
|
|
|
26
26
|
};
|
|
27
27
|
const READ_ONLY: ToolsPolicy = { mode: 'read-only' };
|
|
28
28
|
const ALL: ToolsPolicy = { mode: 'all' };
|
|
29
|
+
const VERIFICATION: ToolsPolicy = { mode: 'verification' };
|
|
29
30
|
const DOCS: ToolsPolicy = {
|
|
30
31
|
mode: 'docs',
|
|
31
32
|
allowedPathGlobs: ['docs/**', 'README.md', 'README.*.md', 'CHANGELOG.md', '*.md'],
|
|
@@ -331,6 +332,36 @@ test('all-mode: execute-task can dispatch subagents', () => {
|
|
|
331
332
|
assert.strictEqual(r.block, false);
|
|
332
333
|
});
|
|
333
334
|
|
|
335
|
+
// ─── verification mode: bash allowed, writes still scoped ─────────────────
|
|
336
|
+
|
|
337
|
+
test('verification-mode: run-uat can run build commands', () => {
|
|
338
|
+
const r = shouldBlockPlanningUnit('bash', 'npm run build 2>&1', BASE, 'run-uat', VERIFICATION);
|
|
339
|
+
assert.strictEqual(r.block, false);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('verification-mode: run-uat blocks destructive bash (rm -rf)', () => {
|
|
343
|
+
const r = shouldBlockPlanningUnit('bash', 'rm -rf dist', BASE, 'run-uat', VERIFICATION);
|
|
344
|
+
assert.strictEqual(r.block, true);
|
|
345
|
+
assert.match(r.reason!, /bash is restricted to build\/test verification commands/);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('verification-mode: run-uat allows read-only investigative bash (git status)', () => {
|
|
349
|
+
const r = shouldBlockPlanningUnit('bash', 'git status', BASE, 'run-uat', VERIFICATION);
|
|
350
|
+
assert.strictEqual(r.block, false);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('verification-mode: run-uat still blocks user source edits', () => {
|
|
354
|
+
const r = shouldBlockPlanningUnit('edit', join(BASE, 'src', 'main.ts'), BASE, 'run-uat', VERIFICATION);
|
|
355
|
+
assert.strictEqual(r.block, true);
|
|
356
|
+
assert.match(r.reason!, /tools-policy "verification"/);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('verification-mode: run-uat still blocks subagent dispatch', () => {
|
|
360
|
+
const r = shouldBlockPlanningUnit('subagent', '', BASE, 'run-uat', VERIFICATION);
|
|
361
|
+
assert.strictEqual(r.block, true);
|
|
362
|
+
assert.match(r.reason!, /subagent dispatch is not permitted/);
|
|
363
|
+
});
|
|
364
|
+
|
|
334
365
|
// ─── read-only mode ───────────────────────────────────────────────────────
|
|
335
366
|
|
|
336
367
|
test('read-only: blocks any edit even to .gsd/', () => {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
upsertTaskPlanning,
|
|
11
11
|
insertGateRow,
|
|
12
12
|
updateSliceStatus,
|
|
13
|
+
setSliceSketchFlag,
|
|
13
14
|
} from "../gsd-db.js";
|
|
14
15
|
import type { GateId } from "../types.js";
|
|
15
16
|
import { invalidateStateCache } from "../state.js";
|
|
@@ -184,6 +185,7 @@ export async function handlePlanSlice(
|
|
|
184
185
|
if (isDeferredStatus(parentSlice.status)) {
|
|
185
186
|
updateSliceStatus(params.milestoneId, params.sliceId, "pending");
|
|
186
187
|
}
|
|
188
|
+
setSliceSketchFlag(params.milestoneId, params.sliceId, false);
|
|
187
189
|
|
|
188
190
|
upsertSlicePlanning(params.milestoneId, params.sliceId, {
|
|
189
191
|
goal: params.goal,
|
|
@@ -132,6 +132,9 @@ export type ContextModePolicy =
|
|
|
132
132
|
* the explicit `allowedPathGlobs` set; Bash safe-allowlist;
|
|
133
133
|
* no subagents. Reserved for rewrite-docs, which legitimately
|
|
134
134
|
* edits project markdown outside .gsd/.
|
|
135
|
+
* - "verification"
|
|
136
|
+
* — Read tools + Bash for verification commands, writes
|
|
137
|
+
* restricted to .gsd/**, no subagents.
|
|
135
138
|
*
|
|
136
139
|
* The allowlist for "docs" is declared per-manifest rather than hardcoded so
|
|
137
140
|
* projects with non-standard doc layouts can extend it without forking the
|
|
@@ -143,7 +146,8 @@ export type ToolsPolicy =
|
|
|
143
146
|
| { readonly mode: "read-only" }
|
|
144
147
|
| { readonly mode: "planning" }
|
|
145
148
|
| { readonly mode: "planning-dispatch"; readonly allowedSubagents: readonly string[] }
|
|
146
|
-
| { readonly mode: "docs"; readonly allowedPathGlobs: readonly string[] }
|
|
149
|
+
| { readonly mode: "docs"; readonly allowedPathGlobs: readonly string[] }
|
|
150
|
+
| { readonly mode: "verification" };
|
|
147
151
|
|
|
148
152
|
// ─── Computed-artifact registry (#4924 v2 contract) ───────────────────────
|
|
149
153
|
|
|
@@ -288,6 +292,7 @@ const COMMON_BUDGET_SMALL = 250_000; // ~65K tokens
|
|
|
288
292
|
|
|
289
293
|
const TOOLS_ALL: ToolsPolicy = { mode: "all" };
|
|
290
294
|
const TOOLS_PLANNING: ToolsPolicy = { mode: "planning" };
|
|
295
|
+
const TOOLS_VERIFICATION: ToolsPolicy = { mode: "verification" };
|
|
291
296
|
// Like TOOLS_PLANNING but permits dispatch to read-only recon/planning
|
|
292
297
|
// specialists. Runtime-enforced by write-gate.ts before the subagent tool runs.
|
|
293
298
|
const TOOLS_PLANNING_DISPATCH_RECON: ToolsPolicy = {
|
|
@@ -406,8 +411,8 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
|
|
|
406
411
|
contextMode: "verification",
|
|
407
412
|
// planning-dispatch: validation is a verification-fan-out unit. It reads
|
|
408
413
|
// the milestone surface and dispatches reviewer/security/tester subagents
|
|
409
|
-
// to report findings without touching user source.
|
|
410
|
-
//
|
|
414
|
+
// to report findings without touching user source. Write isolation to
|
|
415
|
+
// .gsd/ is preserved.
|
|
411
416
|
tools: TOOLS_PLANNING_DISPATCH_REVIEW,
|
|
412
417
|
artifacts: {
|
|
413
418
|
inline: ["roadmap", "slice-summary", "slice-uat", "requirements", "decisions", "templates"],
|
|
@@ -423,11 +428,9 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
|
|
|
423
428
|
codebaseMap: false,
|
|
424
429
|
preferences: "active-only",
|
|
425
430
|
contextMode: "verification",
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
// preserved.
|
|
430
|
-
tools: TOOLS_PLANNING_DISPATCH_REVIEW,
|
|
431
|
+
// Milestone closeout must run unrestricted shell verification commands
|
|
432
|
+
// against the final diff before recording completion.
|
|
433
|
+
tools: TOOLS_ALL,
|
|
431
434
|
artifacts: {
|
|
432
435
|
// #4780 landed slice-summary as excerpt for this unit; phase 2 of
|
|
433
436
|
// the architecture will read this manifest as the source of truth
|
|
@@ -589,7 +592,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
|
|
|
589
592
|
codebaseMap: false,
|
|
590
593
|
preferences: "active-only",
|
|
591
594
|
contextMode: "verification",
|
|
592
|
-
tools:
|
|
595
|
+
tools: TOOLS_VERIFICATION,
|
|
593
596
|
artifacts: {
|
|
594
597
|
// Phase 3 migration (#4782): manifest matches today's actual
|
|
595
598
|
// buildRunUatPrompt inlining. Prior phase-1 entry listed
|
|
@@ -76,6 +76,13 @@ import {
|
|
|
76
76
|
teardownAutoWorktree,
|
|
77
77
|
} from "./auto-worktree.js";
|
|
78
78
|
|
|
79
|
+
const recentWorktreeMergeFailures = new Map<string, number>();
|
|
80
|
+
const MERGE_FAILURE_DEDUPE_MS = 60_000;
|
|
81
|
+
|
|
82
|
+
export function resetRecentWorktreeMergeFailuresForTest(): void {
|
|
83
|
+
recentWorktreeMergeFailures.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
79
86
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
80
87
|
|
|
81
88
|
export interface NotifyCtx {
|
|
@@ -836,6 +843,32 @@ function rebuildGitService(
|
|
|
836
843
|
s.gitService = deps.gitServiceFactory(s.basePath);
|
|
837
844
|
}
|
|
838
845
|
|
|
846
|
+
function emitWorktreeMergeFailedOnce(
|
|
847
|
+
basePath: string,
|
|
848
|
+
milestoneId: string,
|
|
849
|
+
err: unknown,
|
|
850
|
+
): void {
|
|
851
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
852
|
+
const errorCategory = err instanceof Error ? err.name : "Error";
|
|
853
|
+
const now = Date.now();
|
|
854
|
+
const key = `${basePath}\0${milestoneId}\0${errorCategory}`;
|
|
855
|
+
const previous = recentWorktreeMergeFailures.get(key);
|
|
856
|
+
if (previous && now - previous < MERGE_FAILURE_DEDUPE_MS) return;
|
|
857
|
+
for (const [candidate, ts] of recentWorktreeMergeFailures) {
|
|
858
|
+
if (now - ts >= MERGE_FAILURE_DEDUPE_MS) {
|
|
859
|
+
recentWorktreeMergeFailures.delete(candidate);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
emitJournalEvent(basePath, {
|
|
863
|
+
ts: new Date().toISOString(),
|
|
864
|
+
flowId: randomUUID(),
|
|
865
|
+
seq: 0,
|
|
866
|
+
eventType: "worktree-merge-failed",
|
|
867
|
+
data: { milestoneId, error: msg },
|
|
868
|
+
});
|
|
869
|
+
recentWorktreeMergeFailures.set(key, now);
|
|
870
|
+
}
|
|
871
|
+
|
|
839
872
|
// ─── Session-less merge entry (ADR-016 phase 2 / A1) ─────────────────────
|
|
840
873
|
|
|
841
874
|
/**
|
|
@@ -985,13 +1018,7 @@ function _mergeWorktreeModeImpl(
|
|
|
985
1018
|
error: msg,
|
|
986
1019
|
fallback: "chdir-to-project-root",
|
|
987
1020
|
});
|
|
988
|
-
|
|
989
|
-
ts: new Date().toISOString(),
|
|
990
|
-
flowId: randomUUID(),
|
|
991
|
-
seq: 0,
|
|
992
|
-
eventType: "worktree-merge-failed",
|
|
993
|
-
data: { milestoneId, error: msg },
|
|
994
|
-
});
|
|
1021
|
+
emitWorktreeMergeFailedOnce(originalBasePath || worktreeBasePath, milestoneId, err);
|
|
995
1022
|
// Surface a clear, actionable error. Worktree and milestone branch
|
|
996
1023
|
// are intentionally preserved — nothing has been deleted. User can
|
|
997
1024
|
// retry /gsd dispatch complete-milestone or merge manually once the
|