gsd-pi 2.76.0-dev.82e249f7b → 2.76.0-dev.97807402
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/claude-cli-check.js +32 -3
- package/dist/mcp-server.d.ts +7 -0
- package/dist/mcp-server.js +35 -1
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +2 -8
- package/dist/resources/extensions/claude-code-cli/readiness.js +4 -3
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
- package/dist/resources/extensions/gsd/auto/phases.js +14 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +27 -0
- package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +13 -0
- package/dist/resources/extensions/gsd/auto-start.js +27 -18
- package/dist/resources/extensions/gsd/auto-worktree.js +30 -48
- package/dist/resources/extensions/gsd/auto.js +13 -17
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +40 -4
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +12 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
- package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
- package/dist/resources/extensions/gsd/error-classifier.js +10 -3
- package/dist/resources/extensions/gsd/exec-history.js +120 -0
- package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
- package/dist/resources/extensions/gsd/gsd-db.js +115 -7
- package/dist/resources/extensions/gsd/guided-flow.js +189 -0
- package/dist/resources/extensions/gsd/health-widget.js +4 -1
- package/dist/resources/extensions/gsd/init-wizard.js +15 -1
- package/dist/resources/extensions/gsd/key-manager.js +6 -0
- package/dist/resources/extensions/gsd/model-router.js +36 -3
- package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -9
- package/dist/resources/extensions/gsd/preferences-types.js +9 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
- package/dist/resources/extensions/gsd/preferences.js +17 -17
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
- package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
- package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +10 -4
- package/dist/resources/extensions/gsd/safety/safety-harness.js +4 -0
- package/dist/resources/extensions/gsd/token-counter.js +22 -5
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
- package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
- package/dist/resources/skills/verify-before-complete/SKILL.md +2 -1
- package/dist/resources/skills/write-docs/SKILL.md +2 -1
- 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 +7 -7
- 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 +7 -7
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
- package/packages/mcp-server/dist/remote-questions.js +732 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +18 -1
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +64 -25
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +2 -1
- package/packages/mcp-server/src/remote-questions.test.ts +294 -0
- package/packages/mcp-server/src/remote-questions.ts +916 -0
- package/packages/mcp-server/src/server.ts +19 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
- package/packages/mcp-server/src/workflow-tools.ts +84 -43
- package/packages/mcp-server/tsconfig.test.json +19 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
- package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js +16 -1
- package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic-shared.ts +3 -1
- package/packages/pi-ai/src/providers/simple-options.ts +17 -1
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +14 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +16 -0
- package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
- package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
- package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
- package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
- package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
- package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
- package/src/resources/extensions/gsd/auto/phases.ts +14 -0
- package/src/resources/extensions/gsd/auto/run-unit.ts +29 -0
- package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +1 -2
- package/src/resources/extensions/gsd/auto-recovery.ts +15 -0
- package/src/resources/extensions/gsd/auto-start.ts +29 -19
- package/src/resources/extensions/gsd/auto-worktree.ts +34 -52
- package/src/resources/extensions/gsd/auto.ts +12 -17
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +42 -4
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +13 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
- package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
- package/src/resources/extensions/gsd/error-classifier.ts +10 -3
- package/src/resources/extensions/gsd/exec-history.ts +153 -0
- package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
- package/src/resources/extensions/gsd/gsd-db.ts +122 -7
- package/src/resources/extensions/gsd/guided-flow.ts +221 -0
- package/src/resources/extensions/gsd/health-widget.ts +3 -1
- package/src/resources/extensions/gsd/init-wizard.ts +15 -1
- package/src/resources/extensions/gsd/journal.ts +2 -1
- package/src/resources/extensions/gsd/key-manager.ts +6 -0
- package/src/resources/extensions/gsd/model-router.ts +42 -1
- package/src/resources/extensions/gsd/pre-execution-checks.ts +36 -10
- package/src/resources/extensions/gsd/preferences-types.ts +46 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
- package/src/resources/extensions/gsd/preferences.ts +17 -17
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
- package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +14 -3
- package/src/resources/extensions/gsd/safety/safety-harness.ts +6 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/escalation.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +152 -1
- package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/issue-4540-regressions.test.ts +288 -0
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +234 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
- package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
- package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
- package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
- package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
- package/src/resources/extensions/gsd/token-counter.ts +22 -5
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
- package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
- package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
- package/src/resources/skills/verify-before-complete/SKILL.md +2 -1
- package/src/resources/skills/write-docs/SKILL.md +2 -1
- /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → pI48IF3dgfs0CBrYi2bh_}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → pI48IF3dgfs0CBrYi2bh_}/_ssgManifest.js +0 -0
|
@@ -378,3 +378,59 @@ test("#2505: back-to-back merges preserve queued CONTEXT files", () => {
|
|
|
378
378
|
rmSync(repo, { recursive: true, force: true });
|
|
379
379
|
}
|
|
380
380
|
});
|
|
381
|
+
|
|
382
|
+
// #4573: When `.gsd` is a gitignored symlink (ADR-002 layout) and the project
|
|
383
|
+
// `.gitignore` contains `.gsd`, `git stash push --include-untracked -- <pathspec>`
|
|
384
|
+
// fatals with "The following paths are ignored by one of your .gitignore files".
|
|
385
|
+
// The prior tests used a symlinked `.gsd` but no `.gitignore`, so this failure
|
|
386
|
+
// mode was invisible to CI. Fixture must include BOTH the symlink AND the
|
|
387
|
+
// ignore rule to reproduce the bug on pre-fix code.
|
|
388
|
+
test("#4573: gitignored .gsd symlink does not break pre-merge stash", () => {
|
|
389
|
+
const repo = realpathSync(mkdtempSync(join(tmpdir(), "wt-4573-ignored-symlink-")));
|
|
390
|
+
const stateDir = realpathSync(mkdtempSync(join(tmpdir(), "wt-4573-state-")));
|
|
391
|
+
try {
|
|
392
|
+
run("git init", repo);
|
|
393
|
+
run("git config user.email test@test.com", repo);
|
|
394
|
+
run("git config user.name Test", repo);
|
|
395
|
+
writeFileSync(join(repo, "README.md"), "# test\n");
|
|
396
|
+
// Matches what BASELINE_PATTERNS in gitignore.ts writes for real projects.
|
|
397
|
+
writeFileSync(join(repo, ".gitignore"), ".gsd\n.gsd-id\n");
|
|
398
|
+
symlinkSync(stateDir, join(repo, ".gsd"));
|
|
399
|
+
run("git add README.md .gitignore", repo);
|
|
400
|
+
run("git commit -m init", repo);
|
|
401
|
+
run("git branch -M main", repo);
|
|
402
|
+
|
|
403
|
+
const wtPath = createAutoWorktree(repo, "M001");
|
|
404
|
+
const worktreeName = wtPath.replaceAll("\\", "/").split("/").pop() || "M001";
|
|
405
|
+
const sliceBranch = `slice/${worktreeName}/S01`;
|
|
406
|
+
run(`git checkout -b "${sliceBranch}"`, wtPath);
|
|
407
|
+
writeFileSync(join(wtPath, "app.ts"), "export const app = true;\n");
|
|
408
|
+
run("git add app.ts", wtPath);
|
|
409
|
+
run('git commit -m "add feature"', wtPath);
|
|
410
|
+
run("git checkout milestone/M001", wtPath);
|
|
411
|
+
run(`git merge --no-ff "${sliceBranch}" -m "merge S01"`, wtPath);
|
|
412
|
+
|
|
413
|
+
// Dirty a tracked file so the pre-merge stash branch actually runs.
|
|
414
|
+
writeFileSync(join(repo, "README.md"), "# test\n\nDirty.\n");
|
|
415
|
+
|
|
416
|
+
const result = mergeMilestoneToMain(
|
|
417
|
+
repo,
|
|
418
|
+
"M001",
|
|
419
|
+
makeRoadmap("M001", "Feature", [{ id: "S01", title: "Feature" }]),
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
assert.ok(
|
|
423
|
+
result.commitMessage.includes("GSD-Milestone: M001"),
|
|
424
|
+
"merge must succeed despite gitignored .gsd symlink",
|
|
425
|
+
);
|
|
426
|
+
assert.ok(existsSync(join(repo, "app.ts")), "milestone code merged to main");
|
|
427
|
+
assert.equal(
|
|
428
|
+
lstatSync(join(repo, ".gsd")).isSymbolicLink(),
|
|
429
|
+
true,
|
|
430
|
+
".gsd symlink remains in place",
|
|
431
|
+
);
|
|
432
|
+
} finally {
|
|
433
|
+
rmSync(repo, { recursive: true, force: true });
|
|
434
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
435
|
+
}
|
|
436
|
+
});
|
|
@@ -102,7 +102,8 @@ describe("token-counter: estimateTokensForProvider", () => {
|
|
|
102
102
|
|
|
103
103
|
describe("token-counter: backward compatibility", () => {
|
|
104
104
|
it("countTokensSync returns heuristic estimate when tiktoken is not loaded", () => {
|
|
105
|
-
// Without tiktoken loaded, countTokensSync falls back to
|
|
105
|
+
// Without tiktoken loaded, countTokensSync falls back to estimateTokensForProvider.
|
|
106
|
+
// With no provider (defaults to "unknown", ratio 4.0): ceil(100/4) = 25.
|
|
106
107
|
const text = "A".repeat(100);
|
|
107
108
|
const result = countTokensSync(text);
|
|
108
109
|
// Either tiktoken is loaded (exact count) or heuristic (ceil(100/4) = 25)
|
|
@@ -127,3 +128,106 @@ describe("token-counter: backward compatibility", () => {
|
|
|
127
128
|
assert.equal(result, 0);
|
|
128
129
|
});
|
|
129
130
|
});
|
|
131
|
+
|
|
132
|
+
// ─── provider-aware fallback (issue #4529) ───────────────────────────────────
|
|
133
|
+
// Regression tests: countTokens/countTokensSync must use provider-specific
|
|
134
|
+
// ratios for their heuristic fallback, not a hardcoded GPT-4o / 4 divisor.
|
|
135
|
+
|
|
136
|
+
describe("token-counter: provider-aware heuristic fallback", () => {
|
|
137
|
+
// These tests exercise the heuristic path (no tiktoken or before init).
|
|
138
|
+
// We call estimateTokensForProvider directly to validate expected values,
|
|
139
|
+
// then verify countTokens/countTokensSync return the same values when
|
|
140
|
+
// tiktoken is unavailable.
|
|
141
|
+
|
|
142
|
+
it("countTokensSync uses anthropic ratio (3.5) when provider is 'anthropic'", () => {
|
|
143
|
+
const text = "A".repeat(350);
|
|
144
|
+
// anthropic: ceil(350 / 3.5) = 100
|
|
145
|
+
// openai/unknown: ceil(350 / 4.0) = 88
|
|
146
|
+
// These are different — the provider must matter.
|
|
147
|
+
const anthropicEstimate = estimateTokensForProvider(text, "anthropic");
|
|
148
|
+
const unknownEstimate = estimateTokensForProvider(text, "unknown");
|
|
149
|
+
assert.equal(anthropicEstimate, 100, "anthropic ratio should give 100 tokens for 350 chars");
|
|
150
|
+
assert.equal(unknownEstimate, 88, "unknown ratio should give 88 tokens for 350 chars");
|
|
151
|
+
assert.notEqual(
|
|
152
|
+
anthropicEstimate,
|
|
153
|
+
unknownEstimate,
|
|
154
|
+
"anthropic and unknown estimates must differ — if they are equal the provider is being ignored",
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Actually call countTokensSync with the anthropic provider.
|
|
158
|
+
// When tiktoken is not loaded, this must return the provider-aware estimate (100).
|
|
159
|
+
// When tiktoken is loaded, it returns the tiktoken count (which is also > 0 and
|
|
160
|
+
// will be in the range [88, 120] for 350 "A" characters with cl100k_base).
|
|
161
|
+
const syncResult = countTokensSync(text, "anthropic");
|
|
162
|
+
assert.ok(typeof syncResult === "number", "countTokensSync must return a number");
|
|
163
|
+
assert.ok(syncResult > 0, "countTokensSync must return a positive count");
|
|
164
|
+
// If tiktoken is unavailable the result must exactly match the anthropic heuristic.
|
|
165
|
+
// If tiktoken is available we cannot assert the exact value, but we know it will
|
|
166
|
+
// not equal the unknown-provider heuristic (88) for 350 identical characters.
|
|
167
|
+
const tiktokenAvailable = syncResult !== anthropicEstimate;
|
|
168
|
+
if (!tiktokenAvailable) {
|
|
169
|
+
assert.equal(
|
|
170
|
+
syncResult,
|
|
171
|
+
anthropicEstimate,
|
|
172
|
+
`countTokensSync with 'anthropic' provider must return ${anthropicEstimate} (not the unknown-provider value ${unknownEstimate}) when tiktoken is unavailable`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("countTokens uses anthropic ratio when provider='anthropic' and tiktoken unavailable", async () => {
|
|
178
|
+
const text = "A".repeat(350);
|
|
179
|
+
// anthropic heuristic: ceil(350 / 3.5) = 100
|
|
180
|
+
// unknown/hardcoded-4 heuristic: ceil(350 / 4.0) = 88
|
|
181
|
+
const anthropicEstimate = estimateTokensForProvider(text, "anthropic"); // 100
|
|
182
|
+
const unknownEstimate = estimateTokensForProvider(text, "unknown"); // 88
|
|
183
|
+
const hardcodedFallback = Math.ceil(text.length / 4); // 88
|
|
184
|
+
|
|
185
|
+
// The anthropic heuristic must produce more tokens than the old /4 fallback.
|
|
186
|
+
assert.equal(anthropicEstimate, 100, "anthropic heuristic should give 100 tokens for 350 chars");
|
|
187
|
+
assert.ok(
|
|
188
|
+
anthropicEstimate > hardcodedFallback,
|
|
189
|
+
`anthropic estimate (${anthropicEstimate}) must exceed the hardcoded /4 fallback (${hardcodedFallback})`,
|
|
190
|
+
);
|
|
191
|
+
assert.notEqual(
|
|
192
|
+
anthropicEstimate,
|
|
193
|
+
unknownEstimate,
|
|
194
|
+
"anthropic and unknown estimates must differ — provider is being ignored if equal",
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Call countTokens with the anthropic provider and verify the result.
|
|
198
|
+
const result = await countTokens(text, "anthropic");
|
|
199
|
+
assert.ok(typeof result === "number", "should return a number");
|
|
200
|
+
assert.ok(result > 0, "should return a positive token count");
|
|
201
|
+
|
|
202
|
+
// When tiktoken is unavailable: result must equal the anthropic heuristic (100),
|
|
203
|
+
// NOT the unknown-provider heuristic (88). This is the core regression guard for #4529.
|
|
204
|
+
// When tiktoken IS available: result is the tiktoken count, which will differ from
|
|
205
|
+
// the heuristic — but it must never equal the wrong (unknown) heuristic for this text.
|
|
206
|
+
if (result === anthropicEstimate || result === unknownEstimate) {
|
|
207
|
+
// We are on the heuristic path — assert the correct provider ratio was used.
|
|
208
|
+
assert.equal(
|
|
209
|
+
result,
|
|
210
|
+
anthropicEstimate,
|
|
211
|
+
`countTokens with 'anthropic' provider returned ${result} but expected ${anthropicEstimate} (anthropic heuristic) not ${unknownEstimate} (unknown/hardcoded heuristic)`,
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
// tiktoken is active — result is an exact BPE count.
|
|
215
|
+
// For 350 identical "A" characters cl100k_base produces a count in [80, 130].
|
|
216
|
+
assert.ok(
|
|
217
|
+
result >= 80 && result <= 130,
|
|
218
|
+
`tiktoken count ${result} for 350 "A" chars is outside expected range [80, 130]`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("countTokens with provider='anthropic' yields more tokens than provider='openai' (heuristic)", () => {
|
|
224
|
+
const text = "A".repeat(400);
|
|
225
|
+
// anthropic: ceil(400/3.5) = 115, openai: ceil(400/4.0) = 100
|
|
226
|
+
const anthropic = estimateTokensForProvider(text, "anthropic");
|
|
227
|
+
const openai = estimateTokensForProvider(text, "openai");
|
|
228
|
+
assert.ok(
|
|
229
|
+
anthropic > openai,
|
|
230
|
+
`anthropic estimate (${anthropic}) must exceed openai estimate (${openai}) for same text`,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
isToolCompatibleWithProvider,
|
|
15
15
|
filterToolsForProvider,
|
|
16
16
|
adjustToolSet,
|
|
17
|
+
GROQ_MAX_TOOLS,
|
|
17
18
|
} from "../model-router.js";
|
|
18
19
|
|
|
19
20
|
import {
|
|
@@ -197,3 +198,109 @@ describe("adjustToolSet", () => {
|
|
|
197
198
|
assert.deepEqual(removedTools, ["mcp_complex"]);
|
|
198
199
|
});
|
|
199
200
|
});
|
|
201
|
+
|
|
202
|
+
// ─── GROQ_MAX_TOOLS constant ─────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
describe("GROQ_MAX_TOOLS", () => {
|
|
205
|
+
test("equals 128", () => {
|
|
206
|
+
assert.equal(GROQ_MAX_TOOLS, 128);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ─── Groq tool-count cap (#4376) ────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
describe("filterToolsForProvider — Groq 128-tool cap", () => {
|
|
213
|
+
beforeEach(() => {
|
|
214
|
+
resetToolCompatibilityRegistry();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("does not cap when provider is not groq", () => {
|
|
218
|
+
const toolNames = Array.from({ length: 200 }, (_, i) => `tool_${i}`);
|
|
219
|
+
const { compatible, filtered } = filterToolsForProvider(toolNames, "openai-completions");
|
|
220
|
+
assert.equal(compatible.length, 200);
|
|
221
|
+
assert.equal(filtered.length, 0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("does not cap when <= 128 tools with groq provider", () => {
|
|
225
|
+
const toolNames = Array.from({ length: 128 }, (_, i) => `tool_${i}`);
|
|
226
|
+
const { compatible, filtered } = filterToolsForProvider(toolNames, "openai-completions", "groq");
|
|
227
|
+
assert.equal(compatible.length, 128);
|
|
228
|
+
assert.equal(filtered.length, 0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("caps to 128 when >128 tools with groq provider", () => {
|
|
232
|
+
const toolNames = Array.from({ length: 200 }, (_, i) => `tool_${i}`);
|
|
233
|
+
const { compatible, filtered } = filterToolsForProvider(toolNames, "openai-completions", "groq");
|
|
234
|
+
assert.equal(compatible.length, 128);
|
|
235
|
+
assert.equal(filtered.length, 72);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("keeps the first 128 tools when capping", () => {
|
|
239
|
+
const toolNames = Array.from({ length: 200 }, (_, i) => `tool_${i}`);
|
|
240
|
+
const { compatible } = filterToolsForProvider(toolNames, "openai-completions", "groq");
|
|
241
|
+
assert.equal(compatible[0], "tool_0");
|
|
242
|
+
assert.equal(compatible[127], "tool_127");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("trimmed tools appear in filtered list", () => {
|
|
246
|
+
const toolNames = Array.from({ length: 130 }, (_, i) => `tool_${i}`);
|
|
247
|
+
const { filtered } = filterToolsForProvider(toolNames, "openai-completions", "groq");
|
|
248
|
+
assert.equal(filtered.length, 2);
|
|
249
|
+
assert.equal(filtered[0], "tool_128");
|
|
250
|
+
assert.equal(filtered[1], "tool_129");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("emits a warning when tools are trimmed", () => {
|
|
254
|
+
const warnings: string[] = [];
|
|
255
|
+
const original = console.warn;
|
|
256
|
+
console.warn = (...args: unknown[]) => { warnings.push(String(args[0])); };
|
|
257
|
+
try {
|
|
258
|
+
const toolNames = Array.from({ length: 129 }, (_, i) => `tool_${i}`);
|
|
259
|
+
filterToolsForProvider(toolNames, "openai-completions", "groq");
|
|
260
|
+
assert.equal(warnings.length, 1);
|
|
261
|
+
assert.ok(warnings[0].includes("128"), "warning mentions Groq limit");
|
|
262
|
+
} finally {
|
|
263
|
+
console.warn = original;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("does not warn when tools are at the limit", () => {
|
|
268
|
+
const warnings: string[] = [];
|
|
269
|
+
const original = console.warn;
|
|
270
|
+
console.warn = (...args: unknown[]) => { warnings.push(String(args[0])); };
|
|
271
|
+
try {
|
|
272
|
+
const toolNames = Array.from({ length: 128 }, (_, i) => `tool_${i}`);
|
|
273
|
+
filterToolsForProvider(toolNames, "openai-completions", "groq");
|
|
274
|
+
assert.equal(warnings.length, 0);
|
|
275
|
+
} finally {
|
|
276
|
+
console.warn = original;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("adjustToolSet — Groq 128-tool cap", () => {
|
|
282
|
+
beforeEach(() => {
|
|
283
|
+
resetToolCompatibilityRegistry();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("caps to 128 tools when provider is groq and >128 tools active", () => {
|
|
287
|
+
const toolNames = Array.from({ length: 150 }, (_, i) => `tool_${i}`);
|
|
288
|
+
const { toolNames: result, removedTools } = adjustToolSet(toolNames, "openai-completions", "groq");
|
|
289
|
+
assert.equal(result.length, 128);
|
|
290
|
+
assert.equal(removedTools.length, 22);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("does not cap for non-groq providers even with >128 tools", () => {
|
|
294
|
+
const toolNames = Array.from({ length: 150 }, (_, i) => `tool_${i}`);
|
|
295
|
+
const { toolNames: result, removedTools } = adjustToolSet(toolNames, "openai-completions", "openai");
|
|
296
|
+
assert.equal(result.length, 150);
|
|
297
|
+
assert.equal(removedTools.length, 0);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("does not cap when provider is omitted", () => {
|
|
301
|
+
const toolNames = Array.from({ length: 150 }, (_, i) => `tool_${i}`);
|
|
302
|
+
const { toolNames: result, removedTools } = adjustToolSet(toolNames, "openai-completions");
|
|
303
|
+
assert.equal(result.length, 150);
|
|
304
|
+
assert.equal(removedTools.length, 0);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdirSync, rmSync, readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, rmSync, readFileSync, existsSync, writeFileSync, unlinkSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
_getAdapter,
|
|
12
12
|
insertGateRow,
|
|
13
13
|
} from "../gsd-db.ts";
|
|
14
|
-
import { markDepthVerified, clearDiscussionFlowState } from "../bootstrap/write-gate.ts";
|
|
14
|
+
import { markDepthVerified, clearDiscussionFlowState, loadWriteGateSnapshot } from "../bootstrap/write-gate.ts";
|
|
15
15
|
import {
|
|
16
16
|
executeCompleteMilestone,
|
|
17
17
|
executePlanMilestone,
|
|
@@ -742,3 +742,66 @@ test("executeSummarySave leaves sibling CONTEXT-DRAFT intact for non-CONTEXT art
|
|
|
742
742
|
cleanup(base);
|
|
743
743
|
}
|
|
744
744
|
});
|
|
745
|
+
|
|
746
|
+
test("executeSummarySave CONTEXT HARD BLOCK clears after write-gate state file is deleted (#4343)", async () => {
|
|
747
|
+
const base = makeTmpBase();
|
|
748
|
+
const originalEnv = process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
749
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = "1";
|
|
750
|
+
try {
|
|
751
|
+
openTestDb(base);
|
|
752
|
+
clearDiscussionFlowState();
|
|
753
|
+
|
|
754
|
+
// First call: CONTEXT artifact without depth verification → HARD BLOCK
|
|
755
|
+
const blocked = await inProjectDir(base, () => executeSummarySave({
|
|
756
|
+
milestone_id: "M001",
|
|
757
|
+
artifact_type: "CONTEXT",
|
|
758
|
+
content: "# Context\n\ncontent",
|
|
759
|
+
}, base));
|
|
760
|
+
assert.equal(blocked.isError, true, "should be blocked without depth verification");
|
|
761
|
+
assert.match(
|
|
762
|
+
blocked.content[0].text,
|
|
763
|
+
/HARD BLOCK/,
|
|
764
|
+
"blocked result should mention HARD BLOCK",
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
// Verify the state file was written (persist mode is active)
|
|
768
|
+
const stateFilePath = join(base, ".gsd", "runtime", "write-gate-state.json");
|
|
769
|
+
// The state file may or may not exist at this point (block doesn't write state).
|
|
770
|
+
// Write a fake state file simulating stale persisted block state.
|
|
771
|
+
mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
|
|
772
|
+
writeFileSync(stateFilePath, JSON.stringify({
|
|
773
|
+
verifiedDepthMilestones: [],
|
|
774
|
+
activeQueuePhase: false,
|
|
775
|
+
pendingGateId: "depth_verification_M001",
|
|
776
|
+
}));
|
|
777
|
+
|
|
778
|
+
// User deletes the state file to reset the block
|
|
779
|
+
unlinkSync(stateFilePath);
|
|
780
|
+
assert.ok(!existsSync(stateFilePath), "state file deleted");
|
|
781
|
+
|
|
782
|
+
// The snapshot loaded after deletion should be clean (no pending gate, no block)
|
|
783
|
+
const snapshot = loadWriteGateSnapshot(base);
|
|
784
|
+
assert.equal(snapshot.pendingGateId, null, "pendingGateId should be null after file deletion");
|
|
785
|
+
assert.deepEqual(snapshot.verifiedDepthMilestones, [], "verifiedDepthMilestones should be empty after file deletion");
|
|
786
|
+
|
|
787
|
+
// Depth-verify and re-attempt: should succeed after deletion clears stale state
|
|
788
|
+
markDepthVerified("M001", base);
|
|
789
|
+
|
|
790
|
+
const unblocked = await inProjectDir(base, () => executeSummarySave({
|
|
791
|
+
milestone_id: "M001",
|
|
792
|
+
artifact_type: "CONTEXT",
|
|
793
|
+
content: "# Context\n\nfinal content",
|
|
794
|
+
}, base));
|
|
795
|
+
assert.equal(unblocked.isError, undefined, "should not be blocked after depth verification");
|
|
796
|
+
assert.equal(unblocked.details.operation, "save_summary");
|
|
797
|
+
} finally {
|
|
798
|
+
if (originalEnv === undefined) {
|
|
799
|
+
delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
800
|
+
} else {
|
|
801
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = originalEnv;
|
|
802
|
+
}
|
|
803
|
+
clearDiscussionFlowState();
|
|
804
|
+
closeDatabase();
|
|
805
|
+
cleanup(base);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
import test from 'node:test';
|
|
13
13
|
import assert from 'node:assert/strict';
|
|
14
|
+
import { mkdirSync, writeFileSync, unlinkSync, existsSync, rmSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
14
18
|
import {
|
|
15
19
|
isDepthConfirmationAnswer,
|
|
16
20
|
shouldBlockContextWrite,
|
|
@@ -20,8 +24,10 @@ import {
|
|
|
20
24
|
markDepthVerified,
|
|
21
25
|
isMilestoneDepthVerified,
|
|
22
26
|
shouldBlockContextArtifactSave,
|
|
27
|
+
shouldBlockContextArtifactSaveInSnapshot,
|
|
23
28
|
clearDiscussionFlowState,
|
|
24
29
|
resetWriteGateState,
|
|
30
|
+
loadWriteGateSnapshot,
|
|
25
31
|
} from '../bootstrap/write-gate.ts';
|
|
26
32
|
|
|
27
33
|
// ─── Scenario 1: Blocks CONTEXT.md write during discussion without depth verification (absolute path) ──
|
|
@@ -488,3 +494,61 @@ test('write-gate: isDepthConfirmationAnswer falls back to (Recommended) match wi
|
|
|
488
494
|
'should reject non-Recommended via fallback',
|
|
489
495
|
);
|
|
490
496
|
});
|
|
497
|
+
|
|
498
|
+
// ─── Scenario 29: loadWriteGateSnapshot returns clean state when persist file deleted (#4343) ──
|
|
499
|
+
|
|
500
|
+
test('write-gate: loadWriteGateSnapshot returns empty default when persist file is deleted (#4343)', () => {
|
|
501
|
+
const base = join(tmpdir(), `gsd-write-gate-4343-${randomUUID()}`);
|
|
502
|
+
mkdirSync(join(base, '.gsd', 'runtime'), { recursive: true });
|
|
503
|
+
const stateFilePath = join(base, '.gsd', 'runtime', 'write-gate-state.json');
|
|
504
|
+
const originalEnv = process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = '1';
|
|
508
|
+
|
|
509
|
+
// Write a state file with a pending gate and verified milestone
|
|
510
|
+
writeFileSync(stateFilePath, JSON.stringify({
|
|
511
|
+
verifiedDepthMilestones: ['M001'],
|
|
512
|
+
activeQueuePhase: false,
|
|
513
|
+
pendingGateId: 'depth_verification_M001',
|
|
514
|
+
}));
|
|
515
|
+
assert.ok(existsSync(stateFilePath), 'precondition: state file exists');
|
|
516
|
+
|
|
517
|
+
// While file exists, snapshot reflects its contents
|
|
518
|
+
const beforeDeletion = loadWriteGateSnapshot(base);
|
|
519
|
+
assert.strictEqual(beforeDeletion.pendingGateId, 'depth_verification_M001', 'pending gate from file');
|
|
520
|
+
assert.deepEqual(beforeDeletion.verifiedDepthMilestones, ['M001'], 'verified milestones from file');
|
|
521
|
+
|
|
522
|
+
// User deletes the state file to clear the HARD BLOCK
|
|
523
|
+
unlinkSync(stateFilePath);
|
|
524
|
+
assert.ok(!existsSync(stateFilePath), 'state file deleted');
|
|
525
|
+
|
|
526
|
+
// After deletion in persist mode, snapshot should be clean (not stale in-memory)
|
|
527
|
+
const afterDeletion = loadWriteGateSnapshot(base);
|
|
528
|
+
assert.strictEqual(afterDeletion.pendingGateId, null, 'pendingGateId cleared after file deletion');
|
|
529
|
+
assert.deepEqual(afterDeletion.verifiedDepthMilestones, [], 'verifiedDepthMilestones cleared after file deletion');
|
|
530
|
+
assert.strictEqual(afterDeletion.activeQueuePhase, false, 'activeQueuePhase cleared after file deletion');
|
|
531
|
+
|
|
532
|
+
// The CONTEXT artifact block check must also resolve to unblocked after deletion+verification
|
|
533
|
+
// (simulate the re-verify flow users would do: delete → depth verify → save)
|
|
534
|
+
const stillBlocked = shouldBlockContextArtifactSaveInSnapshot(afterDeletion, 'CONTEXT', 'M001', null);
|
|
535
|
+
assert.strictEqual(stillBlocked.block, true, 'still blocked without new depth verification');
|
|
536
|
+
|
|
537
|
+
const verifiedSnapshot = {
|
|
538
|
+
...afterDeletion,
|
|
539
|
+
verifiedDepthMilestones: ['M001'],
|
|
540
|
+
};
|
|
541
|
+
const unblocked = shouldBlockContextArtifactSaveInSnapshot(verifiedSnapshot, 'CONTEXT', 'M001', null);
|
|
542
|
+
assert.strictEqual(unblocked.block, false, 'unblocked after fresh depth verification');
|
|
543
|
+
} finally {
|
|
544
|
+
if (originalEnv === undefined) {
|
|
545
|
+
delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
546
|
+
} else {
|
|
547
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = originalEnv;
|
|
548
|
+
}
|
|
549
|
+
clearDiscussionFlowState();
|
|
550
|
+
try {
|
|
551
|
+
rmSync(base, { recursive: true, force: true });
|
|
552
|
+
} catch { /* swallow */ }
|
|
553
|
+
}
|
|
554
|
+
});
|
|
@@ -72,7 +72,9 @@ const autoStartSrc = readFileSync(
|
|
|
72
72
|
const symlinkIdx = autoStartSrc.indexOf("ensureGsdSymlink(base)");
|
|
73
73
|
assertTrue(symlinkIdx >= 0, "auto-start.ts calls ensureGsdSymlink(base)");
|
|
74
74
|
|
|
75
|
-
const afterSymlink = symlinkIdx >= 0
|
|
75
|
+
const afterSymlink = symlinkIdx >= 0
|
|
76
|
+
? autoStartSrc.slice(symlinkIdx, autoStartSrc.indexOf("Initialize GitServiceImpl", symlinkIdx))
|
|
77
|
+
: "";
|
|
76
78
|
|
|
77
79
|
// The milestones bootstrap must check milestones path, not gsdDir
|
|
78
80
|
// Old (dead) code: if (!existsSync(gsdDir)) { mkdirSync(join(gsdDir, "milestones"), ...) }
|
|
@@ -22,7 +22,11 @@ async function getEncoder(): Promise<TokenEncoder | null> {
|
|
|
22
22
|
try {
|
|
23
23
|
// @ts-ignore — tiktoken may not have type declarations in extensions tsconfig
|
|
24
24
|
const tiktoken = await import("tiktoken");
|
|
25
|
-
|
|
25
|
+
// Use cl100k_base — the most conservative and broadly compatible BPE encoding.
|
|
26
|
+
// It is shared by GPT-3.5/GPT-4 and gives a safer (larger) estimate than
|
|
27
|
+
// gpt-4o's o200k_base encoding, which produces fewer tokens for the same text
|
|
28
|
+
// and would cause context windows for non-OpenAI providers to be under-counted.
|
|
29
|
+
encoder = tiktoken.get_encoding("cl100k_base") as TokenEncoder;
|
|
26
30
|
return encoder;
|
|
27
31
|
} catch {
|
|
28
32
|
encoderFailed = true;
|
|
@@ -30,20 +34,33 @@ async function getEncoder(): Promise<TokenEncoder | null> {
|
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Count tokens in `text` using tiktoken (cl100k_base) when available.
|
|
39
|
+
*
|
|
40
|
+
* When tiktoken is not loaded, falls back to a provider-aware character-ratio
|
|
41
|
+
* estimate via `estimateTokensForProvider`. Passing `provider` is recommended
|
|
42
|
+
* so the heuristic fallback is as accurate as possible.
|
|
43
|
+
*/
|
|
44
|
+
export async function countTokens(text: string, provider?: TokenProvider): Promise<number> {
|
|
34
45
|
const enc = await getEncoder();
|
|
35
46
|
if (enc) {
|
|
36
47
|
const tokens = enc.encode(text);
|
|
37
48
|
return tokens.length;
|
|
38
49
|
}
|
|
39
|
-
return
|
|
50
|
+
return estimateTokensForProvider(text, provider ?? "unknown");
|
|
40
51
|
}
|
|
41
52
|
|
|
42
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Synchronous token count — only accurate after `initTokenCounter()` resolves.
|
|
55
|
+
*
|
|
56
|
+
* Before init, or when tiktoken is unavailable, falls back to a provider-aware
|
|
57
|
+
* character-ratio estimate. Passing `provider` is recommended.
|
|
58
|
+
*/
|
|
59
|
+
export function countTokensSync(text: string, provider?: TokenProvider): number {
|
|
43
60
|
if (encoder) {
|
|
44
61
|
return encoder.encode(text).length;
|
|
45
62
|
}
|
|
46
|
-
return
|
|
63
|
+
return estimateTokensForProvider(text, provider ?? "unknown");
|
|
47
64
|
}
|
|
48
65
|
|
|
49
66
|
export async function initTokenCounter(): Promise<boolean> {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// GSD Exec Search Tool — lists and filters prior gsd_exec runs.
|
|
2
|
+
//
|
|
3
|
+
// Scans .gsd/exec/*.meta.json and returns a ranked summary so agents can
|
|
4
|
+
// re-discover past runs without re-executing. Read-only; no DB writes.
|
|
5
|
+
|
|
6
|
+
import { searchExecHistory, type ExecSearchOptions } from "../exec-history.js";
|
|
7
|
+
|
|
8
|
+
export interface ExecSearchToolParams {
|
|
9
|
+
query?: string;
|
|
10
|
+
runtime?: "bash" | "node" | "python";
|
|
11
|
+
failing_only?: boolean;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ToolExecutionResult {
|
|
16
|
+
content: Array<{ type: "text"; text: string }>;
|
|
17
|
+
details: Record<string, unknown>;
|
|
18
|
+
isError?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function executeExecSearch(
|
|
22
|
+
params: ExecSearchToolParams,
|
|
23
|
+
opts: { baseDir: string },
|
|
24
|
+
): ToolExecutionResult {
|
|
25
|
+
const searchOpts: ExecSearchOptions = {
|
|
26
|
+
query: typeof params.query === "string" ? params.query : undefined,
|
|
27
|
+
runtime: params.runtime,
|
|
28
|
+
failing_only: params.failing_only === true,
|
|
29
|
+
limit: typeof params.limit === "number" ? params.limit : undefined,
|
|
30
|
+
};
|
|
31
|
+
const hits = searchExecHistory(opts.baseDir, searchOpts);
|
|
32
|
+
|
|
33
|
+
if (hits.length === 0) {
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: "No prior gsd_exec runs match those filters." }],
|
|
36
|
+
details: { operation: "gsd_exec_search", matches: 0 },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const lines: string[] = [`Found ${hits.length} exec run(s), most recent first:`];
|
|
41
|
+
for (const hit of hits) {
|
|
42
|
+
const e = hit.entry;
|
|
43
|
+
const status = formatStatus(e);
|
|
44
|
+
const purpose = e.purpose ? ` — ${e.purpose}` : "";
|
|
45
|
+
const truncated = e.stdout_truncated ? " (stdout truncated)" : "";
|
|
46
|
+
lines.push(
|
|
47
|
+
`- [${e.id}] ${e.runtime} ${status} ${e.duration_ms}ms${truncated}${purpose}`,
|
|
48
|
+
` stdout: ${e.stdout_path}`,
|
|
49
|
+
);
|
|
50
|
+
if (hit.digest_preview) {
|
|
51
|
+
const preview = hit.digest_preview.replace(/\n/g, "\n ");
|
|
52
|
+
lines.push(` preview:\n ${preview}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
58
|
+
details: {
|
|
59
|
+
operation: "gsd_exec_search",
|
|
60
|
+
matches: hits.length,
|
|
61
|
+
results: hits.map((hit) => ({
|
|
62
|
+
id: hit.entry.id,
|
|
63
|
+
runtime: hit.entry.runtime,
|
|
64
|
+
exit_code: hit.entry.exit_code,
|
|
65
|
+
timed_out: hit.entry.timed_out,
|
|
66
|
+
duration_ms: hit.entry.duration_ms,
|
|
67
|
+
purpose: hit.entry.purpose,
|
|
68
|
+
stdout_path: hit.entry.stdout_path,
|
|
69
|
+
stderr_path: hit.entry.stderr_path,
|
|
70
|
+
meta_path: hit.entry.meta_path,
|
|
71
|
+
})),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatStatus(entry: { exit_code: number | null; timed_out: boolean; signal: string | null }): string {
|
|
77
|
+
if (entry.timed_out) return "timeout";
|
|
78
|
+
if (entry.signal) return `signal:${entry.signal}`;
|
|
79
|
+
if (entry.exit_code === null) return "exit:null";
|
|
80
|
+
return `exit:${entry.exit_code}`;
|
|
81
|
+
}
|