@opengsd/gsd-pi 1.3.0-dev.965d1788 → 1.3.0-dev.cca9c184
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/cli.js +17 -4
- package/dist/headless-answers.js +1 -1
- package/dist/headless-ui.d.ts +1 -1
- package/dist/headless-ui.js +1 -1
- package/dist/headless.js +1 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/models.js +9 -0
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto-budget.js +5 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +14 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +3 -3
- package/dist/resources/extensions/gsd/auto-recovery.js +3 -3
- package/dist/resources/extensions/gsd/auto.js +19 -10
- package/dist/resources/extensions/gsd/bootstrap/core-session-tools.js +1 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +5 -4
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +25 -2
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +32 -4
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +15 -4
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +1 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands-usage.js +1 -1
- package/dist/resources/extensions/gsd/db/engine.js +4 -1
- package/dist/resources/extensions/gsd/db-memory-fts-schema.js +34 -0
- package/dist/resources/extensions/gsd/delegation-policy.js +1 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +192 -4
- package/dist/resources/extensions/gsd/exec-sandbox.js +8 -2
- package/dist/resources/extensions/gsd/git-service.js +56 -0
- package/dist/resources/extensions/gsd/gitignore.js +1 -1
- package/dist/resources/extensions/gsd/migrate-external.js +11 -2
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/dist/resources/extensions/gsd/root-write-leak-guard.js +7 -1
- package/dist/resources/extensions/gsd/safety/destructive-guard.js +8 -1
- package/dist/resources/extensions/gsd/safety/evidence-collector.js +16 -1
- package/dist/resources/extensions/gsd/tools/plan-slice.js +59 -44
- package/dist/resources/extensions/gsd/tools/plan-task.js +28 -5
- package/dist/resources/extensions/gsd/unit-registry.js +4 -2
- package/dist/resources/extensions/gsd/workflow-manifest.js +122 -12
- 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 +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.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 +11 -11
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- 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 +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts +2 -0
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +23 -5
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/workflow-tools.js +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +3 -2
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +46 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +38 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/models.ts +9 -0
- package/src/resources/extensions/claude-code-cli/tests/index.test.ts +14 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/session.ts +8 -0
- package/src/resources/extensions/gsd/auto-budget.ts +12 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +3 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
- package/src/resources/extensions/gsd/auto.ts +23 -8
- package/src/resources/extensions/gsd/bootstrap/core-session-tools.ts +1 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +5 -4
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +32 -2
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +49 -1
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +34 -4
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +16 -4
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +1 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands-usage.ts +1 -1
- package/src/resources/extensions/gsd/db/engine.ts +8 -1
- package/src/resources/extensions/gsd/db-memory-fts-schema.ts +45 -0
- package/src/resources/extensions/gsd/delegation-policy.ts +1 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +233 -5
- package/src/resources/extensions/gsd/doctor-types.ts +3 -0
- package/src/resources/extensions/gsd/exec-sandbox.ts +7 -2
- package/src/resources/extensions/gsd/git-service.ts +63 -0
- package/src/resources/extensions/gsd/gitignore.ts +1 -1
- package/src/resources/extensions/gsd/migrate-external.ts +8 -2
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/src/resources/extensions/gsd/root-write-leak-guard.ts +13 -1
- package/src/resources/extensions/gsd/safety/destructive-guard.ts +9 -1
- package/src/resources/extensions/gsd/safety/evidence-collector.ts +16 -1
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +14 -5
- package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +182 -4
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/db-memory-fts-schema.test.ts +58 -3
- package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +143 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +239 -2
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +57 -0
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +103 -4
- package/src/resources/extensions/gsd/tests/plan-task.test.ts +44 -1
- package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -6
- package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +20 -1
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +8 -3
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +62 -5
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +44 -17
- package/src/resources/extensions/gsd/tests/workflow-mcp-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +8 -4
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +6 -5
- package/src/resources/extensions/gsd/tools/plan-slice.ts +60 -47
- package/src/resources/extensions/gsd/tools/plan-task.ts +30 -5
- package/src/resources/extensions/gsd/unit-registry.ts +4 -2
- package/src/resources/extensions/gsd/workflow-manifest.ts +137 -6
- /package/dist/web/standalone/.next/static/{-ji77QJck2IKIwrGza1Bs → Eud_8lddsyi_13HIlB2i7}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{-ji77QJck2IKIwrGza1Bs → Eud_8lddsyi_13HIlB2i7}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gsd/pi-coding-agent",
|
|
3
|
-
"version": "1.3.0-dev.
|
|
3
|
+
"version": "1.3.0-dev.cca9c184",
|
|
4
4
|
"description": "Coding agent CLI (vendored from earendil-works/pi)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"gsd": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"copy-assets": "node scripts/copy-assets.cjs"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@opengsd/contracts": "^1.3.0-dev.
|
|
36
|
+
"@opengsd/contracts": "^1.3.0-dev.cca9c184",
|
|
37
37
|
"@mariozechner/jiti": "^2.6.2",
|
|
38
38
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
39
39
|
"chalk": "5.6.2",
|
|
@@ -53,11 +53,11 @@
|
|
|
53
53
|
"typebox": "1.1.38",
|
|
54
54
|
"undici": "7.28.0",
|
|
55
55
|
"yaml": "2.9.0",
|
|
56
|
-
"@gsd/agent-core": "^1.3.0-dev.
|
|
57
|
-
"@gsd/native": "^1.3.0-dev.
|
|
58
|
-
"@gsd/pi-agent-core": "^1.3.0-dev.
|
|
59
|
-
"@gsd/pi-ai": "^1.3.0-dev.
|
|
60
|
-
"@gsd/pi-tui": "^1.3.0-dev.
|
|
56
|
+
"@gsd/agent-core": "^1.3.0-dev.cca9c184",
|
|
57
|
+
"@gsd/native": "^1.3.0-dev.cca9c184",
|
|
58
|
+
"@gsd/pi-agent-core": "^1.3.0-dev.cca9c184",
|
|
59
|
+
"@gsd/pi-ai": "^1.3.0-dev.cca9c184",
|
|
60
|
+
"@gsd/pi-tui": "^1.3.0-dev.cca9c184",
|
|
61
61
|
"@sinclair/typebox": "^0.34.41"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gsd/pi-tui",
|
|
3
|
-
"version": "1.3.0-dev.
|
|
3
|
+
"version": "1.3.0-dev.cca9c184",
|
|
4
4
|
"description": "Terminal UI library (vendored from earendil-works/pi)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"gsd": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"build": "node ../../scripts/clean-package-dist.cjs && tsc -p tsconfig.json --incremental false"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@gsd/native": "^1.3.0-dev.
|
|
24
|
+
"@gsd/native": "^1.3.0-dev.cca9c184",
|
|
25
25
|
"get-east-asian-width": "1.6.0",
|
|
26
26
|
"marked": "15.0.12",
|
|
27
27
|
"@sinclair/typebox": "^0.34.41"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/rpc-client",
|
|
3
|
-
"version": "1.3.0-dev.
|
|
3
|
+
"version": "1.3.0-dev.cca9c184",
|
|
4
4
|
"description": "Standalone RPC client SDK for GSD — zero internal dependencies",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"gsd": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"test": "node --test dist/rpc-client.test.js"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@opengsd/contracts": "^1.3.0-dev.
|
|
37
|
+
"@opengsd/contracts": "^1.3.0-dev.cca9c184"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=22.0.0"
|
package/pkg/package.json
CHANGED
|
@@ -48,6 +48,15 @@ export const CLAUDE_CODE_MODELS = [
|
|
|
48
48
|
contextWindow: 1_000_000,
|
|
49
49
|
maxTokens: 128_000,
|
|
50
50
|
},
|
|
51
|
+
{
|
|
52
|
+
id: "claude-sonnet-5",
|
|
53
|
+
name: "Claude Sonnet 5 (via Claude Code)",
|
|
54
|
+
reasoning: true,
|
|
55
|
+
input: ["text", "image"] as ("text" | "image")[],
|
|
56
|
+
cost: ZERO_COST,
|
|
57
|
+
contextWindow: 1_000_000,
|
|
58
|
+
maxTokens: 128_000,
|
|
59
|
+
},
|
|
51
60
|
{
|
|
52
61
|
id: "claude-sonnet-4-6",
|
|
53
62
|
name: "Claude Sonnet 4.6 (via Claude Code)",
|
|
@@ -30,6 +30,20 @@ test("registers the claude-code provider with a streamSimple delegate", () => {
|
|
|
30
30
|
assert.equal(typeof providers[0].config.streamSimple, "function");
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
test("registers Claude Sonnet 5 as a Claude Code CLI model", () => {
|
|
34
|
+
const { pi, providers } = makeMockPi();
|
|
35
|
+
claudeCodeCli(pi as never);
|
|
36
|
+
|
|
37
|
+
const models = providers[0].config.models as Array<Record<string, unknown>>;
|
|
38
|
+
const sonnet5 = models.find((model) => model.id === "claude-sonnet-5");
|
|
39
|
+
|
|
40
|
+
assert.ok(sonnet5, "claude-sonnet-5 must be selectable via the claude-code provider");
|
|
41
|
+
assert.equal(sonnet5.name, "Claude Sonnet 5 (via Claude Code)");
|
|
42
|
+
assert.equal(sonnet5.reasoning, true);
|
|
43
|
+
assert.equal(sonnet5.contextWindow, 1_000_000);
|
|
44
|
+
assert.equal(sonnet5.maxTokens, 128_000);
|
|
45
|
+
});
|
|
46
|
+
|
|
33
47
|
test("registers a before_provider_request hook to capture the UI context", () => {
|
|
34
48
|
const { pi, handlers } = makeMockPi();
|
|
35
49
|
claudeCodeCli(pi as never);
|
|
@@ -212,6 +212,13 @@ export class AutoSession {
|
|
|
212
212
|
* the issues after MAX_PRE_EXEC_RETRIES re-attempts.
|
|
213
213
|
*/
|
|
214
214
|
readonly preExecRetryCount: Map<string, number> = new Map();
|
|
215
|
+
/**
|
|
216
|
+
* Tracks how many times each slice unit has been re-dispatched to plan-slice
|
|
217
|
+
* because task plan files were missing. Keyed by unitId (e.g. "M001/S01").
|
|
218
|
+
* Separate from preExecRetryCount so pre-exec gate failures do not block or
|
|
219
|
+
* conflate with missing-task-plan recovery.
|
|
220
|
+
*/
|
|
221
|
+
readonly missingTaskPlanRetryCount: Map<string, number> = new Map();
|
|
215
222
|
|
|
216
223
|
// ── Tool invocation errors (#2883) ──────────────────────────────────
|
|
217
224
|
/** Set when a GSD tool execution ends with isError due to malformed/truncated
|
|
@@ -413,6 +420,7 @@ export class AutoSession {
|
|
|
413
420
|
this.consecutiveCompleteBootstraps = 0;
|
|
414
421
|
this.lastPreExecFailure = null;
|
|
415
422
|
this.preExecRetryCount.clear();
|
|
423
|
+
this.missingTaskPlanRetryCount.clear();
|
|
416
424
|
this.lastToolInvocationError = null;
|
|
417
425
|
this.toolUnavailableRetries = 0;
|
|
418
426
|
this.lastUnitAgentEndMessages = null;
|
|
@@ -79,7 +79,9 @@ export function resolveCompactionThresholdPercent(raw: number | undefined): numb
|
|
|
79
79
|
return value <= 1 ? value * 100 : value;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export
|
|
82
|
+
export const HARD_CONTEXT_REROOT_THRESHOLD_PERCENT = 90;
|
|
83
|
+
|
|
84
|
+
export function shouldWarnStepSessionForContext(
|
|
83
85
|
contextPercent: number | null | undefined,
|
|
84
86
|
compactionThresholdPercent?: number,
|
|
85
87
|
): boolean {
|
|
@@ -88,3 +90,12 @@ export function shouldRerootStepSessionForContext(
|
|
|
88
90
|
resolveCompactionThresholdPercent(compactionThresholdPercent),
|
|
89
91
|
) === "pause";
|
|
90
92
|
}
|
|
93
|
+
|
|
94
|
+
export function shouldRerootStepSessionForContext(
|
|
95
|
+
contextPercent: number | null | undefined,
|
|
96
|
+
): boolean {
|
|
97
|
+
return getContextPauseAction(
|
|
98
|
+
contextPercent,
|
|
99
|
+
HARD_CONTEXT_REROOT_THRESHOLD_PERCENT,
|
|
100
|
+
) === "pause";
|
|
101
|
+
}
|
|
@@ -1523,6 +1523,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
1523
1523
|
const sid = state.activeSlice!.id;
|
|
1524
1524
|
const sTitle = state.activeSlice!.title;
|
|
1525
1525
|
const tid = state.activeTask.id;
|
|
1526
|
+
const unitId = `${mid}/${sid}`;
|
|
1526
1527
|
const artifactBasePath = resolveArtifactBasePath(basePath, mid, session);
|
|
1527
1528
|
|
|
1528
1529
|
// Guard: if the slice plan exists but the individual task plan files are
|
|
@@ -1562,6 +1563,17 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
1562
1563
|
!existsSync(projectionTaskPlanPath) &&
|
|
1563
1564
|
!tasksEmbeddedInSlicePlan
|
|
1564
1565
|
) {
|
|
1566
|
+
const MAX_MISSING_TASK_PLAN_RETRIES = 2;
|
|
1567
|
+
const retryCount = session?.missingTaskPlanRetryCount?.get(unitId) ?? 0;
|
|
1568
|
+
if (retryCount >= MAX_MISSING_TASK_PLAN_RETRIES) {
|
|
1569
|
+
session?.missingTaskPlanRetryCount?.delete(unitId);
|
|
1570
|
+
return {
|
|
1571
|
+
action: "stop",
|
|
1572
|
+
reason: `Missing task-plan recovery failed ${retryCount} times for ${unitId} - manual intervention required. Task plan ${tid} is still missing after regenerating the slice plan. Fix the task-plan files manually, then run /gsd auto to resume.`,
|
|
1573
|
+
level: "error",
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
session?.missingTaskPlanRetryCount?.set(unitId, retryCount + 1);
|
|
1565
1577
|
if (isDebugEnabled()) {
|
|
1566
1578
|
const expectedTaskPlanPath = join(artifactBasePath, relTaskFile(artifactBasePath, mid, sid, tid, "PLAN"));
|
|
1567
1579
|
const originalProjectRoot = session?.originalBasePath || basePath;
|
|
@@ -1585,7 +1597,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
1585
1597
|
return {
|
|
1586
1598
|
action: "dispatch",
|
|
1587
1599
|
unitType: "plan-slice",
|
|
1588
|
-
unitId
|
|
1600
|
+
unitId,
|
|
1589
1601
|
prompt: await buildPlanSlicePrompt(
|
|
1590
1602
|
mid,
|
|
1591
1603
|
midTitle,
|
|
@@ -1598,6 +1610,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
1598
1610
|
};
|
|
1599
1611
|
}
|
|
1600
1612
|
|
|
1613
|
+
session?.missingTaskPlanRetryCount?.delete(unitId);
|
|
1601
1614
|
return null;
|
|
1602
1615
|
},
|
|
1603
1616
|
},
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
|
|
18
18
|
resolveTasksDir, resolveTaskFiles, resolveTaskFile,
|
|
19
19
|
relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath,
|
|
20
|
-
resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile,
|
|
20
|
+
relTaskFile, resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile,
|
|
21
21
|
} from "./paths.js";
|
|
22
22
|
import { resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
|
|
23
23
|
import { isContextModeEnabled } from "./preferences-types.js";
|
|
@@ -2662,7 +2662,7 @@ export async function buildExecuteTaskPrompt(
|
|
|
2662
2662
|
|
|
2663
2663
|
const taskPlanPath = resolveTaskFile(base, mid, sid, tid, "PLAN");
|
|
2664
2664
|
const taskPlanContent = taskPlanPath ? await loadFile(taskPlanPath) : null;
|
|
2665
|
-
const taskPlanRelPath =
|
|
2665
|
+
const taskPlanRelPath = relTaskFile(base, mid, sid, tid, "PLAN");
|
|
2666
2666
|
const taskPlanContext = taskPlanContent
|
|
2667
2667
|
? [
|
|
2668
2668
|
"## Inlined Task Plan (authoritative local execution contract)",
|
|
@@ -3901,7 +3901,7 @@ export async function buildReactiveExecutePrompt(
|
|
|
3901
3901
|
|
|
3902
3902
|
const taskPlanPath = resolveTaskFile(base, mid, sid, tid, "PLAN");
|
|
3903
3903
|
const taskPlanContent = taskPlanPath ? await loadFile(taskPlanPath) : null;
|
|
3904
|
-
const taskPlanRelPath =
|
|
3904
|
+
const taskPlanRelPath = relTaskFile(base, mid, sid, tid, "PLAN");
|
|
3905
3905
|
const taskPlanInline = taskPlanContent
|
|
3906
3906
|
? [
|
|
3907
3907
|
"## Inlined Task Plan (authoritative local execution contract)",
|
|
@@ -35,7 +35,7 @@ import { isValidationTerminal } from "./state.js";
|
|
|
35
35
|
import { getErrorMessage } from "./error-utils.js";
|
|
36
36
|
import { logWarning, logError } from "./workflow-logger.js";
|
|
37
37
|
import { readIntegrationBranch } from "./git-service.js";
|
|
38
|
-
import { isClosedStatus } from "./status-guards.js";
|
|
38
|
+
import { isClosedStatus, isInactiveStatus } from "./status-guards.js";
|
|
39
39
|
import {
|
|
40
40
|
resolveSlicePath,
|
|
41
41
|
resolveSliceFile,
|
|
@@ -295,7 +295,7 @@ export function writeReactiveExecuteBlocker(
|
|
|
295
295
|
transaction(() => {
|
|
296
296
|
for (const tid of summaryPresent) {
|
|
297
297
|
const task = getTask(mid, sid, tid);
|
|
298
|
-
if (!task ||
|
|
298
|
+
if (!task || isInactiveStatus(task.status)) {
|
|
299
299
|
unchangedTaskIds.push(tid);
|
|
300
300
|
continue;
|
|
301
301
|
}
|
|
@@ -304,7 +304,7 @@ export function writeReactiveExecuteBlocker(
|
|
|
304
304
|
}
|
|
305
305
|
for (const tid of summaryMissing) {
|
|
306
306
|
const task = getTask(mid, sid, tid);
|
|
307
|
-
if (!task ||
|
|
307
|
+
if (!task || isInactiveStatus(task.status)) {
|
|
308
308
|
unchangedTaskIds.push(tid);
|
|
309
309
|
continue;
|
|
310
310
|
}
|
|
@@ -97,8 +97,10 @@ import {
|
|
|
97
97
|
getNewBudgetAlertLevel,
|
|
98
98
|
getBudgetEnforcementAction,
|
|
99
99
|
getContextPauseAction,
|
|
100
|
+
HARD_CONTEXT_REROOT_THRESHOLD_PERCENT,
|
|
100
101
|
resolveCompactionThresholdPercent,
|
|
101
102
|
shouldRerootStepSessionForContext,
|
|
103
|
+
shouldWarnStepSessionForContext,
|
|
102
104
|
} from "./auto-budget.js";
|
|
103
105
|
import {
|
|
104
106
|
markToolStart as _markToolStart,
|
|
@@ -701,6 +703,7 @@ export {
|
|
|
701
703
|
getContextPauseAction,
|
|
702
704
|
resolveCompactionThresholdPercent,
|
|
703
705
|
shouldRerootStepSessionForContext,
|
|
706
|
+
shouldWarnStepSessionForContext,
|
|
704
707
|
} from "./auto-budget.js";
|
|
705
708
|
|
|
706
709
|
function closeOutSignalInterruptedUnit(currentBasePath: string): void {
|
|
@@ -1375,9 +1378,10 @@ export async function rerootCommandSession(
|
|
|
1375
1378
|
}
|
|
1376
1379
|
|
|
1377
1380
|
/**
|
|
1378
|
-
* After a completed step-mode unit,
|
|
1379
|
-
*
|
|
1380
|
-
* re-
|
|
1381
|
+
* After a completed step-mode unit, warn when usage reaches the configurable
|
|
1382
|
+
* soft context threshold. At the fixed hard boundary, write a context-mode
|
|
1383
|
+
* snapshot and re-root the visible command session while leaving the NEXT
|
|
1384
|
+
* progress surface.
|
|
1381
1385
|
*/
|
|
1382
1386
|
export async function maybeRerootStepSessionForHighContext(
|
|
1383
1387
|
ctx: ExtensionContext,
|
|
@@ -1392,7 +1396,19 @@ export async function maybeRerootStepSessionForHighContext(
|
|
|
1392
1396
|
const prefs = loadEffectiveGSDPreferences(workspaceRoot);
|
|
1393
1397
|
const compactionThreshold = prefs?.preferences.context_management?.compaction_threshold_percent;
|
|
1394
1398
|
|
|
1395
|
-
|
|
1399
|
+
const shouldWarn = shouldWarnStepSessionForContext(contextPercent, compactionThreshold);
|
|
1400
|
+
const shouldReroot = shouldRerootStepSessionForContext(contextPercent);
|
|
1401
|
+
if (!shouldWarn && !shouldReroot) {
|
|
1402
|
+
return { rerooted: false };
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
const displayPercent = (contextPercent as number).toFixed(1);
|
|
1406
|
+
const thresholdDisplay = resolveCompactionThresholdPercent(compactionThreshold).toFixed(0);
|
|
1407
|
+
if (!shouldReroot) {
|
|
1408
|
+
ctx.ui.notify(
|
|
1409
|
+
`Step complete — context at ${displayPercent}% (soft threshold: ${thresholdDisplay}%). Use /compact when convenient; continuing in this session is still safe.`,
|
|
1410
|
+
"info",
|
|
1411
|
+
);
|
|
1396
1412
|
return { rerooted: false };
|
|
1397
1413
|
}
|
|
1398
1414
|
|
|
@@ -1401,12 +1417,10 @@ export async function maybeRerootStepSessionForHighContext(
|
|
|
1401
1417
|
const { writeContextModeCompactionSnapshot } = await import("./context-mode-snapshot.js");
|
|
1402
1418
|
await writeContextModeCompactionSnapshot(snapshotBase);
|
|
1403
1419
|
|
|
1404
|
-
const displayPercent = (contextPercent as number).toFixed(1);
|
|
1405
|
-
const thresholdDisplay = resolveCompactionThresholdPercent(compactionThreshold).toFixed(0);
|
|
1406
1420
|
const result = await rerootCommandSession(cmdCtx, workspaceRoot);
|
|
1407
1421
|
if (result.status === "ok") {
|
|
1408
1422
|
ctx.ui.notify(
|
|
1409
|
-
`Step complete — context at ${displayPercent}% (threshold: ${
|
|
1423
|
+
`Step complete — context at ${displayPercent}% (hard threshold: ${HARD_CONTEXT_REROOT_THRESHOLD_PERCENT}%). Fresh session ready for /gsd next.`,
|
|
1410
1424
|
"info",
|
|
1411
1425
|
);
|
|
1412
1426
|
return { rerooted: true };
|
|
@@ -2102,7 +2116,7 @@ export function _selectStopAutoWorktreeExit(args: {
|
|
|
2102
2116
|
/**
|
|
2103
2117
|
* Pause auto-mode without destroying state. Context is preserved.
|
|
2104
2118
|
* The user can interact with the agent, then `/gsd auto` resumes
|
|
2105
|
-
* from disk state. Called when the user presses Escape
|
|
2119
|
+
* from disk state. Called when the user presses Escape or runs `/gsd pause`.
|
|
2106
2120
|
*/
|
|
2107
2121
|
export async function pauseAuto(
|
|
2108
2122
|
ctx?: ExtensionContext,
|
|
@@ -2129,6 +2143,7 @@ export async function pauseAuto(
|
|
|
2129
2143
|
|
|
2130
2144
|
s.active = false;
|
|
2131
2145
|
s.paused = true;
|
|
2146
|
+
if (options.abortActiveTurn && ctx && !ctx.isIdle()) ctx.abort();
|
|
2132
2147
|
clearUnitTimeout();
|
|
2133
2148
|
stopAutoCommandPolling();
|
|
2134
2149
|
|
|
@@ -28,6 +28,7 @@ const NON_REPEATABLE_FROM_MINIMAL_BASE = new Set<string>([
|
|
|
28
28
|
|
|
29
29
|
/** Additional core tools not in MINIMAL_AUTO_BASE but routinely multi-called per turn. */
|
|
30
30
|
const EXTRA_INHERENTLY_REPEATABLE = [
|
|
31
|
+
"gsd_exec",
|
|
31
32
|
"multi_edit",
|
|
32
33
|
"todo_write",
|
|
33
34
|
"notebook_edit",
|
|
@@ -690,8 +690,9 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|
|
690
690
|
promptSnippet: "Plan a slice via DB write + PLAN render + cache invalidation",
|
|
691
691
|
promptGuidelines: [
|
|
692
692
|
"Use gsd_plan_slice for slice planning instead of writing S##-PLAN.md or task PLAN files directly.",
|
|
693
|
-
"
|
|
694
|
-
"
|
|
693
|
+
"For incremental planning, call gsd_plan_slice with slice metadata only, then call gsd_plan_task once per task.",
|
|
694
|
+
"When tasks is omitted or empty, the tool writes only slice planning metadata and preserves existing tasks.",
|
|
695
|
+
"When tasks is non-empty, the tool validates input, requires an existing parent slice, writes slice/task planning data, renders PLAN.md and task plan files from DB, and clears both state and parse caches after success.",
|
|
695
696
|
"Use the canonical name gsd_plan_slice; gsd_slice_plan is only an alias.",
|
|
696
697
|
],
|
|
697
698
|
parameters: Type.Object({
|
|
@@ -699,7 +700,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|
|
699
700
|
milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
|
|
700
701
|
sliceId: Type.String({ description: "Slice ID (e.g. S01)" }),
|
|
701
702
|
goal: Type.String({ description: "Slice goal" }),
|
|
702
|
-
tasks: Type.Array(Type.Object({
|
|
703
|
+
tasks: Type.Optional(Type.Array(Type.Object({
|
|
703
704
|
taskId: Type.String({ description: "Task ID (e.g. T01)" }),
|
|
704
705
|
title: Type.String({ description: "Task title" }),
|
|
705
706
|
description: Type.String({ description: "Task description / steps block" }),
|
|
@@ -709,7 +710,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|
|
709
710
|
inputs: Type.Array(Type.String(), { description: "Array<string> of input files or references; pass [\"path\"] or [], never a single string" }),
|
|
710
711
|
expectedOutput: Type.Array(Type.String(), { description: "Array<string> of files this task creates or overwrites; pass [\"path\"] or [], never prose or a single string" }),
|
|
711
712
|
observabilityImpact: Type.Optional(Type.String({ description: "Task observability impact" })),
|
|
712
|
-
}), { description: "
|
|
713
|
+
}), { description: "Optional full task replacement for the slice. Omit for incremental planning, then call gsd_plan_task once per task." })),
|
|
713
714
|
// ── Enrichment metadata (optional — defaults to empty) ────────────
|
|
714
715
|
successCriteria: Type.Optional(Type.String({ description: "Slice success criteria block" })),
|
|
715
716
|
proofLevel: Type.Optional(Type.String({ description: "Slice proof level" })),
|
|
@@ -18,6 +18,7 @@ import { registerShortcuts } from "./register-shortcuts.js";
|
|
|
18
18
|
import { writeCrashLog } from "./crash-log.js";
|
|
19
19
|
import { logWarning, isGsdExtensionStderrEnabled } from "../workflow-logger.js";
|
|
20
20
|
import { UNIT_TOOL_CONTRACTS } from "../unit-tool-contracts.js";
|
|
21
|
+
import { installManifestFlushOnProcessTeardown } from "../workflow-manifest.js";
|
|
21
22
|
// Static import so cmux event listeners are registered synchronously during
|
|
22
23
|
// extension bootstrap. Prior implementation used `void import().then()` which
|
|
23
24
|
// queued listener registration as a microtask — any CMUX_CHANNELS emit fired
|
|
@@ -196,6 +197,7 @@ export function registerGsdExtension(pi: ExtensionAPI): void {
|
|
|
196
197
|
});
|
|
197
198
|
|
|
198
199
|
installEpipeGuard();
|
|
200
|
+
installManifestFlushOnProcessTeardown();
|
|
199
201
|
|
|
200
202
|
// Ecosystem handlers captured by the GSDExtensionAPI wrapper for the
|
|
201
203
|
// GSD-owned `before_agent_start` dispatch step (#3338).
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
} from "../auto-runtime-state.js";
|
|
35
35
|
import { applyProviderPayloadPolicy } from "../provider-payload-policy.js";
|
|
36
36
|
|
|
37
|
-
import { checkToolCallLoop, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
|
|
37
|
+
import { checkToolCallLoop, recordToolCallLoopMutation, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
|
|
38
38
|
import { MINIMAL_AUTO_BASE_TOOL_NAMES } from "./core-session-tools.js";
|
|
39
39
|
import { maybePauseAutoForApprovalGate, resetPendingGatePauseGuard } from "./pending-gate-pause.js";
|
|
40
40
|
import { saveActivityLog } from "../activity-log.js";
|
|
@@ -76,6 +76,7 @@ import { supportsSourceObservationsForUnit } from "../source-observations.js";
|
|
|
76
76
|
import { clearPendingAutoStart } from "../pending-auto-start.js";
|
|
77
77
|
import { resolveWorkflowToolBasePath } from "./dynamic-tools.js";
|
|
78
78
|
import { getRequiredWorkflowToolsForUnit } from "../unit-tool-contracts.js";
|
|
79
|
+
import { flushAllManifests } from "../workflow-manifest.js";
|
|
79
80
|
|
|
80
81
|
let approvalQuestionAbortInFlight = false;
|
|
81
82
|
|
|
@@ -583,6 +584,21 @@ function contextBasePath(ctx?: { cwd?: string }): string {
|
|
|
583
584
|
return typeof ctx?.cwd === "string" ? ctx.cwd : process.cwd();
|
|
584
585
|
}
|
|
585
586
|
|
|
587
|
+
const LOOP_GUARD_INTERACTIVE_INSTRUCTIONS = [
|
|
588
|
+
"Do not retry this tool or call other tools this turn — stop and respond to the user in text.",
|
|
589
|
+
"Do not retry this tool or pivot to other tools this turn — stop and respond to the user in text.",
|
|
590
|
+
];
|
|
591
|
+
const LOOP_GUARD_AUTO_INSTRUCTION =
|
|
592
|
+
"Do not re-issue this blocked tool. In /gsd auto, stop tool calls for this turn and return control to the auto-mode recovery/replan path.";
|
|
593
|
+
|
|
594
|
+
function formatLoopGuardBlockReason(reason: string | undefined): string | undefined {
|
|
595
|
+
if (!reason || !getAutoRuntimeSnapshot().active) return reason;
|
|
596
|
+
return LOOP_GUARD_INTERACTIVE_INSTRUCTIONS.reduce(
|
|
597
|
+
(formatted, instruction) => formatted.replace(instruction, LOOP_GUARD_AUTO_INSTRUCTION),
|
|
598
|
+
reason,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
586
602
|
function beginSourceObservationStoreForCurrentUnit(
|
|
587
603
|
ctx?: { cwd?: string },
|
|
588
604
|
): ReturnType<typeof getSourceObservationStore> | null {
|
|
@@ -1080,6 +1096,19 @@ export function registerHooks(
|
|
|
1080
1096
|
const { handleAgentEnd } = await import("./agent-end-recovery.js");
|
|
1081
1097
|
const agentEndBasePath = contextBasePath(ctx);
|
|
1082
1098
|
try {
|
|
1099
|
+
// The manifest is a non-critical projection (the append-only event log is
|
|
1100
|
+
// the authoritative recovery source), so a flush failure must not skip
|
|
1101
|
+
// agent_end recovery. drainManifestWrites still propagates write failures
|
|
1102
|
+
// to explicit flush callers; here we deliberately log and continue so
|
|
1103
|
+
// handleAgentEnd (and the finally below) always run.
|
|
1104
|
+
try {
|
|
1105
|
+
await flushAllManifests();
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
safetyLogWarning(
|
|
1108
|
+
"manifest",
|
|
1109
|
+
`flushAllManifests on agent_end failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1083
1112
|
await handleAgentEnd(pi, event, ctx);
|
|
1084
1113
|
} finally {
|
|
1085
1114
|
activateDeferredApprovalGate(agentEndBasePath);
|
|
@@ -1286,7 +1315,7 @@ export function registerHooks(
|
|
|
1286
1315
|
// ── Loop guard: block repeated identical tool calls ──
|
|
1287
1316
|
const loopCheck = checkToolCallLoop(toolName, event.input as Record<string, unknown>);
|
|
1288
1317
|
if (loopCheck.block) {
|
|
1289
|
-
return { block: true, reason: loopCheck.reason };
|
|
1318
|
+
return { block: true, reason: formatLoopGuardBlockReason(loopCheck.reason) };
|
|
1290
1319
|
}
|
|
1291
1320
|
|
|
1292
1321
|
const deferredGateGuard = shouldBlockDeferredApprovalTool(
|
|
@@ -1558,6 +1587,7 @@ export function registerHooks(
|
|
|
1558
1587
|
}
|
|
1559
1588
|
}
|
|
1560
1589
|
if (!event.isError) {
|
|
1590
|
+
recordToolCallLoopMutation(toolName);
|
|
1561
1591
|
refreshSourceObservationAfterMutation(toolName, event.input, ctx);
|
|
1562
1592
|
clearSourceObservationsAfterShell(toolName);
|
|
1563
1593
|
}
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
import { test, describe, before, after } from "node:test";
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
|
-
import { mkdtempSync, rmSync, existsSync } from "node:fs";
|
|
12
|
+
import { mkdtempSync, rmSync, existsSync, mkdirSync } from "node:fs";
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
markDepthVerified,
|
|
18
18
|
loadWriteGateSnapshot,
|
|
19
|
+
shouldBlockContextArtifactSaveInSnapshot,
|
|
19
20
|
clearDiscussionFlowState,
|
|
20
21
|
} from "../write-gate.js";
|
|
21
22
|
|
|
@@ -100,4 +101,51 @@ describe("write-gate basePath regression", () => {
|
|
|
100
101
|
"loadWriteGateSnapshot(baseDirB) must not bleed state from baseDirA",
|
|
101
102
|
);
|
|
102
103
|
});
|
|
104
|
+
|
|
105
|
+
test("worktree basePath reads depth verification from project-root snapshot", (t) => {
|
|
106
|
+
const prev = process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
107
|
+
t.after(() => {
|
|
108
|
+
if (prev === undefined) {
|
|
109
|
+
delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
110
|
+
} else {
|
|
111
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = prev;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = "1";
|
|
115
|
+
|
|
116
|
+
const projectRoot = makeTempDir();
|
|
117
|
+
const worktreePath = join(projectRoot, ".gsd-worktrees", "M020");
|
|
118
|
+
t.after(() => {
|
|
119
|
+
rmSync(projectRoot, { recursive: true, force: true });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
clearDiscussionFlowState(projectRoot);
|
|
123
|
+
clearDiscussionFlowState(worktreePath);
|
|
124
|
+
|
|
125
|
+
// Simulate MCP routing: verification recorded at project root.
|
|
126
|
+
markDepthVerified("M020", projectRoot);
|
|
127
|
+
|
|
128
|
+
// Worktree has a local .gsd projection but no write-gate snapshot file.
|
|
129
|
+
mkdirSync(join(worktreePath, ".gsd", "runtime"), { recursive: true });
|
|
130
|
+
mkdirSync(join(projectRoot, ".gsd", "milestones"), { recursive: true });
|
|
131
|
+
|
|
132
|
+
const projectSnapshotPath = join(projectRoot, ".gsd", "runtime", "write-gate-state.json");
|
|
133
|
+
const worktreeSnapshotPath = join(worktreePath, ".gsd", "runtime", "write-gate-state.json");
|
|
134
|
+
assert.ok(existsSync(projectSnapshotPath), "verification snapshot lives at project root");
|
|
135
|
+
assert.ok(!existsSync(worktreeSnapshotPath), "worktree must not have its own snapshot file");
|
|
136
|
+
|
|
137
|
+
const snapshotFromWorktree = loadWriteGateSnapshot(worktreePath);
|
|
138
|
+
assert.ok(
|
|
139
|
+
snapshotFromWorktree.verifiedDepthMilestones.includes("M020"),
|
|
140
|
+
"loadWriteGateSnapshot(worktree) must inherit project-root verification",
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const guard = shouldBlockContextArtifactSaveInSnapshot(
|
|
144
|
+
snapshotFromWorktree,
|
|
145
|
+
"CONTEXT",
|
|
146
|
+
"M020",
|
|
147
|
+
null,
|
|
148
|
+
);
|
|
149
|
+
assert.equal(guard.block, false, "CONTEXT save must unblock once project-root gate is verified");
|
|
150
|
+
});
|
|
103
151
|
});
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
* to stop tooling for the rest of that turn and answer in text.
|
|
15
15
|
*
|
|
16
16
|
* The per-tool-name check (#783 Brief C) tracks call counts within a
|
|
17
|
-
* turn regardless of args
|
|
17
|
+
* turn regardless of args, decaying after successful state-mutating tools.
|
|
18
|
+
* This catches improvisation
|
|
18
19
|
* loops where the model attempts the same missing workflow tool through
|
|
19
20
|
* varied surfaces (bash → `node -e` → CLI), each with a different
|
|
20
|
-
* signature, so the identical-args streak never trips
|
|
21
|
+
* signature, so the identical-args streak never trips, while allowing
|
|
22
|
+
* tool-heavy turns that are making file-mutation progress. Whichever guard
|
|
21
23
|
* trips first blocks.
|
|
22
24
|
*/
|
|
23
25
|
|
|
@@ -42,6 +44,8 @@ const MAX_CONSECUTIVE_STRICT = 1;
|
|
|
42
44
|
*/
|
|
43
45
|
const PER_TOOL_DEFAULT_CAP = 6;
|
|
44
46
|
const PER_TOOL_REPEATABLE_CAP = 15;
|
|
47
|
+
const PER_TOOL_CAP_EXEMPT_TOOLS = new Set(["find", "glob", "grep", "ls", "read", "search_and_read"]);
|
|
48
|
+
const STATE_MUTATING_TOOL_SET = new Set(["edit", "write", "multi_edit", "notebook_edit"]);
|
|
45
49
|
|
|
46
50
|
let consecutiveCount = 0;
|
|
47
51
|
let lastSignature = "";
|
|
@@ -50,6 +54,8 @@ let enabled = true;
|
|
|
50
54
|
|
|
51
55
|
/** Per-tool-name call counts within the current turn (#783 Brief C). */
|
|
52
56
|
const perToolCounts = new Map<string, number>();
|
|
57
|
+
let mutationEpoch = 0;
|
|
58
|
+
const perToolLastMutationEpoch = new Map<string, number>();
|
|
53
59
|
|
|
54
60
|
/** Hash tool name + args into a compact signature for comparison. */
|
|
55
61
|
function hashToolCall(toolName: string, args: Record<string, unknown>): string {
|
|
@@ -77,7 +83,8 @@ function hashToolCall(toolName: string, args: Record<string, unknown>): string {
|
|
|
77
83
|
* 1. Identical-signature streak (MAX_CONSECUTIVE_IDENTICAL_CALLS, strict for
|
|
78
84
|
* ask_user_questions).
|
|
79
85
|
* 2. Per-tool-name cap (PER_TOOL_DEFAULT_CAP / PER_TOOL_REPEATABLE_CAP),
|
|
80
|
-
* independent of args
|
|
86
|
+
* independent of args, reset after file-mutation progress — catches
|
|
87
|
+
* improvisation loops (#783).
|
|
81
88
|
*/
|
|
82
89
|
export function checkToolCallLoop(
|
|
83
90
|
toolName: string,
|
|
@@ -114,8 +121,20 @@ export function checkToolCallLoop(
|
|
|
114
121
|
// ── Guard 2: per-tool-name cap, independent of args (#783 Brief C) ──
|
|
115
122
|
// Catches improvisation loops where the same tool is invoked many times with
|
|
116
123
|
// varied args (e.g. retrying a missing workflow tool via bash/node -e/CLI).
|
|
117
|
-
const
|
|
124
|
+
const priorPerToolCount =
|
|
125
|
+
(perToolLastMutationEpoch.get(toolName) ?? mutationEpoch) < mutationEpoch
|
|
126
|
+
? 0
|
|
127
|
+
: (perToolCounts.get(toolName) ?? 0);
|
|
128
|
+
const perToolCount = priorPerToolCount + 1;
|
|
118
129
|
perToolCounts.set(toolName, perToolCount);
|
|
130
|
+
perToolLastMutationEpoch.set(toolName, mutationEpoch);
|
|
131
|
+
|
|
132
|
+
// Read-only navigation tools are normal context gathering; Guard 1 still
|
|
133
|
+
// catches true reread loops with identical arguments.
|
|
134
|
+
if (PER_TOOL_CAP_EXEMPT_TOOLS.has(toolName)) {
|
|
135
|
+
return { block: false, count: consecutiveCount };
|
|
136
|
+
}
|
|
137
|
+
|
|
119
138
|
const perToolCap = INHERENTLY_REPEATABLE_TOOL_SET.has(toolName)
|
|
120
139
|
? PER_TOOL_REPEATABLE_CAP
|
|
121
140
|
: PER_TOOL_DEFAULT_CAP;
|
|
@@ -135,6 +154,13 @@ export function checkToolCallLoop(
|
|
|
135
154
|
return { block: false, count: consecutiveCount };
|
|
136
155
|
}
|
|
137
156
|
|
|
157
|
+
/** Record successful mutation progress so Guard 2 can decay on the next count. */
|
|
158
|
+
export function recordToolCallLoopMutation(toolName: string): void {
|
|
159
|
+
if (!enabled) return;
|
|
160
|
+
if (!STATE_MUTATING_TOOL_SET.has(toolName)) return;
|
|
161
|
+
mutationEpoch++;
|
|
162
|
+
}
|
|
163
|
+
|
|
138
164
|
/** Reset the guard state. Call at agent turn boundaries. */
|
|
139
165
|
export function resetToolCallLoopGuard(): void {
|
|
140
166
|
consecutiveCount = 0;
|
|
@@ -142,6 +168,8 @@ export function resetToolCallLoopGuard(): void {
|
|
|
142
168
|
lastToolName = "";
|
|
143
169
|
enabled = true;
|
|
144
170
|
perToolCounts.clear();
|
|
171
|
+
mutationEpoch = 0;
|
|
172
|
+
perToolLastMutationEpoch.clear();
|
|
145
173
|
}
|
|
146
174
|
|
|
147
175
|
/** Disable the guard (e.g. during shutdown). */
|
|
@@ -151,6 +179,8 @@ export function disableToolCallLoopGuard(): void {
|
|
|
151
179
|
lastSignature = "";
|
|
152
180
|
lastToolName = "";
|
|
153
181
|
perToolCounts.clear();
|
|
182
|
+
mutationEpoch = 0;
|
|
183
|
+
perToolLastMutationEpoch.clear();
|
|
154
184
|
}
|
|
155
185
|
|
|
156
186
|
/** Get current consecutive count for diagnostics. */
|