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
|
@@ -54,14 +54,11 @@ export function createAwaitTool(getManager) {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// the cross-turn case (job already completed before await_job was called).
|
|
61
|
-
// Previously this only set j.awaited = true, which missed the cross-turn
|
|
62
|
-
// case because the queueMicrotask had already fired (#3787).
|
|
57
|
+
// Mark all watched jobs as awaited upfront so the onJobComplete
|
|
58
|
+
// callback (which fires synchronously in the promise .then()) knows
|
|
59
|
+
// to suppress the follow-up message.
|
|
63
60
|
for (const j of watched)
|
|
64
|
-
|
|
61
|
+
j.awaited = true;
|
|
65
62
|
// If all watched jobs are already done, return immediately
|
|
66
63
|
const running = watched.filter((j) => j.status === "running");
|
|
67
64
|
if (running.length === 0) {
|
|
@@ -118,38 +118,13 @@ export class AsyncJobManager {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
// ── Private ────────────────────────────────────────────────────────────
|
|
121
|
-
/**
|
|
122
|
-
* Suppress follow-up notification for a job — cancels any pending delivery
|
|
123
|
-
* timer and marks the job as awaited. Safe to call at any time, including
|
|
124
|
-
* before or after the job completes (#3787).
|
|
125
|
-
*/
|
|
126
|
-
suppressFollowUp(id) {
|
|
127
|
-
const job = this.jobs.get(id);
|
|
128
|
-
if (!job)
|
|
129
|
-
return;
|
|
130
|
-
job.awaited = true;
|
|
131
|
-
if (job.deliveryTimer !== undefined) {
|
|
132
|
-
clearTimeout(job.deliveryTimer);
|
|
133
|
-
job.deliveryTimer = undefined;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
121
|
deliverResult(job) {
|
|
137
122
|
if (!this.onJobComplete)
|
|
138
123
|
return;
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
// a later LLM turn (after the job already completed). queueMicrotask ran
|
|
142
|
-
// immediately and could not be cancelled (#2762, #3787).
|
|
124
|
+
// Defer delivery by one microtask so await_job's .then() chain runs first
|
|
125
|
+
// and can set job.awaited = true before onJobComplete checks it (#2762).
|
|
143
126
|
const cb = this.onJobComplete;
|
|
144
|
-
|
|
145
|
-
job.deliveryTimer = undefined;
|
|
146
|
-
if (!job.awaited)
|
|
147
|
-
cb(job);
|
|
148
|
-
}, 0);
|
|
149
|
-
// Allow process to exit even if timer is pending
|
|
150
|
-
if (typeof job.deliveryTimer === "object" && "unref" in job.deliveryTimer) {
|
|
151
|
-
job.deliveryTimer.unref();
|
|
152
|
-
}
|
|
127
|
+
queueMicrotask(() => cb(job));
|
|
153
128
|
}
|
|
154
129
|
scheduleEviction(id) {
|
|
155
130
|
const existing = this.evictionTimers.get(id);
|
|
@@ -405,25 +405,32 @@ export function makeAbortedMessage(model, lastTextContent) {
|
|
|
405
405
|
/**
|
|
406
406
|
* Resolve the Claude Code permission mode for the current run.
|
|
407
407
|
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
* already enforce. `GSD_CLAUDE_CODE_PERMISSION_MODE` lets security-conscious
|
|
414
|
-
* users opt into a stricter mode (`acceptEdits`, `default`, `plan`).
|
|
408
|
+
* - Auto-mode / headless runs bypass permissions so tool calls don't block
|
|
409
|
+
* on prompts the user isn't watching.
|
|
410
|
+
* - Interactive runs default to `acceptEdits` so file/bash writes still
|
|
411
|
+
* land quickly but the SDK retains a permission gate.
|
|
412
|
+
* - `GSD_CLAUDE_CODE_PERMISSION_MODE` forces a specific mode when set.
|
|
415
413
|
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
* (#4099) is continuous approval fatigue that blocks real work.
|
|
414
|
+
* Cross-extension coupling is kept minimal by dynamically importing
|
|
415
|
+
* `isAutoActive` and falling back to the bypass default if the import
|
|
416
|
+
* fails (e.g. in unit tests that load stream-adapter in isolation).
|
|
420
417
|
*/
|
|
421
418
|
export async function resolveClaudePermissionMode(env = process.env) {
|
|
422
419
|
const override = env.GSD_CLAUDE_CODE_PERMISSION_MODE?.trim();
|
|
423
420
|
if (override === "bypassPermissions" || override === "acceptEdits" || override === "default" || override === "plan") {
|
|
424
421
|
return override;
|
|
425
422
|
}
|
|
426
|
-
|
|
423
|
+
try {
|
|
424
|
+
const autoMod = (await import("../gsd/auto.js"));
|
|
425
|
+
if (typeof autoMod.isAutoActive === "function" && autoMod.isAutoActive()) {
|
|
426
|
+
return "bypassPermissions";
|
|
427
|
+
}
|
|
428
|
+
return "acceptEdits";
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
// auto.ts unavailable (tests, non-GSD contexts) — stay permissive.
|
|
432
|
+
return "bypassPermissions";
|
|
433
|
+
}
|
|
427
434
|
}
|
|
428
435
|
/**
|
|
429
436
|
* Build the options object passed to the Claude Agent SDK's `query()` call.
|
|
@@ -440,21 +447,13 @@ export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
|
|
|
440
447
|
const mcpServers = buildWorkflowMcpServers();
|
|
441
448
|
const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
|
|
442
449
|
const disallowedTools = ["AskUserQuestion"];
|
|
443
|
-
// Pre-authorize
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
"Write",
|
|
451
|
-
"Edit",
|
|
452
|
-
"Glob",
|
|
453
|
-
"Grep",
|
|
454
|
-
"Bash(ls:*)",
|
|
455
|
-
"Bash(pwd)",
|
|
456
|
-
...(mcpServers ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`) : []),
|
|
457
|
-
];
|
|
450
|
+
// Pre-authorize every registered workflow MCP server's tools. Without this,
|
|
451
|
+
// `acceptEdits` mode (the interactive default) auto-approves built-in
|
|
452
|
+
// Edit/Write/Bash but still gates MCP calls like `mcp__gsd-workflow__*`,
|
|
453
|
+
// surfacing "This command requires approval" on every GSD action (#4099).
|
|
454
|
+
const allowedTools = mcpServers
|
|
455
|
+
? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`)
|
|
456
|
+
: [];
|
|
458
457
|
return {
|
|
459
458
|
pathToClaudeCodeExecutable: getClaudePath(),
|
|
460
459
|
model: modelId,
|
|
@@ -13,69 +13,6 @@ import { runPreDispatch, runDispatch, runGuards, runUnitPhase, runFinalize, } fr
|
|
|
13
13
|
import { debugLog } from "../debug-logger.js";
|
|
14
14
|
import { isInfrastructureError, isTransientCooldownError, getCooldownRetryAfterMs, COOLDOWN_FALLBACK_WAIT_MS, MAX_COOLDOWN_RETRIES } from "./infra-errors.js";
|
|
15
15
|
import { resolveEngine } from "../engine-resolver.js";
|
|
16
|
-
import { logWarning } from "../workflow-logger.js";
|
|
17
|
-
import { gsdRoot } from "../paths.js";
|
|
18
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
19
|
-
import { join } from "node:path";
|
|
20
|
-
// ── Stuck detection persistence (#3704) ──────────────────────────────────
|
|
21
|
-
// Persist stuck detection state to disk so it survives session restarts.
|
|
22
|
-
// Without this, restarting auto-mode resets all counters, allowing the
|
|
23
|
-
// same blocked unit to burn a full retry budget each session.
|
|
24
|
-
function stuckStatePath(basePath) {
|
|
25
|
-
return join(gsdRoot(basePath), "runtime", "stuck-state.json");
|
|
26
|
-
}
|
|
27
|
-
function loadStuckState(basePath) {
|
|
28
|
-
try {
|
|
29
|
-
const data = JSON.parse(readFileSync(stuckStatePath(basePath), "utf-8"));
|
|
30
|
-
return {
|
|
31
|
-
recentUnits: Array.isArray(data.recentUnits) ? data.recentUnits : [],
|
|
32
|
-
stuckRecoveryAttempts: typeof data.stuckRecoveryAttempts === "number" ? data.stuckRecoveryAttempts : 0,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
catch (err) {
|
|
36
|
-
debugLog("autoLoop", { phase: "load-stuck-state-failed", error: err instanceof Error ? err.message : String(err) });
|
|
37
|
-
return { recentUnits: [], stuckRecoveryAttempts: 0 };
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function saveStuckState(basePath, state) {
|
|
41
|
-
try {
|
|
42
|
-
const filePath = stuckStatePath(basePath);
|
|
43
|
-
mkdirSync(join(gsdRoot(basePath), "runtime"), { recursive: true });
|
|
44
|
-
writeFileSync(filePath, JSON.stringify({
|
|
45
|
-
recentUnits: state.recentUnits.slice(-20), // keep last 20 entries
|
|
46
|
-
stuckRecoveryAttempts: state.stuckRecoveryAttempts,
|
|
47
|
-
updatedAt: new Date().toISOString(),
|
|
48
|
-
}) + "\n");
|
|
49
|
-
}
|
|
50
|
-
catch (err) {
|
|
51
|
-
debugLog("autoLoop", { phase: "save-stuck-state-failed", error: err instanceof Error ? err.message : String(err) });
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
// ── Memory pressure monitoring (#3331) ──────────────────────────────────
|
|
55
|
-
// Check heap usage every N iterations and trigger graceful shutdown before
|
|
56
|
-
// the OS OOM killer sends SIGKILL. The threshold is 90% of the V8 heap
|
|
57
|
-
// limit (--max-old-space-size or default ~1.5-4GB depending on platform).
|
|
58
|
-
const MEMORY_CHECK_INTERVAL = 5; // check every 5 iterations
|
|
59
|
-
const MEMORY_PRESSURE_THRESHOLD = 0.85; // 85% of heap limit
|
|
60
|
-
function checkMemoryPressure() {
|
|
61
|
-
const mem = process.memoryUsage();
|
|
62
|
-
// v8.getHeapStatistics() gives heap_size_limit but requires import
|
|
63
|
-
// Use a conservative estimate: RSS > 3GB is danger zone on most systems
|
|
64
|
-
const heapMB = Math.round(mem.heapUsed / 1024 / 1024);
|
|
65
|
-
const rssMB = Math.round(mem.rss / 1024 / 1024);
|
|
66
|
-
// Try to get the actual V8 heap limit
|
|
67
|
-
let limitMB = 4096; // conservative default
|
|
68
|
-
try {
|
|
69
|
-
const v8 = require("node:v8");
|
|
70
|
-
const stats = v8.getHeapStatistics();
|
|
71
|
-
limitMB = Math.round(stats.heap_size_limit / 1024 / 1024);
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
limitMB = 4096; /* v8 stats unavailable — use conservative default */
|
|
75
|
-
}
|
|
76
|
-
const pct = heapMB / limitMB;
|
|
77
|
-
return { pressured: pct > MEMORY_PRESSURE_THRESHOLD, heapMB, limitMB, pct };
|
|
78
|
-
}
|
|
79
16
|
/**
|
|
80
17
|
* Main auto-mode execution loop. Iterates: derive → dispatch → guards →
|
|
81
18
|
* runUnit → finalize → repeat. Exits when s.active becomes false or a
|
|
@@ -87,13 +24,7 @@ function checkMemoryPressure() {
|
|
|
87
24
|
export async function autoLoop(ctx, pi, s, deps) {
|
|
88
25
|
debugLog("autoLoop", { phase: "enter" });
|
|
89
26
|
let iteration = 0;
|
|
90
|
-
|
|
91
|
-
const persisted = loadStuckState(s.basePath);
|
|
92
|
-
const loopState = {
|
|
93
|
-
recentUnits: persisted.recentUnits,
|
|
94
|
-
stuckRecoveryAttempts: persisted.stuckRecoveryAttempts,
|
|
95
|
-
consecutiveFinalizeTimeouts: 0,
|
|
96
|
-
};
|
|
27
|
+
const loopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
|
|
97
28
|
let consecutiveErrors = 0;
|
|
98
29
|
let consecutiveCooldowns = 0;
|
|
99
30
|
const recentErrorMessages = [];
|
|
@@ -113,19 +44,6 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
113
44
|
await deps.stopAuto(ctx, pi, `Safety: loop exceeded ${MAX_LOOP_ITERATIONS} iterations — possible runaway`);
|
|
114
45
|
break;
|
|
115
46
|
}
|
|
116
|
-
// ── Memory pressure check (#3331) ──
|
|
117
|
-
// Graceful shutdown before OOM killer sends SIGKILL.
|
|
118
|
-
if (iteration % MEMORY_CHECK_INTERVAL === 0) {
|
|
119
|
-
const mem = checkMemoryPressure();
|
|
120
|
-
debugLog("autoLoop", { phase: "memory-check", ...mem });
|
|
121
|
-
if (mem.pressured) {
|
|
122
|
-
logWarning("dispatch", `Memory pressure: ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%) — stopping auto-mode to prevent OOM kill`);
|
|
123
|
-
await deps.stopAuto(ctx, pi, `Memory pressure: heap at ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%). ` +
|
|
124
|
-
`Stopping gracefully to prevent OOM kill after ${iteration} iterations. ` +
|
|
125
|
-
`Resume with /gsd auto to continue from where you left off.`);
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
47
|
if (!s.cmdCtx) {
|
|
130
48
|
debugLog("autoLoop", { phase: "exit", reason: "no-cmdCtx" });
|
|
131
49
|
break;
|
|
@@ -244,7 +162,6 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
244
162
|
consecutiveCooldowns = 0;
|
|
245
163
|
recentErrorMessages.length = 0;
|
|
246
164
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
|
|
247
|
-
saveStuckState(s.basePath, loopState); // persist across session restarts (#3704)
|
|
248
165
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
249
166
|
if (reconcileResult.outcome === "milestone-complete") {
|
|
250
167
|
await deps.stopAuto(ctx, pi, "Workflow complete");
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
import { validatePlanBoundary, validateExecuteBoundary, validateCompleteBoundary, formatValidationIssues, } from "./observability-validator.js";
|
|
7
|
+
import { parseUnitId } from "./unit-id.js";
|
|
8
|
+
export async function collectObservabilityWarnings(ctx, basePath, unitType, unitId) {
|
|
9
|
+
// Hook units have custom artifacts — skip standard observability checks
|
|
10
|
+
if (unitType.startsWith("hook/"))
|
|
11
|
+
return [];
|
|
12
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
13
|
+
if (!mid || !sid)
|
|
14
|
+
return [];
|
|
15
|
+
let issues = [];
|
|
16
|
+
if (unitType === "plan-slice") {
|
|
17
|
+
issues = await validatePlanBoundary(basePath, mid, sid);
|
|
18
|
+
}
|
|
19
|
+
else if (unitType === "execute-task" && tid) {
|
|
20
|
+
issues = await validateExecuteBoundary(basePath, mid, sid, tid);
|
|
21
|
+
}
|
|
22
|
+
else if (unitType === "complete-slice") {
|
|
23
|
+
issues = await validateCompleteBoundary(basePath, mid, sid);
|
|
24
|
+
}
|
|
25
|
+
if (issues.length > 0) {
|
|
26
|
+
ctx.ui.notify(`Observability check (${unitType}) found ${issues.length} warning${issues.length === 1 ? "" : "s"}:\n${formatValidationIssues(issues)}`, "warning");
|
|
27
|
+
}
|
|
28
|
+
return issues;
|
|
29
|
+
}
|
|
30
|
+
export function buildObservabilityRepairBlock(issues) {
|
|
31
|
+
if (issues.length === 0)
|
|
32
|
+
return "";
|
|
33
|
+
const items = issues.map(issue => {
|
|
34
|
+
const fileName = issue.file.split("/").pop() || issue.file;
|
|
35
|
+
let line = `- **${fileName}**: ${issue.message}`;
|
|
36
|
+
if (issue.suggestion)
|
|
37
|
+
line += ` → ${issue.suggestion}`;
|
|
38
|
+
return line;
|
|
39
|
+
});
|
|
40
|
+
return [
|
|
41
|
+
"",
|
|
42
|
+
"---",
|
|
43
|
+
"",
|
|
44
|
+
"## Pre-flight: Observability gaps to fix FIRST",
|
|
45
|
+
"",
|
|
46
|
+
"The following issues were detected in plan/summary files for this unit.",
|
|
47
|
+
"**Read each flagged file, apply the fix described, then proceed with the unit.**",
|
|
48
|
+
"",
|
|
49
|
+
...items,
|
|
50
|
+
"",
|
|
51
|
+
"---",
|
|
52
|
+
"",
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
@@ -16,7 +16,6 @@ import { loadFile, parseSummary, resolveAllOverrides } from "./files.js";
|
|
|
16
16
|
import { loadPrompt } from "./prompt-loader.js";
|
|
17
17
|
import { resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveMilestoneFile, resolveTasksDir, buildTaskFileName, } from "./paths.js";
|
|
18
18
|
import { invalidateAllCaches } from "./cache.js";
|
|
19
|
-
import { rebuildState } from "./doctor.js";
|
|
20
19
|
import { parseUnitId } from "./unit-id.js";
|
|
21
20
|
import { closeoutUnit } from "./auto-unit-closeout.js";
|
|
22
21
|
import { autoCommitCurrentBranch, } from "./worktree.js";
|
|
@@ -289,11 +288,6 @@ export async function postUnitPreVerification(pctx, opts) {
|
|
|
289
288
|
debugLog("postUnit", { phase: "browser-teardown", status: "closed" });
|
|
290
289
|
}
|
|
291
290
|
});
|
|
292
|
-
// Keep the on-disk STATE.md aligned with the live derived state after
|
|
293
|
-
// ordinary unit completion, before any worktree state is synced back.
|
|
294
|
-
await runSafely("postUnit", "state-rebuild", async () => {
|
|
295
|
-
await rebuildState(s.basePath);
|
|
296
|
-
});
|
|
297
291
|
// Sync worktree state back to project root (skipped for lightweight sidecars)
|
|
298
292
|
if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
|
|
299
293
|
await runSafely("postUnit", "worktree-sync", () => {
|
|
@@ -425,13 +425,9 @@ function cleanupAfterLoopExit(ctx) {
|
|
|
425
425
|
/* best-effort — mirror stopAuto cleanup */
|
|
426
426
|
logWarning("session", `lock cleanup failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
427
427
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
ctx.ui.setStatus("gsd-auto", undefined);
|
|
432
|
-
ctx.ui.setWidget("gsd-progress", undefined);
|
|
433
|
-
ctx.ui.setFooter(undefined);
|
|
434
|
-
}
|
|
428
|
+
ctx.ui.setStatus("gsd-auto", undefined);
|
|
429
|
+
ctx.ui.setWidget("gsd-progress", undefined);
|
|
430
|
+
ctx.ui.setFooter(undefined);
|
|
435
431
|
// Restore CWD out of worktree back to original project root
|
|
436
432
|
if (s.originalBasePath) {
|
|
437
433
|
s.basePath = s.originalBasePath;
|
|
@@ -533,22 +529,7 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
533
529
|
catch (e) {
|
|
534
530
|
debugLog("stop-cleanup-worktree", { error: e instanceof Error ? e.message : String(e) });
|
|
535
531
|
}
|
|
536
|
-
// ── Step 5:
|
|
537
|
-
// rebuildState() calls deriveState() which needs the DB for authoritative
|
|
538
|
-
// state. Previously this ran after closeDatabase(), forcing a filesystem
|
|
539
|
-
// fallback that could disagree with the DB-backed dispatch decisions —
|
|
540
|
-
// a split-brain where dispatch says "blocked" but STATE.md shows work.
|
|
541
|
-
if (s.basePath) {
|
|
542
|
-
try {
|
|
543
|
-
await rebuildState(s.basePath);
|
|
544
|
-
}
|
|
545
|
-
catch (e) {
|
|
546
|
-
debugLog("stop-rebuild-state-failed", {
|
|
547
|
-
error: e instanceof Error ? e.message : String(e),
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
// ── Step 6: DB cleanup ──
|
|
532
|
+
// ── Step 5: DB cleanup ──
|
|
552
533
|
if (isDbAvailable()) {
|
|
553
534
|
try {
|
|
554
535
|
const { closeDatabase } = await import("./gsd-db.js");
|
|
@@ -560,7 +541,7 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
560
541
|
});
|
|
561
542
|
}
|
|
562
543
|
}
|
|
563
|
-
// ── Step
|
|
544
|
+
// ── Step 6: Restore basePath and chdir ──
|
|
564
545
|
try {
|
|
565
546
|
if (s.originalBasePath) {
|
|
566
547
|
s.basePath = s.originalBasePath;
|
|
@@ -576,7 +557,7 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
576
557
|
catch (e) {
|
|
577
558
|
debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
|
|
578
559
|
}
|
|
579
|
-
// ── Step
|
|
560
|
+
// ── Step 7: Ledger notification ──
|
|
580
561
|
try {
|
|
581
562
|
const ledger = getLedger();
|
|
582
563
|
if (ledger && ledger.units.length > 0) {
|
|
@@ -590,6 +571,17 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
590
571
|
catch (e) {
|
|
591
572
|
debugLog("stop-cleanup-ledger", { error: e instanceof Error ? e.message : String(e) });
|
|
592
573
|
}
|
|
574
|
+
// ── Step 8: Rebuild state ──
|
|
575
|
+
if (s.basePath) {
|
|
576
|
+
try {
|
|
577
|
+
await rebuildState(s.basePath);
|
|
578
|
+
}
|
|
579
|
+
catch (e) {
|
|
580
|
+
debugLog("stop-rebuild-state-failed", {
|
|
581
|
+
error: e instanceof Error ? e.message : String(e),
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
593
585
|
// ── Step 9: Cmux sidebar / event log ──
|
|
594
586
|
try {
|
|
595
587
|
clearCmuxSidebar(loadedPreferences);
|
|
@@ -1302,6 +1294,8 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
|
|
|
1302
1294
|
pi.sendMessage({ customType: "gsd-auto", content: hookPrompt, display: true }, { triggerTurn: true });
|
|
1303
1295
|
return true;
|
|
1304
1296
|
}
|
|
1297
|
+
// Direct phase dispatch → auto-direct-dispatch.ts
|
|
1298
|
+
export { dispatchDirectPhase } from "./auto-direct-dispatch.js";
|
|
1305
1299
|
// Re-export recovery functions for external consumers
|
|
1306
1300
|
export { buildLoopRemediationSteps, } from "./auto-recovery.js";
|
|
1307
1301
|
export { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";
|
|
@@ -172,10 +172,14 @@ export function registerHooks(pi) {
|
|
|
172
172
|
// Only gate-shaped ask_user_questions calls should block execution.
|
|
173
173
|
// The gate stays pending until the user selects the approval option.
|
|
174
174
|
if (event.toolName === "ask_user_questions") {
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
if (
|
|
178
|
-
|
|
175
|
+
const milestoneId = getDiscussionMilestoneId(discussionBasePath);
|
|
176
|
+
const inDiscussion = milestoneId !== null || isQueuePhaseActive();
|
|
177
|
+
if (inDiscussion) {
|
|
178
|
+
const questions = event.input?.questions ?? [];
|
|
179
|
+
const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
|
|
180
|
+
if (typeof questionId === "string") {
|
|
181
|
+
setPendingGate(questionId);
|
|
182
|
+
}
|
|
179
183
|
}
|
|
180
184
|
}
|
|
181
185
|
// ── Discussion gate enforcement: block tool calls while gate is pending ──
|
|
@@ -257,6 +261,8 @@ export function registerHooks(pi) {
|
|
|
257
261
|
return;
|
|
258
262
|
const milestoneId = getDiscussionMilestoneId(process.cwd());
|
|
259
263
|
const queueActive = isQueuePhaseActive();
|
|
264
|
+
if (!milestoneId && !queueActive)
|
|
265
|
+
return;
|
|
260
266
|
const details = event.details;
|
|
261
267
|
// ── Discussion gate enforcement: handle gate question responses ──
|
|
262
268
|
// If the result is cancelled or has no response, the pending gate stays active
|
|
@@ -287,16 +293,12 @@ export function registerHooks(pi) {
|
|
|
287
293
|
// Only unlock the gate if the user selected the first option (confirmation).
|
|
288
294
|
// Cross-references against the question's defined options to reject free-form "Other" text.
|
|
289
295
|
const answer = details.response?.answers?.[question.id];
|
|
290
|
-
const inferredMilestoneId = extractDepthVerificationMilestoneId(question.id) ?? milestoneId;
|
|
291
296
|
if (isDepthConfirmationAnswer(answer?.selected, question.options)) {
|
|
292
|
-
markDepthVerified(
|
|
293
|
-
clearPendingGate();
|
|
297
|
+
markDepthVerified(extractDepthVerificationMilestoneId(question.id) ?? milestoneId);
|
|
294
298
|
}
|
|
295
299
|
break;
|
|
296
300
|
}
|
|
297
301
|
}
|
|
298
|
-
if (!milestoneId && !queueActive)
|
|
299
|
-
return;
|
|
300
302
|
if (!milestoneId)
|
|
301
303
|
return;
|
|
302
304
|
const basePath = process.cwd();
|
|
@@ -61,9 +61,6 @@ export function parseDoctorArgs(args) {
|
|
|
61
61
|
const requestedScope = mode === "doctor" ? parts[0] : parts[1];
|
|
62
62
|
return { jsonMode, dryRun, fixFlag, includeBuild, includeTests, mode, requestedScope };
|
|
63
63
|
}
|
|
64
|
-
export function isDoctorHealActionable(issue) {
|
|
65
|
-
return issue.fixable && issue.severity !== "info";
|
|
66
|
-
}
|
|
67
64
|
export async function handleDoctor(args, ctx, pi) {
|
|
68
65
|
const { jsonMode, dryRun, fixFlag, includeBuild, includeTests, mode, requestedScope } = parseDoctorArgs(args);
|
|
69
66
|
const scope = await selectDoctorScope(projectRoot(), requestedScope);
|
|
@@ -91,7 +88,7 @@ export async function handleDoctor(args, ctx, pi) {
|
|
|
91
88
|
scope: effectiveScope,
|
|
92
89
|
includeWarnings: true,
|
|
93
90
|
});
|
|
94
|
-
const actionable = unresolved.filter(
|
|
91
|
+
const actionable = unresolved.filter(issue => issue.severity === "error");
|
|
95
92
|
if (actionable.length === 0) {
|
|
96
93
|
ctx.ui.notify("Doctor heal found nothing actionable to hand off to the LLM.", "info");
|
|
97
94
|
return;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { readFileSync, existsSync } from "node:fs";
|
|
16
16
|
import { resolve, sep } from "node:path";
|
|
17
|
-
import { readFrozenDefinition } from "./
|
|
17
|
+
import { readFrozenDefinition } from "./custom-workflow-engine.js";
|
|
18
18
|
/** Maximum characters per artifact to prevent context window blowout. */
|
|
19
19
|
const MAX_CONTEXT_CHARS = 10_000;
|
|
20
20
|
/**
|
|
@@ -13,13 +13,17 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { readFileSync } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
+
import { parse } from "yaml";
|
|
16
17
|
import { readGraph, writeGraph, getNextPendingStep, markStepComplete, expandIteration, } from "./graph.js";
|
|
17
18
|
import { injectContext } from "./context-injector.js";
|
|
18
|
-
import { readFrozenDefinition } from "./definition-io.js";
|
|
19
19
|
import { parseUnitId } from "./unit-id.js";
|
|
20
20
|
import { withFileLock } from "./file-lock.js";
|
|
21
|
-
|
|
22
|
-
export
|
|
21
|
+
/** Read and parse the frozen DEFINITION.yaml from a run directory. */
|
|
22
|
+
export function readFrozenDefinition(runDir) {
|
|
23
|
+
const defPath = join(runDir, "DEFINITION.yaml");
|
|
24
|
+
const raw = readFileSync(defPath, "utf-8");
|
|
25
|
+
return parse(raw, { schema: "core" });
|
|
26
|
+
}
|
|
23
27
|
export class CustomWorkflowEngine {
|
|
24
28
|
engineId = "custom";
|
|
25
29
|
runDir;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
let watcher = null;
|
|
3
|
+
let pending = new Map();
|
|
4
|
+
const EVENT_MAP = {
|
|
5
|
+
"settings.json": "settings-changed",
|
|
6
|
+
"auth.json": "auth-changed",
|
|
7
|
+
"models.json": "models-changed",
|
|
8
|
+
};
|
|
9
|
+
const EXTENSIONS_DIR = "extensions";
|
|
10
|
+
const IGNORED_PATTERNS = [
|
|
11
|
+
"**/sessions/**",
|
|
12
|
+
"**/*.tmp",
|
|
13
|
+
"**/*.swp",
|
|
14
|
+
"**/*~",
|
|
15
|
+
"**/.DS_Store",
|
|
16
|
+
];
|
|
17
|
+
const DEBOUNCE_MS = 300;
|
|
18
|
+
/**
|
|
19
|
+
* Start watching `agentDir` (e.g. `~/.gsd/agent/`) for config changes.
|
|
20
|
+
* Emits events on the supplied EventBus when watched files are modified.
|
|
21
|
+
*/
|
|
22
|
+
export async function startFileWatcher(agentDir, eventBus) {
|
|
23
|
+
if (watcher) {
|
|
24
|
+
await watcher.close();
|
|
25
|
+
}
|
|
26
|
+
const { watch } = await import("chokidar");
|
|
27
|
+
pending = new Map();
|
|
28
|
+
function debounceEmit(event) {
|
|
29
|
+
const existing = pending.get(event);
|
|
30
|
+
if (existing)
|
|
31
|
+
clearTimeout(existing);
|
|
32
|
+
pending.set(event, setTimeout(() => {
|
|
33
|
+
pending.delete(event);
|
|
34
|
+
eventBus.emit(event, { timestamp: Date.now() });
|
|
35
|
+
}, DEBOUNCE_MS));
|
|
36
|
+
}
|
|
37
|
+
function resolveEvent(filePath) {
|
|
38
|
+
const rel = relative(agentDir, filePath);
|
|
39
|
+
if (rel.startsWith(".."))
|
|
40
|
+
return null;
|
|
41
|
+
// Check direct file matches
|
|
42
|
+
for (const [file, event] of Object.entries(EVENT_MAP)) {
|
|
43
|
+
if (rel === file)
|
|
44
|
+
return event;
|
|
45
|
+
}
|
|
46
|
+
// Check extensions directory
|
|
47
|
+
if (rel.startsWith(EXTENSIONS_DIR + "/") || rel === EXTENSIONS_DIR) {
|
|
48
|
+
return "extensions-changed";
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
watcher = watch(agentDir, {
|
|
53
|
+
ignoreInitial: true,
|
|
54
|
+
depth: 2,
|
|
55
|
+
ignored: IGNORED_PATTERNS,
|
|
56
|
+
});
|
|
57
|
+
for (const eventType of ["add", "change", "unlink"]) {
|
|
58
|
+
watcher.on(eventType, (filePath) => {
|
|
59
|
+
const event = resolveEvent(filePath);
|
|
60
|
+
if (event)
|
|
61
|
+
debounceEmit(event);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Wait for watcher to be ready
|
|
65
|
+
await new Promise((resolve) => {
|
|
66
|
+
watcher.on("ready", resolve);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Stop the file watcher and clean up resources.
|
|
71
|
+
*/
|
|
72
|
+
export async function stopFileWatcher() {
|
|
73
|
+
for (const timer of pending.values())
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
pending.clear();
|
|
76
|
+
if (watcher) {
|
|
77
|
+
await watcher.close();
|
|
78
|
+
watcher = null;
|
|
79
|
+
}
|
|
80
|
+
}
|