gsd-pi 2.80.0-dev.e146beb20 → 2.80.0-dev.e6c48c3af
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +59 -21
- package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +17 -2
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
- package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
- package/dist/resources/extensions/gsd/auto.js +84 -5
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +75 -4
- package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
- package/dist/resources/extensions/gsd/context-budget.js +37 -2
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
- package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
- package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
- package/dist/resources/extensions/gsd/git-service.js +36 -4
- package/dist/resources/extensions/gsd/gsd-db.js +46 -13
- package/dist/resources/extensions/gsd/guided-flow.js +33 -4
- package/dist/resources/extensions/gsd/memory-store.js +69 -12
- package/dist/resources/extensions/gsd/migrate/command.js +40 -1
- package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
- package/dist/resources/extensions/gsd/pre-execution-checks.js +7 -0
- package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
- package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/dist/resources/extensions/gsd/quick.js +34 -2
- package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
- package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
- package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
- package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
- package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +33 -17
- 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 +16 -16
- 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.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 +16 -16
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- 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/package.json +3 -3
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +22 -17
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
- package/packages/mcp-server/src/workflow-tools.ts +30 -16
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +15 -0
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +12 -3
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +12 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.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 +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
- package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +18 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +14 -3
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
- package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
- package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +12 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +18 -8
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +20 -8
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
- package/src/resources/extensions/gsd/auto/phases.ts +85 -35
- package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +22 -2
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
- package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
- package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
- package/src/resources/extensions/gsd/auto.ts +96 -4
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +88 -4
- package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
- package/src/resources/extensions/gsd/context-budget.ts +44 -2
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
- package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
- package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
- package/src/resources/extensions/gsd/git-service.ts +46 -8
- package/src/resources/extensions/gsd/gsd-db.ts +50 -13
- package/src/resources/extensions/gsd/guided-flow.ts +49 -4
- package/src/resources/extensions/gsd/memory-store.ts +77 -12
- package/src/resources/extensions/gsd/migrate/command.ts +47 -1
- package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
- package/src/resources/extensions/gsd/pre-execution-checks.ts +7 -0
- package/src/resources/extensions/gsd/preferences-types.ts +1 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
- package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/src/resources/extensions/gsd/quick.ts +37 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +215 -1
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +166 -4
- package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +14 -1
- package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +239 -1
- package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
- package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +63 -1
- package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
- package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
- package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
- package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
- package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
- package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +36 -15
- /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 4dQ9NTZJ8pEvFwBgDUX93}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 4dQ9NTZJ8pEvFwBgDUX93}/_ssgManifest.js +0 -0
|
@@ -3,21 +3,34 @@
|
|
|
3
3
|
// Exposes the Context Mode runtime tools in-process. Default-on; opt out with
|
|
4
4
|
// `context_mode.enabled: false` in preferences.
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
6
|
+
async function loadContextModePreferences(baseDir) {
|
|
7
|
+
const [{ loadEffectiveGSDPreferences }, { logWarning }] = await Promise.all([
|
|
8
|
+
import("../preferences.js"),
|
|
9
|
+
import("../workflow-logger.js"),
|
|
10
|
+
]);
|
|
11
|
+
try {
|
|
12
|
+
return loadEffectiveGSDPreferences(baseDir)?.preferences ?? null;
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
logWarning("tool", `Context Mode tool could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
6
19
|
export function registerExecTools(pi) {
|
|
7
20
|
pi.registerTool({
|
|
8
21
|
name: "gsd_exec",
|
|
9
22
|
label: "Exec (Sandboxed)",
|
|
10
|
-
description: "Run a short script (bash/node/python) in a subprocess.
|
|
23
|
+
description: "Run a short script (bash/node/python) in a subprocess. Capped stdout/stderr and metadata persist to " +
|
|
11
24
|
".gsd/exec/<id>.{stdout,stderr,meta.json}; only a short digest returns in context. Use " +
|
|
12
25
|
"this instead of reading many files or emitting large tool outputs — e.g. have the script " +
|
|
13
26
|
"count/grep/summarize and log the finding. Enabled by default; opt out via " +
|
|
14
27
|
"preferences.context_mode.enabled=false.",
|
|
15
|
-
promptSnippet: "Run a bash/node/python script in a sandbox;
|
|
28
|
+
promptSnippet: "Run a bash/node/python script in a sandbox; capped output is saved to disk and only a digest returns",
|
|
16
29
|
promptGuidelines: [
|
|
17
30
|
"Prefer gsd_exec for analyses that would otherwise read >3 files or produce large tool output.",
|
|
18
31
|
"Write scripts that log the finding (counts, matches, summaries) rather than raw dumps.",
|
|
19
32
|
"The digest is the last ~300 chars of stdout — size your log output accordingly.",
|
|
20
|
-
"Need
|
|
33
|
+
"Need persisted output? Read the stdout_path returned in details (file on local disk).",
|
|
21
34
|
],
|
|
22
35
|
parameters: Type.Object({
|
|
23
36
|
runtime: Type.Union([Type.Literal("bash"), Type.Literal("node"), Type.Literal("python")], { description: "Interpreter: bash (-c), node (-e), or python3 (-c)." }),
|
|
@@ -30,21 +43,11 @@ export function registerExecTools(pi) {
|
|
|
30
43
|
})),
|
|
31
44
|
}),
|
|
32
45
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
import("../preferences.js"),
|
|
36
|
-
import("../workflow-logger.js"),
|
|
37
|
-
]);
|
|
38
|
-
let prefs = null;
|
|
39
|
-
try {
|
|
40
|
-
prefs = loadEffectiveGSDPreferences();
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
logWarning("tool", `gsd_exec could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
|
|
44
|
-
}
|
|
46
|
+
const { executeGsdExec } = await import("../tools/exec-tool.js");
|
|
47
|
+
const baseDir = process.cwd();
|
|
45
48
|
return executeGsdExec(params, {
|
|
46
|
-
baseDir
|
|
47
|
-
preferences:
|
|
49
|
+
baseDir,
|
|
50
|
+
preferences: await loadContextModePreferences(baseDir),
|
|
48
51
|
});
|
|
49
52
|
},
|
|
50
53
|
});
|
|
@@ -56,7 +59,7 @@ export function registerExecTools(pi) {
|
|
|
56
59
|
promptSnippet: "Search prior gsd_exec runs by substring, runtime, or failing-only filter",
|
|
57
60
|
promptGuidelines: [
|
|
58
61
|
"Use this before re-running an expensive analysis — the prior run's stdout file may still answer.",
|
|
59
|
-
"The preview shows the trailing ~300 chars of stdout; read stdout_path for
|
|
62
|
+
"The preview shows the trailing ~300 chars of stdout; read stdout_path for persisted output.",
|
|
60
63
|
],
|
|
61
64
|
parameters: Type.Object({
|
|
62
65
|
query: Type.Optional(Type.String({ description: "Substring matched against id and purpose (case-insensitive)." })),
|
|
@@ -68,8 +71,10 @@ export function registerExecTools(pi) {
|
|
|
68
71
|
}),
|
|
69
72
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
70
73
|
const { executeExecSearch } = await import("../tools/exec-search-tool.js");
|
|
74
|
+
const baseDir = process.cwd();
|
|
71
75
|
return executeExecSearch(params, {
|
|
72
|
-
baseDir
|
|
76
|
+
baseDir,
|
|
77
|
+
preferences: await loadContextModePreferences(baseDir),
|
|
73
78
|
});
|
|
74
79
|
},
|
|
75
80
|
});
|
|
@@ -87,8 +92,10 @@ export function registerExecTools(pi) {
|
|
|
87
92
|
parameters: Type.Object({}),
|
|
88
93
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
89
94
|
const { executeResume } = await import("../tools/resume-tool.js");
|
|
95
|
+
const baseDir = process.cwd();
|
|
90
96
|
return executeResume(params, {
|
|
91
|
-
baseDir
|
|
97
|
+
baseDir,
|
|
98
|
+
preferences: await loadContextModePreferences(baseDir),
|
|
92
99
|
});
|
|
93
100
|
},
|
|
94
101
|
});
|
|
@@ -23,6 +23,7 @@ import { approvalGateIdForUnit, isExplicitApprovalResponse, shouldPauseForUserAp
|
|
|
23
23
|
// printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
|
|
24
24
|
let isFirstSession = true;
|
|
25
25
|
let approvalQuestionAbortInFlight = false;
|
|
26
|
+
let deferredApprovalGate = null;
|
|
26
27
|
async function deriveGsdState(basePath) {
|
|
27
28
|
const { deriveState } = await import("../state.js");
|
|
28
29
|
return deriveState(basePath);
|
|
@@ -52,6 +53,62 @@ async function applyDisabledModelProviderPolicy(ctx) {
|
|
|
52
53
|
// Non-fatal: keep default provider visibility if preferences cannot be loaded.
|
|
53
54
|
}
|
|
54
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Bridge `context_management.compaction_threshold_percent` from GSD preferences
|
|
58
|
+
* into the agent's runtime compaction settings (#5475). The preference is
|
|
59
|
+
* validated to (0.5, 0.95) at load time, but defense-in-depth normalization
|
|
60
|
+
* here protects against a stale or hand-edited prefs file. Calling with
|
|
61
|
+
* `undefined` clears any prior override so a removed preference does not leak.
|
|
62
|
+
*/
|
|
63
|
+
async function applyCompactionThresholdOverride(ctx) {
|
|
64
|
+
try {
|
|
65
|
+
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
66
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
67
|
+
const raw = prefs?.preferences.context_management?.compaction_threshold_percent;
|
|
68
|
+
const value = typeof raw === "number" && Number.isFinite(raw) && raw > 0 && raw < 1 ? raw : undefined;
|
|
69
|
+
ctx.setCompactionThresholdOverride(value);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Non-fatal: leave any existing override in place.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function clearDeferredApprovalGate(basePath) {
|
|
76
|
+
if (!basePath || deferredApprovalGate?.basePath === basePath) {
|
|
77
|
+
deferredApprovalGate = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function deferApprovalGate(gateId, basePath) {
|
|
81
|
+
deferredApprovalGate = { gateId, basePath };
|
|
82
|
+
}
|
|
83
|
+
function activateDeferredApprovalGate(basePath) {
|
|
84
|
+
if (deferredApprovalGate?.basePath !== basePath)
|
|
85
|
+
return;
|
|
86
|
+
setPendingGate(deferredApprovalGate.gateId, basePath);
|
|
87
|
+
deferredApprovalGate = null;
|
|
88
|
+
}
|
|
89
|
+
function isContextDraftSummarySave(toolName, input) {
|
|
90
|
+
if (toolName !== "gsd_summary_save" && toolName !== "summary_save")
|
|
91
|
+
return false;
|
|
92
|
+
if (!input || typeof input !== "object")
|
|
93
|
+
return false;
|
|
94
|
+
return input.artifact_type === "CONTEXT-DRAFT";
|
|
95
|
+
}
|
|
96
|
+
function shouldBlockDeferredApprovalTool(toolName, input, basePath) {
|
|
97
|
+
if (deferredApprovalGate?.basePath !== basePath)
|
|
98
|
+
return { block: false };
|
|
99
|
+
if (toolName === "ask_user_questions")
|
|
100
|
+
return { block: false };
|
|
101
|
+
if (isContextDraftSummarySave(toolName, input))
|
|
102
|
+
return { block: false };
|
|
103
|
+
return {
|
|
104
|
+
block: true,
|
|
105
|
+
reason: [
|
|
106
|
+
`HARD BLOCK: Approval question "${deferredApprovalGate.gateId}" has been shown to the user.`,
|
|
107
|
+
`Only CONTEXT-DRAFT persistence may finish in this same assistant turn.`,
|
|
108
|
+
`Wait for the user's answer before calling additional tools.`,
|
|
109
|
+
].join(" "),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
55
112
|
export function resolveNotificationStoreBasePath(cwd = process.cwd()) {
|
|
56
113
|
return resolveWorktreeProjectRoot(cwd);
|
|
57
114
|
}
|
|
@@ -98,9 +155,11 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
98
155
|
resetWriteGateState(process.cwd());
|
|
99
156
|
resetToolCallLoopGuard();
|
|
100
157
|
approvalQuestionAbortInFlight = false;
|
|
158
|
+
clearDeferredApprovalGate();
|
|
101
159
|
await resetAskUserQuestionsTurnCache();
|
|
102
160
|
await syncServiceTierStatus(ctx);
|
|
103
161
|
await applyDisabledModelProviderPolicy(ctx);
|
|
162
|
+
await applyCompactionThresholdOverride(ctx);
|
|
104
163
|
// Skip MCP auto-prep when running inside an auto-worktree (see session_switch below).
|
|
105
164
|
const { isInAutoWorktree } = await import("../auto-worktree.js");
|
|
106
165
|
if (!isInAutoWorktree(process.cwd())) {
|
|
@@ -145,10 +204,12 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
145
204
|
initSessionNotifications(ctx);
|
|
146
205
|
resetWriteGateState(process.cwd());
|
|
147
206
|
resetToolCallLoopGuard();
|
|
207
|
+
clearDeferredApprovalGate();
|
|
148
208
|
await resetAskUserQuestionsTurnCache();
|
|
149
209
|
clearDiscussionFlowState(process.cwd());
|
|
150
210
|
await syncServiceTierStatus(ctx);
|
|
151
211
|
await applyDisabledModelProviderPolicy(ctx);
|
|
212
|
+
await applyCompactionThresholdOverride(ctx);
|
|
152
213
|
// Skip MCP auto-prep when running inside an auto-worktree. The worktree
|
|
153
214
|
// already has .mcp.json from createAutoWorktree, and re-running the writer
|
|
154
215
|
// post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
|
|
@@ -180,6 +241,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
180
241
|
markDepthVerified(milestoneId, beforeAgentBasePath);
|
|
181
242
|
clearPendingGate(beforeAgentBasePath);
|
|
182
243
|
}
|
|
244
|
+
clearDeferredApprovalGate(beforeAgentBasePath);
|
|
183
245
|
// GSD's own context injection (existing behavior — unchanged).
|
|
184
246
|
const { buildBeforeAgentStartResult } = await import("./system-context.js");
|
|
185
247
|
const gsdResult = await buildBeforeAgentStartResult(event, ctx);
|
|
@@ -223,7 +285,12 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
223
285
|
resetToolCallLoopGuard();
|
|
224
286
|
await resetAskUserQuestionsTurnCache();
|
|
225
287
|
const { handleAgentEnd } = await import("./agent-end-recovery.js");
|
|
226
|
-
|
|
288
|
+
try {
|
|
289
|
+
await handleAgentEnd(pi, event, ctx);
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
activateDeferredApprovalGate(process.cwd());
|
|
293
|
+
}
|
|
227
294
|
});
|
|
228
295
|
// Squash-merge quick-task branch back to the original branch after the
|
|
229
296
|
// agent turn completes (#2668). cleanupQuickBranch is a no-op when no
|
|
@@ -321,11 +388,12 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
321
388
|
return;
|
|
322
389
|
const gateId = approvalGateIdForUnit(unitType, unitId);
|
|
323
390
|
if (gateId)
|
|
324
|
-
|
|
391
|
+
deferApprovalGate(gateId, process.cwd());
|
|
325
392
|
approvalQuestionAbortInFlight = true;
|
|
326
393
|
ctx.ui.notify(`${unitType}${unitId ? ` ${unitId}` : ""} is waiting for your approval - pausing before more tool calls run.`, "info");
|
|
327
|
-
// The pending gate
|
|
328
|
-
//
|
|
394
|
+
// The durable pending gate is activated at agent_end so same-turn
|
|
395
|
+
// CONTEXT-DRAFT persistence can finish after the text boundary streams.
|
|
396
|
+
// The tool_call hook below still blocks non-draft tools in this turn.
|
|
329
397
|
// Aborting mid-stream eats the model's question text on external CLI
|
|
330
398
|
// providers (Claude Code SDK) because lastTextContent isn't populated
|
|
331
399
|
// from in-flight builder state — the user only ever sees "Claude Code
|
|
@@ -356,6 +424,9 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
356
424
|
if (loopCheck.block) {
|
|
357
425
|
return { block: true, reason: loopCheck.reason };
|
|
358
426
|
}
|
|
427
|
+
const deferredGateGuard = shouldBlockDeferredApprovalTool(toolName, event.input, discussionBasePath);
|
|
428
|
+
if (deferredGateGuard.block)
|
|
429
|
+
return deferredGateGuard;
|
|
359
430
|
// ── Discussion gate enforcement: track pending gate questions ─────────
|
|
360
431
|
// Only gate-shaped ask_user_questions calls should block execution.
|
|
361
432
|
// The gate stays pending until the user selects the approval option.
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* #2909: Adds a fast-path git status check before milestone completion merges.
|
|
5
5
|
* When the working tree is dirty the user is warned and changes are auto-stashed
|
|
6
|
-
* so the merge can proceed cleanly.
|
|
7
|
-
* restores the stashed changes.
|
|
6
|
+
* so the merge can proceed cleanly. After the merge completes, postflightPopStash
|
|
7
|
+
* restores the stashed changes and reports whether manual recovery is needed.
|
|
8
8
|
*
|
|
9
9
|
* Design constraints (from Trek-e approval):
|
|
10
10
|
* - Warn the user before stashing (no silent surprises)
|
|
11
11
|
* - git stash push / git stash pop only — no custom stash management layer
|
|
12
|
-
* - Stash/pop errors are logged but MUST NOT block the merge
|
|
12
|
+
* - Stash/pop errors are logged but MUST NOT block the merge itself
|
|
13
13
|
* - Fast-path status check — clean trees pay no extra cost
|
|
14
14
|
*/
|
|
15
15
|
import { execFileSync } from "node:child_process";
|
|
@@ -98,7 +98,8 @@ export function preflightCleanRoot(basePath, milestoneId, notify) {
|
|
|
98
98
|
*
|
|
99
99
|
* Only called when preflightCleanRoot returned stashPushed=true.
|
|
100
100
|
* Any pop error (e.g. conflict) is logged and notified but does NOT throw —
|
|
101
|
-
* the merge already completed successfully.
|
|
101
|
+
* the merge already completed successfully. Callers must treat
|
|
102
|
+
* needsManualRecovery=true as a dirty workspace stop, not a clean completion.
|
|
102
103
|
*/
|
|
103
104
|
export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
|
|
104
105
|
let stashRef = null;
|
|
@@ -108,7 +109,11 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
|
|
|
108
109
|
const msg = `No matching GSD preflight stash found for milestone ${milestoneId}; leaving stash list untouched.`;
|
|
109
110
|
logWarning("preflight", msg);
|
|
110
111
|
notify(msg, "warning");
|
|
111
|
-
return
|
|
112
|
+
return {
|
|
113
|
+
restored: false,
|
|
114
|
+
needsManualRecovery: true,
|
|
115
|
+
message: msg,
|
|
116
|
+
};
|
|
112
117
|
}
|
|
113
118
|
execFileSync("git", ["stash", "pop", stashRef], {
|
|
114
119
|
cwd: basePath,
|
|
@@ -116,7 +121,14 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
|
|
|
116
121
|
encoding: "utf-8",
|
|
117
122
|
env: GIT_NO_PROMPT_ENV,
|
|
118
123
|
});
|
|
119
|
-
|
|
124
|
+
const msg = `Restored stashed changes after milestone ${milestoneId} merge.`;
|
|
125
|
+
notify(msg, "info");
|
|
126
|
+
return {
|
|
127
|
+
restored: true,
|
|
128
|
+
needsManualRecovery: false,
|
|
129
|
+
message: msg,
|
|
130
|
+
stashRef,
|
|
131
|
+
};
|
|
120
132
|
}
|
|
121
133
|
catch (err) {
|
|
122
134
|
// Pop conflicts mean the merged code collides with the stashed changes.
|
|
@@ -127,5 +139,11 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
|
|
|
127
139
|
const msg = `git stash pop ${stashRef ?? ""}`.trim() + ` failed after merge of milestone ${milestoneId}: ${err instanceof Error ? err.message : String(err)}. ${restoreHint}`;
|
|
128
140
|
logWarning("preflight", msg);
|
|
129
141
|
notify(msg, "warning");
|
|
142
|
+
return {
|
|
143
|
+
restored: false,
|
|
144
|
+
needsManualRecovery: true,
|
|
145
|
+
message: msg,
|
|
146
|
+
...(stashRef ? { stashRef } : {}),
|
|
147
|
+
};
|
|
130
148
|
}
|
|
131
149
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @see D001 (module location), D002 (200K fallback), D003 (section-boundary truncation)
|
|
9
9
|
*/
|
|
10
|
-
import { getCharsPerToken } from "./token-counter.js";
|
|
10
|
+
import { getCharsPerToken, isAccurateCountingAvailable, countTokensSync, } from "./token-counter.js";
|
|
11
11
|
// ─── Budget ratio constants ──────────────────────────────────────────────────
|
|
12
12
|
// Percentages of total context window allocated to each budget category.
|
|
13
13
|
// These are applied after tokens→chars conversion.
|
|
@@ -23,6 +23,22 @@ const CHARS_PER_TOKEN = 4;
|
|
|
23
23
|
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
24
24
|
/** Conservative effective context for Claude Code subscription routing (#4676) */
|
|
25
25
|
const CLAUDE_CODE_EFFECTIVE_CONTEXT_WINDOW = 200_000;
|
|
26
|
+
/**
|
|
27
|
+
* Cached empirical chars-per-token from a tiktoken probe, keyed by provider.
|
|
28
|
+
* countTokensSync's fallback path is provider-aware, so we cache per-provider
|
|
29
|
+
* to preserve that distinction once the encoder warms. The cl100k_base encoder
|
|
30
|
+
* itself gives a stable ratio for ASCII English so a single probe per provider
|
|
31
|
+
* key is sufficient. Empty map means "not yet probed" or "encoder unavailable".
|
|
32
|
+
*/
|
|
33
|
+
const _empiricalCharsPerTokenByProvider = new Map();
|
|
34
|
+
/**
|
|
35
|
+
* Test hook — clears the empirical chars-per-token cache so test cases that
|
|
36
|
+
* assert against the static char-ratio fallback aren't polluted by a prior
|
|
37
|
+
* tiktoken-warmed run in the same process. Production code must not call this.
|
|
38
|
+
*/
|
|
39
|
+
export function _resetEmpiricalCacheForTest() {
|
|
40
|
+
_empiricalCharsPerTokenByProvider.clear();
|
|
41
|
+
}
|
|
26
42
|
/** Percentage of context consumed before suggesting a continue-here checkpoint */
|
|
27
43
|
const CONTINUE_THRESHOLD_PERCENT = 70;
|
|
28
44
|
// ─── Task count bounds ───────────────────────────────────────────────────────
|
|
@@ -46,7 +62,26 @@ const TASK_COUNT_TIERS = [
|
|
|
46
62
|
export function computeBudgets(contextWindow, provider) {
|
|
47
63
|
const effectiveWindow = contextWindow > 0 ? contextWindow : DEFAULT_CONTEXT_WINDOW;
|
|
48
64
|
const charsPerToken = provider ? getCharsPerToken(provider) : CHARS_PER_TOKEN;
|
|
49
|
-
|
|
65
|
+
// Prefer the tiktoken encoder for total-char estimation when it has been
|
|
66
|
+
// warmed (initTokenCounter resolved). The cl100k_base ratio is stable for
|
|
67
|
+
// ASCII English, so probe once per provider and cache — computeBudgets is
|
|
68
|
+
// called multiple times per prompt build and the probe encode is otherwise
|
|
69
|
+
// wasted work.
|
|
70
|
+
let totalChars;
|
|
71
|
+
if (isAccurateCountingAvailable()) {
|
|
72
|
+
const providerKey = provider ?? "__default__";
|
|
73
|
+
let empirical = _empiricalCharsPerTokenByProvider.get(providerKey);
|
|
74
|
+
if (empirical === undefined) {
|
|
75
|
+
const probe = "the quick brown fox jumps over the lazy dog ".repeat(64);
|
|
76
|
+
const probeTokens = countTokensSync(probe, provider);
|
|
77
|
+
empirical = probeTokens > 0 ? probe.length / probeTokens : charsPerToken;
|
|
78
|
+
_empiricalCharsPerTokenByProvider.set(providerKey, empirical);
|
|
79
|
+
}
|
|
80
|
+
totalChars = effectiveWindow * empirical;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
totalChars = effectiveWindow * charsPerToken;
|
|
84
|
+
}
|
|
50
85
|
return {
|
|
51
86
|
summaryBudgetChars: Math.floor(totalChars * SUMMARY_RATIO),
|
|
52
87
|
inlineContextBudgetChars: Math.floor(totalChars * INLINE_CONTEXT_RATIO),
|
|
@@ -253,6 +253,45 @@ export function markCanceled(dispatchId, reason) {
|
|
|
253
253
|
SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
|
|
254
254
|
WHERE id = :id AND status IN ('pending','claimed','running')`).run({ ":id": dispatchId, ":ended_at": now, ":reason": reason });
|
|
255
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Best-effort signal/crash cleanup: cancel the latest active dispatch owned by
|
|
258
|
+
* a worker when the process is exiting before the normal loop can settle it.
|
|
259
|
+
*/
|
|
260
|
+
export function markLatestActiveForWorkerCanceled(workerId, reason) {
|
|
261
|
+
if (!isDbAvailable())
|
|
262
|
+
return false;
|
|
263
|
+
const now = new Date().toISOString();
|
|
264
|
+
const db = _getAdapter();
|
|
265
|
+
const result = transaction(() => {
|
|
266
|
+
return db.prepare(`UPDATE unit_dispatches
|
|
267
|
+
SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
|
|
268
|
+
WHERE id = (
|
|
269
|
+
SELECT id FROM unit_dispatches
|
|
270
|
+
WHERE worker_id = :worker_id
|
|
271
|
+
AND status IN ('pending','claimed','running')
|
|
272
|
+
ORDER BY id DESC
|
|
273
|
+
LIMIT 1
|
|
274
|
+
)`).run({
|
|
275
|
+
":ended_at": now,
|
|
276
|
+
":reason": reason,
|
|
277
|
+
":worker_id": workerId,
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
const changes = typeof result.changes === "number"
|
|
281
|
+
? result.changes
|
|
282
|
+
: 0;
|
|
283
|
+
if (changes <= 0)
|
|
284
|
+
return false;
|
|
285
|
+
insertAuditEvent({
|
|
286
|
+
eventId: randomUUID(),
|
|
287
|
+
traceId: workerId,
|
|
288
|
+
category: "orchestration",
|
|
289
|
+
type: "dispatch-canceled",
|
|
290
|
+
ts: now,
|
|
291
|
+
payload: { workerId, reason },
|
|
292
|
+
});
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
256
295
|
/**
|
|
257
296
|
* Fetch the most recent N dispatches for a unit. Used by recordDispatchClaim
|
|
258
297
|
* callers to compute attempt_n and by detect-stuck.ts (B3) to consult
|
|
@@ -46,7 +46,8 @@ export function createBaseSchemaObjects(db, hooks) {
|
|
|
46
46
|
slice_id TEXT DEFAULT NULL,
|
|
47
47
|
task_id TEXT DEFAULT NULL,
|
|
48
48
|
full_content TEXT NOT NULL DEFAULT '',
|
|
49
|
-
imported_at TEXT NOT NULL DEFAULT ''
|
|
49
|
+
imported_at TEXT NOT NULL DEFAULT '',
|
|
50
|
+
content_hash TEXT DEFAULT NULL
|
|
50
51
|
)
|
|
51
52
|
`);
|
|
52
53
|
db.exec(`
|
|
@@ -64,7 +65,8 @@ export function createBaseSchemaObjects(db, hooks) {
|
|
|
64
65
|
hit_count INTEGER NOT NULL DEFAULT 0,
|
|
65
66
|
scope TEXT NOT NULL DEFAULT 'project',
|
|
66
67
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
67
|
-
structured_fields TEXT DEFAULT NULL
|
|
68
|
+
structured_fields TEXT DEFAULT NULL,
|
|
69
|
+
last_hit_at TEXT DEFAULT NULL
|
|
68
70
|
)
|
|
69
71
|
`);
|
|
70
72
|
db.exec(`
|
|
@@ -379,6 +379,12 @@ export function applyMigrationV26MilestoneCommitAttributions(db) {
|
|
|
379
379
|
`);
|
|
380
380
|
db.exec("CREATE INDEX IF NOT EXISTS idx_milestone_commit_attr_milestone ON milestone_commit_attributions(milestone_id)");
|
|
381
381
|
}
|
|
382
|
+
export function applyMigrationV27ArtifactHash(db) {
|
|
383
|
+
ensureColumn(db, "artifacts", "content_hash", "ALTER TABLE artifacts ADD COLUMN content_hash TEXT DEFAULT NULL");
|
|
384
|
+
}
|
|
385
|
+
export function applyMigrationV28MemoryLastHitAt(db) {
|
|
386
|
+
ensureColumn(db, "memories", "last_hit_at", "ALTER TABLE memories ADD COLUMN last_hit_at TEXT DEFAULT NULL");
|
|
387
|
+
}
|
|
382
388
|
export function applyMigrationV22QualityGateRepair(db, hooks) {
|
|
383
389
|
const qgInfo = db.prepare("PRAGMA table_info(quality_gates)").all();
|
|
384
390
|
const taskIdCol = qgInfo.find((r) => r["name"] === "task_id");
|
|
@@ -13,6 +13,7 @@ import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
|
13
13
|
import { gsdRoot } from "./paths.js";
|
|
14
14
|
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
15
15
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
16
|
+
import { logWarning } from "./workflow-logger.js";
|
|
16
17
|
import { detectWorktreeName, } from "./worktree.js";
|
|
17
18
|
import { SLICE_BRANCH_RE, QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
|
|
18
19
|
import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAllWithExclusions, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, nativeAddPaths, nativeResetSoft, nativeCommitSubject, _resetHasChangesCache, } from "./native-git-bridge.js";
|
|
@@ -535,14 +536,45 @@ export class GitServiceImpl {
|
|
|
535
536
|
if (keyFiles.length === 0)
|
|
536
537
|
return false;
|
|
537
538
|
const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
|
|
538
|
-
const
|
|
539
|
+
const normalized = keyFiles
|
|
539
540
|
.map(file => normalizeRepoRelativePath(this.basePath, file))
|
|
540
541
|
.filter((file) => file !== null)
|
|
541
|
-
.filter(file => !isExcludedScopedPath(file, allExclusions))
|
|
542
|
+
.filter(file => !isExcludedScopedPath(file, allExclusions));
|
|
543
|
+
// Drop entries that don't exist on disk. The LLM occasionally lists files
|
|
544
|
+
// it intended to write but didn't (or names them with wrong casing/path).
|
|
545
|
+
// Pre-`b304f738b` `git add -A` swallowed these silently; the scoped
|
|
546
|
+
// pathspec form passes each path explicitly, so a single bad entry made
|
|
547
|
+
// the whole commit fail (see #5500). Filter so valid paths still commit.
|
|
548
|
+
const missing = [];
|
|
549
|
+
const existing = [];
|
|
550
|
+
for (const path of normalized) {
|
|
551
|
+
if (existsSync(join(this.basePath, path))) {
|
|
552
|
+
existing.push(path);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
missing.push(path);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (missing.length > 0) {
|
|
559
|
+
logWarning("engine", `scoped stage: dropping ${missing.length} non-existent keyFile(s) from task commit: ${missing.join(", ")}`, { file: "git-service.ts" });
|
|
560
|
+
}
|
|
561
|
+
const paths = Array.from(new Set(existing));
|
|
542
562
|
if (paths.length === 0)
|
|
543
563
|
return false;
|
|
544
|
-
|
|
545
|
-
|
|
564
|
+
try {
|
|
565
|
+
nativeAddPaths(this.basePath, paths);
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
// Defense-in-depth: even after existence filtering, libgit2/git can
|
|
570
|
+
// still reject paths (gitignore matches, case-only differences on
|
|
571
|
+
// case-insensitive FS, submodule boundaries). Returning false lets
|
|
572
|
+
// autoCommit fall through to smartStage so the commit still goes out
|
|
573
|
+
// — restoring the resilience the unscoped path used to provide.
|
|
574
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
575
|
+
logWarning("engine", `scoped stage failed (${msg}); falling back to smartStage`, { file: "git-service.ts" });
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
546
578
|
}
|
|
547
579
|
/** Tracks whether runtime file cleanup has run this session. */
|
|
548
580
|
_runtimeFilesCleanedUp = false;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// intentionally independent store for cross-worktree claim races and is
|
|
23
23
|
// excluded from this invariant.
|
|
24
24
|
import { createRequire } from "node:module";
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
25
26
|
import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
26
27
|
import { dirname } from "node:path";
|
|
27
28
|
import { GSDError, GSD_STALE_STATE } from "./errors.js";
|
|
@@ -36,7 +37,7 @@ import { rowToActiveDecision, rowToActiveRequirement, rowToDecision, rowToRequir
|
|
|
36
37
|
import { rowToGate } from "./db-gate-rows.js";
|
|
37
38
|
import { rowToArtifact, rowToMilestone } from "./db-milestone-artifact-rows.js";
|
|
38
39
|
import { backupDatabaseBeforeMigration } from "./db-migration-backup.js";
|
|
39
|
-
import { applyMigrationV2Artifacts, applyMigrationV3Memories, applyMigrationV4DecisionMadeBy, applyMigrationV5HierarchyTables, applyMigrationV6SliceSummaries, applyMigrationV7Dependencies, applyMigrationV8PlanningFields, applyMigrationV9Ordering, applyMigrationV10ReplanTrigger, applyMigrationV11TaskPlanning, applyMigrationV12QualityGates, applyMigrationV13HotPathIndexes, applyMigrationV14SliceDependencies, applyMigrationV15AuditTables, applyMigrationV16EscalationSource, applyMigrationV17TaskEscalation, applyMigrationV18MemorySources, applyMigrationV19MemoryFts, applyMigrationV20MemoryRelations, applyMigrationV21StructuredMemories, applyMigrationV22QualityGateRepair, applyMigrationV23MilestoneQueue, applyMigrationV26MilestoneCommitAttributions, } from "./db-migration-steps.js";
|
|
40
|
+
import { applyMigrationV2Artifacts, applyMigrationV3Memories, applyMigrationV4DecisionMadeBy, applyMigrationV5HierarchyTables, applyMigrationV6SliceSummaries, applyMigrationV7Dependencies, applyMigrationV8PlanningFields, applyMigrationV9Ordering, applyMigrationV10ReplanTrigger, applyMigrationV11TaskPlanning, applyMigrationV12QualityGates, applyMigrationV13HotPathIndexes, applyMigrationV14SliceDependencies, applyMigrationV15AuditTables, applyMigrationV16EscalationSource, applyMigrationV17TaskEscalation, applyMigrationV18MemorySources, applyMigrationV19MemoryFts, applyMigrationV20MemoryRelations, applyMigrationV21StructuredMemories, applyMigrationV22QualityGateRepair, applyMigrationV23MilestoneQueue, applyMigrationV26MilestoneCommitAttributions, applyMigrationV27ArtifactHash, applyMigrationV28MemoryLastHitAt, } from "./db-migration-steps.js";
|
|
40
41
|
import { isMemoriesFtsAvailableSchema, tryCreateMemoriesFtsSchema } from "./db-memory-fts-schema.js";
|
|
41
42
|
import { createDbOpenState } from "./db-open-state.js";
|
|
42
43
|
import { createRuntimeKvTableV25 } from "./db-runtime-kv-schema.js";
|
|
@@ -52,7 +53,7 @@ const providerLoader = createSqliteProviderLoader({
|
|
|
52
53
|
nodeVersion: process.versions.node,
|
|
53
54
|
writeStderr: (message) => process.stderr.write(message),
|
|
54
55
|
});
|
|
55
|
-
export const SCHEMA_VERSION =
|
|
56
|
+
export const SCHEMA_VERSION = 28;
|
|
56
57
|
function initSchema(db, fileBacked) {
|
|
57
58
|
if (fileBacked)
|
|
58
59
|
db.exec("PRAGMA journal_mode=WAL");
|
|
@@ -250,6 +251,14 @@ function migrateSchema(db) {
|
|
|
250
251
|
applyMigrationV26MilestoneCommitAttributions(db);
|
|
251
252
|
recordSchemaVersion(db, 26);
|
|
252
253
|
}
|
|
254
|
+
if (currentVersion < 27) {
|
|
255
|
+
applyMigrationV27ArtifactHash(db);
|
|
256
|
+
recordSchemaVersion(db, 27);
|
|
257
|
+
}
|
|
258
|
+
if (currentVersion < 28) {
|
|
259
|
+
applyMigrationV28MemoryLastHitAt(db);
|
|
260
|
+
recordSchemaVersion(db, 28);
|
|
261
|
+
}
|
|
253
262
|
db.exec("COMMIT");
|
|
254
263
|
}
|
|
255
264
|
catch (err) {
|
|
@@ -821,8 +830,9 @@ export function clearArtifacts() {
|
|
|
821
830
|
export function insertArtifact(a) {
|
|
822
831
|
if (!currentDb)
|
|
823
832
|
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
824
|
-
|
|
825
|
-
|
|
833
|
+
const contentHash = createHash("sha256").update(a.full_content).digest("hex");
|
|
834
|
+
currentDb.prepare(`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash)
|
|
835
|
+
VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at, :content_hash)`).run({
|
|
826
836
|
":path": a.path,
|
|
827
837
|
":artifact_type": a.artifact_type,
|
|
828
838
|
":milestone_id": a.milestone_id,
|
|
@@ -830,6 +840,7 @@ export function insertArtifact(a) {
|
|
|
830
840
|
":task_id": a.task_id,
|
|
831
841
|
":full_content": a.full_content,
|
|
832
842
|
":imported_at": new Date().toISOString(),
|
|
843
|
+
":content_hash": contentHash,
|
|
833
844
|
});
|
|
834
845
|
}
|
|
835
846
|
export function insertMilestone(m) {
|
|
@@ -1521,6 +1532,13 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
|
|
|
1521
1532
|
const hasEscalationAwaiting = wtTaskInfo.some((col) => col["name"] === "escalation_awaiting_review");
|
|
1522
1533
|
const hasEscalationArtifact = wtTaskInfo.some((col) => col["name"] === "escalation_artifact_path");
|
|
1523
1534
|
const hasEscalationOverride = wtTaskInfo.some((col) => col["name"] === "escalation_override_applied_at");
|
|
1535
|
+
const wtArtifactInfo = adapter.prepare("PRAGMA wt.table_info('artifacts')").all();
|
|
1536
|
+
const hasArtifactContentHash = wtArtifactInfo.some((col) => col["name"] === "content_hash");
|
|
1537
|
+
const wtMemoryInfo = adapter.prepare("PRAGMA wt.table_info('memories')").all();
|
|
1538
|
+
const hasMemoryScope = wtMemoryInfo.some((col) => col["name"] === "scope");
|
|
1539
|
+
const hasMemoryTags = wtMemoryInfo.some((col) => col["name"] === "tags");
|
|
1540
|
+
const hasMemoryStructuredFields = wtMemoryInfo.some((col) => col["name"] === "structured_fields");
|
|
1541
|
+
const hasMemoryLastHitAt = wtMemoryInfo.some((col) => col["name"] === "last_hit_at");
|
|
1524
1542
|
const decConf = adapter.prepare(`SELECT m.id FROM decisions m INNER JOIN wt.decisions w ON m.id = w.id WHERE m.decision != w.decision OR m.choice != w.choice OR m.rationale != w.rationale OR ${hasMadeBy ? "m.made_by != w.made_by" : "'agent' != 'agent'"} OR m.superseded_by IS NOT w.superseded_by`).all();
|
|
1525
1543
|
for (const row of decConf)
|
|
1526
1544
|
conflicts.push(`decision ${row["id"]}: modified in both`);
|
|
@@ -1553,12 +1571,17 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
|
|
|
1553
1571
|
supporting_slices, validation, notes, full_content, superseded_by
|
|
1554
1572
|
FROM wt.requirements
|
|
1555
1573
|
`).run());
|
|
1574
|
+
// V27: preserve content_hash. If the worktree predates V27 (no column),
|
|
1575
|
+
// fall back to the main DB's existing hash so reconcile doesn't null
|
|
1576
|
+
// out integrity fingerprints on artifacts that were unchanged in wt.
|
|
1556
1577
|
merged.artifacts = countChanges(adapter.prepare(`
|
|
1557
1578
|
INSERT OR REPLACE INTO artifacts (
|
|
1558
|
-
path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
|
|
1579
|
+
path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash
|
|
1559
1580
|
)
|
|
1560
|
-
SELECT path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
|
|
1561
|
-
|
|
1581
|
+
SELECT w.path, w.artifact_type, w.milestone_id, w.slice_id, w.task_id, w.full_content, w.imported_at,
|
|
1582
|
+
${hasArtifactContentHash ? "w.content_hash" : "m.content_hash"}
|
|
1583
|
+
FROM wt.artifacts w
|
|
1584
|
+
LEFT JOIN artifacts m ON m.path = w.path
|
|
1562
1585
|
`).run());
|
|
1563
1586
|
// Merge milestones — worktree may have updated status/planning fields.
|
|
1564
1587
|
// Never downgrade status: complete > active > pre-planning (#4372).
|
|
@@ -1656,15 +1679,25 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
|
|
|
1656
1679
|
FROM wt.tasks w
|
|
1657
1680
|
LEFT JOIN tasks m ON m.milestone_id = w.milestone_id AND m.slice_id = w.slice_id AND m.id = w.id
|
|
1658
1681
|
`).run());
|
|
1659
|
-
// Merge memories — keep worktree-learned insights
|
|
1682
|
+
// Merge memories — keep worktree-learned insights.
|
|
1683
|
+
// V18 (scope, tags), V21 (structured_fields), V28 (last_hit_at): for each
|
|
1684
|
+
// column the wt may not yet have (older worktree DB), fall back to the
|
|
1685
|
+
// main DB's existing value via LEFT JOIN so reconcile never silently
|
|
1686
|
+
// resets these fields to defaults on rows that already had them.
|
|
1660
1687
|
merged.memories = countChanges(adapter.prepare(`
|
|
1661
1688
|
INSERT OR REPLACE INTO memories (
|
|
1662
1689
|
seq, id, category, content, confidence, source_unit_type, source_unit_id,
|
|
1663
|
-
created_at, updated_at, superseded_by, hit_count
|
|
1690
|
+
created_at, updated_at, superseded_by, hit_count,
|
|
1691
|
+
scope, tags, structured_fields, last_hit_at
|
|
1664
1692
|
)
|
|
1665
|
-
SELECT seq, id, category, content, confidence, source_unit_type, source_unit_id,
|
|
1666
|
-
created_at, updated_at, superseded_by, hit_count
|
|
1667
|
-
|
|
1693
|
+
SELECT w.seq, w.id, w.category, w.content, w.confidence, w.source_unit_type, w.source_unit_id,
|
|
1694
|
+
w.created_at, w.updated_at, w.superseded_by, w.hit_count,
|
|
1695
|
+
${hasMemoryScope ? "w.scope" : "COALESCE(m.scope, 'project')"},
|
|
1696
|
+
${hasMemoryTags ? "w.tags" : "COALESCE(m.tags, '[]')"},
|
|
1697
|
+
${hasMemoryStructuredFields ? "w.structured_fields" : "m.structured_fields"},
|
|
1698
|
+
${hasMemoryLastHitAt ? "w.last_hit_at" : "m.last_hit_at"}
|
|
1699
|
+
FROM wt.memories w
|
|
1700
|
+
LEFT JOIN memories m ON m.id = w.id
|
|
1668
1701
|
`).run());
|
|
1669
1702
|
// Merge verification evidence — append-only, use INSERT OR IGNORE to avoid duplicates
|
|
1670
1703
|
merged.verification_evidence = countChanges(adapter.prepare(`
|
|
@@ -2423,7 +2456,7 @@ export function updateMemoryContentRow(id, content, confidence, updatedAt) {
|
|
|
2423
2456
|
export function incrementMemoryHitCount(id, updatedAt) {
|
|
2424
2457
|
if (!currentDb)
|
|
2425
2458
|
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
2426
|
-
currentDb.prepare("UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at WHERE id = :id").run({ ":updated_at": updatedAt, ":id": id });
|
|
2459
|
+
currentDb.prepare("UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at, last_hit_at = :last_hit_at WHERE id = :id").run({ ":updated_at": updatedAt, ":last_hit_at": updatedAt, ":id": id });
|
|
2427
2460
|
}
|
|
2428
2461
|
export function supersedeMemoryRow(oldId, newId, updatedAt) {
|
|
2429
2462
|
if (!currentDb)
|