gsd-pi 2.76.0-dev.76f9a2dc5 → 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/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/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 +9 -3
- 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 +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/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/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/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/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 +13 -2
- 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 +38 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +152 -1
- 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/{UMCfv_sVnLXawpUAjvArc → pI48IF3dgfs0CBrYi2bh_}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{UMCfv_sVnLXawpUAjvArc → pI48IF3dgfs0CBrYi2bh_}/_ssgManifest.js +0 -0
|
@@ -180,7 +180,7 @@ function openRawDb(path: string): unknown {
|
|
|
180
180
|
return new Database(path);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
const SCHEMA_VERSION =
|
|
183
|
+
const SCHEMA_VERSION = 22;
|
|
184
184
|
|
|
185
185
|
function indexExists(db: DbAdapter, name: string): boolean {
|
|
186
186
|
return !!db.prepare(
|
|
@@ -564,7 +564,9 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
|
|
|
564
564
|
// indexes so old DBs can open far enough for the normal migration chain.
|
|
565
565
|
ensureBootstrapIndexColumns(db);
|
|
566
566
|
|
|
567
|
-
db
|
|
567
|
+
if (columnExists(db, "memories", "scope")) {
|
|
568
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope)");
|
|
569
|
+
}
|
|
568
570
|
db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_kind ON memory_sources(kind)");
|
|
569
571
|
db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_scope ON memory_sources(scope)");
|
|
570
572
|
db.exec("CREATE INDEX IF NOT EXISTS idx_memory_relations_from ON memory_relations(from_id)");
|
|
@@ -947,19 +949,24 @@ function migrateSchema(db: DbAdapter): void {
|
|
|
947
949
|
}
|
|
948
950
|
|
|
949
951
|
if (currentVersion < 12) {
|
|
952
|
+
// NOTE: The original DDL used COALESCE(task_id, '') in the PRIMARY KEY
|
|
953
|
+
// expression, which is invalid SQLite syntax and causes startup errors on
|
|
954
|
+
// DBs that migrate through v12. The corrected DDL uses
|
|
955
|
+
// task_id TEXT NOT NULL DEFAULT '' with a plain column list PK. DBs that
|
|
956
|
+
// were created with the broken DDL are repaired by the v22 migration below.
|
|
950
957
|
db.exec(`
|
|
951
958
|
CREATE TABLE IF NOT EXISTS quality_gates (
|
|
952
959
|
milestone_id TEXT NOT NULL,
|
|
953
960
|
slice_id TEXT NOT NULL,
|
|
954
961
|
gate_id TEXT NOT NULL,
|
|
955
962
|
scope TEXT NOT NULL DEFAULT 'slice',
|
|
956
|
-
task_id TEXT DEFAULT
|
|
963
|
+
task_id TEXT NOT NULL DEFAULT '',
|
|
957
964
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
958
965
|
verdict TEXT NOT NULL DEFAULT '',
|
|
959
966
|
rationale TEXT NOT NULL DEFAULT '',
|
|
960
967
|
findings TEXT NOT NULL DEFAULT '',
|
|
961
968
|
evaluated_at TEXT DEFAULT NULL,
|
|
962
|
-
PRIMARY KEY (milestone_id, slice_id, gate_id,
|
|
969
|
+
PRIMARY KEY (milestone_id, slice_id, gate_id, task_id),
|
|
963
970
|
FOREIGN KEY (milestone_id, slice_id) REFERENCES slices(milestone_id, id)
|
|
964
971
|
)
|
|
965
972
|
`);
|
|
@@ -1187,6 +1194,52 @@ function migrateSchema(db: DbAdapter): void {
|
|
|
1187
1194
|
});
|
|
1188
1195
|
}
|
|
1189
1196
|
|
|
1197
|
+
if (currentVersion < 22) {
|
|
1198
|
+
// v22: Repair quality_gates tables that were created by the broken v12
|
|
1199
|
+
// migration (which used COALESCE(task_id, '') as a PK expression — invalid
|
|
1200
|
+
// SQLite DDL). Those DBs have task_id nullable (dflt_value NULL, notnull 0).
|
|
1201
|
+
// Rebuild the table with the correct schema, migrating existing rows via
|
|
1202
|
+
// COALESCE so no data is lost.
|
|
1203
|
+
const qgInfo = db.prepare("PRAGMA table_info(quality_gates)").all() as Array<Record<string, unknown>>;
|
|
1204
|
+
const taskIdCol = qgInfo.find((r) => r["name"] === "task_id");
|
|
1205
|
+
const needsRepair = taskIdCol && (taskIdCol["notnull"] === 0 || taskIdCol["notnull"] === "0");
|
|
1206
|
+
if (needsRepair) {
|
|
1207
|
+
db.exec(`
|
|
1208
|
+
CREATE TABLE quality_gates_new (
|
|
1209
|
+
milestone_id TEXT NOT NULL,
|
|
1210
|
+
slice_id TEXT NOT NULL,
|
|
1211
|
+
gate_id TEXT NOT NULL,
|
|
1212
|
+
scope TEXT NOT NULL DEFAULT 'slice',
|
|
1213
|
+
task_id TEXT NOT NULL DEFAULT '',
|
|
1214
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
1215
|
+
verdict TEXT NOT NULL DEFAULT '',
|
|
1216
|
+
rationale TEXT NOT NULL DEFAULT '',
|
|
1217
|
+
findings TEXT NOT NULL DEFAULT '',
|
|
1218
|
+
evaluated_at TEXT DEFAULT NULL,
|
|
1219
|
+
PRIMARY KEY (milestone_id, slice_id, gate_id, task_id),
|
|
1220
|
+
FOREIGN KEY (milestone_id, slice_id) REFERENCES slices(milestone_id, id)
|
|
1221
|
+
)
|
|
1222
|
+
`);
|
|
1223
|
+
db.exec(`
|
|
1224
|
+
INSERT OR IGNORE INTO quality_gates_new
|
|
1225
|
+
(milestone_id, slice_id, gate_id, scope, task_id, status, verdict, rationale, findings, evaluated_at)
|
|
1226
|
+
SELECT milestone_id, slice_id, gate_id, scope, COALESCE(task_id, ''), status, verdict, rationale, findings, evaluated_at
|
|
1227
|
+
FROM quality_gates
|
|
1228
|
+
`);
|
|
1229
|
+
db.exec("DROP TABLE quality_gates");
|
|
1230
|
+
db.exec("ALTER TABLE quality_gates_new RENAME TO quality_gates");
|
|
1231
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_quality_gates_pending ON quality_gates(milestone_id, slice_id, status)");
|
|
1232
|
+
}
|
|
1233
|
+
// Ensure scope column exists on quality_gates and assessments (guard
|
|
1234
|
+
// against DBs that somehow lack it after a partial migration).
|
|
1235
|
+
ensureColumn(db, "quality_gates", "scope", "ALTER TABLE quality_gates ADD COLUMN scope TEXT NOT NULL DEFAULT 'slice'");
|
|
1236
|
+
ensureColumn(db, "assessments", "scope", "ALTER TABLE assessments ADD COLUMN scope TEXT NOT NULL DEFAULT ''");
|
|
1237
|
+
db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
|
|
1238
|
+
":version": 22,
|
|
1239
|
+
":applied_at": new Date().toISOString(),
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1190
1243
|
db.exec("COMMIT");
|
|
1191
1244
|
} catch (err) {
|
|
1192
1245
|
db.exec("ROLLBACK");
|
|
@@ -1199,6 +1252,8 @@ let currentPath: string | null = null;
|
|
|
1199
1252
|
let currentPid: number = 0;
|
|
1200
1253
|
let _exitHandlerRegistered = false;
|
|
1201
1254
|
let _dbOpenAttempted = false;
|
|
1255
|
+
let _lastDbError: Error | null = null;
|
|
1256
|
+
let _lastDbPhase: "open" | "initSchema" | "vacuum-recovery" | null = null;
|
|
1202
1257
|
|
|
1203
1258
|
export function getDbProvider(): ProviderName | null {
|
|
1204
1259
|
loadProvider();
|
|
@@ -1219,12 +1274,58 @@ export function wasDbOpenAttempted(): boolean {
|
|
|
1219
1274
|
return _dbOpenAttempted;
|
|
1220
1275
|
}
|
|
1221
1276
|
|
|
1277
|
+
export function getDbStatus(): {
|
|
1278
|
+
available: boolean;
|
|
1279
|
+
provider: ProviderName | null;
|
|
1280
|
+
attempted: boolean;
|
|
1281
|
+
lastError: Error | null;
|
|
1282
|
+
lastPhase: "open" | "initSchema" | "vacuum-recovery" | null;
|
|
1283
|
+
} {
|
|
1284
|
+
loadProvider();
|
|
1285
|
+
return {
|
|
1286
|
+
available: currentDb !== null,
|
|
1287
|
+
provider: providerName,
|
|
1288
|
+
attempted: _dbOpenAttempted,
|
|
1289
|
+
lastError: _lastDbError,
|
|
1290
|
+
lastPhase: _lastDbPhase,
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1222
1294
|
export function openDatabase(path: string): boolean {
|
|
1223
1295
|
_dbOpenAttempted = true;
|
|
1224
1296
|
if (currentDb && currentPath !== path) closeDatabase();
|
|
1225
1297
|
if (currentDb && currentPath === path) return true;
|
|
1226
1298
|
|
|
1227
|
-
|
|
1299
|
+
// Reset error state only when a new open attempt is actually going to run.
|
|
1300
|
+
_lastDbError = null;
|
|
1301
|
+
_lastDbPhase = null;
|
|
1302
|
+
|
|
1303
|
+
let rawDb: unknown;
|
|
1304
|
+
let fallbackProvider: ProviderName | null = null;
|
|
1305
|
+
let fallbackModule: unknown = null;
|
|
1306
|
+
try {
|
|
1307
|
+
rawDb = openRawDb(path);
|
|
1308
|
+
} catch (primaryErr) {
|
|
1309
|
+
_lastDbPhase = "open";
|
|
1310
|
+
_lastDbError = primaryErr instanceof Error ? primaryErr : new Error(String(primaryErr));
|
|
1311
|
+
// node:sqlite loaded but failed to open this file — try better-sqlite3 as fallback.
|
|
1312
|
+
if (providerName === "node:sqlite") {
|
|
1313
|
+
try {
|
|
1314
|
+
const mod = _require("better-sqlite3");
|
|
1315
|
+
const Db = (mod && mod.default) ? mod.default : mod;
|
|
1316
|
+
if (typeof Db === "function") {
|
|
1317
|
+
rawDb = new Db(path);
|
|
1318
|
+
fallbackProvider = "better-sqlite3";
|
|
1319
|
+
fallbackModule = Db;
|
|
1320
|
+
_lastDbError = null;
|
|
1321
|
+
_lastDbPhase = null;
|
|
1322
|
+
}
|
|
1323
|
+
} catch {
|
|
1324
|
+
// fallback unavailable; surface original error
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (!rawDb) throw primaryErr;
|
|
1328
|
+
}
|
|
1228
1329
|
if (!rawDb) return false;
|
|
1229
1330
|
|
|
1230
1331
|
const adapter = createAdapter(rawDb);
|
|
@@ -1240,15 +1341,25 @@ export function openDatabase(path: string): boolean {
|
|
|
1240
1341
|
initSchema(adapter, fileBacked);
|
|
1241
1342
|
process.stderr.write("gsd-db: recovered corrupt database via VACUUM\n");
|
|
1242
1343
|
} catch (retryErr) {
|
|
1344
|
+
_lastDbPhase = "vacuum-recovery";
|
|
1345
|
+
_lastDbError = retryErr instanceof Error ? retryErr : new Error(String(retryErr));
|
|
1243
1346
|
try { adapter.close(); } catch (e) { logWarning("db", `close after VACUUM failed: ${(e as Error).message}`); }
|
|
1244
1347
|
throw retryErr;
|
|
1245
1348
|
}
|
|
1246
1349
|
} else {
|
|
1247
|
-
|
|
1350
|
+
_lastDbPhase = "initSchema";
|
|
1351
|
+
_lastDbError = err instanceof Error ? err : new Error(String(err));
|
|
1352
|
+
try { adapter.close(); } catch (e) { logWarning("db", `close after initSchema failed: ${(e as Error).message}`); }
|
|
1248
1353
|
throw err;
|
|
1249
1354
|
}
|
|
1250
1355
|
}
|
|
1251
1356
|
|
|
1357
|
+
// Commit fallback provider switch only after open + schema both succeeded.
|
|
1358
|
+
if (fallbackProvider) {
|
|
1359
|
+
providerName = fallbackProvider;
|
|
1360
|
+
providerModule = fallbackModule;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1252
1363
|
currentDb = adapter;
|
|
1253
1364
|
currentPath = path;
|
|
1254
1365
|
currentPid = process.pid;
|
|
@@ -1276,8 +1387,12 @@ export function closeDatabase(): void {
|
|
|
1276
1387
|
currentDb = null;
|
|
1277
1388
|
currentPath = null;
|
|
1278
1389
|
currentPid = 0;
|
|
1279
|
-
_dbOpenAttempted = false;
|
|
1280
1390
|
}
|
|
1391
|
+
// Reset session-scoped state unconditionally so stale error info from a
|
|
1392
|
+
// failed open doesn't persist into the next open attempt or status check.
|
|
1393
|
+
_dbOpenAttempted = false;
|
|
1394
|
+
_lastDbError = null;
|
|
1395
|
+
_lastDbPhase = null;
|
|
1281
1396
|
}
|
|
1282
1397
|
|
|
1283
1398
|
/** Run a full VACUUM — call sparingly (e.g. after milestone completion). */
|
|
@@ -130,8 +130,20 @@ interface PendingAutoStartEntry {
|
|
|
130
130
|
milestoneId: string; // the milestone being discussed
|
|
131
131
|
step?: boolean; // preserve step mode through discuss → auto transition
|
|
132
132
|
createdAt: number; // timestamp for staleness detection (#3274)
|
|
133
|
+
// #4573: counter for how many times the LLM emitted the ready phrase
|
|
134
|
+
// without writing the required artifacts. Cleared on entry delete/recreate.
|
|
135
|
+
readyRejectCount?: number;
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
// #4573: cap for how many times we nudge the LLM after a premature ready
|
|
139
|
+
// phrase before giving up and asking the user to re-run /gsd.
|
|
140
|
+
const MAX_READY_REJECTS = 2;
|
|
141
|
+
|
|
142
|
+
// #4573: matches the canonical ready phrase the discuss prompt asks the LLM
|
|
143
|
+
// to emit. Accepts any M-prefixed milestone ID (three digits + optional
|
|
144
|
+
// suffix) with optional trailing punctuation.
|
|
145
|
+
const READY_PHRASE_RE = /\bMilestone\s+M\d{3}[A-Z0-9-]*\s+ready\.?/i;
|
|
146
|
+
|
|
135
147
|
const pendingAutoStartMap = new Map<string, PendingAutoStartEntry>();
|
|
136
148
|
|
|
137
149
|
/**
|
|
@@ -279,6 +291,215 @@ export function checkAutoStartAfterDiscuss(): boolean {
|
|
|
279
291
|
return true;
|
|
280
292
|
}
|
|
281
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Extract the concatenated text content from an assistant message, whether it
|
|
296
|
+
* stores content as a string or as an array of text blocks.
|
|
297
|
+
*/
|
|
298
|
+
function extractAssistantText(msg: any): string {
|
|
299
|
+
if (!msg) return "";
|
|
300
|
+
const content = msg.content;
|
|
301
|
+
if (typeof content === "string") return content;
|
|
302
|
+
if (!Array.isArray(content)) return "";
|
|
303
|
+
const parts: string[] = [];
|
|
304
|
+
for (const block of content) {
|
|
305
|
+
if (!block || typeof block !== "object") continue;
|
|
306
|
+
if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
307
|
+
}
|
|
308
|
+
return parts.join("\n");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Return true if the assistant message contains any tool-use block.
|
|
313
|
+
*/
|
|
314
|
+
function hasToolUse(msg: any): boolean {
|
|
315
|
+
if (!msg) return false;
|
|
316
|
+
const content = msg.content;
|
|
317
|
+
if (!Array.isArray(content)) return false;
|
|
318
|
+
return content.some((b: any) => b && typeof b === "object" && (b.type === "tool_use" || b.type === "tool-use"));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* #4573 — Detect and recover from the "ready phrase without files" failure mode.
|
|
323
|
+
*
|
|
324
|
+
* When the LLM emits "Milestone {{id}} ready." but has not written CONTEXT.md
|
|
325
|
+
* or ROADMAP.md, `checkAutoStartAfterDiscuss()` silently returns false and the
|
|
326
|
+
* next /gsd invocation loops into the "All milestones complete" warning.
|
|
327
|
+
*
|
|
328
|
+
* This function, called from `handleAgentEnd` after `checkAutoStartAfterDiscuss`
|
|
329
|
+
* returns false, pattern-matches the ready phrase on the last assistant message.
|
|
330
|
+
* If it fired AND neither CONTEXT.md nor ROADMAP.md exists, it:
|
|
331
|
+
* 1. Notifies the user that the signal was rejected.
|
|
332
|
+
* 2. Injects a system message via `pi.sendMessage(..., {triggerTurn:true})`
|
|
333
|
+
* telling the LLM the signal was premature and to emit the writes now.
|
|
334
|
+
* 3. Caps at `MAX_READY_REJECTS` per-entry; beyond that, gives up and asks
|
|
335
|
+
* the user to re-run /gsd.
|
|
336
|
+
*
|
|
337
|
+
* Returns true when a nudge (or give-up) was emitted, signaling the caller to
|
|
338
|
+
* skip `resolveAgentEnd`.
|
|
339
|
+
*/
|
|
340
|
+
export function maybeHandleReadyPhraseWithoutFiles(event: { messages: any[] }): boolean {
|
|
341
|
+
const entry = _getPendingAutoStart();
|
|
342
|
+
if (!entry) return false;
|
|
343
|
+
const { ctx, pi, basePath, milestoneId } = entry;
|
|
344
|
+
|
|
345
|
+
// Gate: last assistant message must contain the ready phrase
|
|
346
|
+
const lastMsg = event.messages[event.messages.length - 1];
|
|
347
|
+
const text = extractAssistantText(lastMsg);
|
|
348
|
+
if (!READY_PHRASE_RE.test(text)) return false;
|
|
349
|
+
|
|
350
|
+
// Gate: artifacts must still be missing — if they exist, the happy path
|
|
351
|
+
// already fired and we have nothing to do.
|
|
352
|
+
const contextFile = resolveMilestoneFile(basePath, milestoneId, "CONTEXT");
|
|
353
|
+
const roadmapFile = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
354
|
+
if (contextFile || roadmapFile) return false;
|
|
355
|
+
|
|
356
|
+
entry.readyRejectCount = (entry.readyRejectCount ?? 0) + 1;
|
|
357
|
+
|
|
358
|
+
if (entry.readyRejectCount > MAX_READY_REJECTS) {
|
|
359
|
+
// Give up: clear state and tell the user to re-run /gsd. Avoids an
|
|
360
|
+
// infinite nudge loop when the LLM never produces the writes.
|
|
361
|
+
pendingAutoStartMap.delete(basePath);
|
|
362
|
+
ctx.ui.notify(
|
|
363
|
+
`Milestone ${milestoneId}: LLM signaled "ready" ${entry.readyRejectCount} times without writing files. ` +
|
|
364
|
+
`Stopping auto-nudge. Run /gsd to try again.`,
|
|
365
|
+
"error",
|
|
366
|
+
);
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
ctx.ui.notify(
|
|
371
|
+
`Milestone ${milestoneId}: "ready" signal rejected — CONTEXT.md and ROADMAP.md are missing. Asking the LLM to complete the writes.`,
|
|
372
|
+
"warning",
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const nudge =
|
|
376
|
+
`You emitted "Milestone ${milestoneId} ready." but neither ` +
|
|
377
|
+
`.gsd/milestones/${milestoneId}/${milestoneId}-CONTEXT.md nor ` +
|
|
378
|
+
`.gsd/milestones/${milestoneId}/${milestoneId}-ROADMAP.md exists on disk. ` +
|
|
379
|
+
`The ready phrase is a POST-WRITE signal and has been rejected. ` +
|
|
380
|
+
`In this turn: (1) write PROJECT.md, REQUIREMENTS.md, and the milestone ` +
|
|
381
|
+
`CONTEXT.md, (2) call gsd_plan_milestone, then (3) emit the ready phrase. ` +
|
|
382
|
+
`Do not describe these steps — execute them as tool calls. ` +
|
|
383
|
+
`This is retry ${entry.readyRejectCount}/${MAX_READY_REJECTS}; further ` +
|
|
384
|
+
`premature signals will clear the session.`;
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
pi.sendMessage(
|
|
388
|
+
{ customType: "gsd-ready-no-files", content: nudge, display: false },
|
|
389
|
+
{ triggerTurn: true },
|
|
390
|
+
);
|
|
391
|
+
} catch (e) {
|
|
392
|
+
logWarning("guided", `ready-phrase nudge sendMessage failed: ${(e as Error).message}`);
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* #4573 — Detect and recover from the "announces tool, never calls it" stall.
|
|
400
|
+
*
|
|
401
|
+
* The LLM emits text like "I'll now write the CONTEXT.md file" but the turn
|
|
402
|
+
* ends with zero tool-use blocks. The harness has no post-turn tool-call
|
|
403
|
+
* validation, so the unit promise resolves and the user sees a stalled state.
|
|
404
|
+
*
|
|
405
|
+
* This function, called from `handleAgentEnd`, inspects the last assistant
|
|
406
|
+
* message. If ALL of the following are true, it injects a recovery message:
|
|
407
|
+
* - Text-only (no tool-use blocks)
|
|
408
|
+
* - Contains a commit-intent phrase ("I'll write", "I'll call", etc.)
|
|
409
|
+
* - Auto-mode is active OR a discussion autostart is pending
|
|
410
|
+
* - `emptyTurnRetryCount` is under the cap
|
|
411
|
+
*
|
|
412
|
+
* Per-handler state is held on the `PendingAutoStartEntry` when present, and
|
|
413
|
+
* on a module-level map otherwise. The counter resets on any successful
|
|
414
|
+
* tool-use turn via `resetEmptyTurnCounter`.
|
|
415
|
+
*/
|
|
416
|
+
const emptyTurnCounterByBase = new Map<string, number>();
|
|
417
|
+
const MAX_EMPTY_TURN_RETRIES = 2;
|
|
418
|
+
|
|
419
|
+
// Phrases that indicate the LLM is about to do something but has not yet.
|
|
420
|
+
// Kept tight to avoid flagging legitimate narration like "I'll wait for your answer."
|
|
421
|
+
const COMMIT_INTENT_RE =
|
|
422
|
+
/\b(?:I['’]ll|I will|Next,? I['’]ll|Now I['’]ll|Let me|I['’]m going to|I am going to)\s+(?:now\s+)?(?:write|create|call|invoke|update|add|make|run|execute|generate|produce|emit|compose|implement|save|apply|commit)\b/i;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Reset the empty-turn counter for a basePath after a successful tool-use turn.
|
|
426
|
+
* Called from handleAgentEnd when the last message contains tool_use blocks.
|
|
427
|
+
*/
|
|
428
|
+
export function resetEmptyTurnCounter(basePath?: string): void {
|
|
429
|
+
if (basePath) emptyTurnCounterByBase.delete(basePath);
|
|
430
|
+
else emptyTurnCounterByBase.clear();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function maybeHandleEmptyIntentTurn(
|
|
434
|
+
event: { messages: any[] },
|
|
435
|
+
isAuto: boolean,
|
|
436
|
+
): boolean {
|
|
437
|
+
// Gate: only fire when there is system-driven work in flight. Interactive
|
|
438
|
+
// /gsd discuss (user-driven) produces legitimate text-only turns.
|
|
439
|
+
if (!isAuto && pendingAutoStartMap.size === 0) return false;
|
|
440
|
+
|
|
441
|
+
const lastMsg = event.messages[event.messages.length - 1];
|
|
442
|
+
if (!lastMsg) return false;
|
|
443
|
+
if (hasToolUse(lastMsg)) return false;
|
|
444
|
+
|
|
445
|
+
const text = extractAssistantText(lastMsg).trim();
|
|
446
|
+
if (!text) return false;
|
|
447
|
+
|
|
448
|
+
// Skip if the LLM is emitting the ready phrase — that is the ready-no-files
|
|
449
|
+
// path, handled by maybeHandleReadyPhraseWithoutFiles.
|
|
450
|
+
if (READY_PHRASE_RE.test(text)) return false;
|
|
451
|
+
|
|
452
|
+
// Skip if the LLM is clearly handing back to the user. Keep the heuristic
|
|
453
|
+
// tight: a trailing question mark on the last non-empty line is the common
|
|
454
|
+
// signal for "I asked the user a question and stopped."
|
|
455
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
456
|
+
const lastLine = lines[lines.length - 1] ?? "";
|
|
457
|
+
if (lastLine.endsWith("?")) return false;
|
|
458
|
+
|
|
459
|
+
// Must contain a commit-intent phrase — this is the stall we care about.
|
|
460
|
+
if (!COMMIT_INTENT_RE.test(text)) return false;
|
|
461
|
+
|
|
462
|
+
// Resolve the target basePath + pi for injection. Prefer the pending
|
|
463
|
+
// autostart entry (discuss flow); otherwise we cannot inject.
|
|
464
|
+
const entry = _getPendingAutoStart();
|
|
465
|
+
if (!entry) return false;
|
|
466
|
+
const { ctx, pi, basePath } = entry;
|
|
467
|
+
|
|
468
|
+
const count = (emptyTurnCounterByBase.get(basePath) ?? 0) + 1;
|
|
469
|
+
emptyTurnCounterByBase.set(basePath, count);
|
|
470
|
+
|
|
471
|
+
if (count > MAX_EMPTY_TURN_RETRIES) {
|
|
472
|
+
ctx.ui.notify(
|
|
473
|
+
`Empty-turn recovery: LLM announced intent ${count} times without calling any tool. ` +
|
|
474
|
+
`Stopping auto-nudge.`,
|
|
475
|
+
"error",
|
|
476
|
+
);
|
|
477
|
+
return false; // let the normal flow resolve/pause the unit
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
ctx.ui.notify(
|
|
481
|
+
`Empty-turn detected: LLM announced intent but called no tool. Prompting it to execute.`,
|
|
482
|
+
"info",
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
const nudge =
|
|
486
|
+
`Your last turn announced an action (e.g. "I'll write…" or "Let me call…") ` +
|
|
487
|
+
`but contained no tool call. The system records zero tool-use blocks for ` +
|
|
488
|
+
`that turn. Execute the announced action NOW as a tool call in this turn. ` +
|
|
489
|
+
`Do not describe it again. Retry ${count}/${MAX_EMPTY_TURN_RETRIES}.`;
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
pi.sendMessage(
|
|
493
|
+
{ customType: "gsd-empty-turn-recovery", content: nudge, display: false },
|
|
494
|
+
{ triggerTurn: true },
|
|
495
|
+
);
|
|
496
|
+
} catch (e) {
|
|
497
|
+
logWarning("guided", `empty-turn nudge sendMessage failed: ${(e as Error).message}`);
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
|
|
282
503
|
/**
|
|
283
504
|
* Extract milestone IDs from PROJECT.md milestone sequence table.
|
|
284
505
|
* Looks for rows like "| M001 | Name | Status |" and extracts the ID column.
|
|
@@ -108,6 +108,7 @@ export function initHealthWidget(ctx: ExtensionContext): void {
|
|
|
108
108
|
let data = initialData;
|
|
109
109
|
let cachedLines: string[] | undefined;
|
|
110
110
|
let refreshInFlight = false;
|
|
111
|
+
let isDisposed = false;
|
|
111
112
|
|
|
112
113
|
const refresh = async () => {
|
|
113
114
|
if (refreshInFlight) return;
|
|
@@ -115,7 +116,7 @@ export function initHealthWidget(ctx: ExtensionContext): void {
|
|
|
115
116
|
try {
|
|
116
117
|
data = loadHealthWidgetData(basePath);
|
|
117
118
|
cachedLines = undefined;
|
|
118
|
-
_tui.requestRender();
|
|
119
|
+
if (!isDisposed) _tui.requestRender();
|
|
119
120
|
} catch { /* non-fatal */ } finally {
|
|
120
121
|
refreshInFlight = false;
|
|
121
122
|
}
|
|
@@ -140,6 +141,7 @@ export function initHealthWidget(ctx: ExtensionContext): void {
|
|
|
140
141
|
},
|
|
141
142
|
invalidate(): void { cachedLines = undefined; cachedWidth = undefined; },
|
|
142
143
|
dispose(): void {
|
|
144
|
+
isDisposed = true;
|
|
143
145
|
clearInterval(refreshTimer);
|
|
144
146
|
},
|
|
145
147
|
};
|
|
@@ -39,7 +39,8 @@ export type JournalEventType =
|
|
|
39
39
|
| "worktree-create-failed"
|
|
40
40
|
| "worktree-skip"
|
|
41
41
|
| "worktree-merge-start"
|
|
42
|
-
| "worktree-merge-failed"
|
|
42
|
+
| "worktree-merge-failed"
|
|
43
|
+
| "artifact-verification-retry";
|
|
43
44
|
|
|
44
45
|
/** A single structured event in the journal. */
|
|
45
46
|
export interface JournalEntry {
|
|
@@ -35,6 +35,12 @@ export interface ProviderInfo {
|
|
|
35
35
|
export const PROVIDER_REGISTRY: ProviderInfo[] = [
|
|
36
36
|
// LLM Providers
|
|
37
37
|
{ id: "anthropic", label: "Anthropic (Claude)", category: "llm", envVar: "ANTHROPIC_API_KEY", prefixes: ["sk-ant-"], hasOAuth: true, dashboardUrl: "console.anthropic.com" },
|
|
38
|
+
// Claude Code CLI: routes through the local `claude` binary — no API key,
|
|
39
|
+
// authentication is handled by the CLI's own OAuth flow.
|
|
40
|
+
// Referenced by doctor-providers.ts, auto-model-selection.ts, and others;
|
|
41
|
+
// must be in the canonical registry so all consumers see the same catalog.
|
|
42
|
+
// See: https://github.com/gsd-build/gsd-2/issues/4541
|
|
43
|
+
{ id: "claude-code", label: "Claude Code CLI", category: "llm", hasOAuth: true },
|
|
38
44
|
{ id: "openai", label: "OpenAI", category: "llm", envVar: "OPENAI_API_KEY", prefixes: ["sk-"], dashboardUrl: "platform.openai.com/api-keys" },
|
|
39
45
|
{ id: "github-copilot", label: "GitHub Copilot", category: "llm", envVar: "GITHUB_TOKEN", hasOAuth: true },
|
|
40
46
|
{ id: "openai-codex", label: "ChatGPT Plus/Pro (Codex)",category: "llm", hasOAuth: true },
|
|
@@ -561,6 +561,23 @@ function bareModelId(modelId: string): string {
|
|
|
561
561
|
return modelId.includes("/") ? modelId.split("/").pop()! : modelId;
|
|
562
562
|
}
|
|
563
563
|
|
|
564
|
+
// ─── Provider-specific Tool Limits ─────────────────────────────────────────
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Groq enforces a hard limit of 128 tools per request.
|
|
568
|
+
* Requests exceeding this limit receive a 400 error:
|
|
569
|
+
* "maximum number of items is 128"
|
|
570
|
+
* @see https://console.groq.com/docs/tool-use
|
|
571
|
+
*/
|
|
572
|
+
export const GROQ_MAX_TOOLS = 128;
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Provider IDs that map to the Groq API backend.
|
|
576
|
+
* Used to detect Groq at the GSD routing layer where only the provider string
|
|
577
|
+
* is available (the pi-ai openai-completions adapter is shared across providers).
|
|
578
|
+
*/
|
|
579
|
+
const GROQ_PROVIDER_IDS = new Set(["groq"]);
|
|
580
|
+
|
|
564
581
|
// ─── Tool Compatibility Filter (ADR-005 Phase 3) ───────────────────────────
|
|
565
582
|
|
|
566
583
|
/**
|
|
@@ -588,10 +605,17 @@ export function isToolCompatibleWithProvider(
|
|
|
588
605
|
/**
|
|
589
606
|
* Filter a list of tool names to only those compatible with a provider.
|
|
590
607
|
* Used by the routing pipeline to adjust tool sets when switching providers.
|
|
608
|
+
*
|
|
609
|
+
* @param toolNames - The full list of active tool names to filter.
|
|
610
|
+
* @param providerApi - The pi-ai API string (e.g. "openai-completions").
|
|
611
|
+
* @param provider - Optional provider ID (e.g. "groq"). Used to apply
|
|
612
|
+
* provider-specific limits that can't be expressed as API-level capabilities
|
|
613
|
+
* (e.g. Groq's 128-tool hard limit on the shared openai-completions adapter).
|
|
591
614
|
*/
|
|
592
615
|
export function filterToolsForProvider(
|
|
593
616
|
toolNames: string[],
|
|
594
617
|
providerApi: string,
|
|
618
|
+
provider?: string,
|
|
595
619
|
): { compatible: string[]; filtered: string[] } {
|
|
596
620
|
const providerCaps = getProviderCapabilities(providerApi);
|
|
597
621
|
|
|
@@ -611,6 +635,17 @@ export function filterToolsForProvider(
|
|
|
611
635
|
}
|
|
612
636
|
}
|
|
613
637
|
|
|
638
|
+
// Groq enforces a hard limit of 128 tools per request (#4376).
|
|
639
|
+
// Trim the compatible list to GROQ_MAX_TOOLS and move the excess to filtered.
|
|
640
|
+
if (provider && GROQ_PROVIDER_IDS.has(provider) && compatible.length > GROQ_MAX_TOOLS) {
|
|
641
|
+
const trimmed = compatible.splice(GROQ_MAX_TOOLS);
|
|
642
|
+
filtered.push(...trimmed);
|
|
643
|
+
console.warn(
|
|
644
|
+
`[gsd] Groq tool limit: ${compatible.length + trimmed.length} tools active but Groq allows at most ${GROQ_MAX_TOOLS}. ` +
|
|
645
|
+
`Trimming to the first ${GROQ_MAX_TOOLS} tools. Removed: ${trimmed.join(", ")}`,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
614
649
|
return { compatible, filtered };
|
|
615
650
|
}
|
|
616
651
|
|
|
@@ -620,11 +655,17 @@ export function filterToolsForProvider(
|
|
|
620
655
|
*
|
|
621
656
|
* This is a hard filter only — it removes tools that would fail at the
|
|
622
657
|
* provider level. It does NOT remove tools based on soft heuristics.
|
|
658
|
+
*
|
|
659
|
+
* @param activeToolNames - The full list of currently active tool names.
|
|
660
|
+
* @param selectedModelApi - The pi-ai API string for the selected model.
|
|
661
|
+
* @param provider - Optional provider ID (e.g. "groq") for provider-specific
|
|
662
|
+
* limits beyond what the API-level capability profile expresses.
|
|
623
663
|
*/
|
|
624
664
|
export function adjustToolSet(
|
|
625
665
|
activeToolNames: string[],
|
|
626
666
|
selectedModelApi: string,
|
|
667
|
+
provider?: string,
|
|
627
668
|
): { toolNames: string[]; removedTools: string[] } {
|
|
628
|
-
const { compatible, filtered } = filterToolsForProvider(activeToolNames, selectedModelApi);
|
|
669
|
+
const { compatible, filtered } = filterToolsForProvider(activeToolNames, selectedModelApi, provider);
|
|
629
670
|
return { toolNames: compatible, removedTools: filtered };
|
|
630
671
|
}
|
|
@@ -91,8 +91,13 @@ export function extractPackageReferences(description: string): string[] {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
// require('pkg') or import from 'pkg' in code blocks
|
|
95
|
-
|
|
94
|
+
// require('pkg') or `import ... from 'pkg'` in code blocks.
|
|
95
|
+
// The `from\s+['"]` branch MUST be preceded by an `import` keyword so that
|
|
96
|
+
// natural-language prose like `from "What's Next"` or `from 'master'` does
|
|
97
|
+
// not produce false package-existence failures. Requiring the leading import
|
|
98
|
+
// keyword anchors the match to JavaScript/TypeScript syntax.
|
|
99
|
+
// See: https://github.com/gsd-build/gsd-2/issues/4388
|
|
100
|
+
const importPattern = /(?:require\s*\(\s*['"]|import\b[\s\S]*?\bfrom\s+['"])([a-zA-Z0-9@/_-]+)['"\)]/g;
|
|
96
101
|
let importMatch: RegExpExecArray | null;
|
|
97
102
|
while ((importMatch = importPattern.exec(description)) !== null) {
|
|
98
103
|
// Skip relative imports and node builtins
|
|
@@ -325,7 +330,12 @@ function extractPathFromAnnotation(raw: string): string {
|
|
|
325
330
|
|
|
326
331
|
const annotatedMatch = trimmed.match(/^(.+?)\s+[—–-]\s+.+$/);
|
|
327
332
|
if (annotatedMatch) {
|
|
328
|
-
|
|
333
|
+
const prefix = annotatedMatch[1].trim();
|
|
334
|
+
const prefixBacktickMatch = prefix.match(/`([^`]+)`/);
|
|
335
|
+
if (prefixBacktickMatch && looksLikePathOrUrl(prefixBacktickMatch[1].trim())) {
|
|
336
|
+
return prefixBacktickMatch[1].trim();
|
|
337
|
+
}
|
|
338
|
+
return prefix.replace(/`/g, "").trim();
|
|
329
339
|
}
|
|
330
340
|
|
|
331
341
|
// Fallback: scan all backticked tokens and return the first one that looks
|
|
@@ -388,13 +398,19 @@ function containsGlobPattern(candidate: string): boolean {
|
|
|
388
398
|
|
|
389
399
|
/**
|
|
390
400
|
* Build a set of files that will be created by tasks up to (but not including) taskIndex.
|
|
401
|
+
* Also includes outputs of completed tasks at any position — a completed task has already
|
|
402
|
+
* run and its outputs are available regardless of sequence position or disk state (#4071).
|
|
391
403
|
* All paths are normalized for consistent comparison.
|
|
392
404
|
*/
|
|
393
405
|
function getExpectedOutputsUpTo(tasks: TaskRow[], taskIndex: number): Set<string> {
|
|
394
406
|
const outputs = new Set<string>();
|
|
395
|
-
for (let i = 0; i <
|
|
396
|
-
|
|
397
|
-
|
|
407
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
408
|
+
const task = tasks[i];
|
|
409
|
+
// Include prior tasks (i < taskIndex) OR completed tasks at any position
|
|
410
|
+
if (i < taskIndex || task.status === "completed") {
|
|
411
|
+
for (const file of task.expected_output) {
|
|
412
|
+
outputs.add(normalizeFilePath(file));
|
|
413
|
+
}
|
|
398
414
|
}
|
|
399
415
|
}
|
|
400
416
|
return outputs;
|
|
@@ -481,13 +497,19 @@ export function checkTaskOrdering(
|
|
|
481
497
|
const results: PreExecutionCheckJSON[] = [];
|
|
482
498
|
|
|
483
499
|
// Build map: normalized file → task index that creates it
|
|
484
|
-
const fileCreators = new Map<string, { taskId: string; index: number; originalPath: string }>();
|
|
500
|
+
const fileCreators = new Map<string, { taskId: string; index: number; originalPath: string; completed: boolean }>();
|
|
485
501
|
for (let i = 0; i < tasks.length; i++) {
|
|
486
502
|
const task = tasks[i];
|
|
487
503
|
for (const file of task.expected_output) {
|
|
488
504
|
const normalizedFile = normalizeFilePath(file);
|
|
489
|
-
|
|
490
|
-
|
|
505
|
+
const existing = fileCreators.get(normalizedFile);
|
|
506
|
+
if (!existing || (!existing.completed && task.status === "completed")) {
|
|
507
|
+
fileCreators.set(normalizedFile, {
|
|
508
|
+
taskId: task.id,
|
|
509
|
+
index: i,
|
|
510
|
+
originalPath: file,
|
|
511
|
+
completed: task.status === "completed",
|
|
512
|
+
});
|
|
491
513
|
}
|
|
492
514
|
}
|
|
493
515
|
}
|
|
@@ -511,7 +533,11 @@ export function checkTaskOrdering(
|
|
|
511
533
|
const creator = fileCreators.get(normalizedFile);
|
|
512
534
|
const absolutePath = resolve(basePath, normalizedFile);
|
|
513
535
|
const existsOnDisk = existsSync(absolutePath);
|
|
514
|
-
if
|
|
536
|
+
// Skip if the creating task has already completed — its output is available
|
|
537
|
+
// regardless of disk state (e.g. file was a temp artifact cleaned up after
|
|
538
|
+
// the task ran, or a replan introduced a new earlier-sequence task that
|
|
539
|
+
// reads this pre-execution output). (#4071)
|
|
540
|
+
if (creator && creator.index > i && !existsOnDisk && !creator.completed) {
|
|
515
541
|
// Task reads file that is created later — impossible ordering
|
|
516
542
|
results.push({
|
|
517
543
|
category: "file",
|