@opengsd/gsd-pi 1.1.1-dev.3ea310e → 1.1.1-dev.74e8dd1
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 +4 -3
- package/dist/resources/extensions/gsd/auto-dashboard.js +15 -4
- package/dist/resources/extensions/gsd/auto-post-unit.js +111 -5
- package/dist/resources/extensions/gsd/auto-prompts.js +9 -0
- package/dist/resources/extensions/gsd/auto-start.js +41 -12
- package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +2 -1
- package/dist/resources/extensions/gsd/auto.js +3 -3
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +79 -0
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +43 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +30 -9
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +16 -10
- package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +3 -1
- package/dist/resources/extensions/gsd/commands-verdict.js +1 -1
- package/dist/resources/extensions/gsd/config-overlay.js +2 -1
- package/dist/resources/extensions/gsd/error-classifier.js +2 -1
- package/dist/resources/extensions/gsd/exec-sandbox.js +2 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +10 -4
- package/dist/resources/extensions/gsd/prompts/system.md +3 -1
- package/dist/resources/extensions/gsd/safety/destructive-guard.js +3 -0
- package/dist/resources/extensions/gsd/skill-activation.js +20 -3
- package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +18 -1
- package/dist/resources/extensions/gsd/state-reconciliation/index.js +6 -0
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/tools/exec-tool.js +109 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +366 -3
- package/dist/resources/extensions/gsd/unit-context-manifest.js +8 -3
- package/dist/resources/extensions/gsd/validation-block-guard.js +2 -0
- package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +1 -1
- package/dist/resources/extensions/gsd/workflow-mcp.js +5 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +2 -2
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/dist/workflow.d.ts +14 -0
- package/packages/contracts/dist/workflow.d.ts.map +1 -1
- package/packages/contracts/dist/workflow.js +16 -0
- package/packages/contracts/dist/workflow.js.map +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +10 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +69 -31
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js +5 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +82 -0
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/image-models.generated.d.ts +15 -0
- package/packages/pi-ai/dist/image-models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/image-models.generated.js +15 -0
- package/packages/pi-ai/dist/image-models.generated.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +35 -1
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +53 -19
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/dist/terminal.d.ts +1 -0
- package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
- package/packages/pi-tui/dist/terminal.js +8 -4
- package/packages/pi-tui/dist/terminal.js.map +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +5 -3
- package/src/resources/extensions/gsd/auto-dashboard.ts +16 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +136 -5
- package/src/resources/extensions/gsd/auto-prompts.ts +9 -0
- package/src/resources/extensions/gsd/auto-start.ts +54 -14
- package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +2 -1
- package/src/resources/extensions/gsd/auto.ts +3 -2
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +86 -0
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +51 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +51 -14
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +21 -10
- package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +4 -1
- package/src/resources/extensions/gsd/commands-verdict.ts +1 -1
- package/src/resources/extensions/gsd/config-overlay.ts +3 -1
- package/src/resources/extensions/gsd/error-classifier.ts +2 -1
- package/src/resources/extensions/gsd/exec-sandbox.ts +4 -0
- package/src/resources/extensions/gsd/preferences-types.ts +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +10 -4
- package/src/resources/extensions/gsd/prompts/system.md +3 -1
- package/src/resources/extensions/gsd/safety/destructive-guard.ts +3 -0
- package/src/resources/extensions/gsd/skill-activation.ts +20 -2
- package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +20 -0
- package/src/resources/extensions/gsd/state-reconciliation/index.ts +6 -0
- package/src/resources/extensions/gsd/state-reconciliation/types.ts +1 -0
- package/src/resources/extensions/gsd/state.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +16 -3
- package/src/resources/extensions/gsd/tests/commands-dispatcher-validation-block.test.ts +38 -3
- package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +6 -2
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/exec-tool.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +54 -7
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +18 -1
- package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +84 -10
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/tui-header-lifecycle.test.ts +29 -6
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +29 -6
- package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +83 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +25 -0
- package/src/resources/extensions/gsd/tools/exec-tool.ts +130 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +440 -2
- package/src/resources/extensions/gsd/unit-context-manifest.ts +14 -5
- package/src/resources/extensions/gsd/validation-block-guard.ts +2 -0
- package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +1 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +5 -1
- /package/dist/web/standalone/.next/static/{xACmObbrDjwLriepRgaa9 → eRWf-RI9bzbrwEurm_3uI}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{xACmObbrDjwLriepRgaa9 → eRWf-RI9bzbrwEurm_3uI}/_ssgManifest.js +0 -0
|
@@ -28,7 +28,9 @@ import { approvalGateIdForUnit, isExplicitApprovalResponse, shouldPauseForUserAp
|
|
|
28
28
|
import { applyUnitSkillVisibility, unitHasSkillManifest } from "../skill-scope.js";
|
|
29
29
|
import { getGuidedUnitContext } from "../guided-unit-context.js";
|
|
30
30
|
import { registerPlanMilestoneSchemaRecovery } from "./plan-milestone-schema-recovery.js";
|
|
31
|
-
import { AUTO_UNIT_SCOPED_TOOLS, isWorkflowAliasTool } from "../auto-unit-tool-scope.js";
|
|
31
|
+
import { AUTO_UNIT_SCOPED_TOOLS, RUN_UAT_BROWSER_TOOL_NAMES, isWorkflowAliasTool } from "../auto-unit-tool-scope.js";
|
|
32
|
+
import { filterToolsForProvider } from "../model-router.js";
|
|
33
|
+
import { RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.js";
|
|
32
34
|
let approvalQuestionAbortInFlight = false;
|
|
33
35
|
async function loadWelcomeScreenModule() {
|
|
34
36
|
const candidates = [];
|
|
@@ -104,6 +106,7 @@ export const MINIMAL_GSD_TOOL_NAMES = [
|
|
|
104
106
|
"gsd_resume",
|
|
105
107
|
"gsd_milestone_status",
|
|
106
108
|
"gsd_checkpoint_db",
|
|
109
|
+
"gsd_plan_milestone",
|
|
107
110
|
"memory_query",
|
|
108
111
|
"capture_thought",
|
|
109
112
|
];
|
|
@@ -187,6 +190,9 @@ export function buildMinimalGsdToolSet(activeToolNames) {
|
|
|
187
190
|
return withPreservedShimTools([...new Set([...preserved, ...minimal])]);
|
|
188
191
|
}
|
|
189
192
|
export function buildMinimalAutoGsdToolSet(activeToolNames, unitType, registeredToolNames = activeToolNames) {
|
|
193
|
+
if (unitType === "run-uat") {
|
|
194
|
+
return buildRunUatGsdToolSet(activeToolNames, registeredToolNames);
|
|
195
|
+
}
|
|
190
196
|
const unitTools = unitType ? AUTO_UNIT_SCOPED_TOOLS[unitType] ?? [] : [];
|
|
191
197
|
const autoBaseTools = new Set(MINIMAL_AUTO_BASE_TOOL_NAMES);
|
|
192
198
|
const availableBaseTools = registeredToolNames.filter((name) => autoBaseTools.has(name));
|
|
@@ -197,6 +203,10 @@ export function buildMinimalAutoGsdToolSet(activeToolNames, unitType, registered
|
|
|
197
203
|
const scoped = resolveScopedToolNames([...activeToolNames, ...registeredToolNames], [...MINIMAL_GSD_TOOL_NAMES, ...unitTools]);
|
|
198
204
|
return withPreservedShimTools([...new Set([...preserved, ...scoped])]);
|
|
199
205
|
}
|
|
206
|
+
export function buildRunUatGsdToolSet(activeToolNames, registeredToolNames = activeToolNames) {
|
|
207
|
+
const scoped = resolveScopedToolNames([...activeToolNames, ...registeredToolNames], [...RUN_UAT_WORKFLOW_TOOL_NAMES, "subagent", ...RUN_UAT_BROWSER_TOOL_NAMES]);
|
|
208
|
+
return [...new Set(scoped)];
|
|
209
|
+
}
|
|
200
210
|
export function buildMinimalGsdWorkflowToolSet(activeToolNames, registeredToolNames = activeToolNames) {
|
|
201
211
|
const autoBaseTools = new Set(MINIMAL_AUTO_BASE_TOOL_NAMES);
|
|
202
212
|
const availableBaseTools = registeredToolNames.filter((name) => autoBaseTools.has(name));
|
|
@@ -836,10 +846,8 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
836
846
|
if (result.block)
|
|
837
847
|
return result;
|
|
838
848
|
});
|
|
839
|
-
// ── Safety harness: evidence collection + destructive command
|
|
849
|
+
// ── Safety harness: evidence collection + destructive command blocking ──
|
|
840
850
|
pi.on("tool_call", async (event, ctx) => {
|
|
841
|
-
if (!isAutoActive())
|
|
842
|
-
return;
|
|
843
851
|
markToolStart(event.toolCallId, event.toolName);
|
|
844
852
|
safetyRecordToolCall(event.toolCallId, event.toolName, event.input);
|
|
845
853
|
// Persist immediately at dispatch so a mid-unit re-dispatch — which calls
|
|
@@ -854,14 +862,23 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
854
862
|
saveEvidenceToDisk(callDash.basePath, cMid, cSid, cTid);
|
|
855
863
|
}
|
|
856
864
|
}
|
|
857
|
-
// Destructive command classification
|
|
865
|
+
// Destructive command classification + hard gate in all modes.
|
|
858
866
|
if (isToolCallEventType("bash", event)) {
|
|
859
867
|
const classification = classifyCommand(event.input.command);
|
|
860
868
|
if (classification.destructive) {
|
|
869
|
+
const reason = [
|
|
870
|
+
"HARD BLOCK: destructive Bash command requires explicit human confirmation.",
|
|
871
|
+
`Detected: ${classification.labels.join(", ")}`,
|
|
872
|
+
"Run this via ask_user_questions, wait for the user's response,",
|
|
873
|
+
"then issue the command only when confirmed in the current turn.",
|
|
874
|
+
].join(" ");
|
|
861
875
|
safetyLogWarning("safety", `destructive command: ${classification.labels.join(", ")}`, {
|
|
862
876
|
command: String(event.input.command).slice(0, 200),
|
|
863
877
|
});
|
|
864
|
-
|
|
878
|
+
if (ctx) {
|
|
879
|
+
await maybePauseAutoForApprovalGate(ctx, pi, isAutoActive(), "Depth confirmation is waiting for your answer — pausing auto-mode.");
|
|
880
|
+
}
|
|
881
|
+
return { block: true, reason };
|
|
865
882
|
}
|
|
866
883
|
}
|
|
867
884
|
});
|
|
@@ -1123,21 +1140,25 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
1123
1140
|
const fullToolsRequested = isFullGsdToolSurfaceRequested();
|
|
1124
1141
|
const dropAliases = !fullToolsRequested;
|
|
1125
1142
|
const dropBrowser = !fullToolsRequested && !isBrowserToolSurfaceRequested();
|
|
1126
|
-
const
|
|
1143
|
+
const aliasFilteredCompatible = compatible.filter((name) => !(dropAliases && isWorkflowAliasTool(name)));
|
|
1144
|
+
const providerCompatible = aliasFilteredCompatible.filter((name) => !(dropBrowser && isBrowserTool(name)));
|
|
1127
1145
|
const surfaceReduced = providerCompatible.length !== compatible.length;
|
|
1128
1146
|
if (fullToolsRequested) {
|
|
1129
1147
|
return surfaceReduced ? { toolNames: providerCompatible } : undefined;
|
|
1130
1148
|
}
|
|
1131
1149
|
const registeredToolNames = resolveRegisteredToolNames(pi, event.activeToolNames);
|
|
1150
|
+
const compatibleRegisteredToolNames = filterToolsForProvider(registeredToolNames, event.selectedModelApi, event.selectedModelProvider).compatible.filter((name) => !(dropAliases && isWorkflowAliasTool(name)));
|
|
1132
1151
|
const guidedUnit = getGuidedUnitContext();
|
|
1133
|
-
const requestScoped = buildRequestScopedGsdToolSet(providerCompatible, event.requestCustomMessages, registeredToolNames, guidedUnit?.unitType);
|
|
1152
|
+
const requestScoped = buildRequestScopedGsdToolSet(guidedUnit?.unitType === "run-uat" ? aliasFilteredCompatible : providerCompatible, event.requestCustomMessages, guidedUnit?.unitType === "run-uat" ? compatibleRegisteredToolNames : registeredToolNames, guidedUnit?.unitType);
|
|
1134
1153
|
if (requestScoped) {
|
|
1135
1154
|
return { toolNames: requestScoped };
|
|
1136
1155
|
}
|
|
1137
1156
|
const dash = getAutoRuntimeSnapshot();
|
|
1138
1157
|
if (dash.active && dash.currentUnit) {
|
|
1139
1158
|
return {
|
|
1140
|
-
toolNames: buildMinimalAutoGsdToolSet(providerCompatible, dash.currentUnit.type,
|
|
1159
|
+
toolNames: buildMinimalAutoGsdToolSet(dash.currentUnit.type === "run-uat" ? aliasFilteredCompatible : providerCompatible, dash.currentUnit.type, dash.currentUnit.type === "run-uat"
|
|
1160
|
+
? compatibleRegisteredToolNames
|
|
1161
|
+
: resolveRegisteredToolNames(pi, event.activeToolNames)),
|
|
1141
1162
|
};
|
|
1142
1163
|
}
|
|
1143
1164
|
if (isGeneralGsdToolScopingRequested()) {
|
|
@@ -565,6 +565,7 @@ const PLANNING_SUBAGENT_TOOLS = new Set(["subagent", "task"]);
|
|
|
565
565
|
* manifests still declare per-unit subsets via ToolsPolicy.allowedSubagents.
|
|
566
566
|
*/
|
|
567
567
|
const PLANNING_DISPATCH_AGENT_REGISTRY = {
|
|
568
|
+
mnemo: { readOnlySpecialist: true },
|
|
568
569
|
scout: { readOnlySpecialist: true },
|
|
569
570
|
planner: { readOnlySpecialist: true },
|
|
570
571
|
reviewer: { readOnlySpecialist: true },
|
|
@@ -574,7 +575,7 @@ const PLANNING_DISPATCH_AGENT_REGISTRY = {
|
|
|
574
575
|
export const ALLOWED_PLANNING_DISPATCH_AGENTS = new Set(Object.entries(PLANNING_DISPATCH_AGENT_REGISTRY)
|
|
575
576
|
.filter(([, metadata]) => metadata.readOnlySpecialist)
|
|
576
577
|
.map(([agentId]) => agentId));
|
|
577
|
-
let
|
|
578
|
+
let warnedMissingControlledDispatchAgentClasses = false;
|
|
578
579
|
function isReadOnlySpecialist(agentId) {
|
|
579
580
|
const metadata = PLANNING_DISPATCH_AGENT_REGISTRY[agentId];
|
|
580
581
|
return metadata?.readOnlySpecialist === true;
|
|
@@ -582,12 +583,16 @@ function isReadOnlySpecialist(agentId) {
|
|
|
582
583
|
function allowedPlanningDispatchAgentsList() {
|
|
583
584
|
return [...ALLOWED_PLANNING_DISPATCH_AGENTS].join(", ");
|
|
584
585
|
}
|
|
585
|
-
function
|
|
586
|
-
|
|
586
|
+
function allowsControlledSubagentDispatch(policy) {
|
|
587
|
+
return ((policy.mode === "planning-dispatch" || policy.mode === "verification") &&
|
|
588
|
+
Array.isArray(policy.allowedSubagents));
|
|
589
|
+
}
|
|
590
|
+
function warnMissingControlledDispatchAgentClasses(unitType, mode, toolName) {
|
|
591
|
+
if (warnedMissingControlledDispatchAgentClasses)
|
|
587
592
|
return;
|
|
588
|
-
|
|
593
|
+
warnedMissingControlledDispatchAgentClasses = true;
|
|
589
594
|
// TODO(#5060): Remove this migration shim once all subagent/task callers are verified to forward agent identities.
|
|
590
|
-
const message = `[write-gate]
|
|
595
|
+
const message = `[write-gate] controlled-dispatch: shouldBlockPlanningUnit called for tool "${toolName}" ` +
|
|
591
596
|
`on unit "${unitType}" without agentClasses - stale caller; blocking dispatch.`;
|
|
592
597
|
console.warn(message);
|
|
593
598
|
logWarning("intercept", message, {
|
|
@@ -653,8 +658,9 @@ function blockReason(unitType, mode, what) {
|
|
|
653
658
|
* - "docs" → like "planning" but also allows writes to paths
|
|
654
659
|
* matching `allowedPathGlobs` relative to basePath.
|
|
655
660
|
* - "verification"
|
|
656
|
-
* → allows Bash for project verification commands,
|
|
657
|
-
* writes restricted to .gsd
|
|
661
|
+
* → allows Bash for project verification commands, keeps
|
|
662
|
+
* writes restricted to .gsd/, and permits subagent dispatch
|
|
663
|
+
* only when the manifest declares allowedSubagents.
|
|
658
664
|
*
|
|
659
665
|
* `pathOrCommand` is the file path for write/edit-shaped tools and the
|
|
660
666
|
* shell command for bash. Other tools ignore this argument.
|
|
@@ -695,7 +701,7 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
|
|
|
695
701
|
if (tool.startsWith("gsd_"))
|
|
696
702
|
return { block: false };
|
|
697
703
|
if (PLANNING_SUBAGENT_TOOLS.has(tool)) {
|
|
698
|
-
if (policy
|
|
704
|
+
if (allowsControlledSubagentDispatch(policy)) {
|
|
699
705
|
const requested = (agentClasses ?? []).map(a => a.trim()).filter(Boolean);
|
|
700
706
|
const dispatchContract = compileSubagentPermissionContract(policy);
|
|
701
707
|
const allowedSubagents = dispatchContract.allowedSubagents;
|
|
@@ -704,7 +710,7 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
|
|
|
704
710
|
// agent identities yet. Block and warn so stale callers surface in telemetry
|
|
705
711
|
// instead of silently bypassing the gate.
|
|
706
712
|
if (agentClasses === undefined) {
|
|
707
|
-
|
|
713
|
+
warnMissingControlledDispatchAgentClasses(unitType, policy.mode, tool);
|
|
708
714
|
return {
|
|
709
715
|
block: true,
|
|
710
716
|
reason: blockReason(unitType, policy.mode, `subagent dispatch blocked: stale caller did not supply agent identities for "${tool}"; update extractSubagentAgentClasses to handle this input shape`),
|
|
@@ -720,7 +726,7 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
|
|
|
720
726
|
if (globallyDisallowed) {
|
|
721
727
|
return {
|
|
722
728
|
block: true,
|
|
723
|
-
reason: blockReason(unitType, policy.mode, `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from
|
|
729
|
+
reason: blockReason(unitType, policy.mode, `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from ${policy.mode} units`),
|
|
724
730
|
};
|
|
725
731
|
}
|
|
726
732
|
const disallowedByPolicy = requested.find(a => !allowed.has(a));
|
|
@@ -69,7 +69,7 @@ export function showHelp(ctx, args = "") {
|
|
|
69
69
|
" /gsd new-milestone Create milestone from headless context (used by gsd headless)",
|
|
70
70
|
" /gsd new-project Bootstrap a new project (use --deep for staged project-level discovery)",
|
|
71
71
|
" /gsd quick Execute a quick task without full planning overhead",
|
|
72
|
-
" /gsd dispatch Dispatch a specific phase directly [research|plan|execute|complete|uat|replan]",
|
|
72
|
+
" /gsd dispatch Dispatch a specific phase directly [research|plan|execute|complete|validate|reassess|uat|replan]",
|
|
73
73
|
" /gsd verdict <v> Override milestone validation verdict [pass|needs-attention|needs-remediation] [--milestone Mxxx] [--rationale \"...\"]",
|
|
74
74
|
" /gsd parallel Parallel milestone orchestration [start|status|stop|pause|resume|merge|watch]",
|
|
75
75
|
" /gsd workflow Custom workflow lifecycle [new|run|list|validate|pause|resume]",
|
|
@@ -11,6 +11,8 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
import { getGlobalGSDPreferencesPath, getLegacyGlobalGSDPreferencesPath, getProjectGSDPreferencesPath, loadGlobalGSDPreferences, loadProjectGSDPreferences, loadEffectiveGSDPreferences, normalizePreferencesShape, resolveAllSkillReferences, } from "./preferences.js";
|
|
12
12
|
import { loadFile, saveFile, splitFrontmatter, parseFrontmatterMap } from "./files.js";
|
|
13
13
|
import { runClaudeImportFlow } from "./claude-import.js";
|
|
14
|
+
const DEFAULT_WIDGET_MODE = "small";
|
|
15
|
+
const WIDGET_MODE_OPTIONS = [DEFAULT_WIDGET_MODE, "full", "min", "off"];
|
|
14
16
|
/** Extract body content after frontmatter closing delimiter, or null if none. */
|
|
15
17
|
function extractBodyAfterFrontmatter(content) {
|
|
16
18
|
const closingIdx = content.indexOf("\n---", content.indexOf("---"));
|
|
@@ -1509,7 +1511,7 @@ async function configureAdvanced(ctx, prefs) {
|
|
|
1509
1511
|
else if (minRequestInterval !== undefined) {
|
|
1510
1512
|
prefs.min_request_interval_ms = minRequestInterval;
|
|
1511
1513
|
}
|
|
1512
|
-
const widget = await promptEnum(ctx, "Auto-mode widget display", prefs.widget_mode,
|
|
1514
|
+
const widget = await promptEnum(ctx, "Auto-mode widget display", prefs.widget_mode, WIDGET_MODE_OPTIONS, DEFAULT_WIDGET_MODE);
|
|
1513
1515
|
if (widget !== undefined)
|
|
1514
1516
|
prefs.widget_mode = widget;
|
|
1515
1517
|
const experimental = prefs.experimental ?? {};
|
|
@@ -166,6 +166,6 @@ export async function handleVerdict(rawArgs, ctx, basePath) {
|
|
|
166
166
|
ctx.ui.notify(`Milestone ${milestoneId} verdict: ${prevVerdict} -> ${effectiveVerdict} (${existingValidation.source})`, "success");
|
|
167
167
|
}
|
|
168
168
|
if (effectiveVerdict === "needs-remediation") {
|
|
169
|
-
ctx.ui.notify("Follow up with
|
|
169
|
+
ctx.ui.notify("Follow up with /gsd dispatch reassess to add remediation slices, then re-run /gsd auto.", "info");
|
|
170
170
|
}
|
|
171
171
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { matchesKey, Key, truncateToWidth } from "@gsd/pi-tui";
|
|
10
10
|
import { renderDialogFrame, renderKeyHints } from "./tui/render-kit.js";
|
|
11
11
|
import { loadEffectiveGSDPreferences, loadGlobalGSDPreferences, loadProjectGSDPreferences, getGlobalGSDPreferencesPath, getProjectGSDPreferencesPath, resolveDynamicRoutingConfig, resolveEffectiveProfile, resolveModelWithFallbacksForUnit, resolveAutoSupervisorConfig, } from "./preferences.js";
|
|
12
|
+
const DEFAULT_WIDGET_MODE = "small";
|
|
12
13
|
function collectConfigSections() {
|
|
13
14
|
const sections = [];
|
|
14
15
|
const globalPrefs = loadGlobalGSDPreferences();
|
|
@@ -157,7 +158,7 @@ function collectConfigSections() {
|
|
|
157
158
|
toggleRows.push({ label: "search_provider", value: prefs.search_provider });
|
|
158
159
|
if (prefs?.context_selection)
|
|
159
160
|
toggleRows.push({ label: "context_selection", value: prefs.context_selection });
|
|
160
|
-
if (prefs?.widget_mode && prefs.widget_mode !==
|
|
161
|
+
if (prefs?.widget_mode && prefs.widget_mode !== DEFAULT_WIDGET_MODE)
|
|
161
162
|
toggleRows.push({ label: "widget_mode", value: prefs.widget_mode });
|
|
162
163
|
if (prefs?.experimental?.rtk)
|
|
163
164
|
toggleRows.push({ label: "experimental.rtk", value: "on" });
|
|
@@ -21,9 +21,10 @@ export function resetRetryState(state) {
|
|
|
21
21
|
const PERMANENT_RE = /auth|unauthorized|forbidden|invalid.*key|invalid.*api|billing|quota exceeded|account/i;
|
|
22
22
|
// Include provider-specific quota-window phrasing like:
|
|
23
23
|
// - "You've hit your limit"
|
|
24
|
+
// - "You've reached your limit"
|
|
24
25
|
// - "usage limit" / "quota reached"
|
|
25
26
|
// - "out of extra usage"
|
|
26
|
-
const RATE_LIMIT_RE = /rate.?limit|too many requests|429|hit your limit|usage limit|out of extra usage|quota (?:reached|hit)|limit.*resets?/i;
|
|
27
|
+
const RATE_LIMIT_RE = /rate.?limit|too many requests|429|(?:hit|reached) your (?:\w+ )?limit|(?:usage|session|weekly|daily|monthly|quota) limit|out of extra usage|quota (?:reached|hit)|limit.*resets?/i;
|
|
27
28
|
// OpenRouter affordability-style quota errors should be treated as transient
|
|
28
29
|
// so core retry logic can lower maxTokens and continue in-session.
|
|
29
30
|
const AFFORDABILITY_RE = /requires more credits|can only afford|insufficient credits|not enough credits|fewer max_tokens/i;
|
|
@@ -247,6 +247,7 @@ function writeMeta(path, result, request, now) {
|
|
|
247
247
|
id: result.id,
|
|
248
248
|
runtime: result.runtime,
|
|
249
249
|
purpose: request.purpose ?? null,
|
|
250
|
+
...(request.metadata ? { metadata: request.metadata } : {}),
|
|
250
251
|
script_chars: request.script.length,
|
|
251
252
|
started_at: now.toISOString(),
|
|
252
253
|
finished_at: new Date(now.getTime() + result.duration_ms).toISOString(),
|
|
@@ -260,6 +261,7 @@ function writeMeta(path, result, request, now) {
|
|
|
260
261
|
stderr_truncated: result.stderr_truncated,
|
|
261
262
|
stdout_path: result.stdout_path,
|
|
262
263
|
stderr_path: result.stderr_path,
|
|
264
|
+
...(request.metadata ? { metadata: request.metadata } : {}),
|
|
263
265
|
};
|
|
264
266
|
writeFileSync(path, `${JSON.stringify(meta, null, 2)}\n`);
|
|
265
267
|
}
|
|
@@ -37,7 +37,13 @@ You are the UAT runner. Execute every check defined in `{{uatPath}}` as deeply a
|
|
|
37
37
|
|
|
38
38
|
Choose the lightest tool that proves the check honestly:
|
|
39
39
|
|
|
40
|
-
- Run
|
|
40
|
+
- Run automated checks with `gsd_uat_exec`
|
|
41
|
+
- Use `uat-artifact-check` as `intent` for static file, grep, structure, or artifact checks.
|
|
42
|
+
- Use `uat-runtime-check` as `intent` for executing tests, scripts, or runtime assertions.
|
|
43
|
+
- Use `uat-browser-check` as `intent` for browser interaction or screenshot-backed UI checks.
|
|
44
|
+
- Use `uat-service-start` as `intent` only when starting or connecting to an app/service.
|
|
45
|
+
- Use `uat-log-inspection` as `intent` for checking logs or captured output files.
|
|
46
|
+
- The result-table evidence mode is separate; do not use `artifact`, `runtime`, or `human-follow-up` as `intent`.
|
|
41
47
|
- Run `grep` / `rg` checks against files
|
|
42
48
|
- Run `node` / other script invocations
|
|
43
49
|
- Read files and verify their contents
|
|
@@ -48,7 +54,7 @@ Choose the lightest tool that proves the check honestly:
|
|
|
48
54
|
For each check, record:
|
|
49
55
|
- The check description (from the UAT file)
|
|
50
56
|
- The evidence mode used: `artifact`, `runtime`, or `human-follow-up`
|
|
51
|
-
- The command or action taken
|
|
57
|
+
- The command or action taken, including the `gsd_uat_exec` evidence ID for automated checks
|
|
52
58
|
- The actual result observed
|
|
53
59
|
- `PASS`, `FAIL`, or `NEEDS-HUMAN`
|
|
54
60
|
|
|
@@ -57,7 +63,7 @@ After running all checks, compute the **overall verdict**:
|
|
|
57
63
|
- `FAIL` — one or more automatable checks failed
|
|
58
64
|
- `PARTIAL` — one or more automatable checks were skipped or returned inconclusive results (not the same as `NEEDS-HUMAN` — use PARTIAL only when the agent itself could not determine pass/fail for a check it was supposed to automate)
|
|
59
65
|
|
|
60
|
-
Call `gsd_summary_save` with `milestone_id: {{milestoneId}}`, `slice_id: {{sliceId}}`, `artifact_type: "ASSESSMENT"`, and the full UAT result markdown as `content
|
|
66
|
+
Call `gsd_summary_save` with `milestone_id: "{{milestoneId}}"`, `slice_id: "{{sliceId}}"`, `artifact_type: "ASSESSMENT"`, and the full UAT result markdown as `content`. The tool computes the assessment path, persists to DB/disk, and saves the aggregate UAT gate. The content should follow this logical shape:
|
|
61
67
|
|
|
62
68
|
```markdown
|
|
63
69
|
---
|
|
@@ -86,6 +92,6 @@ date: <ISO 8601 timestamp>
|
|
|
86
92
|
|
|
87
93
|
---
|
|
88
94
|
|
|
89
|
-
**You MUST call `gsd_summary_save` with the UAT result content before finishing.**
|
|
95
|
+
**You MUST call `gsd_summary_save` with `artifact_type: "ASSESSMENT"` and the UAT result content before finishing. Do not write the assessment file directly.**
|
|
90
96
|
|
|
91
97
|
When done, say: "UAT {{sliceId}} complete."
|
|
@@ -32,7 +32,7 @@ GSD ships with bundled skills. Installed skills are listed in `<available_skills
|
|
|
32
32
|
- Never print, echo, log, or restate secrets or credentials. Report only key names and applied/skipped status.
|
|
33
33
|
- Never ask the user to edit `.env` files or set secrets manually. Use `secure_env_collect`.
|
|
34
34
|
- In enduring files, write current state only unless the file is explicitly historical.
|
|
35
|
-
- **Never take outward-facing actions on GitHub or external services without explicit user confirmation.** This includes creating/closing issues, merging/approving/commenting on PRs, pushing remote branches, publishing packages, or any state change outside local filesystem. Read-only listing/viewing/diffing is fine. Present intent and get a clear "yes" first. **Non-bypassable:** no response, ambiguity, or `ask_user_questions` failure means re-ask; never rationalize past the block. Missing "yes" means "no."
|
|
35
|
+
- **Never take outward-facing actions on GitHub or external services without explicit user confirmation.** This includes creating/closing issues, merging/approving/commenting on PRs, pushing remote branches, publishing packages, terragrunt/aws/kubectl mutations, or any state change outside local filesystem. Read-only listing/viewing/diffing is fine. Present intent and get a clear "yes" first. **Non-bypassable:** no response, ambiguity, or `ask_user_questions` failure means re-ask; never rationalize past the block. Missing "yes" means "no."
|
|
36
36
|
|
|
37
37
|
If a `GSD Skill Preferences` block appears below, treat it as durable guidance for skills to use, prefer, or avoid unless it conflicts with artifact rules, verification, or higher-priority instructions.
|
|
38
38
|
|
|
@@ -160,4 +160,6 @@ Fix root causes, not symptoms. If applying temporary mitigation, label it and pr
|
|
|
160
160
|
- When debugging, stay curious. Problems are puzzles. Say what's interesting about the failure before reaching for fixes.
|
|
161
161
|
- After completing a task, give a brief summary and 2-4 numbered next-step options; last option is always "Other". Omit the list for strict output formats.
|
|
162
162
|
|
|
163
|
+
If any next step is destructive/outward-facing, present it via `ask_user_questions` and wait for the user's answer before execution. Do not execute a next-step item from a prior plain-text numbered list without fresh confirmation.
|
|
164
|
+
|
|
163
165
|
Good narration states a decision or finding: "Three handlers follow a middleware pattern - using that instead of a custom wrapper." Bad narration just announces the next call ("Reading the file now.") or emits compressed planner notes ("Need create plan artifact maybe read existing plans.").
|
|
@@ -16,6 +16,9 @@ const DESTRUCTIVE_PATTERNS = [
|
|
|
16
16
|
{ pattern: /\btruncate\s+table\b/i, label: "SQL truncate" },
|
|
17
17
|
{ pattern: /\bchmod\s+777\b/, label: "world-writable permissions" },
|
|
18
18
|
{ pattern: /\bcurl\s.*\|\s*(bash|sh|zsh)\b/, label: "pipe to shell" },
|
|
19
|
+
{ pattern: /\bterra(form|grunt)\s+(apply|destroy)/i, label: "IaC apply/destroy" },
|
|
20
|
+
{ pattern: /\baws\s+\w+\s+(delete|create|put|remove|terminate)\b/i, label: "AWS mutation" },
|
|
21
|
+
{ pattern: /\bkubectl\s+(delete|apply)\b/i, label: "kubectl mutation" },
|
|
19
22
|
];
|
|
20
23
|
/**
|
|
21
24
|
* Classify a bash command for destructive operations.
|
|
@@ -41,6 +41,16 @@ function tokenizeSkillContext(...parts) {
|
|
|
41
41
|
}
|
|
42
42
|
return tokens;
|
|
43
43
|
}
|
|
44
|
+
function tokenizeUnitType(unitType) {
|
|
45
|
+
const tokens = new Set();
|
|
46
|
+
const value = unitType?.trim().toLowerCase();
|
|
47
|
+
if (!value)
|
|
48
|
+
return tokens;
|
|
49
|
+
tokens.add(value);
|
|
50
|
+
tokens.add(value.replace(/[-_]+/g, " "));
|
|
51
|
+
tokens.add(value.replace(/[-_\s]+/g, ""));
|
|
52
|
+
return tokens;
|
|
53
|
+
}
|
|
44
54
|
function skillMatchesContext(skill, contextTokens) {
|
|
45
55
|
const haystacks = [
|
|
46
56
|
skill.name.toLowerCase(),
|
|
@@ -63,13 +73,20 @@ function ruleMatchesContext(when, contextTokens) {
|
|
|
63
73
|
const whenTokens = tokenizeSkillContext(when);
|
|
64
74
|
return [...whenTokens].some(token => contextTokens.has(token) || [...contextTokens].some(ctx => ctx.includes(token) || token.includes(ctx)));
|
|
65
75
|
}
|
|
66
|
-
function
|
|
76
|
+
function ruleMatchesUnitType(when, unitType) {
|
|
77
|
+
if (!unitType)
|
|
78
|
+
return false;
|
|
79
|
+
const whenTokens = tokenizeSkillContext(when);
|
|
80
|
+
const unitTokens = tokenizeUnitType(unitType);
|
|
81
|
+
return [...unitTokens].some(token => whenTokens.has(token));
|
|
82
|
+
}
|
|
83
|
+
function resolveSkillRuleMatches(prefs, contextTokens, base, unitType) {
|
|
67
84
|
if (!prefs?.skill_rules?.length)
|
|
68
85
|
return { include: [], avoid: [] };
|
|
69
86
|
const include = [];
|
|
70
87
|
const avoid = [];
|
|
71
88
|
for (const rule of prefs.skill_rules) {
|
|
72
|
-
if (!ruleMatchesContext(rule.when, contextTokens))
|
|
89
|
+
if (!ruleMatchesContext(rule.when, contextTokens) && !ruleMatchesUnitType(rule.when, unitType))
|
|
73
90
|
continue;
|
|
74
91
|
include.push(...resolvePreferenceSkillNames([...(rule.use ?? []), ...(rule.prefer ?? [])], base));
|
|
75
92
|
avoid.push(...resolvePreferenceSkillNames(rule.avoid ?? [], base));
|
|
@@ -141,7 +158,7 @@ export function buildSkillActivationBlock(params) {
|
|
|
141
158
|
for (const name of resolvePreferenceSkillNames(prefs?.always_use_skills ?? [], params.base)) {
|
|
142
159
|
matched.add(name);
|
|
143
160
|
}
|
|
144
|
-
const ruleMatches = resolveSkillRuleMatches(prefs, contextTokens, params.base);
|
|
161
|
+
const ruleMatches = resolveSkillRuleMatches(prefs, contextTokens, params.base, params.unitType);
|
|
145
162
|
for (const name of ruleMatches.include)
|
|
146
163
|
matched.add(name);
|
|
147
164
|
for (const name of ruleMatches.avoid)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// checkboxes) and the DB slice rows for that milestone, then re-renders the
|
|
5
5
|
// ROADMAP projection from the authoritative DB rows.
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
-
import { getMilestone, getMilestoneSlices, isDbAvailable, } from "../../gsd-db.js";
|
|
7
|
+
import { getMilestone, getMilestoneSlices, getSliceTasks, isDbAvailable, } from "../../gsd-db.js";
|
|
8
8
|
import { renderRoadmapFromDb } from "../../markdown-renderer.js";
|
|
9
9
|
import { findMilestoneIds } from "../../milestone-ids.js";
|
|
10
10
|
import { parseRoadmap } from "../../parsers-legacy.js";
|
|
@@ -18,6 +18,15 @@ function arraysEqual(a, b) {
|
|
|
18
18
|
return false;
|
|
19
19
|
return true;
|
|
20
20
|
}
|
|
21
|
+
function getSlicesReadyForDivergenceCheck(milestoneId, dbSlices) {
|
|
22
|
+
const ready = new Set();
|
|
23
|
+
for (const slice of dbSlices) {
|
|
24
|
+
if (isClosedStatus(slice.status) || getSliceTasks(milestoneId, slice.id).length > 0) {
|
|
25
|
+
ready.add(slice.id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return ready;
|
|
29
|
+
}
|
|
21
30
|
function milestoneHasDivergence(basePath, milestoneId) {
|
|
22
31
|
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
23
32
|
if (!roadmapPath || !existsSync(roadmapPath))
|
|
@@ -31,6 +40,10 @@ function milestoneHasDivergence(basePath, milestoneId) {
|
|
|
31
40
|
}
|
|
32
41
|
const dbSlices = getMilestoneSlices(milestoneId);
|
|
33
42
|
const dbSliceMap = new Map(dbSlices.map((s) => [s.id, s]));
|
|
43
|
+
const readySliceIds = getSlicesReadyForDivergenceCheck(milestoneId, dbSlices);
|
|
44
|
+
if (dbSlices.length > 0 && readySliceIds.size === 0) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
34
47
|
const roadmapSliceIds = new Set();
|
|
35
48
|
for (let i = 0; i < roadmap.slices.length; i++) {
|
|
36
49
|
const roadmapSlice = roadmap.slices[i];
|
|
@@ -39,6 +52,8 @@ function milestoneHasDivergence(basePath, milestoneId) {
|
|
|
39
52
|
const dbSlice = dbSliceMap.get(roadmapSlice.id);
|
|
40
53
|
if (!dbSlice)
|
|
41
54
|
return true; // Roadmap has a slice the DB doesn't.
|
|
55
|
+
if (!readySliceIds.has(dbSlice.id))
|
|
56
|
+
continue;
|
|
42
57
|
if (dbSlice.sequence !== expectedSequence)
|
|
43
58
|
return true;
|
|
44
59
|
if (!arraysEqual(dbSlice.depends, roadmapSlice.depends))
|
|
@@ -47,6 +62,8 @@ function milestoneHasDivergence(basePath, milestoneId) {
|
|
|
47
62
|
return true;
|
|
48
63
|
}
|
|
49
64
|
for (const dbSlice of dbSlices) {
|
|
65
|
+
if (!readySliceIds.has(dbSlice.id))
|
|
66
|
+
continue;
|
|
50
67
|
if (!roadmapSliceIds.has(dbSlice.id))
|
|
51
68
|
return true;
|
|
52
69
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// File Purpose: ADR-017 drift-driven State Reconciliation Module entry point.
|
|
3
3
|
// reconcileBeforeDispatch runs before every Dispatch decision and worker spawn.
|
|
4
4
|
import { deriveState as defaultDeriveState, invalidateStateCache as defaultInvalidate, } from "../state.js";
|
|
5
|
+
import { clearParseCache as defaultClearParseCache } from "../files.js";
|
|
5
6
|
import { ReconciliationFailedError, } from "./errors.js";
|
|
6
7
|
import { DRIFT_REGISTRY } from "./registry.js";
|
|
7
8
|
export { ReconciliationFailedError } from "./errors.js";
|
|
@@ -10,6 +11,7 @@ const MAX_PASSES = 2;
|
|
|
10
11
|
const defaultDeps = {
|
|
11
12
|
invalidateStateCache: defaultInvalidate,
|
|
12
13
|
deriveState: defaultDeriveState,
|
|
14
|
+
clearParseCache: defaultClearParseCache,
|
|
13
15
|
};
|
|
14
16
|
/**
|
|
15
17
|
* Drift-driven pre-dispatch reconciliation per ADR-017.
|
|
@@ -27,6 +29,7 @@ const defaultDeps = {
|
|
|
27
29
|
*/
|
|
28
30
|
export async function reconcileBeforeDispatch(basePath, deps = defaultDeps) {
|
|
29
31
|
const registry = deps.registry ?? DRIFT_REGISTRY;
|
|
32
|
+
const clearParseCache = deps.clearParseCache ?? defaultClearParseCache;
|
|
30
33
|
const repaired = [];
|
|
31
34
|
for (let pass = 0; pass < MAX_PASSES; pass++) {
|
|
32
35
|
deps.invalidateStateCache();
|
|
@@ -67,6 +70,9 @@ export async function reconcileBeforeDispatch(basePath, deps = defaultDeps) {
|
|
|
67
70
|
failures.push({ drift: record, cause });
|
|
68
71
|
}
|
|
69
72
|
}
|
|
73
|
+
if (repairedThisPass) {
|
|
74
|
+
clearParseCache();
|
|
75
|
+
}
|
|
70
76
|
if (blockers.length > 0) {
|
|
71
77
|
let blockerState = stateSnapshot;
|
|
72
78
|
if (repairedThisPass) {
|
|
@@ -35,7 +35,7 @@ function formatNeedsRemediationBlocker(milestoneId) {
|
|
|
35
35
|
return [
|
|
36
36
|
`Milestone ${milestoneId} is blocked because milestone validation returned needs-remediation, but all slices are complete.`,
|
|
37
37
|
`Fix options:`,
|
|
38
|
-
`1.
|
|
38
|
+
`1. Run \`/gsd dispatch reassess\` to add remediation slices, then run \`/gsd auto\``,
|
|
39
39
|
`2. If the finding is acceptable, override it: \`/gsd verdict pass --rationale "why this is okay"\``,
|
|
40
40
|
`3. If this should wait, defer it explicitly: \`/gsd park ${milestoneId}\``,
|
|
41
41
|
].join("\n");
|
|
@@ -5,6 +5,27 @@ import { realpathSync } from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { isContextModeEnabled } from "../preferences-types.js";
|
|
7
7
|
import { contextModeDisabledResult } from "./context-mode-tool-result.js";
|
|
8
|
+
const UAT_EXEC_INTENTS = [
|
|
9
|
+
"uat-artifact-check",
|
|
10
|
+
"uat-runtime-check",
|
|
11
|
+
"uat-browser-check",
|
|
12
|
+
"uat-service-start",
|
|
13
|
+
"uat-log-inspection",
|
|
14
|
+
];
|
|
15
|
+
const UAT_EXEC_INTENT_ALIASES = {
|
|
16
|
+
artifact: "uat-artifact-check",
|
|
17
|
+
"artifact-driven": "uat-artifact-check",
|
|
18
|
+
runtime: "uat-runtime-check",
|
|
19
|
+
"runtime-executable": "uat-runtime-check",
|
|
20
|
+
"live-runtime": "uat-runtime-check",
|
|
21
|
+
browser: "uat-browser-check",
|
|
22
|
+
"browser-executable": "uat-browser-check",
|
|
23
|
+
service: "uat-service-start",
|
|
24
|
+
"service-start": "uat-service-start",
|
|
25
|
+
log: "uat-log-inspection",
|
|
26
|
+
logs: "uat-log-inspection",
|
|
27
|
+
"log-inspection": "uat-log-inspection",
|
|
28
|
+
};
|
|
8
29
|
export function buildExecOptions(baseDir, cfg, extras) {
|
|
9
30
|
const allowlist = Array.isArray(cfg?.exec_env_allowlist) ? cfg.exec_env_allowlist : EXEC_DEFAULTS.envAllowlist;
|
|
10
31
|
const stdoutCap = clampNumber(cfg?.exec_stdout_cap_bytes, EXEC_DEFAULTS.stdoutCapBytes, 4_096, 16_777_216);
|
|
@@ -73,6 +94,39 @@ function normalizeScript(params) {
|
|
|
73
94
|
}
|
|
74
95
|
return paramError("script is required and must be a non-empty string");
|
|
75
96
|
}
|
|
97
|
+
function normalizeRequiredString(value, field) {
|
|
98
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
99
|
+
return paramError(`${field} is required and must be a non-empty string`);
|
|
100
|
+
}
|
|
101
|
+
return value.trim();
|
|
102
|
+
}
|
|
103
|
+
function normalizeUatIntent(value) {
|
|
104
|
+
if (typeof value !== "string") {
|
|
105
|
+
return paramError(`intent is required and must be one of: ${UAT_EXEC_INTENTS.join(", ")}`);
|
|
106
|
+
}
|
|
107
|
+
const normalized = value.trim().toLowerCase();
|
|
108
|
+
if (UAT_EXEC_INTENTS.includes(normalized))
|
|
109
|
+
return normalized;
|
|
110
|
+
const alias = UAT_EXEC_INTENT_ALIASES[normalized];
|
|
111
|
+
if (alias)
|
|
112
|
+
return alias;
|
|
113
|
+
return paramError(`invalid intent "${value}" — must be one of: ${UAT_EXEC_INTENTS.join(", ")}`);
|
|
114
|
+
}
|
|
115
|
+
function rejectUatScript(script) {
|
|
116
|
+
const patterns = [
|
|
117
|
+
{ re: /\b(?:npm|pnpm|yarn|bun)\s+(?:i|install|add|remove|update|upgrade)\b/i, reason: "package dependency mutation is not allowed during UAT" },
|
|
118
|
+
{ re: /\b(?:pip|pip3|python\s+-m\s+pip)\s+install\b/i, reason: "package dependency mutation is not allowed during UAT" },
|
|
119
|
+
{ re: /\bgit\s+(?:add|commit|push|reset|checkout|switch|merge|rebase|clean|rm|mv|tag|branch)\b/i, reason: "git mutations are not allowed during UAT" },
|
|
120
|
+
{ re: /\brm\s+-[^\n\r;|&]*r[^\n\r;|&]*f\b/i, reason: "destructive filesystem cleanup is not allowed during UAT" },
|
|
121
|
+
{ re: /\b(?:env|printenv)\b(?:\s|$)/i, reason: "dumping environment variables is not allowed during UAT" },
|
|
122
|
+
{ re: /\bcat\s+\.env(?:\b|\.|$)/i, reason: "reading credential files is not allowed during UAT" },
|
|
123
|
+
];
|
|
124
|
+
for (const pattern of patterns) {
|
|
125
|
+
if (pattern.re.test(script))
|
|
126
|
+
return pattern.reason;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
76
130
|
function isToolExecutionResult(value) {
|
|
77
131
|
return typeof value === "object" && value !== null && Array.isArray(value.content);
|
|
78
132
|
}
|
|
@@ -207,6 +261,7 @@ export async function executeGsdExec(params, deps) {
|
|
|
207
261
|
runtime,
|
|
208
262
|
script,
|
|
209
263
|
...(typeof params.purpose === "string" ? { purpose: params.purpose } : {}),
|
|
264
|
+
...(params.metadata && typeof params.metadata === "object" ? { metadata: params.metadata } : {}),
|
|
210
265
|
...(typeof params.timeout_ms === "number" ? { timeout_ms: params.timeout_ms } : {}),
|
|
211
266
|
}, opts);
|
|
212
267
|
return formatResult(result);
|
|
@@ -220,6 +275,60 @@ export async function executeGsdExec(params, deps) {
|
|
|
220
275
|
};
|
|
221
276
|
}
|
|
222
277
|
}
|
|
278
|
+
export async function executeUatExec(params, deps) {
|
|
279
|
+
const milestoneId = normalizeRequiredString(params.milestoneId, "milestoneId");
|
|
280
|
+
if (isToolExecutionResult(milestoneId))
|
|
281
|
+
return milestoneId;
|
|
282
|
+
const sliceId = normalizeRequiredString(params.sliceId, "sliceId");
|
|
283
|
+
if (isToolExecutionResult(sliceId))
|
|
284
|
+
return sliceId;
|
|
285
|
+
const checkId = normalizeRequiredString(params.checkId, "checkId");
|
|
286
|
+
if (isToolExecutionResult(checkId))
|
|
287
|
+
return checkId;
|
|
288
|
+
const intent = normalizeUatIntent(params.intent);
|
|
289
|
+
if (isToolExecutionResult(intent))
|
|
290
|
+
return intent;
|
|
291
|
+
const script = normalizeScript(params);
|
|
292
|
+
if (isToolExecutionResult(script))
|
|
293
|
+
return script;
|
|
294
|
+
const rejected = rejectUatScript(script);
|
|
295
|
+
if (rejected) {
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: "text", text: `Error: gsd_uat_exec blocked command — ${rejected}` }],
|
|
298
|
+
details: { operation: "gsd_uat_exec", error: "uat_exec_policy_block", reason: rejected },
|
|
299
|
+
isError: true,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const result = await executeGsdExec({
|
|
303
|
+
...params,
|
|
304
|
+
script,
|
|
305
|
+
purpose: typeof params.purpose === "string" && params.purpose.trim().length > 0
|
|
306
|
+
? params.purpose
|
|
307
|
+
: `UAT ${milestoneId}/${sliceId}/${checkId} (${intent})`,
|
|
308
|
+
metadata: {
|
|
309
|
+
kind: "uat_exec",
|
|
310
|
+
milestoneId,
|
|
311
|
+
sliceId,
|
|
312
|
+
checkId,
|
|
313
|
+
intent,
|
|
314
|
+
...(typeof params.expected === "string" && params.expected.trim().length > 0
|
|
315
|
+
? { expected: params.expected.trim() }
|
|
316
|
+
: {}),
|
|
317
|
+
},
|
|
318
|
+
}, deps);
|
|
319
|
+
const details = result.details ?? {};
|
|
320
|
+
return {
|
|
321
|
+
...result,
|
|
322
|
+
details: {
|
|
323
|
+
...details,
|
|
324
|
+
operation: "gsd_uat_exec",
|
|
325
|
+
milestoneId,
|
|
326
|
+
sliceId,
|
|
327
|
+
checkId,
|
|
328
|
+
intent,
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
223
332
|
function formatResult(result) {
|
|
224
333
|
const headerLines = [
|
|
225
334
|
`gsd_exec[${result.id}] runtime=${result.runtime} exit=${formatExit(result)} duration=${result.duration_ms}ms`,
|