gsd-pi 2.78.1-dev.e9d88a536 → 2.78.1-dev.eccf86e27
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 +5 -7
- package/dist/help-text.js +1 -1
- package/dist/resource-loader.js +6 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
- package/dist/resources/extensions/gsd/auto/loop.js +235 -36
- package/dist/resources/extensions/gsd/auto/phases.js +14 -7
- package/dist/resources/extensions/gsd/auto/session.js +36 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
- package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
- package/dist/resources/extensions/gsd/auto.js +139 -49
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
- package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
- package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
- package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
- package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
- 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/docs/COORDINATION.md +42 -0
- package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
- package/dist/resources/extensions/gsd/doctor.js +12 -2
- package/dist/resources/extensions/gsd/gsd-db.js +355 -3
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +116 -26
- package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
- 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/state.js +21 -6
- 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 +79 -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 +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/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/detect-stuck.ts +37 -5
- package/src/resources/extensions/gsd/auto/loop.ts +263 -41
- package/src/resources/extensions/gsd/auto/phases.ts +15 -7
- package/src/resources/extensions/gsd/auto/session.ts +40 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
- package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
- package/src/resources/extensions/gsd/auto.ts +166 -43
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
- 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/crash-recovery.ts +177 -43
- package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
- package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
- package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
- package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
- 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/docs/COORDINATION.md +42 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
- package/src/resources/extensions/gsd/doctor.ts +10 -2
- package/src/resources/extensions/gsd/gsd-db.ts +354 -3
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +152 -26
- package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
- 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/state.ts +44 -6
- 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-loop-no-copy-artifacts.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
- package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
- 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/detect-stuck-respects-retry.test.ts +173 -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/auto-worktree.test.ts +22 -12
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
- package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
- 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/milestone-leases.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
- package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -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/paused-session-via-db.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
- package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -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 +98 -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/unit-dispatches.test.ts +247 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -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 +196 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
- 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 +78 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
- package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
|
@@ -21,9 +21,13 @@ Before your first action, print this banner verbatim in chat:
|
|
|
21
21
|
## Pre-flight
|
|
22
22
|
|
|
23
23
|
1. Read `.gsd/PROJECT.md` end-to-end. If it does not exist, STOP and emit: `"PROJECT.md missing — run discuss-project first."`
|
|
24
|
-
2. Extract: Core Value, Anti-goals, Constraints, Milestone Sequence.
|
|
24
|
+
2. Extract: Core Value, Anti-goals, Constraints, Milestone Sequence, and the project shape verdict — read the `## Project Shape` section and look for `**Complexity:**` (verdict is either `simple` or `complex`; default to `complex` if the section is missing or unclear).
|
|
25
25
|
3. Check for existing `.gsd/REQUIREMENTS.md` — if present, this is a refinement pass, not a fresh write. Read existing requirements and treat them as the working set.
|
|
26
26
|
|
|
27
|
+
**Shape-dependent cadence:**
|
|
28
|
+
- **`simple`** — favor a single fast pass: extract requirements directly from PROJECT.md, ask 1–2 plain-text clarifying questions only if a class or status assignment is genuinely ambiguous, then write REQUIREMENTS.md.
|
|
29
|
+
- **`complex`** — full multi-round questioning with structured 3–4-option questions where alternatives matter.
|
|
30
|
+
|
|
27
31
|
---
|
|
28
32
|
|
|
29
33
|
## Interview Protocol
|
|
@@ -51,7 +55,7 @@ Ask **1–3 questions per round**. Each round targets one dimension:
|
|
|
51
55
|
|
|
52
56
|
**Never fabricate or simulate user input.** Wait for actual responses.
|
|
53
57
|
|
|
54
|
-
**If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions`. Every question object MUST include a stable lowercase `id`. For class assignments, present the allowed classes as multi-select options. For status, present the four statuses as exclusive options. Ask 1–3 questions per call. Wait for each tool result before asking the next round.
|
|
58
|
+
**If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions`. Every question object MUST include a stable lowercase `id`. For class assignments, present the allowed classes as multi-select options. For status, present the four statuses as exclusive options. In **`complex`** mode, any free-form question MUST present **3 or 4 concrete, researched options** plus a final **"Other — let me discuss"** option grounded in the investigation above. The class-assignment and status questions are exempt — they have fixed enumerations. Ask 1–3 questions per call. Wait for each tool result before asking the next round.
|
|
55
59
|
|
|
56
60
|
**If `{{structuredQuestionsAvailable}}` is `false`:** ask in plain text. Keep each round to 1–3 questions.
|
|
57
61
|
|
|
@@ -8,6 +8,13 @@ Your goal is **not** to center the discussion on tech stack trivia, naming conve
|
|
|
8
8
|
|
|
9
9
|
## Interview Protocol
|
|
10
10
|
|
|
11
|
+
### Read project shape
|
|
12
|
+
|
|
13
|
+
Before your first question round, read `.gsd/PROJECT.md` and look for `## Project Shape` → `**Complexity:**`. The verdict is either **`simple`** or **`complex`** (default to `complex` if the section is missing or unclear).
|
|
14
|
+
|
|
15
|
+
- **`simple`** — favor 1–2 plain-text rounds, write the slice context fast. Skip parallel-research investigation.
|
|
16
|
+
- **`complex`** — full investigation with structured 3–4-option questions.
|
|
17
|
+
|
|
11
18
|
### Before your first question round
|
|
12
19
|
|
|
13
20
|
Do a lightweight targeted investigation so your questions are grounded in reality:
|
|
@@ -24,7 +31,7 @@ Do **not** go deep — just enough that your questions reflect what's actually t
|
|
|
24
31
|
|
|
25
32
|
**Never fabricate or simulate user input.** Never generate fake transcript markers like `[User]`, `[Human]`, or `User:`. Ask one question round, then wait for the user's actual response before continuing.
|
|
26
33
|
|
|
27
|
-
**If `{{structuredQuestionsAvailable}}` is `true`:** Ask **1–3 questions per round** using `ask_user_questions`. **Call `ask_user_questions` exactly once per turn — never make multiple calls with the same or overlapping questions. Wait for the user's response before asking the next round.**
|
|
34
|
+
**If `{{structuredQuestionsAvailable}}` is `true`:** Ask **1–3 questions per round** using `ask_user_questions`. In **`complex`** mode, each multi-choice question MUST present **3 or 4 concrete, researched options** plus a final **"Other — let me discuss"** option; options must be grounded in the investigation above (codebase signals, library docs, prior `.gsd/` artifacts), not generic placeholders. In **`simple`** mode, 2 options is fine. Binary wrap-up gates are exempt from the 3-or-4 rule. **Call `ask_user_questions` exactly once per turn — never make multiple calls with the same or overlapping questions. Wait for the user's response before asking the next round.**
|
|
28
35
|
**If `{{structuredQuestionsAvailable}}` is `false`:** Ask **1–3 questions per round** in plain text. Number them and wait for the user's response before asking the next round.
|
|
29
36
|
Keep each question focused on one of:
|
|
30
37
|
- **UX and user-facing behaviour** — what does the user see, click, trigger, or experience?
|
|
@@ -270,6 +270,21 @@ export async function getActiveMilestoneId(basePath: string): Promise<string | n
|
|
|
270
270
|
return null;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Options for deriveState read-path routing.
|
|
275
|
+
*
|
|
276
|
+
* `projectRootForReads`: canonical project root (e.g. from
|
|
277
|
+
* `s.canonicalProjectRoot`) used for both the cache key and the artifact-read
|
|
278
|
+
* root in `_deriveStateImpl`. When omitted, behavior is identical to the
|
|
279
|
+
* single-arg signature (back-compat for all existing callers).
|
|
280
|
+
*
|
|
281
|
+
* Typed as an object literal (not `string | DeriveStateOptions`) so accidental
|
|
282
|
+
* `deriveState(path, "string")` is rejected at compile time.
|
|
283
|
+
*/
|
|
284
|
+
export interface DeriveStateOptions {
|
|
285
|
+
projectRootForReads?: string;
|
|
286
|
+
}
|
|
287
|
+
|
|
273
288
|
/**
|
|
274
289
|
* Reconstruct GSD state from the authoritative DB.
|
|
275
290
|
* STATE.md is a rendered cache of this output.
|
|
@@ -278,11 +293,22 @@ export async function getActiveMilestoneId(basePath: string): Promise<string | n
|
|
|
278
293
|
* Legacy filesystem parsing is available only through an explicit opt-in for
|
|
279
294
|
* tests/recovery flows; runtime must not silently infer state from markdown.
|
|
280
295
|
*/
|
|
281
|
-
export async function deriveState(
|
|
282
|
-
|
|
296
|
+
export async function deriveState(
|
|
297
|
+
basePath: string,
|
|
298
|
+
opts?: DeriveStateOptions,
|
|
299
|
+
): Promise<GSDState> {
|
|
300
|
+
// Use the canonical project root (when provided) as the cache key so that
|
|
301
|
+
// two calls with different basePath strings (e.g. worktree path vs project
|
|
302
|
+
// root) but the same canonical .gsd/ share a single cache entry. The same
|
|
303
|
+
// key is used for both the lookup AND the write below — keying lookup on
|
|
304
|
+
// canonical-root while writing on basePath would silently return stale
|
|
305
|
+
// results across path-form alternation.
|
|
306
|
+
const cacheKey = opts?.projectRootForReads ?? basePath;
|
|
307
|
+
|
|
308
|
+
// Return cached result if within the TTL window for the same cacheKey
|
|
283
309
|
if (
|
|
284
310
|
_stateCache &&
|
|
285
|
-
_stateCache.basePath ===
|
|
311
|
+
_stateCache.basePath === cacheKey &&
|
|
286
312
|
Date.now() - _stateCache.timestamp < CACHE_TTL_MS
|
|
287
313
|
) {
|
|
288
314
|
return _stateCache.result;
|
|
@@ -303,7 +329,7 @@ export async function deriveState(basePath: string): Promise<GSDState> {
|
|
|
303
329
|
if (wasDbOpenAttempted()) {
|
|
304
330
|
logWarning("state", "DB unavailable — using explicit legacy filesystem state derivation");
|
|
305
331
|
}
|
|
306
|
-
result = await _deriveStateImpl(basePath);
|
|
332
|
+
result = await _deriveStateImpl(basePath, opts);
|
|
307
333
|
_telemetry.markdownDeriveCount++;
|
|
308
334
|
} else {
|
|
309
335
|
if (wasDbOpenAttempted()) {
|
|
@@ -325,7 +351,7 @@ export async function deriveState(basePath: string): Promise<GSDState> {
|
|
|
325
351
|
|
|
326
352
|
stopTimer({ phase: result.phase, milestone: result.activeMilestone?.id });
|
|
327
353
|
debugCount("deriveStateCalls");
|
|
328
|
-
_stateCache = { basePath, result, timestamp: Date.now() };
|
|
354
|
+
_stateCache = { basePath: cacheKey, result, timestamp: Date.now() };
|
|
329
355
|
return result;
|
|
330
356
|
}
|
|
331
357
|
|
|
@@ -838,7 +864,19 @@ export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
|
|
|
838
864
|
// LEGACY: Filesystem-based state derivation for unmigrated projects.
|
|
839
865
|
// DB-backed projects use deriveStateFromDb() above. Target: extract to
|
|
840
866
|
// state-legacy.ts when all projects are DB-backed.
|
|
841
|
-
export async function _deriveStateImpl(
|
|
867
|
+
export async function _deriveStateImpl(
|
|
868
|
+
basePath: string,
|
|
869
|
+
opts?: DeriveStateOptions,
|
|
870
|
+
): Promise<GSDState> {
|
|
871
|
+
// When the caller supplies a canonical project root for reads (e.g.
|
|
872
|
+
// s.canonicalProjectRoot from auto-mode), route all artifact reads through
|
|
873
|
+
// it. This prevents the worktree-local empty `.gsd/` from being consulted
|
|
874
|
+
// when the canonical state lives at the project root (or via a `.gsd`
|
|
875
|
+
// symlink into the external state dir).
|
|
876
|
+
if (opts?.projectRootForReads) {
|
|
877
|
+
basePath = opts.projectRootForReads;
|
|
878
|
+
}
|
|
879
|
+
|
|
842
880
|
const diskIds = findMilestoneIds(basePath);
|
|
843
881
|
const customOrder = loadQueueOrder(basePath);
|
|
844
882
|
const milestoneIds = sortByQueueOrder(diskIds, customOrder);
|
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
|
|
12
12
|
{{theOneThingThatMustWorkEvenIfEverythingElseIsCut}}
|
|
13
13
|
|
|
14
|
+
## Project Shape
|
|
15
|
+
|
|
16
|
+
<!-- Drives questioning depth in downstream stages. `simple` → short plain-text
|
|
17
|
+
rounds, fast PROJECT/CONTEXT/REQUIREMENTS writes. `complex` → researched
|
|
18
|
+
3–4-option questions with an "Other — let me discuss" hatch.
|
|
19
|
+
Default to `complex` when uncertain. -->
|
|
20
|
+
|
|
21
|
+
- **Complexity:** {{simple | complex}}
|
|
22
|
+
- **Why:** {{one-line rationale citing the signals that decided it}}
|
|
23
|
+
|
|
14
24
|
## Current State
|
|
15
25
|
|
|
16
26
|
{{whatHasBeenBuiltSoFar — what works, what exists, what's deployed}}
|
|
@@ -35,7 +35,7 @@ import { _setAutoActiveForTest } from '../auto.ts';
|
|
|
35
35
|
// Reset all relevant state before and after each test.
|
|
36
36
|
function resetState(): void {
|
|
37
37
|
_setAutoActiveForTest(false);
|
|
38
|
-
clearDiscussionFlowState();
|
|
38
|
+
clearDiscussionFlowState(process.cwd());
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
@@ -51,7 +51,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
51
51
|
_setAutoActiveForTest(true);
|
|
52
52
|
|
|
53
53
|
// Before mark: blocked
|
|
54
|
-
const snapshotBefore = loadWriteGateSnapshot();
|
|
54
|
+
const snapshotBefore = loadWriteGateSnapshot(process.cwd());
|
|
55
55
|
const beforeResult = shouldBlockContextArtifactSaveInSnapshot(
|
|
56
56
|
snapshotBefore,
|
|
57
57
|
'CONTEXT',
|
|
@@ -61,10 +61,10 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
61
61
|
assert.strictEqual(beforeResult.block, true, 'should block before markDepthVerified');
|
|
62
62
|
|
|
63
63
|
// Simulate what the dispatch rule now does in auto-mode
|
|
64
|
-
markDepthVerified('M001');
|
|
64
|
+
markDepthVerified('M001', process.cwd());
|
|
65
65
|
|
|
66
66
|
// After mark: unblocked
|
|
67
|
-
const snapshotAfter = loadWriteGateSnapshot();
|
|
67
|
+
const snapshotAfter = loadWriteGateSnapshot(process.cwd());
|
|
68
68
|
const afterResult = shouldBlockContextArtifactSaveInSnapshot(
|
|
69
69
|
snapshotAfter,
|
|
70
70
|
'CONTEXT',
|
|
@@ -87,7 +87,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
87
87
|
assert.strictEqual(beforeResult.block, true, 'write should be blocked before markDepthVerified');
|
|
88
88
|
|
|
89
89
|
// Simulate dispatch rule auto-mark
|
|
90
|
-
markDepthVerified('M001');
|
|
90
|
+
markDepthVerified('M001', process.cwd());
|
|
91
91
|
|
|
92
92
|
// After mark: unblocked
|
|
93
93
|
const afterResult = shouldBlockContextWrite('write', contextPath, 'M001');
|
|
@@ -109,8 +109,8 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
109
109
|
// the dispatch-site call site is safe regardless of prior session state.
|
|
110
110
|
test('Test 3: session_switch ordering — clearDiscussionFlowState clears mark; dispatch-site call re-establishes it', () => {
|
|
111
111
|
// Simulate a mark from a prior session
|
|
112
|
-
markDepthVerified('M001');
|
|
113
|
-
let snapshot = loadWriteGateSnapshot();
|
|
112
|
+
markDepthVerified('M001', process.cwd());
|
|
113
|
+
let snapshot = loadWriteGateSnapshot(process.cwd());
|
|
114
114
|
assert.strictEqual(
|
|
115
115
|
isMilestoneDepthVerifiedInSnapshot(snapshot, 'M001'),
|
|
116
116
|
true,
|
|
@@ -119,8 +119,8 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
119
119
|
|
|
120
120
|
// session_switch fires clearDiscussionFlowState() — this is exactly what
|
|
121
121
|
// register-hooks.ts:106 does
|
|
122
|
-
clearDiscussionFlowState();
|
|
123
|
-
snapshot = loadWriteGateSnapshot();
|
|
122
|
+
clearDiscussionFlowState(process.cwd());
|
|
123
|
+
snapshot = loadWriteGateSnapshot(process.cwd());
|
|
124
124
|
assert.strictEqual(
|
|
125
125
|
isMilestoneDepthVerifiedInSnapshot(snapshot, 'M001'),
|
|
126
126
|
false,
|
|
@@ -130,9 +130,9 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
130
130
|
// Now the dispatch rule fires (after session_switch cleared state)
|
|
131
131
|
// and re-establishes the mark for the new session
|
|
132
132
|
_setAutoActiveForTest(true);
|
|
133
|
-
markDepthVerified('M001'); // this is what the dispatch rule does
|
|
133
|
+
markDepthVerified('M001', process.cwd()); // this is what the dispatch rule does
|
|
134
134
|
|
|
135
|
-
snapshot = loadWriteGateSnapshot();
|
|
135
|
+
snapshot = loadWriteGateSnapshot(process.cwd());
|
|
136
136
|
assert.strictEqual(
|
|
137
137
|
isMilestoneDepthVerifiedInSnapshot(snapshot, 'M001'),
|
|
138
138
|
true,
|
|
@@ -158,7 +158,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
158
158
|
|
|
159
159
|
// CONTEXT artifact save is still blocked
|
|
160
160
|
const snapshotResult = shouldBlockContextArtifactSaveInSnapshot(
|
|
161
|
-
loadWriteGateSnapshot(),
|
|
161
|
+
loadWriteGateSnapshot(process.cwd()),
|
|
162
162
|
'CONTEXT',
|
|
163
163
|
'M002',
|
|
164
164
|
null,
|
|
@@ -238,7 +238,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
238
238
|
);
|
|
239
239
|
|
|
240
240
|
// ── Deep auto-mode case: the user-facing approval gate must stay closed ──
|
|
241
|
-
clearDiscussionFlowState();
|
|
241
|
+
clearDiscussionFlowState(process.cwd());
|
|
242
242
|
if (existsSync(snapshotFile)) unlinkSync(snapshotFile);
|
|
243
243
|
_setAutoActiveForTest(true);
|
|
244
244
|
const deepCtx = {
|
|
@@ -264,7 +264,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
|
|
|
264
264
|
// ── Interactive case: the rule must NOT call markDepthVerified ──
|
|
265
265
|
// clearDiscussionFlowState() only deletes the snapshot at process.cwd(),
|
|
266
266
|
// so we must explicitly remove the snapshot under our tempBase too.
|
|
267
|
-
clearDiscussionFlowState();
|
|
267
|
+
clearDiscussionFlowState(process.cwd());
|
|
268
268
|
if (existsSync(snapshotFile)) unlinkSync(snapshotFile);
|
|
269
269
|
_setAutoActiveForTest(false);
|
|
270
270
|
snap = loadWriteGateSnapshot(tempBase);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// gsd-2 + Phase C deletion regression: createAutoWorktree no longer copies .gsd/
|
|
2
|
+
//
|
|
3
|
+
// Verifies that createAutoWorktree on a project with a real (non-symlinked)
|
|
4
|
+
// .gsd/ does NOT populate .gsd/milestones/ inside the worktree. Pre-Phase-C,
|
|
5
|
+
// copyPlanningArtifacts would mirror the project-root .gsd/ into the
|
|
6
|
+
// worktree-local .gsd/. Phase C deleted that helper because writers in
|
|
7
|
+
// auto-mode now route through s.canonicalProjectRoot, so the worktree never
|
|
8
|
+
// needs a parallel .gsd/ projection.
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, realpathSync, rmSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
|
|
17
|
+
import { createAutoWorktree, teardownAutoWorktree } from "../auto-worktree.ts";
|
|
18
|
+
|
|
19
|
+
function git(args: string[], cwd: string): void {
|
|
20
|
+
execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("createAutoWorktree does NOT copy project-root .gsd/milestones into the worktree", (t) => {
|
|
24
|
+
const base = realpathSync(mkdtempSync(join(tmpdir(), "gsd-no-copy-")));
|
|
25
|
+
|
|
26
|
+
// Initialize a real git repo with a real .gsd/ directory containing some
|
|
27
|
+
// planning artifacts that the deleted copyPlanningArtifacts would have
|
|
28
|
+
// mirrored.
|
|
29
|
+
git(["init", "-b", "main"], base);
|
|
30
|
+
git(["config", "user.name", "Pi Test"], base);
|
|
31
|
+
git(["config", "user.email", "pi@example.com"], base);
|
|
32
|
+
writeFileSync(join(base, "README.md"), "# Test\n", "utf-8");
|
|
33
|
+
git(["add", "README.md"], base);
|
|
34
|
+
git(["commit", "-m", "chore: init"], base);
|
|
35
|
+
|
|
36
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
37
|
+
writeFileSync(
|
|
38
|
+
join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"),
|
|
39
|
+
"# M001 Context\n",
|
|
40
|
+
"utf-8",
|
|
41
|
+
);
|
|
42
|
+
writeFileSync(
|
|
43
|
+
join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
|
|
44
|
+
"# M001 Roadmap\n",
|
|
45
|
+
"utf-8",
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const wtPath = createAutoWorktree(base, "M001");
|
|
49
|
+
t.after(() => {
|
|
50
|
+
try { teardownAutoWorktree(base, "M001"); } catch { /* noop */ }
|
|
51
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Phase C invariant: the worktree's .gsd/milestones/M001 must NOT exist
|
|
55
|
+
// (no copyPlanningArtifacts), and the project-root version must still be
|
|
56
|
+
// intact (it was the source, not destination).
|
|
57
|
+
assert.equal(
|
|
58
|
+
existsSync(join(wtPath, ".gsd", "milestones", "M001", "M001-CONTEXT.md")),
|
|
59
|
+
false,
|
|
60
|
+
"worktree should NOT have a copy of M001-CONTEXT.md (copyPlanningArtifacts deleted)",
|
|
61
|
+
);
|
|
62
|
+
assert.equal(
|
|
63
|
+
existsSync(join(wtPath, ".gsd", "milestones", "M001", "M001-ROADMAP.md")),
|
|
64
|
+
false,
|
|
65
|
+
"worktree should NOT have a copy of M001-ROADMAP.md",
|
|
66
|
+
);
|
|
67
|
+
assert.equal(
|
|
68
|
+
existsSync(join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md")),
|
|
69
|
+
true,
|
|
70
|
+
"project-root .gsd/ retains the canonical CONTEXT.md",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// gsd-2 + Symlinked .gsd worktree-loop reproduction (Phase A pt 2 follow-up to PR #5236)
|
|
2
|
+
//
|
|
3
|
+
// Regression coverage for the auto-mode loop bug observed on projects whose
|
|
4
|
+
// .gsd/ is a symlink into ~/.gsd/projects/<hash>/ (the external-state layout).
|
|
5
|
+
//
|
|
6
|
+
// Two assertions:
|
|
7
|
+
// 1. deriveState's cache key is the canonical project root when callers
|
|
8
|
+
// opt into projectRootForReads — so two derive calls that should refer
|
|
9
|
+
// to the same canonical state share a single cache entry, regardless of
|
|
10
|
+
// whether the caller passed the worktree path or the project-root path.
|
|
11
|
+
// 2. _deriveStateImpl's projectRootForReads option routes legacy markdown
|
|
12
|
+
// reads through the canonical project root, finding files that live in
|
|
13
|
+
// the symlink target rather than the worktree-local empty .gsd/.
|
|
14
|
+
//
|
|
15
|
+
// Per project rule #11: regression test using node:test + node:assert/strict,
|
|
16
|
+
// no source-grep assertions. The first test would fail on main without the
|
|
17
|
+
// cache-key fix in state.ts (lookup vs write keys would diverge across
|
|
18
|
+
// path-form alternation, producing cache misses). The second test would
|
|
19
|
+
// fail on main because _deriveStateImpl doesn't accept the option at all.
|
|
20
|
+
|
|
21
|
+
import test from "node:test";
|
|
22
|
+
import assert from "node:assert/strict";
|
|
23
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, realpathSync, symlinkSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { tmpdir } from "node:os";
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
deriveState,
|
|
29
|
+
_deriveStateImpl,
|
|
30
|
+
invalidateStateCache,
|
|
31
|
+
type DeriveStateOptions,
|
|
32
|
+
} from "../state.ts";
|
|
33
|
+
import {
|
|
34
|
+
openDatabase,
|
|
35
|
+
closeDatabase,
|
|
36
|
+
insertMilestone,
|
|
37
|
+
insertSlice,
|
|
38
|
+
insertTask,
|
|
39
|
+
} from "../gsd-db.ts";
|
|
40
|
+
|
|
41
|
+
// ─── Fixture helpers ──────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
interface SymlinkedFixture {
|
|
44
|
+
/** Project root containing .gsd as a symlink. */
|
|
45
|
+
projectRoot: string;
|
|
46
|
+
/** External state dir that .gsd points at (acts as the canonical .gsd/). */
|
|
47
|
+
externalState: string;
|
|
48
|
+
/** Worktree path under the external state's worktrees/ dir. */
|
|
49
|
+
worktreePath: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeSymlinkedFixture(prefix: string): SymlinkedFixture {
|
|
53
|
+
// Use realpathSync on tmpdir so that subsequent realpath comparisons are stable
|
|
54
|
+
// — macOS /var symlinks to /private/var, which would otherwise pollute the
|
|
55
|
+
// canonical-root assertions below.
|
|
56
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), `gsd-${prefix}-`)));
|
|
57
|
+
const projectRoot = join(root, "project");
|
|
58
|
+
const externalState = join(root, "external-state", "projects", "abc123");
|
|
59
|
+
|
|
60
|
+
mkdirSync(projectRoot, { recursive: true });
|
|
61
|
+
mkdirSync(externalState, { recursive: true });
|
|
62
|
+
|
|
63
|
+
// .gsd → externalState (the layout that triggered the original bug)
|
|
64
|
+
symlinkSync(externalState, join(projectRoot, ".gsd"), "junction");
|
|
65
|
+
|
|
66
|
+
// Worktree path lives under the external state's worktrees/ dir, mirroring
|
|
67
|
+
// the canonicalProjectRoot resolution that resolveGsdPathContract performs
|
|
68
|
+
// for the external-state layout.
|
|
69
|
+
const worktreePath = join(externalState, "worktrees", "M001");
|
|
70
|
+
mkdirSync(worktreePath, { recursive: true });
|
|
71
|
+
|
|
72
|
+
return { projectRoot, externalState, worktreePath };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cleanupFixture(fx: SymlinkedFixture): void {
|
|
76
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
77
|
+
// The mkdtemp root is two levels above projectRoot.
|
|
78
|
+
try {
|
|
79
|
+
const root = join(fx.projectRoot, "..");
|
|
80
|
+
rmSync(root, { recursive: true, force: true });
|
|
81
|
+
} catch { /* noop */ }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
85
|
+
// Test 1: cache-key invariance under projectRootForReads
|
|
86
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
87
|
+
|
|
88
|
+
test("deriveState: cache key is canonical when projectRootForReads is supplied", async (t) => {
|
|
89
|
+
const fx = makeSymlinkedFixture("symlink-cache");
|
|
90
|
+
t.after(() => cleanupFixture(fx));
|
|
91
|
+
|
|
92
|
+
// Open the DB at the canonical .gsd location (externalState).
|
|
93
|
+
openDatabase(join(fx.externalState, "gsd.db"));
|
|
94
|
+
insertMilestone({ id: "M001", title: "Symlinked", status: "active" });
|
|
95
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice" });
|
|
96
|
+
// No tasks → DB-derived state is "planning".
|
|
97
|
+
|
|
98
|
+
invalidateStateCache();
|
|
99
|
+
|
|
100
|
+
const optsCanonical: DeriveStateOptions = { projectRootForReads: fx.projectRoot };
|
|
101
|
+
|
|
102
|
+
// First call: seed the cache through the worktree-path form.
|
|
103
|
+
const stateA = await deriveState(fx.worktreePath, optsCanonical);
|
|
104
|
+
assert.equal(stateA.activeMilestone?.id, "M001");
|
|
105
|
+
assert.equal(stateA.activeSlice?.id, "S01");
|
|
106
|
+
assert.equal(stateA.phase, "planning");
|
|
107
|
+
|
|
108
|
+
// Second call: canonical project-root form must hit the same cache entry.
|
|
109
|
+
const stateB = await deriveState(fx.projectRoot);
|
|
110
|
+
assert.equal(stateB, stateA, "second call with same canonical key must return the cached object");
|
|
111
|
+
|
|
112
|
+
// Third call: worktree-path form with projectRootForReads must also hit the
|
|
113
|
+
// same cache entry, proving the cache key is symmetric across both call
|
|
114
|
+
// orders.
|
|
115
|
+
const stateC = await deriveState(fx.worktreePath, optsCanonical);
|
|
116
|
+
assert.equal(stateC, stateA, "third call with worktree path plus canonical reads must hit the same cache entry");
|
|
117
|
+
|
|
118
|
+
// Mutation invalidates: insert a task, clear cache, re-derive — must
|
|
119
|
+
// observe the new state via the canonical key path.
|
|
120
|
+
insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "Task", status: "active" });
|
|
121
|
+
invalidateStateCache();
|
|
122
|
+
|
|
123
|
+
const stateD = await deriveState(fx.worktreePath, optsCanonical);
|
|
124
|
+
assert.notEqual(stateD, stateA, "post-mutation derive must re-compute, not reuse the prior cached object");
|
|
125
|
+
assert.equal(stateD.activeTask?.id, "T01", "mutation must surface in the re-derived state");
|
|
126
|
+
assert.equal(stateD.phase, "executing");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
130
|
+
// Test 2: _deriveStateImpl reads from canonical root via projectRootForReads
|
|
131
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
132
|
+
|
|
133
|
+
test("_deriveStateImpl: projectRootForReads routes legacy markdown reads to the canonical .gsd/", async (t) => {
|
|
134
|
+
const fx = makeSymlinkedFixture("symlink-md");
|
|
135
|
+
t.after(() => cleanupFixture(fx));
|
|
136
|
+
// No DB opened — exercise the markdown fallback.
|
|
137
|
+
|
|
138
|
+
// Seed the external state dir (the symlink target) with a roadmap so the
|
|
139
|
+
// legacy filesystem state derivation has a milestone to find.
|
|
140
|
+
const m1Dir = join(fx.externalState, "milestones", "M001");
|
|
141
|
+
mkdirSync(m1Dir, { recursive: true });
|
|
142
|
+
writeFileSync(
|
|
143
|
+
join(m1Dir, "M001-CONTEXT.md"),
|
|
144
|
+
"# M001: Symlinked legacy md test\n\nTest project.\n",
|
|
145
|
+
"utf-8",
|
|
146
|
+
);
|
|
147
|
+
writeFileSync(
|
|
148
|
+
join(m1Dir, "M001-ROADMAP.md"),
|
|
149
|
+
[
|
|
150
|
+
"# M001 Roadmap",
|
|
151
|
+
"",
|
|
152
|
+
"## Slices",
|
|
153
|
+
"",
|
|
154
|
+
"- [ ] **S01: First slice** — depends:",
|
|
155
|
+
"",
|
|
156
|
+
].join("\n"),
|
|
157
|
+
"utf-8",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
invalidateStateCache();
|
|
161
|
+
|
|
162
|
+
// Calling _deriveStateImpl with the worktree path AND projectRootForReads
|
|
163
|
+
// pointing at the project root must consult the canonical .gsd/ (via the
|
|
164
|
+
// symlink target externalState), find M001/S01, and report planning phase
|
|
165
|
+
// because no slice plan file exists yet.
|
|
166
|
+
const state = await _deriveStateImpl(fx.worktreePath, { projectRootForReads: fx.projectRoot });
|
|
167
|
+
assert.equal(state.activeMilestone?.id, "M001", "must find M001 via canonical .gsd/ reads");
|
|
168
|
+
assert.equal(state.activeSlice?.id, "S01", "must find S01 from the roadmap");
|
|
169
|
+
assert.equal(state.phase, "planning", "no slice PLAN.md yet → planning phase");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
173
|
+
// Test 3: type-safety guard for the deriveState opts overload (compile-time)
|
|
174
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
175
|
+
//
|
|
176
|
+
// The DeriveStateOptions parameter is typed as an object literal so accidental
|
|
177
|
+
// `deriveState(path, "string")` is a TypeScript compile error. The
|
|
178
|
+
// expect-error directive verifies that this guard is in place — if the
|
|
179
|
+
// overload were widened to `string | DeriveStateOptions`, the directive would
|
|
180
|
+
// trigger TS2578 ("Unused '@ts-expect-error' directive") at build time.
|
|
181
|
+
|
|
182
|
+
test("deriveState: opts param rejects non-object values at compile time", () => {
|
|
183
|
+
// The actual assertion is the TypeScript compile-time check below; the
|
|
184
|
+
// runtime body just confirms the test ran.
|
|
185
|
+
if (false) {
|
|
186
|
+
// @ts-expect-error — projectRootForReads must be a string
|
|
187
|
+
void deriveState("/nonexistent", { projectRootForReads: 123 });
|
|
188
|
+
}
|
|
189
|
+
assert.ok(true);
|
|
190
|
+
});
|