gsd-pi 2.75.0-dev.b6ad8c5f7 → 2.75.0-dev.e41b70b10
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/extensions/gsd/auto/phases.js +2 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +22 -1
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +8 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +11 -11
- package/dist/resources/extensions/gsd/auto-model-selection.js +3 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +19 -9
- package/dist/resources/extensions/gsd/auto-worktree.js +16 -1
- package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +128 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +17 -4
- package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +52 -92
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
- package/dist/resources/extensions/gsd/commands-memory.js +462 -0
- package/dist/resources/extensions/gsd/doctor-git-checks.js +22 -2
- package/dist/resources/extensions/gsd/gsd-db.js +237 -4
- package/dist/resources/extensions/gsd/memory-embeddings.js +219 -0
- package/dist/resources/extensions/gsd/memory-extractor.js +78 -27
- package/dist/resources/extensions/gsd/memory-ingest.js +218 -0
- package/dist/resources/extensions/gsd/memory-relations.js +189 -0
- package/dist/resources/extensions/gsd/memory-source-store.js +113 -0
- package/dist/resources/extensions/gsd/memory-store.js +299 -6
- package/dist/resources/extensions/gsd/pre-execution-checks.js +12 -8
- package/dist/resources/extensions/gsd/prompts/add-tests.md +1 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -0
- package/dist/resources/extensions/gsd/tools/memory-tools.js +306 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +14 -0
- package/dist/resources/extensions/search-the-web/command-search-provider.js +4 -1
- package/dist/resources/extensions/search-the-web/native-search.js +13 -2
- 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 +10 -10
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- 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/package.json +1 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +12 -10
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +8 -1
- package/packages/mcp-server/dist/session-manager.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +1 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +207 -71
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +40 -4
- package/packages/mcp-server/src/server.ts +12 -10
- package/packages/mcp-server/src/session-manager.ts +10 -3
- package/packages/mcp-server/src/workflow-tools.test.ts +346 -1
- package/packages/mcp-server/src/workflow-tools.ts +228 -75
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/index.d.ts +1 -0
- package/packages/pi-ai/dist/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/index.js +1 -0
- package/packages/pi-ai/dist/index.js.map +1 -1
- package/packages/pi-ai/dist/providers/api-family.d.ts +27 -0
- package/packages/pi-ai/dist/providers/api-family.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/api-family.js +47 -0
- package/packages/pi-ai/dist/providers/api-family.js.map +1 -0
- package/packages/pi-ai/dist/providers/api-family.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/api-family.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/api-family.test.js +101 -0
- package/packages/pi-ai/dist/providers/api-family.test.js.map +1 -0
- package/packages/pi-ai/src/index.ts +1 -0
- package/packages/pi-ai/src/providers/api-family.test.ts +129 -0
- package/packages/pi-ai/src/providers/api-family.ts +57 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts +6 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +14 -4
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/cli/args.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/cli/args.test.js +38 -0
- package/packages/pi-coding-agent/dist/cli/args.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +4 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +7 -1
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +3 -0
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.test.ts +44 -0
- package/packages/pi-coding-agent/src/cli/args.ts +21 -6
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +4 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -2
- package/packages/pi-coding-agent/src/core/retry-handler.ts +4 -1
- package/packages/pi-coding-agent/src/core/sdk.ts +17 -1
- package/packages/pi-coding-agent/src/main.ts +4 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -10
- package/src/resources/extensions/gsd/auto/phases.ts +3 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +25 -1
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +15 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +21 -7
- package/src/resources/extensions/gsd/auto-model-selection.ts +3 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +33 -9
- package/src/resources/extensions/gsd/auto-worktree.ts +16 -1
- package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +158 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +20 -4
- package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +65 -131
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
- package/src/resources/extensions/gsd/commands-memory.ts +551 -0
- package/src/resources/extensions/gsd/doctor-git-checks.ts +23 -2
- package/src/resources/extensions/gsd/gsd-db.ts +273 -4
- package/src/resources/extensions/gsd/memory-embeddings.ts +235 -0
- package/src/resources/extensions/gsd/memory-extractor.ts +100 -34
- package/src/resources/extensions/gsd/memory-ingest.ts +286 -0
- package/src/resources/extensions/gsd/memory-relations.ts +240 -0
- package/src/resources/extensions/gsd/memory-source-store.ts +138 -0
- package/src/resources/extensions/gsd/memory-store.ts +351 -7
- package/src/resources/extensions/gsd/pre-execution-checks.ts +12 -8
- package/src/resources/extensions/gsd/prompts/add-tests.md +1 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/integration/doctor-git-symlink-cwd.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-embeddings.test.ts +213 -0
- package/src/resources/extensions/gsd/tests/memory-ingest.test.ts +153 -0
- package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/memory-relations.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/memory-tools.test.ts +295 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +132 -8
- package/src/resources/extensions/gsd/tests/prompts-no-gitignored-test-refs.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +97 -0
- package/src/resources/extensions/gsd/tools/memory-tools.ts +380 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +14 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
- package/src/resources/extensions/search-the-web/command-search-provider.ts +4 -1
- package/src/resources/extensions/search-the-web/native-search.ts +13 -3
- package/src/resources/extensions/gsd/tests/onboarding-handler-loader.test.ts +0 -41
- /package/dist/web/standalone/.next/static/{J2z3GMC9QtSLr7gyoM38c → By_yegSJ-AA1OP0QjYbSl}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{J2z3GMC9QtSLr7gyoM38c → By_yegSJ-AA1OP0QjYbSl}/_ssgManifest.js +0 -0
|
@@ -23,7 +23,7 @@ import { readKnowledge } from './readers/knowledge.js';
|
|
|
23
23
|
import { buildGraph, writeGraph, writeSnapshot, graphStatus, graphQuery, graphDiff } from './readers/graph.js';
|
|
24
24
|
import { resolveGsdRoot } from './readers/paths.js';
|
|
25
25
|
import { runDoctorLite } from './readers/doctor-lite.js';
|
|
26
|
-
import { registerWorkflowTools } from './workflow-tools.js';
|
|
26
|
+
import { registerWorkflowTools, validateProjectDir } from './workflow-tools.js';
|
|
27
27
|
import { applySecrets, checkExistingEnvKeys, detectDestination } from './env-writer.js';
|
|
28
28
|
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
@@ -497,7 +497,8 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|
|
497
497
|
async (args: Record<string, unknown>) => {
|
|
498
498
|
const { projectDir, query } = args as { projectDir: string; query?: string };
|
|
499
499
|
try {
|
|
500
|
-
const
|
|
500
|
+
const validated = validateProjectDir(projectDir);
|
|
501
|
+
const state = await readProjectState(validated, query);
|
|
501
502
|
return jsonContent(state);
|
|
502
503
|
} catch (err) {
|
|
503
504
|
return errorContent(err instanceof Error ? err.message : String(err));
|
|
@@ -589,7 +590,7 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|
|
589
590
|
};
|
|
590
591
|
|
|
591
592
|
try {
|
|
592
|
-
const resolvedProjectDir =
|
|
593
|
+
const resolvedProjectDir = validateProjectDir(projectDir);
|
|
593
594
|
const resolvedEnvPath = resolve(resolvedProjectDir, envFilePath ?? '.env');
|
|
594
595
|
|
|
595
596
|
// (1) Check which keys already exist
|
|
@@ -696,7 +697,7 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|
|
696
697
|
async (args: Record<string, unknown>) => {
|
|
697
698
|
const { projectDir } = args as { projectDir: string };
|
|
698
699
|
try {
|
|
699
|
-
return jsonContent(readProgress(projectDir));
|
|
700
|
+
return jsonContent(readProgress(validateProjectDir(projectDir)));
|
|
700
701
|
} catch (err) {
|
|
701
702
|
return errorContent(err instanceof Error ? err.message : String(err));
|
|
702
703
|
}
|
|
@@ -716,7 +717,7 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|
|
716
717
|
async (args: Record<string, unknown>) => {
|
|
717
718
|
const { projectDir, milestoneId } = args as { projectDir: string; milestoneId?: string };
|
|
718
719
|
try {
|
|
719
|
-
return jsonContent(readRoadmap(projectDir, milestoneId));
|
|
720
|
+
return jsonContent(readRoadmap(validateProjectDir(projectDir), milestoneId));
|
|
720
721
|
} catch (err) {
|
|
721
722
|
return errorContent(err instanceof Error ? err.message : String(err));
|
|
722
723
|
}
|
|
@@ -736,7 +737,7 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|
|
736
737
|
async (args: Record<string, unknown>) => {
|
|
737
738
|
const { projectDir, limit } = args as { projectDir: string; limit?: number };
|
|
738
739
|
try {
|
|
739
|
-
return jsonContent(readHistory(projectDir, limit));
|
|
740
|
+
return jsonContent(readHistory(validateProjectDir(projectDir), limit));
|
|
740
741
|
} catch (err) {
|
|
741
742
|
return errorContent(err instanceof Error ? err.message : String(err));
|
|
742
743
|
}
|
|
@@ -756,7 +757,7 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|
|
756
757
|
async (args: Record<string, unknown>) => {
|
|
757
758
|
const { projectDir, scope } = args as { projectDir: string; scope?: string };
|
|
758
759
|
try {
|
|
759
|
-
return jsonContent(runDoctorLite(projectDir, scope));
|
|
760
|
+
return jsonContent(runDoctorLite(validateProjectDir(projectDir), scope));
|
|
760
761
|
} catch (err) {
|
|
761
762
|
return errorContent(err instanceof Error ? err.message : String(err));
|
|
762
763
|
}
|
|
@@ -776,7 +777,7 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|
|
776
777
|
async (args: Record<string, unknown>) => {
|
|
777
778
|
const { projectDir, filter } = args as { projectDir: string; filter?: 'all' | 'pending' | 'actionable' };
|
|
778
779
|
try {
|
|
779
|
-
return jsonContent(readCaptures(projectDir, filter ?? 'all'));
|
|
780
|
+
return jsonContent(readCaptures(validateProjectDir(projectDir), filter ?? 'all'));
|
|
780
781
|
} catch (err) {
|
|
781
782
|
return errorContent(err instanceof Error ? err.message : String(err));
|
|
782
783
|
}
|
|
@@ -795,7 +796,7 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|
|
795
796
|
async (args: Record<string, unknown>) => {
|
|
796
797
|
const { projectDir } = args as { projectDir: string };
|
|
797
798
|
try {
|
|
798
|
-
return jsonContent(readKnowledge(projectDir));
|
|
799
|
+
return jsonContent(readKnowledge(validateProjectDir(projectDir)));
|
|
799
800
|
} catch (err) {
|
|
800
801
|
return errorContent(err instanceof Error ? err.message : String(err));
|
|
801
802
|
}
|
|
@@ -836,7 +837,7 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|
|
836
837
|
snapshot: z.boolean().optional().describe('Write snapshot before build (for future diff)'),
|
|
837
838
|
},
|
|
838
839
|
async (args: Record<string, unknown>) => {
|
|
839
|
-
const { projectDir, mode, term, budget, snapshot } = args as {
|
|
840
|
+
const { projectDir: rawProjectDir, mode, term, budget, snapshot } = args as {
|
|
840
841
|
projectDir: string;
|
|
841
842
|
mode: 'build' | 'query' | 'status' | 'diff';
|
|
842
843
|
term?: string;
|
|
@@ -845,6 +846,7 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|
|
845
846
|
};
|
|
846
847
|
|
|
847
848
|
try {
|
|
849
|
+
const projectDir = validateProjectDir(rawProjectDir);
|
|
848
850
|
const gsdRoot = resolveGsdRoot(projectDir);
|
|
849
851
|
|
|
850
852
|
switch (mode) {
|
|
@@ -71,9 +71,16 @@ export class SessionManager {
|
|
|
71
71
|
|
|
72
72
|
const existing = this.sessions.get(resolvedDir);
|
|
73
73
|
if (existing) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
// Only block when a genuinely active session is running. Terminal
|
|
75
|
+
// states (error, completed, cancelled) are evicted so the caller can
|
|
76
|
+
// start a fresh session for the same projectDir.
|
|
77
|
+
if (existing.status === 'starting' || existing.status === 'running' || existing.status === 'blocked') {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Session already active for ${resolvedDir} (sessionId: ${existing.sessionId}, status: ${existing.status})`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
existing.unsubscribe?.();
|
|
83
|
+
this.sessions.delete(resolvedDir);
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
const cliPath = options.cliPath ?? SessionManager.resolveCLIPath();
|
|
@@ -5,8 +5,10 @@ import { join } from "node:path";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
|
|
8
|
+
import { symlinkSync, realpathSync } from "node:fs";
|
|
9
|
+
|
|
8
10
|
import { _getAdapter, closeDatabase } from "../../../src/resources/extensions/gsd/gsd-db.ts";
|
|
9
|
-
import { registerWorkflowTools, WORKFLOW_TOOL_NAMES } from "./workflow-tools.ts";
|
|
11
|
+
import { registerWorkflowTools, WORKFLOW_TOOL_NAMES, validateProjectDir } from "./workflow-tools.ts";
|
|
10
12
|
|
|
11
13
|
function makeTmpBase(): string {
|
|
12
14
|
const base = join(tmpdir(), `gsd-mcp-workflow-${randomUUID()}`);
|
|
@@ -384,6 +386,261 @@ describe("workflow MCP tools", () => {
|
|
|
384
386
|
}
|
|
385
387
|
});
|
|
386
388
|
|
|
389
|
+
it("other workflow tools reject empty required strings at the schema layer", async () => {
|
|
390
|
+
const base = makeTmpBase();
|
|
391
|
+
try {
|
|
392
|
+
const server = makeMockServer();
|
|
393
|
+
registerWorkflowTools(server as any);
|
|
394
|
+
|
|
395
|
+
const expectRejection = async (toolName: string, args: Record<string, unknown>, expectedField: string) => {
|
|
396
|
+
const tool = server.tools.find((t) => t.name === toolName);
|
|
397
|
+
assert.ok(tool, `${toolName} should be registered`);
|
|
398
|
+
let caught: unknown;
|
|
399
|
+
try {
|
|
400
|
+
await tool!.handler(args);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
caught = err;
|
|
403
|
+
}
|
|
404
|
+
assert.ok(caught, `${toolName} should reject empty ${expectedField}`);
|
|
405
|
+
const message = caught instanceof Error ? caught.message : String(caught);
|
|
406
|
+
assert.ok(
|
|
407
|
+
message.includes(expectedField),
|
|
408
|
+
`${toolName} error should mention ${expectedField}, got: ${message}`,
|
|
409
|
+
);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// Empty sliceId top-level
|
|
413
|
+
await expectRejection("gsd_plan_slice", {
|
|
414
|
+
projectDir: base,
|
|
415
|
+
milestoneId: "M001",
|
|
416
|
+
sliceId: "",
|
|
417
|
+
goal: "Persist slice plan.",
|
|
418
|
+
tasks: [],
|
|
419
|
+
}, "sliceId");
|
|
420
|
+
|
|
421
|
+
// Empty task verify inside tasks array
|
|
422
|
+
await expectRejection("gsd_plan_slice", {
|
|
423
|
+
projectDir: base,
|
|
424
|
+
milestoneId: "M001",
|
|
425
|
+
sliceId: "S01",
|
|
426
|
+
goal: "Persist slice plan.",
|
|
427
|
+
tasks: [
|
|
428
|
+
{
|
|
429
|
+
taskId: "T01",
|
|
430
|
+
title: "Add bridge",
|
|
431
|
+
description: "Implement bridge.",
|
|
432
|
+
estimate: "15m",
|
|
433
|
+
files: ["src/x.ts"],
|
|
434
|
+
verify: "",
|
|
435
|
+
inputs: ["ROADMAP.md"],
|
|
436
|
+
expectedOutput: ["S01-PLAN.md"],
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
}, "verify");
|
|
440
|
+
|
|
441
|
+
// Empty element inside files[] array
|
|
442
|
+
await expectRejection("gsd_plan_slice", {
|
|
443
|
+
projectDir: base,
|
|
444
|
+
milestoneId: "M001",
|
|
445
|
+
sliceId: "S01",
|
|
446
|
+
goal: "Persist slice plan.",
|
|
447
|
+
tasks: [
|
|
448
|
+
{
|
|
449
|
+
taskId: "T01",
|
|
450
|
+
title: "Add bridge",
|
|
451
|
+
description: "Implement bridge.",
|
|
452
|
+
estimate: "15m",
|
|
453
|
+
files: ["src/x.ts", " "],
|
|
454
|
+
verify: "node --test",
|
|
455
|
+
inputs: ["ROADMAP.md"],
|
|
456
|
+
expectedOutput: ["S01-PLAN.md"],
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
}, "files");
|
|
460
|
+
|
|
461
|
+
// Empty milestoneId on gsd_plan_task
|
|
462
|
+
await expectRejection("gsd_plan_task", {
|
|
463
|
+
projectDir: base,
|
|
464
|
+
milestoneId: "",
|
|
465
|
+
sliceId: "S01",
|
|
466
|
+
taskId: "T01",
|
|
467
|
+
title: "t",
|
|
468
|
+
description: "d",
|
|
469
|
+
estimate: "1m",
|
|
470
|
+
files: [],
|
|
471
|
+
verify: "v",
|
|
472
|
+
inputs: [],
|
|
473
|
+
expectedOutput: [],
|
|
474
|
+
}, "milestoneId");
|
|
475
|
+
|
|
476
|
+
// Empty observabilityImpact explicitly rejected (optional-but-non-empty)
|
|
477
|
+
await expectRejection("gsd_plan_task", {
|
|
478
|
+
projectDir: base,
|
|
479
|
+
milestoneId: "M001",
|
|
480
|
+
sliceId: "S01",
|
|
481
|
+
taskId: "T01",
|
|
482
|
+
title: "t",
|
|
483
|
+
description: "d",
|
|
484
|
+
estimate: "1m",
|
|
485
|
+
files: [],
|
|
486
|
+
verify: "v",
|
|
487
|
+
inputs: [],
|
|
488
|
+
expectedOutput: [],
|
|
489
|
+
observabilityImpact: " ",
|
|
490
|
+
}, "observabilityImpact");
|
|
491
|
+
|
|
492
|
+
// Empty assessment on gsd_reassess_roadmap
|
|
493
|
+
await expectRejection("gsd_reassess_roadmap", {
|
|
494
|
+
projectDir: base,
|
|
495
|
+
milestoneId: "M001",
|
|
496
|
+
completedSliceId: "S01",
|
|
497
|
+
verdict: "roadmap-confirmed",
|
|
498
|
+
assessment: "",
|
|
499
|
+
sliceChanges: { modified: [], added: [], removed: [] },
|
|
500
|
+
}, "assessment");
|
|
501
|
+
|
|
502
|
+
// Empty keyRisks[i].risk on gsd_plan_milestone top-level arrays
|
|
503
|
+
await expectRejection("gsd_plan_milestone", {
|
|
504
|
+
projectDir: base,
|
|
505
|
+
milestoneId: "M001",
|
|
506
|
+
title: "T",
|
|
507
|
+
vision: "V",
|
|
508
|
+
slices: [],
|
|
509
|
+
keyRisks: [{ risk: "", whyItMatters: "because." }],
|
|
510
|
+
}, "risk");
|
|
511
|
+
|
|
512
|
+
// Empty blockerDescription on gsd_replan_slice
|
|
513
|
+
await expectRejection("gsd_replan_slice", {
|
|
514
|
+
projectDir: base,
|
|
515
|
+
milestoneId: "M001",
|
|
516
|
+
sliceId: "S01",
|
|
517
|
+
blockerTaskId: "T01",
|
|
518
|
+
blockerDescription: "",
|
|
519
|
+
whatChanged: "x",
|
|
520
|
+
updatedTasks: [],
|
|
521
|
+
removedTaskIds: [],
|
|
522
|
+
}, "blockerDescription");
|
|
523
|
+
|
|
524
|
+
// Empty milestoneId on gsd_task_complete
|
|
525
|
+
await expectRejection("gsd_task_complete", {
|
|
526
|
+
projectDir: base,
|
|
527
|
+
taskId: "T01",
|
|
528
|
+
sliceId: "S01",
|
|
529
|
+
milestoneId: "",
|
|
530
|
+
oneLiner: "ol",
|
|
531
|
+
narrative: "n",
|
|
532
|
+
verification: "v",
|
|
533
|
+
}, "milestoneId");
|
|
534
|
+
} finally {
|
|
535
|
+
cleanup(base);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it("gsd_plan_milestone rejects empty slice fields up front with all violations", async () => {
|
|
540
|
+
const base = makeTmpBase();
|
|
541
|
+
try {
|
|
542
|
+
const server = makeMockServer();
|
|
543
|
+
registerWorkflowTools(server as any);
|
|
544
|
+
const milestoneTool = server.tools.find((t) => t.name === "gsd_plan_milestone");
|
|
545
|
+
assert.ok(milestoneTool, "milestone planning tool should be registered");
|
|
546
|
+
|
|
547
|
+
let caught: unknown;
|
|
548
|
+
try {
|
|
549
|
+
await milestoneTool!.handler({
|
|
550
|
+
projectDir: base,
|
|
551
|
+
milestoneId: "M001",
|
|
552
|
+
title: "Workflow MCP planning",
|
|
553
|
+
vision: "Plan milestone over MCP.",
|
|
554
|
+
slices: [
|
|
555
|
+
{
|
|
556
|
+
sliceId: "S01",
|
|
557
|
+
title: "Bridge planning",
|
|
558
|
+
risk: "medium",
|
|
559
|
+
depends: [],
|
|
560
|
+
demo: "Milestone plan persists through MCP.",
|
|
561
|
+
goal: "Persist roadmap state.",
|
|
562
|
+
successCriteria: "",
|
|
563
|
+
proofLevel: "",
|
|
564
|
+
integrationClosure: " ",
|
|
565
|
+
observabilityImpact: "",
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
});
|
|
569
|
+
} catch (err) {
|
|
570
|
+
caught = err;
|
|
571
|
+
}
|
|
572
|
+
assert.ok(caught, "empty slice fields should be rejected");
|
|
573
|
+
const message = caught instanceof Error ? caught.message : String(caught);
|
|
574
|
+
for (const field of ["successCriteria", "proofLevel", "integrationClosure", "observabilityImpact"]) {
|
|
575
|
+
assert.ok(
|
|
576
|
+
message.includes(field),
|
|
577
|
+
`parse error should mention ${field}, got: ${message}`,
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
} finally {
|
|
581
|
+
cleanup(base);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it("gsd_plan_milestone requires sketchScope when isSketch=true and skips heavy fields", async () => {
|
|
586
|
+
const base = makeTmpBase();
|
|
587
|
+
try {
|
|
588
|
+
const server = makeMockServer();
|
|
589
|
+
registerWorkflowTools(server as any);
|
|
590
|
+
const milestoneTool = server.tools.find((t) => t.name === "gsd_plan_milestone");
|
|
591
|
+
assert.ok(milestoneTool, "milestone planning tool should be registered");
|
|
592
|
+
|
|
593
|
+
let caught: unknown;
|
|
594
|
+
try {
|
|
595
|
+
await milestoneTool!.handler({
|
|
596
|
+
projectDir: base,
|
|
597
|
+
milestoneId: "M001",
|
|
598
|
+
title: "Sketch milestone",
|
|
599
|
+
vision: "Sketch first, refine later.",
|
|
600
|
+
slices: [
|
|
601
|
+
{
|
|
602
|
+
sliceId: "S01",
|
|
603
|
+
title: "Sketch slice",
|
|
604
|
+
risk: "low",
|
|
605
|
+
depends: [],
|
|
606
|
+
demo: "Stub demo.",
|
|
607
|
+
goal: "Stub goal.",
|
|
608
|
+
isSketch: true,
|
|
609
|
+
sketchScope: "",
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
});
|
|
613
|
+
} catch (err) {
|
|
614
|
+
caught = err;
|
|
615
|
+
}
|
|
616
|
+
assert.ok(caught, "empty sketchScope should be rejected when isSketch=true");
|
|
617
|
+
const message = caught instanceof Error ? caught.message : String(caught);
|
|
618
|
+
assert.ok(message.includes("sketchScope"), `expected sketchScope error, got: ${message}`);
|
|
619
|
+
|
|
620
|
+
const sketchResult = await milestoneTool!.handler({
|
|
621
|
+
projectDir: base,
|
|
622
|
+
milestoneId: "M001",
|
|
623
|
+
title: "Sketch milestone",
|
|
624
|
+
vision: "Sketch first, refine later.",
|
|
625
|
+
slices: [
|
|
626
|
+
{
|
|
627
|
+
sliceId: "S01",
|
|
628
|
+
title: "Sketch slice",
|
|
629
|
+
risk: "low",
|
|
630
|
+
depends: [],
|
|
631
|
+
demo: "Stub demo.",
|
|
632
|
+
goal: "Stub goal.",
|
|
633
|
+
isSketch: true,
|
|
634
|
+
sketchScope: "Defer heavy planning fields until refine-slice.",
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
});
|
|
638
|
+
assert.match((sketchResult as any).content[0].text as string, /Planned milestone M001/);
|
|
639
|
+
} finally {
|
|
640
|
+
cleanup(base);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
387
644
|
it("gsd_requirement_save opens the DB before inline requirement writes", async () => {
|
|
388
645
|
const base = makeTmpBase();
|
|
389
646
|
try {
|
|
@@ -1091,3 +1348,91 @@ describe("URL scheme regex — Windows drive letter safety", () => {
|
|
|
1091
1348
|
assert.ok(!urlSchemeRegex.test("../parent/path.js"), "parent relative path should not match");
|
|
1092
1349
|
});
|
|
1093
1350
|
});
|
|
1351
|
+
|
|
1352
|
+
// ---------------------------------------------------------------------------
|
|
1353
|
+
// validateProjectDir — symlink containment hardening (#4476)
|
|
1354
|
+
// ---------------------------------------------------------------------------
|
|
1355
|
+
//
|
|
1356
|
+
// The regression: a symlink inside the allowed root could point outside it,
|
|
1357
|
+
// and a lexical-only containment check would happily admit the path. The fix
|
|
1358
|
+
// realpath()s the candidate (and the allowed root) before checking
|
|
1359
|
+
// containment, falling back to the lexical path only when the candidate
|
|
1360
|
+
// itself does not exist (a legitimate brand-new-worktree case).
|
|
1361
|
+
|
|
1362
|
+
describe("validateProjectDir", () => {
|
|
1363
|
+
it("rejects a symlink inside the allowed root that points outside it", () => {
|
|
1364
|
+
const allowedRoot = makeTmpBase();
|
|
1365
|
+
const outside = makeTmpBase();
|
|
1366
|
+
const linkInside = join(allowedRoot, "escape-link");
|
|
1367
|
+
symlinkSync(outside, linkInside, "dir");
|
|
1368
|
+
|
|
1369
|
+
const prevRoot = process.env.GSD_WORKFLOW_PROJECT_ROOT;
|
|
1370
|
+
try {
|
|
1371
|
+
process.env.GSD_WORKFLOW_PROJECT_ROOT = allowedRoot;
|
|
1372
|
+
assert.throws(
|
|
1373
|
+
() => validateProjectDir(linkInside),
|
|
1374
|
+
/configured workflow project root/,
|
|
1375
|
+
"symlink-to-outside must not bypass the containment check",
|
|
1376
|
+
);
|
|
1377
|
+
} finally {
|
|
1378
|
+
if (prevRoot === undefined) {
|
|
1379
|
+
delete process.env.GSD_WORKFLOW_PROJECT_ROOT;
|
|
1380
|
+
} else {
|
|
1381
|
+
process.env.GSD_WORKFLOW_PROJECT_ROOT = prevRoot;
|
|
1382
|
+
}
|
|
1383
|
+
cleanup(allowedRoot);
|
|
1384
|
+
cleanup(outside);
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
it("accepts a non-existent path inside the allowed root (new worktree case)", () => {
|
|
1389
|
+
const allowedRoot = makeTmpBase();
|
|
1390
|
+
// Use the realpath form so that on platforms where /tmp resolves through a
|
|
1391
|
+
// symlink (macOS /var → /private/var) the lexical fallback for ENOENT
|
|
1392
|
+
// candidates still lines up with the allowed root.
|
|
1393
|
+
const canonicalRoot = realpathSync(allowedRoot);
|
|
1394
|
+
const futureWorktree = join(canonicalRoot, "worktrees", "M999-not-yet-created");
|
|
1395
|
+
|
|
1396
|
+
const prevRoot = process.env.GSD_WORKFLOW_PROJECT_ROOT;
|
|
1397
|
+
try {
|
|
1398
|
+
process.env.GSD_WORKFLOW_PROJECT_ROOT = canonicalRoot;
|
|
1399
|
+
const result = validateProjectDir(futureWorktree);
|
|
1400
|
+
assert.equal(result, futureWorktree, "ENOENT should fall back to the lexical path, not throw");
|
|
1401
|
+
} finally {
|
|
1402
|
+
if (prevRoot === undefined) {
|
|
1403
|
+
delete process.env.GSD_WORKFLOW_PROJECT_ROOT;
|
|
1404
|
+
} else {
|
|
1405
|
+
process.env.GSD_WORKFLOW_PROJECT_ROOT = prevRoot;
|
|
1406
|
+
}
|
|
1407
|
+
cleanup(allowedRoot);
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
it("accepts a real directory inside the allowed root", () => {
|
|
1412
|
+
const allowedRoot = makeTmpBase();
|
|
1413
|
+
const child = join(allowedRoot, "child");
|
|
1414
|
+
mkdirSync(child, { recursive: true });
|
|
1415
|
+
|
|
1416
|
+
const prevRoot = process.env.GSD_WORKFLOW_PROJECT_ROOT;
|
|
1417
|
+
try {
|
|
1418
|
+
process.env.GSD_WORKFLOW_PROJECT_ROOT = allowedRoot;
|
|
1419
|
+
const result = validateProjectDir(child);
|
|
1420
|
+
// realpath may canonicalize macOS /var → /private/var; assert it ends with our child segment.
|
|
1421
|
+
assert.ok(result.endsWith("child"), `expected resolved path to end with 'child', got ${result}`);
|
|
1422
|
+
} finally {
|
|
1423
|
+
if (prevRoot === undefined) {
|
|
1424
|
+
delete process.env.GSD_WORKFLOW_PROJECT_ROOT;
|
|
1425
|
+
} else {
|
|
1426
|
+
process.env.GSD_WORKFLOW_PROJECT_ROOT = prevRoot;
|
|
1427
|
+
}
|
|
1428
|
+
cleanup(allowedRoot);
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
it("rejects relative paths", () => {
|
|
1433
|
+
assert.throws(
|
|
1434
|
+
() => validateProjectDir("relative/path"),
|
|
1435
|
+
/must be an absolute path/,
|
|
1436
|
+
);
|
|
1437
|
+
});
|
|
1438
|
+
});
|