gsd-pi 2.78.1-dev.b6a389b66 → 2.78.1-dev.d8826a445
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +7 -2
- package/dist/resources/extensions/gsd/auto/session.js +3 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +3 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -40
- package/dist/resources/extensions/gsd/auto.js +62 -1
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -16
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- package/dist/resources/extensions/gsd/db-writer.js +96 -16
- package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
- package/dist/resources/extensions/gsd/gsd-db.js +194 -0
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +117 -25
- package/dist/resources/extensions/gsd/metrics.js +287 -1
- package/dist/resources/extensions/gsd/paths.js +79 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/dist/resources/extensions/gsd/templates/project.md +10 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
- package/dist/resources/extensions/gsd/workspace.js +59 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +15 -2
- package/dist/resources/extensions/gsd/write-intercept.js +3 -3
- 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/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/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/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/package.json +1 -1
- package/packages/mcp-server/README.md +2 -11
- package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +28 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +94 -4
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +226 -0
- package/packages/mcp-server/src/remote-questions.test.ts +103 -0
- package/packages/mcp-server/src/remote-questions.ts +35 -0
- package/packages/mcp-server/src/server.ts +129 -6
- package/packages/mcp-server/src/workflow-tools.ts +1 -1
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +8 -2
- package/src/resources/extensions/gsd/auto/session.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +8 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +225 -47
- package/src/resources/extensions/gsd/auto.ts +79 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +17 -17
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
- package/src/resources/extensions/gsd/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/gsd-db.ts +184 -0
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +154 -25
- package/src/resources/extensions/gsd/metrics.ts +321 -1
- package/src/resources/extensions/gsd/paths.ts +67 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/src/resources/extensions/gsd/templates/project.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
- package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
- package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +371 -0
- package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
- package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
- package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +74 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +28 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +453 -0
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/workspace.test.ts +190 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +67 -52
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
- package/src/resources/extensions/gsd/workspace.ts +95 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +16 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// GSD-2 + Unified path normalization tests: normalizeRealPath and tryRealpath parity
|
|
2
|
+
|
|
3
|
+
import { describe, test, before, beforeEach, afterEach } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import {
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import { normalizeRealPath } from "../paths.ts";
|
|
17
|
+
import { createWorkspace } from "../workspace.ts";
|
|
18
|
+
|
|
19
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detect whether the filesystem hosting `dir` is case-insensitive.
|
|
23
|
+
* Creates a file with a lowercase name, then probes it via an uppercase name.
|
|
24
|
+
*/
|
|
25
|
+
function isCaseInsensitiveFs(dir: string): boolean {
|
|
26
|
+
const lower = join(dir, "ci_probe_lower.txt");
|
|
27
|
+
const upper = join(dir, "CI_PROBE_LOWER.TXT");
|
|
28
|
+
try {
|
|
29
|
+
writeFileSync(lower, "probe");
|
|
30
|
+
return existsSync(upper);
|
|
31
|
+
} finally {
|
|
32
|
+
try { rmSync(lower); } catch { /* ignore */ }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeProjectDir(): string {
|
|
37
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-pathnorm-")));
|
|
38
|
+
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Suite 1: normalizeRealPath and tryRealpath return identical strings ──────
|
|
43
|
+
|
|
44
|
+
describe("normalizeRealPath and tryRealpath produce identical results", () => {
|
|
45
|
+
let projectDir: string;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
projectDir = makeProjectDir();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("normalizeRealPath on an existing directory matches realpathSync.native", () => {
|
|
56
|
+
const result = normalizeRealPath(projectDir);
|
|
57
|
+
// realpathSync.native is the canonical form — result must equal it
|
|
58
|
+
const expected = realpathSync.native(projectDir);
|
|
59
|
+
assert.equal(result, expected);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("createWorkspace identityKey equals normalizeRealPath of projectRoot", () => {
|
|
63
|
+
const ws = createWorkspace(projectDir);
|
|
64
|
+
// identityKey is computed via tryRealpath → normalizeRealPath
|
|
65
|
+
assert.equal(ws.identityKey, normalizeRealPath(ws.projectRoot));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("createWorkspace identityKey and normalizeRealPath agree on the same input", () => {
|
|
69
|
+
const ws = createWorkspace(projectDir);
|
|
70
|
+
const direct = normalizeRealPath(projectDir);
|
|
71
|
+
assert.equal(ws.identityKey, direct);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ─── Suite 2: non-existent path fallback ─────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
describe("normalizeRealPath: fallback for non-existent paths", () => {
|
|
78
|
+
test("returns a resolved (absolute) path for a non-existent input", () => {
|
|
79
|
+
const ghost = join(tmpdir(), "gsd-pathnorm-ghost-does-not-exist-" + Date.now());
|
|
80
|
+
const result = normalizeRealPath(ghost);
|
|
81
|
+
// Must be a string, must be absolute, must not throw
|
|
82
|
+
assert.equal(typeof result, "string");
|
|
83
|
+
assert.ok(result.startsWith("/") || /^[A-Za-z]:/.test(result), "result must be absolute");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("normalizeRealPath of a non-existent path is idempotent", () => {
|
|
87
|
+
const ghost = join(tmpdir(), "gsd-pathnorm-ghost2-" + Date.now());
|
|
88
|
+
const first = normalizeRealPath(ghost);
|
|
89
|
+
const second = normalizeRealPath(first);
|
|
90
|
+
assert.equal(first, second);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ─── Suite 3: idempotency on existing paths ───────────────────────────────────
|
|
95
|
+
|
|
96
|
+
describe("normalizeRealPath: idempotency on existing paths", () => {
|
|
97
|
+
let projectDir: string;
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
projectDir = makeProjectDir();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("normalizeRealPath of a normalizeRealPath result is the same", () => {
|
|
108
|
+
const once = normalizeRealPath(projectDir);
|
|
109
|
+
const twice = normalizeRealPath(once);
|
|
110
|
+
assert.equal(once, twice);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("createWorkspace identityKey is idempotent across two calls with same path", () => {
|
|
114
|
+
const ws1 = createWorkspace(projectDir);
|
|
115
|
+
const ws2 = createWorkspace(projectDir);
|
|
116
|
+
assert.equal(ws1.identityKey, ws2.identityKey);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ─── Suite 4: case-insensitive filesystem (macOS HFS+/APFS) ──────────────────
|
|
121
|
+
|
|
122
|
+
describe("normalizeRealPath: case normalization on case-insensitive volumes", () => {
|
|
123
|
+
let projectDir: string;
|
|
124
|
+
let caseInsensitive: boolean;
|
|
125
|
+
|
|
126
|
+
before(() => {
|
|
127
|
+
// Detect FS case sensitivity once for the suite
|
|
128
|
+
const probe = mkdtempSync(join(tmpdir(), "gsd-ci-probe-"));
|
|
129
|
+
caseInsensitive = isCaseInsensitiveFs(probe);
|
|
130
|
+
rmSync(probe, { recursive: true, force: true });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
projectDir = makeProjectDir();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
afterEach(() => {
|
|
138
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("upper and lower case paths resolve to the same canonical string on case-insensitive FS", (t) => {
|
|
142
|
+
if (!caseInsensitive) {
|
|
143
|
+
t.skip("case-sensitive filesystem — case-normalization check not applicable");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const lower = projectDir.toLowerCase();
|
|
148
|
+
const upper = projectDir.toUpperCase();
|
|
149
|
+
|
|
150
|
+
const canonicalFromLower = normalizeRealPath(lower);
|
|
151
|
+
const canonicalFromUpper = normalizeRealPath(upper);
|
|
152
|
+
|
|
153
|
+
assert.equal(
|
|
154
|
+
canonicalFromLower,
|
|
155
|
+
canonicalFromUpper,
|
|
156
|
+
"both case variants must resolve to the same canonical path on case-insensitive FS",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("createWorkspace identityKey is case-stable on case-insensitive FS", (t) => {
|
|
161
|
+
if (!caseInsensitive) {
|
|
162
|
+
t.skip("case-sensitive filesystem — case-normalization check not applicable");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const wsLower = createWorkspace(projectDir.toLowerCase());
|
|
167
|
+
const wsUpper = createWorkspace(projectDir.toUpperCase());
|
|
168
|
+
|
|
169
|
+
assert.equal(
|
|
170
|
+
wsLower.identityKey,
|
|
171
|
+
wsUpper.identityKey,
|
|
172
|
+
"identityKey must be identical regardless of input case on case-insensitive FS",
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// GSD2 — paths cache normalization and clearPathCache() invalidation tests
|
|
2
|
+
|
|
3
|
+
import { describe, test, beforeEach, afterEach } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { mkdtempSync, mkdirSync, rmSync, renameSync, realpathSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
import { gsdRoot, clearPathCache, _clearGsdRootCache } from '../paths.ts';
|
|
10
|
+
|
|
11
|
+
describe('gsdRootCache key normalization', () => {
|
|
12
|
+
let projectDir: string;
|
|
13
|
+
let fakeHome: string;
|
|
14
|
+
let savedHome: string | undefined;
|
|
15
|
+
let savedUserProfile: string | undefined;
|
|
16
|
+
let savedGsdHome: string | undefined;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-cache-norm-')));
|
|
20
|
+
mkdirSync(join(projectDir, '.gsd'), { recursive: true });
|
|
21
|
+
|
|
22
|
+
fakeHome = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-cache-home-')));
|
|
23
|
+
|
|
24
|
+
savedHome = process.env.HOME;
|
|
25
|
+
savedUserProfile = process.env.USERPROFILE;
|
|
26
|
+
savedGsdHome = process.env.GSD_HOME;
|
|
27
|
+
|
|
28
|
+
// Point HOME and GSD_HOME at an unrelated temp dir to prevent ~/.gsd interference.
|
|
29
|
+
process.env.HOME = fakeHome;
|
|
30
|
+
process.env.USERPROFILE = fakeHome;
|
|
31
|
+
process.env.GSD_HOME = join(fakeHome, '.gsd');
|
|
32
|
+
|
|
33
|
+
_clearGsdRootCache();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
if (savedHome === undefined) delete process.env.HOME;
|
|
38
|
+
else process.env.HOME = savedHome;
|
|
39
|
+
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
|
40
|
+
else process.env.USERPROFILE = savedUserProfile;
|
|
41
|
+
if (savedGsdHome === undefined) delete process.env.GSD_HOME;
|
|
42
|
+
else process.env.GSD_HOME = savedGsdHome;
|
|
43
|
+
|
|
44
|
+
clearPathCache();
|
|
45
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
46
|
+
rmSync(fakeHome, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('gsdRoot with trailing slash returns same result as without', () => {
|
|
50
|
+
const withoutSlash = gsdRoot(projectDir);
|
|
51
|
+
_clearGsdRootCache();
|
|
52
|
+
const withSlash = gsdRoot(projectDir + '/');
|
|
53
|
+
|
|
54
|
+
assert.equal(
|
|
55
|
+
withoutSlash,
|
|
56
|
+
withSlash,
|
|
57
|
+
'gsdRoot must return the same path regardless of trailing slash',
|
|
58
|
+
);
|
|
59
|
+
assert.equal(
|
|
60
|
+
withoutSlash,
|
|
61
|
+
join(projectDir, '.gsd'),
|
|
62
|
+
'both calls should resolve to projectDir/.gsd',
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('second call with trailing slash hits cache set by first call without slash', () => {
|
|
67
|
+
// Prime the cache with the no-slash form.
|
|
68
|
+
const first = gsdRoot(projectDir);
|
|
69
|
+
// Now remove .gsd so a fresh probe would return a different path.
|
|
70
|
+
renameSync(join(projectDir, '.gsd'), join(projectDir, '.gsd-hidden'));
|
|
71
|
+
// Call with trailing slash — must hit the normalized cache entry (no re-probe).
|
|
72
|
+
const second = gsdRoot(projectDir + '/');
|
|
73
|
+
// Restore for cleanup.
|
|
74
|
+
renameSync(join(projectDir, '.gsd-hidden'), join(projectDir, '.gsd'));
|
|
75
|
+
|
|
76
|
+
assert.equal(
|
|
77
|
+
second,
|
|
78
|
+
first,
|
|
79
|
+
'trailing-slash call must return cached result from the no-slash call',
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('clearPathCache() does NOT invalidate gsdRootCache (process-lifetime semantics)', () => {
|
|
85
|
+
let projectDir: string;
|
|
86
|
+
let fakeHome: string;
|
|
87
|
+
let savedHome: string | undefined;
|
|
88
|
+
let savedUserProfile: string | undefined;
|
|
89
|
+
let savedGsdHome: string | undefined;
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-cache-clear-')));
|
|
93
|
+
mkdirSync(join(projectDir, '.gsd'), { recursive: true });
|
|
94
|
+
|
|
95
|
+
fakeHome = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-cache-home2-')));
|
|
96
|
+
|
|
97
|
+
savedHome = process.env.HOME;
|
|
98
|
+
savedUserProfile = process.env.USERPROFILE;
|
|
99
|
+
savedGsdHome = process.env.GSD_HOME;
|
|
100
|
+
|
|
101
|
+
process.env.HOME = fakeHome;
|
|
102
|
+
process.env.USERPROFILE = fakeHome;
|
|
103
|
+
process.env.GSD_HOME = join(fakeHome, '.gsd');
|
|
104
|
+
|
|
105
|
+
_clearGsdRootCache();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
if (savedHome === undefined) delete process.env.HOME;
|
|
110
|
+
else process.env.HOME = savedHome;
|
|
111
|
+
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
|
112
|
+
else process.env.USERPROFILE = savedUserProfile;
|
|
113
|
+
if (savedGsdHome === undefined) delete process.env.GSD_HOME;
|
|
114
|
+
else process.env.GSD_HOME = savedGsdHome;
|
|
115
|
+
|
|
116
|
+
_clearGsdRootCache();
|
|
117
|
+
clearPathCache();
|
|
118
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
119
|
+
rmSync(fakeHome, { recursive: true, force: true });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('clearPathCache() does NOT evict a cached gsdRoot result', (t) => {
|
|
123
|
+
// Prime the cache.
|
|
124
|
+
const firstResult = gsdRoot(projectDir);
|
|
125
|
+
assert.equal(firstResult, join(projectDir, '.gsd'));
|
|
126
|
+
|
|
127
|
+
// Remove .gsd so a fresh probe would return a different (fallback) result.
|
|
128
|
+
renameSync(join(projectDir, '.gsd'), join(projectDir, '.gsd-hidden'));
|
|
129
|
+
t.after(() => {
|
|
130
|
+
try { renameSync(join(projectDir, '.gsd-hidden'), join(projectDir, '.gsd')); } catch { /* ignore */ }
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// clearPathCache() only clears volatile dir caches — gsdRootCache is untouched.
|
|
134
|
+
clearPathCache();
|
|
135
|
+
const afterClearPath = gsdRoot(projectDir);
|
|
136
|
+
assert.equal(
|
|
137
|
+
afterClearPath,
|
|
138
|
+
firstResult,
|
|
139
|
+
'clearPathCache must NOT evict gsdRootCache — result must still be the cached value',
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('_clearGsdRootCache() DOES evict gsdRootCache, causing re-probe', (t) => {
|
|
144
|
+
// Prime the cache.
|
|
145
|
+
const firstResult = gsdRoot(projectDir);
|
|
146
|
+
assert.equal(firstResult, join(projectDir, '.gsd'));
|
|
147
|
+
|
|
148
|
+
// Remove .gsd so a fresh probe returns the creation fallback.
|
|
149
|
+
renameSync(join(projectDir, '.gsd'), join(projectDir, '.gsd-hidden'));
|
|
150
|
+
t.after(() => {
|
|
151
|
+
try { renameSync(join(projectDir, '.gsd-hidden'), join(projectDir, '.gsd')); } catch { /* ignore */ }
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// _clearGsdRootCache() evicts the entry — next call re-probes.
|
|
155
|
+
_clearGsdRootCache();
|
|
156
|
+
const afterClearRoot = gsdRoot(projectDir);
|
|
157
|
+
assert.equal(
|
|
158
|
+
afterClearRoot,
|
|
159
|
+
join(projectDir, '.gsd'),
|
|
160
|
+
'after _clearGsdRootCache, gsdRoot must re-probe and return creation fallback',
|
|
161
|
+
);
|
|
162
|
+
// The two results are equal (same path) but the key point is re-probe occurred;
|
|
163
|
+
// the cached firstResult also happened to equal the fallback path.
|
|
164
|
+
// Verify: if we prime again without removing .gsd, clearing root re-probes to gsd.
|
|
165
|
+
renameSync(join(projectDir, '.gsd-hidden'), join(projectDir, '.gsd'));
|
|
166
|
+
_clearGsdRootCache();
|
|
167
|
+
const reprobe = gsdRoot(projectDir);
|
|
168
|
+
assert.equal(reprobe, join(projectDir, '.gsd'), 're-probe after restore returns .gsd');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// GSD-2 + Tests for MilestoneScope pinning in pendingAutoStartMap (C1 regression guard)
|
|
2
|
+
|
|
3
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { mkdtempSync, mkdirSync, rmSync, realpathSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
setPendingAutoStart,
|
|
11
|
+
clearPendingAutoStart,
|
|
12
|
+
_getPendingAutoStart,
|
|
13
|
+
} from "../guided-flow.ts";
|
|
14
|
+
|
|
15
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function makeProjectDir(): string {
|
|
18
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-pas-scope-")));
|
|
19
|
+
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
describe("pendingAutoStart scope pinning (C1)", () => {
|
|
26
|
+
let base: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
clearPendingAutoStart();
|
|
30
|
+
base = makeProjectDir();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
clearPendingAutoStart();
|
|
35
|
+
if (base) {
|
|
36
|
+
rmSync(base, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("setPendingAutoStart stores a scope whose paths derive from the basePath at reservation time", () => {
|
|
41
|
+
setPendingAutoStart(base, { basePath: base, milestoneId: "M001" });
|
|
42
|
+
|
|
43
|
+
const entry = _getPendingAutoStart(base);
|
|
44
|
+
assert.ok(entry, "entry should exist");
|
|
45
|
+
assert.ok(entry.scope, "entry.scope should be set");
|
|
46
|
+
assert.equal(entry.scope.milestoneId, "M001");
|
|
47
|
+
|
|
48
|
+
const expectedContext = join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md");
|
|
49
|
+
const expectedRoadmap = join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md");
|
|
50
|
+
const expectedState = join(base, ".gsd", "STATE.md");
|
|
51
|
+
|
|
52
|
+
assert.equal(entry.scope.contextFile(), expectedContext);
|
|
53
|
+
assert.equal(entry.scope.roadmapFile(), expectedRoadmap);
|
|
54
|
+
assert.equal(entry.scope.stateFile(), expectedState);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("scope paths are unaffected by process.chdir after reservation", (t) => {
|
|
58
|
+
setPendingAutoStart(base, { basePath: base, milestoneId: "M002" });
|
|
59
|
+
|
|
60
|
+
const entry = _getPendingAutoStart(base);
|
|
61
|
+
assert.ok(entry, "entry should exist");
|
|
62
|
+
|
|
63
|
+
// Capture paths before cwd change
|
|
64
|
+
const ctxBefore = entry.scope.contextFile();
|
|
65
|
+
const roadmapBefore = entry.scope.roadmapFile();
|
|
66
|
+
const stateBefore = entry.scope.stateFile();
|
|
67
|
+
|
|
68
|
+
// Change cwd to a different directory, then check that scope is unchanged
|
|
69
|
+
const originalCwd = process.cwd();
|
|
70
|
+
const altDir = mkdtempSync(join(tmpdir(), "gsd-cwd-alt-"));
|
|
71
|
+
t.after(() => {
|
|
72
|
+
process.chdir(originalCwd);
|
|
73
|
+
rmSync(altDir, { recursive: true, force: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
process.chdir(altDir);
|
|
77
|
+
|
|
78
|
+
assert.equal(entry.scope.contextFile(), ctxBefore, "contextFile must not change after cwd drift");
|
|
79
|
+
assert.equal(entry.scope.roadmapFile(), roadmapBefore, "roadmapFile must not change after cwd drift");
|
|
80
|
+
assert.equal(entry.scope.stateFile(), stateBefore, "stateFile must not change after cwd drift");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("scope identityKey matches the realpath of the original basePath even with trailing slash", () => {
|
|
84
|
+
const baseWithSlash = base + "/";
|
|
85
|
+
setPendingAutoStart(base, { basePath: baseWithSlash, milestoneId: "M003" });
|
|
86
|
+
|
|
87
|
+
const entry = _getPendingAutoStart(base);
|
|
88
|
+
assert.ok(entry, "entry should exist");
|
|
89
|
+
|
|
90
|
+
const expectedIdentityKey = realpathSync(base);
|
|
91
|
+
assert.equal(
|
|
92
|
+
entry.scope.workspace.identityKey,
|
|
93
|
+
expectedIdentityKey,
|
|
94
|
+
"identityKey must match realpath of the original (non-canonical) basePath",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("clearPendingAutoStart removes the entry", () => {
|
|
99
|
+
setPendingAutoStart(base, { basePath: base, milestoneId: "M001" });
|
|
100
|
+
|
|
101
|
+
const before = _getPendingAutoStart(base);
|
|
102
|
+
assert.ok(before, "entry should exist before clear");
|
|
103
|
+
|
|
104
|
+
clearPendingAutoStart(base);
|
|
105
|
+
|
|
106
|
+
const after = _getPendingAutoStart(base);
|
|
107
|
+
assert.equal(after, null, "entry should be null after clearPendingAutoStart(base)");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("_getPendingAutoStart with no basePath argument returns the sole entry", () => {
|
|
111
|
+
setPendingAutoStart(base, { basePath: base, milestoneId: "M001" });
|
|
112
|
+
|
|
113
|
+
// No argument — should return the sole entry
|
|
114
|
+
const entry = _getPendingAutoStart();
|
|
115
|
+
assert.ok(entry, "sole entry should be returned when no basePath given");
|
|
116
|
+
assert.equal(entry.milestoneId, "M001");
|
|
117
|
+
assert.ok(entry.scope, "sole entry must have a scope");
|
|
118
|
+
assert.equal(entry.scope.milestoneId, "M001");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -4,11 +4,16 @@ import { readFileSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
6
|
const promptsDir = join(process.cwd(), "src/resources/extensions/gsd/prompts");
|
|
7
|
+
const templatesDir = join(process.cwd(), "src/resources/extensions/gsd/templates");
|
|
7
8
|
|
|
8
9
|
function readPrompt(name: string): string {
|
|
9
10
|
return readFileSync(join(promptsDir, `${name}.md`), "utf-8");
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
function readTemplate(name: string): string {
|
|
14
|
+
return readFileSync(join(templatesDir, `${name}.md`), "utf-8");
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
test("reactive-execute prompt keeps task summaries with subagents and avoids batch commits", () => {
|
|
13
18
|
const prompt = readPrompt("reactive-execute");
|
|
14
19
|
assert.match(prompt, /subagent-written summary as authoritative/i);
|
|
@@ -174,15 +179,15 @@ test("guided-resume-task prompt preserves recovery state until work is supersede
|
|
|
174
179
|
|
|
175
180
|
// ─── Prompt migration: execute-task → gsd_complete_task ───────────────
|
|
176
181
|
|
|
177
|
-
test("execute-task prompt references
|
|
182
|
+
test("execute-task prompt references gsd_task_complete tool", () => {
|
|
178
183
|
const prompt = readPrompt("execute-task");
|
|
179
|
-
assert.match(prompt, /
|
|
184
|
+
assert.match(prompt, /gsd_task_complete/);
|
|
180
185
|
});
|
|
181
186
|
|
|
182
|
-
test("execute-task prompt uses
|
|
187
|
+
test("execute-task prompt uses gsd_task_complete as canonical summary write path", () => {
|
|
183
188
|
const prompt = readPrompt("execute-task");
|
|
184
189
|
assert.match(prompt, /\{\{taskSummaryPath\}\}/);
|
|
185
|
-
assert.match(prompt, /
|
|
190
|
+
assert.match(prompt, /gsd_task_complete/);
|
|
186
191
|
assert.match(prompt, /DB-backed tool is the canonical write path/i);
|
|
187
192
|
assert.match(prompt, /Do \*\*not\*\* manually write `?\{\{taskSummaryPath\}\}`?/i);
|
|
188
193
|
assert.doesNotMatch(prompt, /^\d+\.\s+Write `?\{\{taskSummaryPath\}\}`?\s*$/m);
|
|
@@ -202,9 +207,9 @@ test("execute-task prompt still contains template variables for context", () =>
|
|
|
202
207
|
|
|
203
208
|
// ─── Prompt migration: complete-slice → gsd_complete_slice ────────────
|
|
204
209
|
|
|
205
|
-
test("complete-slice prompt references
|
|
210
|
+
test("complete-slice prompt references gsd_slice_complete tool", () => {
|
|
206
211
|
const prompt = readPrompt("complete-slice");
|
|
207
|
-
assert.match(prompt, /
|
|
212
|
+
assert.match(prompt, /gsd_slice_complete/);
|
|
208
213
|
});
|
|
209
214
|
|
|
210
215
|
test("complete-slice prompt does not instruct LLM to toggle checkboxes manually", () => {
|
|
@@ -216,7 +221,7 @@ test("complete-slice prompt instructs writing summary and UAT files before tool
|
|
|
216
221
|
const prompt = readPrompt("complete-slice");
|
|
217
222
|
assert.match(prompt, /\{\{sliceSummaryPath\}\}/);
|
|
218
223
|
assert.match(prompt, /\{\{sliceUatPath\}\}/);
|
|
219
|
-
assert.match(prompt, /
|
|
224
|
+
assert.match(prompt, /gsd_slice_complete/);
|
|
220
225
|
assert.match(prompt, /DB-backed tool is the canonical write path/i);
|
|
221
226
|
assert.match(prompt, /Do \*\*not\*\* manually write `?\{\{sliceSummaryPath\}\}`?/i);
|
|
222
227
|
assert.match(prompt, /Do \*\*not\*\* manually write `?\{\{sliceUatPath\}\}`?/i);
|
|
@@ -398,3 +403,141 @@ test("reactive-execute prompt references tool calls instead of checkbox updates"
|
|
|
398
403
|
assert.doesNotMatch(prompt, /checkbox edits/);
|
|
399
404
|
assert.match(prompt, /completion tool calls/);
|
|
400
405
|
});
|
|
406
|
+
|
|
407
|
+
// ─── Project-shape classifier + 3-or-4-options-with-Other-hatch contract ──
|
|
408
|
+
|
|
409
|
+
test("guided-discuss-project classifies project shape and persists the verdict to PROJECT.md", () => {
|
|
410
|
+
const prompt = readPrompt("guided-discuss-project");
|
|
411
|
+
assert.match(prompt, /Classify project shape/i, "must include the classifier section");
|
|
412
|
+
assert.match(prompt, /`simple`/);
|
|
413
|
+
assert.match(prompt, /`complex`/);
|
|
414
|
+
assert.match(prompt, /Default to `complex` when uncertain/i);
|
|
415
|
+
assert.match(prompt, /## Project Shape/, "must reference the persisted PROJECT.md section");
|
|
416
|
+
assert.match(prompt, /\*\*Complexity:\*\*\s*simple/);
|
|
417
|
+
assert.match(prompt, /\*\*Complexity:\*\*\s*complex/);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("guided-discuss prompts require 3-or-4 options plus Other-let-me-discuss in complex mode", () => {
|
|
421
|
+
for (const name of [
|
|
422
|
+
"guided-discuss-project",
|
|
423
|
+
"guided-discuss-milestone",
|
|
424
|
+
"guided-discuss-slice",
|
|
425
|
+
]) {
|
|
426
|
+
const prompt = readPrompt(name);
|
|
427
|
+
assert.match(
|
|
428
|
+
prompt,
|
|
429
|
+
/3 or 4 concrete, researched options/i,
|
|
430
|
+
`${name} must require 3 or 4 grounded options in complex mode`,
|
|
431
|
+
);
|
|
432
|
+
assert.match(
|
|
433
|
+
prompt,
|
|
434
|
+
/"Other — let me discuss"/,
|
|
435
|
+
`${name} must include the "Other — let me discuss" escape hatch`,
|
|
436
|
+
);
|
|
437
|
+
assert.match(
|
|
438
|
+
prompt,
|
|
439
|
+
/grounded in (the |your |)investigation/i,
|
|
440
|
+
`${name} must require options grounded in prior investigation`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("guided-discuss-requirements scopes the 3-or-4-options rule to free-form questions only", () => {
|
|
446
|
+
const prompt = readPrompt("guided-discuss-requirements");
|
|
447
|
+
assert.match(prompt, /3 or 4 concrete, researched options/i);
|
|
448
|
+
assert.match(prompt, /"Other — let me discuss"/);
|
|
449
|
+
// Class-assignment and status questions have fixed enumerations, so the rule must exempt them.
|
|
450
|
+
assert.match(prompt, /class-assignment.*status.*exempt/i);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("downstream discuss prompts read project shape verdict from PROJECT.md", () => {
|
|
454
|
+
for (const name of [
|
|
455
|
+
"guided-discuss-milestone",
|
|
456
|
+
"guided-discuss-requirements",
|
|
457
|
+
"guided-discuss-slice",
|
|
458
|
+
]) {
|
|
459
|
+
const prompt = readPrompt(name);
|
|
460
|
+
assert.match(
|
|
461
|
+
prompt,
|
|
462
|
+
/Project Shape/,
|
|
463
|
+
`${name} must reference Project Shape from PROJECT.md`,
|
|
464
|
+
);
|
|
465
|
+
assert.match(
|
|
466
|
+
prompt,
|
|
467
|
+
/default to `complex`/i,
|
|
468
|
+
`${name} must default to complex when the verdict is missing`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("project template includes the Project Shape section so the verdict has a home", () => {
|
|
474
|
+
const template = readTemplate("project");
|
|
475
|
+
assert.match(template, /## Project Shape/);
|
|
476
|
+
assert.match(template, /\*\*Complexity:\*\*/);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// ─── Project shape verdict — end-to-end propagation contract (F7 / #5267) ──
|
|
480
|
+
// The verdict is propagated from discuss-project to downstream stages via
|
|
481
|
+
// PROJECT.md text only, with no parser. These tests pin the round-trip:
|
|
482
|
+
// the format the upstream stage is told to write must be discoverable by
|
|
483
|
+
// the regex pattern the downstream stage is told to look for.
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Render the project.md template with a concrete complexity verdict so we
|
|
487
|
+
* can assert on a realistic PROJECT.md (the placeholder is filled the way
|
|
488
|
+
* an LLM following the prompt would fill it).
|
|
489
|
+
*/
|
|
490
|
+
function renderProjectMd(verdict: "simple" | "complex"): string {
|
|
491
|
+
return readTemplate("project")
|
|
492
|
+
.replace("{{simple | complex}}", verdict)
|
|
493
|
+
.replace("{{one-line rationale citing the signals that decided it}}", "Test fixture rationale.");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
test("project shape verdict survives the discuss-project → discuss-milestone round trip", () => {
|
|
497
|
+
for (const verdict of ["simple", "complex"] as const) {
|
|
498
|
+
const projectMd = renderProjectMd(verdict);
|
|
499
|
+
|
|
500
|
+
// Upstream contract: the PROJECT.md the discuss-project prompt writes
|
|
501
|
+
// must contain the section header and the bolded `**Complexity:** <verdict>`
|
|
502
|
+
// marker that downstream stages are told to grep for.
|
|
503
|
+
assert.match(projectMd, /## Project Shape/, `rendered ${verdict} PROJECT.md must keep the section header`);
|
|
504
|
+
const complexityMarker = new RegExp(`\\*\\*Complexity:\\*\\*\\s*${verdict}\\b`);
|
|
505
|
+
assert.match(
|
|
506
|
+
projectMd,
|
|
507
|
+
complexityMarker,
|
|
508
|
+
`rendered ${verdict} PROJECT.md must expose the bolded Complexity marker the downstream regex looks for`,
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
// Downstream contract: discuss-milestone, discuss-requirements, and
|
|
512
|
+
// discuss-slice must each instruct the LLM to look at the same section
|
|
513
|
+
// header AND the same `**Complexity:**` marker the template writes.
|
|
514
|
+
// Without this, the upstream verdict is silently dropped.
|
|
515
|
+
for (const downstream of ["guided-discuss-milestone", "guided-discuss-requirements", "guided-discuss-slice"]) {
|
|
516
|
+
const prompt = readPrompt(downstream);
|
|
517
|
+
assert.match(
|
|
518
|
+
prompt,
|
|
519
|
+
/## Project Shape/,
|
|
520
|
+
`${downstream} must direct the LLM to the same section header the template writes`,
|
|
521
|
+
);
|
|
522
|
+
assert.match(
|
|
523
|
+
prompt,
|
|
524
|
+
/\*\*Complexity:\*\*/,
|
|
525
|
+
`${downstream} must direct the LLM to the same **Complexity:** marker the template writes`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("downstream discuss prompts default to complex when PROJECT.md lacks the verdict", () => {
|
|
532
|
+
// Safe-by-default: if upstream omits the section (existing projects, LLM
|
|
533
|
+
// drift, future template change), each downstream stage must explicitly
|
|
534
|
+
// fall back to complex so behavior is conservative rather than stuck.
|
|
535
|
+
for (const downstream of ["guided-discuss-milestone", "guided-discuss-requirements", "guided-discuss-slice"]) {
|
|
536
|
+
const prompt = readPrompt(downstream);
|
|
537
|
+
assert.match(
|
|
538
|
+
prompt,
|
|
539
|
+
/default to `complex`/i,
|
|
540
|
+
`${downstream} must default to complex when the upstream verdict is missing`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
});
|