gsd-pi 2.80.0-dev.cf9433f56 → 2.80.0-dev.d4fc28e6b
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/cli.js +0 -19
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +29 -0
- package/dist/resources/extensions/gsd/auto/loop.js +71 -8
- package/dist/resources/extensions/gsd/auto/phases.js +150 -94
- package/dist/resources/extensions/gsd/auto/resolve.js +12 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +10 -30
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto/workflow-dispatch-claim.js +33 -1
- package/dist/resources/extensions/gsd/auto/workflow-worker-heartbeat.js +9 -1
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +5 -32
- package/dist/resources/extensions/gsd/auto-dispatch.js +16 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +17 -4
- package/dist/resources/extensions/gsd/auto-prompts.js +90 -15
- package/dist/resources/extensions/gsd/auto-start.js +197 -6
- package/dist/resources/extensions/gsd/auto-worktree.js +111 -1
- package/dist/resources/extensions/gsd/auto.js +18 -22
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +86 -19
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +49 -36
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +15 -5
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +9 -3
- package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +7 -1
- package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +9 -3
- package/dist/resources/extensions/gsd/bootstrap/query-tools.js +8 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +298 -54
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +82 -23
- package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
- package/dist/resources/extensions/gsd/commands-handlers.js +23 -9
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +53 -0
- package/dist/resources/extensions/gsd/ecosystem/gsd-extension-api.js +2 -0
- package/dist/resources/extensions/gsd/guided-flow.js +47 -28
- package/dist/resources/extensions/gsd/native-git-bridge.js +32 -8
- package/dist/resources/extensions/gsd/orphan-stash-audit.js +101 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +13 -3
- package/dist/resources/extensions/gsd/pre-execution-checks.js +15 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +2 -2
- package/dist/resources/extensions/gsd/workflow-protocol.js +131 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +35 -4
- 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 +12 -12
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
- 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/welcome-screen.d.ts +2 -0
- package/dist/welcome-screen.js +9 -7
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +4 -1
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts +5 -0
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +2 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/index.d.ts +1 -0
- package/packages/pi-agent-core/dist/index.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/index.js +2 -0
- package/packages/pi-agent-core/dist/index.js.map +1 -1
- package/packages/pi-agent-core/dist/token-audit.d.ts +47 -0
- package/packages/pi-agent-core/dist/token-audit.d.ts.map +1 -0
- package/packages/pi-agent-core/dist/token-audit.js +221 -0
- package/packages/pi-agent-core/dist/token-audit.js.map +1 -0
- package/packages/pi-agent-core/dist/types.d.ts +9 -0
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.test.ts +128 -0
- package/packages/pi-agent-core/src/agent-loop.ts +4 -1
- package/packages/pi-agent-core/src/agent.ts +8 -0
- package/packages/pi-agent-core/src/index.ts +2 -0
- package/packages/pi-agent-core/src/token-audit.test.ts +189 -0
- package/packages/pi-agent-core/src/token-audit.ts +287 -0
- package/packages/pi-agent-core/src/types.ts +14 -0
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +18 -0
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +36 -7
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -6
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +3 -3
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +32 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/hooks-runner.test.js +2 -0
- package/packages/pi-coding-agent/dist/core/hooks-runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js +46 -0
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -2
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +74 -2
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js +22 -0
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +6 -7
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -3
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +25 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +40 -7
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +10 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +3 -3
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -5
- package/packages/pi-coding-agent/src/core/extensions/types.ts +35 -1
- package/packages/pi-coding-agent/src/core/hooks-runner.test.ts +2 -0
- package/packages/pi-coding-agent/src/core/sdk-tool-filter.test.ts +60 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +85 -3
- package/packages/pi-coding-agent/src/core/skill-tool.test.ts +28 -0
- package/packages/pi-coding-agent/src/core/system-prompt.ts +8 -10
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +30 -0
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +26 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
- package/src/resources/extensions/gsd/auto/loop.ts +84 -8
- package/src/resources/extensions/gsd/auto/phases.ts +218 -154
- package/src/resources/extensions/gsd/auto/resolve.ts +19 -0
- package/src/resources/extensions/gsd/auto/run-unit.ts +10 -29
- package/src/resources/extensions/gsd/auto/session.ts +8 -0
- package/src/resources/extensions/gsd/auto/workflow-dispatch-claim.ts +63 -1
- package/src/resources/extensions/gsd/auto/workflow-worker-heartbeat.ts +14 -1
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +8 -34
- package/src/resources/extensions/gsd/auto-dispatch.ts +16 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +18 -4
- package/src/resources/extensions/gsd/auto-prompts.ts +95 -14
- package/src/resources/extensions/gsd/auto-start.ts +230 -9
- package/src/resources/extensions/gsd/auto-worktree.ts +123 -0
- package/src/resources/extensions/gsd/auto.ts +18 -18
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +100 -18
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +50 -36
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +16 -5
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +10 -3
- package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +8 -1
- package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +10 -3
- package/src/resources/extensions/gsd/bootstrap/query-tools.ts +9 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +347 -54
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +90 -22
- package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
- package/src/resources/extensions/gsd/commands-handlers.ts +34 -15
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +66 -0
- package/src/resources/extensions/gsd/ecosystem/gsd-extension-api.ts +3 -0
- package/src/resources/extensions/gsd/guided-flow.ts +52 -35
- package/src/resources/extensions/gsd/native-git-bridge.ts +39 -6
- package/src/resources/extensions/gsd/orphan-stash-audit.ts +117 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +13 -3
- package/src/resources/extensions/gsd/pre-execution-checks.ts +16 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
- package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +361 -10
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +168 -6
- package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
- package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/complete-slice-composer.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/context-store.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/execute-task-rendering.test.ts +5 -2
- package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +219 -0
- package/src/resources/extensions/gsd/tests/finalize-survivor-branch.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +242 -0
- package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +34 -2
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/orphan-merge-bootstrap.test.ts +133 -0
- package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +201 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestrator-fast-forward.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +7 -5
- package/src/resources/extensions/gsd/tests/prompt-duplication-cuts.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/query-tools-db-open.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +38 -17
- package/src/resources/extensions/gsd/tests/select-resumable-milestone.test.ts +96 -0
- package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +77 -0
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +166 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/system-context-memory.test.ts +112 -0
- package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +7 -9
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +291 -0
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +50 -1
- package/src/resources/extensions/gsd/tests/unstructured-continue-context-injection.test.ts +5 -4
- package/src/resources/extensions/gsd/tests/workflow-dispatch-claim.test.ts +142 -0
- package/src/resources/extensions/gsd/tests/workflow-protocol-excerpt.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/workflow-worker-heartbeat.test.ts +32 -1
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +22 -19
- package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +104 -3
- package/src/resources/extensions/gsd/workflow-protocol.ts +160 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +49 -4
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +0 -97
- /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// GSD-2 + src/resources/extensions/gsd/tests/parallel-orchestrator-fast-forward.test.ts
|
|
2
|
+
// Regression: parallel-orchestrator's `_createMilestoneWorktree` must
|
|
3
|
+
// fast-forward a reused milestone branch onto integration before creating
|
|
4
|
+
// the worktree, matching the behavior added to the auto-mode path in
|
|
5
|
+
// commit 8996cb68e (#5549 post-merge audit, R3).
|
|
6
|
+
|
|
7
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { _createMilestoneWorktree } from "../parallel-orchestrator.js";
|
|
15
|
+
|
|
16
|
+
const NO_PROMPT_ENV = {
|
|
17
|
+
...process.env,
|
|
18
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
19
|
+
GIT_AUTHOR_NAME: "test",
|
|
20
|
+
GIT_AUTHOR_EMAIL: "test@example.com",
|
|
21
|
+
GIT_COMMITTER_NAME: "test",
|
|
22
|
+
GIT_COMMITTER_EMAIL: "test@example.com",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function git(cwd: string, ...args: string[]): string {
|
|
26
|
+
return execFileSync("git", args, {
|
|
27
|
+
cwd,
|
|
28
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
env: NO_PROMPT_ENV,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rev(cwd: string, ref: string): string {
|
|
35
|
+
return git(cwd, "rev-parse", ref).trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("_createMilestoneWorktree fast-forwards reused milestone branches (#5549 R3)", () => {
|
|
39
|
+
let repo: string;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
repo = mkdtempSync(join(tmpdir(), "parallel-orch-ff-"));
|
|
43
|
+
git(repo, "init", "-q", "-b", "main");
|
|
44
|
+
git(repo, "config", "user.email", "test@example.com");
|
|
45
|
+
git(repo, "config", "user.name", "test");
|
|
46
|
+
writeFileSync(join(repo, "seed.txt"), "seed\n");
|
|
47
|
+
git(repo, "add", "seed.txt");
|
|
48
|
+
git(repo, "commit", "-q", "-m", "initial");
|
|
49
|
+
// Minimal .gsd/ structure so syncGsdStateToWorktree doesn't crash on a
|
|
50
|
+
// bare repo. We don't care if it copies anything — only that the FF ran.
|
|
51
|
+
mkdirSync(join(repo, ".gsd"), { recursive: true });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
rmSync(repo, { recursive: true, force: true });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("reused milestone branch behind main is fast-forwarded before worktree creation", () => {
|
|
59
|
+
// Worker N drained M001 in a previous run, leaving milestone/M001 forked
|
|
60
|
+
// from old main. Worker N+1 picks up M002 — but the milestone/M002 branch
|
|
61
|
+
// was created from old main in a sibling run, so it's now N commits behind.
|
|
62
|
+
git(repo, "branch", "milestone/M002");
|
|
63
|
+
const m002Initial = rev(repo, "milestone/M002");
|
|
64
|
+
|
|
65
|
+
writeFileSync(join(repo, "seed.txt"), "main moved forward\n");
|
|
66
|
+
git(repo, "add", "seed.txt");
|
|
67
|
+
git(repo, "commit", "-q", "-m", "main advanced");
|
|
68
|
+
const mainTip = rev(repo, "main");
|
|
69
|
+
|
|
70
|
+
assert.notEqual(m002Initial, mainTip, "main must be ahead before the test");
|
|
71
|
+
|
|
72
|
+
// _createMilestoneWorktree may throw inside createWorktree/syncGsdStateToWorktree
|
|
73
|
+
// (e.g. if the worktree-manager has stricter requirements than this minimal
|
|
74
|
+
// repo provides). The fast-forward runs BEFORE those calls, so the branch
|
|
75
|
+
// ref should have moved regardless. Catch and assert on observable state.
|
|
76
|
+
try {
|
|
77
|
+
_createMilestoneWorktree(repo, "M002");
|
|
78
|
+
} catch {
|
|
79
|
+
// Fine — we only care that FF executed.
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
assert.equal(
|
|
83
|
+
rev(repo, "milestone/M002"),
|
|
84
|
+
mainTip,
|
|
85
|
+
"milestone/M002 must be fast-forwarded to main's tip before worktree is built",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("diverged milestone branch is NOT touched (would lose work)", () => {
|
|
90
|
+
git(repo, "checkout", "-q", "-b", "milestone/M002");
|
|
91
|
+
writeFileSync(join(repo, "wip.txt"), "milestone-only work\n");
|
|
92
|
+
git(repo, "add", "wip.txt");
|
|
93
|
+
git(repo, "commit", "-q", "-m", "M002 work");
|
|
94
|
+
const m002Tip = rev(repo, "milestone/M002");
|
|
95
|
+
|
|
96
|
+
git(repo, "checkout", "-q", "main");
|
|
97
|
+
writeFileSync(join(repo, "seed.txt"), "main moved forward\n");
|
|
98
|
+
git(repo, "add", "seed.txt");
|
|
99
|
+
git(repo, "commit", "-q", "-m", "main advanced");
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
_createMilestoneWorktree(repo, "M002");
|
|
103
|
+
} catch {
|
|
104
|
+
// ignored
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
assert.equal(
|
|
108
|
+
rev(repo, "milestone/M002"),
|
|
109
|
+
m002Tip,
|
|
110
|
+
"diverged milestone branch must NOT be touched — would lose committed work",
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -1819,7 +1819,7 @@ describe("checkFilePathConsistency completed-task output exemption (#4071)", ()
|
|
|
1819
1819
|
);
|
|
1820
1820
|
});
|
|
1821
1821
|
|
|
1822
|
-
test("pending task at higher index
|
|
1822
|
+
test("pending task at higher index does NOT cause a duplicate consistency error (ordering check handles it)", (t) => {
|
|
1823
1823
|
const tempDir = join(tmpdir(), `pre-exec-fc-pending-${Date.now()}`);
|
|
1824
1824
|
mkdirSync(tempDir, { recursive: true });
|
|
1825
1825
|
t.after(() => rmSync(tempDir, { recursive: true, force: true }));
|
|
@@ -1841,14 +1841,16 @@ describe("checkFilePathConsistency completed-task output exemption (#4071)", ()
|
|
|
1841
1841
|
}),
|
|
1842
1842
|
];
|
|
1843
1843
|
|
|
1844
|
+
// checkFilePathConsistency suppresses the error here because checkTaskOrdering
|
|
1845
|
+
// will fire a more precise "sequence violation" error for the same file.
|
|
1846
|
+
// The combined output of runPreExecutionChecks still flags the issue — just
|
|
1847
|
+
// once, via the ordering check, instead of twice.
|
|
1844
1848
|
const results = checkFilePathConsistency(tasks, tempDir);
|
|
1845
1849
|
assert.equal(
|
|
1846
1850
|
results.length,
|
|
1847
|
-
|
|
1848
|
-
"
|
|
1851
|
+
0,
|
|
1852
|
+
"consistency check must not duplicate what the ordering check already reports",
|
|
1849
1853
|
);
|
|
1850
|
-
assert.equal(results[0].blocking, true);
|
|
1851
|
-
assert.equal(results[0].target, "artifacts/output.json");
|
|
1852
1854
|
});
|
|
1853
1855
|
});
|
|
1854
1856
|
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// Project/App: GSD-2
|
|
2
|
+
// File Purpose: Verifies low-risk auto-prompt duplication cuts render through prompt builders.
|
|
3
|
+
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import type { TestContext } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
|
|
11
|
+
import { invalidateAllCaches } from "../cache.ts";
|
|
12
|
+
import {
|
|
13
|
+
closeDatabase,
|
|
14
|
+
insertMilestone,
|
|
15
|
+
insertSlice,
|
|
16
|
+
insertTask,
|
|
17
|
+
openDatabase,
|
|
18
|
+
upsertMilestonePlanning,
|
|
19
|
+
} from "../gsd-db.ts";
|
|
20
|
+
|
|
21
|
+
type AutoPromptBuilders = typeof import("../auto-prompts.ts");
|
|
22
|
+
|
|
23
|
+
function makeBase(prefix: string): string {
|
|
24
|
+
const base = mkdtempSync(join(tmpdir(), prefix));
|
|
25
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
|
|
26
|
+
return base;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cleanup(base: string): void {
|
|
30
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
31
|
+
invalidateAllCaches();
|
|
32
|
+
rmSync(base, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function loadAutoPromptBuilders(t: TestContext): Promise<AutoPromptBuilders> {
|
|
36
|
+
const previousGsdHome = process.env.GSD_HOME;
|
|
37
|
+
const isolatedHome = mkdtempSync(join(tmpdir(), "gsd-prompt-loader-home-"));
|
|
38
|
+
process.env.GSD_HOME = isolatedHome;
|
|
39
|
+
t.after(() => {
|
|
40
|
+
if (previousGsdHome === undefined) delete process.env.GSD_HOME;
|
|
41
|
+
else process.env.GSD_HOME = previousGsdHome;
|
|
42
|
+
rmSync(isolatedHome, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
return import(`../auto-prompts.ts?promptDupCuts=${Date.now()}-${Math.random()}`) as Promise<AutoPromptBuilders>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function seedDb(base: string, taskStatus = "complete"): void {
|
|
48
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
49
|
+
insertMilestone({ id: "M001", title: "Prompt Cuts", status: "active", depends_on: [] });
|
|
50
|
+
upsertMilestonePlanning("M001", {
|
|
51
|
+
title: "Prompt Cuts",
|
|
52
|
+
status: "active",
|
|
53
|
+
vision: "Reduce duplicate prompt reads.",
|
|
54
|
+
successCriteria: ["Prompt builders render compact context."],
|
|
55
|
+
keyRisks: [],
|
|
56
|
+
proofStrategy: [],
|
|
57
|
+
verificationContract: "",
|
|
58
|
+
verificationIntegration: "",
|
|
59
|
+
verificationOperational: "",
|
|
60
|
+
verificationUat: "",
|
|
61
|
+
definitionOfDone: [],
|
|
62
|
+
requirementCoverage: "",
|
|
63
|
+
boundaryMapMarkdown: "",
|
|
64
|
+
});
|
|
65
|
+
insertSlice({
|
|
66
|
+
id: "S01",
|
|
67
|
+
milestoneId: "M001",
|
|
68
|
+
title: "Prompt Slice",
|
|
69
|
+
status: "active",
|
|
70
|
+
risk: "low",
|
|
71
|
+
depends: [],
|
|
72
|
+
demo: "",
|
|
73
|
+
sequence: 1,
|
|
74
|
+
});
|
|
75
|
+
insertTask({
|
|
76
|
+
id: "T01",
|
|
77
|
+
sliceId: "S01",
|
|
78
|
+
milestoneId: "M001",
|
|
79
|
+
title: "Task one",
|
|
80
|
+
status: taskStatus,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeRoadmapAndPlan(base: string): void {
|
|
85
|
+
writeFileSync(
|
|
86
|
+
join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
|
|
87
|
+
[
|
|
88
|
+
"# M001 Roadmap",
|
|
89
|
+
"## Slices",
|
|
90
|
+
"- [ ] **S01: Prompt Slice** `risk:low` `depends:[]`",
|
|
91
|
+
].join("\n"),
|
|
92
|
+
);
|
|
93
|
+
writeFileSync(
|
|
94
|
+
join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"),
|
|
95
|
+
[
|
|
96
|
+
"# S01 Plan",
|
|
97
|
+
"",
|
|
98
|
+
"**Goal:** Reduce duplicate prompt reads.",
|
|
99
|
+
"",
|
|
100
|
+
"## Tasks",
|
|
101
|
+
"- [x] **T01: Task one** `est:15m`",
|
|
102
|
+
].join("\n"),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function writeTaskSummary(base: string, options?: { blocker?: boolean; repeatedNarrative?: string }): void {
|
|
107
|
+
const narrative = options?.repeatedNarrative ?? "This full implementation narrative should stay out of closer prompts.";
|
|
108
|
+
writeFileSync(
|
|
109
|
+
join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-SUMMARY.md"),
|
|
110
|
+
[
|
|
111
|
+
"---",
|
|
112
|
+
"id: T01",
|
|
113
|
+
"parent: S01",
|
|
114
|
+
"milestone: M001",
|
|
115
|
+
"provides:",
|
|
116
|
+
" - prompt context reduction",
|
|
117
|
+
"key_files:",
|
|
118
|
+
" - src/resources/extensions/gsd/auto-prompts.ts",
|
|
119
|
+
"key_decisions:",
|
|
120
|
+
" - use compact excerpts before full reads",
|
|
121
|
+
"patterns_established:",
|
|
122
|
+
" - excerpt-first complete-slice context",
|
|
123
|
+
"observability_surfaces: []",
|
|
124
|
+
"duration: 15m",
|
|
125
|
+
"verification_result: passed",
|
|
126
|
+
"completed_at: 2026-05-06T12:00:00Z",
|
|
127
|
+
`blocker_discovered: ${options?.blocker ? "true" : "false"}`,
|
|
128
|
+
"---",
|
|
129
|
+
"",
|
|
130
|
+
"# T01: Task one",
|
|
131
|
+
"**One-line result.**",
|
|
132
|
+
"",
|
|
133
|
+
"## What Happened",
|
|
134
|
+
narrative,
|
|
135
|
+
"",
|
|
136
|
+
"## Verification",
|
|
137
|
+
"node:test passed.",
|
|
138
|
+
"",
|
|
139
|
+
"## Diagnostics",
|
|
140
|
+
"Prompt size stayed bounded.",
|
|
141
|
+
].join("\n"),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
test("execute-task rendering makes memory_query and template disk reads fallback-only", async (t) => {
|
|
146
|
+
const base = makeBase("gsd-execute-dup-cuts-");
|
|
147
|
+
t.after(() => cleanup(base));
|
|
148
|
+
invalidateAllCaches();
|
|
149
|
+
|
|
150
|
+
seedDb(base, "pending");
|
|
151
|
+
writeRoadmapAndPlan(base);
|
|
152
|
+
writeFileSync(
|
|
153
|
+
join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-PLAN.md"),
|
|
154
|
+
"# T01 Plan\n\nDo the prompt edit.\n",
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const { buildExecuteTaskPrompt } = await loadAutoPromptBuilders(t);
|
|
158
|
+
const prompt = await buildExecuteTaskPrompt("M001", "S01", "Prompt Slice", "T01", "Task one", base);
|
|
159
|
+
|
|
160
|
+
assert.match(prompt, /Call `memory_query`.*only when no injected memory block exists or the inlined memory\/context is insufficient/s);
|
|
161
|
+
assert.doesNotMatch(prompt, /Call `memory_query` with 2-4 keywords from the task title and touched files unless this is purely mechanical/);
|
|
162
|
+
assert.match(prompt, /Use the inlined Task Summary template below/);
|
|
163
|
+
assert.match(prompt, /Read `.*task-summary\.md` only if the inlined template is absent or visibly truncated/);
|
|
164
|
+
assert.doesNotMatch(prompt, /Read the template at `.*task-summary\.md`/);
|
|
165
|
+
assert.match(prompt, /### Output Template: Task Summary/);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("complete-slice renders task summary excerpts without full summary bodies", async (t) => {
|
|
169
|
+
const base = makeBase("gsd-complete-slice-excerpts-");
|
|
170
|
+
t.after(() => cleanup(base));
|
|
171
|
+
invalidateAllCaches();
|
|
172
|
+
|
|
173
|
+
seedDb(base);
|
|
174
|
+
writeRoadmapAndPlan(base);
|
|
175
|
+
const repeatedNarrative = "FULL_TASK_BODY_SHOULD_NOT_RENDER ".repeat(40);
|
|
176
|
+
writeTaskSummary(base, { repeatedNarrative });
|
|
177
|
+
|
|
178
|
+
const { buildCompleteSlicePrompt } = await loadAutoPromptBuilders(t);
|
|
179
|
+
const prompt = await buildCompleteSlicePrompt("M001", "Prompt Cuts", "S01", "Prompt Slice", base);
|
|
180
|
+
|
|
181
|
+
assert.match(prompt, /### Task Summary: T01 \(excerpt\)/);
|
|
182
|
+
assert.match(prompt, /On-demand.*read `\.gsd\/milestones\/M001\/slices\/S01\/tasks\/T01-SUMMARY\.md` only when this excerpt is absent\/truncated/s);
|
|
183
|
+
assert.doesNotMatch(prompt, /FULL_TASK_BODY_SHOULD_NOT_RENDER/);
|
|
184
|
+
assert.match(prompt, /Review the inlined task-summary excerpts/);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("complete-slice caps malformed task summaries instead of inlining full bodies", async (t) => {
|
|
188
|
+
const base = makeBase("gsd-complete-slice-malformed-excerpts-");
|
|
189
|
+
t.after(() => cleanup(base));
|
|
190
|
+
invalidateAllCaches();
|
|
191
|
+
|
|
192
|
+
seedDb(base);
|
|
193
|
+
writeRoadmapAndPlan(base);
|
|
194
|
+
writeFileSync(
|
|
195
|
+
join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-SUMMARY.md"),
|
|
196
|
+
[
|
|
197
|
+
"# Legacy summary without frontmatter id",
|
|
198
|
+
"LEGACY_FULL_BODY_SHOULD_BE_CAPPED ".repeat(200),
|
|
199
|
+
].join("\n"),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const { buildCompleteSlicePrompt } = await loadAutoPromptBuilders(t);
|
|
203
|
+
const prompt = await buildCompleteSlicePrompt("M001", "Prompt Cuts", "S01", "Prompt Slice", base);
|
|
204
|
+
|
|
205
|
+
assert.match(prompt, /Truncated malformed summary/);
|
|
206
|
+
assert.ok(prompt.length < 20_000);
|
|
207
|
+
assert.ok((prompt.match(/LEGACY_FULL_BODY_SHOULD_BE_CAPPED/g) ?? []).length < 60);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("replan-slice renders blocker summary excerpt and tells the agent to read full only on demand", async (t) => {
|
|
211
|
+
const base = makeBase("gsd-replan-excerpts-");
|
|
212
|
+
t.after(() => cleanup(base));
|
|
213
|
+
invalidateAllCaches();
|
|
214
|
+
|
|
215
|
+
seedDb(base);
|
|
216
|
+
writeRoadmapAndPlan(base);
|
|
217
|
+
writeTaskSummary(base, {
|
|
218
|
+
blocker: true,
|
|
219
|
+
repeatedNarrative: "FULL_BLOCKER_BODY_SHOULD_NOT_RENDER ".repeat(40),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const { buildReplanSlicePrompt } = await loadAutoPromptBuilders(t);
|
|
223
|
+
const prompt = await buildReplanSlicePrompt("M001", "Prompt Cuts", "S01", "Prompt Slice", base);
|
|
224
|
+
|
|
225
|
+
assert.match(prompt, /### Blocker Task Summary: T01 \(excerpt\)/);
|
|
226
|
+
assert.match(prompt, /Use the inlined blocker summary excerpt first/);
|
|
227
|
+
assert.match(prompt, /Read the full blocker task summary only if the excerpt is absent, marked truncated, or lacks the specific blocker evidence needed to replan/);
|
|
228
|
+
assert.doesNotMatch(prompt, /FULL_BLOCKER_BODY_SHOULD_NOT_RENDER/);
|
|
229
|
+
assert.doesNotMatch(prompt, /Read the blocker task summary carefully/);
|
|
230
|
+
});
|
|
@@ -28,8 +28,8 @@ describe('query-tools ensureDbOpen usage (#3672)', () => {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
test('calls ensureDbOpen() before DB queries', () => {
|
|
31
|
-
assert.match(source, /await ensureDbOpen\(
|
|
32
|
-
'query-tools should call await ensureDbOpen()');
|
|
31
|
+
assert.match(source, /await ensureDbOpen\([^)]*\)/,
|
|
32
|
+
'query-tools should call await ensureDbOpen(...)');
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
test('no longer imports isDbAvailable in the execute path', () => {
|
|
@@ -41,7 +41,7 @@ describe('query-tools ensureDbOpen usage (#3672)', () => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
test('uses dbAvailable result from ensureDbOpen', () => {
|
|
44
|
-
assert.match(source, /dbAvailable\s*=\s*await ensureDbOpen\(
|
|
44
|
+
assert.match(source, /dbAvailable\s*=\s*await ensureDbOpen\([^)]*\)/,
|
|
45
45
|
'should store ensureDbOpen result in dbAvailable');
|
|
46
46
|
});
|
|
47
47
|
});
|
|
@@ -24,7 +24,7 @@ describe('restore tools after discuss flow scoping (#3628)', () => {
|
|
|
24
24
|
it('savedTools is declared before the discuss scoping block', () => {
|
|
25
25
|
// savedTools must be declared before the discuss-* check
|
|
26
26
|
const savedToolsDecl = src.indexOf('let savedTools')
|
|
27
|
-
const discussCheck = src.indexOf('if (unitType?.startsWith("discuss-")
|
|
27
|
+
const discussCheck = src.indexOf('if (unitType?.startsWith("discuss-")')
|
|
28
28
|
assert.ok(savedToolsDecl !== -1, 'savedTools variable must be declared')
|
|
29
29
|
assert.ok(discussCheck !== -1, 'discuss-* type check must exist')
|
|
30
30
|
assert.ok(
|
|
@@ -33,40 +33,61 @@ describe('restore tools after discuss flow scoping (#3628)', () => {
|
|
|
33
33
|
)
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
it('savedTools captures current tools
|
|
37
|
-
const discussCheck = src.indexOf('if (unitType?.startsWith("discuss-")
|
|
36
|
+
it('savedTools captures current tools before scoping can mutate active state', () => {
|
|
37
|
+
const discussCheck = src.indexOf('if (unitType?.startsWith("discuss-")')
|
|
38
38
|
assert.ok(discussCheck !== -1)
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
const
|
|
40
|
+
const currentToolsDecl = src.indexOf('const currentTools = pi.getActiveTools()')
|
|
41
|
+
const savedToolsAssign = src.indexOf('savedTools = {', currentToolsDecl)
|
|
42
|
+
const firstMutation = src.indexOf('pi.setActiveTools(scopedTools)')
|
|
42
43
|
assert.ok(
|
|
43
|
-
|
|
44
|
-
'
|
|
44
|
+
currentToolsDecl !== -1 && savedToolsAssign !== -1 && firstMutation !== -1,
|
|
45
|
+
'guided-flow.ts must capture current tools, save them, and then scope active tools',
|
|
46
|
+
)
|
|
47
|
+
assert.ok(
|
|
48
|
+
currentToolsDecl < savedToolsAssign && savedToolsAssign < firstMutation,
|
|
49
|
+
'savedTools must capture currentTools before any discuss scoping mutation',
|
|
50
|
+
)
|
|
51
|
+
assert.ok(
|
|
52
|
+
src.slice(savedToolsAssign, firstMutation).includes('tools: currentTools'),
|
|
53
|
+
'savedTools must include currentTools before the first scoping mutation',
|
|
45
54
|
)
|
|
46
55
|
})
|
|
47
56
|
|
|
57
|
+
it('scoping and workflow read happen inside the restore try block', () => {
|
|
58
|
+
const savedToolsDecl = src.indexOf('let savedTools')
|
|
59
|
+
const tryIdx = src.indexOf('try {', savedToolsDecl)
|
|
60
|
+
const firstMutation = src.indexOf('pi.setActiveTools(scopedTools)')
|
|
61
|
+
const workflowRead = src.indexOf('readFileSync(workflowPath')
|
|
62
|
+
const finallyIdx = src.indexOf('} finally {', tryIdx)
|
|
63
|
+
|
|
64
|
+
assert.ok(savedToolsDecl !== -1, 'savedTools variable must be declared')
|
|
65
|
+
assert.ok(tryIdx !== -1, 'restore try block must exist')
|
|
66
|
+
assert.ok(firstMutation !== -1, 'discuss scoping mutation must exist')
|
|
67
|
+
assert.ok(workflowRead !== -1, 'workflow file read must exist')
|
|
68
|
+
assert.ok(finallyIdx !== -1, 'restore finally block must exist')
|
|
69
|
+
assert.ok(tryIdx < firstMutation && firstMutation < finallyIdx, 'scoping mutation must be inside try/finally')
|
|
70
|
+
assert.ok(tryIdx < workflowRead && workflowRead < finallyIdx, 'workflow file read must be inside try/finally')
|
|
71
|
+
})
|
|
72
|
+
|
|
48
73
|
it('savedTools is restored after sendMessage', () => {
|
|
49
74
|
// #4573: guided-flow.ts now contains multiple `triggerTurn: true` calls
|
|
50
75
|
// (ready-phrase and empty-turn recovery paths). The discuss-flow scoping
|
|
51
|
-
// sendMessage is the one that follows `
|
|
76
|
+
// sendMessage is the one that follows `tools: currentTools`, so
|
|
52
77
|
// anchor the search there rather than at the first `triggerTurn: true`.
|
|
53
|
-
const savedToolsAssign = src.indexOf('
|
|
54
|
-
assert.ok(savedToolsAssign !== -1, 'savedTools
|
|
78
|
+
const savedToolsAssign = src.indexOf('tools: currentTools')
|
|
79
|
+
assert.ok(savedToolsAssign !== -1, 'savedTools must capture currentTools')
|
|
55
80
|
|
|
56
81
|
const sendMsg = src.indexOf('triggerTurn: true', savedToolsAssign)
|
|
57
82
|
assert.ok(sendMsg !== -1, 'discuss-flow sendMessage with triggerTurn must exist after savedTools capture')
|
|
58
83
|
|
|
59
|
-
// After sendMessage, savedTools should be restored via
|
|
84
|
+
// After sendMessage, savedTools should be restored via the shared helper.
|
|
60
85
|
// Use fromIdx to anchor at the discuss-flow sendMessage, not the first
|
|
61
86
|
// triggerTurn: true occurrence in the file.
|
|
62
87
|
const afterSend = extractSourceRegion(src, 'triggerTurn: true', { fromIdx: savedToolsAssign })
|
|
63
88
|
assert.ok(
|
|
64
|
-
afterSend.includes('
|
|
65
|
-
'savedTools
|
|
66
|
-
)
|
|
67
|
-
assert.ok(
|
|
68
|
-
afterSend.includes('setActiveTools(savedTools)'),
|
|
69
|
-
'setActiveTools(savedTools) must be called to restore the full tool set',
|
|
89
|
+
afterSend.includes('restoreGsdWorkflowTools(pi, savedTools)'),
|
|
90
|
+
'restoreGsdWorkflowTools(pi, savedTools) must restore the full scoped state',
|
|
70
91
|
)
|
|
71
92
|
})
|
|
72
93
|
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// GSD-2 + src/resources/extensions/gsd/tests/select-resumable-milestone.test.ts
|
|
2
|
+
// Regression: bootstrap must rederive `currentMilestoneId` from an unmerged
|
|
3
|
+
// completed milestone branch when in-memory state was lost across a process
|
|
4
|
+
// restart (#5538-followup).
|
|
5
|
+
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
|
|
9
|
+
import { _selectResumableMilestone } from "../auto-start.js";
|
|
10
|
+
|
|
11
|
+
const ALL_COMPLETE = (_id: string) => true;
|
|
12
|
+
const NEVER_COMPLETE = (_id: string) => false;
|
|
13
|
+
const HAS_COMMITS = (_branch: string) => 1;
|
|
14
|
+
const NO_COMMITS = (_branch: string) => 0;
|
|
15
|
+
|
|
16
|
+
test("returns null when no branches exist", () => {
|
|
17
|
+
const result = _selectResumableMilestone([], new Set(), ALL_COMPLETE, HAS_COMMITS);
|
|
18
|
+
assert.equal(result, null);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("returns null when every milestone branch is already merged", () => {
|
|
22
|
+
const branches = ["milestone/M001", "milestone/M002"];
|
|
23
|
+
const merged = new Set(branches);
|
|
24
|
+
const result = _selectResumableMilestone(branches, merged, ALL_COMPLETE, HAS_COMMITS);
|
|
25
|
+
assert.equal(result, null);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("returns null when no candidate milestones are complete", () => {
|
|
29
|
+
const branches = ["milestone/M002"];
|
|
30
|
+
const result = _selectResumableMilestone(branches, new Set(), NEVER_COMPLETE, HAS_COMMITS);
|
|
31
|
+
assert.equal(result, null);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("returns null when no branch has commits ahead", () => {
|
|
35
|
+
const branches = ["milestone/M002"];
|
|
36
|
+
const result = _selectResumableMilestone(branches, new Set(), ALL_COMPLETE, NO_COMMITS);
|
|
37
|
+
assert.equal(result, null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("returns the unmerged completed milestone (regression: M002 stranded after restart)", () => {
|
|
41
|
+
// Repro of test12345 state: M001 ✅ merged, M002 ✅ unmerged, M003 🔄.
|
|
42
|
+
// Bootstrap must seed currentMilestoneId = "M002" so the loop's transition
|
|
43
|
+
// guard fires when the next iteration sees mid="M003".
|
|
44
|
+
const branches = ["milestone/M002", "milestone/M003"];
|
|
45
|
+
const merged = new Set<string>(); // none merged from this list
|
|
46
|
+
const isComplete = (id: string) => id === "M002"; // M003 still in progress
|
|
47
|
+
const result = _selectResumableMilestone(branches, merged, isComplete, HAS_COMMITS);
|
|
48
|
+
assert.equal(result, "M002");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("picks the lex-greatest milestone when multiple candidates exist", () => {
|
|
52
|
+
// M001, M002 both unmerged complete -> pick M002 (the most recent).
|
|
53
|
+
const branches = ["milestone/M001", "milestone/M002"];
|
|
54
|
+
const result = _selectResumableMilestone(
|
|
55
|
+
branches,
|
|
56
|
+
new Set(),
|
|
57
|
+
ALL_COMPLETE,
|
|
58
|
+
HAS_COMMITS,
|
|
59
|
+
);
|
|
60
|
+
assert.equal(result, "M002");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("ignores branch names that don't follow the milestone/ prefix", () => {
|
|
64
|
+
const branches = ["feature/something", "milestone/M002", "fix/bug-1"];
|
|
65
|
+
const result = _selectResumableMilestone(
|
|
66
|
+
branches,
|
|
67
|
+
new Set(),
|
|
68
|
+
ALL_COMPLETE,
|
|
69
|
+
HAS_COMMITS,
|
|
70
|
+
);
|
|
71
|
+
assert.equal(result, "M002");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("isComplete callback throwing skips that milestone but does not crash", () => {
|
|
75
|
+
const branches = ["milestone/M001", "milestone/M002"];
|
|
76
|
+
const isComplete = (id: string) => {
|
|
77
|
+
if (id === "M001") throw new Error("db unavailable for M001");
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
80
|
+
// Implementation choice: a thrown isComplete is propagated. Wrapping in
|
|
81
|
+
// try/catch happens at the production wrapper level (findUnmergedCompletedMilestone).
|
|
82
|
+
// Verify the helper itself surfaces the error so callers see real failures.
|
|
83
|
+
assert.throws(() => _selectResumableMilestone(branches, new Set(), isComplete, HAS_COMMITS));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("commitsAhead callback throwing for one branch falls through to others", () => {
|
|
87
|
+
// Production wrapper: commits-ahead failures should not abort the search.
|
|
88
|
+
// The helper catches throws from commitsAhead per-branch and treats as 0.
|
|
89
|
+
const branches = ["milestone/M001", "milestone/M002"];
|
|
90
|
+
const commitsAhead = (branch: string) => {
|
|
91
|
+
if (branch === "milestone/M001") throw new Error("rev-walk failed");
|
|
92
|
+
return 5;
|
|
93
|
+
};
|
|
94
|
+
const result = _selectResumableMilestone(branches, new Set(), ALL_COMPLETE, commitsAhead);
|
|
95
|
+
assert.equal(result, "M002");
|
|
96
|
+
});
|
|
@@ -206,6 +206,83 @@ test("session_start does NOT call setFooter or suppress gsd-health when isAutoAc
|
|
|
206
206
|
assert.equal(healthWidgetHideCount, 0, "gsd-health must NOT be hidden when isAutoActive() is false");
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
+
test("session_start installs the welcome screen as the TUI header", async (t) => {
|
|
210
|
+
const dir = join(
|
|
211
|
+
tmpdir(),
|
|
212
|
+
`gsd-welcome-header-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
213
|
+
);
|
|
214
|
+
mkdirSync(join(dir, "bin"), { recursive: true });
|
|
215
|
+
mkdirSync(join(dir, "dist"), { recursive: true });
|
|
216
|
+
writeFileSync(join(dir, "bin", "welcome-screen.js"), "export const stale = true;\n", "utf-8");
|
|
217
|
+
writeFileSync(
|
|
218
|
+
join(dir, "dist", "welcome-screen.js"),
|
|
219
|
+
[
|
|
220
|
+
"export function buildWelcomeScreenLines(opts) {",
|
|
221
|
+
" return [`welcome ${opts.version} ${opts.remoteChannel ?? 'none'} ${opts.width}`];",
|
|
222
|
+
"}",
|
|
223
|
+
"",
|
|
224
|
+
].join("\n"),
|
|
225
|
+
"utf-8",
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const originalCwd = process.cwd();
|
|
229
|
+
const originalGsdPkgRoot = process.env.GSD_PKG_ROOT;
|
|
230
|
+
const originalGsdBinPath = process.env.GSD_BIN_PATH;
|
|
231
|
+
const originalGsdVersion = process.env.GSD_VERSION;
|
|
232
|
+
const originalFirstRunBanner = process.env.GSD_FIRST_RUN_BANNER;
|
|
233
|
+
process.chdir(dir);
|
|
234
|
+
process.env.GSD_PKG_ROOT = dir;
|
|
235
|
+
process.env.GSD_BIN_PATH = join(dir, "bin", "loader.js");
|
|
236
|
+
process.env.GSD_VERSION = "9.9.9-test";
|
|
237
|
+
delete process.env.GSD_FIRST_RUN_BANNER;
|
|
238
|
+
t.after(() => {
|
|
239
|
+
process.chdir(originalCwd);
|
|
240
|
+
if (originalGsdPkgRoot === undefined) delete process.env.GSD_PKG_ROOT;
|
|
241
|
+
else process.env.GSD_PKG_ROOT = originalGsdPkgRoot;
|
|
242
|
+
if (originalGsdBinPath === undefined) delete process.env.GSD_BIN_PATH;
|
|
243
|
+
else process.env.GSD_BIN_PATH = originalGsdBinPath;
|
|
244
|
+
if (originalGsdVersion === undefined) delete process.env.GSD_VERSION;
|
|
245
|
+
else process.env.GSD_VERSION = originalGsdVersion;
|
|
246
|
+
if (originalFirstRunBanner === undefined) delete process.env.GSD_FIRST_RUN_BANNER;
|
|
247
|
+
else process.env.GSD_FIRST_RUN_BANNER = originalFirstRunBanner;
|
|
248
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const handlers = new Map<string, (event: unknown, ctx: any) => Promise<void> | void>();
|
|
252
|
+
const pi = {
|
|
253
|
+
on(event: string, handler: (event: unknown, ctx: any) => Promise<void> | void) {
|
|
254
|
+
handlers.set(event, handler);
|
|
255
|
+
},
|
|
256
|
+
} as any;
|
|
257
|
+
|
|
258
|
+
registerHooks(pi, []);
|
|
259
|
+
|
|
260
|
+
const sessionStart = handlers.get("session_start");
|
|
261
|
+
assert.ok(sessionStart, "session_start handler must be registered");
|
|
262
|
+
|
|
263
|
+
let headerFactory: ((tui: unknown, theme: unknown) => { render(width: number): string[] }) | undefined;
|
|
264
|
+
await sessionStart!({}, {
|
|
265
|
+
hasUI: true,
|
|
266
|
+
ui: {
|
|
267
|
+
notify: () => {},
|
|
268
|
+
setStatus: () => {},
|
|
269
|
+
setFooter: () => {},
|
|
270
|
+
setHeader: (factory: typeof headerFactory) => {
|
|
271
|
+
headerFactory = factory;
|
|
272
|
+
},
|
|
273
|
+
setWorkingMessage: () => {},
|
|
274
|
+
onTerminalInput: () => () => {},
|
|
275
|
+
setWidget: () => {},
|
|
276
|
+
},
|
|
277
|
+
sessionManager: { getSessionId: () => null },
|
|
278
|
+
model: null,
|
|
279
|
+
} as any);
|
|
280
|
+
|
|
281
|
+
assert.equal(typeof headerFactory, "function", "session_start should install a header factory");
|
|
282
|
+
const header = headerFactory!({}, {});
|
|
283
|
+
assert.deepEqual(header.render(123), ["welcome 9.9.9-test none 123"]);
|
|
284
|
+
});
|
|
285
|
+
|
|
209
286
|
test("session_start and session_switch apply disabled model provider policy from current preferences", async (t) => {
|
|
210
287
|
const dir = join(
|
|
211
288
|
tmpdir(),
|