gsd-pi 2.70.0-dev.55a1c68 → 2.70.0-dev.c236ea4
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/loader.js +4 -0
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +150 -2
- package/dist/resources/extensions/gsd/auto-prompts.js +7 -3
- package/dist/resources/extensions/gsd/auto.js +12 -8
- package/dist/resources/extensions/gsd/commands-handlers.js +22 -8
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +12 -0
- package/dist/resources/extensions/gsd/doctor-format.js +2 -0
- package/dist/resources/extensions/gsd/guided-flow.js +19 -9
- package/dist/resources/extensions/gsd/pre-execution-checks.js +5 -3
- package/dist/resources/extensions/gsd/validate-directory.js +30 -12
- package/dist/resources/extensions/gsd/workflow-mcp.js +11 -0
- package/dist/resources/extensions/slash-commands/audit.js +2 -1
- package/dist/resources/extensions/subagent/isolation.js +4 -2
- package/dist/update-check.d.ts +1 -0
- package/dist/update-check.js +30 -27
- package/dist/update-cmd.js +3 -11
- 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 +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.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/update/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/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/server.js +1 -1
- package/dist/web-mode.js +4 -0
- package/package.json +11 -11
- package/packages/mcp-server/dist/workflow-tools.d.ts +2 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +35 -3
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/import-candidates.test.ts +48 -0
- package/packages/mcp-server/src/workflow-tools.ts +34 -1
- package/packages/pi-agent-core/dist/agent.d.ts +8 -0
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +3 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/src/agent.test.ts +82 -0
- package/packages/pi-agent-core/src/agent.ts +12 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js +38 -15
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +10 -0
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +3 -1
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
- package/packages/pi-coding-agent/src/core/lsp/config.ts +43 -17
- package/packages/pi-coding-agent/src/core/sdk.ts +8 -0
- package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +7 -5
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +227 -2
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +172 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +7 -3
- package/src/resources/extensions/gsd/auto.ts +12 -8
- package/src/resources/extensions/gsd/commands-handlers.ts +22 -7
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +14 -0
- package/src/resources/extensions/gsd/doctor-format.ts +1 -0
- package/src/resources/extensions/gsd/doctor-types.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +23 -8
- package/src/resources/extensions/gsd/pre-execution-checks.ts +6 -3
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +48 -1
- package/src/resources/extensions/gsd/tests/resource-loader-import-path.test.ts +8 -7
- package/src/resources/extensions/gsd/tests/validate-directory.test.ts +33 -1
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +87 -1
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +25 -0
- package/src/resources/extensions/gsd/validate-directory.ts +33 -11
- package/src/resources/extensions/gsd/workflow-mcp.ts +15 -0
- package/src/resources/extensions/slash-commands/audit.ts +2 -1
- package/src/resources/extensions/subagent/isolation.ts +4 -3
- /package/dist/web/standalone/.next/static/{0CnmwCBOy-QNRFzdWLB7Q → LWbeDf2XwDjfq_mOlqoGf}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{0CnmwCBOy-QNRFzdWLB7Q → LWbeDf2XwDjfq_mOlqoGf}/_ssgManifest.js +0 -0
|
@@ -13,6 +13,20 @@ export async function checkEngineHealth(
|
|
|
13
13
|
issues: DoctorIssue[],
|
|
14
14
|
fixesApplied: string[],
|
|
15
15
|
): Promise<void> {
|
|
16
|
+
const dbPath = join(basePath, ".gsd", "gsd.db");
|
|
17
|
+
|
|
18
|
+
if (!isDbAvailable() && existsSync(dbPath)) {
|
|
19
|
+
issues.push({
|
|
20
|
+
severity: "warning",
|
|
21
|
+
code: "db_unavailable",
|
|
22
|
+
scope: "project",
|
|
23
|
+
unitId: "project",
|
|
24
|
+
message: "Database unavailable — using filesystem state derivation (degraded mode). State queries may be slower and less reliable.",
|
|
25
|
+
file: ".gsd/gsd.db",
|
|
26
|
+
fixable: false,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
// ── DB constraint violation detection (full doctor only, not pre-dispatch per D-10) ──
|
|
17
31
|
try {
|
|
18
32
|
if (isDbAvailable()) {
|
|
@@ -2,6 +2,7 @@ import type { DoctorIssue, DoctorIssueCode, DoctorReport, DoctorSummary } from "
|
|
|
2
2
|
|
|
3
3
|
function matchesScope(unitId: string, scope?: string): boolean {
|
|
4
4
|
if (!scope) return true;
|
|
5
|
+
if (unitId === "project" || unitId === "environment") return true;
|
|
5
6
|
return unitId === scope || unitId.startsWith(`${scope}/`) || unitId.startsWith(`${scope}`);
|
|
6
7
|
}
|
|
7
8
|
|
|
@@ -48,6 +48,7 @@ import { DISCUSS_TOOLS_ALLOWLIST } from "./constants.js";
|
|
|
48
48
|
import {
|
|
49
49
|
getWorkflowTransportSupportError,
|
|
50
50
|
getRequiredWorkflowToolsForGuidedUnit,
|
|
51
|
+
supportsStructuredQuestions,
|
|
51
52
|
} from "./workflow-mcp.js";
|
|
52
53
|
import {
|
|
53
54
|
runPreparation,
|
|
@@ -367,6 +368,20 @@ async function dispatchWorkflow(
|
|
|
367
368
|
}
|
|
368
369
|
}
|
|
369
370
|
|
|
371
|
+
function getStructuredQuestionsAvailability(
|
|
372
|
+
pi: ExtensionAPI,
|
|
373
|
+
ctx: ExtensionContext | undefined,
|
|
374
|
+
): "true" | "false" {
|
|
375
|
+
if (!ctx) return "false";
|
|
376
|
+
|
|
377
|
+
const provider = ctx.model?.provider;
|
|
378
|
+
const authMode = provider ? ctx.modelRegistry.getProviderAuthMode(provider) : undefined;
|
|
379
|
+
return supportsStructuredQuestions(pi.getActiveTools(), {
|
|
380
|
+
authMode,
|
|
381
|
+
baseUrl: ctx.model?.baseUrl,
|
|
382
|
+
}) ? "true" : "false";
|
|
383
|
+
}
|
|
384
|
+
|
|
370
385
|
/**
|
|
371
386
|
* Resolve a model ID string to a model object from available models.
|
|
372
387
|
* Handles "provider/model" and bare ID formats.
|
|
@@ -739,7 +754,7 @@ export async function showDiscuss(
|
|
|
739
754
|
|
|
740
755
|
if (choice === "discuss_draft") {
|
|
741
756
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
742
|
-
const structuredQuestionsAvailable = pi
|
|
757
|
+
const structuredQuestionsAvailable = getStructuredQuestionsAvailability(pi, ctx);
|
|
743
758
|
const basePrompt = loadPrompt("guided-discuss-milestone", {
|
|
744
759
|
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
745
760
|
commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
|
|
@@ -752,7 +767,7 @@ export async function showDiscuss(
|
|
|
752
767
|
await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "discuss-milestone");
|
|
753
768
|
} else if (choice === "discuss_fresh") {
|
|
754
769
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
755
|
-
const structuredQuestionsAvailable = pi
|
|
770
|
+
const structuredQuestionsAvailable = getStructuredQuestionsAvailability(pi, ctx);
|
|
756
771
|
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: mid, step: false, createdAt: Date.now() });
|
|
757
772
|
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
758
773
|
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
@@ -910,7 +925,7 @@ export async function showDiscuss(
|
|
|
910
925
|
if (confirm !== "rediscuss") continue;
|
|
911
926
|
}
|
|
912
927
|
|
|
913
|
-
const sqAvail = pi
|
|
928
|
+
const sqAvail = getStructuredQuestionsAvailability(pi, ctx);
|
|
914
929
|
const prompt = await buildDiscussSlicePrompt(mid, chosen.id, chosen.title, basePath, { rediscuss: isRediscuss, structuredQuestionsAvailable: sqAvail });
|
|
915
930
|
await dispatchWorkflow(pi, prompt, "gsd-discuss", ctx, "discuss-slice");
|
|
916
931
|
|
|
@@ -1020,7 +1035,7 @@ async function dispatchDiscussForMilestone(
|
|
|
1020
1035
|
].join("\n")
|
|
1021
1036
|
: "";
|
|
1022
1037
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
1023
|
-
const structuredQuestionsAvailable = pi
|
|
1038
|
+
const structuredQuestionsAvailable = getStructuredQuestionsAvailability(pi, ctx);
|
|
1024
1039
|
const basePrompt = loadPrompt("guided-discuss-milestone", {
|
|
1025
1040
|
milestoneId: mid,
|
|
1026
1041
|
milestoneTitle,
|
|
@@ -1461,7 +1476,7 @@ export async function showSmartEntry(
|
|
|
1461
1476
|
|
|
1462
1477
|
if (choice === "discuss_draft") {
|
|
1463
1478
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
1464
|
-
const structuredQuestionsAvailable = pi
|
|
1479
|
+
const structuredQuestionsAvailable = getStructuredQuestionsAvailability(pi, ctx);
|
|
1465
1480
|
const basePrompt = loadPrompt("guided-discuss-milestone", {
|
|
1466
1481
|
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
1467
1482
|
commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
|
|
@@ -1474,7 +1489,7 @@ export async function showSmartEntry(
|
|
|
1474
1489
|
await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "discuss-milestone");
|
|
1475
1490
|
} else if (choice === "discuss_fresh") {
|
|
1476
1491
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
1477
|
-
const structuredQuestionsAvailable = pi
|
|
1492
|
+
const structuredQuestionsAvailable = getStructuredQuestionsAvailability(pi, ctx);
|
|
1478
1493
|
pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId, step: stepMode, createdAt: Date.now() });
|
|
1479
1494
|
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
1480
1495
|
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
@@ -1572,7 +1587,7 @@ export async function showSmartEntry(
|
|
|
1572
1587
|
}), "gsd-run", ctx, "plan-milestone");
|
|
1573
1588
|
} else if (choice === "discuss") {
|
|
1574
1589
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
1575
|
-
const structuredQuestionsAvailable = pi
|
|
1590
|
+
const structuredQuestionsAvailable = getStructuredQuestionsAvailability(pi, ctx);
|
|
1576
1591
|
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
1577
1592
|
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
1578
1593
|
commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
|
|
@@ -1712,7 +1727,7 @@ export async function showSmartEntry(
|
|
|
1712
1727
|
}),
|
|
1713
1728
|
}), "gsd-run", ctx, "plan-slice");
|
|
1714
1729
|
} else if (choice === "discuss") {
|
|
1715
|
-
const sqAvail = pi
|
|
1730
|
+
const sqAvail = getStructuredQuestionsAvailability(pi, ctx);
|
|
1716
1731
|
await dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext, structuredQuestionsAvailable: sqAvail }), "gsd-run", ctx, "discuss-slice");
|
|
1717
1732
|
} else if (choice === "research") {
|
|
1718
1733
|
const researchTemplates = inlineTemplate("research", "Research");
|
|
@@ -20,6 +20,8 @@ import { resolve } from "node:path";
|
|
|
20
20
|
import type { TaskRow } from "./gsd-db.ts";
|
|
21
21
|
import type { PreExecutionCheckJSON } from "./verification-evidence.ts";
|
|
22
22
|
|
|
23
|
+
const NPM_COMMAND = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
24
|
+
|
|
23
25
|
// ─── Result Types ────────────────────────────────────────────────────────────
|
|
24
26
|
|
|
25
27
|
export interface PreExecutionResult {
|
|
@@ -126,9 +128,10 @@ async function checkPackageOnNpm(
|
|
|
126
128
|
timeoutMs = 5000
|
|
127
129
|
): Promise<{ exists: boolean; error?: string }> {
|
|
128
130
|
return new Promise((resolve) => {
|
|
129
|
-
const child = spawn(
|
|
131
|
+
const child = spawn(NPM_COMMAND, ["view", packageName, "name"], {
|
|
130
132
|
stdio: ["ignore", "pipe", "pipe"],
|
|
131
133
|
timeout: timeoutMs,
|
|
134
|
+
shell: process.platform === "win32",
|
|
132
135
|
});
|
|
133
136
|
|
|
134
137
|
let stdout = "";
|
|
@@ -263,9 +266,9 @@ function extractPathFromAnnotation(raw: string): string {
|
|
|
263
266
|
const trimmed = raw.trim();
|
|
264
267
|
if (!trimmed) return trimmed;
|
|
265
268
|
|
|
266
|
-
const backtickMatch = trimmed.match(
|
|
269
|
+
const backtickMatch = trimmed.match(/^(`+)([^`]+)\1(?:(?:\s+[—–-]\s+.+)|(?:\s+\([^()]+\)))?$/);
|
|
267
270
|
if (backtickMatch) {
|
|
268
|
-
return backtickMatch[
|
|
271
|
+
return backtickMatch[2].trim();
|
|
269
272
|
}
|
|
270
273
|
|
|
271
274
|
const annotatedMatch = trimmed.match(/^(.+?)\s+[—–-]\s+.+$/);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { afterEach, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { closeDatabase } from "../gsd-db.ts";
|
|
4
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { filterDoctorIssues } from "../doctor-format.ts";
|
|
8
|
+
import { checkEngineHealth } from "../doctor-engine-checks.ts";
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
closeDatabase();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("filterDoctorIssues keeps project and environment issues in scoped reports", () => {
|
|
15
|
+
const issues = [
|
|
16
|
+
{ severity: "error", code: "env_dependencies", scope: "project", unitId: "environment", message: "node_modules missing", fixable: false },
|
|
17
|
+
{ severity: "warning", code: "db_unavailable", scope: "project", unitId: "project", message: "DB unavailable", fixable: false },
|
|
18
|
+
{ severity: "warning", code: "state_file_missing", scope: "slice", unitId: "M016/S01", message: "slice warning", fixable: false },
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
const filtered = filterDoctorIssues([...issues], { scope: "M016", includeWarnings: true });
|
|
22
|
+
assert.deepEqual(
|
|
23
|
+
filtered.map((issue) => issue.unitId),
|
|
24
|
+
["environment", "project", "M016/S01"],
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("checkEngineHealth reports db_unavailable when gsd.db exists but the DB is closed", async (t) => {
|
|
29
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-db-unavailable-"));
|
|
30
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
31
|
+
|
|
32
|
+
const gsdDir = join(base, ".gsd");
|
|
33
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
34
|
+
writeFileSync(join(gsdDir, "gsd.db"), "");
|
|
35
|
+
|
|
36
|
+
const issues: any[] = [];
|
|
37
|
+
await checkEngineHealth(base, issues, []);
|
|
38
|
+
|
|
39
|
+
const dbIssue = issues.find((issue) => issue.code === "db_unavailable");
|
|
40
|
+
assert.ok(dbIssue, "doctor should surface degraded DB mode when a DB file exists");
|
|
41
|
+
assert.equal(dbIssue.unitId, "project");
|
|
42
|
+
assert.equal(dbIssue.file, ".gsd/gsd.db");
|
|
43
|
+
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { describe, it } from 'node:test'
|
|
13
13
|
import assert from 'node:assert/strict'
|
|
14
14
|
import { normalizeFilePath, checkFilePathConsistency } from '../pre-execution-checks.ts'
|
|
15
|
-
import { readFileSync } from 'node:fs'
|
|
15
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
16
16
|
import { resolve } from 'node:path'
|
|
17
17
|
|
|
18
18
|
const src = readFileSync(
|
|
@@ -25,6 +25,11 @@ describe('normalizeFilePath backtick stripping (#3649)', () => {
|
|
|
25
25
|
assert.equal(normalizeFilePath('`src/foo.ts`'), 'src/foo.ts')
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
+
it('strips doubled backticks and trailing notes from file paths', () => {
|
|
29
|
+
assert.equal(normalizeFilePath('``src/foo.ts`` - current state'), 'src/foo.ts')
|
|
30
|
+
assert.equal(normalizeFilePath('``src/foo.ts`` (current state)'), 'src/foo.ts')
|
|
31
|
+
})
|
|
32
|
+
|
|
28
33
|
it('strips backticks even when mixed with other normalization', () => {
|
|
29
34
|
assert.equal(normalizeFilePath('`./src//bar.ts`'), 'src/bar.ts')
|
|
30
35
|
})
|
|
@@ -66,3 +71,45 @@ describe('checkFilePathConsistency checks task.inputs not task.files (#3626)', (
|
|
|
66
71
|
)
|
|
67
72
|
})
|
|
68
73
|
})
|
|
74
|
+
|
|
75
|
+
describe('checkFilePathConsistency handles doubled-backtick annotations (#3892)', () => {
|
|
76
|
+
it('accepts existing files when task.inputs include doubled-backtick notes', () => {
|
|
77
|
+
const task = {
|
|
78
|
+
milestone_id: 'M001',
|
|
79
|
+
slice_id: 'S01',
|
|
80
|
+
id: 'T01',
|
|
81
|
+
title: 'Test Task',
|
|
82
|
+
status: 'pending',
|
|
83
|
+
one_liner: '',
|
|
84
|
+
narrative: '',
|
|
85
|
+
verification_result: '',
|
|
86
|
+
duration: '',
|
|
87
|
+
completed_at: null,
|
|
88
|
+
blocker_discovered: false,
|
|
89
|
+
deviations: '',
|
|
90
|
+
known_issues: '',
|
|
91
|
+
key_files: [],
|
|
92
|
+
key_decisions: [],
|
|
93
|
+
full_summary_md: '',
|
|
94
|
+
description: '',
|
|
95
|
+
estimate: '',
|
|
96
|
+
files: [],
|
|
97
|
+
verify: '',
|
|
98
|
+
inputs: ['``src/foo.ts`` (current state)'],
|
|
99
|
+
expected_output: [],
|
|
100
|
+
observability_impact: '',
|
|
101
|
+
full_plan_md: '',
|
|
102
|
+
sequence: 0,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const tmp = resolve(process.cwd(), '.tmp-pre-exec-3892')
|
|
106
|
+
try {
|
|
107
|
+
mkdirSync(resolve(tmp, 'src'), { recursive: true })
|
|
108
|
+
writeFileSync(resolve(tmp, 'src', 'foo.ts'), '// ok')
|
|
109
|
+
const results = checkFilePathConsistency([task as any], tmp)
|
|
110
|
+
assert.deepEqual(results, [])
|
|
111
|
+
} finally {
|
|
112
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -22,16 +22,17 @@ describe("resource-loader import path", () => {
|
|
|
22
22
|
);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test("uses
|
|
26
|
-
// The fix uses
|
|
27
|
-
// dist/resource-loader.js
|
|
25
|
+
test("uses GSD_PKG_ROOT to resolve resource-loader from package root", () => {
|
|
26
|
+
// The fix uses GSD_PKG_ROOT (set by loader.ts) to construct an absolute
|
|
27
|
+
// file URL to dist/resource-loader.js — works in both source and deployed,
|
|
28
|
+
// and on Windows where raw paths fail with ERR_UNSUPPORTED_ESM_URL_SCHEME.
|
|
28
29
|
assert.ok(
|
|
29
|
-
autoSrc.includes('
|
|
30
|
-
"auto.ts should use
|
|
30
|
+
autoSrc.includes('process.env.GSD_PKG_ROOT'),
|
|
31
|
+
"auto.ts should use GSD_PKG_ROOT to resolve resource-loader",
|
|
31
32
|
);
|
|
32
33
|
assert.ok(
|
|
33
|
-
autoSrc.includes('
|
|
34
|
-
"auto.ts should
|
|
34
|
+
autoSrc.includes('pathToFileURL'),
|
|
35
|
+
"auto.ts should convert path to file URL for cross-platform import()",
|
|
35
36
|
);
|
|
36
37
|
});
|
|
37
38
|
});
|
|
@@ -74,6 +74,27 @@ test("validateDirectory: C:\\Windows is blocked", { skip: !isWindows ? "Windows-
|
|
|
74
74
|
assert.equal(result.severity, "blocked");
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
test("validateDirectory: D:\\Windows is blocked", { skip: !isWindows ? "Windows-only test" : undefined }, () => {
|
|
78
|
+
const result = validateDirectory("D:\\Windows");
|
|
79
|
+
assert.equal(result.safe, false);
|
|
80
|
+
assert.equal(result.severity, "blocked");
|
|
81
|
+
assert.ok(result.reason?.includes("system directory"));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("validateDirectory: E:\\Program Files is blocked", { skip: !isWindows ? "Windows-only test" : undefined }, () => {
|
|
85
|
+
const result = validateDirectory("E:\\Program Files");
|
|
86
|
+
assert.equal(result.safe, false);
|
|
87
|
+
assert.equal(result.severity, "blocked");
|
|
88
|
+
assert.ok(result.reason?.includes("system directory"));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("validateDirectory: any Windows drive root is blocked", { skip: !isWindows ? "Windows-only test" : undefined }, () => {
|
|
92
|
+
const result = validateDirectory("D:\\");
|
|
93
|
+
assert.equal(result.safe, false);
|
|
94
|
+
assert.equal(result.severity, "blocked");
|
|
95
|
+
assert.ok(result.reason?.includes("system directory"));
|
|
96
|
+
});
|
|
97
|
+
|
|
77
98
|
// ─── Home directory (cross-platform) ─────────────────────────────────────────────
|
|
78
99
|
|
|
79
100
|
test("validateDirectory: home directory itself is blocked", () => {
|
|
@@ -104,7 +125,13 @@ test("validateDirectory: subdirectory of home is NOT blocked", () => {
|
|
|
104
125
|
// Regression test for #1317: GSD worktree inside $HOME must not be blocked even
|
|
105
126
|
// when the resolved project root equals $HOME (e.g. home dir is a git repo).
|
|
106
127
|
test("validateDirectory: GSD worktree path nested under home is NOT blocked (#1317)", () => {
|
|
128
|
+
const originalHome = process.env.HOME;
|
|
129
|
+
const originalUserProfile = process.env.USERPROFILE;
|
|
130
|
+
const fakeHome = makeTempDir("fake-home");
|
|
131
|
+
process.env.HOME = fakeHome;
|
|
132
|
+
process.env.USERPROFILE = fakeHome;
|
|
107
133
|
const worktreePath = join(homedir(), ".gsd", "worktrees", "M001");
|
|
134
|
+
const worktreeRoot = join(fakeHome, ".gsd", "worktrees", "M001");
|
|
108
135
|
mkdirSync(worktreePath, { recursive: true });
|
|
109
136
|
try {
|
|
110
137
|
// The worktree CWD itself is a valid location — it must pass.
|
|
@@ -112,7 +139,12 @@ test("validateDirectory: GSD worktree path nested under home is NOT blocked (#13
|
|
|
112
139
|
assert.equal(result.safe, true, "GSD worktree path should be safe to run in");
|
|
113
140
|
assert.equal(result.severity, "ok");
|
|
114
141
|
} finally {
|
|
115
|
-
|
|
142
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
143
|
+
else process.env.HOME = originalHome;
|
|
144
|
+
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
|
145
|
+
else process.env.USERPROFILE = originalUserProfile;
|
|
146
|
+
rmSync(worktreeRoot, { recursive: true, force: true });
|
|
147
|
+
rmSync(fakeHome, { recursive: true, force: true });
|
|
116
148
|
}
|
|
117
149
|
});
|
|
118
150
|
|
|
@@ -9,10 +9,11 @@ import { deriveState, isValidationTerminal } from "../state.ts";
|
|
|
9
9
|
import { resolveExpectedArtifactPath, diagnoseExpectedArtifact } from "../auto-artifact-paths.ts";
|
|
10
10
|
import { verifyExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.ts";
|
|
11
11
|
import { resolveDispatch, type DispatchContext } from "../auto-dispatch.ts";
|
|
12
|
-
import { buildValidateMilestonePrompt } from "../auto-prompts.ts";
|
|
12
|
+
import { buildCompleteMilestonePrompt, buildValidateMilestonePrompt } from "../auto-prompts.ts";
|
|
13
13
|
import type { GSDState } from "../types.ts";
|
|
14
14
|
import { clearPathCache } from "../paths.ts";
|
|
15
15
|
import { clearParseCache } from "../files.ts";
|
|
16
|
+
import { closeDatabase, insertMilestone, insertSlice, openDatabase } from "../gsd-db.ts";
|
|
16
17
|
|
|
17
18
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
18
19
|
|
|
@@ -25,9 +26,15 @@ function makeTmpBase(): string {
|
|
|
25
26
|
function cleanup(base: string): void {
|
|
26
27
|
clearPathCache();
|
|
27
28
|
clearParseCache();
|
|
29
|
+
closeDatabase();
|
|
28
30
|
try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
function openTestDb(base: string): void {
|
|
34
|
+
const dbPath = join(base, ".gsd", "gsd.db");
|
|
35
|
+
assert.equal(openDatabase(dbPath), true, "test DB should open");
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
function writeRoadmap(base: string, mid: string, content: string): void {
|
|
32
39
|
const dir = join(base, ".gsd", "milestones", mid);
|
|
33
40
|
mkdirSync(dir, { recursive: true });
|
|
@@ -218,6 +225,85 @@ test("buildValidateMilestonePrompt inlines ASSESSMENT evidence instead of UAT sp
|
|
|
218
225
|
}
|
|
219
226
|
});
|
|
220
227
|
|
|
228
|
+
test("buildCompleteMilestonePrompt skips skipped slices from DB-backed summary inlining", async () => {
|
|
229
|
+
const base = makeTmpBase();
|
|
230
|
+
try {
|
|
231
|
+
writeRoadmap(base, "M001", `# M001: Test Milestone
|
|
232
|
+
|
|
233
|
+
## Vision
|
|
234
|
+
Test
|
|
235
|
+
|
|
236
|
+
## Success Criteria
|
|
237
|
+
- It works
|
|
238
|
+
|
|
239
|
+
## Slices
|
|
240
|
+
|
|
241
|
+
- [x] **S01: First slice** \`risk:low\` \`depends:[]\`
|
|
242
|
+
> Done
|
|
243
|
+
- [ ] **S02: Skipped slice** \`risk:low\` \`depends:[]\`
|
|
244
|
+
> Intentionally skipped
|
|
245
|
+
|
|
246
|
+
## Boundary Map
|
|
247
|
+
|
|
248
|
+
| From | To | Produces | Consumes |
|
|
249
|
+
|------|-----|----------|----------|
|
|
250
|
+
| S01 | terminal | output | nothing |
|
|
251
|
+
`);
|
|
252
|
+
openTestDb(base);
|
|
253
|
+
insertMilestone({ id: "M001", title: "Test Milestone", status: "active" });
|
|
254
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "First slice", status: "complete", depends: [], sequence: 1 });
|
|
255
|
+
insertSlice({ id: "S02", milestoneId: "M001", title: "Skipped slice", status: "skipped", depends: [], sequence: 2 });
|
|
256
|
+
writeSliceSummary(base, "M001", "S01", "# S01 Summary\nDelivered.");
|
|
257
|
+
|
|
258
|
+
const prompt = await buildCompleteMilestonePrompt("M001", "Test Milestone", base);
|
|
259
|
+
assert.match(prompt, /S01 Summary/i, "prompt should inline non-skipped slice summaries");
|
|
260
|
+
assert.doesNotMatch(prompt, /### S02 Summary/i, "prompt should not inline skipped slice summaries");
|
|
261
|
+
assert.doesNotMatch(prompt, /not found — file does not exist yet/i, "prompt should not emit skipped-slice missing-file placeholders");
|
|
262
|
+
} finally {
|
|
263
|
+
cleanup(base);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("buildValidateMilestonePrompt skips skipped slices from DB-backed summary inlining", async () => {
|
|
268
|
+
const base = makeTmpBase();
|
|
269
|
+
try {
|
|
270
|
+
writeRoadmap(base, "M001", `# M001: Test Milestone
|
|
271
|
+
|
|
272
|
+
## Vision
|
|
273
|
+
Test
|
|
274
|
+
|
|
275
|
+
## Success Criteria
|
|
276
|
+
- It works
|
|
277
|
+
|
|
278
|
+
## Slices
|
|
279
|
+
|
|
280
|
+
- [x] **S01: First slice** \`risk:low\` \`depends:[]\`
|
|
281
|
+
> Done
|
|
282
|
+
- [ ] **S02: Skipped slice** \`risk:low\` \`depends:[]\`
|
|
283
|
+
> Intentionally skipped
|
|
284
|
+
|
|
285
|
+
## Boundary Map
|
|
286
|
+
|
|
287
|
+
| From | To | Produces | Consumes |
|
|
288
|
+
|------|-----|----------|----------|
|
|
289
|
+
| S01 | terminal | output | nothing |
|
|
290
|
+
`);
|
|
291
|
+
openTestDb(base);
|
|
292
|
+
insertMilestone({ id: "M001", title: "Test Milestone", status: "active" });
|
|
293
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "First slice", status: "complete", depends: [], sequence: 1 });
|
|
294
|
+
insertSlice({ id: "S02", milestoneId: "M001", title: "Skipped slice", status: "skipped", depends: [], sequence: 2 });
|
|
295
|
+
writeSliceSummary(base, "M001", "S01", "# S01 Summary\nDelivered.");
|
|
296
|
+
writeSliceAssessment(base, "M001", "S01", "---\nverdict: PASS\n---\n# Assessment\nEvidence captured.");
|
|
297
|
+
|
|
298
|
+
const prompt = await buildValidateMilestonePrompt("M001", "Test Milestone", base);
|
|
299
|
+
assert.match(prompt, /S01 Summary/i, "prompt should inline non-skipped slice summaries");
|
|
300
|
+
assert.doesNotMatch(prompt, /### S02 Summary/i, "prompt should not inline skipped slice summaries");
|
|
301
|
+
assert.doesNotMatch(prompt, /not found — file does not exist yet/i, "prompt should not emit skipped-slice missing-file placeholders");
|
|
302
|
+
} finally {
|
|
303
|
+
cleanup(base);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
221
307
|
// ─── Dispatch rule ────────────────────────────────────────────────────────
|
|
222
308
|
|
|
223
309
|
test("dispatch rule matches validating-milestone phase", async () => {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getWorkflowTransportSupportError,
|
|
14
14
|
getRequiredWorkflowToolsForAutoUnit,
|
|
15
15
|
getRequiredWorkflowToolsForGuidedUnit,
|
|
16
|
+
supportsStructuredQuestions,
|
|
16
17
|
usesWorkflowMcpTransport,
|
|
17
18
|
} from "../workflow-mcp.ts";
|
|
18
19
|
|
|
@@ -291,6 +292,30 @@ test("usesWorkflowMcpTransport matches local externalCli providers", () => {
|
|
|
291
292
|
assert.equal(usesWorkflowMcpTransport("oauth", "local://custom"), false);
|
|
292
293
|
});
|
|
293
294
|
|
|
295
|
+
test("supportsStructuredQuestions disables structured ask flow on workflow MCP transports", () => {
|
|
296
|
+
assert.equal(
|
|
297
|
+
supportsStructuredQuestions(["ask_user_questions"], {
|
|
298
|
+
authMode: "externalCli",
|
|
299
|
+
baseUrl: "local://claude-code",
|
|
300
|
+
}),
|
|
301
|
+
false,
|
|
302
|
+
);
|
|
303
|
+
assert.equal(
|
|
304
|
+
supportsStructuredQuestions(["ask_user_questions"], {
|
|
305
|
+
authMode: "oauth",
|
|
306
|
+
baseUrl: "https://api.anthropic.com",
|
|
307
|
+
}),
|
|
308
|
+
true,
|
|
309
|
+
);
|
|
310
|
+
assert.equal(
|
|
311
|
+
supportsStructuredQuestions([], {
|
|
312
|
+
authMode: "oauth",
|
|
313
|
+
baseUrl: "https://api.anthropic.com",
|
|
314
|
+
}),
|
|
315
|
+
false,
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
|
|
294
319
|
test("transport compatibility passes when required tools fit current MCP surface", () => {
|
|
295
320
|
const error = getWorkflowTransportSupportError(
|
|
296
321
|
"claude-code",
|
|
@@ -61,6 +61,33 @@ const WINDOWS_BLOCKED_PATHS = new Set([
|
|
|
61
61
|
"C:\\Program Files (x86)",
|
|
62
62
|
]);
|
|
63
63
|
|
|
64
|
+
const WINDOWS_BLOCKED_SUFFIXES = new Set([
|
|
65
|
+
"\\",
|
|
66
|
+
"\\windows",
|
|
67
|
+
"\\windows\\system32",
|
|
68
|
+
"\\program files",
|
|
69
|
+
"\\program files (x86)",
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
function normalizePathForComparison(dirPath: string): string {
|
|
73
|
+
let normalized = dirPath.replace(/[/\\]+$/, "");
|
|
74
|
+
if (normalized === "") {
|
|
75
|
+
normalized = "/";
|
|
76
|
+
} else if (/^[A-Za-z]:$/.test(normalized)) {
|
|
77
|
+
normalized += "\\";
|
|
78
|
+
}
|
|
79
|
+
return platform() === "win32" ? normalized.toLowerCase() : normalized;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isBlockedWindowsPath(normalized: string): boolean {
|
|
83
|
+
if (!/^[a-z]:\\/.test(normalized)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const suffix = normalized.slice(2);
|
|
88
|
+
return WINDOWS_BLOCKED_SUFFIXES.has(suffix);
|
|
89
|
+
}
|
|
90
|
+
|
|
64
91
|
// ─── Core Validation ────────────────────────────────────────────────────────────
|
|
65
92
|
|
|
66
93
|
/**
|
|
@@ -84,16 +111,11 @@ export function validateDirectory(dirPath: string): DirectoryValidationResult {
|
|
|
84
111
|
|
|
85
112
|
// Normalize trailing slashes for consistent comparison.
|
|
86
113
|
// Special cases: "/" → "/" (not ""), "C:\" → "C:\" (not "C:")
|
|
87
|
-
|
|
88
|
-
if (normalized === "") {
|
|
89
|
-
normalized = "/";
|
|
90
|
-
} else if (/^[A-Za-z]:$/.test(normalized)) {
|
|
91
|
-
normalized = normalized + "\\";
|
|
92
|
-
}
|
|
114
|
+
const normalized = normalizePathForComparison(resolved);
|
|
93
115
|
|
|
94
116
|
// ── Check 1: Blocked system paths ──────────────────────────────────────
|
|
95
117
|
const blockedPaths = platform() === "win32" ? WINDOWS_BLOCKED_PATHS : UNIX_BLOCKED_PATHS;
|
|
96
|
-
if (blockedPaths.has(normalized)) {
|
|
118
|
+
if (platform() === "win32" ? isBlockedWindowsPath(normalized) : blockedPaths.has(normalized)) {
|
|
97
119
|
return {
|
|
98
120
|
safe: false,
|
|
99
121
|
severity: "blocked",
|
|
@@ -104,9 +126,9 @@ export function validateDirectory(dirPath: string): DirectoryValidationResult {
|
|
|
104
126
|
// ── Check 2: Home directory itself (not subdirs) ───────────────────────
|
|
105
127
|
let resolvedHome: string;
|
|
106
128
|
try {
|
|
107
|
-
resolvedHome = realpathSync(resolve(homedir()))
|
|
129
|
+
resolvedHome = normalizePathForComparison(realpathSync(resolve(homedir())));
|
|
108
130
|
} catch {
|
|
109
|
-
resolvedHome = resolve(homedir())
|
|
131
|
+
resolvedHome = normalizePathForComparison(resolve(homedir()));
|
|
110
132
|
}
|
|
111
133
|
|
|
112
134
|
if (normalized === resolvedHome) {
|
|
@@ -120,9 +142,9 @@ export function validateDirectory(dirPath: string): DirectoryValidationResult {
|
|
|
120
142
|
// ── Check 3: Temp directory root ───────────────────────────────────────
|
|
121
143
|
let resolvedTmp: string;
|
|
122
144
|
try {
|
|
123
|
-
resolvedTmp = realpathSync(resolve(tmpdir()))
|
|
145
|
+
resolvedTmp = normalizePathForComparison(realpathSync(resolve(tmpdir())));
|
|
124
146
|
} catch {
|
|
125
|
-
resolvedTmp = resolve(tmpdir())
|
|
147
|
+
resolvedTmp = normalizePathForComparison(resolve(tmpdir()));
|
|
126
148
|
}
|
|
127
149
|
|
|
128
150
|
if (normalized === resolvedTmp) {
|
|
@@ -348,6 +348,21 @@ export function usesWorkflowMcpTransport(
|
|
|
348
348
|
return authMode === "externalCli" && typeof baseUrl === "string" && baseUrl.startsWith("local://");
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
export function supportsStructuredQuestions(
|
|
352
|
+
activeTools: string[],
|
|
353
|
+
options: Pick<WorkflowCapabilityOptions, "authMode" | "baseUrl"> = {},
|
|
354
|
+
): boolean {
|
|
355
|
+
if (!activeTools.includes("ask_user_questions")) return false;
|
|
356
|
+
|
|
357
|
+
// Workflow MCP currently exposes ask_user_questions via MCP form elicitation.
|
|
358
|
+
// Local external CLI transports such as Claude Code can invoke the tool, but
|
|
359
|
+
// do not reliably complete that elicitation round-trip yet, so guided discuss
|
|
360
|
+
// prompts must fall back to plain-text questioning.
|
|
361
|
+
if (usesWorkflowMcpTransport(options.authMode, options.baseUrl)) return false;
|
|
362
|
+
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
|
|
351
366
|
export function getWorkflowTransportSupportError(
|
|
352
367
|
provider: string | undefined,
|
|
353
368
|
requiredTools: string[],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
2
3
|
|
|
3
4
|
export default function auditCommand(pi: ExtensionAPI) {
|
|
4
5
|
pi.registerCommand("audit", {
|
|
@@ -39,7 +40,7 @@ export default function auditCommand(pi: ExtensionAPI) {
|
|
|
39
40
|
|
|
40
41
|
// ── Step 3: Ensure the output directory exists ───────────────────────
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
mkdirSync(".gsd/audits", { recursive: true });
|
|
43
44
|
|
|
44
45
|
// ── Step 4: Send the audit prompt to the agent ───────────────────────
|
|
45
46
|
|
|
@@ -53,8 +53,10 @@ interface Baseline {
|
|
|
53
53
|
// Directory helpers
|
|
54
54
|
// ============================================================================
|
|
55
55
|
|
|
56
|
-
function encodeCwd(cwd: string): string {
|
|
57
|
-
|
|
56
|
+
export function encodeCwd(cwd: string): string {
|
|
57
|
+
// Encode the entire cwd so Windows drive letters, separators, and UNC
|
|
58
|
+
// prefixes cannot leak into the isolation path.
|
|
59
|
+
return Buffer.from(cwd, "utf8").toString("base64url");
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
const gsdHome = process.env.GSD_HOME || path.join(os.homedir(), ".gsd");
|
|
@@ -500,4 +502,3 @@ export function readIsolationMode(): IsolationMode {
|
|
|
500
502
|
return "none";
|
|
501
503
|
}
|
|
502
504
|
}
|
|
503
|
-
|
|
File without changes
|
|
File without changes
|