gsd-pi 2.72.0-dev.3159350 → 2.72.0-dev.4f3264a
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/resources/extensions/async-jobs/await-tool.js +4 -7
- package/dist/resources/extensions/async-jobs/job-manager.js +3 -28
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +26 -27
- package/dist/resources/extensions/gsd/auto/loop.js +1 -84
- package/dist/resources/extensions/gsd/auto-observability.js +54 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +0 -6
- package/dist/resources/extensions/gsd/auto.js +19 -25
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +11 -9
- package/dist/resources/extensions/gsd/commands-handlers.js +1 -4
- package/dist/resources/extensions/gsd/context-injector.js +1 -1
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +7 -3
- package/dist/resources/extensions/gsd/file-watcher.js +80 -0
- package/dist/resources/extensions/gsd/gsd-db.js +5 -47
- package/dist/resources/extensions/gsd/key-manager.js +0 -2
- package/dist/resources/extensions/gsd/preferences-skills.js +34 -2
- package/dist/resources/extensions/gsd/preferences-types.js +0 -15
- package/dist/resources/extensions/gsd/preferences.js +3 -16
- package/dist/resources/extensions/gsd/prompt-loader.js +1 -4
- package/dist/resources/extensions/gsd/rtk-status.js +43 -0
- package/dist/resources/extensions/gsd/state.js +1 -21
- package/dist/resources/extensions/gsd/write-intercept.js +1 -10
- package/dist/resources/extensions/ollama/index.js +5 -4
- package/dist/resources/extensions/ollama/ollama-client.js +6 -35
- package/dist/resources/extensions/ollama/ollama-discovery.js +6 -32
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +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/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +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/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +3 -3
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +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/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/chunks/2331.js +16 -16
- package/dist/web/standalone/.next/server/chunks/4741.js +12 -12
- package/dist/web/standalone/.next/server/chunks/5822.js +2 -2
- package/dist/web/standalone/.next/server/chunks/63.js +8 -8
- package/dist/web/standalone/.next/server/chunks/6897.js +3 -3
- package/dist/web/standalone/.next/server/functions-config-manifest.json +9 -0
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +2 -29
- package/dist/web/standalone/.next/server/middleware.js +12 -4
- 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/.next/server/webpack-runtime.js +1 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/env-api-keys.js +0 -1
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.custom.d.ts +0 -105
- package/packages/pi-ai/dist/models.custom.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.custom.js +0 -97
- package/packages/pi-ai/dist/models.custom.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +140 -648
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +364 -861
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/models.test.js +0 -105
- package/packages/pi-ai/dist/models.test.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +1 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/env-api-keys.ts +0 -1
- package/packages/pi-ai/src/models.custom.ts +0 -98
- package/packages/pi-ai/src/models.generated.ts +364 -861
- package/packages/pi-ai/src/models.test.ts +0 -135
- package/packages/pi-ai/src/types.ts +0 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +0 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/src/core/model-resolver.ts +0 -1
- package/src/resources/extensions/async-jobs/await-tool.test.ts +7 -40
- package/src/resources/extensions/async-jobs/await-tool.ts +4 -7
- package/src/resources/extensions/async-jobs/job-manager.ts +3 -33
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +26 -27
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +2 -20
- package/src/resources/extensions/gsd/auto/loop.ts +1 -89
- package/src/resources/extensions/gsd/auto-observability.ts +72 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +0 -7
- package/src/resources/extensions/gsd/auto.ts +20 -25
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +10 -8
- package/src/resources/extensions/gsd/commands-handlers.ts +1 -5
- package/src/resources/extensions/gsd/context-injector.ts +1 -1
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +8 -4
- package/src/resources/extensions/gsd/file-watcher.ts +100 -0
- package/src/resources/extensions/gsd/gsd-db.ts +5 -52
- package/src/resources/extensions/gsd/key-manager.ts +0 -2
- package/src/resources/extensions/gsd/preferences-skills.ts +36 -2
- package/src/resources/extensions/gsd/preferences-types.ts +0 -16
- package/src/resources/extensions/gsd/preferences.ts +6 -19
- package/src/resources/extensions/gsd/prompt-loader.ts +1 -6
- package/src/resources/extensions/gsd/rtk-status.ts +53 -0
- package/src/resources/extensions/gsd/state.ts +0 -20
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +0 -74
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +0 -63
- package/src/resources/extensions/gsd/tests/preferences.test.ts +0 -53
- package/src/resources/extensions/gsd/write-intercept.ts +1 -10
- package/src/resources/extensions/ollama/index.ts +5 -4
- package/src/resources/extensions/ollama/ollama-client.ts +6 -35
- package/src/resources/extensions/ollama/ollama-discovery.ts +6 -37
- package/src/resources/extensions/ollama/tests/ollama-discovery.test.ts +0 -54
- package/dist/resources/extensions/gsd/definition-io.js +0 -15
- package/dist/web/standalone/.next/server/edge-runtime-webpack.js +0 -2
- package/packages/pi-ai/dist/models.generated.test.d.ts +0 -2
- package/packages/pi-ai/dist/models.generated.test.d.ts.map +0 -1
- package/packages/pi-ai/dist/models.generated.test.js +0 -334
- package/packages/pi-ai/dist/models.generated.test.js.map +0 -1
- package/packages/pi-ai/src/models.generated.test.ts +0 -373
- package/src/resources/extensions/gsd/definition-io.ts +0 -18
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +0 -27
- package/src/resources/extensions/gsd/tests/block-db-writes.test.ts +0 -63
- package/src/resources/extensions/gsd/tests/definition-io.test.ts +0 -57
- package/src/resources/extensions/gsd/tests/doctor-heal-fixable-warnings.test.ts +0 -14
- package/src/resources/extensions/gsd/tests/false-degraded-mode-warning.test.ts +0 -104
- package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +0 -54
- package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +0 -34
- package/src/resources/extensions/gsd/tests/preferences-formatting.test.ts +0 -87
- package/src/resources/extensions/gsd/tests/prompt-loader-working-directory.test.ts +0 -19
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +0 -97
- package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +0 -41
- /package/dist/web/standalone/.next/static/{eR2tLKungpmiiOyUIhqjF → vr6Pbde48w4rMUplqDdh_}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{eR2tLKungpmiiOyUIhqjF → vr6Pbde48w4rMUplqDdh_}/_ssgManifest.js +0 -0
|
@@ -29,69 +29,6 @@ import {
|
|
|
29
29
|
import { debugLog } from "../debug-logger.js";
|
|
30
30
|
import { isInfrastructureError, isTransientCooldownError, getCooldownRetryAfterMs, COOLDOWN_FALLBACK_WAIT_MS, MAX_COOLDOWN_RETRIES } from "./infra-errors.js";
|
|
31
31
|
import { resolveEngine } from "../engine-resolver.js";
|
|
32
|
-
import { logWarning } from "../workflow-logger.js";
|
|
33
|
-
import { gsdRoot } from "../paths.js";
|
|
34
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
35
|
-
import { join } from "node:path";
|
|
36
|
-
|
|
37
|
-
// ── Stuck detection persistence (#3704) ──────────────────────────────────
|
|
38
|
-
// Persist stuck detection state to disk so it survives session restarts.
|
|
39
|
-
// Without this, restarting auto-mode resets all counters, allowing the
|
|
40
|
-
// same blocked unit to burn a full retry budget each session.
|
|
41
|
-
function stuckStatePath(basePath: string): string {
|
|
42
|
-
return join(gsdRoot(basePath), "runtime", "stuck-state.json");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function loadStuckState(basePath: string): { recentUnits: Array<{ key: string }>; stuckRecoveryAttempts: number } {
|
|
46
|
-
try {
|
|
47
|
-
const data = JSON.parse(readFileSync(stuckStatePath(basePath), "utf-8"));
|
|
48
|
-
return {
|
|
49
|
-
recentUnits: Array.isArray(data.recentUnits) ? data.recentUnits : [],
|
|
50
|
-
stuckRecoveryAttempts: typeof data.stuckRecoveryAttempts === "number" ? data.stuckRecoveryAttempts : 0,
|
|
51
|
-
};
|
|
52
|
-
} catch (err) {
|
|
53
|
-
debugLog("autoLoop", { phase: "load-stuck-state-failed", error: err instanceof Error ? err.message : String(err) });
|
|
54
|
-
return { recentUnits: [], stuckRecoveryAttempts: 0 };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function saveStuckState(basePath: string, state: LoopState): void {
|
|
59
|
-
try {
|
|
60
|
-
const filePath = stuckStatePath(basePath);
|
|
61
|
-
mkdirSync(join(gsdRoot(basePath), "runtime"), { recursive: true });
|
|
62
|
-
writeFileSync(filePath, JSON.stringify({
|
|
63
|
-
recentUnits: state.recentUnits.slice(-20), // keep last 20 entries
|
|
64
|
-
stuckRecoveryAttempts: state.stuckRecoveryAttempts,
|
|
65
|
-
updatedAt: new Date().toISOString(),
|
|
66
|
-
}) + "\n");
|
|
67
|
-
} catch (err) {
|
|
68
|
-
debugLog("autoLoop", { phase: "save-stuck-state-failed", error: err instanceof Error ? err.message : String(err) });
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── Memory pressure monitoring (#3331) ──────────────────────────────────
|
|
73
|
-
// Check heap usage every N iterations and trigger graceful shutdown before
|
|
74
|
-
// the OS OOM killer sends SIGKILL. The threshold is 90% of the V8 heap
|
|
75
|
-
// limit (--max-old-space-size or default ~1.5-4GB depending on platform).
|
|
76
|
-
const MEMORY_CHECK_INTERVAL = 5; // check every 5 iterations
|
|
77
|
-
const MEMORY_PRESSURE_THRESHOLD = 0.85; // 85% of heap limit
|
|
78
|
-
|
|
79
|
-
function checkMemoryPressure(): { pressured: boolean; heapMB: number; limitMB: number; pct: number } {
|
|
80
|
-
const mem = process.memoryUsage();
|
|
81
|
-
// v8.getHeapStatistics() gives heap_size_limit but requires import
|
|
82
|
-
// Use a conservative estimate: RSS > 3GB is danger zone on most systems
|
|
83
|
-
const heapMB = Math.round(mem.heapUsed / 1024 / 1024);
|
|
84
|
-
const rssMB = Math.round(mem.rss / 1024 / 1024);
|
|
85
|
-
// Try to get the actual V8 heap limit
|
|
86
|
-
let limitMB = 4096; // conservative default
|
|
87
|
-
try {
|
|
88
|
-
const v8 = require("node:v8");
|
|
89
|
-
const stats = v8.getHeapStatistics();
|
|
90
|
-
limitMB = Math.round(stats.heap_size_limit / 1024 / 1024);
|
|
91
|
-
} catch { limitMB = 4096; /* v8 stats unavailable — use conservative default */ }
|
|
92
|
-
const pct = heapMB / limitMB;
|
|
93
|
-
return { pressured: pct > MEMORY_PRESSURE_THRESHOLD, heapMB, limitMB, pct };
|
|
94
|
-
}
|
|
95
32
|
|
|
96
33
|
/**
|
|
97
34
|
* Main auto-mode execution loop. Iterates: derive → dispatch → guards →
|
|
@@ -109,13 +46,7 @@ export async function autoLoop(
|
|
|
109
46
|
): Promise<void> {
|
|
110
47
|
debugLog("autoLoop", { phase: "enter" });
|
|
111
48
|
let iteration = 0;
|
|
112
|
-
|
|
113
|
-
const persisted = loadStuckState(s.basePath);
|
|
114
|
-
const loopState: LoopState = {
|
|
115
|
-
recentUnits: persisted.recentUnits,
|
|
116
|
-
stuckRecoveryAttempts: persisted.stuckRecoveryAttempts,
|
|
117
|
-
consecutiveFinalizeTimeouts: 0,
|
|
118
|
-
};
|
|
49
|
+
const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
|
|
119
50
|
let consecutiveErrors = 0;
|
|
120
51
|
let consecutiveCooldowns = 0;
|
|
121
52
|
const recentErrorMessages: string[] = [];
|
|
@@ -143,24 +74,6 @@ export async function autoLoop(
|
|
|
143
74
|
break;
|
|
144
75
|
}
|
|
145
76
|
|
|
146
|
-
// ── Memory pressure check (#3331) ──
|
|
147
|
-
// Graceful shutdown before OOM killer sends SIGKILL.
|
|
148
|
-
if (iteration % MEMORY_CHECK_INTERVAL === 0) {
|
|
149
|
-
const mem = checkMemoryPressure();
|
|
150
|
-
debugLog("autoLoop", { phase: "memory-check", ...mem });
|
|
151
|
-
if (mem.pressured) {
|
|
152
|
-
logWarning("dispatch", `Memory pressure: ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%) — stopping auto-mode to prevent OOM kill`);
|
|
153
|
-
await deps.stopAuto(
|
|
154
|
-
ctx,
|
|
155
|
-
pi,
|
|
156
|
-
`Memory pressure: heap at ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%). ` +
|
|
157
|
-
`Stopping gracefully to prevent OOM kill after ${iteration} iterations. ` +
|
|
158
|
-
`Resume with /gsd auto to continue from where you left off.`,
|
|
159
|
-
);
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
77
|
if (!s.cmdCtx) {
|
|
165
78
|
debugLog("autoLoop", { phase: "exit", reason: "no-cmdCtx" });
|
|
166
79
|
break;
|
|
@@ -294,7 +207,6 @@ export async function autoLoop(
|
|
|
294
207
|
consecutiveCooldowns = 0;
|
|
295
208
|
recentErrorMessages.length = 0;
|
|
296
209
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
|
|
297
|
-
saveStuckState(s.basePath, loopState); // persist across session restarts (#3704)
|
|
298
210
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
299
211
|
|
|
300
212
|
if (reconcileResult.outcome === "milestone-complete") {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-dispatch observability checks for auto-mode units.
|
|
3
|
+
* Validates plan/summary file quality and builds repair instructions
|
|
4
|
+
* for the agent to fix gaps before proceeding with the unit.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
validatePlanBoundary,
|
|
10
|
+
validateExecuteBoundary,
|
|
11
|
+
validateCompleteBoundary,
|
|
12
|
+
formatValidationIssues,
|
|
13
|
+
} from "./observability-validator.js";
|
|
14
|
+
import type { ValidationIssue } from "./observability-validator.js";
|
|
15
|
+
import { parseUnitId } from "./unit-id.js";
|
|
16
|
+
|
|
17
|
+
export async function collectObservabilityWarnings(
|
|
18
|
+
ctx: ExtensionContext,
|
|
19
|
+
basePath: string,
|
|
20
|
+
unitType: string,
|
|
21
|
+
unitId: string,
|
|
22
|
+
): Promise<ValidationIssue[]> {
|
|
23
|
+
// Hook units have custom artifacts — skip standard observability checks
|
|
24
|
+
if (unitType.startsWith("hook/")) return [];
|
|
25
|
+
|
|
26
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
27
|
+
|
|
28
|
+
if (!mid || !sid) return [];
|
|
29
|
+
|
|
30
|
+
let issues = [] as Awaited<ReturnType<typeof validatePlanBoundary>>;
|
|
31
|
+
|
|
32
|
+
if (unitType === "plan-slice") {
|
|
33
|
+
issues = await validatePlanBoundary(basePath, mid, sid);
|
|
34
|
+
} else if (unitType === "execute-task" && tid) {
|
|
35
|
+
issues = await validateExecuteBoundary(basePath, mid, sid, tid);
|
|
36
|
+
} else if (unitType === "complete-slice") {
|
|
37
|
+
issues = await validateCompleteBoundary(basePath, mid, sid);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (issues.length > 0) {
|
|
41
|
+
ctx.ui.notify(
|
|
42
|
+
`Observability check (${unitType}) found ${issues.length} warning${issues.length === 1 ? "" : "s"}:\n${formatValidationIssues(issues)}`,
|
|
43
|
+
"warning",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return issues;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildObservabilityRepairBlock(issues: ValidationIssue[]): string {
|
|
51
|
+
if (issues.length === 0) return "";
|
|
52
|
+
const items = issues.map(issue => {
|
|
53
|
+
const fileName = issue.file.split("/").pop() || issue.file;
|
|
54
|
+
let line = `- **${fileName}**: ${issue.message}`;
|
|
55
|
+
if (issue.suggestion) line += ` → ${issue.suggestion}`;
|
|
56
|
+
return line;
|
|
57
|
+
});
|
|
58
|
+
return [
|
|
59
|
+
"",
|
|
60
|
+
"---",
|
|
61
|
+
"",
|
|
62
|
+
"## Pre-flight: Observability gaps to fix FIRST",
|
|
63
|
+
"",
|
|
64
|
+
"The following issues were detected in plan/summary files for this unit.",
|
|
65
|
+
"**Read each flagged file, apply the fix described, then proceed with the unit.**",
|
|
66
|
+
"",
|
|
67
|
+
...items,
|
|
68
|
+
"",
|
|
69
|
+
"---",
|
|
70
|
+
"",
|
|
71
|
+
].join("\n");
|
|
72
|
+
}
|
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
buildTaskFileName,
|
|
26
26
|
} from "./paths.js";
|
|
27
27
|
import { invalidateAllCaches } from "./cache.js";
|
|
28
|
-
import { rebuildState } from "./doctor.js";
|
|
29
28
|
import { parseUnitId } from "./unit-id.js";
|
|
30
29
|
import { closeoutUnit, type CloseoutOptions } from "./auto-unit-closeout.js";
|
|
31
30
|
import {
|
|
@@ -368,12 +367,6 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
|
|
|
368
367
|
}
|
|
369
368
|
});
|
|
370
369
|
|
|
371
|
-
// Keep the on-disk STATE.md aligned with the live derived state after
|
|
372
|
-
// ordinary unit completion, before any worktree state is synced back.
|
|
373
|
-
await runSafely("postUnit", "state-rebuild", async () => {
|
|
374
|
-
await rebuildState(s.basePath);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
370
|
// Sync worktree state back to project root (skipped for lightweight sidecars)
|
|
378
371
|
if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
|
|
379
372
|
await runSafely("postUnit", "worktree-sync", () => {
|
|
@@ -677,13 +677,9 @@ function cleanupAfterLoopExit(ctx: ExtensionContext): void {
|
|
|
677
677
|
logWarning("session", `lock cleanup failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
ctx.ui.setStatus("gsd-auto", undefined);
|
|
684
|
-
ctx.ui.setWidget("gsd-progress", undefined);
|
|
685
|
-
ctx.ui.setFooter(undefined);
|
|
686
|
-
}
|
|
680
|
+
ctx.ui.setStatus("gsd-auto", undefined);
|
|
681
|
+
ctx.ui.setWidget("gsd-progress", undefined);
|
|
682
|
+
ctx.ui.setFooter(undefined);
|
|
687
683
|
|
|
688
684
|
// Restore CWD out of worktree back to original project root
|
|
689
685
|
if (s.originalBasePath) {
|
|
@@ -795,22 +791,7 @@ export async function stopAuto(
|
|
|
795
791
|
debugLog("stop-cleanup-worktree", { error: e instanceof Error ? e.message : String(e) });
|
|
796
792
|
}
|
|
797
793
|
|
|
798
|
-
// ── Step 5:
|
|
799
|
-
// rebuildState() calls deriveState() which needs the DB for authoritative
|
|
800
|
-
// state. Previously this ran after closeDatabase(), forcing a filesystem
|
|
801
|
-
// fallback that could disagree with the DB-backed dispatch decisions —
|
|
802
|
-
// a split-brain where dispatch says "blocked" but STATE.md shows work.
|
|
803
|
-
if (s.basePath) {
|
|
804
|
-
try {
|
|
805
|
-
await rebuildState(s.basePath);
|
|
806
|
-
} catch (e) {
|
|
807
|
-
debugLog("stop-rebuild-state-failed", {
|
|
808
|
-
error: e instanceof Error ? e.message : String(e),
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// ── Step 6: DB cleanup ──
|
|
794
|
+
// ── Step 5: DB cleanup ──
|
|
814
795
|
if (isDbAvailable()) {
|
|
815
796
|
try {
|
|
816
797
|
const { closeDatabase } = await import("./gsd-db.js");
|
|
@@ -822,7 +803,7 @@ export async function stopAuto(
|
|
|
822
803
|
}
|
|
823
804
|
}
|
|
824
805
|
|
|
825
|
-
// ── Step
|
|
806
|
+
// ── Step 6: Restore basePath and chdir ──
|
|
826
807
|
try {
|
|
827
808
|
if (s.originalBasePath) {
|
|
828
809
|
s.basePath = s.originalBasePath;
|
|
@@ -837,7 +818,7 @@ export async function stopAuto(
|
|
|
837
818
|
debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
|
|
838
819
|
}
|
|
839
820
|
|
|
840
|
-
// ── Step
|
|
821
|
+
// ── Step 7: Ledger notification ──
|
|
841
822
|
try {
|
|
842
823
|
const ledger = getLedger();
|
|
843
824
|
if (ledger && ledger.units.length > 0) {
|
|
@@ -853,6 +834,17 @@ export async function stopAuto(
|
|
|
853
834
|
debugLog("stop-cleanup-ledger", { error: e instanceof Error ? e.message : String(e) });
|
|
854
835
|
}
|
|
855
836
|
|
|
837
|
+
// ── Step 8: Rebuild state ──
|
|
838
|
+
if (s.basePath) {
|
|
839
|
+
try {
|
|
840
|
+
await rebuildState(s.basePath);
|
|
841
|
+
} catch (e) {
|
|
842
|
+
debugLog("stop-rebuild-state-failed", {
|
|
843
|
+
error: e instanceof Error ? e.message : String(e),
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
856
848
|
// ── Step 9: Cmux sidebar / event log ──
|
|
857
849
|
try {
|
|
858
850
|
clearCmuxSidebar(loadedPreferences);
|
|
@@ -1721,6 +1713,9 @@ export async function dispatchHookUnit(
|
|
|
1721
1713
|
return true;
|
|
1722
1714
|
}
|
|
1723
1715
|
|
|
1716
|
+
// Direct phase dispatch → auto-direct-dispatch.ts
|
|
1717
|
+
export { dispatchDirectPhase } from "./auto-direct-dispatch.js";
|
|
1718
|
+
|
|
1724
1719
|
// Re-export recovery functions for external consumers
|
|
1725
1720
|
export {
|
|
1726
1721
|
buildLoopRemediationSteps,
|
|
@@ -181,10 +181,14 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
181
181
|
// Only gate-shaped ask_user_questions calls should block execution.
|
|
182
182
|
// The gate stays pending until the user selects the approval option.
|
|
183
183
|
if (event.toolName === "ask_user_questions") {
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
187
|
-
|
|
184
|
+
const milestoneId = getDiscussionMilestoneId(discussionBasePath);
|
|
185
|
+
const inDiscussion = milestoneId !== null || isQueuePhaseActive();
|
|
186
|
+
if (inDiscussion) {
|
|
187
|
+
const questions: any[] = (event.input as any)?.questions ?? [];
|
|
188
|
+
const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
|
|
189
|
+
if (typeof questionId === "string") {
|
|
190
|
+
setPendingGate(questionId);
|
|
191
|
+
}
|
|
188
192
|
}
|
|
189
193
|
}
|
|
190
194
|
|
|
@@ -282,6 +286,7 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
282
286
|
if (event.toolName !== "ask_user_questions") return;
|
|
283
287
|
const milestoneId = getDiscussionMilestoneId(process.cwd());
|
|
284
288
|
const queueActive = isQueuePhaseActive();
|
|
289
|
+
if (!milestoneId && !queueActive) return;
|
|
285
290
|
|
|
286
291
|
const details = event.details as any;
|
|
287
292
|
|
|
@@ -314,16 +319,13 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
314
319
|
// Only unlock the gate if the user selected the first option (confirmation).
|
|
315
320
|
// Cross-references against the question's defined options to reject free-form "Other" text.
|
|
316
321
|
const answer = details.response?.answers?.[question.id];
|
|
317
|
-
const inferredMilestoneId = extractDepthVerificationMilestoneId(question.id) ?? milestoneId;
|
|
318
322
|
if (isDepthConfirmationAnswer(answer?.selected, question.options)) {
|
|
319
|
-
markDepthVerified(
|
|
320
|
-
clearPendingGate();
|
|
323
|
+
markDepthVerified(extractDepthVerificationMilestoneId(question.id) ?? milestoneId);
|
|
321
324
|
}
|
|
322
325
|
break;
|
|
323
326
|
}
|
|
324
327
|
}
|
|
325
328
|
|
|
326
|
-
if (!milestoneId && !queueActive) return;
|
|
327
329
|
if (!milestoneId) return;
|
|
328
330
|
|
|
329
331
|
const basePath = process.cwd();
|
|
@@ -78,10 +78,6 @@ export function parseDoctorArgs(args: string) {
|
|
|
78
78
|
return { jsonMode, dryRun, fixFlag, includeBuild, includeTests, mode, requestedScope };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
export function isDoctorHealActionable(issue: { fixable: boolean; severity: string }): boolean {
|
|
82
|
-
return issue.fixable && issue.severity !== "info";
|
|
83
|
-
}
|
|
84
|
-
|
|
85
81
|
export async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
86
82
|
const { jsonMode, dryRun, fixFlag, includeBuild, includeTests, mode, requestedScope } = parseDoctorArgs(args);
|
|
87
83
|
const scope = await selectDoctorScope(projectRoot(), requestedScope);
|
|
@@ -113,7 +109,7 @@ export async function handleDoctor(args: string, ctx: ExtensionCommandContext, p
|
|
|
113
109
|
scope: effectiveScope,
|
|
114
110
|
includeWarnings: true,
|
|
115
111
|
});
|
|
116
|
-
const actionable = unresolved.filter(
|
|
112
|
+
const actionable = unresolved.filter(issue => issue.severity === "error");
|
|
117
113
|
if (actionable.length === 0) {
|
|
118
114
|
ctx.ui.notify("Doctor heal found nothing actionable to hand off to the LLM.", "info");
|
|
119
115
|
return;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { readFileSync, existsSync } from "node:fs";
|
|
17
17
|
import { join, resolve, sep } from "node:path";
|
|
18
18
|
import type { StepDefinition } from "./definition-loader.js";
|
|
19
|
-
import { readFrozenDefinition } from "./
|
|
19
|
+
import { readFrozenDefinition } from "./custom-workflow-engine.js";
|
|
20
20
|
|
|
21
21
|
/** Maximum characters per artifact to prevent context window blowout. */
|
|
22
22
|
const MAX_CONTEXT_CHARS = 10_000;
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
} from "./engine-types.js";
|
|
23
23
|
import { readFileSync } from "node:fs";
|
|
24
24
|
import { join } from "node:path";
|
|
25
|
+
import { parse } from "yaml";
|
|
25
26
|
import {
|
|
26
27
|
readGraph,
|
|
27
28
|
writeGraph,
|
|
@@ -31,13 +32,16 @@ import {
|
|
|
31
32
|
type WorkflowGraph,
|
|
32
33
|
} from "./graph.js";
|
|
33
34
|
import { injectContext } from "./context-injector.js";
|
|
34
|
-
import type { StepDefinition } from "./definition-loader.js";
|
|
35
|
-
import { readFrozenDefinition } from "./definition-io.js";
|
|
35
|
+
import type { WorkflowDefinition, StepDefinition } from "./definition-loader.js";
|
|
36
36
|
import { parseUnitId } from "./unit-id.js";
|
|
37
37
|
import { withFileLock } from "./file-lock.js";
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
export
|
|
39
|
+
/** Read and parse the frozen DEFINITION.yaml from a run directory. */
|
|
40
|
+
export function readFrozenDefinition(runDir: string): WorkflowDefinition {
|
|
41
|
+
const defPath = join(runDir, "DEFINITION.yaml");
|
|
42
|
+
const raw = readFileSync(defPath, "utf-8");
|
|
43
|
+
return parse(raw, { schema: "core" }) as WorkflowDefinition;
|
|
44
|
+
}
|
|
41
45
|
|
|
42
46
|
export class CustomWorkflowEngine implements WorkflowEngine {
|
|
43
47
|
readonly engineId = "custom";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { FSWatcher } from "chokidar";
|
|
2
|
+
import type { EventBus } from "@gsd/pi-coding-agent";
|
|
3
|
+
import { relative } from "node:path";
|
|
4
|
+
|
|
5
|
+
let watcher: FSWatcher | null = null;
|
|
6
|
+
let pending = new Map<string, ReturnType<typeof setTimeout>>();
|
|
7
|
+
|
|
8
|
+
const EVENT_MAP: Record<string, string> = {
|
|
9
|
+
"settings.json": "settings-changed",
|
|
10
|
+
"auth.json": "auth-changed",
|
|
11
|
+
"models.json": "models-changed",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const EXTENSIONS_DIR = "extensions";
|
|
15
|
+
|
|
16
|
+
const IGNORED_PATTERNS = [
|
|
17
|
+
"**/sessions/**",
|
|
18
|
+
"**/*.tmp",
|
|
19
|
+
"**/*.swp",
|
|
20
|
+
"**/*~",
|
|
21
|
+
"**/.DS_Store",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const DEBOUNCE_MS = 300;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Start watching `agentDir` (e.g. `~/.gsd/agent/`) for config changes.
|
|
28
|
+
* Emits events on the supplied EventBus when watched files are modified.
|
|
29
|
+
*/
|
|
30
|
+
export async function startFileWatcher(
|
|
31
|
+
agentDir: string,
|
|
32
|
+
eventBus: EventBus,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
if (watcher) {
|
|
35
|
+
await watcher.close();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { watch } = await import("chokidar");
|
|
39
|
+
|
|
40
|
+
pending = new Map<string, ReturnType<typeof setTimeout>>();
|
|
41
|
+
|
|
42
|
+
function debounceEmit(event: string): void {
|
|
43
|
+
const existing = pending.get(event);
|
|
44
|
+
if (existing) clearTimeout(existing);
|
|
45
|
+
pending.set(
|
|
46
|
+
event,
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
pending.delete(event);
|
|
49
|
+
eventBus.emit(event, { timestamp: Date.now() });
|
|
50
|
+
}, DEBOUNCE_MS),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveEvent(filePath: string): string | null {
|
|
55
|
+
const rel = relative(agentDir, filePath);
|
|
56
|
+
if (rel.startsWith("..")) return null;
|
|
57
|
+
|
|
58
|
+
// Check direct file matches
|
|
59
|
+
for (const [file, event] of Object.entries(EVENT_MAP)) {
|
|
60
|
+
if (rel === file) return event;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check extensions directory
|
|
64
|
+
if (rel.startsWith(EXTENSIONS_DIR + "/") || rel === EXTENSIONS_DIR) {
|
|
65
|
+
return "extensions-changed";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
watcher = watch(agentDir, {
|
|
72
|
+
ignoreInitial: true,
|
|
73
|
+
depth: 2,
|
|
74
|
+
ignored: IGNORED_PATTERNS,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
for (const eventType of ["add", "change", "unlink"] as const) {
|
|
78
|
+
watcher.on(eventType, (filePath: string) => {
|
|
79
|
+
const event = resolveEvent(filePath);
|
|
80
|
+
if (event) debounceEmit(event);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Wait for watcher to be ready
|
|
85
|
+
await new Promise<void>((resolve) => {
|
|
86
|
+
watcher!.on("ready", resolve);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Stop the file watcher and clean up resources.
|
|
92
|
+
*/
|
|
93
|
+
export async function stopFileWatcher(): Promise<void> {
|
|
94
|
+
for (const timer of pending.values()) clearTimeout(timer);
|
|
95
|
+
pending.clear();
|
|
96
|
+
if (watcher) {
|
|
97
|
+
await watcher.close();
|
|
98
|
+
watcher = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -163,29 +163,6 @@ function openRawDb(path: string): unknown {
|
|
|
163
163
|
|
|
164
164
|
const SCHEMA_VERSION = 14;
|
|
165
165
|
|
|
166
|
-
function indexExists(db: DbAdapter, name: string): boolean {
|
|
167
|
-
return !!db.prepare(
|
|
168
|
-
"SELECT 1 as present FROM sqlite_master WHERE type = 'index' AND name = ?",
|
|
169
|
-
).get(name);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function dedupeVerificationEvidenceRows(db: DbAdapter): void {
|
|
173
|
-
db.exec(`
|
|
174
|
-
DELETE FROM verification_evidence
|
|
175
|
-
WHERE rowid NOT IN (
|
|
176
|
-
SELECT MIN(rowid)
|
|
177
|
-
FROM verification_evidence
|
|
178
|
-
GROUP BY task_id, slice_id, milestone_id, command, verdict
|
|
179
|
-
)
|
|
180
|
-
`);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function ensureVerificationEvidenceDedupIndex(db: DbAdapter): void {
|
|
184
|
-
if (indexExists(db, "idx_verification_evidence_dedup")) return;
|
|
185
|
-
dedupeVerificationEvidenceRows(db);
|
|
186
|
-
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_verification_evidence_dedup ON verification_evidence(task_id, slice_id, milestone_id, command, verdict)");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
166
|
function initSchema(db: DbAdapter, fileBacked: boolean): void {
|
|
190
167
|
if (fileBacked) db.exec("PRAGMA journal_mode=WAL");
|
|
191
168
|
if (fileBacked) db.exec("PRAGMA busy_timeout = 5000");
|
|
@@ -433,7 +410,7 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
|
|
|
433
410
|
db.exec("CREATE INDEX IF NOT EXISTS idx_milestones_status ON milestones(status)");
|
|
434
411
|
db.exec("CREATE INDEX IF NOT EXISTS idx_quality_gates_pending ON quality_gates(milestone_id, slice_id, status)");
|
|
435
412
|
db.exec("CREATE INDEX IF NOT EXISTS idx_verification_evidence_task ON verification_evidence(milestone_id, slice_id, task_id)");
|
|
436
|
-
|
|
413
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_verification_evidence_dedup ON verification_evidence(task_id, slice_id, milestone_id, command, verdict)");
|
|
437
414
|
|
|
438
415
|
// v14 index — slice dependency lookups
|
|
439
416
|
db.exec("CREATE INDEX IF NOT EXISTS idx_slice_deps_target ON slice_dependencies(milestone_id, depends_on_slice_id)");
|
|
@@ -766,7 +743,7 @@ function migrateSchema(db: DbAdapter): void {
|
|
|
766
743
|
db.exec("CREATE INDEX IF NOT EXISTS idx_milestones_status ON milestones(status)");
|
|
767
744
|
db.exec("CREATE INDEX IF NOT EXISTS idx_quality_gates_pending ON quality_gates(milestone_id, slice_id, status)");
|
|
768
745
|
db.exec("CREATE INDEX IF NOT EXISTS idx_verification_evidence_task ON verification_evidence(milestone_id, slice_id, task_id)");
|
|
769
|
-
|
|
746
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_verification_evidence_dedup ON verification_evidence(task_id, slice_id, milestone_id, command, verdict)");
|
|
770
747
|
db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
|
|
771
748
|
":version": 13,
|
|
772
749
|
":applied_at": new Date().toISOString(),
|
|
@@ -1565,30 +1542,6 @@ export interface TaskRow {
|
|
|
1565
1542
|
}
|
|
1566
1543
|
|
|
1567
1544
|
function rowToTask(row: Record<string, unknown>): TaskRow {
|
|
1568
|
-
const parseTaskArray = (value: unknown): string[] => {
|
|
1569
|
-
if (Array.isArray(value)) {
|
|
1570
|
-
return value.filter((entry): entry is string => typeof entry === "string");
|
|
1571
|
-
}
|
|
1572
|
-
if (typeof value !== "string") return [];
|
|
1573
|
-
|
|
1574
|
-
const trimmed = value.trim();
|
|
1575
|
-
if (!trimmed) return [];
|
|
1576
|
-
|
|
1577
|
-
try {
|
|
1578
|
-
const parsed = JSON.parse(trimmed);
|
|
1579
|
-
if (Array.isArray(parsed)) {
|
|
1580
|
-
return parsed.filter((entry): entry is string => typeof entry === "string");
|
|
1581
|
-
}
|
|
1582
|
-
if (typeof parsed === "string" && parsed.trim()) {
|
|
1583
|
-
return [parsed.trim()];
|
|
1584
|
-
}
|
|
1585
|
-
} catch {
|
|
1586
|
-
// Older/corrupt DB rows may contain raw comma-separated paths instead of JSON arrays.
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
return trimmed.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
1590
|
-
};
|
|
1591
|
-
|
|
1592
1545
|
return {
|
|
1593
1546
|
milestone_id: row["milestone_id"] as string,
|
|
1594
1547
|
slice_id: row["slice_id"] as string,
|
|
@@ -1608,10 +1561,10 @@ function rowToTask(row: Record<string, unknown>): TaskRow {
|
|
|
1608
1561
|
full_summary_md: row["full_summary_md"] as string,
|
|
1609
1562
|
description: (row["description"] as string) ?? "",
|
|
1610
1563
|
estimate: (row["estimate"] as string) ?? "",
|
|
1611
|
-
files:
|
|
1564
|
+
files: JSON.parse((row["files"] as string) || "[]"),
|
|
1612
1565
|
verify: (row["verify"] as string) ?? "",
|
|
1613
|
-
inputs:
|
|
1614
|
-
expected_output:
|
|
1566
|
+
inputs: JSON.parse((row["inputs"] as string) || "[]"),
|
|
1567
|
+
expected_output: JSON.parse((row["expected_output"] as string) || "[]"),
|
|
1615
1568
|
observability_impact: (row["observability_impact"] as string) ?? "",
|
|
1616
1569
|
full_plan_md: (row["full_plan_md"] as string) ?? "",
|
|
1617
1570
|
sequence: (row["sequence"] as number) ?? 0,
|
|
@@ -49,8 +49,6 @@ export const PROVIDER_REGISTRY: ProviderInfo[] = [
|
|
|
49
49
|
{ id: "custom-openai", label: "Custom (OpenAI-compat)", category: "llm", envVar: "CUSTOM_OPENAI_API_KEY" },
|
|
50
50
|
{ id: "cerebras", label: "Cerebras", category: "llm", envVar: "CEREBRAS_API_KEY" },
|
|
51
51
|
{ id: "azure-openai-responses", label: "Azure OpenAI", category: "llm", envVar: "AZURE_OPENAI_API_KEY" },
|
|
52
|
-
{ id: "alibaba-coding-plan", label: "Alibaba Coding Plan", category: "llm", envVar: "ALIBABA_API_KEY", dashboardUrl: "bailian.console.aliyun.com" },
|
|
53
|
-
{ id: "alibaba-dashscope", label: "Alibaba DashScope", category: "llm", envVar: "DASHSCOPE_API_KEY", dashboardUrl: "dashscope.console.aliyun.com" },
|
|
54
52
|
|
|
55
53
|
// Tool Keys
|
|
56
54
|
{ id: "context7", label: "Context7 Docs", category: "tool", envVar: "CONTEXT7_API_KEY", dashboardUrl: "context7.com/dashboard" },
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
SkillResolutionReport,
|
|
18
18
|
} from "./preferences-types.js";
|
|
19
19
|
import { validatePreferences } from "./preferences-validation.js";
|
|
20
|
+
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
20
21
|
|
|
21
22
|
// Re-export types so existing consumers of ./preferences-skills.js keep working
|
|
22
23
|
export type { GSDSkillRule, SkillDiscoveryMode, SkillResolution, SkillResolutionReport } from "./preferences-types.js";
|
|
@@ -142,5 +143,38 @@ export function resolveAllSkillReferences(preferences: GSDPreferences, cwd: stri
|
|
|
142
143
|
return { resolutions, warnings };
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Format a skill reference for the system prompt.
|
|
148
|
+
* If resolved, shows the path so the agent knows exactly where to read.
|
|
149
|
+
* If unresolved, marks it clearly.
|
|
150
|
+
*/
|
|
151
|
+
export function formatSkillRef(ref: string, resolutions: Map<string, SkillResolution>): string {
|
|
152
|
+
const resolution = resolutions.get(ref);
|
|
153
|
+
if (!resolution || resolution.method === "unresolved") {
|
|
154
|
+
return `${ref} (⚠ not found — check skill name or path)`;
|
|
155
|
+
}
|
|
156
|
+
// For absolute paths where SKILL.md is just appended, don't clutter the output
|
|
157
|
+
if (resolution.method === "absolute-path" || resolution.method === "absolute-dir") {
|
|
158
|
+
return ref;
|
|
159
|
+
}
|
|
160
|
+
// For bare names resolved from skill directories, show the resolved path
|
|
161
|
+
return `${ref} → \`${resolution.resolvedPath}\``;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Resolve the skill discovery mode from effective preferences.
|
|
166
|
+
* Defaults to "suggest" -- skills are identified during research but not installed automatically.
|
|
167
|
+
*/
|
|
168
|
+
export function resolveSkillDiscoveryMode(): SkillDiscoveryMode {
|
|
169
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
170
|
+
return prefs?.preferences.skill_discovery ?? "suggest";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolve the skill staleness threshold in days.
|
|
175
|
+
* Returns 0 if disabled, default 60 if not configured.
|
|
176
|
+
*/
|
|
177
|
+
export function resolveSkillStalenessDays(): number {
|
|
178
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
179
|
+
return prefs?.preferences.skill_staleness_days ?? 60;
|
|
180
|
+
}
|