@opengsd/gsd-pi 1.1.1-dev.2034b16 → 1.1.1-dev.2de7ea0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +3 -2
- package/dist/help-text.js +10 -6
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +495 -0
- package/dist/resources/extensions/browser-tools/engine/selection.js +16 -0
- package/dist/resources/extensions/browser-tools/extension-manifest.json +2 -2
- package/dist/resources/extensions/browser-tools/index.js +57 -9
- package/dist/resources/extensions/browser-tools/package.json +5 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +21 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +15 -6
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +2 -2
- package/dist/resources/extensions/gsd/browser-evidence.js +29 -2
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +2 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +76 -11
- package/dist/resources/extensions/gsd/commands-mcp-status.js +2 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +8 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +2 -2
- package/dist/resources/extensions/gsd/mcp-project-config.js +9 -76
- package/dist/resources/extensions/gsd/post-unit-hooks.js +9 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +39 -0
- package/dist/resources/extensions/gsd/prompt-loader.js +7 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +40 -22
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +3 -3
- package/dist/resources/extensions/gsd/rule-registry.js +428 -52
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +46 -16
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +29 -14
- package/dist/resources/extensions/gsd/verdict-parser.js +59 -15
- package/dist/resources/extensions/shared/gsd-browser-cli.js +145 -0
- package/dist/rtk.d.ts +7 -1
- package/dist/rtk.js +27 -11
- package/dist/update-check.d.ts +15 -1
- package/dist/update-check.js +87 -12
- package/dist/update-cmd.d.ts +1 -0
- package/dist/update-cmd.js +53 -2
- 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/api/update/route.js +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 +3 -2
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/dist/session/agent-session-compaction.d.ts +2 -0
- package/packages/gsd-agent-core/dist/session/agent-session-compaction.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/session/agent-session-compaction.js +8 -2
- package/packages/gsd-agent-core/dist/session/agent-session-compaction.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +23 -9
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +17 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +19 -2
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +579 -0
- package/src/resources/extensions/browser-tools/engine/selection.ts +19 -0
- package/src/resources/extensions/browser-tools/extension-manifest.json +2 -2
- package/src/resources/extensions/browser-tools/index.ts +60 -9
- package/src/resources/extensions/browser-tools/package.json +5 -1
- package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +35 -0
- package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +33 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +28 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +16 -6
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +2 -2
- package/src/resources/extensions/gsd/browser-evidence.ts +26 -2
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +2 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +76 -11
- package/src/resources/extensions/gsd/commands-mcp-status.ts +2 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +8 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +2 -2
- package/src/resources/extensions/gsd/mcp-project-config.ts +13 -78
- package/src/resources/extensions/gsd/post-unit-hooks.ts +14 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +36 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +8 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +40 -22
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +3 -3
- package/src/resources/extensions/gsd/rule-registry.ts +558 -58
- package/src/resources/extensions/gsd/rule-types.ts +2 -0
- package/src/resources/extensions/gsd/tests/browser-evidence.test.ts +142 -0
- package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/doctor-runtime-checks.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +66 -10
- package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/mcp-status.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +157 -0
- package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +179 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +29 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +22 -1
- package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/rule-registry.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +133 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +74 -0
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +46 -15
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +31 -14
- package/src/resources/extensions/gsd/types.ts +63 -0
- package/src/resources/extensions/gsd/verdict-parser.ts +54 -13
- package/src/resources/extensions/shared/gsd-browser-cli.ts +172 -0
- /package/dist/web/standalone/.next/static/{StOMnvtgGiBHrBOZJZ1Gr → JdwzU6IGLVBZPf84PIaJQ}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{StOMnvtgGiBHrBOZJZ1Gr → JdwzU6IGLVBZPf84PIaJQ}/_ssgManifest.js +0 -0
|
@@ -78,18 +78,10 @@ function getRequiredVerificationClasses(milestoneId: string): string[] {
|
|
|
78
78
|
return required;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const artifact = getArtifact(artifactPath);
|
|
86
|
-
if (artifact?.full_content) chunks.push(artifact.full_content);
|
|
87
|
-
|
|
88
|
-
const assessmentPath = resolveSliceFile(basePath, milestoneId, slice.id, "ASSESSMENT");
|
|
89
|
-
const assessmentContent = assessmentPath ? await loadFile(assessmentPath) : null;
|
|
90
|
-
if (assessmentContent) chunks.push(assessmentContent);
|
|
91
|
-
}
|
|
92
|
-
return chunks.join("\n\n");
|
|
81
|
+
function hasRuntimeExecutableUatEvidenceText(text: string): boolean {
|
|
82
|
+
if (!/\buatType:\s*runtime-executable\b/i.test(text)) return false;
|
|
83
|
+
if (!/\bverdict:\s*PASS\b/i.test(text)) return false;
|
|
84
|
+
return /^\|\s*[^|\n]+\s*\|\s*runtime\s*\|\s*PASS\s*\|[^|\n]*\bgsd_uat_exec\b/mi.test(text);
|
|
93
85
|
}
|
|
94
86
|
|
|
95
87
|
async function browserEvidenceGateRequiresAttention(
|
|
@@ -114,7 +106,38 @@ async function browserEvidenceGateRequiresAttention(
|
|
|
114
106
|
]);
|
|
115
107
|
if (!hasBrowserRequiredText(requirementText)) return false;
|
|
116
108
|
|
|
117
|
-
|
|
109
|
+
// Collect per-slice evidence so the runtime bypass is checked independently
|
|
110
|
+
// for each slice. Concatenating all slices before checking would allow runtime
|
|
111
|
+
// evidence from one slice to cover another slice's browser requirements.
|
|
112
|
+
const sliceEvidencePairs: Array<{ sliceRequirementText: string; evidenceText: string }> = [];
|
|
113
|
+
for (const slice of slices) {
|
|
114
|
+
const chunks: string[] = [];
|
|
115
|
+
const artifactPath = `milestones/${params.milestoneId}/slices/${slice.id}/${slice.id}-ASSESSMENT.md`;
|
|
116
|
+
const artifact = getArtifact(artifactPath);
|
|
117
|
+
if (artifact?.full_content) chunks.push(artifact.full_content);
|
|
118
|
+
const assessmentPath = resolveSliceFile(basePath, params.milestoneId, slice.id, "ASSESSMENT");
|
|
119
|
+
const assessmentContent = assessmentPath ? await loadFile(assessmentPath) : null;
|
|
120
|
+
if (assessmentContent) chunks.push(assessmentContent);
|
|
121
|
+
sliceEvidencePairs.push({
|
|
122
|
+
sliceRequirementText: compactTextParts([slice.demo, slice.goal, slice.success_criteria]),
|
|
123
|
+
evidenceText: chunks.join("\n\n"),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const persistedEvidence = sliceEvidencePairs.map((s) => s.evidenceText).join("\n\n");
|
|
127
|
+
|
|
128
|
+
// Runtime bypass: each slice whose own requirement text has browser-observable
|
|
129
|
+
// criteria must have its own runtime-executable UAT evidence. When no individual
|
|
130
|
+
// slice has slice-level browser requirements (e.g., they come from milestone-level
|
|
131
|
+
// fields only), fall back to checking whether any slice has runtime evidence.
|
|
132
|
+
const browserRequiringSlices = sliceEvidencePairs.filter((s) =>
|
|
133
|
+
hasBrowserRequiredText(s.sliceRequirementText),
|
|
134
|
+
);
|
|
135
|
+
const runtimeBypasses =
|
|
136
|
+
browserRequiringSlices.length > 0
|
|
137
|
+
? browserRequiringSlices.every((s) => hasRuntimeExecutableUatEvidenceText(s.evidenceText))
|
|
138
|
+
: sliceEvidencePairs.some((s) => hasRuntimeExecutableUatEvidenceText(s.evidenceText));
|
|
139
|
+
if (runtimeBypasses) return false;
|
|
140
|
+
|
|
118
141
|
const validationEvidence = compactTextParts([
|
|
119
142
|
params.successCriteriaChecklist,
|
|
120
143
|
params.verificationClasses,
|
|
@@ -184,14 +207,22 @@ export async function handleValidateMilestone(
|
|
|
184
207
|
const requiredClasses = getRequiredVerificationClasses(params.milestoneId);
|
|
185
208
|
if (requiredClasses.length > 0) {
|
|
186
209
|
const verificationClasses = params.verificationClasses ?? "";
|
|
187
|
-
const
|
|
210
|
+
const missingClasses = requiredClasses.filter(
|
|
188
211
|
(className) => !new RegExp(`\\b${className}\\b`, "i").test(verificationClasses),
|
|
189
212
|
);
|
|
190
|
-
if (
|
|
213
|
+
if (missingClasses.length === 1) {
|
|
214
|
+
const missingClass = missingClasses[0];
|
|
191
215
|
return {
|
|
192
216
|
error: `verificationClasses must include canonical row "${missingClass}" because this milestone planned ${missingClass.toLowerCase()} verification`,
|
|
193
217
|
};
|
|
194
218
|
}
|
|
219
|
+
if (missingClasses.length > 1) {
|
|
220
|
+
const quotedClasses = missingClasses.map((className) => `"${className}"`).join(", ");
|
|
221
|
+
const plannedClasses = missingClasses.map((className) => className.toLowerCase()).join(", ");
|
|
222
|
+
return {
|
|
223
|
+
error: `verificationClasses must include canonical rows ${quotedClasses} because this milestone planned ${plannedClasses} verification`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
195
226
|
}
|
|
196
227
|
|
|
197
228
|
const artifactBasePath = resolveCanonicalMilestoneRoot(basePath, params.milestoneId);
|
|
@@ -1150,6 +1150,9 @@ function validateUatChecks(basePath: string, params: UatResultSaveParams): strin
|
|
|
1150
1150
|
function validateUatMode(params: UatResultSaveParams): string | null {
|
|
1151
1151
|
const modes = new Set(params.checks.map((check) => check.mode));
|
|
1152
1152
|
const hasHuman = params.checks.some((check) => check.result === "NEEDS-HUMAN");
|
|
1153
|
+
if (params.uatType === "artifact-driven" && hasHuman && params.verdict === "PASS") {
|
|
1154
|
+
return "artifact-driven UAT cannot PASS with human-only checks";
|
|
1155
|
+
}
|
|
1153
1156
|
if (
|
|
1154
1157
|
hasHuman &&
|
|
1155
1158
|
params.verdict === "PASS" &&
|
|
@@ -1167,12 +1170,13 @@ function validateUatMode(params: UatResultSaveParams): string | null {
|
|
|
1167
1170
|
if (params.uatType === "live-runtime" && !modes.has("runtime") && !modes.has("browser")) {
|
|
1168
1171
|
return "live-runtime UAT requires runtime or browser evidence";
|
|
1169
1172
|
}
|
|
1170
|
-
if (params.uatType === "artifact-driven" && hasHuman && params.verdict === "PASS") {
|
|
1171
|
-
return "artifact-driven UAT cannot PASS with human-only checks";
|
|
1172
|
-
}
|
|
1173
1173
|
return null;
|
|
1174
1174
|
}
|
|
1175
1175
|
|
|
1176
|
+
function quoteToolNames(toolNames: readonly string[]): string {
|
|
1177
|
+
return toolNames.map((toolName) => `"${toolName}"`).join(", ");
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1176
1180
|
function validateCanonicalPresentation(params: UatResultSaveParams): string | null {
|
|
1177
1181
|
const aliasHints: Record<string, string> = {
|
|
1178
1182
|
gsd_save_summary: "gsd_summary_save",
|
|
@@ -1180,10 +1184,11 @@ function validateCanonicalPresentation(params: UatResultSaveParams): string | nu
|
|
|
1180
1184
|
gsd_complete_slice: "gsd_slice_complete",
|
|
1181
1185
|
gsd_milestone_complete: "gsd_complete_milestone",
|
|
1182
1186
|
};
|
|
1187
|
+
const errors: string[] = [];
|
|
1183
1188
|
for (const toolName of params.presentation.presentedTools) {
|
|
1184
1189
|
const baseName = parseMcpToolName(toolName)?.tool ?? toolName;
|
|
1185
1190
|
const canonical = aliasHints[baseName];
|
|
1186
|
-
if (canonical)
|
|
1191
|
+
if (canonical) errors.push(`presentation tool "${toolName}" uses an alias; use canonical "${canonical}"`);
|
|
1187
1192
|
}
|
|
1188
1193
|
|
|
1189
1194
|
const presentedCanonical = new Set(
|
|
@@ -1191,10 +1196,13 @@ function validateCanonicalPresentation(params: UatResultSaveParams): string | nu
|
|
|
1191
1196
|
canonicalWorkflowToolName(parseMcpToolName(toolName)?.tool ?? toolName)
|
|
1192
1197
|
),
|
|
1193
1198
|
);
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1199
|
+
const missingRequiredTools = RUN_UAT_WORKFLOW_TOOL_NAMES.filter(
|
|
1200
|
+
(requiredTool) => !presentedCanonical.has(requiredTool),
|
|
1201
|
+
);
|
|
1202
|
+
if (missingRequiredTools.length === 1) {
|
|
1203
|
+
errors.push(`presentation is missing required UAT tool "${missingRequiredTools[0]}"`);
|
|
1204
|
+
} else if (missingRequiredTools.length > 1) {
|
|
1205
|
+
errors.push(`presentation is missing required UAT tools ${quoteToolNames(missingRequiredTools)}`);
|
|
1198
1206
|
}
|
|
1199
1207
|
|
|
1200
1208
|
const forbiddenCanonical = new Set(
|
|
@@ -1202,24 +1210,33 @@ function validateCanonicalPresentation(params: UatResultSaveParams): string | nu
|
|
|
1202
1210
|
.filter((toolName) => !toolName.includes("*"))
|
|
1203
1211
|
.map((toolName) => canonicalWorkflowToolName(parseMcpToolName(toolName)?.tool ?? toolName)),
|
|
1204
1212
|
);
|
|
1213
|
+
const forbiddenPresentedTools: string[] = [];
|
|
1205
1214
|
for (const toolName of params.presentation.presentedTools) {
|
|
1206
1215
|
const canonical = canonicalWorkflowToolName(parseMcpToolName(toolName)?.tool ?? toolName);
|
|
1207
1216
|
if (toolName === "mcp__gsd-workflow__*" || forbiddenCanonical.has(canonical)) {
|
|
1208
|
-
|
|
1217
|
+
forbiddenPresentedTools.push(toolName);
|
|
1209
1218
|
}
|
|
1210
1219
|
}
|
|
1220
|
+
if (forbiddenPresentedTools.length === 1) {
|
|
1221
|
+
errors.push(`presentation includes forbidden run-uat tool "${forbiddenPresentedTools[0]}"`);
|
|
1222
|
+
} else if (forbiddenPresentedTools.length > 1) {
|
|
1223
|
+
errors.push(`presentation includes forbidden run-uat tools ${quoteToolNames(forbiddenPresentedTools)}`);
|
|
1224
|
+
}
|
|
1211
1225
|
|
|
1212
1226
|
const blockedCanonical = new Set(
|
|
1213
1227
|
params.presentation.blockedTools.map((entry) =>
|
|
1214
1228
|
canonicalWorkflowToolName(parseMcpToolName(entry.name)?.tool ?? entry.name)
|
|
1215
1229
|
),
|
|
1216
1230
|
);
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1231
|
+
const missingBlockedTools = ["gsd_exec", "gsd_summary_save", "gsd_save_gate_result"].filter(
|
|
1232
|
+
(blockedTool) => !blockedCanonical.has(blockedTool),
|
|
1233
|
+
);
|
|
1234
|
+
if (missingBlockedTools.length === 1) {
|
|
1235
|
+
errors.push(`presentation must record "${missingBlockedTools[0]}" as blocked during run-uat`);
|
|
1236
|
+
} else if (missingBlockedTools.length > 1) {
|
|
1237
|
+
errors.push(`presentation must record ${quoteToolNames(missingBlockedTools)} as blocked during run-uat`);
|
|
1221
1238
|
}
|
|
1222
|
-
return null;
|
|
1239
|
+
return errors.length > 0 ? errors.join("; ") : null;
|
|
1223
1240
|
}
|
|
1224
1241
|
|
|
1225
1242
|
function nextUatAttempt(basePath: string, milestoneId: string, sliceId: string): number {
|
|
@@ -270,6 +270,29 @@ export interface GSDActiveUnit {
|
|
|
270
270
|
|
|
271
271
|
// ─── Post-Unit Hook Types ─────────────────────────────────────────────────
|
|
272
272
|
|
|
273
|
+
export type PostUnitHookCriticality = "advisory" | "blocking";
|
|
274
|
+
|
|
275
|
+
export type PostUnitHookOutcomeVerdict =
|
|
276
|
+
| "pass"
|
|
277
|
+
| "advisory"
|
|
278
|
+
| "needs-rework"
|
|
279
|
+
| "needs-remediation"
|
|
280
|
+
| "needs-attention";
|
|
281
|
+
|
|
282
|
+
export type PostUnitHookOnBlockAction =
|
|
283
|
+
| "retry-unit"
|
|
284
|
+
| "retry-task"
|
|
285
|
+
| "queue-task"
|
|
286
|
+
| "queue-slice"
|
|
287
|
+
| "pause";
|
|
288
|
+
|
|
289
|
+
export interface PostUnitHookOnBlockConfig {
|
|
290
|
+
/** Routing action for blocking hook findings. */
|
|
291
|
+
action: PostUnitHookOnBlockAction;
|
|
292
|
+
/** Optional artifact used by compatibility retry routing. */
|
|
293
|
+
artifact?: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
273
296
|
export interface PostUnitHookConfig {
|
|
274
297
|
/** Unique hook identifier — used in idempotency keys and logging. */
|
|
275
298
|
name: string;
|
|
@@ -283,8 +306,12 @@ export interface PostUnitHookConfig {
|
|
|
283
306
|
model?: string;
|
|
284
307
|
/** Expected output file name (relative to task/slice dir). Used for idempotency — skip if exists. */
|
|
285
308
|
artifact?: string;
|
|
309
|
+
/** Whether the hook is advisory or blocks unit advancement. Default advisory. */
|
|
310
|
+
criticality?: PostUnitHookCriticality;
|
|
286
311
|
/** If this file is produced instead of artifact, re-run the trigger unit then re-run hooks. */
|
|
287
312
|
retry_on?: string;
|
|
313
|
+
/** Optional routing for blocking findings. */
|
|
314
|
+
on_block?: PostUnitHookOnBlockConfig;
|
|
288
315
|
/** Agent definition file to use. */
|
|
289
316
|
agent?: string;
|
|
290
317
|
/** Set false to disable without removing config. Default true. */
|
|
@@ -317,6 +344,31 @@ export interface HookDispatchResult {
|
|
|
317
344
|
unitId: string;
|
|
318
345
|
}
|
|
319
346
|
|
|
347
|
+
export interface PostUnitGateBlock {
|
|
348
|
+
/** Blocking hook name. */
|
|
349
|
+
hookName: string;
|
|
350
|
+
/** The unit type that triggered the gate. */
|
|
351
|
+
triggerUnitType: string;
|
|
352
|
+
/** The unit ID that triggered the gate. */
|
|
353
|
+
triggerUnitId: string;
|
|
354
|
+
/** Gate artifact name, when configured. */
|
|
355
|
+
artifact?: string;
|
|
356
|
+
/** Absolute path to the gate artifact, when known. */
|
|
357
|
+
artifactPath?: string;
|
|
358
|
+
/** Parsed blocking verdict, when present. */
|
|
359
|
+
verdict?: PostUnitHookOutcomeVerdict | "failed";
|
|
360
|
+
/** Configured routing action that caused the pause. */
|
|
361
|
+
action: PostUnitHookOnBlockAction;
|
|
362
|
+
/** Human-readable pause reason. */
|
|
363
|
+
reason: string;
|
|
364
|
+
/** Current hook cycle count. */
|
|
365
|
+
cycle: number;
|
|
366
|
+
/** Configured max cycle count. */
|
|
367
|
+
maxCycles: number;
|
|
368
|
+
/** Optional compatibility retry artifact. */
|
|
369
|
+
retryArtifact?: string;
|
|
370
|
+
}
|
|
371
|
+
|
|
320
372
|
// ─── Budget & Notification Types ──────────────────────────────────────────
|
|
321
373
|
|
|
322
374
|
export type BudgetEnforcementMode = "warn" | "pause" | "halt";
|
|
@@ -452,6 +504,15 @@ export interface PreDispatchResult {
|
|
|
452
504
|
export interface PersistedHookState {
|
|
453
505
|
/** Cycle counts keyed as "hookName/triggerUnitType/triggerUnitId". */
|
|
454
506
|
cycleCounts: Record<string, number>;
|
|
507
|
+
/** In-flight hook, persisted so blocking gates cannot be skipped after resume. */
|
|
508
|
+
activeHook?: HookExecutionState | null;
|
|
509
|
+
/** Remaining hook queue by hook name and trigger unit. */
|
|
510
|
+
hookQueue?: Array<{
|
|
511
|
+
hookName: string;
|
|
512
|
+
triggerUnitType: string;
|
|
513
|
+
triggerUnitId: string;
|
|
514
|
+
forceRun?: boolean;
|
|
515
|
+
}>;
|
|
455
516
|
/** Timestamp of last state save. */
|
|
456
517
|
savedAt: string;
|
|
457
518
|
}
|
|
@@ -465,6 +526,8 @@ export interface HookStatusEntry {
|
|
|
465
526
|
enabled: boolean;
|
|
466
527
|
/** What unit types it targets. */
|
|
467
528
|
targets: string[];
|
|
529
|
+
/** Whether this post-unit hook is advisory or blocking. */
|
|
530
|
+
criticality?: PostUnitHookCriticality;
|
|
468
531
|
/** Current cycle counts for active triggers. */
|
|
469
532
|
activeCycles: Record<string, number>;
|
|
470
533
|
}
|
|
@@ -7,9 +7,60 @@
|
|
|
7
7
|
|
|
8
8
|
import { extractUatType } from "./files.js";
|
|
9
9
|
import type { UatType } from "./files.js";
|
|
10
|
+
import { splitFrontmatter, parseFrontmatterMap } from "../shared/frontmatter.js";
|
|
11
|
+
import { parse as parseYaml } from "yaml";
|
|
12
|
+
|
|
13
|
+
function normalizeVerdict(value: unknown): string | undefined {
|
|
14
|
+
if (typeof value !== "string") return undefined;
|
|
15
|
+
let verdict = value.trim().toLowerCase();
|
|
16
|
+
if (!verdict) return undefined;
|
|
17
|
+
if (verdict === "passed") verdict = "pass";
|
|
18
|
+
return verdict;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getCaseInsensitive(obj: Record<string, unknown>, key: string): unknown {
|
|
22
|
+
const lowerKey = key.toLowerCase();
|
|
23
|
+
for (const [candidate, value] of Object.entries(obj)) {
|
|
24
|
+
if (candidate.toLowerCase() === lowerKey) return value;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
10
28
|
|
|
11
29
|
// ── Verdict extraction ──────────────────────────────────────────────────
|
|
12
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Extract and normalize the frontmatter `verdict` value.
|
|
33
|
+
*
|
|
34
|
+
* Supports both top-level `verdict` and the hook outcome shape
|
|
35
|
+
* `outcome.verdict`. Returns `undefined` when frontmatter is absent or has no
|
|
36
|
+
* verdict field.
|
|
37
|
+
*/
|
|
38
|
+
export function extractFrontmatterVerdict(content: string): string | undefined {
|
|
39
|
+
const [frontmatterLines] = splitFrontmatter(content);
|
|
40
|
+
if (!frontmatterLines) return undefined;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const parsed = parseYaml(frontmatterLines.join("\n")) as unknown;
|
|
44
|
+
if (parsed && typeof parsed === "object") {
|
|
45
|
+
const root = parsed as Record<string, unknown>;
|
|
46
|
+
const topLevel = normalizeVerdict(getCaseInsensitive(root, "verdict"));
|
|
47
|
+
if (topLevel) return topLevel;
|
|
48
|
+
const outcome = getCaseInsensitive(root, "outcome");
|
|
49
|
+
if (outcome && typeof outcome === "object") {
|
|
50
|
+
const nested = normalizeVerdict(getCaseInsensitive(outcome as Record<string, unknown>, "verdict"));
|
|
51
|
+
if (nested) return nested;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Fall through to the permissive parser used by legacy frontmatter paths.
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const frontmatter = parseFrontmatterMap(frontmatterLines);
|
|
59
|
+
const topLevel = normalizeVerdict(getCaseInsensitive(frontmatter, "verdict"));
|
|
60
|
+
if (topLevel) return topLevel;
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
13
64
|
/**
|
|
14
65
|
* Extract and normalize the `verdict` value from YAML frontmatter.
|
|
15
66
|
*
|
|
@@ -21,24 +72,14 @@ import type { UatType } from "./files.js";
|
|
|
21
72
|
*/
|
|
22
73
|
export function extractVerdict(content: string): string | undefined {
|
|
23
74
|
// Primary: YAML frontmatter verdict (canonical format)
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
const verdictMatch = fmMatch[1].match(/verdict:\s*([\w-]+)/i);
|
|
27
|
-
if (verdictMatch) {
|
|
28
|
-
let v = verdictMatch[1].toLowerCase();
|
|
29
|
-
if (v === "passed") v = "pass";
|
|
30
|
-
return v;
|
|
31
|
-
}
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
75
|
+
const [frontmatterLines] = splitFrontmatter(content);
|
|
76
|
+
if (frontmatterLines) return extractFrontmatterVerdict(content);
|
|
34
77
|
|
|
35
78
|
// Fallback: detect verdict in markdown body (LLM manual writes, #2960).
|
|
36
79
|
// Matches patterns like: **Verdict:** PASS, **Verdict:** ✅ PASS, **Verdict** needs-remediation
|
|
37
80
|
const bodyMatch = content.match(/\*\*Verdict:?\*\*\s*(?:✅\s*)?(\w[\w-]*)/i);
|
|
38
81
|
if (bodyMatch) {
|
|
39
|
-
|
|
40
|
-
if (v === "passed") v = "pass";
|
|
41
|
-
return v;
|
|
82
|
+
return normalizeVerdict(bodyMatch[1]);
|
|
42
83
|
}
|
|
43
84
|
|
|
44
85
|
return undefined;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { basename, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
export const GSD_BROWSER_MCP_SERVER_NAME = "gsd-browser";
|
|
9
|
+
|
|
10
|
+
export interface GsdBrowserMcpLaunchConfig {
|
|
11
|
+
serverName: string;
|
|
12
|
+
command: string;
|
|
13
|
+
args: string[];
|
|
14
|
+
cwd: string;
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
projectRoot: string;
|
|
17
|
+
sessionName: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GsdBrowserMcpLaunchOptions {
|
|
21
|
+
sessionName?: string;
|
|
22
|
+
sessionSuffix?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseJsonEnv<T>(env: NodeJS.ProcessEnv, name: string): T | undefined {
|
|
26
|
+
const raw = env[name];
|
|
27
|
+
if (!raw) return undefined;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(raw) as T;
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error(`Invalid JSON in ${name}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sanitizeSessionSegment(value: string): string {
|
|
36
|
+
return value
|
|
37
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
38
|
+
.replace(/^-+|-+$/g, "")
|
|
39
|
+
.slice(0, 40);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function compareSemverLocal(a: string, b: string): number {
|
|
43
|
+
const left = a.split(".").map(Number);
|
|
44
|
+
const right = b.split(".").map(Number);
|
|
45
|
+
for (let index = 0; index < Math.max(left.length, right.length); index++) {
|
|
46
|
+
const leftValue = left[index] || 0;
|
|
47
|
+
const rightValue = right[index] || 0;
|
|
48
|
+
if (leftValue > rightValue) return 1;
|
|
49
|
+
if (leftValue < rightValue) return -1;
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseGsdBrowserVersion(output: string): string | null {
|
|
55
|
+
return output.match(/\b(\d+\.\d+\.\d+)\b/)?.[1] ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveBundledGsdBrowserPackageVersion(): string | null {
|
|
59
|
+
try {
|
|
60
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
61
|
+
const packageJsonPath = requireFromHere.resolve("@opengsd/gsd-browser/package.json");
|
|
62
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { version?: unknown };
|
|
63
|
+
return typeof pkg.version === "string" ? parseGsdBrowserVersion(pkg.version) : null;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolvePathGsdBrowserVersion(env: NodeJS.ProcessEnv): string | null {
|
|
70
|
+
const explicit = env.GSD_BROWSER_PATH_VERSION?.trim();
|
|
71
|
+
if (explicit) return parseGsdBrowserVersion(explicit);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
return parseGsdBrowserVersion(execFileSync("gsd-browser", ["--version"], {
|
|
75
|
+
encoding: "utf-8",
|
|
76
|
+
env,
|
|
77
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
78
|
+
timeout: 2000,
|
|
79
|
+
}));
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function shouldPreferPathGsdBrowser(env: NodeJS.ProcessEnv): boolean {
|
|
86
|
+
const pathVersion = resolvePathGsdBrowserVersion(env);
|
|
87
|
+
if (!pathVersion) return false;
|
|
88
|
+
|
|
89
|
+
const bundledVersion = resolveBundledGsdBrowserPackageVersion();
|
|
90
|
+
return !bundledVersion || compareSemverLocal(pathVersion, bundledVersion) > 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveBundledGsdBrowserCliPath(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
94
|
+
const explicit = env.GSD_BROWSER_CLI_PATH?.trim() || env.GSD_BROWSER_BIN_PATH?.trim();
|
|
95
|
+
if (explicit) return explicit;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
99
|
+
const packageJsonPath = requireFromHere.resolve("@opengsd/gsd-browser/package.json");
|
|
100
|
+
const candidate = resolve(packageJsonPath, "..", "bin", "gsd-browser");
|
|
101
|
+
if (existsSync(candidate)) return candidate;
|
|
102
|
+
} catch {
|
|
103
|
+
// Fall through to path candidates for source/dist layouts.
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const candidates = [
|
|
107
|
+
resolve(fileURLToPath(new URL("../../../../node_modules/@opengsd/gsd-browser/bin/gsd-browser", import.meta.url))),
|
|
108
|
+
resolve(fileURLToPath(new URL("../../../../node_modules/.bin/gsd-browser", import.meta.url))),
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
for (const candidate of candidates) {
|
|
112
|
+
if (existsSync(candidate)) return candidate;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function buildGsdBrowserSessionName(projectRoot: string, suffix?: string): string {
|
|
119
|
+
const resolvedProjectRoot = resolve(projectRoot);
|
|
120
|
+
const base = sanitizeSessionSegment(basename(resolvedProjectRoot)) || "project";
|
|
121
|
+
const hash = createHash("sha1").update(resolvedProjectRoot).digest("hex").slice(0, 8);
|
|
122
|
+
const cleanSuffix = suffix ? sanitizeSessionSegment(suffix) : "";
|
|
123
|
+
return cleanSuffix ? `gsd-${base}-${hash}-${cleanSuffix}` : `gsd-${base}-${hash}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function resolveGsdBrowserMcpLaunchConfig(
|
|
127
|
+
projectRoot: string,
|
|
128
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
129
|
+
options: GsdBrowserMcpLaunchOptions = {},
|
|
130
|
+
): GsdBrowserMcpLaunchConfig {
|
|
131
|
+
const resolvedProjectRoot = resolve(projectRoot);
|
|
132
|
+
const serverName = env.GSD_BROWSER_MCP_NAME?.trim() || GSD_BROWSER_MCP_SERVER_NAME;
|
|
133
|
+
const explicitArgs = parseJsonEnv<unknown>(env, "GSD_BROWSER_MCP_ARGS");
|
|
134
|
+
const explicitEnv = parseJsonEnv<Record<string, string>>(env, "GSD_BROWSER_MCP_ENV");
|
|
135
|
+
const explicitCommand = env.GSD_BROWSER_MCP_COMMAND?.trim();
|
|
136
|
+
const explicitCliPath = env.GSD_BROWSER_CLI_PATH?.trim() || env.GSD_BROWSER_BIN_PATH?.trim();
|
|
137
|
+
const preferPathCli = !explicitCommand && !explicitCliPath && shouldPreferPathGsdBrowser(env);
|
|
138
|
+
const bundledCliPath = !explicitCommand && !explicitCliPath && !preferPathCli
|
|
139
|
+
? resolveBundledGsdBrowserCliPath(env)
|
|
140
|
+
: null;
|
|
141
|
+
const sessionName =
|
|
142
|
+
options.sessionName?.trim() || buildGsdBrowserSessionName(resolvedProjectRoot, options.sessionSuffix);
|
|
143
|
+
const command =
|
|
144
|
+
explicitCommand
|
|
145
|
+
|| explicitCliPath
|
|
146
|
+
|| (preferPathCli ? "gsd-browser" : undefined)
|
|
147
|
+
|| (bundledCliPath ? process.execPath : undefined)
|
|
148
|
+
|| "gsd-browser";
|
|
149
|
+
const args = Array.isArray(explicitArgs) && explicitArgs.length > 0
|
|
150
|
+
? explicitArgs.map(String)
|
|
151
|
+
: [
|
|
152
|
+
...(bundledCliPath ? [bundledCliPath] : []),
|
|
153
|
+
"mcp",
|
|
154
|
+
"--session",
|
|
155
|
+
sessionName,
|
|
156
|
+
"--identity-scope",
|
|
157
|
+
"project",
|
|
158
|
+
"--identity-project",
|
|
159
|
+
resolvedProjectRoot,
|
|
160
|
+
];
|
|
161
|
+
const cwd = env.GSD_BROWSER_MCP_CWD?.trim() || resolvedProjectRoot;
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
serverName,
|
|
165
|
+
command,
|
|
166
|
+
args,
|
|
167
|
+
cwd,
|
|
168
|
+
...(explicitEnv ? { env: explicitEnv } : {}),
|
|
169
|
+
projectRoot: resolvedProjectRoot,
|
|
170
|
+
sessionName,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
File without changes
|
|
File without changes
|