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
|
@@ -18,9 +18,7 @@ import { join } from "node:path";
|
|
|
18
18
|
import { resolveUokFlags } from "./uok/flags.js";
|
|
19
19
|
import { UokGateRunner } from "./uok/gate-runner.js";
|
|
20
20
|
import { verificationRetryKey } from "./auto/verification-retry-policy.js";
|
|
21
|
-
|
|
22
|
-
return /\b(ENOENT|ENOTFOUND|ETIMEDOUT|ECONNRESET|EAI_AGAIN|spawn\s+\S+\s+ENOENT|command not found)\b/i.test(stderr);
|
|
23
|
-
}
|
|
21
|
+
import { decideVerificationVerdict } from "./verification-verdict.js";
|
|
24
22
|
/**
|
|
25
23
|
* Post-unit guard for `validate-milestone` units (#4094).
|
|
26
24
|
*
|
|
@@ -128,7 +126,7 @@ async function countIncompleteSlices(basePath, milestoneId) {
|
|
|
128
126
|
/**
|
|
129
127
|
* Run the verification gate for the current execute-task unit.
|
|
130
128
|
* Returns:
|
|
131
|
-
* - "continue" —
|
|
129
|
+
* - "continue" — host-owned verification passed, proceed normally
|
|
132
130
|
* - "retry" — gate failed with retries remaining, s.pendingVerificationRetry set for loop re-iteration
|
|
133
131
|
* - "pause" — gate failed with retries exhausted, pauseAuto already called
|
|
134
132
|
*/
|
|
@@ -178,6 +176,10 @@ export async function runPostUnitVerification(vctx, pauseAuto) {
|
|
|
178
176
|
process.stderr.write(` [${w.severity}] ${w.name}: ${w.title}\n`);
|
|
179
177
|
}
|
|
180
178
|
}
|
|
179
|
+
const verdict = decideVerificationVerdict(s.currentUnit.type, result);
|
|
180
|
+
if (!verdict.passed) {
|
|
181
|
+
result.passed = false;
|
|
182
|
+
}
|
|
181
183
|
if (uokFlags.gates) {
|
|
182
184
|
const gateRunner = new UokGateRunner();
|
|
183
185
|
gateRunner.register({
|
|
@@ -190,10 +192,12 @@ export async function runPostUnitVerification(vctx, pauseAuto) {
|
|
|
190
192
|
: "verification",
|
|
191
193
|
rationale: result.passed
|
|
192
194
|
? "verification checks passed"
|
|
193
|
-
:
|
|
195
|
+
: verdict.reason === "no-host-checks"
|
|
196
|
+
? "no runnable host-owned verification checks discovered"
|
|
197
|
+
: "verification checks failed",
|
|
194
198
|
findings: result.passed
|
|
195
199
|
? ""
|
|
196
|
-
: formatFailureContext(result),
|
|
200
|
+
: verdict.failureContext || formatFailureContext(result),
|
|
197
201
|
}),
|
|
198
202
|
});
|
|
199
203
|
await gateRunner.run("verification-gate", {
|
|
@@ -251,7 +255,11 @@ export async function runPostUnitVerification(vctx, pauseAuto) {
|
|
|
251
255
|
}
|
|
252
256
|
else {
|
|
253
257
|
const nextAttempt = attempt + 1;
|
|
254
|
-
|
|
258
|
+
const includeRetryMetadata = !result.passed &&
|
|
259
|
+
verdict.retryable &&
|
|
260
|
+
autoFixEnabled &&
|
|
261
|
+
nextAttempt <= maxRetries;
|
|
262
|
+
writeVerificationJSON(result, tasksDir, tid, s.currentUnit.id, includeRetryMetadata ? nextAttempt : undefined, includeRetryMetadata ? maxRetries : undefined);
|
|
255
263
|
}
|
|
256
264
|
}
|
|
257
265
|
}
|
|
@@ -259,18 +267,6 @@ export async function runPostUnitVerification(vctx, pauseAuto) {
|
|
|
259
267
|
logWarning("engine", `verification-evidence write error: ${evidenceErr.message}`);
|
|
260
268
|
}
|
|
261
269
|
}
|
|
262
|
-
const advisoryFailure = !result.passed &&
|
|
263
|
-
(result.discoverySource === "package-json" ||
|
|
264
|
-
result.checks.some((check) => isInfraVerificationFailure(check.stderr)));
|
|
265
|
-
if (advisoryFailure) {
|
|
266
|
-
s.verificationRetryCount.delete(retryKey);
|
|
267
|
-
s.verificationRetryFailureHashes.delete(retryKey);
|
|
268
|
-
s.pendingVerificationRetry = null;
|
|
269
|
-
ctx.ui.notify(result.discoverySource === "package-json"
|
|
270
|
-
? "Verification failed in auto-discovered package.json checks — treating as advisory."
|
|
271
|
-
: "Verification failed due to infrastructure/runtime environment issues — treating as advisory.", "warning");
|
|
272
|
-
return "continue";
|
|
273
|
-
}
|
|
274
270
|
// ── Post-execution checks (run after main verification passes for execute-task units) ──
|
|
275
271
|
let postExecChecks;
|
|
276
272
|
let postExecBlockingFailure = false;
|
|
@@ -430,6 +426,15 @@ export async function runPostUnitVerification(vctx, pauseAuto) {
|
|
|
430
426
|
s.pendingVerificationRetry = null;
|
|
431
427
|
return "continue";
|
|
432
428
|
}
|
|
429
|
+
else if (verdict.reason === "no-host-checks") {
|
|
430
|
+
s.verificationRetryCount.delete(retryKey);
|
|
431
|
+
s.verificationRetryFailureHashes.delete(retryKey);
|
|
432
|
+
s.pendingVerificationRetry = null;
|
|
433
|
+
ctx.ui.notify("Verification gate FAILED — no runnable host-owned verification checks were discovered. Pausing for human review.", "error");
|
|
434
|
+
process.stderr.write(`verification-gate: ${verdict.failureContext}\n`);
|
|
435
|
+
await pauseAuto(ctx, pi);
|
|
436
|
+
return "pause";
|
|
437
|
+
}
|
|
433
438
|
else if (postExecBlockingFailure) {
|
|
434
439
|
// Post-execution failures are cross-task consistency issues — retrying the same task won't fix them.
|
|
435
440
|
// Skip retry and pause immediately for human review.
|
|
@@ -445,7 +450,7 @@ export async function runPostUnitVerification(vctx, pauseAuto) {
|
|
|
445
450
|
s.verificationRetryCount.set(retryKey, nextAttempt);
|
|
446
451
|
s.pendingVerificationRetry = {
|
|
447
452
|
unitId: s.currentUnit.id,
|
|
448
|
-
failureContext: formatFailureContext(result),
|
|
453
|
+
failureContext: verdict.failureContext || formatFailureContext(result),
|
|
449
454
|
attempt: nextAttempt,
|
|
450
455
|
};
|
|
451
456
|
const failedCmds = result.checks
|
|
@@ -475,9 +480,10 @@ export async function runPostUnitVerification(vctx, pauseAuto) {
|
|
|
475
480
|
}
|
|
476
481
|
}
|
|
477
482
|
catch (err) {
|
|
478
|
-
// Gate errors are non-fatal
|
|
479
483
|
logWarning("engine", `verification-gate error: ${err.message}`);
|
|
480
|
-
|
|
484
|
+
ctx.ui.notify(`Verification gate errored before producing an authoritative verdict: ${err.message}`, "error");
|
|
485
|
+
await pauseAuto(ctx, pi);
|
|
486
|
+
return "pause";
|
|
481
487
|
}
|
|
482
488
|
}
|
|
483
489
|
/**
|
|
@@ -204,6 +204,52 @@ function gitRemoteExists(basePath, remote) {
|
|
|
204
204
|
return false;
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
|
+
function findRegularMergeChangedPaths(basePath, milestoneBranch, mainBranch) {
|
|
208
|
+
const changedPaths = new Set();
|
|
209
|
+
let mergeLog = "";
|
|
210
|
+
try {
|
|
211
|
+
mergeLog = execFileSync("git", ["rev-list", "--merges", "--parents", mainBranch], {
|
|
212
|
+
cwd: basePath,
|
|
213
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
214
|
+
encoding: "utf-8",
|
|
215
|
+
}).trim();
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
logWarning("worktree", `regular merge lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
219
|
+
return changedPaths;
|
|
220
|
+
}
|
|
221
|
+
for (const line of mergeLog.split("\n").filter(Boolean)) {
|
|
222
|
+
const [mergeCommit, firstParent, ...otherParents] = line.split(" ");
|
|
223
|
+
if (!mergeCommit || !firstParent || otherParents.length === 0)
|
|
224
|
+
continue;
|
|
225
|
+
const mergedMilestone = otherParents.some((parent) => {
|
|
226
|
+
try {
|
|
227
|
+
return nativeIsAncestor(basePath, milestoneBranch, parent);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
if (!mergedMilestone)
|
|
234
|
+
continue;
|
|
235
|
+
try {
|
|
236
|
+
const output = execFileSync("git", ["diff", "--name-only", firstParent, mergeCommit], {
|
|
237
|
+
cwd: basePath,
|
|
238
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
239
|
+
encoding: "utf-8",
|
|
240
|
+
}).trim();
|
|
241
|
+
for (const path of output.split("\n").filter(Boolean)) {
|
|
242
|
+
if (!path.startsWith(".gsd/"))
|
|
243
|
+
changedPaths.add(path);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
logWarning("worktree", `regular merge diff lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
248
|
+
}
|
|
249
|
+
return changedPaths;
|
|
250
|
+
}
|
|
251
|
+
return changedPaths;
|
|
252
|
+
}
|
|
207
253
|
function clearProjectRootStateFiles(basePath, milestoneId) {
|
|
208
254
|
const gsdDir = gsdRoot(basePath);
|
|
209
255
|
// Phase C pt 2: auto.lock removed from this list — the file is gone
|
|
@@ -1482,6 +1528,56 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
|
|
|
1482
1528
|
});
|
|
1483
1529
|
}
|
|
1484
1530
|
}
|
|
1531
|
+
// Already regular-merged milestones can skip the squash path and proceed to cleanup (#5831).
|
|
1532
|
+
if (nativeIsAncestor(originalBasePath_, milestoneBranch, mainBranch)) {
|
|
1533
|
+
const codeChanges = nativeDiffNumstat(originalBasePath_, mainBranch, milestoneBranch).filter((entry) => !entry.path.startsWith(".gsd/"));
|
|
1534
|
+
if (codeChanges.length > 0) {
|
|
1535
|
+
const regularMergeChangedPaths = findRegularMergeChangedPaths(originalBasePath_, milestoneBranch, mainBranch);
|
|
1536
|
+
const unanchoredCodeChanges = codeChanges.filter((entry) => regularMergeChangedPaths.has(entry.path));
|
|
1537
|
+
if (unanchoredCodeChanges.length > 0) {
|
|
1538
|
+
process.chdir(previousCwd);
|
|
1539
|
+
throw new GSDError(GSD_GIT_ERROR, `Milestone branch "${milestoneBranch}" is reachable from "${mainBranch}" ` +
|
|
1540
|
+
`but has ${unanchoredCodeChanges.length} milestone-touched code file(s) not on current "${mainBranch}". ` +
|
|
1541
|
+
`Aborting worktree teardown to prevent data loss.`);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
debugLog("mergeMilestoneToMain", {
|
|
1545
|
+
action: "skip-squash-already-merged",
|
|
1546
|
+
milestoneId,
|
|
1547
|
+
milestoneBranch,
|
|
1548
|
+
mainBranch,
|
|
1549
|
+
});
|
|
1550
|
+
try {
|
|
1551
|
+
clearProjectRootStateFiles(originalBasePath_, milestoneId);
|
|
1552
|
+
}
|
|
1553
|
+
catch (err) {
|
|
1554
|
+
logWarning("worktree", `clearProjectRootStateFiles failed during already-merged cleanup: ${err instanceof Error ? err.message : String(err)}`);
|
|
1555
|
+
}
|
|
1556
|
+
try {
|
|
1557
|
+
removeWorktree(originalBasePath_, milestoneId, {
|
|
1558
|
+
branch: milestoneBranch,
|
|
1559
|
+
deleteBranch: false,
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
catch (err) {
|
|
1563
|
+
logWarning("worktree", `worktree removal failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1564
|
+
}
|
|
1565
|
+
try {
|
|
1566
|
+
nativeBranchDelete(originalBasePath_, milestoneBranch);
|
|
1567
|
+
}
|
|
1568
|
+
catch (err) {
|
|
1569
|
+
logWarning("worktree", `git branch-delete failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1570
|
+
}
|
|
1571
|
+
setActiveWorkspace(null);
|
|
1572
|
+
nudgeGitBranchCache(previousCwd);
|
|
1573
|
+
try {
|
|
1574
|
+
process.chdir(originalBasePath_);
|
|
1575
|
+
}
|
|
1576
|
+
catch (err) {
|
|
1577
|
+
logWarning("worktree", `chdir to project root after already-merged cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1578
|
+
}
|
|
1579
|
+
return { commitMessage, pushed: false, prCreated: false, codeFilesChanged: true };
|
|
1580
|
+
}
|
|
1485
1581
|
// 7. Shelter queued milestone directories before the squash merge (#2505).
|
|
1486
1582
|
// The milestone branch may contain copies of queued milestone dirs (via
|
|
1487
1583
|
// copyPlanningArtifacts), so `git merge --squash` rejects when those same
|
|
@@ -92,6 +92,7 @@ import { compileUnitToolContract } from "./tool-contract.js";
|
|
|
92
92
|
import { createWorktreeSafetyModule } from "./worktree-safety.js";
|
|
93
93
|
import { resolveManifest } from "./unit-context-manifest.js";
|
|
94
94
|
import { classifyFailure } from "./recovery-classification.js";
|
|
95
|
+
import { supportsStructuredQuestions } from "./workflow-mcp.js";
|
|
95
96
|
import { WorktreeLifecycle, } from "./worktree-lifecycle.js";
|
|
96
97
|
import { WorktreeStateProjection } from "./worktree-state-projection.js";
|
|
97
98
|
import { reorderForCaching } from "./prompt-ordering.js";
|
|
@@ -711,13 +712,11 @@ export async function cleanupAfterLoopExit(ctx) {
|
|
|
711
712
|
}
|
|
712
713
|
initHealthWidget(ctx);
|
|
713
714
|
}
|
|
714
|
-
// ADR-016 phase 3 (#5693): the stop-path basePath restore routes
|
|
715
|
-
// `Lifecycle.restoreToProjectRoot()`, the sole owner of
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
// The chdir stays here because `restoreToProjectRoot` is a pure
|
|
720
|
-
// session-state mutation.
|
|
715
|
+
// ADR-016 phase 3 (#5693): the stop-path basePath restore + chdir routes
|
|
716
|
+
// through `Lifecycle.restoreToProjectRoot()`, the sole owner of both
|
|
717
|
+
// `s.basePath` mutation and the paired `process.chdir` for auto-loop
|
|
718
|
+
// transitions. The verb assigns `s.basePath` before any throwable work, so
|
|
719
|
+
// a thrown error still leaves basePath restored.
|
|
721
720
|
if (s.originalBasePath) {
|
|
722
721
|
try {
|
|
723
722
|
buildLifecycle().restoreToProjectRoot();
|
|
@@ -725,12 +724,6 @@ export async function cleanupAfterLoopExit(ctx) {
|
|
|
725
724
|
catch (err) {
|
|
726
725
|
logWarning("engine", `restore project root failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
727
726
|
}
|
|
728
|
-
try {
|
|
729
|
-
process.chdir(s.originalBasePath);
|
|
730
|
-
}
|
|
731
|
-
catch (err) {
|
|
732
|
-
logWarning("engine", `basePath restore/chdir failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
733
|
-
}
|
|
734
727
|
}
|
|
735
728
|
if (s.originalBasePath && s.cmdCtx) {
|
|
736
729
|
const result = await rerootCommandSession(s.cmdCtx, s.originalBasePath);
|
|
@@ -1001,8 +994,8 @@ export async function stopAuto(ctx, pi, reason, options = {}) {
|
|
|
1001
994
|
}
|
|
1002
995
|
}
|
|
1003
996
|
// ── Step 7: Restore basePath and chdir (ADR-016 phase 3, #5693) ──
|
|
1004
|
-
// `restoreToProjectRoot`
|
|
1005
|
-
// no
|
|
997
|
+
// `restoreToProjectRoot` owns both s.basePath restore and process.chdir;
|
|
998
|
+
// no paired chdir is needed at the call site.
|
|
1006
999
|
if (s.originalBasePath) {
|
|
1007
1000
|
try {
|
|
1008
1001
|
buildLifecycle().restoreToProjectRoot();
|
|
@@ -1010,13 +1003,6 @@ export async function stopAuto(ctx, pi, reason, options = {}) {
|
|
|
1010
1003
|
catch (e) {
|
|
1011
1004
|
debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
|
|
1012
1005
|
}
|
|
1013
|
-
try {
|
|
1014
|
-
process.chdir(s.basePath);
|
|
1015
|
-
}
|
|
1016
|
-
catch (err) {
|
|
1017
|
-
/* best-effort */
|
|
1018
|
-
logWarning("engine", `chdir failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
1019
|
-
}
|
|
1020
1006
|
}
|
|
1021
1007
|
// Re-root the active command session/tool runtime after worktree teardown.
|
|
1022
1008
|
// mergeAndExit restores process.cwd(), but AgentSession has already captured
|
|
@@ -1385,6 +1371,66 @@ export function buildWorktreeLifecycleDeps() {
|
|
|
1385
1371
|
function buildLifecycle() {
|
|
1386
1372
|
return new WorktreeLifecycle(s, buildWorktreeLifecycleDeps());
|
|
1387
1373
|
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Build the production `DispatchAdapter` used by `createWiredAutoOrchestrationModule`.
|
|
1376
|
+
*
|
|
1377
|
+
* Exported so tests can verify parity with `runDispatch`'s `resolveDispatch` call —
|
|
1378
|
+
* the wired adapter must derive `structuredQuestionsAvailable`, `sessionContextWindow`,
|
|
1379
|
+
* `sessionProvider`, and `modelRegistry` the same way phases.ts:runDispatch does.
|
|
1380
|
+
*/
|
|
1381
|
+
export function createWiredDispatchAdapter(ctx, pi, dispatchBasePath) {
|
|
1382
|
+
return {
|
|
1383
|
+
async decideNextUnit(input) {
|
|
1384
|
+
const state = input.stateSnapshot;
|
|
1385
|
+
const active = state.activeMilestone;
|
|
1386
|
+
if (!active)
|
|
1387
|
+
return null;
|
|
1388
|
+
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1389
|
+
// Derive session-derived dispatch inputs the same way phases.ts:runDispatch does
|
|
1390
|
+
// (#5789). Prefer caller-supplied values when present so test harnesses and
|
|
1391
|
+
// alternative wirings can inject deterministic snapshots; otherwise pull from
|
|
1392
|
+
// the captured pi/ctx references.
|
|
1393
|
+
const sessionProvider = input.sessionProvider ?? ctx.model?.provider;
|
|
1394
|
+
const sessionContextWindow = input.sessionContextWindow ?? ctx.model?.contextWindow;
|
|
1395
|
+
const modelRegistry = input.modelRegistry ?? ctx.modelRegistry;
|
|
1396
|
+
const authMode = sessionProvider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
|
|
1397
|
+
? ctx.modelRegistry.getProviderAuthMode(sessionProvider)
|
|
1398
|
+
: undefined;
|
|
1399
|
+
const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
|
|
1400
|
+
// Mirrors runDispatch: deep-planning keeps approval gates in plain chat
|
|
1401
|
+
// because structured questions can be cancelled outside the chat turn on
|
|
1402
|
+
// some transports.
|
|
1403
|
+
const structuredQuestionsAvailable = input.structuredQuestionsAvailable ??
|
|
1404
|
+
(prefs?.planning_depth === "deep"
|
|
1405
|
+
? "false"
|
|
1406
|
+
: supportsStructuredQuestions(activeTools, {
|
|
1407
|
+
authMode,
|
|
1408
|
+
baseUrl: ctx.model?.baseUrl,
|
|
1409
|
+
})
|
|
1410
|
+
? "true"
|
|
1411
|
+
: "false");
|
|
1412
|
+
const action = await resolveDispatch({
|
|
1413
|
+
basePath: dispatchBasePath,
|
|
1414
|
+
mid: active.id,
|
|
1415
|
+
midTitle: active.title,
|
|
1416
|
+
state,
|
|
1417
|
+
prefs,
|
|
1418
|
+
structuredQuestionsAvailable,
|
|
1419
|
+
sessionContextWindow,
|
|
1420
|
+
sessionProvider,
|
|
1421
|
+
modelRegistry,
|
|
1422
|
+
});
|
|
1423
|
+
if (action.action !== "dispatch")
|
|
1424
|
+
return null;
|
|
1425
|
+
return {
|
|
1426
|
+
unitType: action.unitType,
|
|
1427
|
+
unitId: action.unitId,
|
|
1428
|
+
reason: action.matchedRule ?? "dispatch",
|
|
1429
|
+
preconditions: [],
|
|
1430
|
+
};
|
|
1431
|
+
},
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1388
1434
|
/**
|
|
1389
1435
|
* Thin entry glue for the new Auto Orchestration module.
|
|
1390
1436
|
*
|
|
@@ -1392,7 +1438,7 @@ function buildLifecycle() {
|
|
|
1392
1438
|
* no behavior changes to the existing auto loop. It provides a concrete seam
|
|
1393
1439
|
* the next refactor steps can adopt incrementally.
|
|
1394
1440
|
*/
|
|
1395
|
-
export function createWiredAutoOrchestrationModule(ctx,
|
|
1441
|
+
export function createWiredAutoOrchestrationModule(ctx, pi, dispatchBasePath, runtimeBasePath = resolveProjectRoot(dispatchBasePath)) {
|
|
1396
1442
|
const flowId = `auto-orchestrator-${Date.now()}`;
|
|
1397
1443
|
let seq = 0;
|
|
1398
1444
|
const deps = {
|
|
@@ -1416,30 +1462,7 @@ export function createWiredAutoOrchestrationModule(ctx, _pi, dispatchBasePath, r
|
|
|
1416
1462
|
};
|
|
1417
1463
|
},
|
|
1418
1464
|
},
|
|
1419
|
-
dispatch:
|
|
1420
|
-
async decideNextUnit(input) {
|
|
1421
|
-
const state = input.stateSnapshot;
|
|
1422
|
-
const active = state.activeMilestone;
|
|
1423
|
-
if (!active)
|
|
1424
|
-
return null;
|
|
1425
|
-
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1426
|
-
const action = await resolveDispatch({
|
|
1427
|
-
basePath: dispatchBasePath,
|
|
1428
|
-
mid: active.id,
|
|
1429
|
-
midTitle: active.title,
|
|
1430
|
-
state,
|
|
1431
|
-
prefs,
|
|
1432
|
-
});
|
|
1433
|
-
if (action.action !== "dispatch")
|
|
1434
|
-
return null;
|
|
1435
|
-
return {
|
|
1436
|
-
unitType: action.unitType,
|
|
1437
|
-
unitId: action.unitId,
|
|
1438
|
-
reason: action.matchedRule ?? "dispatch",
|
|
1439
|
-
preconditions: [],
|
|
1440
|
-
};
|
|
1441
|
-
},
|
|
1442
|
-
},
|
|
1465
|
+
dispatch: createWiredDispatchAdapter(ctx, pi, dispatchBasePath),
|
|
1443
1466
|
recovery: {
|
|
1444
1467
|
async classifyAndRecover(input) {
|
|
1445
1468
|
const recovery = classifyFailure(input);
|
|
@@ -1488,12 +1511,26 @@ export function createWiredAutoOrchestrationModule(ctx, _pi, dispatchBasePath, r
|
|
|
1488
1511
|
async cleanupOnStop() { },
|
|
1489
1512
|
},
|
|
1490
1513
|
health: {
|
|
1514
|
+
checkResourcesStale() {
|
|
1515
|
+
return checkResourcesStale(s.resourceVersionOnStart);
|
|
1516
|
+
},
|
|
1491
1517
|
async preAdvanceGate() {
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1518
|
+
try {
|
|
1519
|
+
const gate = await preDispatchHealthGate(dispatchBasePath);
|
|
1520
|
+
if (gate.proceed) {
|
|
1521
|
+
return {
|
|
1522
|
+
kind: "pass",
|
|
1523
|
+
fixesApplied: gate.fixesApplied,
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
return {
|
|
1527
|
+
kind: "fail",
|
|
1528
|
+
reason: gate.reason ?? "Pre-dispatch health check failed — run /gsd doctor for details.",
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
catch (error) {
|
|
1532
|
+
return { kind: "threw", error };
|
|
1533
|
+
}
|
|
1497
1534
|
},
|
|
1498
1535
|
async postAdvanceRecord(result) {
|
|
1499
1536
|
if (result.kind === "error") {
|
|
@@ -1561,6 +1598,45 @@ export function createWiredAutoOrchestrationModule(ctx, _pi, dispatchBasePath, r
|
|
|
1561
1598
|
}
|
|
1562
1599
|
},
|
|
1563
1600
|
},
|
|
1601
|
+
uokGate: {
|
|
1602
|
+
async emit(input) {
|
|
1603
|
+
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1604
|
+
const uokFlags = resolveUokFlags(prefs);
|
|
1605
|
+
if (!uokFlags.gates)
|
|
1606
|
+
return;
|
|
1607
|
+
const milestoneId = input.milestoneId ?? s.currentMilestoneId ?? undefined;
|
|
1608
|
+
try {
|
|
1609
|
+
const { UokGateRunner } = await import("./uok/gate-runner.js");
|
|
1610
|
+
const runner = new UokGateRunner();
|
|
1611
|
+
runner.register({
|
|
1612
|
+
id: input.gateId,
|
|
1613
|
+
type: input.gateType,
|
|
1614
|
+
execute: async () => ({
|
|
1615
|
+
outcome: input.outcome,
|
|
1616
|
+
failureClass: input.failureClass,
|
|
1617
|
+
rationale: input.rationale,
|
|
1618
|
+
findings: input.findings ?? "",
|
|
1619
|
+
}),
|
|
1620
|
+
});
|
|
1621
|
+
await runner.run(input.gateId, {
|
|
1622
|
+
basePath: dispatchBasePath,
|
|
1623
|
+
traceId: `pre-dispatch:${flowId}`,
|
|
1624
|
+
turnId: `orch-${seq}`,
|
|
1625
|
+
milestoneId,
|
|
1626
|
+
unitType: "pre-dispatch",
|
|
1627
|
+
unitId: `orch-${seq}`,
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
catch (err) {
|
|
1631
|
+
logWarning("engine", `uok gate emit failed: ${getErrorMessage(err)}`, {
|
|
1632
|
+
file: "auto.ts",
|
|
1633
|
+
gateId: input.gateId,
|
|
1634
|
+
gateType: input.gateType,
|
|
1635
|
+
...(milestoneId ? { milestoneId } : {}),
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
},
|
|
1639
|
+
},
|
|
1564
1640
|
};
|
|
1565
1641
|
return createAutoOrchestrator(deps);
|
|
1566
1642
|
}
|
|
@@ -18,6 +18,9 @@ const MAX_NETWORK_RETRIES = 2;
|
|
|
18
18
|
function isObjectRecord(value) {
|
|
19
19
|
return !!value && typeof value === "object";
|
|
20
20
|
}
|
|
21
|
+
export function _hasEmptyAgentEndContent(content) {
|
|
22
|
+
return content == null || (Array.isArray(content) && content.length === 0);
|
|
23
|
+
}
|
|
21
24
|
/**
|
|
22
25
|
* Cap on auto-resume attempts for sustained transient-provider errors.
|
|
23
26
|
*
|
|
@@ -246,7 +249,7 @@ export async function handleAgentEnd(pi, event, ctx) {
|
|
|
246
249
|
// that carry error context — e.g. errorMessage field or non-empty content
|
|
247
250
|
// indicating a mid-stream failure. (#2695)
|
|
248
251
|
const content = "content" in lastMsg ? lastMsg.content : undefined;
|
|
249
|
-
const hasEmptyContent =
|
|
252
|
+
const hasEmptyContent = _hasEmptyAgentEndContent(content);
|
|
250
253
|
const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
|
|
251
254
|
if (hasEmptyContent && !hasErrorMessage) {
|
|
252
255
|
// Non-fatal: treat as a normal agent end so the loop can continue
|
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
export function extractSubagentAgentClasses(input) {
|
|
2
2
|
if (!input || typeof input !== "object")
|
|
3
3
|
return [];
|
|
4
|
-
const record = input;
|
|
5
4
|
const agentClasses = [];
|
|
5
|
+
const visited = new WeakSet();
|
|
6
6
|
const addAgentClass = (value) => {
|
|
7
7
|
if (typeof value === "string" && value.trim().length > 0)
|
|
8
8
|
agentClasses.push(value.trim());
|
|
9
9
|
};
|
|
10
|
-
const
|
|
10
|
+
const visitItems = (value) => {
|
|
11
11
|
if (!Array.isArray(value))
|
|
12
12
|
return;
|
|
13
13
|
for (const item of value) {
|
|
14
|
-
|
|
15
|
-
addAgentClass(item.agent);
|
|
14
|
+
visit(item);
|
|
16
15
|
}
|
|
17
16
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const visit = (value) => {
|
|
18
|
+
if (!value || typeof value !== "object")
|
|
19
|
+
return;
|
|
20
|
+
if (visited.has(value))
|
|
21
|
+
return;
|
|
22
|
+
visited.add(value);
|
|
23
|
+
const record = value;
|
|
24
|
+
addAgentClass(record.agent);
|
|
25
|
+
visitItems(record.tasks);
|
|
26
|
+
visitItems(record.chain);
|
|
27
|
+
visitItems(record.parallel);
|
|
28
|
+
};
|
|
29
|
+
visit(input);
|
|
21
30
|
return agentClasses;
|
|
22
31
|
}
|
|
@@ -3,6 +3,7 @@ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkS
|
|
|
3
3
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { minimatch } from "minimatch";
|
|
5
5
|
import { getIsolationMode } from "../preferences.js";
|
|
6
|
+
import { compileSubagentPermissionContract } from "../unit-context-manifest.js";
|
|
6
7
|
import { logWarning } from "../workflow-logger.js";
|
|
7
8
|
import { isGsdWorktreePath, resolveWorktreeProjectRoot } from "../worktree-root.js";
|
|
8
9
|
/**
|
|
@@ -58,6 +59,7 @@ const QUEUE_SAFE_TOOLS = new Set([
|
|
|
58
59
|
* true / false — shell no-ops / test exit codes
|
|
59
60
|
*/
|
|
60
61
|
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)/;
|
|
62
|
+
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)/;
|
|
61
63
|
function createEmptyWriteGateState() {
|
|
62
64
|
return {
|
|
63
65
|
verifiedDepthMilestones: new Set(),
|
|
@@ -642,6 +644,9 @@ function blockReason(unitType, mode, what) {
|
|
|
642
644
|
* and listed in the policy's allowedSubagents.
|
|
643
645
|
* - "docs" → like "planning" but also allows writes to paths
|
|
644
646
|
* matching `allowedPathGlobs` relative to basePath.
|
|
647
|
+
* - "verification"
|
|
648
|
+
* → allows Bash for project verification commands, but keeps
|
|
649
|
+
* writes restricted to .gsd/ and blocks subagent dispatch.
|
|
645
650
|
*
|
|
646
651
|
* `pathOrCommand` is the file path for write/edit-shaped tools and the
|
|
647
652
|
* shell command for bash. Other tools ignore this argument.
|
|
@@ -673,7 +678,7 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
|
|
|
673
678
|
// Unknown tool in read-only mode — block by default.
|
|
674
679
|
return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
|
|
675
680
|
}
|
|
676
|
-
// planning / planning-dispatch / docs modes share the same surface for safe tools, bash, and subagent.
|
|
681
|
+
// planning / planning-dispatch / docs / verification modes share the same surface for safe tools, bash, and subagent.
|
|
677
682
|
if (PLANNING_SAFE_TOOLS.has(tool))
|
|
678
683
|
return { block: false };
|
|
679
684
|
if (tool.startsWith("gsd_"))
|
|
@@ -681,7 +686,8 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
|
|
|
681
686
|
if (PLANNING_SUBAGENT_TOOLS.has(tool)) {
|
|
682
687
|
if (policy.mode === "planning-dispatch") {
|
|
683
688
|
const requested = (agentClasses ?? []).map(a => a.trim()).filter(Boolean);
|
|
684
|
-
const
|
|
689
|
+
const dispatchContract = compileSubagentPermissionContract(policy);
|
|
690
|
+
const allowedSubagents = dispatchContract.allowedSubagents;
|
|
685
691
|
const allowed = new Set(allowedSubagents);
|
|
686
692
|
// When agentClasses is undefined, the caller has not been updated to extract
|
|
687
693
|
// agent identities yet. Block and warn so stale callers surface in telemetry
|
|
@@ -718,6 +724,14 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
|
|
|
718
724
|
return { block: true, reason: blockReason(unitType, policy.mode, `subagent dispatch is not permitted in planning units`) };
|
|
719
725
|
}
|
|
720
726
|
if (tool === "bash") {
|
|
727
|
+
if (policy.mode === "verification") {
|
|
728
|
+
if (BASH_VERIFICATION_RE.test(pathOrCommand) || BASH_READ_ONLY_RE.test(pathOrCommand))
|
|
729
|
+
return { block: false };
|
|
730
|
+
return {
|
|
731
|
+
block: true,
|
|
732
|
+
reason: blockReason(unitType, policy.mode, `bash is restricted to build/test verification commands (npm run build, npm test, etc.); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
721
735
|
if (BASH_READ_ONLY_RE.test(pathOrCommand))
|
|
722
736
|
return { block: false };
|
|
723
737
|
return {
|