@opengsd/gsd-pi 1.3.0-dev.65546769 → 1.3.0-dev.eed73bea
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 +11 -2
- package/dist/resources/extensions/google-cli/stream-adapter.js +82 -15
- package/dist/resources/extensions/gsd/auto/orchestrator.js +12 -3
- package/dist/resources/extensions/gsd/auto-dispatch.js +17 -14
- package/dist/resources/extensions/gsd/auto-prompts.js +43 -12
- package/dist/resources/extensions/gsd/auto-recovery.js +13 -6
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +103 -13
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +6 -1
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -3
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -19
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +75 -1
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
- package/dist/resources/extensions/gsd/commands-context.js +19 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +16 -10
- package/dist/resources/extensions/gsd/commands-worktree.js +12 -10
- package/dist/resources/extensions/gsd/dashboard-overlay.js +32 -3
- package/dist/resources/extensions/gsd/db/queries.js +60 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +92 -8
- package/dist/resources/extensions/gsd/exec-sandbox.js +45 -9
- package/dist/resources/extensions/gsd/forensics.js +2 -32
- package/dist/resources/extensions/gsd/git-service.js +4 -4
- package/dist/resources/extensions/gsd/guided-flow-queue.js +59 -5
- package/dist/resources/extensions/gsd/health-widget.js +55 -29
- package/dist/resources/extensions/gsd/markdown-renderer.js +6 -2
- package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +44 -21
- package/dist/resources/extensions/gsd/milestone-implementation-evidence.js +26 -20
- package/dist/resources/extensions/gsd/quick.js +45 -2
- package/dist/resources/extensions/gsd/session-forensics.js +11 -1
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +52 -3
- package/dist/resources/extensions/gsd/tools/complete-slice.js +34 -3
- package/dist/resources/extensions/gsd/tools/complete-task.js +78 -16
- package/dist/resources/extensions/gsd/tools/exec-tool.js +7 -2
- package/dist/resources/extensions/gsd/unit-context-composer.js +23 -7
- package/dist/resources/extensions/gsd/unit-registry.js +25 -3
- package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +33 -3
- package/dist/resources/extensions/gsd/validation-block-guard.js +9 -4
- package/dist/resources/extensions/gsd/workspace-git-preflight.js +30 -1
- 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 +14 -14
- 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/react-loadable-manifest.json +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.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/api/visualizer/route.js +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 +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-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/dist/web/standalone/.next/static/chunks/{796.e0bdc932325d7e03.js → 796.3976108148518f7d.js} +3 -3
- package/dist/web/standalone/.next/static/chunks/{webpack-f46ea08200a0227e.js → webpack-7c1d97e39be2da11.js} +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/dist/workflow.d.ts +1 -0
- package/packages/contracts/dist/workflow.d.ts.map +1 -1
- package/packages/contracts/dist/workflow.js +2 -0
- package/packages/contracts/dist/workflow.js.map +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +21 -9
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/README.md +1 -1
- package/packages/mcp-server/dist/server.d.ts +1 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +3 -3
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +13 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +34 -20
- package/packages/mcp-server/dist/workflow-tools.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/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 +20 -2
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +80 -0
- package/src/resources/extensions/google-cli/stream-adapter.ts +106 -19
- package/src/resources/extensions/gsd/auto/orchestrator.ts +25 -11
- package/src/resources/extensions/gsd/auto-dispatch.ts +18 -17
- package/src/resources/extensions/gsd/auto-prompts.ts +54 -12
- package/src/resources/extensions/gsd/auto-recovery.ts +13 -6
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +125 -12
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +6 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -3
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +52 -18
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +82 -1
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
- package/src/resources/extensions/gsd/commands-context.ts +18 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -9
- package/src/resources/extensions/gsd/commands-worktree.ts +12 -10
- package/src/resources/extensions/gsd/dashboard-overlay.ts +32 -3
- package/src/resources/extensions/gsd/db/queries.ts +79 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +103 -9
- package/src/resources/extensions/gsd/exec-sandbox.ts +49 -9
- package/src/resources/extensions/gsd/forensics.ts +2 -33
- package/src/resources/extensions/gsd/git-service.ts +5 -5
- package/src/resources/extensions/gsd/guided-flow-queue.ts +82 -4
- package/src/resources/extensions/gsd/health-widget.ts +69 -32
- package/src/resources/extensions/gsd/markdown-renderer.ts +6 -1
- package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +51 -19
- package/src/resources/extensions/gsd/milestone-implementation-evidence.ts +35 -21
- package/src/resources/extensions/gsd/quick.ts +43 -2
- package/src/resources/extensions/gsd/session-forensics.ts +11 -1
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +76 -8
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +111 -1
- package/src/resources/extensions/gsd/tests/commands-context.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +80 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +48 -8
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +55 -2
- package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +26 -1
- package/src/resources/extensions/gsd/tests/doctor-forensics-db-open-regression.test.ts +70 -2
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/exec-graceful-kill.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/exec-tool.test.ts +45 -1
- package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +268 -3
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +119 -1
- package/src/resources/extensions/gsd/tests/integration/queue-active-milestone-context-budget.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/integration/quick-branch-lifecycle.test.ts +56 -9
- package/src/resources/extensions/gsd/tests/knowledge-cold-start.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/memory-consolidation-scanner.test.ts +78 -0
- package/src/resources/extensions/gsd/tests/orchestrator-logs.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +54 -1
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +195 -1
- package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +193 -14
- package/src/resources/extensions/gsd/tests/unmerged-milestone-guard.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +151 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +30 -3
- package/src/resources/extensions/gsd/tools/complete-task.ts +86 -16
- package/src/resources/extensions/gsd/tools/exec-tool.ts +7 -3
- package/src/resources/extensions/gsd/unit-context-composer.ts +33 -7
- package/src/resources/extensions/gsd/unit-registry.ts +25 -3
- package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +41 -5
- package/src/resources/extensions/gsd/validation-block-guard.ts +13 -7
- package/src/resources/extensions/gsd/workspace-git-preflight.ts +31 -0
- /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_ssgManifest.js +0 -0
|
@@ -19,6 +19,16 @@ import { resolveTasksDir } from "./paths.js";
|
|
|
19
19
|
|
|
20
20
|
/** Large enough for unbounded milestone-history git log scans in big repos. */
|
|
21
21
|
const GIT_LOG_MAX_BUFFER = 16 * 1024 * 1024;
|
|
22
|
+
const LOG_FIELD_SEPARATOR = "\x1f";
|
|
23
|
+
const LOG_RECORD_SEPARATOR = "\x1e";
|
|
24
|
+
|
|
25
|
+
type CommitRecord = {
|
|
26
|
+
hash: string;
|
|
27
|
+
parents: string;
|
|
28
|
+
committedAt: string;
|
|
29
|
+
message: string;
|
|
30
|
+
files: string[];
|
|
31
|
+
};
|
|
22
32
|
|
|
23
33
|
/**
|
|
24
34
|
* Check whether a milestone produced implementation artifacts (non-`.gsd/`
|
|
@@ -198,7 +208,7 @@ function getChangedFilesFromMilestoneTaggedCommits(
|
|
|
198
208
|
// Primary: path-scoped log against .gsd/milestones/<id>. Fast and unbounded
|
|
199
209
|
// by depth when .gsd/ is tracked in git.
|
|
200
210
|
const scoped = scanGsdTaggedCommits(basePath, milestoneId, [
|
|
201
|
-
"log", "--format=%H%x1f%B%
|
|
211
|
+
"log", "--full-diff", "--name-only", "--format=%x1e%H%x1f%B%x1f", "HEAD", "--", `.gsd/milestones/${milestoneId}`,
|
|
202
212
|
]);
|
|
203
213
|
if (!scoped.ok) return scoped;
|
|
204
214
|
if (scoped.matched && classifyImplementationFiles(scoped.files) === "present") return scoped;
|
|
@@ -213,7 +223,7 @@ function getChangedFilesFromMilestoneTaggedCommits(
|
|
|
213
223
|
// reintroducing the rolling-depth failure class removed in #4699 where
|
|
214
224
|
// milestone evidence aged out behind unrelated activity.
|
|
215
225
|
const unscoped = scanGsdTaggedCommits(basePath, milestoneId, [
|
|
216
|
-
"log", "--format=%H%x1f%B%
|
|
226
|
+
"log", "--name-only", "--format=%x1e%H%x1f%B%x1f", "HEAD",
|
|
217
227
|
]);
|
|
218
228
|
if (!unscoped.ok) return scoped.matched ? scoped : unscoped;
|
|
219
229
|
if (!unscoped.matched) return scoped;
|
|
@@ -299,8 +309,7 @@ function backfillChangedFilesFromUntaggedMilestoneCommits(
|
|
|
299
309
|
if (record.parents.trim().split(/\s+/).filter(Boolean).length > 1) continue;
|
|
300
310
|
if (commitMessageHasGsdTrailer(record.message)) continue;
|
|
301
311
|
|
|
302
|
-
const
|
|
303
|
-
const implementationFiles = commitFiles.map(normalizeRepoPath).filter(isImplementationPath);
|
|
312
|
+
const implementationFiles = record.files.map(normalizeRepoPath).filter(isImplementationPath);
|
|
304
313
|
if (implementationFiles.length === 0) continue;
|
|
305
314
|
if (!implementationFiles.some((file) => hintSet.has(file))) continue;
|
|
306
315
|
|
|
@@ -323,25 +332,30 @@ function backfillChangedFilesFromUntaggedMilestoneCommits(
|
|
|
323
332
|
}
|
|
324
333
|
}
|
|
325
334
|
|
|
326
|
-
function getCommitRecords(basePath: string):
|
|
327
|
-
const logOutput = execFileSync("git", ["log", "--format=%H%x1f%P%x1f%cI%x1f%B%
|
|
335
|
+
function getCommitRecords(basePath: string): CommitRecord[] {
|
|
336
|
+
const logOutput = execFileSync("git", ["log", "--name-only", "--format=%x1e%H%x1f%P%x1f%cI%x1f%B%x1f", "HEAD"], {
|
|
328
337
|
cwd: basePath,
|
|
329
338
|
stdio: ["ignore", "pipe", "pipe"],
|
|
330
339
|
encoding: "utf-8",
|
|
331
340
|
maxBuffer: GIT_LOG_MAX_BUFFER,
|
|
332
341
|
});
|
|
333
342
|
return logOutput
|
|
334
|
-
.split(
|
|
335
|
-
.map((record) => record.trim())
|
|
343
|
+
.split(LOG_RECORD_SEPARATOR)
|
|
336
344
|
.filter(Boolean)
|
|
337
345
|
.flatMap((record) => {
|
|
338
|
-
const parts = record.split(
|
|
339
|
-
if (parts.length <
|
|
340
|
-
const [hash, parents, committedAt
|
|
341
|
-
|
|
346
|
+
const parts = record.split(LOG_FIELD_SEPARATOR);
|
|
347
|
+
if (parts.length < 5) return [];
|
|
348
|
+
const [hash, parents, committedAt] = parts;
|
|
349
|
+
const files = parseNameOnlyFiles(parts.at(-1) ?? "");
|
|
350
|
+
const message = parts.slice(3, -1).join(LOG_FIELD_SEPARATOR);
|
|
351
|
+
return [{ hash: hash.trim(), parents: parents.trim(), committedAt: committedAt.trim(), message, files }];
|
|
342
352
|
});
|
|
343
353
|
}
|
|
344
354
|
|
|
355
|
+
function parseNameOnlyFiles(rawFiles: string): string[] {
|
|
356
|
+
return rawFiles.split(/\r?\n/).map((file) => file.trim()).filter(Boolean);
|
|
357
|
+
}
|
|
358
|
+
|
|
345
359
|
function isFullCommitSha(value: string): boolean {
|
|
346
360
|
return /^[0-9a-f]{40}$/i.test(value);
|
|
347
361
|
}
|
|
@@ -359,23 +373,23 @@ function scanGsdTaggedCommits(
|
|
|
359
373
|
maxBuffer: GIT_LOG_MAX_BUFFER,
|
|
360
374
|
});
|
|
361
375
|
const records = logOutput
|
|
362
|
-
.split(
|
|
363
|
-
.map((record) => record.trim())
|
|
376
|
+
.split(LOG_RECORD_SEPARATOR)
|
|
364
377
|
.filter(Boolean)
|
|
365
378
|
.flatMap((record) => {
|
|
366
|
-
const
|
|
367
|
-
if (
|
|
368
|
-
const hash =
|
|
369
|
-
|
|
370
|
-
|
|
379
|
+
const parts = record.split(LOG_FIELD_SEPARATOR);
|
|
380
|
+
if (parts.length < 3) return [];
|
|
381
|
+
const hash = parts[0].trim();
|
|
382
|
+
if (!hash) return [];
|
|
383
|
+
const files = parseNameOnlyFiles(parts.at(-1) ?? "");
|
|
384
|
+
const message = parts.slice(1, -1).join(LOG_FIELD_SEPARATOR);
|
|
385
|
+
return [{ message, files }];
|
|
371
386
|
});
|
|
372
387
|
|
|
373
388
|
const files = new Set<string>();
|
|
374
389
|
let matched = false;
|
|
375
|
-
for (const {
|
|
390
|
+
for (const { message, files: commitFiles } of records) {
|
|
376
391
|
if (!commitMessageHasGsdTrailer(message)) continue;
|
|
377
392
|
|
|
378
|
-
const commitFiles = getChangedFilesForCommit(basePath, hash);
|
|
379
393
|
if (!commitMatchesMilestone(basePath, message, milestoneId, commitFiles)) continue;
|
|
380
394
|
|
|
381
395
|
matched = true;
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
-
import { isAbsolute, join, relative } from "node:path";
|
|
13
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
15
15
|
import { QUICK_BRANCH_RE } from "./branch-patterns.js";
|
|
16
16
|
import { loadPrompt } from "./prompt-loader.js";
|
|
17
17
|
import { gsdRoot } from "./paths.js";
|
|
@@ -31,6 +31,7 @@ interface QuickReturnState {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
let pendingQuickReturn: QuickReturnState | null = null;
|
|
34
|
+
const pendingQuickReturnMisses = new Map<string, string>();
|
|
34
35
|
|
|
35
36
|
// ─── Quick Task Helpers ───────────────────────────────────────────────────────
|
|
36
37
|
|
|
@@ -189,12 +190,35 @@ export function buildQuickCommitInstruction(basePath: string, root: string): str
|
|
|
189
190
|
].join("\n");
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
function readHeadBranchName(basePath: string): string | null {
|
|
194
|
+
try {
|
|
195
|
+
const gitPath = join(basePath, ".git");
|
|
196
|
+
if (!existsSync(gitPath)) return null;
|
|
197
|
+
|
|
198
|
+
let headPath: string;
|
|
199
|
+
if (lstatSync(gitPath).isDirectory()) {
|
|
200
|
+
headPath = join(gitPath, "HEAD");
|
|
201
|
+
} else {
|
|
202
|
+
const gitFile = readFileSync(gitPath, "utf-8").trim();
|
|
203
|
+
if (!gitFile.startsWith("gitdir: ")) return null;
|
|
204
|
+
headPath = join(resolve(basePath, gitFile.slice("gitdir: ".length)), "HEAD");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const head = readFileSync(headPath, "utf-8").trim();
|
|
208
|
+
if (!head.startsWith("ref: refs/heads/")) return null;
|
|
209
|
+
return head.slice("ref: refs/heads/".length);
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
192
215
|
function quickReturnStatePath(basePath: string): string {
|
|
193
216
|
return join(gsdRoot(basePath), "runtime", "quick-return.json");
|
|
194
217
|
}
|
|
195
218
|
|
|
196
219
|
function persistPendingReturn(state: QuickReturnState): void {
|
|
197
220
|
pendingQuickReturn = state;
|
|
221
|
+
pendingQuickReturnMisses.delete(state.basePath);
|
|
198
222
|
mkdirSync(join(gsdRoot(state.basePath), "runtime"), { recursive: true });
|
|
199
223
|
writeFileSync(quickReturnStatePath(state.basePath), JSON.stringify(state) + "\n", "utf-8");
|
|
200
224
|
}
|
|
@@ -203,6 +227,13 @@ function readPendingReturn(basePath: string): QuickReturnState | null {
|
|
|
203
227
|
if (pendingQuickReturn && pendingQuickReturn.basePath === basePath) {
|
|
204
228
|
return pendingQuickReturn;
|
|
205
229
|
}
|
|
230
|
+
if (pendingQuickReturnMisses.has(basePath)) {
|
|
231
|
+
const statePath = quickReturnStatePath(basePath);
|
|
232
|
+
if (!existsSync(statePath) && readHeadBranchName(basePath) === pendingQuickReturnMisses.get(basePath)) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
pendingQuickReturnMisses.delete(basePath);
|
|
236
|
+
}
|
|
206
237
|
|
|
207
238
|
try {
|
|
208
239
|
const raw = readFileSync(quickReturnStatePath(basePath), "utf-8");
|
|
@@ -216,6 +247,7 @@ function readPendingReturn(basePath: string): QuickReturnState | null {
|
|
|
216
247
|
&& typeof parsed.description === "string"
|
|
217
248
|
) {
|
|
218
249
|
pendingQuickReturn = parsed as QuickReturnState;
|
|
250
|
+
pendingQuickReturnMisses.delete(basePath);
|
|
219
251
|
return pendingQuickReturn;
|
|
220
252
|
}
|
|
221
253
|
} catch {
|
|
@@ -225,9 +257,14 @@ function readPendingReturn(basePath: string): QuickReturnState | null {
|
|
|
225
257
|
const inferred = inferQuickReturnFromBranch(basePath);
|
|
226
258
|
if (inferred) {
|
|
227
259
|
pendingQuickReturn = inferred;
|
|
260
|
+
pendingQuickReturnMisses.delete(basePath);
|
|
228
261
|
return inferred;
|
|
229
262
|
}
|
|
230
263
|
|
|
264
|
+
const branchAtMiss = readHeadBranchName(basePath);
|
|
265
|
+
if (branchAtMiss) {
|
|
266
|
+
pendingQuickReturnMisses.set(basePath, branchAtMiss);
|
|
267
|
+
}
|
|
231
268
|
return null;
|
|
232
269
|
}
|
|
233
270
|
|
|
@@ -235,6 +272,10 @@ function clearPendingReturn(basePath: string): void {
|
|
|
235
272
|
if (pendingQuickReturn?.basePath === basePath) {
|
|
236
273
|
pendingQuickReturn = null;
|
|
237
274
|
}
|
|
275
|
+
const branchAtMiss = readHeadBranchName(basePath);
|
|
276
|
+
if (branchAtMiss) {
|
|
277
|
+
pendingQuickReturnMisses.set(basePath, branchAtMiss);
|
|
278
|
+
}
|
|
238
279
|
rmSync(quickReturnStatePath(basePath), { force: true });
|
|
239
280
|
}
|
|
240
281
|
|
|
@@ -223,11 +223,21 @@ export function extractTrace(entries: unknown[]): ExecutionTrace {
|
|
|
223
223
|
|
|
224
224
|
// Flush any pending tool calls that never got results (crash mid-tool)
|
|
225
225
|
for (const [, pending] of pendingTools) {
|
|
226
|
+
const missingResultError = `Tool call ${pending.name} started but no toolResult was recorded`;
|
|
226
227
|
toolCalls.push({
|
|
227
228
|
name: pending.name,
|
|
228
229
|
input: redactInput(pending.name, pending.input),
|
|
229
|
-
|
|
230
|
+
result: "missing tool result (stream/tool-call abort before execution)",
|
|
231
|
+
isError: true,
|
|
230
232
|
});
|
|
233
|
+
errors.push(missingResultError);
|
|
234
|
+
|
|
235
|
+
// Mark the matching commandsRun entry as failed so it is consistent with
|
|
236
|
+
// the isError: true on the tool call above (bash/bg_shell only).
|
|
237
|
+
if (pending.name === "bash" || pending.name === "bg_shell") {
|
|
238
|
+
const lastCmd = findLast(commandsRun, c => c.command === String(pending.input.command));
|
|
239
|
+
if (lastCmd) lastCmd.failed = true;
|
|
240
|
+
}
|
|
231
241
|
}
|
|
232
242
|
|
|
233
243
|
return {
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
// had zero callers in production code — wiring it through
|
|
7
7
|
// reconcileBeforeDispatch closes that gap.
|
|
8
8
|
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
9
12
|
import {
|
|
10
13
|
detectStaleRenders,
|
|
11
14
|
renderPlanCheckboxes,
|
|
@@ -13,8 +16,20 @@ import {
|
|
|
13
16
|
renderSliceSummary,
|
|
14
17
|
renderTaskSummary,
|
|
15
18
|
} from "../../markdown-renderer.js";
|
|
16
|
-
import {
|
|
17
|
-
|
|
19
|
+
import {
|
|
20
|
+
getMilestone,
|
|
21
|
+
getMilestoneSlices,
|
|
22
|
+
getSlice,
|
|
23
|
+
getSliceTasks,
|
|
24
|
+
isDbAvailable,
|
|
25
|
+
setSliceSummaryMd,
|
|
26
|
+
} from "../../gsd-db.js";
|
|
27
|
+
import {
|
|
28
|
+
getWorkflowDatabasePath,
|
|
29
|
+
openWorkflowDatabasePath,
|
|
30
|
+
refreshWorkflowDatabaseFromDisk,
|
|
31
|
+
} from "../../db-workspace.js";
|
|
32
|
+
import { gsdRoot, resolveSliceFile } from "../../paths.js";
|
|
18
33
|
import type { GSDState } from "../../types.js";
|
|
19
34
|
import { logWarning } from "../../workflow-logger.js";
|
|
20
35
|
import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
|
|
@@ -107,10 +122,44 @@ function resolveRoadmapMilestoneIdFromPath(normPath: string): string {
|
|
|
107
122
|
return fileMatch?.[1] ?? milestoneMatch[1];
|
|
108
123
|
}
|
|
109
124
|
|
|
125
|
+
function expectedDbPathForStaleRenderRepair(basePath: string): string {
|
|
126
|
+
return join(gsdRoot(basePath), "gsd.db");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function ensureDbForStaleRenderRepair(basePath: string): boolean {
|
|
130
|
+
const dbPath = expectedDbPathForStaleRenderRepair(basePath);
|
|
131
|
+
if (isDbAvailable() && getWorkflowDatabasePath() === dbPath) return true;
|
|
132
|
+
if (!existsSync(dbPath)) return false;
|
|
133
|
+
try {
|
|
134
|
+
return openWorkflowDatabasePath(dbPath);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
logWarning("reconcile", `stale-render repair could not reopen DB: ${(err as Error).message}`);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function retryDbForStaleRenderRepair(basePath: string): boolean {
|
|
142
|
+
const dbPath = expectedDbPathForStaleRenderRepair(basePath);
|
|
143
|
+
if (!existsSync(dbPath)) return false;
|
|
144
|
+
try {
|
|
145
|
+
if (isDbAvailable() && getWorkflowDatabasePath() === dbPath && refreshWorkflowDatabaseFromDisk()) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
return openWorkflowDatabasePath(dbPath);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
logWarning("reconcile", `stale-render repair could not reopen DB: ${(err as Error).message}`);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
110
155
|
async function repairStaleRenderFromBasePath(
|
|
111
156
|
record: StaleRenderDrift,
|
|
112
157
|
basePath: string,
|
|
113
158
|
): Promise<void> {
|
|
159
|
+
if (!ensureDbForStaleRenderRepair(basePath)) {
|
|
160
|
+
throw new Error(`stale-render drift: database unavailable for repair (${basePath})`);
|
|
161
|
+
}
|
|
162
|
+
|
|
114
163
|
const normPath = record.renderPath.replace(/\\/g, "/");
|
|
115
164
|
const reason = record.reason;
|
|
116
165
|
|
|
@@ -136,12 +185,31 @@ async function repairStaleRenderFromBasePath(
|
|
|
136
185
|
const sliceId = pathMatch[2] && pathMatch[3] && /^\d+$/.test(pathMatch[2])
|
|
137
186
|
? `S${String(parseInt(pathMatch[3]!, 10)).padStart(2, "0")}`
|
|
138
187
|
: pathMatch[2]!;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
188
|
+
let wrote = false;
|
|
189
|
+
try {
|
|
190
|
+
wrote = await renderPlanCheckboxes(
|
|
191
|
+
basePath,
|
|
192
|
+
milestoneId,
|
|
193
|
+
sliceId,
|
|
194
|
+
record.renderPath,
|
|
195
|
+
);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (!retryDbForStaleRenderRepair(basePath)) throw err;
|
|
198
|
+
wrote = await renderPlanCheckboxes(
|
|
199
|
+
basePath,
|
|
200
|
+
milestoneId,
|
|
201
|
+
sliceId,
|
|
202
|
+
record.renderPath,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (!wrote && retryDbForStaleRenderRepair(basePath)) {
|
|
206
|
+
wrote = await renderPlanCheckboxes(
|
|
207
|
+
basePath,
|
|
208
|
+
milestoneId,
|
|
209
|
+
sliceId,
|
|
210
|
+
record.renderPath,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
145
213
|
if (!wrote) {
|
|
146
214
|
throw new Error(
|
|
147
215
|
`stale-render drift: plan re-render wrote nothing for ${milestoneId}/${pathMatch[2]} ` +
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// File Purpose: Tests auto-mode artifact verification and recovery behavior.
|
|
3
3
|
import test, { afterEach } from "node:test";
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { chmodSync, mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
@@ -964,6 +964,45 @@ function makeGitBase(): string {
|
|
|
964
964
|
return base;
|
|
965
965
|
}
|
|
966
966
|
|
|
967
|
+
function shellQuote(value: string): string {
|
|
968
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function withLoggedGitCommands<T>(base: string, action: () => T): { result: T; commands: string[] } {
|
|
972
|
+
const realGit = execFileSync("which", ["git"], {
|
|
973
|
+
encoding: "utf-8",
|
|
974
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
975
|
+
}).trim().split(/\r?\n/)[0];
|
|
976
|
+
if (!realGit) throw new Error("Unable to resolve git executable for invocation logging test");
|
|
977
|
+
const binDir = join(base, ".git-wrapper-bin");
|
|
978
|
+
const logFile = join(base, "git-invocations.log");
|
|
979
|
+
mkdirSync(binDir, { recursive: true });
|
|
980
|
+
const wrapper = join(binDir, "git");
|
|
981
|
+
writeFileSync(
|
|
982
|
+
wrapper,
|
|
983
|
+
[
|
|
984
|
+
"#!/usr/bin/env bash",
|
|
985
|
+
`printf '%s\\n' "$1" >> ${shellQuote(logFile)}`,
|
|
986
|
+
`exec ${shellQuote(realGit)} "$@"`,
|
|
987
|
+
"",
|
|
988
|
+
].join("\n"),
|
|
989
|
+
);
|
|
990
|
+
chmodSync(wrapper, 0o755);
|
|
991
|
+
|
|
992
|
+
const originalPath = process.env.PATH;
|
|
993
|
+
process.env.PATH = originalPath ? `${binDir}:${originalPath}` : binDir;
|
|
994
|
+
try {
|
|
995
|
+
const result = action();
|
|
996
|
+
const commands = existsSync(logFile)
|
|
997
|
+
? readFileSync(logFile, "utf-8").split(/\r?\n/).filter(Boolean)
|
|
998
|
+
: [];
|
|
999
|
+
return { result, commands };
|
|
1000
|
+
} finally {
|
|
1001
|
+
if (originalPath === undefined) delete process.env.PATH;
|
|
1002
|
+
else process.env.PATH = originalPath;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
967
1006
|
test("hasImplementationArtifacts returns false when only .gsd/ files committed (#1703)", () => {
|
|
968
1007
|
const base = makeGitBase();
|
|
969
1008
|
try {
|
|
@@ -1344,6 +1383,77 @@ test("hasImplementationArtifacts finds implementation commits when .gsd/ is giti
|
|
|
1344
1383
|
}
|
|
1345
1384
|
});
|
|
1346
1385
|
|
|
1386
|
+
test("hasImplementationArtifacts scans GSD-tagged history without per-commit diff-tree forks (#892)", { skip: process.platform === "win32" }, () => {
|
|
1387
|
+
const base = makeGitBase();
|
|
1388
|
+
try {
|
|
1389
|
+
writeFileSync(join(base, ".git", "info", "exclude"), ".gsd/\n");
|
|
1390
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
|
|
1391
|
+
writeFileSync(
|
|
1392
|
+
join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-SUMMARY.md"),
|
|
1393
|
+
"# Summary",
|
|
1394
|
+
);
|
|
1395
|
+
|
|
1396
|
+
mkdirSync(join(base, "src"), { recursive: true });
|
|
1397
|
+
for (let i = 0; i < 3; i++) {
|
|
1398
|
+
writeFileSync(join(base, "src", `feature-${i}.ts`), `export const feature${i} = true;\n`);
|
|
1399
|
+
execFileSync("git", ["add", "src"], { cwd: base, stdio: "ignore" });
|
|
1400
|
+
execFileSync(
|
|
1401
|
+
"git",
|
|
1402
|
+
["commit", "-m", `feat: materialize M001 evidence ${i}\n\nGSD-Task: S01/T01`],
|
|
1403
|
+
{ cwd: base, stdio: "ignore" },
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const { result, commands } = withLoggedGitCommands(base, () => hasImplementationArtifacts(base, "M001"));
|
|
1408
|
+
assert.equal(result, "present", "milestone-tagged commits should still prove implementation evidence");
|
|
1409
|
+
assert.ok(commands.includes("log"), "milestone evidence fallback should scan history with git log");
|
|
1410
|
+
assert.equal(commands.filter((command) => command === "diff-tree").length, 0);
|
|
1411
|
+
} finally {
|
|
1412
|
+
cleanup(base);
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
test("hasImplementationArtifacts backfill scans commit records without per-commit diff-tree forks (#892)", { skip: process.platform === "win32" }, () => {
|
|
1417
|
+
const base = makeGitBase();
|
|
1418
|
+
try {
|
|
1419
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
1420
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
1421
|
+
insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
|
|
1422
|
+
insertSlice({
|
|
1423
|
+
id: "S01",
|
|
1424
|
+
milestoneId: "M001",
|
|
1425
|
+
title: "Slice One",
|
|
1426
|
+
status: "complete",
|
|
1427
|
+
risk: "low",
|
|
1428
|
+
depends: [],
|
|
1429
|
+
});
|
|
1430
|
+
insertTask({
|
|
1431
|
+
id: "T01",
|
|
1432
|
+
sliceId: "S01",
|
|
1433
|
+
milestoneId: "M001",
|
|
1434
|
+
title: "Task One",
|
|
1435
|
+
status: "complete",
|
|
1436
|
+
keyFiles: ["src/expected-0.ts", "src/expected-1.ts"],
|
|
1437
|
+
planning: { files: ["src/expected-0.ts", "src/expected-1.ts"] },
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
mkdirSync(join(base, "src"), { recursive: true });
|
|
1441
|
+
for (let i = 0; i < 2; i++) {
|
|
1442
|
+
writeFileSync(join(base, "src", `expected-${i}.ts`), `export const expected${i} = true;\n`);
|
|
1443
|
+
execFileSync("git", ["add", "src"], { cwd: base, stdio: "ignore" });
|
|
1444
|
+
execFileSync("git", ["commit", "-m", `feat: untagged implementation ${i}`], { cwd: base, stdio: "ignore" });
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const { result, commands } = withLoggedGitCommands(base, () => hasImplementationArtifacts(base, "M001"));
|
|
1448
|
+
assert.equal(result, "present", "completed task file hints should still backfill untagged commits");
|
|
1449
|
+
assert.equal(getMilestoneCommitAttributionShas("M001").length, 2);
|
|
1450
|
+
assert.ok(commands.includes("log"), "backfill should scan commit records with git log");
|
|
1451
|
+
assert.equal(commands.filter((command) => command === "diff-tree").length, 0);
|
|
1452
|
+
} finally {
|
|
1453
|
+
cleanup(base);
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1347
1457
|
test("hasImplementationArtifacts binds GSD-Task trailer to milestone via DB state when .gsd/ is gitignored", () => {
|
|
1348
1458
|
const base = makeGitBase();
|
|
1349
1459
|
try {
|
|
@@ -101,6 +101,32 @@ test("analyzeSessionContext buckets injections, tool results, and loaded skills"
|
|
|
101
101
|
assert.equal(result.subagentSpawns, 1);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
test("analyzeSessionContext bounds large assistant tool-call arguments", () => {
|
|
105
|
+
const result = analyzeSessionContext(sessionEntries({
|
|
106
|
+
role: "assistant",
|
|
107
|
+
content: [
|
|
108
|
+
{
|
|
109
|
+
type: "toolCall",
|
|
110
|
+
id: "tc-save",
|
|
111
|
+
name: "gsd_summary_save",
|
|
112
|
+
arguments: {
|
|
113
|
+
path: ".gsd/milestones/M001/M001-SUMMARY.md",
|
|
114
|
+
artifact_type: "SUMMARY",
|
|
115
|
+
content: "x".repeat(20_000),
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
timestamp: TS,
|
|
120
|
+
}), PROVIDER);
|
|
121
|
+
|
|
122
|
+
const assistant = result.conversationSections.find((section) => section.label === "Assistant responses");
|
|
123
|
+
assert.ok(assistant);
|
|
124
|
+
assert.ok(
|
|
125
|
+
assistant.tokens < 200,
|
|
126
|
+
`assistant tool-call arguments should be redacted before token counting, got ${assistant.tokens} tokens`,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
104
130
|
test("formatContextReport lists skills and subagents", () => {
|
|
105
131
|
const report = buildContextBreakdown({
|
|
106
132
|
modelLabel: "claude-code/claude-sonnet-4-6",
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
3
7
|
|
|
4
8
|
import {
|
|
5
9
|
formatCleanKeepReason,
|
|
10
|
+
handleWorktree,
|
|
6
11
|
type WorktreeStatus,
|
|
7
12
|
} from "../commands-worktree.ts";
|
|
13
|
+
import { withCommandCwd } from "../commands/context.ts";
|
|
14
|
+
import { createWorktree } from "../worktree-manager.ts";
|
|
15
|
+
import {
|
|
16
|
+
disableDebug,
|
|
17
|
+
enableDebug,
|
|
18
|
+
getDebugCounters,
|
|
19
|
+
} from "../debug-logger.ts";
|
|
8
20
|
|
|
9
21
|
function mkStatus(over: Partial<WorktreeStatus>): WorktreeStatus {
|
|
10
22
|
const name = over.name ?? "feat-x";
|
|
@@ -22,6 +34,45 @@ function mkStatus(over: Partial<WorktreeStatus>): WorktreeStatus {
|
|
|
22
34
|
};
|
|
23
35
|
}
|
|
24
36
|
|
|
37
|
+
function git(cwd: string, args: string[]): string {
|
|
38
|
+
return execFileSync("git", args, {
|
|
39
|
+
cwd,
|
|
40
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
}).trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeRepo(): string {
|
|
46
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-worktree-command-"));
|
|
47
|
+
git(base, ["init", "-b", "main"]);
|
|
48
|
+
git(base, ["config", "user.name", "Test User"]);
|
|
49
|
+
git(base, ["config", "user.email", "test@example.com"]);
|
|
50
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
51
|
+
writeFileSync(join(base, "README.md"), "# Test\n", "utf-8");
|
|
52
|
+
git(base, ["add", "."]);
|
|
53
|
+
git(base, ["commit", "-m", "chore: init"]);
|
|
54
|
+
return base;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createCommittedWorktree(base: string, name: string): void {
|
|
58
|
+
const wt = createWorktree(base, name);
|
|
59
|
+
writeFileSync(join(wt.path, `${name}.txt`), `${name}\n`, "utf-8");
|
|
60
|
+
git(wt.path, ["add", "."]);
|
|
61
|
+
git(wt.path, ["commit", "-m", `feat: ${name}`]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createMockCtx() {
|
|
65
|
+
const notifications: { message: string; level: string }[] = [];
|
|
66
|
+
return {
|
|
67
|
+
notifications,
|
|
68
|
+
ui: {
|
|
69
|
+
notify(message: string, level: string) {
|
|
70
|
+
notifications.push({ message, level });
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
25
76
|
test("clean keep reason shows uncommitted-only worktrees clearly", () => {
|
|
26
77
|
const reason = formatCleanKeepReason(mkStatus({ uncommitted: true }));
|
|
27
78
|
assert.equal(reason, "uncommitted changes");
|
|
@@ -46,3 +97,32 @@ test("clean keep reason uses singular form for a single changed file", () => {
|
|
|
46
97
|
const reason = formatCleanKeepReason(mkStatus({ filesChanged: 1, uncommitted: false }));
|
|
47
98
|
assert.equal(reason, "1 changed file");
|
|
48
99
|
});
|
|
100
|
+
|
|
101
|
+
test("worktree list detects main branch once for the command", async (t) => {
|
|
102
|
+
if (process.env.GSD_ENABLE_NATIVE_GSD_GIT === "1") {
|
|
103
|
+
t.skip("git invocation regression is specific to the CLI fallback path");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const base = makeRepo();
|
|
108
|
+
try {
|
|
109
|
+
createCommittedWorktree(base, "feature-a");
|
|
110
|
+
createCommittedWorktree(base, "feature-b");
|
|
111
|
+
|
|
112
|
+
const ctx = createMockCtx();
|
|
113
|
+
enableDebug(base);
|
|
114
|
+
try {
|
|
115
|
+
await withCommandCwd(base, async () => {
|
|
116
|
+
await handleWorktree("list", ctx as any);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
assert.equal(ctx.notifications.length, 1);
|
|
120
|
+
assert.match(ctx.notifications[0].message, /Worktrees — 2/);
|
|
121
|
+
assert.equal(getDebugCounters().gitInvocations, 12);
|
|
122
|
+
} finally {
|
|
123
|
+
disableDebug();
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
rmSync(base, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
@@ -203,6 +203,10 @@ console.log('\n=== complete-slice: handler happy path ===');
|
|
|
203
203
|
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second Slice', risk: 'low', depends: ['S01'], demo: 'advanced stuff', sequence: 2 });
|
|
204
204
|
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Task 1' });
|
|
205
205
|
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Task 2' });
|
|
206
|
+
insertTask({ id: 'T99', sliceId: 'S02', milestoneId: 'M001', status: 'complete', title: 'Sibling Task' });
|
|
207
|
+
const siblingSummaryPath = path.join(path.dirname(roadmapPath), 'T99-SUMMARY.md');
|
|
208
|
+
const siblingSummaryBefore = '# Existing sibling task summary\n\nDo not rewrite this file.\n';
|
|
209
|
+
fs.writeFileSync(siblingSummaryPath, siblingSummaryBefore);
|
|
206
210
|
|
|
207
211
|
const params = makeValidSliceParams();
|
|
208
212
|
const result = await handleCompleteSlice(params, basePath);
|
|
@@ -267,6 +271,13 @@ console.log('\n=== complete-slice: handler happy path ===');
|
|
|
267
271
|
// (e) Verify slice status is complete in DB
|
|
268
272
|
assertEq(sliceAfter!.status, 'complete', 'slice status should be complete in DB');
|
|
269
273
|
assertTrue(sliceAfter!.completed_at !== null, 'completed_at should be set in DB');
|
|
274
|
+
|
|
275
|
+
// (f) Verify unrelated completed task summaries are not re-rendered during slice completion
|
|
276
|
+
assertEq(
|
|
277
|
+
fs.readFileSync(siblingSummaryPath, 'utf-8'),
|
|
278
|
+
siblingSummaryBefore,
|
|
279
|
+
'complete-slice should not re-render sibling completed task summaries',
|
|
280
|
+
);
|
|
270
281
|
}
|
|
271
282
|
|
|
272
283
|
cleanupDir(basePath);
|