gsd-pi 2.37.1 → 2.38.0-dev.29edcdc
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/README.md +1 -1
- package/dist/app-paths.js +1 -1
- package/dist/cli.js +9 -0
- package/dist/extension-discovery.d.ts +5 -3
- package/dist/extension-discovery.js +14 -9
- package/dist/extension-registry.js +2 -2
- package/dist/onboarding.js +1 -0
- package/dist/remote-questions-config.js +2 -2
- package/dist/resource-loader.js +34 -1
- package/dist/resources/extensions/browser-tools/package.json +3 -1
- package/dist/resources/extensions/cmux/index.js +55 -1
- package/dist/resources/extensions/context7/package.json +1 -1
- package/dist/resources/extensions/env-utils.js +29 -0
- package/dist/resources/extensions/get-secrets-from-user.js +5 -24
- package/dist/resources/extensions/github-sync/cli.js +284 -0
- package/dist/resources/extensions/github-sync/index.js +73 -0
- package/dist/resources/extensions/github-sync/mapping.js +67 -0
- package/dist/resources/extensions/github-sync/sync.js +424 -0
- package/dist/resources/extensions/github-sync/templates.js +118 -0
- package/dist/resources/extensions/github-sync/types.js +7 -0
- package/dist/resources/extensions/google-search/package.json +3 -1
- package/dist/resources/extensions/gsd/auto/session.js +6 -23
- package/dist/resources/extensions/gsd/auto-dispatch.js +75 -10
- package/dist/resources/extensions/gsd/auto-loop.js +597 -588
- package/dist/resources/extensions/gsd/auto-post-unit.js +111 -68
- package/dist/resources/extensions/gsd/auto-prompts.js +114 -45
- package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
- package/dist/resources/extensions/gsd/auto-start.js +13 -2
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
- package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
- package/dist/resources/extensions/gsd/auto.js +143 -96
- package/dist/resources/extensions/gsd/captures.js +9 -1
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +24 -3
- package/dist/resources/extensions/gsd/context-budget.js +2 -10
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
- package/dist/resources/extensions/gsd/doctor-format.js +15 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +62 -12
- package/dist/resources/extensions/gsd/doctor.js +204 -12
- package/dist/resources/extensions/gsd/exit-command.js +2 -1
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +47 -2
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- package/dist/resources/extensions/gsd/git-service.js +15 -12
- package/dist/resources/extensions/gsd/guided-flow.js +82 -32
- package/dist/resources/extensions/gsd/index.js +24 -20
- package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
- package/dist/resources/extensions/gsd/observability-validator.js +24 -0
- package/dist/resources/extensions/gsd/package.json +1 -1
- package/dist/resources/extensions/gsd/preferences-models.js +0 -12
- package/dist/resources/extensions/gsd/preferences-types.js +3 -2
- package/dist/resources/extensions/gsd/preferences-validation.js +101 -11
- package/dist/resources/extensions/gsd/preferences.js +8 -5
- package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +27 -10
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
- package/dist/resources/extensions/gsd/repo-identity.js +21 -4
- package/dist/resources/extensions/gsd/resource-version.js +2 -1
- package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
- package/dist/resources/extensions/gsd/worktree.js +35 -16
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- package/dist/resources/extensions/remote-questions/status.js +2 -1
- package/dist/resources/extensions/remote-questions/store.js +2 -1
- package/dist/resources/extensions/search-the-web/provider.js +2 -1
- package/dist/resources/extensions/subagent/index.js +12 -3
- package/dist/resources/extensions/subagent/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- package/dist/resources/extensions/universal-config/package.json +1 -1
- package/dist/welcome-screen.d.ts +12 -0
- package/dist/welcome-screen.js +53 -0
- package/package.json +2 -1
- package/packages/pi-ai/dist/env-api-keys.js +13 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +172 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +172 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +47 -764
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
- package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +2 -2
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
- package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
- package/packages/pi-ai/package.json +1 -0
- package/packages/pi-ai/src/env-api-keys.ts +14 -0
- package/packages/pi-ai/src/models.generated.ts +172 -0
- package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
- package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
- package/packages/pi-ai/src/providers/anthropic.ts +76 -868
- package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
- package/packages/pi-ai/src/types.ts +2 -0
- package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/index.ts +57 -1
- package/src/resources/extensions/env-utils.ts +31 -0
- package/src/resources/extensions/get-secrets-from-user.ts +5 -24
- package/src/resources/extensions/github-sync/cli.ts +364 -0
- package/src/resources/extensions/github-sync/index.ts +93 -0
- package/src/resources/extensions/github-sync/mapping.ts +81 -0
- package/src/resources/extensions/github-sync/sync.ts +556 -0
- package/src/resources/extensions/github-sync/templates.ts +183 -0
- package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
- package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
- package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
- package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
- package/src/resources/extensions/github-sync/types.ts +47 -0
- package/src/resources/extensions/gsd/auto/session.ts +7 -25
- package/src/resources/extensions/gsd/auto-dispatch.ts +100 -9
- package/src/resources/extensions/gsd/auto-loop.ts +484 -546
- package/src/resources/extensions/gsd/auto-post-unit.ts +92 -42
- package/src/resources/extensions/gsd/auto-prompts.ts +150 -48
- package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
- package/src/resources/extensions/gsd/auto-start.ts +18 -2
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
- package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
- package/src/resources/extensions/gsd/auto.ts +139 -101
- package/src/resources/extensions/gsd/captures.ts +10 -1
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +26 -4
- package/src/resources/extensions/gsd/context-budget.ts +2 -12
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
- package/src/resources/extensions/gsd/doctor-format.ts +20 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +64 -10
- package/src/resources/extensions/gsd/doctor-types.ts +16 -1
- package/src/resources/extensions/gsd/doctor.ts +199 -14
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +50 -3
- package/src/resources/extensions/gsd/forensics.ts +1 -1
- package/src/resources/extensions/gsd/git-service.ts +20 -10
- package/src/resources/extensions/gsd/guided-flow.ts +110 -38
- package/src/resources/extensions/gsd/index.ts +24 -17
- package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
- package/src/resources/extensions/gsd/observability-validator.ts +27 -0
- package/src/resources/extensions/gsd/preferences-models.ts +0 -12
- package/src/resources/extensions/gsd/preferences-types.ts +9 -5
- package/src/resources/extensions/gsd/preferences-validation.ts +92 -11
- package/src/resources/extensions/gsd/preferences.ts +8 -5
- package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -8
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +27 -10
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
- package/src/resources/extensions/gsd/repo-identity.ts +23 -4
- package/src/resources/extensions/gsd/resource-version.ts +3 -1
- package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/state.ts +1 -1
- package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
- package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +191 -3
- package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
- package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
- package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
- package/src/resources/extensions/gsd/types.ts +43 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
- package/src/resources/extensions/gsd/worktree.ts +35 -15
- package/src/resources/extensions/mcp-client/index.ts +17 -1
- package/src/resources/extensions/remote-questions/status.ts +3 -1
- package/src/resources/extensions/remote-questions/store.ts +3 -1
- package/src/resources/extensions/search-the-web/provider.ts +2 -1
- package/src/resources/extensions/subagent/index.ts +12 -3
- package/src/resources/extensions/subagent/isolation.ts +3 -1
- package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
- package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
- package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
- package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
- package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
- package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
- package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
- package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
- package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
- package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
- package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
- package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
deriveTaskGraph,
|
|
5
|
+
getReadyTasks,
|
|
6
|
+
chooseNonConflictingSubset,
|
|
7
|
+
isGraphAmbiguous,
|
|
8
|
+
detectDeadlock,
|
|
9
|
+
graphMetrics,
|
|
10
|
+
} from "../reactive-graph.ts";
|
|
11
|
+
import { parseTaskPlanIO } from "../files.ts";
|
|
12
|
+
import type { TaskIO, DerivedTaskNode } from "../types.ts";
|
|
13
|
+
|
|
14
|
+
// ─── parseTaskPlanIO ──────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
test("parseTaskPlanIO extracts backtick-wrapped file paths from Inputs and Expected Output", () => {
|
|
17
|
+
const content = `---
|
|
18
|
+
estimated_steps: 3
|
|
19
|
+
estimated_files: 2
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# T01: Setup Models
|
|
23
|
+
|
|
24
|
+
**Slice:** S01 — Core Setup
|
|
25
|
+
**Milestone:** M001
|
|
26
|
+
|
|
27
|
+
## Description
|
|
28
|
+
|
|
29
|
+
Create the core data models.
|
|
30
|
+
|
|
31
|
+
## Steps
|
|
32
|
+
|
|
33
|
+
1. Create types file
|
|
34
|
+
2. Create models file
|
|
35
|
+
|
|
36
|
+
## Must-Haves
|
|
37
|
+
|
|
38
|
+
- [ ] Type definitions complete
|
|
39
|
+
|
|
40
|
+
## Verification
|
|
41
|
+
|
|
42
|
+
- Run type checker
|
|
43
|
+
|
|
44
|
+
## Inputs
|
|
45
|
+
|
|
46
|
+
- \`src/types.ts\` — Existing type definitions from prior work
|
|
47
|
+
- \`src/config.json\` — Configuration schema
|
|
48
|
+
|
|
49
|
+
## Expected Output
|
|
50
|
+
|
|
51
|
+
- \`src/models.ts\` — New data model definitions
|
|
52
|
+
- \`src/models.test.ts\` — Unit tests for models
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const io = parseTaskPlanIO(content);
|
|
56
|
+
assert.deepEqual(io.inputFiles, ["src/types.ts", "src/config.json"]);
|
|
57
|
+
assert.deepEqual(io.outputFiles, ["src/models.ts", "src/models.test.ts"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("parseTaskPlanIO returns empty arrays for missing sections", () => {
|
|
61
|
+
const content = `# T01: Something\n\n## Description\n\nNo IO sections here.\n`;
|
|
62
|
+
const io = parseTaskPlanIO(content);
|
|
63
|
+
assert.deepEqual(io.inputFiles, []);
|
|
64
|
+
assert.deepEqual(io.outputFiles, []);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("parseTaskPlanIO ignores non-file-path backtick tokens", () => {
|
|
68
|
+
const content = `# T01: Test
|
|
69
|
+
|
|
70
|
+
## Inputs
|
|
71
|
+
|
|
72
|
+
- \`true\` — a boolean flag
|
|
73
|
+
- \`src/index.ts\` — main entry
|
|
74
|
+
- \`npm run test\` — a command, not a file
|
|
75
|
+
|
|
76
|
+
## Expected Output
|
|
77
|
+
|
|
78
|
+
- \`dist/bundle.js\` — compiled output
|
|
79
|
+
- \`false\` — not a file
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
const io = parseTaskPlanIO(content);
|
|
83
|
+
assert.deepEqual(io.inputFiles, ["src/index.ts"]);
|
|
84
|
+
assert.deepEqual(io.outputFiles, ["dist/bundle.js"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("parseTaskPlanIO handles multiple backtick tokens on one line", () => {
|
|
88
|
+
const content = `# T01: Multi
|
|
89
|
+
|
|
90
|
+
## Inputs
|
|
91
|
+
|
|
92
|
+
- \`src/a.ts\` and \`src/b.ts\` — both needed
|
|
93
|
+
|
|
94
|
+
## Expected Output
|
|
95
|
+
|
|
96
|
+
- \`src/c.ts\` — output
|
|
97
|
+
`;
|
|
98
|
+
const io = parseTaskPlanIO(content);
|
|
99
|
+
assert.deepEqual(io.inputFiles, ["src/a.ts", "src/b.ts"]);
|
|
100
|
+
assert.deepEqual(io.outputFiles, ["src/c.ts"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ─── deriveTaskGraph ──────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
test("deriveTaskGraph: linear chain T01→T02→T03", () => {
|
|
106
|
+
const tasks: TaskIO[] = [
|
|
107
|
+
{ id: "T01", title: "First", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
|
|
108
|
+
{ id: "T02", title: "Second", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false },
|
|
109
|
+
{ id: "T03", title: "Third", inputFiles: ["src/b.ts"], outputFiles: ["src/c.ts"], done: false },
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const graph = deriveTaskGraph(tasks);
|
|
113
|
+
assert.deepEqual(graph[0].dependsOn, []);
|
|
114
|
+
assert.deepEqual(graph[1].dependsOn, ["T01"]);
|
|
115
|
+
assert.deepEqual(graph[2].dependsOn, ["T02"]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("deriveTaskGraph: diamond dependency", () => {
|
|
119
|
+
const tasks: TaskIO[] = [
|
|
120
|
+
{ id: "T01", title: "Base", inputFiles: [], outputFiles: ["src/base.ts"], done: false },
|
|
121
|
+
{ id: "T02", title: "Left", inputFiles: ["src/base.ts"], outputFiles: ["src/left.ts"], done: false },
|
|
122
|
+
{ id: "T03", title: "Right", inputFiles: ["src/base.ts"], outputFiles: ["src/right.ts"], done: false },
|
|
123
|
+
{ id: "T04", title: "Merge", inputFiles: ["src/left.ts", "src/right.ts"], outputFiles: ["src/final.ts"], done: false },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const graph = deriveTaskGraph(tasks);
|
|
127
|
+
assert.deepEqual(graph[0].dependsOn, []);
|
|
128
|
+
assert.deepEqual(graph[1].dependsOn, ["T01"]);
|
|
129
|
+
assert.deepEqual(graph[2].dependsOn, ["T01"]);
|
|
130
|
+
assert.deepEqual(graph[3].dependsOn, ["T02", "T03"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("deriveTaskGraph: fully independent tasks", () => {
|
|
134
|
+
const tasks: TaskIO[] = [
|
|
135
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
|
|
136
|
+
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false },
|
|
137
|
+
{ id: "T03", title: "C", inputFiles: [], outputFiles: ["src/c.ts"], done: false },
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const graph = deriveTaskGraph(tasks);
|
|
141
|
+
assert.deepEqual(graph[0].dependsOn, []);
|
|
142
|
+
assert.deepEqual(graph[1].dependsOn, []);
|
|
143
|
+
assert.deepEqual(graph[2].dependsOn, []);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("deriveTaskGraph: self-referencing output→input is excluded", () => {
|
|
147
|
+
const tasks: TaskIO[] = [
|
|
148
|
+
{ id: "T01", title: "Self", inputFiles: ["src/a.ts"], outputFiles: ["src/a.ts"], done: false },
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const graph = deriveTaskGraph(tasks);
|
|
152
|
+
assert.deepEqual(graph[0].dependsOn, []);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ─── getReadyTasks ────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
test("getReadyTasks: partially completed graph", () => {
|
|
158
|
+
const tasks: TaskIO[] = [
|
|
159
|
+
{ id: "T01", title: "Base", inputFiles: [], outputFiles: ["src/a.ts"], done: true },
|
|
160
|
+
{ id: "T02", title: "Dep", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false },
|
|
161
|
+
{ id: "T03", title: "Blocked", inputFiles: ["src/b.ts"], outputFiles: ["src/c.ts"], done: false },
|
|
162
|
+
];
|
|
163
|
+
const graph = deriveTaskGraph(tasks);
|
|
164
|
+
const ready = getReadyTasks(graph, new Set(["T01"]), new Set());
|
|
165
|
+
assert.deepEqual(ready, ["T02"]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("getReadyTasks: nothing complete → only root tasks ready", () => {
|
|
169
|
+
const tasks: TaskIO[] = [
|
|
170
|
+
{ id: "T01", title: "Root", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
|
|
171
|
+
{ id: "T02", title: "Dep", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false },
|
|
172
|
+
];
|
|
173
|
+
const graph = deriveTaskGraph(tasks);
|
|
174
|
+
const ready = getReadyTasks(graph, new Set(), new Set());
|
|
175
|
+
assert.deepEqual(ready, ["T01"]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("getReadyTasks: all complete → empty", () => {
|
|
179
|
+
const tasks: TaskIO[] = [
|
|
180
|
+
{ id: "T01", title: "Done", inputFiles: [], outputFiles: ["src/a.ts"], done: true },
|
|
181
|
+
];
|
|
182
|
+
const graph = deriveTaskGraph(tasks);
|
|
183
|
+
const ready = getReadyTasks(graph, new Set(["T01"]), new Set());
|
|
184
|
+
assert.deepEqual(ready, []);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("getReadyTasks: in-flight tasks excluded", () => {
|
|
188
|
+
const tasks: TaskIO[] = [
|
|
189
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
|
|
190
|
+
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false },
|
|
191
|
+
];
|
|
192
|
+
const graph = deriveTaskGraph(tasks);
|
|
193
|
+
const ready = getReadyTasks(graph, new Set(), new Set(["T01"]));
|
|
194
|
+
assert.deepEqual(ready, ["T02"]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ─── chooseNonConflictingSubset ───────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
test("chooseNonConflictingSubset: output conflicts", () => {
|
|
200
|
+
const tasks: TaskIO[] = [
|
|
201
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/shared.ts"], done: false },
|
|
202
|
+
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/shared.ts"], done: false },
|
|
203
|
+
{ id: "T03", title: "C", inputFiles: [], outputFiles: ["src/other.ts"], done: false },
|
|
204
|
+
];
|
|
205
|
+
const graph = deriveTaskGraph(tasks);
|
|
206
|
+
const selected = chooseNonConflictingSubset(["T01", "T02", "T03"], graph, 3, new Set());
|
|
207
|
+
// T01 claims shared.ts, T02 conflicts, T03 is fine
|
|
208
|
+
assert.deepEqual(selected, ["T01", "T03"]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("chooseNonConflictingSubset: respects maxParallel", () => {
|
|
212
|
+
const tasks: TaskIO[] = [
|
|
213
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
|
|
214
|
+
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false },
|
|
215
|
+
{ id: "T03", title: "C", inputFiles: [], outputFiles: ["src/c.ts"], done: false },
|
|
216
|
+
];
|
|
217
|
+
const graph = deriveTaskGraph(tasks);
|
|
218
|
+
const selected = chooseNonConflictingSubset(["T01", "T02", "T03"], graph, 2, new Set());
|
|
219
|
+
assert.deepEqual(selected, ["T01", "T02"]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("chooseNonConflictingSubset: respects inFlightOutputs", () => {
|
|
223
|
+
const tasks: TaskIO[] = [
|
|
224
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
|
|
225
|
+
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false },
|
|
226
|
+
];
|
|
227
|
+
const graph = deriveTaskGraph(tasks);
|
|
228
|
+
const selected = chooseNonConflictingSubset(["T01", "T02"], graph, 4, new Set(["src/a.ts"]));
|
|
229
|
+
assert.deepEqual(selected, ["T02"]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ─── isGraphAmbiguous ─────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
test("isGraphAmbiguous: task with no IO → ambiguous", () => {
|
|
235
|
+
const graph: DerivedTaskNode[] = [
|
|
236
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: [], done: false, dependsOn: [] },
|
|
237
|
+
{ id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: [] },
|
|
238
|
+
];
|
|
239
|
+
assert.equal(isGraphAmbiguous(graph), true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("isGraphAmbiguous: all tasks have IO → not ambiguous", () => {
|
|
243
|
+
const graph: DerivedTaskNode[] = [
|
|
244
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false, dependsOn: [] },
|
|
245
|
+
{ id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: ["T01"] },
|
|
246
|
+
];
|
|
247
|
+
assert.equal(isGraphAmbiguous(graph), false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("isGraphAmbiguous: done tasks with no IO are ignored", () => {
|
|
251
|
+
const graph: DerivedTaskNode[] = [
|
|
252
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: [], done: true, dependsOn: [] },
|
|
253
|
+
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false, dependsOn: [] },
|
|
254
|
+
];
|
|
255
|
+
assert.equal(isGraphAmbiguous(graph), false);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ─── detectDeadlock ───────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
test("detectDeadlock: circular dependency detected", () => {
|
|
261
|
+
// T01 depends on T02, T02 depends on T01 — deadlock
|
|
262
|
+
const graph: DerivedTaskNode[] = [
|
|
263
|
+
{ id: "T01", title: "A", inputFiles: ["src/b.ts"], outputFiles: ["src/a.ts"], done: false, dependsOn: ["T02"] },
|
|
264
|
+
{ id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: ["T01"] },
|
|
265
|
+
];
|
|
266
|
+
assert.equal(detectDeadlock(graph, new Set(), new Set()), true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("detectDeadlock: normal blocked-waiting-for-in-flight → not deadlock", () => {
|
|
270
|
+
const graph: DerivedTaskNode[] = [
|
|
271
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false, dependsOn: [] },
|
|
272
|
+
{ id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: ["T01"] },
|
|
273
|
+
];
|
|
274
|
+
// T01 is in-flight, T02 is waiting → not deadlock
|
|
275
|
+
assert.equal(detectDeadlock(graph, new Set(), new Set(["T01"])), false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("detectDeadlock: all complete → not deadlock", () => {
|
|
279
|
+
const graph: DerivedTaskNode[] = [
|
|
280
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: true, dependsOn: [] },
|
|
281
|
+
];
|
|
282
|
+
assert.equal(detectDeadlock(graph, new Set(["T01"]), new Set()), false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ─── graphMetrics ─────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
test("graphMetrics computes correct values", () => {
|
|
288
|
+
const tasks: TaskIO[] = [
|
|
289
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: true },
|
|
290
|
+
{ id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false },
|
|
291
|
+
{ id: "T03", title: "C", inputFiles: [], outputFiles: ["src/c.ts"], done: false },
|
|
292
|
+
];
|
|
293
|
+
const graph = deriveTaskGraph(tasks);
|
|
294
|
+
const metrics = graphMetrics(graph);
|
|
295
|
+
assert.equal(metrics.taskCount, 3);
|
|
296
|
+
assert.equal(metrics.edgeCount, 1); // T02 depends on T01
|
|
297
|
+
assert.equal(metrics.readySetSize, 2); // T02 (T01 done) and T03 (no deps)
|
|
298
|
+
assert.equal(metrics.ambiguous, false);
|
|
299
|
+
});
|
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
5
|
|
|
6
|
-
import { externalGsdRoot, ensureGsdSymlink } from "../repo-identity.ts";
|
|
6
|
+
import { repoIdentity, externalGsdRoot, ensureGsdSymlink, validateProjectId } from "../repo-identity.ts";
|
|
7
7
|
import { createTestContext } from "./test-helpers.ts";
|
|
8
8
|
|
|
9
9
|
const { assertEq, assertTrue, report } = createTestContext();
|
|
@@ -57,6 +57,26 @@ async function main(): Promise<void> {
|
|
|
57
57
|
assertEq(preservedDirState, join(worktreePath, ".gsd"), "worktree .gsd directory is left in place for sync-based refresh");
|
|
58
58
|
assertTrue(lstatSync(join(worktreePath, ".gsd")).isDirectory(), "worktree .gsd directory remains a directory");
|
|
59
59
|
assertTrue(existsSync(join(worktreePath, ".gsd", "milestones", "stale.txt")), "existing worktree .gsd directory contents remain available for sync logic");
|
|
60
|
+
|
|
61
|
+
console.log("\n=== GSD_PROJECT_ID overrides computed repo hash ===");
|
|
62
|
+
process.env.GSD_PROJECT_ID = "my-project";
|
|
63
|
+
assertEq(repoIdentity(base), "my-project", "repoIdentity returns GSD_PROJECT_ID when set");
|
|
64
|
+
assertEq(externalGsdRoot(base), join(stateDir, "projects", "my-project"), "externalGsdRoot uses GSD_PROJECT_ID");
|
|
65
|
+
delete process.env.GSD_PROJECT_ID;
|
|
66
|
+
|
|
67
|
+
console.log("\n=== GSD_PROJECT_ID falls back to hash when unset ===");
|
|
68
|
+
const hashIdentity = repoIdentity(base);
|
|
69
|
+
assertTrue(/^[0-9a-f]{12}$/.test(hashIdentity), "repoIdentity returns 12-char hex hash when GSD_PROJECT_ID is unset");
|
|
70
|
+
|
|
71
|
+
console.log("\n=== validateProjectId rejects invalid values ===");
|
|
72
|
+
for (const invalid of ["has spaces", "path/traversal", "dot..dot", "back\\slash"]) {
|
|
73
|
+
assertTrue(!validateProjectId(invalid), `validateProjectId rejects invalid value: "${invalid}"`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log("\n=== validateProjectId accepts valid values ===");
|
|
77
|
+
for (const valid of ["my-project", "foo_bar", "abc123", "A-Z_0-9"]) {
|
|
78
|
+
assertTrue(validateProjectId(valid), `validateProjectId accepts valid value: "${valid}"`);
|
|
79
|
+
}
|
|
60
80
|
} finally {
|
|
61
81
|
delete process.env.GSD_STATE_DIR;
|
|
62
82
|
rmSync(base, { recursive: true, force: true });
|
|
@@ -210,7 +210,7 @@ async function main(): Promise<void> {
|
|
|
210
210
|
const sliceId = 'S01';
|
|
211
211
|
const uatPath = '.gsd/milestones/M001/slices/S01/S01-UAT.md';
|
|
212
212
|
const uatResultPath = '.gsd/milestones/M001/slices/S01/S01-UAT-RESULT.md';
|
|
213
|
-
const uatType = '
|
|
213
|
+
const uatType = 'live-runtime';
|
|
214
214
|
const inlinedContext = '<!-- no context -->';
|
|
215
215
|
|
|
216
216
|
let promptResult: string | undefined;
|
|
@@ -246,13 +246,21 @@ async function main(): Promise<void> {
|
|
|
246
246
|
promptResult?.includes(uatResultPath) ?? false,
|
|
247
247
|
`prompt contains uatResultPath value after substitution`,
|
|
248
248
|
);
|
|
249
|
+
assertTrue(
|
|
250
|
+
promptResult?.includes(`Detected UAT mode:** \`${uatType}\``) ?? false,
|
|
251
|
+
`prompt contains detected dynamic uatType value "${uatType}" after substitution`,
|
|
252
|
+
);
|
|
253
|
+
assertTrue(
|
|
254
|
+
promptResult?.includes(`uatType: ${uatType}`) ?? false,
|
|
255
|
+
`prompt contains dynamic uatType frontmatter value "${uatType}" after substitution`,
|
|
256
|
+
);
|
|
249
257
|
assertTrue(
|
|
250
258
|
!/\{\{[^}]+\}\}/.test(promptResult ?? ''),
|
|
251
259
|
'no unreplaced {{...}} tokens remain after variable substitution',
|
|
252
260
|
);
|
|
253
261
|
assertTrue(
|
|
254
|
-
/
|
|
255
|
-
'prompt contains
|
|
262
|
+
/browser|runtime|execute|run/i.test(promptResult ?? ''),
|
|
263
|
+
'prompt contains runtime execution language (browser/runtime/execute/run)',
|
|
256
264
|
);
|
|
257
265
|
assertTrue(
|
|
258
266
|
!/surfaced for human review/i.test(promptResult ?? ''),
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getMainBranch,
|
|
12
12
|
getSliceBranchName,
|
|
13
13
|
parseSliceBranch,
|
|
14
|
+
resolveProjectRoot,
|
|
14
15
|
setActiveMilestoneId,
|
|
15
16
|
SLICE_BRANCH_RE,
|
|
16
17
|
} from "../worktree.ts";
|
|
@@ -165,6 +166,52 @@ async function main(): Promise<void> {
|
|
|
165
166
|
rmSync(repo, { recursive: true, force: true });
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
// ── detectWorktreeName: symlink-resolved paths ───────────────────────────
|
|
170
|
+
console.log("\n=== detectWorktreeName (symlink-resolved paths) ===");
|
|
171
|
+
assertEq(
|
|
172
|
+
detectWorktreeName("/Users/fran/.gsd/projects/89e1c9ad49bf/worktrees/M001"),
|
|
173
|
+
"M001",
|
|
174
|
+
"detects milestone in symlink-resolved path",
|
|
175
|
+
);
|
|
176
|
+
assertEq(
|
|
177
|
+
detectWorktreeName("/Users/fran/.gsd/projects/abc123/worktrees/M002/subdir"),
|
|
178
|
+
"M002",
|
|
179
|
+
"detects milestone with trailing subdir in symlink-resolved path",
|
|
180
|
+
);
|
|
181
|
+
assertEq(
|
|
182
|
+
detectWorktreeName("/Users/fran/.gsd/projects/abc123"),
|
|
183
|
+
null,
|
|
184
|
+
"returns null for project root without worktrees segment",
|
|
185
|
+
);
|
|
186
|
+
assertEq(
|
|
187
|
+
detectWorktreeName("/foo/.gsd/worktrees/M001"),
|
|
188
|
+
"M001",
|
|
189
|
+
"still detects direct layout path",
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// ── resolveProjectRoot: symlink-resolved paths ──────────────────────────
|
|
193
|
+
console.log("\n=== resolveProjectRoot (symlink-resolved paths) ===");
|
|
194
|
+
assertEq(
|
|
195
|
+
resolveProjectRoot("/Users/fran/.gsd/projects/89e1c9ad49bf/worktrees/M001"),
|
|
196
|
+
"/Users/fran",
|
|
197
|
+
"resolves to user home for symlink-resolved path",
|
|
198
|
+
);
|
|
199
|
+
assertEq(
|
|
200
|
+
resolveProjectRoot("/foo/.gsd/worktrees/M001"),
|
|
201
|
+
"/foo",
|
|
202
|
+
"still resolves direct layout path",
|
|
203
|
+
);
|
|
204
|
+
assertEq(
|
|
205
|
+
resolveProjectRoot("/some/repo"),
|
|
206
|
+
"/some/repo",
|
|
207
|
+
"returns unchanged for non-worktree path",
|
|
208
|
+
);
|
|
209
|
+
assertEq(
|
|
210
|
+
resolveProjectRoot("/data/.gsd/projects/deadbeef/worktrees/M003/nested"),
|
|
211
|
+
"/data",
|
|
212
|
+
"resolves correctly with nested subdirs after worktree name",
|
|
213
|
+
);
|
|
214
|
+
|
|
168
215
|
rmSync(base, { recursive: true, force: true });
|
|
169
216
|
report();
|
|
170
217
|
}
|
|
@@ -423,7 +423,6 @@ export interface Requirement {
|
|
|
423
423
|
|
|
424
424
|
// ─── Parallel Orchestration Types ────────────────────────────────────────
|
|
425
425
|
|
|
426
|
-
export type CompressionStrategy = "truncate" | "compress";
|
|
427
426
|
export type ContextSelectionMode = "full" | "smart";
|
|
428
427
|
|
|
429
428
|
export type MergeStrategy = "per-slice" | "per-milestone";
|
|
@@ -436,3 +435,46 @@ export interface ParallelConfig {
|
|
|
436
435
|
merge_strategy: MergeStrategy;
|
|
437
436
|
auto_merge: AutoMergeMode;
|
|
438
437
|
}
|
|
438
|
+
|
|
439
|
+
// ─── Reactive Task Execution Types ───────────────────────────────────────
|
|
440
|
+
|
|
441
|
+
/** IO signature extracted from a single task plan's Inputs/Expected Output sections. */
|
|
442
|
+
export interface TaskIO {
|
|
443
|
+
id: string; // e.g. "T01"
|
|
444
|
+
title: string;
|
|
445
|
+
inputFiles: string[];
|
|
446
|
+
outputFiles: string[];
|
|
447
|
+
done: boolean;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** A task node with derived dependency edges from input/output intersection. */
|
|
451
|
+
export interface DerivedTaskNode extends TaskIO {
|
|
452
|
+
/** IDs of tasks whose outputFiles overlap with this task's inputFiles. */
|
|
453
|
+
dependsOn: string[];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Configuration for reactive (graph-derived parallel) task execution within a slice. */
|
|
457
|
+
export interface ReactiveExecutionConfig {
|
|
458
|
+
enabled: boolean;
|
|
459
|
+
/** Maximum number of tasks to dispatch in parallel. Clamped to 1–8. */
|
|
460
|
+
max_parallel: number;
|
|
461
|
+
/** Isolation mode for parallel tasks within a slice. Currently only "same-tree" is supported. */
|
|
462
|
+
isolation_mode: "same-tree";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Per-slice reactive execution runtime state, persisted to disk. */
|
|
466
|
+
export interface ReactiveExecutionState {
|
|
467
|
+
sliceId: string;
|
|
468
|
+
/** Task IDs that have been verified as completed. */
|
|
469
|
+
completed: string[];
|
|
470
|
+
/** Task IDs dispatched in the current/most recent reactive batch. */
|
|
471
|
+
dispatched: string[];
|
|
472
|
+
/** Snapshot of the graph at last dispatch. */
|
|
473
|
+
graphSnapshot: {
|
|
474
|
+
taskCount: number;
|
|
475
|
+
edgeCount: number;
|
|
476
|
+
readySetSize: number;
|
|
477
|
+
ambiguous: boolean;
|
|
478
|
+
};
|
|
479
|
+
updatedAt: string;
|
|
480
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
4
4
|
import { deriveState } from './state.js';
|
|
5
5
|
import { parseRoadmap, parsePlan, parseSummary, loadFile } from './files.js';
|
|
6
|
-
import { findMilestoneIds } from './
|
|
6
|
+
import { findMilestoneIds } from './milestone-ids.js';
|
|
7
7
|
import { resolveMilestoneFile, resolveSliceFile, resolveGsdRootFile } from './paths.js';
|
|
8
8
|
import {
|
|
9
9
|
getLedger,
|
|
@@ -67,40 +67,60 @@ export function captureIntegrationBranch(basePath: string, milestoneId: string,
|
|
|
67
67
|
|
|
68
68
|
// ─── Pure Utility Functions (unchanged) ────────────────────────────────────
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Find the worktrees segment in a path, supporting both direct
|
|
72
|
+
* (`/.gsd/worktrees/`) and symlink-resolved (`/.gsd/projects/<hash>/worktrees/`)
|
|
73
|
+
* layouts. When `.gsd` is a symlink to `~/.gsd/projects/<hash>`, resolved
|
|
74
|
+
* paths contain the intermediate `projects/<hash>/` segment that the old
|
|
75
|
+
* single-marker check missed.
|
|
76
|
+
*/
|
|
77
|
+
function findWorktreeSegment(normalizedPath: string): { gsdIdx: number; afterWorktrees: number } | null {
|
|
78
|
+
// Direct layout: /.gsd/worktrees/<name>
|
|
79
|
+
const directMarker = "/.gsd/worktrees/";
|
|
80
|
+
const idx = normalizedPath.indexOf(directMarker);
|
|
81
|
+
if (idx !== -1) {
|
|
82
|
+
return { gsdIdx: idx, afterWorktrees: idx + directMarker.length };
|
|
83
|
+
}
|
|
84
|
+
// Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/<name>
|
|
85
|
+
const symlinkRe = /\/\.gsd\/projects\/[a-f0-9]+\/worktrees\//;
|
|
86
|
+
const match = normalizedPath.match(symlinkRe);
|
|
87
|
+
if (match && match.index !== undefined) {
|
|
88
|
+
return { gsdIdx: match.index, afterWorktrees: match.index + match[0].length };
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
70
93
|
/**
|
|
71
94
|
* Detect the active worktree name from the current working directory.
|
|
72
95
|
* Returns null if not inside a GSD worktree (.gsd/worktrees/<name>/).
|
|
73
96
|
*/
|
|
74
97
|
export function detectWorktreeName(basePath: string): string | null {
|
|
75
98
|
const normalizedPath = basePath.replaceAll("\\", "/");
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const afterMarker = normalizedPath.slice(idx + marker.length);
|
|
99
|
+
const seg = findWorktreeSegment(normalizedPath);
|
|
100
|
+
if (!seg) return null;
|
|
101
|
+
const afterMarker = normalizedPath.slice(seg.afterWorktrees);
|
|
80
102
|
const name = afterMarker.split("/")[0];
|
|
81
103
|
return name || null;
|
|
82
104
|
}
|
|
83
105
|
|
|
84
106
|
/**
|
|
85
107
|
* Resolve the project root from a path that may be inside a worktree.
|
|
86
|
-
* If the path contains
|
|
87
|
-
*
|
|
108
|
+
* If the path contains a worktrees segment, returns the portion before
|
|
109
|
+
* `/.gsd/`. Otherwise returns the input unchanged.
|
|
88
110
|
*
|
|
89
111
|
* Use this in commands that call `process.cwd()` to ensure they always
|
|
90
112
|
* operate against the real project root, not a worktree subdirectory.
|
|
91
113
|
*/
|
|
92
114
|
export function resolveProjectRoot(basePath: string): string {
|
|
93
115
|
const normalizedPath = basePath.replaceAll("\\", "/");
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// Return the original path up to the .gsd/ marker (un-normalized)
|
|
98
|
-
// Account for potential OS-specific separators
|
|
116
|
+
const seg = findWorktreeSegment(normalizedPath);
|
|
117
|
+
if (!seg) return basePath;
|
|
118
|
+
// Return the original path up to the /.gsd/ boundary
|
|
99
119
|
const sep = basePath.includes("\\") ? "\\" : "/";
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
return basePath.slice(0,
|
|
120
|
+
const gsdMarker = `${sep}.gsd${sep}`;
|
|
121
|
+
const gsdIdx = basePath.indexOf(gsdMarker);
|
|
122
|
+
if (gsdIdx !== -1) return basePath.slice(0, gsdIdx);
|
|
123
|
+
return basePath.slice(0, seg.gsdIdx);
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
/**
|
|
@@ -114,6 +114,22 @@ function getServerConfig(name: string): McpServerConfig | undefined {
|
|
|
114
114
|
return readConfigs().find((s) => s.name === name);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/** Resolve ${VAR} references in env values against process.env. */
|
|
118
|
+
function resolveEnv(env: Record<string, string>): Record<string, string> {
|
|
119
|
+
const resolved: Record<string, string> = {};
|
|
120
|
+
for (const [key, value] of Object.entries(env)) {
|
|
121
|
+
if (typeof value === "string") {
|
|
122
|
+
resolved[key] = value.replace(
|
|
123
|
+
/\$\{([^}]+)\}/g,
|
|
124
|
+
(_match, varName) => process.env[varName] ?? "",
|
|
125
|
+
);
|
|
126
|
+
} else {
|
|
127
|
+
resolved[key] = value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return resolved;
|
|
131
|
+
}
|
|
132
|
+
|
|
117
133
|
async function getOrConnect(name: string, signal?: AbortSignal): Promise<Client> {
|
|
118
134
|
const existing = connections.get(name);
|
|
119
135
|
if (existing) return existing.client;
|
|
@@ -128,7 +144,7 @@ async function getOrConnect(name: string, signal?: AbortSignal): Promise<Client>
|
|
|
128
144
|
transport = new StdioClientTransport({
|
|
129
145
|
command: config.command,
|
|
130
146
|
args: config.args,
|
|
131
|
-
env: config.env ? { ...process.env, ...config.env } as Record<string, string> : undefined,
|
|
147
|
+
env: config.env ? { ...process.env, ...resolveEnv(config.env) } as Record<string, string> : undefined,
|
|
132
148
|
cwd: config.cwd,
|
|
133
149
|
stderr: "pipe",
|
|
134
150
|
});
|
|
@@ -7,6 +7,8 @@ import { join } from "node:path";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { readPromptRecord } from "./store.js";
|
|
9
9
|
|
|
10
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
11
|
+
|
|
10
12
|
export interface LatestPromptSummary {
|
|
11
13
|
id: string;
|
|
12
14
|
status: string;
|
|
@@ -14,7 +16,7 @@ export interface LatestPromptSummary {
|
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export function getLatestPromptSummary(): LatestPromptSummary | null {
|
|
17
|
-
const runtimeDir = join(
|
|
19
|
+
const runtimeDir = join(gsdHome, "runtime", "remote-questions");
|
|
18
20
|
if (!existsSync(runtimeDir)) return null;
|
|
19
21
|
const files = readdirSync(runtimeDir).filter((f) => f.endsWith(".json"));
|
|
20
22
|
if (files.length === 0) return null;
|
|
@@ -7,8 +7,10 @@ import { join } from "node:path";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import type { RemotePrompt, RemotePromptRecord, RemotePromptRef, RemoteAnswer, RemotePromptStatus } from "./types.js";
|
|
9
9
|
|
|
10
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
11
|
+
|
|
10
12
|
function runtimeDir(): string {
|
|
11
|
-
return join(
|
|
13
|
+
return join(gsdHome, "runtime", "remote-questions");
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
function recordPath(id: string): string {
|
|
@@ -17,7 +17,8 @@ import { resolveSearchProviderFromPreferences } from '../gsd/preferences.js'
|
|
|
17
17
|
// Compute authFilePath locally instead of importing from app-paths.ts,
|
|
18
18
|
// because extensions are copied to ~/.gsd/agent/extensions/ at runtime
|
|
19
19
|
// where the relative import '../../../app-paths.ts' doesn't resolve.
|
|
20
|
-
const
|
|
20
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), '.gsd')
|
|
21
|
+
const authFilePath = join(gsdHome, 'agent', 'auth.json')
|
|
21
22
|
|
|
22
23
|
export type SearchProvider = 'tavily' | 'brave' | 'ollama'
|
|
23
24
|
export type SearchProviderPreference = SearchProvider | 'auto'
|