gsd-pi 2.19.0 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/cli.js +3 -3
- package/dist/onboarding.d.ts +3 -1
- package/dist/onboarding.js +77 -3
- package/dist/remote-questions-config.d.ts +1 -1
- package/dist/resources/extensions/google-search/index.ts +164 -47
- package/dist/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/dist/resources/extensions/gsd/auto.ts +424 -30
- package/dist/resources/extensions/gsd/commands.ts +518 -36
- package/dist/resources/extensions/gsd/context-budget.ts +243 -0
- package/dist/resources/extensions/gsd/context-store.ts +195 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/dist/resources/extensions/gsd/db-writer.ts +341 -0
- package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/dist/resources/extensions/gsd/doctor.ts +283 -2
- package/dist/resources/extensions/gsd/export.ts +81 -2
- package/dist/resources/extensions/gsd/files.ts +39 -9
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
- package/dist/resources/extensions/gsd/history.ts +0 -1
- package/dist/resources/extensions/gsd/index.ts +277 -1
- package/dist/resources/extensions/gsd/md-importer.ts +526 -0
- package/dist/resources/extensions/gsd/metrics.ts +39 -3
- package/dist/resources/extensions/gsd/notifications.ts +0 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/dist/resources/extensions/gsd/preferences.ts +125 -150
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/quick.ts +156 -0
- package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/dist/resources/extensions/gsd/skill-health.ts +417 -0
- package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/dist/resources/extensions/gsd/types.ts +29 -0
- package/dist/resources/extensions/gsd/undo.ts +0 -1
- package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/dist/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/dist/resources/extensions/remote-questions/config.ts +4 -2
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/dist/resources/extensions/remote-questions/format.ts +154 -8
- package/dist/resources/extensions/remote-questions/manager.ts +9 -7
- package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/dist/resources/extensions/remote-questions/types.ts +2 -1
- package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/dist/resources/extensions/voice/index.ts +4 -3
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
- 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 +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
- package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
- package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
- package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
- package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
- package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
- package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
- package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
- package/src/resources/extensions/google-search/index.ts +164 -47
- package/src/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/src/resources/extensions/gsd/auto.ts +424 -30
- package/src/resources/extensions/gsd/commands.ts +518 -36
- package/src/resources/extensions/gsd/context-budget.ts +243 -0
- package/src/resources/extensions/gsd/context-store.ts +195 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/src/resources/extensions/gsd/db-writer.ts +341 -0
- package/src/resources/extensions/gsd/debug-logger.ts +178 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/src/resources/extensions/gsd/doctor.ts +283 -2
- package/src/resources/extensions/gsd/export.ts +81 -2
- package/src/resources/extensions/gsd/files.ts +39 -9
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gsd-db.ts +752 -0
- package/src/resources/extensions/gsd/guided-flow.ts +26 -1
- package/src/resources/extensions/gsd/history.ts +0 -1
- package/src/resources/extensions/gsd/index.ts +277 -1
- package/src/resources/extensions/gsd/md-importer.ts +526 -0
- package/src/resources/extensions/gsd/metrics.ts +39 -3
- package/src/resources/extensions/gsd/notifications.ts +0 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/src/resources/extensions/gsd/preferences.ts +125 -150
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/quick.ts +156 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/src/resources/extensions/gsd/skill-health.ts +417 -0
- package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/src/resources/extensions/gsd/types.ts +29 -0
- package/src/resources/extensions/gsd/undo.ts +0 -1
- package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/src/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/src/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/src/resources/extensions/gsd/worktree-command.ts +18 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/src/resources/extensions/remote-questions/config.ts +4 -2
- package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/src/resources/extensions/remote-questions/format.ts +154 -8
- package/src/resources/extensions/remote-questions/manager.ts +9 -7
- package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/src/resources/extensions/remote-questions/types.ts +2 -1
- package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/src/resources/extensions/voice/index.ts +4 -3
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Skill Telemetry — Track which skills are loaded per unit (#599)
|
|
3
|
+
*
|
|
4
|
+
* Captures skill names at dispatch time for inclusion in UnitMetrics.
|
|
5
|
+
* Distinguishes between "available" skills (in system prompt) and
|
|
6
|
+
* "actively loaded" skills (read via tool calls during execution).
|
|
7
|
+
*
|
|
8
|
+
* Data flow:
|
|
9
|
+
* 1. At dispatch, captureAvailableSkills() records skills from the system prompt
|
|
10
|
+
* 2. During execution, recordSkillRead() tracks explicit SKILL.md reads
|
|
11
|
+
* 3. At unit completion, getAndClearSkills() returns the loaded list for metrics
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { getAgentDir } from "@gsd/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
// ─── In-memory state ──────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Skills available in the system prompt for the current unit */
|
|
21
|
+
let availableSkills: string[] = [];
|
|
22
|
+
|
|
23
|
+
/** Skills explicitly read (SKILL.md loaded) during the current unit */
|
|
24
|
+
const activelyLoadedSkills = new Set<string>();
|
|
25
|
+
|
|
26
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Capture the list of available skill names at dispatch time.
|
|
30
|
+
* Called before each unit starts.
|
|
31
|
+
*/
|
|
32
|
+
export function captureAvailableSkills(): void {
|
|
33
|
+
const skillsDir = join(getAgentDir(), "skills");
|
|
34
|
+
availableSkills = listSkillNames(skillsDir);
|
|
35
|
+
activelyLoadedSkills.clear();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Record that a skill was actively loaded (its SKILL.md was read).
|
|
40
|
+
* Call this when the agent reads a SKILL.md file.
|
|
41
|
+
*/
|
|
42
|
+
export function recordSkillRead(skillName: string): void {
|
|
43
|
+
activelyLoadedSkills.add(skillName);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get the skill names for the current unit and clear state.
|
|
48
|
+
* Returns actively loaded skills if any, otherwise available skills.
|
|
49
|
+
* This gives the most useful signal: if the agent read specific skills,
|
|
50
|
+
* report those; otherwise report what was available.
|
|
51
|
+
*/
|
|
52
|
+
export function getAndClearSkills(): string[] {
|
|
53
|
+
const result = activelyLoadedSkills.size > 0
|
|
54
|
+
? Array.from(activelyLoadedSkills)
|
|
55
|
+
: [...availableSkills];
|
|
56
|
+
availableSkills = [];
|
|
57
|
+
activelyLoadedSkills.clear();
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Reset all telemetry state. Called when auto-mode stops.
|
|
63
|
+
*/
|
|
64
|
+
export function resetSkillTelemetry(): void {
|
|
65
|
+
availableSkills = [];
|
|
66
|
+
activelyLoadedSkills.clear();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get last-used timestamps for all skills from metrics data.
|
|
71
|
+
* Returns a Map from skill name to most recent ms timestamp.
|
|
72
|
+
*/
|
|
73
|
+
export function getSkillLastUsed(units: Array<{ finishedAt: number; skills?: string[] }>): Map<string, number> {
|
|
74
|
+
const lastUsed = new Map<string, number>();
|
|
75
|
+
for (const u of units) {
|
|
76
|
+
if (!u.skills) continue;
|
|
77
|
+
for (const skill of u.skills) {
|
|
78
|
+
const existing = lastUsed.get(skill) ?? 0;
|
|
79
|
+
if (u.finishedAt > existing) {
|
|
80
|
+
lastUsed.set(skill, u.finishedAt);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return lastUsed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Detect stale skills — those not used within the given threshold (in days).
|
|
89
|
+
* Returns skill names that should be deprioritized.
|
|
90
|
+
*/
|
|
91
|
+
export function detectStaleSkills(
|
|
92
|
+
units: Array<{ finishedAt: number; skills?: string[] }>,
|
|
93
|
+
thresholdDays: number,
|
|
94
|
+
): string[] {
|
|
95
|
+
if (thresholdDays <= 0) return [];
|
|
96
|
+
|
|
97
|
+
const lastUsed = getSkillLastUsed(units);
|
|
98
|
+
const cutoff = Date.now() - (thresholdDays * 24 * 60 * 60 * 1000);
|
|
99
|
+
const stale: string[] = [];
|
|
100
|
+
|
|
101
|
+
// Check all installed skills, not just those with usage data
|
|
102
|
+
const skillsDir = join(getAgentDir(), "skills");
|
|
103
|
+
const installed = listSkillNames(skillsDir);
|
|
104
|
+
|
|
105
|
+
for (const skill of installed) {
|
|
106
|
+
const lastTs = lastUsed.get(skill);
|
|
107
|
+
if (lastTs === undefined || lastTs < cutoff) {
|
|
108
|
+
stale.push(skill);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return stale;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Internals ────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
function listSkillNames(skillsDir: string): string[] {
|
|
118
|
+
if (!existsSync(skillsDir)) return [];
|
|
119
|
+
try {
|
|
120
|
+
return readdirSync(skillsDir, { withFileTypes: true })
|
|
121
|
+
.filter(d => d.isDirectory() && !d.name.startsWith("."))
|
|
122
|
+
.filter(d => existsSync(join(skillsDir, d.name, "SKILL.md")))
|
|
123
|
+
.map(d => d.name);
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -32,8 +32,10 @@ import {
|
|
|
32
32
|
|
|
33
33
|
import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
|
|
34
34
|
import { nativeBatchParseGsdFiles, type BatchParsedFile } from './native-parser-bridge.js';
|
|
35
|
+
import { isDbAvailable, _getAdapter } from './gsd-db.js';
|
|
35
36
|
|
|
36
37
|
import { join, resolve } from 'path';
|
|
38
|
+
import { debugCount, debugTime } from './debug-logger.js';
|
|
37
39
|
|
|
38
40
|
// ─── Query Functions ───────────────────────────────────────────────────────
|
|
39
41
|
|
|
@@ -116,7 +118,10 @@ export async function deriveState(basePath: string): Promise<GSDState> {
|
|
|
116
118
|
return _stateCache.result;
|
|
117
119
|
}
|
|
118
120
|
|
|
121
|
+
const stopTimer = debugTime("derive-state-impl");
|
|
119
122
|
const result = await _deriveStateImpl(basePath);
|
|
123
|
+
stopTimer({ phase: result.phase, milestone: result.activeMilestone?.id });
|
|
124
|
+
debugCount("deriveStateCalls");
|
|
120
125
|
_stateCache = { basePath, result, timestamp: Date.now() };
|
|
121
126
|
return result;
|
|
122
127
|
}
|
|
@@ -131,6 +136,30 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
131
136
|
const fileContentCache = new Map<string, string>();
|
|
132
137
|
const gsdDir = gsdRoot(basePath);
|
|
133
138
|
|
|
139
|
+
// ── DB-first content loading ──
|
|
140
|
+
// When the DB is available, load artifact content from the artifacts table
|
|
141
|
+
// (indexed SELECT instead of O(N) file I/O). Falls back to native Rust batch
|
|
142
|
+
// parser, which in turn falls back to sequential JS reads via cachedLoadFile.
|
|
143
|
+
let dbContentLoaded = false;
|
|
144
|
+
if (isDbAvailable()) {
|
|
145
|
+
const adapter = _getAdapter();
|
|
146
|
+
if (adapter) {
|
|
147
|
+
try {
|
|
148
|
+
const rows = adapter.prepare('SELECT path, full_content FROM artifacts').all();
|
|
149
|
+
for (const row of rows) {
|
|
150
|
+
const relPath = (row as Record<string, unknown>)['path'] as string;
|
|
151
|
+
const content = (row as Record<string, unknown>)['full_content'] as string;
|
|
152
|
+
const absPath = resolve(gsdDir, relPath);
|
|
153
|
+
fileContentCache.set(absPath, content);
|
|
154
|
+
}
|
|
155
|
+
dbContentLoaded = rows.length > 0;
|
|
156
|
+
} catch {
|
|
157
|
+
// DB query failed — fall through to native batch parse
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!dbContentLoaded) {
|
|
134
163
|
const batchFiles = nativeBatchParseGsdFiles(gsdDir);
|
|
135
164
|
if (batchFiles) {
|
|
136
165
|
for (const f of batchFiles) {
|
|
@@ -138,6 +167,7 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
138
167
|
fileContentCache.set(absPath, f.rawContent);
|
|
139
168
|
}
|
|
140
169
|
}
|
|
170
|
+
}
|
|
141
171
|
|
|
142
172
|
/**
|
|
143
173
|
* Load file content from batch cache first, falling back to disk read.
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for context-budget.ts — the budget engine.
|
|
3
|
+
* Tests pure functions with dependency-injected fakes.
|
|
4
|
+
* No I/O, no extension context, no global state.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type BudgetAllocation,
|
|
12
|
+
type MinimalModel,
|
|
13
|
+
type MinimalModelRegistry,
|
|
14
|
+
type MinimalPreferences,
|
|
15
|
+
type TruncationResult,
|
|
16
|
+
computeBudgets,
|
|
17
|
+
truncateAtSectionBoundary,
|
|
18
|
+
resolveExecutorContextWindow,
|
|
19
|
+
} from "../context-budget.js";
|
|
20
|
+
|
|
21
|
+
// ─── Test helpers ─────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function makeRegistry(models: MinimalModel[]): MinimalModelRegistry {
|
|
24
|
+
return { getAll: () => models };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeModel(id: string, provider: string, contextWindow: number): MinimalModel {
|
|
28
|
+
return { id, provider, contextWindow };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── computeBudgets ──────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
describe("context-budget: computeBudgets", () => {
|
|
34
|
+
it("returns proportional allocations for 128K context window", () => {
|
|
35
|
+
const b = computeBudgets(128_000);
|
|
36
|
+
// 128K tokens × 4 chars/token = 512K chars total
|
|
37
|
+
assert.equal(b.summaryBudgetChars, Math.floor(512_000 * 0.15));
|
|
38
|
+
assert.equal(b.inlineContextBudgetChars, Math.floor(512_000 * 0.40));
|
|
39
|
+
assert.equal(b.verificationBudgetChars, Math.floor(512_000 * 0.10));
|
|
40
|
+
assert.equal(b.continueThresholdPercent, 70);
|
|
41
|
+
assert.equal(b.taskCountRange.min, 2);
|
|
42
|
+
assert.equal(b.taskCountRange.max, 5);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns proportional allocations for 200K context window", () => {
|
|
46
|
+
const b = computeBudgets(200_000);
|
|
47
|
+
// 200K tokens × 4 = 800K chars
|
|
48
|
+
assert.equal(b.summaryBudgetChars, Math.floor(800_000 * 0.15));
|
|
49
|
+
assert.equal(b.inlineContextBudgetChars, Math.floor(800_000 * 0.40));
|
|
50
|
+
assert.equal(b.verificationBudgetChars, Math.floor(800_000 * 0.10));
|
|
51
|
+
assert.equal(b.taskCountRange.min, 2);
|
|
52
|
+
assert.equal(b.taskCountRange.max, 6);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns proportional allocations for 1M context window", () => {
|
|
56
|
+
const b = computeBudgets(1_000_000);
|
|
57
|
+
// 1M tokens × 4 = 4M chars
|
|
58
|
+
assert.equal(b.summaryBudgetChars, Math.floor(4_000_000 * 0.15));
|
|
59
|
+
assert.equal(b.inlineContextBudgetChars, Math.floor(4_000_000 * 0.40));
|
|
60
|
+
assert.equal(b.verificationBudgetChars, Math.floor(4_000_000 * 0.10));
|
|
61
|
+
assert.equal(b.taskCountRange.min, 2);
|
|
62
|
+
assert.equal(b.taskCountRange.max, 8);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("scales proportionally — 1M > 200K > 128K for all budget fields", () => {
|
|
66
|
+
const b128 = computeBudgets(128_000);
|
|
67
|
+
const b200 = computeBudgets(200_000);
|
|
68
|
+
const b1M = computeBudgets(1_000_000);
|
|
69
|
+
|
|
70
|
+
assert.ok(b1M.summaryBudgetChars > b200.summaryBudgetChars);
|
|
71
|
+
assert.ok(b200.summaryBudgetChars > b128.summaryBudgetChars);
|
|
72
|
+
|
|
73
|
+
assert.ok(b1M.inlineContextBudgetChars > b200.inlineContextBudgetChars);
|
|
74
|
+
assert.ok(b200.inlineContextBudgetChars > b128.inlineContextBudgetChars);
|
|
75
|
+
|
|
76
|
+
assert.ok(b1M.verificationBudgetChars > b200.verificationBudgetChars);
|
|
77
|
+
assert.ok(b200.verificationBudgetChars > b128.verificationBudgetChars);
|
|
78
|
+
|
|
79
|
+
assert.ok(b1M.taskCountRange.max >= b200.taskCountRange.max);
|
|
80
|
+
assert.ok(b200.taskCountRange.max >= b128.taskCountRange.max);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("enforces task count floor (min ≥ 2) at all sizes", () => {
|
|
84
|
+
for (const size of [128_000, 200_000, 1_000_000, 50_000]) {
|
|
85
|
+
const b = computeBudgets(size);
|
|
86
|
+
assert.ok(b.taskCountRange.min >= 2, `min should be ≥ 2 at ${size}, got ${b.taskCountRange.min}`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("task count ceiling exists and is bounded", () => {
|
|
91
|
+
const b = computeBudgets(10_000_000); // very large window
|
|
92
|
+
assert.ok(b.taskCountRange.max <= 8, `max should be capped, got ${b.taskCountRange.max}`);
|
|
93
|
+
assert.ok(b.taskCountRange.max >= b.taskCountRange.min);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("handles zero input gracefully — defaults to 200K", () => {
|
|
97
|
+
const b = computeBudgets(0);
|
|
98
|
+
const b200 = computeBudgets(200_000);
|
|
99
|
+
assert.deepStrictEqual(b, b200);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("handles negative input gracefully — defaults to 200K", () => {
|
|
103
|
+
const b = computeBudgets(-100);
|
|
104
|
+
const b200 = computeBudgets(200_000);
|
|
105
|
+
assert.deepStrictEqual(b, b200);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ─── truncateAtSectionBoundary ───────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
describe("context-budget: truncateAtSectionBoundary", () => {
|
|
112
|
+
it("returns content unchanged when under budget", () => {
|
|
113
|
+
const content = "### Section 1\nSome text.\n\n### Section 2\nMore text.";
|
|
114
|
+
const result = truncateAtSectionBoundary(content, 10_000);
|
|
115
|
+
assert.equal(result.content, content);
|
|
116
|
+
assert.equal(result.droppedSections, 0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("returns empty string unchanged", () => {
|
|
120
|
+
const result = truncateAtSectionBoundary("", 100);
|
|
121
|
+
assert.equal(result.content, "");
|
|
122
|
+
assert.equal(result.droppedSections, 0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("truncates at section boundary with ### markers", () => {
|
|
126
|
+
const content = [
|
|
127
|
+
"### Section A\nContent A is here.\n",
|
|
128
|
+
"### Section B\nContent B is here.\n",
|
|
129
|
+
"### Section C\nContent C is here.\n",
|
|
130
|
+
].join("");
|
|
131
|
+
|
|
132
|
+
// Budget enough for section A only
|
|
133
|
+
const sectionALen = "### Section A\nContent A is here.\n".length;
|
|
134
|
+
const result = truncateAtSectionBoundary(content, sectionALen + 5);
|
|
135
|
+
|
|
136
|
+
assert.ok(result.content.includes("### Section A"), "should keep section A");
|
|
137
|
+
assert.ok(result.content.includes("Content A"), "should keep section A content");
|
|
138
|
+
assert.ok(!result.content.includes("### Section C"), "should drop section C");
|
|
139
|
+
assert.ok(result.content.includes("[...truncated"), "should include truncation indicator");
|
|
140
|
+
// Verify truncation count
|
|
141
|
+
assert.ok(result.content.includes("truncated 2 sections"), `should show 2 truncated, got: ${result.content}`);
|
|
142
|
+
assert.equal(result.droppedSections, 2);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("truncates at --- divider boundaries", () => {
|
|
146
|
+
const content = "Intro text.\n\n---\n\nMiddle section.\n\n---\n\nFinal section.";
|
|
147
|
+
// Budget enough for intro only
|
|
148
|
+
const result = truncateAtSectionBoundary(content, 20);
|
|
149
|
+
|
|
150
|
+
assert.ok(result.content.includes("Intro text"), "should keep intro");
|
|
151
|
+
assert.ok(result.content.includes("[...truncated"), "should include truncation indicator");
|
|
152
|
+
assert.ok(result.droppedSections > 0, "should report dropped sections");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("handles content with no section markers — keeps as much as fits", () => {
|
|
156
|
+
const content = "A".repeat(200);
|
|
157
|
+
const result = truncateAtSectionBoundary(content, 50);
|
|
158
|
+
|
|
159
|
+
assert.ok(result.content.length < 200, "should be shorter than original");
|
|
160
|
+
assert.ok(result.content.includes("[...truncated 1 sections]"), "should indicate truncation");
|
|
161
|
+
assert.ok(result.content.startsWith("AAAA"), "should keep content from the start");
|
|
162
|
+
assert.equal(result.droppedSections, 1);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("handles content at exact boundary — returns unchanged", () => {
|
|
166
|
+
const content = "### Section 1\nText here.";
|
|
167
|
+
const result = truncateAtSectionBoundary(content, content.length);
|
|
168
|
+
assert.equal(result.content, content);
|
|
169
|
+
assert.equal(result.droppedSections, 0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("always keeps at least the first section even if it exceeds budget", () => {
|
|
173
|
+
const content = "### Long Section\n" + "X".repeat(500) + "\n\n### Short\nY";
|
|
174
|
+
const result = truncateAtSectionBoundary(content, 10);
|
|
175
|
+
|
|
176
|
+
// First section should be present even though it exceeds budget
|
|
177
|
+
assert.ok(result.content.includes("### Long Section"), "should keep first section");
|
|
178
|
+
assert.ok(result.content.includes("[...truncated 1 sections]"), "should indicate remaining sections dropped");
|
|
179
|
+
assert.equal(result.droppedSections, 1);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ─── resolveExecutorContextWindow ────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
describe("context-budget: resolveExecutorContextWindow", () => {
|
|
186
|
+
it("returns configured executor model's contextWindow when found", () => {
|
|
187
|
+
const registry = makeRegistry([
|
|
188
|
+
makeModel("claude-opus-4-6", "anthropic", 200_000),
|
|
189
|
+
makeModel("claude-sonnet-4-20250514", "anthropic", 200_000),
|
|
190
|
+
makeModel("gpt-4o", "openai", 128_000),
|
|
191
|
+
]);
|
|
192
|
+
const prefs: MinimalPreferences = {
|
|
193
|
+
models: { execution: "gpt-4o" },
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const result = resolveExecutorContextWindow(registry, prefs);
|
|
197
|
+
assert.equal(result, 128_000);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("supports provider/model format in preferences", () => {
|
|
201
|
+
const registry = makeRegistry([
|
|
202
|
+
makeModel("gpt-4o", "openai", 128_000),
|
|
203
|
+
makeModel("gpt-4o", "azure", 64_000),
|
|
204
|
+
]);
|
|
205
|
+
const prefs: MinimalPreferences = {
|
|
206
|
+
models: { execution: "azure/gpt-4o" },
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const result = resolveExecutorContextWindow(registry, prefs);
|
|
210
|
+
assert.equal(result, 64_000);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("supports object format preferences with model + fallbacks", () => {
|
|
214
|
+
const registry = makeRegistry([
|
|
215
|
+
makeModel("claude-opus-4-6", "anthropic", 200_000),
|
|
216
|
+
]);
|
|
217
|
+
const prefs: MinimalPreferences = {
|
|
218
|
+
models: { execution: { model: "claude-opus-4-6", fallbacks: ["gpt-4o"] } },
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const result = resolveExecutorContextWindow(registry, prefs);
|
|
222
|
+
assert.equal(result, 200_000);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("falls back to sessionContextWindow when executor model not found", () => {
|
|
226
|
+
const registry = makeRegistry([
|
|
227
|
+
makeModel("claude-opus-4-6", "anthropic", 200_000),
|
|
228
|
+
]);
|
|
229
|
+
const prefs: MinimalPreferences = {
|
|
230
|
+
models: { execution: "nonexistent-model" },
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const result = resolveExecutorContextWindow(registry, prefs, 300_000);
|
|
234
|
+
assert.equal(result, 300_000);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("falls back to sessionContextWindow when no execution preference set", () => {
|
|
238
|
+
const registry = makeRegistry([
|
|
239
|
+
makeModel("claude-opus-4-6", "anthropic", 200_000),
|
|
240
|
+
]);
|
|
241
|
+
const prefs: MinimalPreferences = { models: {} };
|
|
242
|
+
|
|
243
|
+
const result = resolveExecutorContextWindow(registry, prefs, 128_000);
|
|
244
|
+
assert.equal(result, 128_000);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("falls back to 200K when no session and no executor model", () => {
|
|
248
|
+
const registry = makeRegistry([]);
|
|
249
|
+
const prefs: MinimalPreferences = { models: { execution: "missing" } };
|
|
250
|
+
|
|
251
|
+
const result = resolveExecutorContextWindow(registry, prefs);
|
|
252
|
+
assert.equal(result, 200_000);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("falls back to 200K with undefined preferences", () => {
|
|
256
|
+
const result = resolveExecutorContextWindow(undefined, undefined);
|
|
257
|
+
assert.equal(result, 200_000);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("falls back to 200K with undefined registry", () => {
|
|
261
|
+
const prefs: MinimalPreferences = { models: { execution: "claude-opus-4-6" } };
|
|
262
|
+
const result = resolveExecutorContextWindow(undefined, prefs);
|
|
263
|
+
assert.equal(result, 200_000);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("ignores models with contextWindow ≤ 0", () => {
|
|
267
|
+
const registry = makeRegistry([
|
|
268
|
+
makeModel("broken-model", "test", 0),
|
|
269
|
+
]);
|
|
270
|
+
const prefs: MinimalPreferences = { models: { execution: "broken-model" } };
|
|
271
|
+
|
|
272
|
+
const result = resolveExecutorContextWindow(registry, prefs, 128_000);
|
|
273
|
+
assert.equal(result, 128_000); // falls through to session
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("ignores sessionContextWindow ≤ 0", () => {
|
|
277
|
+
const registry = makeRegistry([]);
|
|
278
|
+
const prefs: MinimalPreferences = {};
|
|
279
|
+
|
|
280
|
+
const result = resolveExecutorContextWindow(registry, prefs, -1);
|
|
281
|
+
assert.equal(result, 200_000); // falls through to default
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -128,7 +128,7 @@ test("compression: buildCompleteMilestonePrompt minimal drops root GSD files", (
|
|
|
128
128
|
const block = promptsSrc.slice(completeMilestoneIdx, nextBuilder);
|
|
129
129
|
assert.ok(
|
|
130
130
|
block.includes('inlineLevel !== "minimal"') &&
|
|
131
|
-
block.includes('inlineGsdRootFile(base, "requirements.md"'),
|
|
131
|
+
(block.includes('inlineGsdRootFile(base, "requirements.md"') || block.includes('inlineRequirementsFromDb(base')),
|
|
132
132
|
"complete-milestone should gate root file inlining on level",
|
|
133
133
|
);
|
|
134
134
|
});
|