gsd-pi 2.78.1-dev.b6a389b66 → 2.78.1-dev.d8826a445
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +7 -2
- package/dist/resources/extensions/gsd/auto/session.js +3 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +3 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -40
- package/dist/resources/extensions/gsd/auto.js +62 -1
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -16
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- package/dist/resources/extensions/gsd/db-writer.js +96 -16
- package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
- package/dist/resources/extensions/gsd/gsd-db.js +194 -0
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +117 -25
- package/dist/resources/extensions/gsd/metrics.js +287 -1
- package/dist/resources/extensions/gsd/paths.js +79 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/dist/resources/extensions/gsd/templates/project.md +10 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
- package/dist/resources/extensions/gsd/workspace.js +59 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +15 -2
- package/dist/resources/extensions/gsd/write-intercept.js +3 -3
- 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 +10 -10
- 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/required-server-files.json +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.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 +10 -10
- 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/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/README.md +2 -11
- package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +28 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +94 -4
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +226 -0
- package/packages/mcp-server/src/remote-questions.test.ts +103 -0
- package/packages/mcp-server/src/remote-questions.ts +35 -0
- package/packages/mcp-server/src/server.ts +129 -6
- package/packages/mcp-server/src/workflow-tools.ts +1 -1
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +8 -2
- package/src/resources/extensions/gsd/auto/session.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +8 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +225 -47
- package/src/resources/extensions/gsd/auto.ts +79 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +17 -17
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
- package/src/resources/extensions/gsd/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/gsd-db.ts +184 -0
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +154 -25
- package/src/resources/extensions/gsd/metrics.ts +321 -1
- package/src/resources/extensions/gsd/paths.ts +67 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/src/resources/extensions/gsd/templates/project.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
- package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
- package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +371 -0
- package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
- package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
- package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +74 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +28 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +453 -0
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/workspace.test.ts +190 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +67 -52
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
- package/src/resources/extensions/gsd/workspace.ts +95 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +16 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_ssgManifest.js +0 -0
|
@@ -60,7 +60,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
60
60
|
const { initHealthWidget } = await import("../health-widget.js");
|
|
61
61
|
initHealthWidget(ctx);
|
|
62
62
|
}
|
|
63
|
-
resetWriteGateState();
|
|
63
|
+
resetWriteGateState(process.cwd());
|
|
64
64
|
resetToolCallLoopGuard();
|
|
65
65
|
approvalQuestionAbortInFlight = false;
|
|
66
66
|
await resetAskUserQuestionsTurnCache();
|
|
@@ -109,10 +109,10 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
109
109
|
pi.on("session_switch", async (_event, ctx) => {
|
|
110
110
|
initNotificationStore(process.cwd());
|
|
111
111
|
installNotifyInterceptor(ctx);
|
|
112
|
-
resetWriteGateState();
|
|
112
|
+
resetWriteGateState(process.cwd());
|
|
113
113
|
resetToolCallLoopGuard();
|
|
114
114
|
await resetAskUserQuestionsTurnCache();
|
|
115
|
-
clearDiscussionFlowState();
|
|
115
|
+
clearDiscussionFlowState(process.cwd());
|
|
116
116
|
await syncServiceTierStatus(ctx);
|
|
117
117
|
await applyDisabledModelProviderPolicy(ctx);
|
|
118
118
|
// Skip MCP auto-prep when running inside an auto-worktree. The worktree
|
|
@@ -137,13 +137,14 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
137
137
|
// Wait for ecosystem loader to finish (no-op after first turn).
|
|
138
138
|
const { getEcosystemReadyPromise } = await import("../ecosystem/loader.js");
|
|
139
139
|
await getEcosystemReadyPromise();
|
|
140
|
+
const beforeAgentBasePath = process.cwd();
|
|
140
141
|
const pendingApprovalGate = getPendingGate();
|
|
141
142
|
if (pendingApprovalGate && isExplicitApprovalResponse(event.prompt, pendingApprovalGate)) {
|
|
142
|
-
markApprovalGateVerified(pendingApprovalGate);
|
|
143
|
+
markApprovalGateVerified(pendingApprovalGate, beforeAgentBasePath);
|
|
143
144
|
const milestoneId = extractDepthVerificationMilestoneId(pendingApprovalGate);
|
|
144
145
|
if (milestoneId)
|
|
145
|
-
markDepthVerified(milestoneId);
|
|
146
|
-
clearPendingGate();
|
|
146
|
+
markDepthVerified(milestoneId, beforeAgentBasePath);
|
|
147
|
+
clearPendingGate(beforeAgentBasePath);
|
|
147
148
|
}
|
|
148
149
|
// GSD's own context injection (existing behavior — unchanged).
|
|
149
150
|
const { buildBeforeAgentStartResult } = await import("./system-context.js");
|
|
@@ -318,7 +319,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
318
319
|
return;
|
|
319
320
|
const gateId = approvalGateIdForUnit(unitType, unitId);
|
|
320
321
|
if (gateId)
|
|
321
|
-
setPendingGate(gateId);
|
|
322
|
+
setPendingGate(gateId, process.cwd());
|
|
322
323
|
approvalQuestionAbortInFlight = true;
|
|
323
324
|
ctx.ui.notify(`${unitType}${unitId ? ` ${unitId}` : ""} is waiting for your approval - pausing before more tool calls run.`, "info");
|
|
324
325
|
// The pending gate set above blocks subsequent non-read-only tool calls
|
|
@@ -360,7 +361,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
360
361
|
const questions = event.input?.questions ?? [];
|
|
361
362
|
const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
|
|
362
363
|
if (typeof questionId === "string") {
|
|
363
|
-
setPendingGate(questionId);
|
|
364
|
+
setPendingGate(questionId, discussionBasePath);
|
|
364
365
|
}
|
|
365
366
|
}
|
|
366
367
|
// ── Discussion gate enforcement: block tool calls while gate is pending ──
|
|
@@ -501,7 +502,8 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
501
502
|
const toolName = canonicalToolName(event.toolName);
|
|
502
503
|
if (toolName !== "ask_user_questions")
|
|
503
504
|
return;
|
|
504
|
-
const
|
|
505
|
+
const basePath = process.cwd();
|
|
506
|
+
const milestoneId = await getDiscussionMilestoneIdFor(basePath);
|
|
505
507
|
const queueActive = isQueuePhaseActive();
|
|
506
508
|
const details = event.details;
|
|
507
509
|
// ── Discussion gate enforcement: handle gate question responses ──
|
|
@@ -533,11 +535,11 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
533
535
|
if (pendingQuestion) {
|
|
534
536
|
const answer = details.response?.answers?.[currentPendingGate];
|
|
535
537
|
if (isDepthConfirmationAnswer(answer?.selected, pendingQuestion.options)) {
|
|
536
|
-
markApprovalGateVerified(currentPendingGate);
|
|
538
|
+
markApprovalGateVerified(currentPendingGate, basePath);
|
|
537
539
|
const milestoneIdFromGate = extractDepthVerificationMilestoneId(currentPendingGate);
|
|
538
540
|
if (milestoneIdFromGate)
|
|
539
|
-
markDepthVerified(milestoneIdFromGate);
|
|
540
|
-
clearPendingGate();
|
|
541
|
+
markDepthVerified(milestoneIdFromGate, basePath);
|
|
542
|
+
clearPendingGate(basePath);
|
|
541
543
|
}
|
|
542
544
|
}
|
|
543
545
|
}
|
|
@@ -553,9 +555,9 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
553
555
|
if (isDepthConfirmationAnswer(answer?.selected, question.options)) {
|
|
554
556
|
if (currentPendingGate && question.id !== currentPendingGate)
|
|
555
557
|
break;
|
|
556
|
-
markApprovalGateVerified(question.id);
|
|
557
|
-
markDepthVerified(inferredMilestoneId);
|
|
558
|
-
clearPendingGate();
|
|
558
|
+
markApprovalGateVerified(question.id, basePath);
|
|
559
|
+
markDepthVerified(inferredMilestoneId, basePath);
|
|
560
|
+
clearPendingGate(basePath);
|
|
559
561
|
}
|
|
560
562
|
break;
|
|
561
563
|
}
|
|
@@ -564,7 +566,6 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
564
566
|
return;
|
|
565
567
|
if (!milestoneId)
|
|
566
568
|
return;
|
|
567
|
-
const basePath = process.cwd();
|
|
568
569
|
const milestoneDir = resolveMilestonePath(basePath, milestoneId);
|
|
569
570
|
if (!milestoneDir)
|
|
570
571
|
return;
|
|
@@ -55,19 +55,31 @@ const QUEUE_SAFE_TOOLS = new Set([
|
|
|
55
55
|
* true / false — shell no-ops / test exit codes
|
|
56
56
|
*/
|
|
57
57
|
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)/;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
function createEmptyWriteGateState() {
|
|
59
|
+
return {
|
|
60
|
+
verifiedDepthMilestones: new Set(),
|
|
61
|
+
verifiedApprovalGates: new Set(),
|
|
62
|
+
activeQueuePhase: false,
|
|
63
|
+
pendingGateId: null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const writeGateStatesByBasePath = new Map();
|
|
67
|
+
function writeGateStateKey(basePath) {
|
|
68
|
+
return resolve(basePath);
|
|
69
|
+
}
|
|
70
|
+
function getWriteGateState(basePath = process.cwd()) {
|
|
71
|
+
const key = writeGateStateKey(basePath);
|
|
72
|
+
let state = writeGateStatesByBasePath.get(key);
|
|
73
|
+
if (!state) {
|
|
74
|
+
state = createEmptyWriteGateState();
|
|
75
|
+
writeGateStatesByBasePath.set(key, state);
|
|
76
|
+
}
|
|
77
|
+
return state;
|
|
78
|
+
}
|
|
61
79
|
/**
|
|
62
|
-
* Discussion gate enforcement state
|
|
63
|
-
*
|
|
64
|
-
* When ask_user_questions is called with a recognized gate question ID,
|
|
65
|
-
* we track the pending gate. Until the gate is confirmed (user selects the
|
|
66
|
-
* first/recommended option), all non-read-only tool calls are blocked.
|
|
67
|
-
* This mechanically prevents the model from rationalizing past failed or
|
|
68
|
-
* cancelled gate questions.
|
|
80
|
+
* Discussion gate enforcement state is scoped per basePath so multiple
|
|
81
|
+
* workspaces can coexist in the same process without sharing gate state.
|
|
69
82
|
*/
|
|
70
|
-
let pendingGateId = null;
|
|
71
83
|
/**
|
|
72
84
|
* Recognized gate question ID patterns.
|
|
73
85
|
* These appear in discuss.md (depth/requirements/roadmap).
|
|
@@ -99,24 +111,25 @@ function shouldPersistWriteGateSnapshot(env = process.env) {
|
|
|
99
111
|
const v = env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
100
112
|
return v !== "0" && v !== "false";
|
|
101
113
|
}
|
|
102
|
-
function writeGateSnapshotPath(basePath
|
|
114
|
+
function writeGateSnapshotPath(basePath) {
|
|
103
115
|
return join(basePath, ".gsd", "runtime", "write-gate-state.json");
|
|
104
116
|
}
|
|
105
|
-
function currentWriteGateSnapshot() {
|
|
117
|
+
function currentWriteGateSnapshot(basePath = process.cwd()) {
|
|
118
|
+
const state = getWriteGateState(basePath);
|
|
106
119
|
return {
|
|
107
|
-
verifiedDepthMilestones: [...verifiedDepthMilestones].sort(),
|
|
108
|
-
verifiedApprovalGates: [...verifiedApprovalGates].sort(),
|
|
109
|
-
activeQueuePhase,
|
|
110
|
-
pendingGateId,
|
|
120
|
+
verifiedDepthMilestones: [...state.verifiedDepthMilestones].sort(),
|
|
121
|
+
verifiedApprovalGates: [...state.verifiedApprovalGates].sort(),
|
|
122
|
+
activeQueuePhase: state.activeQueuePhase,
|
|
123
|
+
pendingGateId: state.pendingGateId,
|
|
111
124
|
};
|
|
112
125
|
}
|
|
113
|
-
function persistWriteGateSnapshot(basePath
|
|
126
|
+
function persistWriteGateSnapshot(basePath) {
|
|
114
127
|
if (!shouldPersistWriteGateSnapshot())
|
|
115
128
|
return;
|
|
116
129
|
const path = writeGateSnapshotPath(basePath);
|
|
117
130
|
mkdirSync(join(basePath, ".gsd", "runtime"), { recursive: true });
|
|
118
131
|
const tempPath = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
119
|
-
writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(), null, 2), "utf-8");
|
|
132
|
+
writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(basePath), null, 2), "utf-8");
|
|
120
133
|
try {
|
|
121
134
|
renameSync(tempPath, path);
|
|
122
135
|
}
|
|
@@ -132,7 +145,7 @@ function persistWriteGateSnapshot(basePath = process.cwd()) {
|
|
|
132
145
|
}
|
|
133
146
|
}
|
|
134
147
|
}
|
|
135
|
-
function clearPersistedWriteGateSnapshot(basePath
|
|
148
|
+
function clearPersistedWriteGateSnapshot(basePath) {
|
|
136
149
|
if (!shouldPersistWriteGateSnapshot())
|
|
137
150
|
return;
|
|
138
151
|
const path = writeGateSnapshotPath(basePath);
|
|
@@ -164,7 +177,7 @@ const EMPTY_SNAPSHOT = {
|
|
|
164
177
|
activeQueuePhase: false,
|
|
165
178
|
pendingGateId: null,
|
|
166
179
|
};
|
|
167
|
-
export function loadWriteGateSnapshot(basePath
|
|
180
|
+
export function loadWriteGateSnapshot(basePath) {
|
|
168
181
|
const path = writeGateSnapshotPath(basePath);
|
|
169
182
|
if (!existsSync(path)) {
|
|
170
183
|
// When persist mode is active and the file has been deleted, treat it as a
|
|
@@ -172,61 +185,59 @@ export function loadWriteGateSnapshot(basePath = process.cwd()) {
|
|
|
172
185
|
// In non-persist mode the file is never written, so fall back to in-memory.
|
|
173
186
|
if (shouldPersistWriteGateSnapshot())
|
|
174
187
|
return EMPTY_SNAPSHOT;
|
|
175
|
-
return currentWriteGateSnapshot();
|
|
188
|
+
return currentWriteGateSnapshot(basePath);
|
|
176
189
|
}
|
|
177
190
|
try {
|
|
178
191
|
return normalizeWriteGateSnapshot(JSON.parse(readFileSync(path, "utf-8")));
|
|
179
192
|
}
|
|
180
193
|
catch {
|
|
181
|
-
return currentWriteGateSnapshot();
|
|
194
|
+
return currentWriteGateSnapshot(basePath);
|
|
182
195
|
}
|
|
183
196
|
}
|
|
184
|
-
export function isDepthVerified() {
|
|
185
|
-
return verifiedDepthMilestones.size > 0;
|
|
197
|
+
export function isDepthVerified(basePath = process.cwd()) {
|
|
198
|
+
return getWriteGateState(basePath).verifiedDepthMilestones.size > 0;
|
|
186
199
|
}
|
|
187
200
|
/**
|
|
188
201
|
* Check whether a specific milestone has passed depth verification.
|
|
189
202
|
*/
|
|
190
|
-
export function isMilestoneDepthVerified(milestoneId) {
|
|
203
|
+
export function isMilestoneDepthVerified(milestoneId, basePath = process.cwd()) {
|
|
191
204
|
if (!milestoneId)
|
|
192
205
|
return false;
|
|
193
|
-
return verifiedDepthMilestones.has(milestoneId);
|
|
206
|
+
return getWriteGateState(basePath).verifiedDepthMilestones.has(milestoneId);
|
|
194
207
|
}
|
|
195
208
|
export function isMilestoneDepthVerifiedInSnapshot(snapshot, milestoneId) {
|
|
196
209
|
if (!milestoneId)
|
|
197
210
|
return false;
|
|
198
211
|
return snapshot.verifiedDepthMilestones.includes(milestoneId);
|
|
199
212
|
}
|
|
200
|
-
export function isQueuePhaseActive() {
|
|
201
|
-
return activeQueuePhase;
|
|
213
|
+
export function isQueuePhaseActive(basePath = process.cwd()) {
|
|
214
|
+
return getWriteGateState(basePath).activeQueuePhase;
|
|
202
215
|
}
|
|
203
|
-
export function setQueuePhaseActive(active) {
|
|
204
|
-
activeQueuePhase = active;
|
|
205
|
-
persistWriteGateSnapshot();
|
|
216
|
+
export function setQueuePhaseActive(active, basePath) {
|
|
217
|
+
getWriteGateState(basePath).activeQueuePhase = active;
|
|
218
|
+
persistWriteGateSnapshot(basePath);
|
|
206
219
|
}
|
|
207
|
-
export function resetWriteGateState() {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
220
|
+
export function resetWriteGateState(basePath) {
|
|
221
|
+
const state = getWriteGateState(basePath);
|
|
222
|
+
state.verifiedDepthMilestones.clear();
|
|
223
|
+
state.verifiedApprovalGates.clear();
|
|
224
|
+
state.pendingGateId = null;
|
|
225
|
+
persistWriteGateSnapshot(basePath);
|
|
212
226
|
}
|
|
213
|
-
export function clearDiscussionFlowState() {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
activeQueuePhase = false;
|
|
217
|
-
pendingGateId = null;
|
|
218
|
-
clearPersistedWriteGateSnapshot();
|
|
227
|
+
export function clearDiscussionFlowState(basePath) {
|
|
228
|
+
writeGateStatesByBasePath.delete(writeGateStateKey(basePath));
|
|
229
|
+
clearPersistedWriteGateSnapshot(basePath);
|
|
219
230
|
}
|
|
220
231
|
export function markDepthVerified(milestoneId, basePath = process.cwd()) {
|
|
221
232
|
if (!milestoneId)
|
|
222
233
|
return;
|
|
223
|
-
verifiedDepthMilestones.add(milestoneId);
|
|
234
|
+
getWriteGateState(basePath).verifiedDepthMilestones.add(milestoneId);
|
|
224
235
|
persistWriteGateSnapshot(basePath);
|
|
225
236
|
}
|
|
226
237
|
export function markApprovalGateVerified(gateId, basePath = process.cwd()) {
|
|
227
238
|
if (!gateId)
|
|
228
239
|
return;
|
|
229
|
-
verifiedApprovalGates.add(gateId);
|
|
240
|
+
getWriteGateState(basePath).verifiedApprovalGates.add(gateId);
|
|
230
241
|
persistWriteGateSnapshot(basePath);
|
|
231
242
|
}
|
|
232
243
|
export function isApprovalGateVerifiedInSnapshot(snapshot, gateId) {
|
|
@@ -258,26 +269,27 @@ function extractContextMilestoneId(inputPath) {
|
|
|
258
269
|
/**
|
|
259
270
|
* Mark a gate as pending (called when ask_user_questions is invoked with a gate ID).
|
|
260
271
|
*/
|
|
261
|
-
export function setPendingGate(gateId) {
|
|
262
|
-
|
|
263
|
-
|
|
272
|
+
export function setPendingGate(gateId, basePath) {
|
|
273
|
+
const state = getWriteGateState(basePath);
|
|
274
|
+
state.pendingGateId = gateId;
|
|
275
|
+
state.verifiedApprovalGates.delete(gateId);
|
|
264
276
|
const milestoneId = extractDepthVerificationMilestoneId(gateId);
|
|
265
277
|
if (milestoneId)
|
|
266
|
-
verifiedDepthMilestones.delete(milestoneId);
|
|
267
|
-
persistWriteGateSnapshot();
|
|
278
|
+
state.verifiedDepthMilestones.delete(milestoneId);
|
|
279
|
+
persistWriteGateSnapshot(basePath);
|
|
268
280
|
}
|
|
269
281
|
/**
|
|
270
282
|
* Clear the pending gate (called when the user confirms).
|
|
271
283
|
*/
|
|
272
|
-
export function clearPendingGate() {
|
|
273
|
-
pendingGateId = null;
|
|
274
|
-
persistWriteGateSnapshot();
|
|
284
|
+
export function clearPendingGate(basePath) {
|
|
285
|
+
getWriteGateState(basePath).pendingGateId = null;
|
|
286
|
+
persistWriteGateSnapshot(basePath);
|
|
275
287
|
}
|
|
276
288
|
/**
|
|
277
289
|
* Get the currently pending gate, if any.
|
|
278
290
|
*/
|
|
279
|
-
export function getPendingGate() {
|
|
280
|
-
return pendingGateId;
|
|
291
|
+
export function getPendingGate(basePath = process.cwd()) {
|
|
292
|
+
return getWriteGateState(basePath).pendingGateId;
|
|
281
293
|
}
|
|
282
294
|
/**
|
|
283
295
|
* Check whether a tool call should be blocked because a discussion gate
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Critical invariant: generated markdown must round-trip through
|
|
9
9
|
// parseDecisionsTable() and parseRequirementsSections() with field fidelity.
|
|
10
|
-
import { resolve } from 'node:path';
|
|
10
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
11
11
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
12
12
|
import { resolveGsdRootFile } from './paths.js';
|
|
13
13
|
import { saveFile } from './files.js';
|
|
@@ -16,6 +16,7 @@ import { logWarning, logError } from './workflow-logger.js';
|
|
|
16
16
|
import { invalidateStateCache } from './state.js';
|
|
17
17
|
import { clearPathCache } from './paths.js';
|
|
18
18
|
import { clearParseCache } from './files.js';
|
|
19
|
+
import { createWorkspace, scopeMilestone } from './workspace.js';
|
|
19
20
|
// ─── Freeform Detection ───────────────────────────────────────────────────
|
|
20
21
|
/**
|
|
21
22
|
* Detect whether a DECISIONS.md file is in canonical table format
|
|
@@ -614,40 +615,102 @@ export async function updateRequirementInDb(id, updates, basePath) {
|
|
|
614
615
|
}
|
|
615
616
|
}
|
|
616
617
|
/**
|
|
617
|
-
* Save
|
|
618
|
+
* Save a root-level artifact (no milestone) to DB and write to disk,
|
|
619
|
+
* routing path construction through workspace.contract.projectGsd directly.
|
|
620
|
+
* Use this instead of saveArtifactToDbByScope when milestone_id is absent.
|
|
621
|
+
*/
|
|
622
|
+
export async function saveArtifactToDbForWorkspace(workspace, opts) {
|
|
623
|
+
try {
|
|
624
|
+
const db = await import('./gsd-db.js');
|
|
625
|
+
const gsdDir = workspace.contract.projectGsd;
|
|
626
|
+
const fullPath = resolve(gsdDir, opts.path);
|
|
627
|
+
const rel0 = relative(gsdDir, fullPath);
|
|
628
|
+
if (rel0.startsWith('..') || isAbsolute(rel0)) {
|
|
629
|
+
throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbForWorkspace: path escapes .gsd/ directory: ${opts.path}`);
|
|
630
|
+
}
|
|
631
|
+
let contentToPersist = opts.content;
|
|
632
|
+
if (opts.artifact_type === 'REQUIREMENTS' && opts.path === 'REQUIREMENTS.md') {
|
|
633
|
+
const activeRequirements = db.getActiveRequirements();
|
|
634
|
+
if (activeRequirements.length === 0) {
|
|
635
|
+
throw new GSDError(GSD_STALE_STATE, 'saveArtifactToDbForWorkspace: REQUIREMENTS final save requires active DB-backed requirements');
|
|
636
|
+
}
|
|
637
|
+
contentToPersist = generateRequirementsMd(activeRequirements);
|
|
638
|
+
}
|
|
639
|
+
let skipDiskWrite = false;
|
|
640
|
+
if (!isRootCanonicalArtifact(opts) && existsSync(fullPath)) {
|
|
641
|
+
const existingSize = statSync(fullPath).size;
|
|
642
|
+
const newSize = Buffer.byteLength(contentToPersist, 'utf-8');
|
|
643
|
+
if (existingSize > 0 && newSize < existingSize * 0.5) {
|
|
644
|
+
logWarning('projection', `new content (${newSize}B) is <50% of existing projection (${existingSize}B), preserving disk file while DB remains authoritative`, { fn: 'saveArtifactToDbForWorkspace', path: opts.path });
|
|
645
|
+
skipDiskWrite = true;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
db.insertArtifact({
|
|
649
|
+
path: opts.path,
|
|
650
|
+
artifact_type: opts.artifact_type,
|
|
651
|
+
milestone_id: null,
|
|
652
|
+
slice_id: null,
|
|
653
|
+
task_id: null,
|
|
654
|
+
full_content: contentToPersist,
|
|
655
|
+
});
|
|
656
|
+
if (!skipDiskWrite) {
|
|
657
|
+
try {
|
|
658
|
+
await saveFile(fullPath, contentToPersist);
|
|
659
|
+
}
|
|
660
|
+
catch (diskErr) {
|
|
661
|
+
logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: 'saveArtifactToDbForWorkspace', path: opts.path, error: String(diskErr.message) });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
invalidateStateCache();
|
|
665
|
+
clearPathCache();
|
|
666
|
+
clearParseCache();
|
|
667
|
+
}
|
|
668
|
+
catch (err) {
|
|
669
|
+
logError('manifest', 'saveArtifactToDbForWorkspace failed', { fn: 'saveArtifactToDbForWorkspace', error: String(err.message) });
|
|
670
|
+
throw err;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Save an artifact to DB and write the corresponding markdown file to disk,
|
|
675
|
+
* routing all path construction through the workspace contract.
|
|
676
|
+
*
|
|
618
677
|
* The path is relative to .gsd/ (e.g. "milestones/M001/slices/S06/tasks/T01-SUMMARY.md").
|
|
619
|
-
* The full file path is computed as
|
|
678
|
+
* The full file path is computed as scope.workspace.contract.projectGsd + '/' + path.
|
|
620
679
|
*/
|
|
621
|
-
export async function
|
|
680
|
+
export async function saveArtifactToDbByScope(scope, opts) {
|
|
681
|
+
// Guard: an empty milestoneId produces malformed paths (milestoneDir = join(gsd, "milestones", "")).
|
|
682
|
+
// Callers that have no milestone should use saveArtifactToDbForWorkspace instead.
|
|
683
|
+
if (!scope.milestoneId) {
|
|
684
|
+
throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbByScope: milestoneId is empty — use saveArtifactToDbForWorkspace for root artifacts`);
|
|
685
|
+
}
|
|
622
686
|
try {
|
|
623
687
|
const db = await import('./gsd-db.js');
|
|
688
|
+
// Use contract.projectGsd as the canonical .gsd directory — never a hand-rolled basePath join.
|
|
689
|
+
const gsdDir = scope.workspace.contract.projectGsd;
|
|
690
|
+
const fullPath = resolve(gsdDir, opts.path);
|
|
624
691
|
// Guard against path traversal before any reads/writes
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
throw new GSDError(GSD_IO_ERROR, `saveArtifactToDb: path escapes .gsd/ directory: ${opts.path}`);
|
|
692
|
+
const rel1 = relative(gsdDir, fullPath);
|
|
693
|
+
if (rel1.startsWith('..') || isAbsolute(rel1)) {
|
|
694
|
+
throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbByScope: path escapes .gsd/ directory: ${opts.path}`);
|
|
629
695
|
}
|
|
630
696
|
let contentToPersist = opts.content;
|
|
631
697
|
if (opts.artifact_type === 'REQUIREMENTS' && opts.path === 'REQUIREMENTS.md') {
|
|
632
698
|
const activeRequirements = db.getActiveRequirements();
|
|
633
699
|
if (activeRequirements.length === 0) {
|
|
634
|
-
throw new GSDError(GSD_STALE_STATE, '
|
|
700
|
+
throw new GSDError(GSD_STALE_STATE, 'saveArtifactToDbByScope: REQUIREMENTS final save requires active DB-backed requirements');
|
|
635
701
|
}
|
|
636
702
|
contentToPersist = generateRequirementsMd(activeRequirements);
|
|
637
703
|
}
|
|
638
704
|
// Shrinkage guard: if the projection file already exists and the new
|
|
639
705
|
// content is significantly smaller (<50%), preserve the richer file on
|
|
640
706
|
// disk, but keep the DB row authoritative with the caller-provided content.
|
|
641
|
-
//
|
|
642
|
-
// Root canonical artifacts are exempt because their content is rendered
|
|
643
|
-
// from canonical DB state, and cleanup/consolidation is often intentionally
|
|
644
|
-
// much smaller than a malformed accumulated file.
|
|
707
|
+
// Root canonical artifacts are exempt (rendered from canonical DB state).
|
|
645
708
|
let skipDiskWrite = false;
|
|
646
709
|
if (!isRootCanonicalArtifact(opts) && existsSync(fullPath)) {
|
|
647
710
|
const existingSize = statSync(fullPath).size;
|
|
648
711
|
const newSize = Buffer.byteLength(contentToPersist, 'utf-8');
|
|
649
712
|
if (existingSize > 0 && newSize < existingSize * 0.5) {
|
|
650
|
-
logWarning('projection', `new content (${newSize}B) is <50% of existing projection (${existingSize}B), preserving disk file while DB remains authoritative`, { fn: '
|
|
713
|
+
logWarning('projection', `new content (${newSize}B) is <50% of existing projection (${existingSize}B), preserving disk file while DB remains authoritative`, { fn: 'saveArtifactToDbByScope', path: opts.path });
|
|
651
714
|
skipDiskWrite = true;
|
|
652
715
|
}
|
|
653
716
|
}
|
|
@@ -665,7 +728,7 @@ export async function saveArtifactToDb(opts, basePath) {
|
|
|
665
728
|
await saveFile(fullPath, contentToPersist);
|
|
666
729
|
}
|
|
667
730
|
catch (diskErr) {
|
|
668
|
-
logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: '
|
|
731
|
+
logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: 'saveArtifactToDbByScope', path: opts.path, error: String(diskErr.message) });
|
|
669
732
|
}
|
|
670
733
|
}
|
|
671
734
|
// Invalidate file-read caches so deriveState() sees the updated markdown.
|
|
@@ -675,7 +738,24 @@ export async function saveArtifactToDb(opts, basePath) {
|
|
|
675
738
|
clearParseCache();
|
|
676
739
|
}
|
|
677
740
|
catch (err) {
|
|
678
|
-
logError('manifest', '
|
|
741
|
+
logError('manifest', 'saveArtifactToDbByScope failed', { fn: 'saveArtifactToDbByScope', error: String(err.message) });
|
|
679
742
|
throw err;
|
|
680
743
|
}
|
|
681
744
|
}
|
|
745
|
+
/**
|
|
746
|
+
* Save an artifact to DB and write the corresponding markdown file to disk.
|
|
747
|
+
* The path is relative to .gsd/ (e.g. "milestones/M001/slices/S06/tasks/T01-SUMMARY.md").
|
|
748
|
+
* The full file path is computed as basePath + '.gsd/' + path.
|
|
749
|
+
*
|
|
750
|
+
* @deprecated Use saveArtifactToDbByScope instead, which routes through the
|
|
751
|
+
* workspace contract for canonical path resolution.
|
|
752
|
+
* TODO(C-future): remove this legacy wrapper once all callers are migrated.
|
|
753
|
+
*/
|
|
754
|
+
export async function saveArtifactToDb(opts, basePath) {
|
|
755
|
+
const workspace = createWorkspace(basePath);
|
|
756
|
+
const milestoneId = opts.milestone_id;
|
|
757
|
+
if (milestoneId) {
|
|
758
|
+
return saveArtifactToDbByScope(scopeMilestone(workspace, milestoneId), opts);
|
|
759
|
+
}
|
|
760
|
+
return saveArtifactToDbForWorkspace(workspace, opts);
|
|
761
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Delegation policy — codifies which GSD MCP tools are safe to run as
|
|
2
|
+
// background sub-agents while the foreground /gsd flow continues. Verdicts
|
|
3
|
+
// are derived from the round-1 and round-2 evaluations recorded in this
|
|
4
|
+
// branch's PR description; the rationale field on each entry preserves
|
|
5
|
+
// the reason so future changes have to revisit the analysis explicitly.
|
|
6
|
+
//
|
|
7
|
+
// Default-deny: unknown tools are never backgroundable.
|
|
8
|
+
//
|
|
9
|
+
// ─── Tool-name vs unit-type namespaces ───────────────────────────────────
|
|
10
|
+
// Entries are keyed by canonical MCP tool name (`gsd_*`). The optional
|
|
11
|
+
// `unitType` field is a *secondary* index for the dispatcher's convenience
|
|
12
|
+
// — it bridges this policy to `auto-dispatch.ts`' `DispatchAction.unitType`
|
|
13
|
+
// values. The two namespaces are not 1:1:
|
|
14
|
+
//
|
|
15
|
+
// - Some tools have no corresponding unit type (e.g. `gsd_doctor`,
|
|
16
|
+
// `gsd_plan_task`) and intentionally omit `unitType`.
|
|
17
|
+
// - Some unit types share a tool — e.g. `execute-task`, `execute-task-simple`,
|
|
18
|
+
// and `reactive-execute` all invoke `gsd_execute`. The current shape
|
|
19
|
+
// allows only one `unitType` per entry, so those units fall through to
|
|
20
|
+
// `getVerdictByUnitType() === null` (→ `backgroundable: false`) even
|
|
21
|
+
// though `gsd_execute` itself is GOOD. This is the intended default-deny
|
|
22
|
+
// posture until a future PR wires actual background dispatch and
|
|
23
|
+
// decides whether each unit-level orchestration is safe — the unit
|
|
24
|
+
// wraps a prompt, harness setup, and post-processing on top of the
|
|
25
|
+
// tool, and the tool's safety doesn't transfer automatically.
|
|
26
|
+
//
|
|
27
|
+
// Auto-dispatch produces 20 distinct unit types; only 5 are explicitly
|
|
28
|
+
// classified here. The other 15 default-deny:
|
|
29
|
+
// complete-milestone, complete-slice, discuss-milestone, discuss-project,
|
|
30
|
+
// discuss-requirements, execute-task, execute-task-simple, gate-evaluate,
|
|
31
|
+
// reactive-execute, refine-slice, research-decision, research-milestone,
|
|
32
|
+
// research-project, research-slice, rewrite-docs, run-uat
|
|
33
|
+
//
|
|
34
|
+
// Adding a `unitType` mapping (or a future `unitTypes: string[]`) to an
|
|
35
|
+
// existing entry is the place to lift any of these out of default-deny
|
|
36
|
+
// when the analysis has been done.
|
|
37
|
+
const POLICY = {
|
|
38
|
+
gsd_plan_slice: {
|
|
39
|
+
toolName: "gsd_plan_slice",
|
|
40
|
+
unitType: "plan-slice",
|
|
41
|
+
verdict: "good",
|
|
42
|
+
rationale: "Self-contained, no user prompts, atomic DB tx; existing slice-parallel-orchestrator pattern transfers cleanly.",
|
|
43
|
+
constraints: [
|
|
44
|
+
"Lock the slice from further user discussion once dispatched (context is frozen at dispatch time).",
|
|
45
|
+
"Foreground must not derive state for that slice while the transaction is in flight.",
|
|
46
|
+
"Foreground must await background completion before any tool reads the planned tasks/gates.",
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
gsd_execute: {
|
|
50
|
+
toolName: "gsd_execute",
|
|
51
|
+
// No `unitType` set on purpose — the underlying tool is safe, but the
|
|
52
|
+
// unit-level orchestrations that invoke it (`execute-task`,
|
|
53
|
+
// `execute-task-simple`, `reactive-execute`) wrap additional prompt and
|
|
54
|
+
// harness work whose safety is a separate analysis. Default-deny those
|
|
55
|
+
// units until that analysis is recorded; adding `unitType` here would
|
|
56
|
+
// promote them silently.
|
|
57
|
+
verdict: "good",
|
|
58
|
+
rationale: "No DB writes; UUID-isolated stdout/stderr/meta files; existing reactive-execute parallel-subagent precedent.",
|
|
59
|
+
},
|
|
60
|
+
gsd_validate_milestone: {
|
|
61
|
+
toolName: "gsd_validate_milestone",
|
|
62
|
+
unitType: "validate-milestone",
|
|
63
|
+
verdict: "good",
|
|
64
|
+
rationale: "Verdict pre-computed by parallel reviewers; atomic DB tx plus isolated VALIDATION.md write; no user interaction.",
|
|
65
|
+
},
|
|
66
|
+
gsd_reassess_roadmap: {
|
|
67
|
+
toolName: "gsd_reassess_roadmap",
|
|
68
|
+
unitType: "reassess-roadmap",
|
|
69
|
+
verdict: "good",
|
|
70
|
+
rationale: "Narrower mutation scope than plan_milestone; structural guards prevent modification of completed slices.",
|
|
71
|
+
},
|
|
72
|
+
gsd_doctor: {
|
|
73
|
+
toolName: "gsd_doctor",
|
|
74
|
+
verdict: "risky",
|
|
75
|
+
rationale: "Diagnostic-only mode (fix=false) is safe to background; fix=true writes STATE.md/ROADMAP.md without session-lock coordination and can race the foreground flow.",
|
|
76
|
+
constraints: [
|
|
77
|
+
"Background only with fix=false (diagnostic-only).",
|
|
78
|
+
"Apply fixes synchronously, only when no foreground unit is dispatched.",
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
gsd_plan_milestone: {
|
|
82
|
+
toolName: "gsd_plan_milestone",
|
|
83
|
+
unitType: "plan-milestone",
|
|
84
|
+
verdict: "risky",
|
|
85
|
+
rationale: "Inputs require CONTEXT.md from discuss-milestone, so initial questioning is already done by the time it can start; TOCTOU guards and projection coherence make concurrency unsafe.",
|
|
86
|
+
},
|
|
87
|
+
gsd_replan_slice: {
|
|
88
|
+
toolName: "gsd_replan_slice",
|
|
89
|
+
unitType: "replan-slice",
|
|
90
|
+
verdict: "risky",
|
|
91
|
+
rationale: "Blocks the replanning→executing state transition on a gate that waits for S##-REPLAN.md; background failure leaves the flow stuck.",
|
|
92
|
+
},
|
|
93
|
+
gsd_plan_task: {
|
|
94
|
+
toolName: "gsd_plan_task",
|
|
95
|
+
verdict: "no",
|
|
96
|
+
rationale: "plan-slice prompt explicitly forbids calling gsd_plan_task separately; per-task granularity multiplies manifest writes and projection re-renders with no payoff.",
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
// Alias map keyed on the secondary name; resolves to the canonical entry above.
|
|
100
|
+
// Sourced from packages/mcp-server/src/workflow-tools.ts alias registrations
|
|
101
|
+
// (gsd_milestone_validate, gsd_roadmap_reassess, gsd_slice_replan, gsd_task_plan).
|
|
102
|
+
const ALIASES = {
|
|
103
|
+
gsd_milestone_validate: "gsd_validate_milestone",
|
|
104
|
+
gsd_roadmap_reassess: "gsd_reassess_roadmap",
|
|
105
|
+
gsd_slice_replan: "gsd_replan_slice",
|
|
106
|
+
gsd_task_plan: "gsd_plan_task",
|
|
107
|
+
};
|
|
108
|
+
function resolveCanonical(name) {
|
|
109
|
+
return ALIASES[name] ?? name;
|
|
110
|
+
}
|
|
111
|
+
export function getDelegationVerdict(toolName) {
|
|
112
|
+
return POLICY[resolveCanonical(toolName)] ?? null;
|
|
113
|
+
}
|
|
114
|
+
export function isBackgroundable(toolName) {
|
|
115
|
+
const entry = getDelegationVerdict(toolName);
|
|
116
|
+
return entry?.verdict === "good";
|
|
117
|
+
}
|
|
118
|
+
export function listBackgroundableTools() {
|
|
119
|
+
return Object.values(POLICY)
|
|
120
|
+
.filter((entry) => entry.verdict === "good")
|
|
121
|
+
.map((entry) => entry.toolName)
|
|
122
|
+
.sort();
|
|
123
|
+
}
|
|
124
|
+
export function getVerdictByUnitType(unitType) {
|
|
125
|
+
for (const entry of Object.values(POLICY)) {
|
|
126
|
+
if (entry.unitType === unitType)
|
|
127
|
+
return entry;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Annotates a dispatch action in place with `backgroundable: true` when its
|
|
133
|
+
* unitType has a `good` verdict in the policy. Stop/skip actions pass through
|
|
134
|
+
* unchanged. Default-deny: unknown unit types resolve to `false`.
|
|
135
|
+
*
|
|
136
|
+
* **Mutation contract.** The `backgroundable` field is written directly onto
|
|
137
|
+
* the passed action object. This is intentional — every dispatch path in
|
|
138
|
+
* `auto-dispatch.ts` constructs a fresh action object per `where(ctx)` /
|
|
139
|
+
* `evaluateDispatch(ctx)` invocation, so in-place mutation cannot leak across
|
|
140
|
+
* dispatch cycles. Future dispatch rules MUST follow that convention: never
|
|
141
|
+
* cache or share `DispatchAction` objects across calls. If you need to cache,
|
|
142
|
+
* either freeze the cached object (`Object.freeze`) and clone on read, or
|
|
143
|
+
* stop calling `annotateBackgroundable` on the shared instance. The annotator
|
|
144
|
+
* always recomputes from the policy on every call (no internal cache), so
|
|
145
|
+
* repeated invocations on the same object will overwrite stale values
|
|
146
|
+
* deterministically — see the `annotateBackgroundable recomputes on each call`
|
|
147
|
+
* test for the contract pin.
|
|
148
|
+
*/
|
|
149
|
+
export function annotateBackgroundable(action) {
|
|
150
|
+
if (action.action !== "dispatch")
|
|
151
|
+
return action;
|
|
152
|
+
const verdict = getVerdictByUnitType(action.unitType);
|
|
153
|
+
action.backgroundable = verdict?.verdict === "good";
|
|
154
|
+
return action;
|
|
155
|
+
}
|