gsd-pi 2.82.0-dev.725028083 → 2.82.0-dev.9d5798940
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 +3 -2
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/GSD-WORKFLOW.md +3 -1
- package/dist/resources/extensions/claude-code-cli/partial-builder.js +2 -1
- package/dist/resources/extensions/cmux/index.js +5 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +113 -6
- package/dist/resources/extensions/gsd/auto-dispatch.js +13 -6
- package/dist/resources/extensions/gsd/auto-post-unit.js +232 -126
- package/dist/resources/extensions/gsd/auto-prompts.js +2 -2
- package/dist/resources/extensions/gsd/auto-recovery.js +31 -1
- package/dist/resources/extensions/gsd/auto-start.js +7 -3
- package/dist/resources/extensions/gsd/auto-verification.js +28 -22
- package/dist/resources/extensions/gsd/auto-worktree.js +96 -0
- package/dist/resources/extensions/gsd/auto.js +128 -52
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +4 -1
- package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +16 -7
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +16 -2
- package/dist/resources/extensions/gsd/clean-root-preflight.js +170 -8
- package/dist/resources/extensions/gsd/commands/catalog.js +4 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +37 -0
- package/dist/resources/extensions/gsd/commands-bootstrap.js +5 -0
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +2 -2
- package/dist/resources/extensions/gsd/export-html.js +27 -425
- package/dist/resources/extensions/gsd/md-importer.js +1 -1
- package/dist/resources/extensions/gsd/migrate/command.js +5 -0
- package/dist/resources/extensions/gsd/migrate/preview.js +9 -0
- package/dist/resources/extensions/gsd/migrate/transformer.js +51 -4
- package/dist/resources/extensions/gsd/migrate/writer.js +11 -1
- package/dist/resources/extensions/gsd/milestone-actions.js +11 -4
- package/dist/resources/extensions/gsd/native-git-bridge.js +8 -3
- package/dist/resources/extensions/gsd/state-reconciliation/drift/merge-state.js +6 -1
- package/dist/resources/extensions/gsd/tools/plan-slice.js +2 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +119 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +32 -10
- package/dist/resources/extensions/gsd/verification-verdict.js +26 -0
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +49 -9
- package/dist/resources/extensions/shared/html-shell.js +388 -0
- package/dist/resources/extensions/subagent/index.js +448 -78
- package/dist/resources/extensions/subagent/launch.js +77 -0
- package/dist/resources/extensions/subagent/run-store.js +148 -0
- package/dist/resources/extensions/visual-brief/artifact-policy.js +29 -0
- package/dist/resources/extensions/visual-brief/extension-manifest.json +8 -0
- package/dist/resources/extensions/visual-brief/index.js +5 -0
- package/dist/resources/extensions/visual-brief/page-contract.js +124 -0
- package/dist/resources/extensions/visual-brief/prompts.js +140 -0
- 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 +15 -15
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +4 -7
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +4 -7
- 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 +4 -5
- 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 +2 -5
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -7
- 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 +4 -7
- 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 +4 -5
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -5
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
- package/dist/web/standalone/.next/server/chunks/4266.js +2 -0
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +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/dist/web/standalone/.next/static/chunks/2973.33f26573894b6153.js +2 -0
- package/dist/web/standalone/.next/static/chunks/{8359.e059d86b255fce1c.js → 8359.7eb3bb8f8ecf4c01.js} +2 -2
- package/dist/web/standalone/.next/static/chunks/app/layout-8c10ec293ae0f1d5.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-de742b64187e13fe.js → webpack-9a4db269f9ed63ad.js} +1 -1
- package/dist/web/standalone/.next/static/css/746ee28c929d1880.css +1 -0
- package/package.json +3 -3
- package/packages/contracts/dist/rpc.test.js +7 -0
- package/packages/contracts/dist/rpc.test.js.map +1 -1
- package/packages/contracts/dist/workflow.d.ts +21 -0
- package/packages/contracts/dist/workflow.d.ts.map +1 -1
- package/packages/contracts/dist/workflow.js +24 -0
- package/packages/contracts/dist/workflow.js.map +1 -1
- package/packages/contracts/src/rpc.test.ts +8 -0
- package/packages/contracts/src/workflow.ts +24 -0
- package/packages/mcp-server/README.md +13 -4
- package/packages/mcp-server/dist/workflow-tools.d.ts +0 -3
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +80 -0
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +22 -0
- package/packages/mcp-server/src/workflow-tools.ts +168 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- 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 +5 -0
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +6 -0
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
- package/src/resources/GSD-WORKFLOW.md +3 -1
- package/src/resources/extensions/claude-code-cli/partial-builder.ts +2 -1
- package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +19 -2
- package/src/resources/extensions/cmux/index.ts +6 -0
- package/src/resources/extensions/gsd/auto/contracts.ts +46 -11
- package/src/resources/extensions/gsd/auto/orchestrator.ts +118 -6
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -6
- package/src/resources/extensions/gsd/auto-post-unit.ts +265 -138
- package/src/resources/extensions/gsd/auto-prompts.ts +2 -2
- package/src/resources/extensions/gsd/auto-recovery.ts +29 -0
- package/src/resources/extensions/gsd/auto-start.ts +7 -3
- package/src/resources/extensions/gsd/auto-verification.ts +36 -34
- package/src/resources/extensions/gsd/auto-worktree.ts +104 -0
- package/src/resources/extensions/gsd/auto.ts +136 -51
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +6 -1
- package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +16 -6
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +19 -3
- package/src/resources/extensions/gsd/clean-root-preflight.ts +174 -8
- package/src/resources/extensions/gsd/commands/catalog.ts +4 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +40 -0
- package/src/resources/extensions/gsd/commands-bootstrap.ts +10 -0
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +3 -3
- package/src/resources/extensions/gsd/export-html.ts +27 -427
- package/src/resources/extensions/gsd/md-importer.ts +1 -1
- package/src/resources/extensions/gsd/migrate/command.ts +5 -0
- package/src/resources/extensions/gsd/migrate/preview.ts +10 -0
- package/src/resources/extensions/gsd/migrate/transformer.ts +58 -4
- package/src/resources/extensions/gsd/migrate/writer.ts +14 -1
- package/src/resources/extensions/gsd/milestone-actions.ts +10 -4
- package/src/resources/extensions/gsd/native-git-bridge.ts +8 -3
- package/src/resources/extensions/gsd/state-reconciliation/drift/merge-state.ts +8 -1
- package/src/resources/extensions/gsd/tests/auto-deterministic-error-classification-4973.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +408 -4
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +6 -5
- package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +12 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +15 -1
- package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +69 -1
- package/src/resources/extensions/gsd/tests/brief-command.test.ts +89 -0
- package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +107 -2
- package/src/resources/extensions/gsd/tests/closeout-git-deferral.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +57 -2
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/evidence-cross-ref.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/integration/migrate-command.test.ts +48 -3
- package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +55 -1
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +79 -1
- package/src/resources/extensions/gsd/tests/post-unit-git-failure.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +46 -2
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +86 -7
- package/src/resources/extensions/gsd/tests/verification-verdict.test.ts +78 -0
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +64 -12
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +47 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +135 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +47 -11
- package/src/resources/extensions/gsd/verification-verdict.ts +47 -0
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +54 -9
- package/src/resources/extensions/shared/html-shell.ts +412 -0
- package/src/resources/extensions/subagent/index.ts +567 -103
- package/src/resources/extensions/subagent/launch.ts +131 -0
- package/src/resources/extensions/subagent/run-store.ts +218 -0
- package/src/resources/extensions/subagent/tests/launch.test.ts +115 -0
- package/src/resources/extensions/subagent/tests/run-store.test.ts +111 -0
- package/src/resources/extensions/visual-brief/artifact-policy.ts +41 -0
- package/src/resources/extensions/visual-brief/extension-manifest.json +8 -0
- package/src/resources/extensions/visual-brief/index.ts +8 -0
- package/src/resources/extensions/visual-brief/page-contract.ts +136 -0
- package/src/resources/extensions/visual-brief/prompts.ts +183 -0
- package/src/resources/extensions/visual-brief/tests/visual-brief.test.ts +212 -0
- package/dist/web/standalone/.next/server/chunks/5822.js +0 -2
- package/dist/web/standalone/.next/static/chunks/2556.0527fea66e123b7f.js +0 -1
- package/dist/web/standalone/.next/static/chunks/app/layout-a16c7a7ecdf0c2cf.js +0 -1
- package/dist/web/standalone/.next/static/css/54ec2745c1da488b.css +0 -1
- package/dist/web/standalone/.next/static/css/de70bee13400563f.css +0 -1
- package/dist/web/standalone/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/747892c23ea88013-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/8d697b304b401681-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/9610d9e46709d722-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
- /package/dist/web/standalone/.next/static/{KDRTXR-22LPCsa80X9dey → BdZQhe8yKl6bdKLiXVEzh}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{KDRTXR-22LPCsa80X9dey → BdZQhe8yKl6bdKLiXVEzh}/_ssgManifest.js +0 -0
|
@@ -40,6 +40,7 @@ import { join } from "node:path";
|
|
|
40
40
|
import { resolveUokFlags } from "./uok/flags.js";
|
|
41
41
|
import { UokGateRunner } from "./uok/gate-runner.js";
|
|
42
42
|
import { verificationRetryKey } from "./auto/verification-retry-policy.js";
|
|
43
|
+
import { decideVerificationVerdict } from "./verification-verdict.js";
|
|
43
44
|
|
|
44
45
|
export interface VerificationContext {
|
|
45
46
|
s: AutoSession;
|
|
@@ -49,12 +50,6 @@ export interface VerificationContext {
|
|
|
49
50
|
|
|
50
51
|
export type VerificationResult = "continue" | "retry" | "pause";
|
|
51
52
|
|
|
52
|
-
function isInfraVerificationFailure(stderr: string): boolean {
|
|
53
|
-
return /\b(ENOENT|ENOTFOUND|ETIMEDOUT|ECONNRESET|EAI_AGAIN|spawn\s+\S+\s+ENOENT|command not found)\b/i.test(
|
|
54
|
-
stderr,
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
53
|
/**
|
|
59
54
|
* Post-unit guard for `validate-milestone` units (#4094).
|
|
60
55
|
*
|
|
@@ -196,7 +191,7 @@ async function countIncompleteSlices(basePath: string, milestoneId: string): Pro
|
|
|
196
191
|
/**
|
|
197
192
|
* Run the verification gate for the current execute-task unit.
|
|
198
193
|
* Returns:
|
|
199
|
-
* - "continue" —
|
|
194
|
+
* - "continue" — host-owned verification passed, proceed normally
|
|
200
195
|
* - "retry" — gate failed with retries remaining, s.pendingVerificationRetry set for loop re-iteration
|
|
201
196
|
* - "pause" — gate failed with retries exhausted, pauseAuto already called
|
|
202
197
|
*/
|
|
@@ -260,6 +255,11 @@ export async function runPostUnitVerification(
|
|
|
260
255
|
}
|
|
261
256
|
}
|
|
262
257
|
|
|
258
|
+
const verdict = decideVerificationVerdict(s.currentUnit.type, result);
|
|
259
|
+
if (!verdict.passed) {
|
|
260
|
+
result.passed = false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
263
|
if (uokFlags.gates) {
|
|
264
264
|
const gateRunner = new UokGateRunner();
|
|
265
265
|
gateRunner.register({
|
|
@@ -272,10 +272,12 @@ export async function runPostUnitVerification(
|
|
|
272
272
|
: "verification",
|
|
273
273
|
rationale: result.passed
|
|
274
274
|
? "verification checks passed"
|
|
275
|
-
:
|
|
275
|
+
: verdict.reason === "no-host-checks"
|
|
276
|
+
? "no runnable host-owned verification checks discovered"
|
|
277
|
+
: "verification checks failed",
|
|
276
278
|
findings: result.passed
|
|
277
279
|
? ""
|
|
278
|
-
: formatFailureContext(result),
|
|
280
|
+
: verdict.failureContext || formatFailureContext(result),
|
|
279
281
|
}),
|
|
280
282
|
});
|
|
281
283
|
|
|
@@ -343,13 +345,18 @@ export async function runPostUnitVerification(
|
|
|
343
345
|
writeVerificationJSON(result, tasksDir, tid, s.currentUnit.id);
|
|
344
346
|
} else {
|
|
345
347
|
const nextAttempt = attempt + 1;
|
|
348
|
+
const includeRetryMetadata =
|
|
349
|
+
!result.passed &&
|
|
350
|
+
verdict.retryable &&
|
|
351
|
+
autoFixEnabled &&
|
|
352
|
+
nextAttempt <= maxRetries;
|
|
346
353
|
writeVerificationJSON(
|
|
347
354
|
result,
|
|
348
355
|
tasksDir,
|
|
349
356
|
tid,
|
|
350
357
|
s.currentUnit.id,
|
|
351
|
-
nextAttempt,
|
|
352
|
-
maxRetries,
|
|
358
|
+
includeRetryMetadata ? nextAttempt : undefined,
|
|
359
|
+
includeRetryMetadata ? maxRetries : undefined,
|
|
353
360
|
);
|
|
354
361
|
}
|
|
355
362
|
}
|
|
@@ -358,26 +365,6 @@ export async function runPostUnitVerification(
|
|
|
358
365
|
}
|
|
359
366
|
}
|
|
360
367
|
|
|
361
|
-
const advisoryFailure =
|
|
362
|
-
!result.passed &&
|
|
363
|
-
(result.discoverySource === "package-json" ||
|
|
364
|
-
result.checks.some((check) =>
|
|
365
|
-
isInfraVerificationFailure(check.stderr),
|
|
366
|
-
));
|
|
367
|
-
|
|
368
|
-
if (advisoryFailure) {
|
|
369
|
-
s.verificationRetryCount.delete(retryKey);
|
|
370
|
-
s.verificationRetryFailureHashes.delete(retryKey);
|
|
371
|
-
s.pendingVerificationRetry = null;
|
|
372
|
-
ctx.ui.notify(
|
|
373
|
-
result.discoverySource === "package-json"
|
|
374
|
-
? "Verification failed in auto-discovered package.json checks — treating as advisory."
|
|
375
|
-
: "Verification failed due to infrastructure/runtime environment issues — treating as advisory.",
|
|
376
|
-
"warning",
|
|
377
|
-
);
|
|
378
|
-
return "continue";
|
|
379
|
-
}
|
|
380
|
-
|
|
381
368
|
// ── Post-execution checks (run after main verification passes for execute-task units) ──
|
|
382
369
|
let postExecChecks: PostExecutionCheckJSON[] | undefined;
|
|
383
370
|
let postExecBlockingFailure = false;
|
|
@@ -572,6 +559,17 @@ export async function runPostUnitVerification(
|
|
|
572
559
|
s.verificationRetryFailureHashes.delete(retryKey);
|
|
573
560
|
s.pendingVerificationRetry = null;
|
|
574
561
|
return "continue";
|
|
562
|
+
} else if (verdict.reason === "no-host-checks") {
|
|
563
|
+
s.verificationRetryCount.delete(retryKey);
|
|
564
|
+
s.verificationRetryFailureHashes.delete(retryKey);
|
|
565
|
+
s.pendingVerificationRetry = null;
|
|
566
|
+
ctx.ui.notify(
|
|
567
|
+
"Verification gate FAILED — no runnable host-owned verification checks were discovered. Pausing for human review.",
|
|
568
|
+
"error",
|
|
569
|
+
);
|
|
570
|
+
process.stderr.write(`verification-gate: ${verdict.failureContext}\n`);
|
|
571
|
+
await pauseAuto(ctx, pi);
|
|
572
|
+
return "pause";
|
|
575
573
|
} else if (postExecBlockingFailure) {
|
|
576
574
|
// Post-execution failures are cross-task consistency issues — retrying the same task won't fix them.
|
|
577
575
|
// Skip retry and pause immediately for human review.
|
|
@@ -589,7 +587,7 @@ export async function runPostUnitVerification(
|
|
|
589
587
|
s.verificationRetryCount.set(retryKey, nextAttempt);
|
|
590
588
|
s.pendingVerificationRetry = {
|
|
591
589
|
unitId: s.currentUnit.id,
|
|
592
|
-
failureContext: formatFailureContext(result),
|
|
590
|
+
failureContext: verdict.failureContext || formatFailureContext(result),
|
|
593
591
|
attempt: nextAttempt,
|
|
594
592
|
};
|
|
595
593
|
const failedCmds = result.checks
|
|
@@ -623,9 +621,13 @@ export async function runPostUnitVerification(
|
|
|
623
621
|
return "pause";
|
|
624
622
|
}
|
|
625
623
|
} catch (err) {
|
|
626
|
-
// Gate errors are non-fatal
|
|
627
624
|
logWarning("engine", `verification-gate error: ${(err as Error).message}`);
|
|
628
|
-
|
|
625
|
+
ctx.ui.notify(
|
|
626
|
+
`Verification gate errored before producing an authoritative verdict: ${(err as Error).message}`,
|
|
627
|
+
"error",
|
|
628
|
+
);
|
|
629
|
+
await pauseAuto(ctx, pi);
|
|
630
|
+
return "pause";
|
|
629
631
|
}
|
|
630
632
|
}
|
|
631
633
|
|
|
@@ -282,6 +282,50 @@ function gitRemoteExists(basePath: string, remote: string): boolean {
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
function findRegularMergeChangedPaths(basePath: string, milestoneBranch: string, mainBranch: string): Set<string> {
|
|
286
|
+
const changedPaths = new Set<string>();
|
|
287
|
+
let mergeLog = "";
|
|
288
|
+
try {
|
|
289
|
+
mergeLog = execFileSync("git", ["rev-list", "--merges", "--parents", mainBranch], {
|
|
290
|
+
cwd: basePath,
|
|
291
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
292
|
+
encoding: "utf-8",
|
|
293
|
+
}).trim();
|
|
294
|
+
} catch (err) {
|
|
295
|
+
logWarning("worktree", `regular merge lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
296
|
+
return changedPaths;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const line of mergeLog.split("\n").filter(Boolean)) {
|
|
300
|
+
const [mergeCommit, firstParent, ...otherParents] = line.split(" ");
|
|
301
|
+
if (!mergeCommit || !firstParent || otherParents.length === 0) continue;
|
|
302
|
+
const mergedMilestone = otherParents.some((parent) => {
|
|
303
|
+
try {
|
|
304
|
+
return nativeIsAncestor(basePath, milestoneBranch, parent);
|
|
305
|
+
} catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
if (!mergedMilestone) continue;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const output = execFileSync("git", ["diff", "--name-only", firstParent, mergeCommit], {
|
|
313
|
+
cwd: basePath,
|
|
314
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
315
|
+
encoding: "utf-8",
|
|
316
|
+
}).trim();
|
|
317
|
+
for (const path of output.split("\n").filter(Boolean)) {
|
|
318
|
+
if (!path.startsWith(".gsd/")) changedPaths.add(path);
|
|
319
|
+
}
|
|
320
|
+
} catch (err) {
|
|
321
|
+
logWarning("worktree", `regular merge diff lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
322
|
+
}
|
|
323
|
+
return changedPaths;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return changedPaths;
|
|
327
|
+
}
|
|
328
|
+
|
|
285
329
|
function clearProjectRootStateFiles(basePath: string, milestoneId: string): void {
|
|
286
330
|
const gsdDir = gsdRoot(basePath);
|
|
287
331
|
// Phase C pt 2: auto.lock removed from this list — the file is gone
|
|
@@ -1739,6 +1783,66 @@ export function mergeMilestoneToMain(
|
|
|
1739
1783
|
}
|
|
1740
1784
|
}
|
|
1741
1785
|
|
|
1786
|
+
// Already regular-merged milestones can skip the squash path and proceed to cleanup (#5831).
|
|
1787
|
+
if (nativeIsAncestor(originalBasePath_, milestoneBranch, mainBranch)) {
|
|
1788
|
+
const codeChanges = nativeDiffNumstat(
|
|
1789
|
+
originalBasePath_,
|
|
1790
|
+
mainBranch,
|
|
1791
|
+
milestoneBranch,
|
|
1792
|
+
).filter((entry) => !entry.path.startsWith(".gsd/"));
|
|
1793
|
+
if (codeChanges.length > 0) {
|
|
1794
|
+
const regularMergeChangedPaths = findRegularMergeChangedPaths(
|
|
1795
|
+
originalBasePath_,
|
|
1796
|
+
milestoneBranch,
|
|
1797
|
+
mainBranch,
|
|
1798
|
+
);
|
|
1799
|
+
const unanchoredCodeChanges = codeChanges.filter((entry) =>
|
|
1800
|
+
regularMergeChangedPaths.has(entry.path)
|
|
1801
|
+
);
|
|
1802
|
+
if (unanchoredCodeChanges.length > 0) {
|
|
1803
|
+
process.chdir(previousCwd);
|
|
1804
|
+
throw new GSDError(
|
|
1805
|
+
GSD_GIT_ERROR,
|
|
1806
|
+
`Milestone branch "${milestoneBranch}" is reachable from "${mainBranch}" ` +
|
|
1807
|
+
`but has ${unanchoredCodeChanges.length} milestone-touched code file(s) not on current "${mainBranch}". ` +
|
|
1808
|
+
`Aborting worktree teardown to prevent data loss.`,
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
debugLog("mergeMilestoneToMain", {
|
|
1813
|
+
action: "skip-squash-already-merged",
|
|
1814
|
+
milestoneId,
|
|
1815
|
+
milestoneBranch,
|
|
1816
|
+
mainBranch,
|
|
1817
|
+
});
|
|
1818
|
+
try {
|
|
1819
|
+
clearProjectRootStateFiles(originalBasePath_, milestoneId);
|
|
1820
|
+
} catch (err) {
|
|
1821
|
+
logWarning("worktree", `clearProjectRootStateFiles failed during already-merged cleanup: ${err instanceof Error ? err.message : String(err)}`);
|
|
1822
|
+
}
|
|
1823
|
+
try {
|
|
1824
|
+
removeWorktree(originalBasePath_, milestoneId, {
|
|
1825
|
+
branch: milestoneBranch,
|
|
1826
|
+
deleteBranch: false,
|
|
1827
|
+
});
|
|
1828
|
+
} catch (err) {
|
|
1829
|
+
logWarning("worktree", `worktree removal failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1830
|
+
}
|
|
1831
|
+
try {
|
|
1832
|
+
nativeBranchDelete(originalBasePath_, milestoneBranch);
|
|
1833
|
+
} catch (err) {
|
|
1834
|
+
logWarning("worktree", `git branch-delete failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1835
|
+
}
|
|
1836
|
+
setActiveWorkspace(null);
|
|
1837
|
+
nudgeGitBranchCache(previousCwd);
|
|
1838
|
+
try {
|
|
1839
|
+
process.chdir(originalBasePath_);
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
logWarning("worktree", `chdir to project root after already-merged cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1842
|
+
}
|
|
1843
|
+
return { commitMessage, pushed: false, prCreated: false, codeFilesChanged: true };
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1742
1846
|
// 7. Shelter queued milestone directories before the squash merge (#2505).
|
|
1743
1847
|
// The milestone branch may contain copies of queued milestone dirs (via
|
|
1744
1848
|
// copyPlanningArtifacts), so `git merge --squash` rejects when those same
|
|
@@ -240,12 +240,14 @@ import { runAutoLoopWithUok } from "./uok/kernel.js";
|
|
|
240
240
|
import { resolveUokFlags } from "./uok/flags.js";
|
|
241
241
|
import { validateDirectory } from "./validate-directory.js";
|
|
242
242
|
import { createAutoOrchestrator } from "./auto/orchestrator.js";
|
|
243
|
-
import type { AutoOrchestrationModule, AutoOrchestratorDeps } from "./auto/contracts.js";
|
|
243
|
+
import type { AutoOrchestrationModule, AutoOrchestratorDeps, DispatchAdapter } from "./auto/contracts.js";
|
|
244
244
|
import { reconcileBeforeDispatch } from "./state-reconciliation.js";
|
|
245
245
|
import { compileUnitToolContract } from "./tool-contract.js";
|
|
246
246
|
import { createWorktreeSafetyModule } from "./worktree-safety.js";
|
|
247
247
|
import { resolveManifest } from "./unit-context-manifest.js";
|
|
248
248
|
import { classifyFailure } from "./recovery-classification.js";
|
|
249
|
+
import { supportsStructuredQuestions } from "./workflow-mcp.js";
|
|
250
|
+
import type { MinimalModelRegistry } from "./context-budget.js";
|
|
249
251
|
// Slice-level parallelism (#2340)
|
|
250
252
|
import { getEligibleSlices } from "./slice-parallel-eligibility.js";
|
|
251
253
|
import { startSliceParallel } from "./slice-parallel-orchestrator.js";
|
|
@@ -1048,13 +1050,11 @@ export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void>
|
|
|
1048
1050
|
initHealthWidget(ctx);
|
|
1049
1051
|
}
|
|
1050
1052
|
|
|
1051
|
-
// ADR-016 phase 3 (#5693): the stop-path basePath restore routes
|
|
1052
|
-
// `Lifecycle.restoreToProjectRoot()`, the sole owner of
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
1055
|
-
//
|
|
1056
|
-
// The chdir stays here because `restoreToProjectRoot` is a pure
|
|
1057
|
-
// session-state mutation.
|
|
1053
|
+
// ADR-016 phase 3 (#5693): the stop-path basePath restore + chdir routes
|
|
1054
|
+
// through `Lifecycle.restoreToProjectRoot()`, the sole owner of both
|
|
1055
|
+
// `s.basePath` mutation and the paired `process.chdir` for auto-loop
|
|
1056
|
+
// transitions. The verb assigns `s.basePath` before any throwable work, so
|
|
1057
|
+
// a thrown error still leaves basePath restored.
|
|
1058
1058
|
if (s.originalBasePath) {
|
|
1059
1059
|
try {
|
|
1060
1060
|
buildLifecycle().restoreToProjectRoot();
|
|
@@ -1065,11 +1065,6 @@ export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void>
|
|
|
1065
1065
|
{ file: "auto.ts" },
|
|
1066
1066
|
);
|
|
1067
1067
|
}
|
|
1068
|
-
try {
|
|
1069
|
-
process.chdir(s.originalBasePath);
|
|
1070
|
-
} catch (err) {
|
|
1071
|
-
logWarning("engine", `basePath restore/chdir failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
1072
|
-
}
|
|
1073
1068
|
}
|
|
1074
1069
|
|
|
1075
1070
|
if (s.originalBasePath && s.cmdCtx) {
|
|
@@ -1384,20 +1379,14 @@ export async function stopAuto(
|
|
|
1384
1379
|
}
|
|
1385
1380
|
|
|
1386
1381
|
// ── Step 7: Restore basePath and chdir (ADR-016 phase 3, #5693) ──
|
|
1387
|
-
// `restoreToProjectRoot`
|
|
1388
|
-
// no
|
|
1382
|
+
// `restoreToProjectRoot` owns both s.basePath restore and process.chdir;
|
|
1383
|
+
// no paired chdir is needed at the call site.
|
|
1389
1384
|
if (s.originalBasePath) {
|
|
1390
1385
|
try {
|
|
1391
1386
|
buildLifecycle().restoreToProjectRoot();
|
|
1392
1387
|
} catch (e) {
|
|
1393
1388
|
debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
|
|
1394
1389
|
}
|
|
1395
|
-
try {
|
|
1396
|
-
process.chdir(s.basePath);
|
|
1397
|
-
} catch (err) {
|
|
1398
|
-
/* best-effort */
|
|
1399
|
-
logWarning("engine", `chdir failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
1400
|
-
}
|
|
1401
1390
|
}
|
|
1402
1391
|
|
|
1403
1392
|
// Re-root the active command session/tool runtime after worktree teardown.
|
|
@@ -1792,6 +1781,75 @@ function buildLifecycle(): WorktreeLifecycle {
|
|
|
1792
1781
|
return new WorktreeLifecycle(s, buildWorktreeLifecycleDeps());
|
|
1793
1782
|
}
|
|
1794
1783
|
|
|
1784
|
+
/**
|
|
1785
|
+
* Build the production `DispatchAdapter` used by `createWiredAutoOrchestrationModule`.
|
|
1786
|
+
*
|
|
1787
|
+
* Exported so tests can verify parity with `runDispatch`'s `resolveDispatch` call —
|
|
1788
|
+
* the wired adapter must derive `structuredQuestionsAvailable`, `sessionContextWindow`,
|
|
1789
|
+
* `sessionProvider`, and `modelRegistry` the same way phases.ts:runDispatch does.
|
|
1790
|
+
*/
|
|
1791
|
+
export function createWiredDispatchAdapter(
|
|
1792
|
+
ctx: ExtensionContext,
|
|
1793
|
+
pi: ExtensionAPI,
|
|
1794
|
+
dispatchBasePath: string,
|
|
1795
|
+
): DispatchAdapter {
|
|
1796
|
+
return {
|
|
1797
|
+
async decideNextUnit(input) {
|
|
1798
|
+
const state = input.stateSnapshot;
|
|
1799
|
+
const active = state.activeMilestone;
|
|
1800
|
+
if (!active) return null;
|
|
1801
|
+
|
|
1802
|
+
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1803
|
+
|
|
1804
|
+
// Derive session-derived dispatch inputs the same way phases.ts:runDispatch does
|
|
1805
|
+
// (#5789). Prefer caller-supplied values when present so test harnesses and
|
|
1806
|
+
// alternative wirings can inject deterministic snapshots; otherwise pull from
|
|
1807
|
+
// the captured pi/ctx references.
|
|
1808
|
+
const sessionProvider = input.sessionProvider ?? ctx.model?.provider;
|
|
1809
|
+
const sessionContextWindow = input.sessionContextWindow ?? ctx.model?.contextWindow;
|
|
1810
|
+
const modelRegistry = input.modelRegistry ?? (ctx.modelRegistry as MinimalModelRegistry | undefined);
|
|
1811
|
+
const authMode =
|
|
1812
|
+
sessionProvider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
|
|
1813
|
+
? ctx.modelRegistry.getProviderAuthMode(sessionProvider)
|
|
1814
|
+
: undefined;
|
|
1815
|
+
const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
|
|
1816
|
+
// Mirrors runDispatch: deep-planning keeps approval gates in plain chat
|
|
1817
|
+
// because structured questions can be cancelled outside the chat turn on
|
|
1818
|
+
// some transports.
|
|
1819
|
+
const structuredQuestionsAvailable =
|
|
1820
|
+
input.structuredQuestionsAvailable ??
|
|
1821
|
+
(prefs?.planning_depth === "deep"
|
|
1822
|
+
? "false"
|
|
1823
|
+
: supportsStructuredQuestions(activeTools, {
|
|
1824
|
+
authMode,
|
|
1825
|
+
baseUrl: ctx.model?.baseUrl,
|
|
1826
|
+
})
|
|
1827
|
+
? "true"
|
|
1828
|
+
: "false");
|
|
1829
|
+
|
|
1830
|
+
const action = await resolveDispatch({
|
|
1831
|
+
basePath: dispatchBasePath,
|
|
1832
|
+
mid: active.id,
|
|
1833
|
+
midTitle: active.title,
|
|
1834
|
+
state,
|
|
1835
|
+
prefs,
|
|
1836
|
+
structuredQuestionsAvailable,
|
|
1837
|
+
sessionContextWindow,
|
|
1838
|
+
sessionProvider,
|
|
1839
|
+
modelRegistry,
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
if (action.action !== "dispatch") return null;
|
|
1843
|
+
return {
|
|
1844
|
+
unitType: action.unitType,
|
|
1845
|
+
unitId: action.unitId,
|
|
1846
|
+
reason: action.matchedRule ?? "dispatch",
|
|
1847
|
+
preconditions: [],
|
|
1848
|
+
};
|
|
1849
|
+
},
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1795
1853
|
/**
|
|
1796
1854
|
* Thin entry glue for the new Auto Orchestration module.
|
|
1797
1855
|
*
|
|
@@ -1801,7 +1859,7 @@ function buildLifecycle(): WorktreeLifecycle {
|
|
|
1801
1859
|
*/
|
|
1802
1860
|
export function createWiredAutoOrchestrationModule(
|
|
1803
1861
|
ctx: ExtensionContext,
|
|
1804
|
-
|
|
1862
|
+
pi: ExtensionAPI,
|
|
1805
1863
|
dispatchBasePath: string,
|
|
1806
1864
|
runtimeBasePath = resolveProjectRoot(dispatchBasePath),
|
|
1807
1865
|
): AutoOrchestrationModule {
|
|
@@ -1830,30 +1888,7 @@ export function createWiredAutoOrchestrationModule(
|
|
|
1830
1888
|
};
|
|
1831
1889
|
},
|
|
1832
1890
|
},
|
|
1833
|
-
dispatch:
|
|
1834
|
-
async decideNextUnit(input) {
|
|
1835
|
-
const state = input.stateSnapshot;
|
|
1836
|
-
const active = state.activeMilestone;
|
|
1837
|
-
if (!active) return null;
|
|
1838
|
-
|
|
1839
|
-
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1840
|
-
const action = await resolveDispatch({
|
|
1841
|
-
basePath: dispatchBasePath,
|
|
1842
|
-
mid: active.id,
|
|
1843
|
-
midTitle: active.title,
|
|
1844
|
-
state,
|
|
1845
|
-
prefs,
|
|
1846
|
-
});
|
|
1847
|
-
|
|
1848
|
-
if (action.action !== "dispatch") return null;
|
|
1849
|
-
return {
|
|
1850
|
-
unitType: action.unitType,
|
|
1851
|
-
unitId: action.unitId,
|
|
1852
|
-
reason: action.matchedRule ?? "dispatch",
|
|
1853
|
-
preconditions: [],
|
|
1854
|
-
};
|
|
1855
|
-
},
|
|
1856
|
-
},
|
|
1891
|
+
dispatch: createWiredDispatchAdapter(ctx, pi, dispatchBasePath),
|
|
1857
1892
|
recovery: {
|
|
1858
1893
|
async classifyAndRecover(input) {
|
|
1859
1894
|
const recovery = classifyFailure(input);
|
|
@@ -1902,12 +1937,25 @@ export function createWiredAutoOrchestrationModule(
|
|
|
1902
1937
|
async cleanupOnStop() {},
|
|
1903
1938
|
},
|
|
1904
1939
|
health: {
|
|
1940
|
+
checkResourcesStale() {
|
|
1941
|
+
return checkResourcesStale(s.resourceVersionOnStart);
|
|
1942
|
+
},
|
|
1905
1943
|
async preAdvanceGate() {
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1944
|
+
try {
|
|
1945
|
+
const gate = await preDispatchHealthGate(dispatchBasePath);
|
|
1946
|
+
if (gate.proceed) {
|
|
1947
|
+
return {
|
|
1948
|
+
kind: "pass",
|
|
1949
|
+
fixesApplied: gate.fixesApplied,
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
return {
|
|
1953
|
+
kind: "fail",
|
|
1954
|
+
reason: gate.reason ?? "Pre-dispatch health check failed — run /gsd doctor for details.",
|
|
1955
|
+
};
|
|
1956
|
+
} catch (error) {
|
|
1957
|
+
return { kind: "threw", error };
|
|
1958
|
+
}
|
|
1911
1959
|
},
|
|
1912
1960
|
async postAdvanceRecord(result) {
|
|
1913
1961
|
if (result.kind === "error") {
|
|
@@ -1975,6 +2023,43 @@ export function createWiredAutoOrchestrationModule(
|
|
|
1975
2023
|
}
|
|
1976
2024
|
},
|
|
1977
2025
|
},
|
|
2026
|
+
uokGate: {
|
|
2027
|
+
async emit(input) {
|
|
2028
|
+
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
2029
|
+
const uokFlags = resolveUokFlags(prefs);
|
|
2030
|
+
if (!uokFlags.gates) return;
|
|
2031
|
+
const milestoneId = input.milestoneId ?? s.currentMilestoneId ?? undefined;
|
|
2032
|
+
try {
|
|
2033
|
+
const { UokGateRunner } = await import("./uok/gate-runner.js");
|
|
2034
|
+
const runner = new UokGateRunner();
|
|
2035
|
+
runner.register({
|
|
2036
|
+
id: input.gateId,
|
|
2037
|
+
type: input.gateType,
|
|
2038
|
+
execute: async () => ({
|
|
2039
|
+
outcome: input.outcome,
|
|
2040
|
+
failureClass: input.failureClass,
|
|
2041
|
+
rationale: input.rationale,
|
|
2042
|
+
findings: input.findings ?? "",
|
|
2043
|
+
}),
|
|
2044
|
+
});
|
|
2045
|
+
await runner.run(input.gateId, {
|
|
2046
|
+
basePath: dispatchBasePath,
|
|
2047
|
+
traceId: `pre-dispatch:${flowId}`,
|
|
2048
|
+
turnId: `orch-${seq}`,
|
|
2049
|
+
milestoneId,
|
|
2050
|
+
unitType: "pre-dispatch",
|
|
2051
|
+
unitId: `orch-${seq}`,
|
|
2052
|
+
});
|
|
2053
|
+
} catch (err) {
|
|
2054
|
+
logWarning("engine", `uok gate emit failed: ${getErrorMessage(err)}`, {
|
|
2055
|
+
file: "auto.ts",
|
|
2056
|
+
gateId: input.gateId,
|
|
2057
|
+
gateType: input.gateType,
|
|
2058
|
+
...(milestoneId ? { milestoneId } : {}),
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
},
|
|
2062
|
+
},
|
|
1978
2063
|
};
|
|
1979
2064
|
|
|
1980
2065
|
return createAutoOrchestrator(deps);
|
|
@@ -48,6 +48,11 @@ const MAX_NETWORK_RETRIES = 2;
|
|
|
48
48
|
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
49
49
|
return !!value && typeof value === "object";
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
export function _hasEmptyAgentEndContent(content: unknown): boolean {
|
|
53
|
+
return content == null || (Array.isArray(content) && content.length === 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
/**
|
|
52
57
|
* Cap on auto-resume attempts for sustained transient-provider errors.
|
|
53
58
|
*
|
|
@@ -310,7 +315,7 @@ export async function handleAgentEnd(
|
|
|
310
315
|
// that carry error context — e.g. errorMessage field or non-empty content
|
|
311
316
|
// indicating a mid-stream failure. (#2695)
|
|
312
317
|
const content = "content" in lastMsg ? lastMsg.content : undefined;
|
|
313
|
-
const hasEmptyContent =
|
|
318
|
+
const hasEmptyContent = _hasEmptyAgentEndContent(content);
|
|
314
319
|
const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
|
|
315
320
|
|
|
316
321
|
if (hasEmptyContent && !hasErrorMessage) {
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
export function extractSubagentAgentClasses(input: unknown): string[] {
|
|
2
2
|
if (!input || typeof input !== "object") return [];
|
|
3
3
|
|
|
4
|
-
const record = input as Record<string, unknown>;
|
|
5
4
|
const agentClasses: string[] = [];
|
|
5
|
+
const visited = new WeakSet<object>();
|
|
6
6
|
const addAgentClass = (value: unknown): void => {
|
|
7
7
|
if (typeof value === "string" && value.trim().length > 0) agentClasses.push(value.trim());
|
|
8
8
|
};
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
const visitItems = (value: unknown): void => {
|
|
10
11
|
if (!Array.isArray(value)) return;
|
|
11
12
|
for (const item of value) {
|
|
12
|
-
|
|
13
|
+
visit(item);
|
|
13
14
|
}
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const visit = (value: unknown): void => {
|
|
18
|
+
if (!value || typeof value !== "object") return;
|
|
19
|
+
if (visited.has(value)) return;
|
|
20
|
+
visited.add(value);
|
|
21
|
+
const record = value as Record<string, unknown>;
|
|
22
|
+
addAgentClass(record.agent);
|
|
23
|
+
visitItems(record.tasks);
|
|
24
|
+
visitItems(record.chain);
|
|
25
|
+
visitItems(record.parallel);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
visit(input);
|
|
19
29
|
return agentClasses;
|
|
20
30
|
}
|
|
@@ -5,7 +5,7 @@ import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import { minimatch } from "minimatch";
|
|
6
6
|
|
|
7
7
|
import { getIsolationMode } from "../preferences.js";
|
|
8
|
-
import type
|
|
8
|
+
import { compileSubagentPermissionContract, type ToolsPolicy } from "../unit-context-manifest.js";
|
|
9
9
|
import { logWarning } from "../workflow-logger.js";
|
|
10
10
|
import { isGsdWorktreePath, resolveWorktreeProjectRoot } from "../worktree-root.js";
|
|
11
11
|
|
|
@@ -65,6 +65,7 @@ const QUEUE_SAFE_TOOLS = new Set([
|
|
|
65
65
|
* true / false — shell no-ops / test exit codes
|
|
66
66
|
*/
|
|
67
67
|
const BASH_READ_ONLY_RE = /^\s*(cat|head|tail|less|more|wc|file|stat|du|df|which|type|echo|printf|ls|find|grep|rg|awk|sed\b(?!.*-i)|sort|uniq|diff|comm|tr|cut|tee\s+-a\s+\/dev\/null|git\s+(log|show|diff|status|branch|tag|remote|rev-parse|ls-files|blame|shortlog|describe|stash\s+list|config\s+--get|cat-file)|gh\s+(issue|pr|api|repo|release)\s+(view|list|diff|status|checks)|mkdir\s+-p\s+\.gsd|rtk\s|npm\s+run\s+(test|test:\w+|lint|lint:\w+|typecheck|type-check|type-check:\w+|check|verify|audit|outdated|format:check|ci|validate)\b|npm\s+(ls|list|info|view|show|outdated|audit|explain|doctor|ping|--version|-v)\b|npx\s|tsx\s|node\s+(--print|--version|-v\b)|python[23]?\s+(-c\s+'[^']*'|--version|-V\b|-m\s+(pip\s+show|pip\s+list|site))|pip[23]?\s+(show|list|freeze|check|index\s+versions)\b|jq\s|yq\s|curl\s+(-s\b|--silent\b)(?!\s+[^|>]*\s-[oO]\b)(?!\s+[^|>]*\s--output\b)[^|>]*$|openssl\s+(version|x509|s_client)|env\b|printenv\b|true\b|false\b)/;
|
|
68
|
+
const BASH_VERIFICATION_RE = /^\s*(npm\s+(run\s+(build|test|test:\w+|lint|lint:\w+|typecheck|type-check|verify|ci|validate)\b|test\b)|pnpm\s+(build|test|lint|typecheck|verify)\b|yarn\s+(build|test|lint|typecheck|verify)\b|vitest\b|jest\b|go\s+test\b)/;
|
|
68
69
|
|
|
69
70
|
interface InMemoryWriteGateState {
|
|
70
71
|
verifiedDepthMilestones: Set<string>;
|
|
@@ -767,6 +768,9 @@ function blockReason(unitType: string, mode: string, what: string): string {
|
|
|
767
768
|
* and listed in the policy's allowedSubagents.
|
|
768
769
|
* - "docs" → like "planning" but also allows writes to paths
|
|
769
770
|
* matching `allowedPathGlobs` relative to basePath.
|
|
771
|
+
* - "verification"
|
|
772
|
+
* → allows Bash for project verification commands, but keeps
|
|
773
|
+
* writes restricted to .gsd/ and blocks subagent dispatch.
|
|
770
774
|
*
|
|
771
775
|
* `pathOrCommand` is the file path for write/edit-shaped tools and the
|
|
772
776
|
* shell command for bash. Other tools ignore this argument.
|
|
@@ -804,14 +808,15 @@ export function shouldBlockPlanningUnit(
|
|
|
804
808
|
return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
|
|
805
809
|
}
|
|
806
810
|
|
|
807
|
-
// planning / planning-dispatch / docs modes share the same surface for safe tools, bash, and subagent.
|
|
811
|
+
// planning / planning-dispatch / docs / verification modes share the same surface for safe tools, bash, and subagent.
|
|
808
812
|
if (PLANNING_SAFE_TOOLS.has(tool)) return { block: false };
|
|
809
813
|
if (tool.startsWith("gsd_")) return { block: false };
|
|
810
814
|
|
|
811
815
|
if (PLANNING_SUBAGENT_TOOLS.has(tool)) {
|
|
812
816
|
if (policy.mode === "planning-dispatch") {
|
|
813
817
|
const requested = (agentClasses ?? []).map(a => a.trim()).filter(Boolean);
|
|
814
|
-
const
|
|
818
|
+
const dispatchContract = compileSubagentPermissionContract(policy);
|
|
819
|
+
const allowedSubagents = dispatchContract.allowedSubagents;
|
|
815
820
|
const allowed = new Set(allowedSubagents);
|
|
816
821
|
// When agentClasses is undefined, the caller has not been updated to extract
|
|
817
822
|
// agent identities yet. Block and warn so stale callers surface in telemetry
|
|
@@ -861,6 +866,17 @@ export function shouldBlockPlanningUnit(
|
|
|
861
866
|
}
|
|
862
867
|
|
|
863
868
|
if (tool === "bash") {
|
|
869
|
+
if (policy.mode === "verification") {
|
|
870
|
+
if (BASH_VERIFICATION_RE.test(pathOrCommand) || BASH_READ_ONLY_RE.test(pathOrCommand)) return { block: false };
|
|
871
|
+
return {
|
|
872
|
+
block: true,
|
|
873
|
+
reason: blockReason(
|
|
874
|
+
unitType,
|
|
875
|
+
policy.mode,
|
|
876
|
+
`bash is restricted to build/test verification commands (npm run build, npm test, etc.); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`,
|
|
877
|
+
),
|
|
878
|
+
};
|
|
879
|
+
}
|
|
864
880
|
if (BASH_READ_ONLY_RE.test(pathOrCommand)) return { block: false };
|
|
865
881
|
return {
|
|
866
882
|
block: true,
|