@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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node --test tests/*.test.mjs"
|
|
7
|
+
"test": "node --import ../gsd/tests/resolve-ts.mjs --experimental-strip-types --test tests/*.test.mjs"
|
|
8
8
|
},
|
|
9
9
|
"pi": {
|
|
10
10
|
"extensions": [
|
|
@@ -12,10 +12,14 @@
|
|
|
12
12
|
]
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
+
"@opengsd/gsd-browser": ">=0.1.27",
|
|
15
16
|
"playwright": ">=1.40.0",
|
|
16
17
|
"sharp": ">=0.33.0"
|
|
17
18
|
},
|
|
18
19
|
"peerDependenciesMeta": {
|
|
20
|
+
"@opengsd/gsd-browser": {
|
|
21
|
+
"optional": true
|
|
22
|
+
},
|
|
19
23
|
"playwright": {
|
|
20
24
|
"optional": true
|
|
21
25
|
},
|
|
@@ -32,7 +32,7 @@ import { isDbAvailable, getDbPath, refreshOpenDatabaseFromDisk, getTask, getSlic
|
|
|
32
32
|
import { renderPlanCheckboxes, renderRoadmapFromDb } from "./markdown-renderer.js";
|
|
33
33
|
import { parseRoadmap as parseLegacyRoadmap } from "./parsers-legacy.js";
|
|
34
34
|
import { consumeSignal } from "./session-status-io.js";
|
|
35
|
-
import { checkPostUnitHooks, isRetryPending, consumeRetryTrigger, persistHookState, resolveHookArtifactPath, } from "./post-unit-hooks.js";
|
|
35
|
+
import { checkPostUnitHooks, consumeHookFailure, isRetryPending, consumeRetryTrigger, consumeGateBlock, persistHookState, resolveHookArtifactPath, } from "./post-unit-hooks.js";
|
|
36
36
|
import { hasPendingCaptures, loadPendingCaptures, revertExecutorResolvedCaptures } from "./captures.js";
|
|
37
37
|
import { debugLog } from "./debug-logger.js";
|
|
38
38
|
import { runSafely } from "./auto-utils.js";
|
|
@@ -1860,18 +1860,25 @@ export async function postUnitPostVerification(pctx) {
|
|
|
1860
1860
|
// ── Post-unit hooks ──
|
|
1861
1861
|
if (s.currentUnit && !s.stepMode) {
|
|
1862
1862
|
const hookUnit = checkPostUnitHooks(s.currentUnit.type, s.currentUnit.id, s.basePath);
|
|
1863
|
+
persistHookState(s.basePath);
|
|
1863
1864
|
if (hookUnit) {
|
|
1864
1865
|
if (s.currentUnit) {
|
|
1865
1866
|
await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
1866
1867
|
}
|
|
1867
|
-
persistHookState(s.basePath);
|
|
1868
1868
|
return enqueueSidecar(s, ctx, { kind: "hook", unitType: hookUnit.unitType, unitId: hookUnit.unitId, prompt: hookUnit.prompt, model: hookUnit.model }, { hookName: hookUnit.hookName });
|
|
1869
1869
|
}
|
|
1870
|
+
const hookFailure = consumeHookFailure();
|
|
1871
|
+
if (hookFailure) {
|
|
1872
|
+
ctx.ui.notify(`Post-unit hook ${hookFailure.hookName} failed for ${hookFailure.unitId}: ${hookFailure.reason}. Pausing auto-mode.`, "warning");
|
|
1873
|
+
await pauseAuto(ctx, pi);
|
|
1874
|
+
return "stopped";
|
|
1875
|
+
}
|
|
1870
1876
|
// Check if a hook requested a retry of the trigger unit
|
|
1871
1877
|
if (isRetryPending()) {
|
|
1872
1878
|
const trigger = consumeRetryTrigger();
|
|
1873
1879
|
if (trigger) {
|
|
1874
|
-
|
|
1880
|
+
persistHookState(s.basePath);
|
|
1881
|
+
ctx.ui.notify(`Hook requested retry of ${trigger.unitType} ${trigger.unitId} — resetting trigger unit state.`, "info");
|
|
1875
1882
|
await s.orchestration?.retryActiveUnit({
|
|
1876
1883
|
unitType: trigger.unitType,
|
|
1877
1884
|
unitId: trigger.unitId,
|
|
@@ -1918,6 +1925,17 @@ export async function postUnitPostVerification(pctx) {
|
|
|
1918
1925
|
// Fall through to normal dispatch — deriveState will re-derive the unit
|
|
1919
1926
|
}
|
|
1920
1927
|
}
|
|
1928
|
+
const gateBlock = consumeGateBlock();
|
|
1929
|
+
if (gateBlock) {
|
|
1930
|
+
persistHookState(s.basePath);
|
|
1931
|
+
const verdict = gateBlock.verdict ? ` verdict=${gateBlock.verdict};` : "";
|
|
1932
|
+
const artifact = gateBlock.artifact ? ` artifact=${gateBlock.artifact};` : "";
|
|
1933
|
+
const message = `Post-unit gate "${gateBlock.hookName}" blocked ${gateBlock.triggerUnitType} ${gateBlock.triggerUnitId}:` +
|
|
1934
|
+
`${verdict}${artifact} ${gateBlock.reason}. Run /gsd status to inspect, then /gsd auto after recovery.`;
|
|
1935
|
+
ctx.ui.notify(message, "warning");
|
|
1936
|
+
await pauseAuto(ctx, pi);
|
|
1937
|
+
return "stopped";
|
|
1938
|
+
}
|
|
1921
1939
|
}
|
|
1922
1940
|
// ── Fast-path stop detection (#3487) ──
|
|
1923
1941
|
// Before waiting for triage, check if any PENDING captures contain explicit
|
|
@@ -1271,7 +1271,7 @@ export async function checkNeedsRunUat(base, mid, state, prefs) {
|
|
|
1271
1271
|
if (hasVerdict(uatContent))
|
|
1272
1272
|
continue;
|
|
1273
1273
|
// Also check the ASSESSMENT file — the run-uat prompt writes the verdict
|
|
1274
|
-
// there (via
|
|
1274
|
+
// there (via gsd_uat_result_save), not into the
|
|
1275
1275
|
// UAT spec file. Without this check the unit re-dispatches indefinitely.
|
|
1276
1276
|
const assessmentFile = resolveSliceFile(base, mid, sid, "ASSESSMENT");
|
|
1277
1277
|
if (assessmentFile) {
|
|
@@ -2568,17 +2568,26 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) {
|
|
|
2568
2568
|
if (isDbAvailable()) {
|
|
2569
2569
|
const milestone = getMilestone(mid);
|
|
2570
2570
|
if (milestone) {
|
|
2571
|
+
const escapeCell = (value) => value.replace(/[\\|]/g, (char) => `\\${char}`).replace(/\r?\n/g, " ");
|
|
2571
2572
|
const classes = [];
|
|
2572
2573
|
if (milestone.verification_contract)
|
|
2573
|
-
classes.push(
|
|
2574
|
+
classes.push(`| Contract | ${escapeCell(milestone.verification_contract)} |`);
|
|
2574
2575
|
if (milestone.verification_integration)
|
|
2575
|
-
classes.push(
|
|
2576
|
+
classes.push(`| Integration | ${escapeCell(milestone.verification_integration)} |`);
|
|
2576
2577
|
if (milestone.verification_operational)
|
|
2577
|
-
classes.push(
|
|
2578
|
+
classes.push(`| Operational | ${escapeCell(milestone.verification_operational)} |`);
|
|
2578
2579
|
if (milestone.verification_uat)
|
|
2579
|
-
classes.push(
|
|
2580
|
+
classes.push(`| UAT | ${escapeCell(milestone.verification_uat)} |`);
|
|
2580
2581
|
if (classes.length > 0) {
|
|
2581
|
-
const verificationClasses =
|
|
2582
|
+
const verificationClasses = [
|
|
2583
|
+
"### Verification Classes (from planning)",
|
|
2584
|
+
"",
|
|
2585
|
+
"These verification tiers were defined during milestone planning. Every row in this table must appear in `verificationClasses` with the same canonical class name.",
|
|
2586
|
+
"",
|
|
2587
|
+
"| Class | Planned Check |",
|
|
2588
|
+
"| --- | --- |",
|
|
2589
|
+
...classes,
|
|
2590
|
+
].join("\n");
|
|
2582
2591
|
inlined.push(verificationClasses);
|
|
2583
2592
|
trackPromptContext(contextTelemetry, "verification-classes", "inline", verificationClasses);
|
|
2584
2593
|
}
|
|
@@ -1010,7 +1010,7 @@ export function registerDbTools(pi) {
|
|
|
1010
1010
|
promptGuidelines: [
|
|
1011
1011
|
"Use gsd_validate_milestone when all slices are done and the milestone needs validation before completion.",
|
|
1012
1012
|
"Parameters: milestoneId, verdict, remediationRound, successCriteriaChecklist, sliceDeliveryAudit, crossSliceIntegration, requirementCoverage, verificationClasses (optional), verdictRationale, remediationPlan (optional).",
|
|
1013
|
-
"If verification classes were planned, verificationClasses must
|
|
1013
|
+
"If verification classes were planned, verificationClasses must be a complete canonical table with one row for every applicable planned class using the exact class names Contract, Integration, Operational, and UAT. Do not submit a partial table.",
|
|
1014
1014
|
"Planned verification text marked as none/not required/not applicable/N/A (including suffixed variants such as 'not required - backend-only') is treated as not applicable and does not require a class row.",
|
|
1015
1015
|
"If verdict is 'needs-remediation', also provide remediationPlan and use gsd_reassess_roadmap to add remediation slices to the roadmap.",
|
|
1016
1016
|
"On success, returns validationPath where VALIDATION.md was written.",
|
|
@@ -1023,7 +1023,7 @@ export function registerDbTools(pi) {
|
|
|
1023
1023
|
sliceDeliveryAudit: Type.String({ description: "Markdown table auditing each slice's claimed vs delivered output" }),
|
|
1024
1024
|
crossSliceIntegration: Type.String({ description: "Markdown describing any cross-slice boundary mismatches" }),
|
|
1025
1025
|
requirementCoverage: Type.String({ description: "Markdown describing any unaddressed requirements" }),
|
|
1026
|
-
verificationClasses: Type.Optional(Type.String({ description: "
|
|
1026
|
+
verificationClasses: Type.Optional(Type.String({ description: "Complete markdown table describing verification class compliance and gaps; include one canonical row for every applicable planned class (Contract, Integration, Operational, UAT)" })),
|
|
1027
1027
|
verdictRationale: Type.String({ description: "Why this verdict was chosen" }),
|
|
1028
1028
|
remediationPlan: Type.Optional(Type.String({ description: "Remediation plan (required if verdict is needs-remediation)" })),
|
|
1029
1029
|
}),
|
|
@@ -1,17 +1,44 @@
|
|
|
1
1
|
// Project/App: gsd-pi
|
|
2
2
|
// File Purpose: Shared browser-observable UAT requirement and evidence detection.
|
|
3
|
-
export const BROWSER_REQUIREMENT_RE = /\b(?:
|
|
3
|
+
export const BROWSER_REQUIREMENT_RE = /\b(?:file:\/\/|localhost|playwright|chrome|screenshot|snapshot|browser_(?:assert|batch|find|verify|snapshot_refs))\b|\b(?:open|launch|navigate|load|visit|serve|start)\b.{0,80}\b(?:browser|page|localhost|file:\/\/)\b|\bbrowser\s+(?:check|session|test|uat|tool|automation|interaction|flow)\b/i;
|
|
4
4
|
export const NO_BROWSER_EVIDENCE_RE = /\b(?:no|without|not|wasn'?t|isn'?t)\s+(?:automated\s+)?(?:live\s+)?browser(?:\s+(?:session|test|uat))?|\bno\s+automated\s+browser\b|\bnot\s+conducted\b/i;
|
|
5
5
|
export const BROWSER_RUNTIME_RE = /\b(?:browser|playwright|chrome|camoufox|browser_(?:assert|batch|find|verify|snapshot_refs)|screenshot|snapshot|file:\/\/|localhost)\b/i;
|
|
6
6
|
export const BROWSER_ACTION_RE = /\b(?:open(?:ed)?|navigate(?:d)?|click(?:ed)?|type(?:d)?|reload(?:ed)?|capture(?:d)?|screenshot|snapshot)\b/i;
|
|
7
7
|
export const BROWSER_ASSERTION_RE = /\b(?:assert(?:ed|ion)?|observed|confirmed|verified|expected|visible|text|count|label|strikethrough|localstorage|screenshot|snapshot|passed)\b/i;
|
|
8
|
+
const NON_REQUIREMENT_BROWSER_HEADING_RE = /^(?:not\s+proven|not\s+covered|out\s+of\s+scope|deferred|follow-?ups?|known\s+limitations|notes\s+for\s+tester)\b/i;
|
|
9
|
+
const NON_REQUIREMENT_BROWSER_LINE_RE = /\b(?:deferred|not\s+proven|not\s+covered|out\s+of\s+scope|future\s+slice|follow-?up|no\s+(?:live\s+)?browser|without\s+(?:a\s+)?browser|not\s+(?:a\s+)?browser)\b/i;
|
|
8
10
|
export function compactTextParts(parts) {
|
|
9
11
|
return parts.flatMap((part) => Array.isArray(part) ? part : [part])
|
|
10
12
|
.filter((part) => typeof part === "string" && part.trim().length > 0)
|
|
11
13
|
.join("\n");
|
|
12
14
|
}
|
|
13
15
|
export function hasBrowserRequiredText(text) {
|
|
14
|
-
|
|
16
|
+
let inNonRequirementSection = false;
|
|
17
|
+
let nonRequirementDepth = 0;
|
|
18
|
+
for (const line of text.split(/\r?\n/)) {
|
|
19
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
20
|
+
if (headingMatch) {
|
|
21
|
+
const depth = headingMatch[1].length;
|
|
22
|
+
const title = headingMatch[2] ?? "";
|
|
23
|
+
// Only update section context when at the same or higher level than the
|
|
24
|
+
// heading that opened the non-requirement zone. A sub-heading deeper than
|
|
25
|
+
// the opening heading must not escape or re-enter the zone on its own.
|
|
26
|
+
if (!inNonRequirementSection || depth <= nonRequirementDepth) {
|
|
27
|
+
inNonRequirementSection = NON_REQUIREMENT_BROWSER_HEADING_RE.test(title);
|
|
28
|
+
nonRequirementDepth = inNonRequirementSection ? depth : 0;
|
|
29
|
+
}
|
|
30
|
+
// Check the heading title itself — section state is already updated, so
|
|
31
|
+
// we correctly skip headings that opened a non-requirement zone.
|
|
32
|
+
if (!inNonRequirementSection && BROWSER_REQUIREMENT_RE.test(title))
|
|
33
|
+
return true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (inNonRequirementSection || NON_REQUIREMENT_BROWSER_LINE_RE.test(line))
|
|
37
|
+
continue;
|
|
38
|
+
if (BROWSER_REQUIREMENT_RE.test(line))
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
15
42
|
}
|
|
16
43
|
export function hasBrowserEvidenceText(text) {
|
|
17
44
|
if (!text.trim())
|
|
@@ -266,8 +266,8 @@ Examples:
|
|
|
266
266
|
await handleInspect(ctx);
|
|
267
267
|
return true;
|
|
268
268
|
}
|
|
269
|
-
if (trimmed === "update" || trimmed === "upgrade") {
|
|
270
|
-
await handleUpdate(ctx);
|
|
269
|
+
if (trimmed === "update" || trimmed.startsWith("update ") || trimmed === "upgrade" || trimmed.startsWith("upgrade ")) {
|
|
270
|
+
await handleUpdate(ctx, trimmed.replace(/^(?:update|upgrade)\s*/, "").trim());
|
|
271
271
|
return true;
|
|
272
272
|
}
|
|
273
273
|
if (trimmed === "fast" || trimmed.startsWith("fast ")) {
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* handleRunHook, handleUpdate, handleSkillHealth
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
8
10
|
import { join, resolve as resolvePath, sep } from "node:path";
|
|
9
11
|
import { homedir } from "node:os";
|
|
10
12
|
import { deriveState } from "./state.js";
|
|
@@ -20,7 +22,10 @@ import { loadPrompt } from "./prompt-loader.js";
|
|
|
20
22
|
import { isPnpmInstall } from "../../shared/package-manager-detection.js";
|
|
21
23
|
import { buildDoctorHealIssuePayload, buildDoctorHealSummary, buildWorkflowDispatchContent, } from "./workflow-protocol.js";
|
|
22
24
|
import { restoreGsdWorkflowTools, scopeGsdWorkflowToolsForDispatch, } from "./bootstrap/register-hooks.js";
|
|
25
|
+
const GSD_PI_PACKAGE = "@opengsd/gsd-pi";
|
|
26
|
+
const GSD_BROWSER_PACKAGE = "@opengsd/gsd-browser";
|
|
23
27
|
const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/@opengsd%2fgsd-pi/latest";
|
|
28
|
+
const BROWSER_UPDATE_REGISTRY_URL = "https://registry.npmjs.org/@opengsd%2fgsd-browser/latest";
|
|
24
29
|
const UPDATE_FETCH_TIMEOUT_MS = 5000;
|
|
25
30
|
// Detects a bun-installed gsd via `process.argv[1]`. Mirrors isBunInstall in
|
|
26
31
|
// src/update-check.ts — duplicated because tsconfig.resources.json rootDir
|
|
@@ -47,11 +52,11 @@ function resolveInstallCommand(pkg) {
|
|
|
47
52
|
return `pnpm add -g ${pkg}`;
|
|
48
53
|
return `npm install -g ${pkg}`;
|
|
49
54
|
}
|
|
50
|
-
async function fetchLatestVersionForCommand() {
|
|
55
|
+
async function fetchLatestVersionForCommand(registryUrl = UPDATE_REGISTRY_URL) {
|
|
51
56
|
const controller = new AbortController();
|
|
52
57
|
const timeout = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS);
|
|
53
58
|
try {
|
|
54
|
-
const res = await fetch(
|
|
59
|
+
const res = await fetch(registryUrl, { signal: controller.signal });
|
|
55
60
|
if (!res.ok)
|
|
56
61
|
return null;
|
|
57
62
|
const data = (await res.json());
|
|
@@ -65,6 +70,19 @@ async function fetchLatestVersionForCommand() {
|
|
|
65
70
|
clearTimeout(timeout);
|
|
66
71
|
}
|
|
67
72
|
}
|
|
73
|
+
function resolveInstalledPackageVersionForCommand(packageName) {
|
|
74
|
+
try {
|
|
75
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
76
|
+
const packageJsonPath = requireFromHere.resolve(`${packageName}/package.json`);
|
|
77
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
78
|
+
return typeof pkg.version === "string" && pkg.version.trim().length > 0
|
|
79
|
+
? pkg.version.trim().replace(/^v/, "")
|
|
80
|
+
: null;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
68
86
|
export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
|
|
69
87
|
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(gsdHome(), "agent", "GSD-WORKFLOW.md");
|
|
70
88
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
@@ -386,27 +404,74 @@ function compareSemverLocal(a, b) {
|
|
|
386
404
|
}
|
|
387
405
|
return 0;
|
|
388
406
|
}
|
|
389
|
-
|
|
407
|
+
function formatCommandVersion(version) {
|
|
408
|
+
return version ? `v${version}` : "unknown";
|
|
409
|
+
}
|
|
410
|
+
function pickHigherVersionForCommand(a, b) {
|
|
411
|
+
if (!a)
|
|
412
|
+
return b;
|
|
413
|
+
if (!b)
|
|
414
|
+
return a;
|
|
415
|
+
return compareSemverLocal(a, b) >= 0 ? a : b;
|
|
416
|
+
}
|
|
417
|
+
// Mirrors resolveGsdBrowserPathVersion in src/update-check.ts — duplicated because
|
|
418
|
+
// tsconfig.resources.json rootDir prevents importing from src/.
|
|
419
|
+
function resolveGsdBrowserPathVersionForCommand(env = process.env) {
|
|
420
|
+
const explicit = env.GSD_BROWSER_PATH_VERSION?.trim();
|
|
421
|
+
if (explicit)
|
|
422
|
+
return explicit.match(/\b(\d+\.\d+\.\d+)\b/)?.[1] ?? null;
|
|
423
|
+
try {
|
|
424
|
+
const out = execFileSync("gsd-browser", ["--version"], {
|
|
425
|
+
encoding: "utf-8",
|
|
426
|
+
env,
|
|
427
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
428
|
+
timeout: 2000,
|
|
429
|
+
});
|
|
430
|
+
return out.match(/\b(\d+\.\d+\.\d+)\b/)?.[1] ?? null;
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
export async function handleUpdate(ctx, args = "") {
|
|
390
437
|
const { execSync } = await import("node:child_process");
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
438
|
+
const target = args.trim();
|
|
439
|
+
const browserUpdate = target === "browser" || target === "gsd-browser";
|
|
440
|
+
if (target && !browserUpdate) {
|
|
441
|
+
ctx.ui.notify("Usage: /gsd update [browser]", "warning");
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const NPM_PACKAGE = browserUpdate ? GSD_BROWSER_PACKAGE : GSD_PI_PACKAGE;
|
|
445
|
+
const registryUrl = browserUpdate ? BROWSER_UPDATE_REGISTRY_URL : UPDATE_REGISTRY_URL;
|
|
446
|
+
const bundledVersion = browserUpdate
|
|
447
|
+
? resolveInstalledPackageVersionForCommand(GSD_BROWSER_PACKAGE)
|
|
448
|
+
: null;
|
|
449
|
+
const current = browserUpdate
|
|
450
|
+
? pickHigherVersionForCommand(bundledVersion, resolveGsdBrowserPathVersionForCommand())
|
|
451
|
+
: process.env.GSD_VERSION || "0.0.0";
|
|
452
|
+
const label = browserUpdate ? "gsd-browser version" : "version";
|
|
453
|
+
ctx.ui.notify(`Current ${label}: ${formatCommandVersion(current)}\nChecking npm registry...`, "info");
|
|
454
|
+
const latest = await fetchLatestVersionForCommand(registryUrl);
|
|
395
455
|
if (!latest) {
|
|
396
456
|
ctx.ui.notify("Failed to reach npm registry. Check your network connection.", "error");
|
|
397
457
|
return;
|
|
398
458
|
}
|
|
399
|
-
if (compareSemverLocal(latest, current) <= 0) {
|
|
400
|
-
ctx.ui.notify(`Already up to date (
|
|
459
|
+
if (current && compareSemverLocal(latest, current) <= 0) {
|
|
460
|
+
ctx.ui.notify(`Already up to date (${formatCommandVersion(current)}).`, "info");
|
|
401
461
|
return;
|
|
402
462
|
}
|
|
403
|
-
ctx.ui.notify(`Updating:
|
|
463
|
+
ctx.ui.notify(`Updating: ${formatCommandVersion(current)} → v${latest}...`, "info");
|
|
404
464
|
const installCmd = resolveInstallCommand(`${NPM_PACKAGE}@latest`);
|
|
405
465
|
try {
|
|
406
466
|
execSync(installCmd, {
|
|
407
467
|
stdio: ["ignore", "pipe", "ignore"],
|
|
408
468
|
});
|
|
409
|
-
|
|
469
|
+
const newPathVersion = browserUpdate ? resolveGsdBrowserPathVersionForCommand() : null;
|
|
470
|
+
const pathReady = !browserUpdate || (!!newPathVersion && compareSemverLocal(newPathVersion, latest) >= 0);
|
|
471
|
+
ctx.ui.notify(browserUpdate
|
|
472
|
+
? `Updated gsd-browser to v${latest}. Restart your GSD session to use the new browser automation version.` +
|
|
473
|
+
(pathReady ? "" : "\nNote: Ensure the npm global bin directory is on your PATH so MCP automation uses the updated binary.")
|
|
474
|
+
: `Updated to v${latest}. Restart your GSD session to use the new version.`, "info");
|
|
410
475
|
}
|
|
411
476
|
catch {
|
|
412
477
|
ctx.ui.notify(`Update failed. Try manually: ${installCmd}`, "error");
|
|
@@ -33,7 +33,8 @@ export function formatMcpInitResult(status, configPath, targetPath) {
|
|
|
33
33
|
`Project: ${targetPath}`,
|
|
34
34
|
`Config: ${configPath}`,
|
|
35
35
|
"",
|
|
36
|
-
"MCP-capable clients can now load the GSD workflow MCP
|
|
36
|
+
"MCP-capable clients can now load the GSD workflow and gsd-browser MCP servers from this folder.",
|
|
37
|
+
"Pi Providers use the managed gsd-browser engine directly; this project config is for External MCP Clients.",
|
|
37
38
|
"Restart or reconnect any client that already has this project open.",
|
|
38
39
|
].join("\n");
|
|
39
40
|
}
|
|
@@ -305,10 +305,18 @@ This config sets a parent workspace with two child repositories. The implicit `p
|
|
|
305
305
|
- `max_cycles`: number — max times this hook fires per trigger (default: 1, max: 10).
|
|
306
306
|
- `model`: string — optional model override.
|
|
307
307
|
- `artifact`: string — expected output file name (relative to task/slice dir). Hook is skipped if file already exists (idempotent).
|
|
308
|
+
- `criticality`: `"advisory"` or `"blocking"` — advisory preserves current best-effort behavior; blocking requires clean hook completion plus a valid outcome verdict before auto-mode advances. Default: `"advisory"`.
|
|
308
309
|
- `retry_on`: string — if this file is produced instead of the artifact, re-run the trigger unit then re-run hooks.
|
|
310
|
+
- `on_block`: object — optional routing for blocking findings:
|
|
311
|
+
- `action`: `"retry-unit"`, `"retry-task"`, `"queue-task"`, `"queue-slice"`, or `"pause"`.
|
|
312
|
+
- `artifact`: string — optional compatibility artifact for retry routing.
|
|
309
313
|
- `agent`: string — agent definition file to use for hook execution.
|
|
310
314
|
- `enabled`: boolean — toggle without removing (default: `true`).
|
|
311
315
|
|
|
316
|
+
Blocking hook artifacts must begin with YAML frontmatter containing either `verdict` or `outcome.verdict`.
|
|
317
|
+
Supported verdicts are `pass`, `advisory`, `needs-rework`, `needs-remediation`, and `needs-attention`.
|
|
318
|
+
`pass` and `advisory` continue; `needs-rework` retries the trigger unit when routed with `retry-unit`/`retry-task`; `needs-remediation` and `needs-attention` pause with recovery guidance.
|
|
319
|
+
|
|
312
320
|
- `pre_dispatch_hooks`: array — hooks that fire before a unit is dispatched. Each entry has:
|
|
313
321
|
- `name`: string — unique hook identifier.
|
|
314
322
|
- `before`: string[] — unit types to intercept.
|
|
@@ -258,14 +258,14 @@ export async function checkRuntimeHealth(basePath, issues, fixesApplied, shouldF
|
|
|
258
258
|
catch {
|
|
259
259
|
count = MAX_UAT_ATTEMPTS + 1;
|
|
260
260
|
}
|
|
261
|
-
if (count
|
|
261
|
+
if (count < MAX_UAT_ATTEMPTS)
|
|
262
262
|
continue;
|
|
263
263
|
issues.push({
|
|
264
264
|
severity: "warning",
|
|
265
265
|
code: "uat_retry_exhausted",
|
|
266
266
|
scope: "slice",
|
|
267
267
|
unitId: `${mid}/${sid}`,
|
|
268
|
-
message: `run-uat for ${mid}/${sid} exhausted ${count
|
|
268
|
+
message: `run-uat for ${mid}/${sid} exhausted ${count} attempt(s) without an ASSESSMENT verdict. Reset the retry counter after fixing the underlying UAT/tool issue, then rerun /gsd auto.`,
|
|
269
269
|
file: `.gsd/runtime/${fileName}`,
|
|
270
270
|
fixable: true,
|
|
271
271
|
});
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
import { basename, resolve } from "node:path";
|
|
2
|
+
import { resolve } from "node:path";
|
|
5
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { GSD_BROWSER_MCP_SERVER_NAME, resolveBundledGsdBrowserCliPath, resolveGsdBrowserMcpLaunchConfig, } from "../shared/gsd-browser-cli.js";
|
|
6
5
|
import { assertSafeDirectory } from "./validate-directory.js";
|
|
7
6
|
import { detectWorkflowMcpLaunchConfig } from "./workflow-mcp.js";
|
|
8
7
|
export const GSD_WORKFLOW_MCP_SERVER_NAME = "gsd-workflow";
|
|
9
|
-
export
|
|
8
|
+
export { GSD_BROWSER_MCP_SERVER_NAME, resolveBundledGsdBrowserCliPath };
|
|
10
9
|
export function resolveBundledGsdCliPath(env = process.env) {
|
|
11
10
|
const explicit = env.GSD_CLI_PATH?.trim() || env.GSD_BIN_PATH?.trim();
|
|
12
11
|
if (explicit)
|
|
@@ -22,30 +21,6 @@ export function resolveBundledGsdCliPath(env = process.env) {
|
|
|
22
21
|
}
|
|
23
22
|
return null;
|
|
24
23
|
}
|
|
25
|
-
export function resolveBundledGsdBrowserCliPath(env = process.env) {
|
|
26
|
-
const explicit = env.GSD_BROWSER_CLI_PATH?.trim() || env.GSD_BROWSER_BIN_PATH?.trim();
|
|
27
|
-
if (explicit)
|
|
28
|
-
return explicit;
|
|
29
|
-
try {
|
|
30
|
-
const requireFromHere = createRequire(import.meta.url);
|
|
31
|
-
const packageJsonPath = requireFromHere.resolve("@opengsd/gsd-browser/package.json");
|
|
32
|
-
const candidate = resolve(packageJsonPath, "..", "bin", "gsd-browser");
|
|
33
|
-
if (existsSync(candidate))
|
|
34
|
-
return candidate;
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
// Fall through to path candidates for source/dist layouts.
|
|
38
|
-
}
|
|
39
|
-
const candidates = [
|
|
40
|
-
resolve(fileURLToPath(new URL("../../../../node_modules/@opengsd/gsd-browser/bin/gsd-browser", import.meta.url))),
|
|
41
|
-
resolve(fileURLToPath(new URL("../../../../node_modules/.bin/gsd-browser", import.meta.url))),
|
|
42
|
-
];
|
|
43
|
-
for (const candidate of candidates) {
|
|
44
|
-
if (existsSync(candidate))
|
|
45
|
-
return candidate;
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
24
|
export function buildProjectWorkflowMcpServerConfig(projectRoot, env = process.env) {
|
|
50
25
|
return buildProjectWorkflowMcpServerSpec(projectRoot, env).server;
|
|
51
26
|
}
|
|
@@ -69,68 +44,26 @@ function buildProjectWorkflowMcpServerSpec(projectRoot, env = process.env) {
|
|
|
69
44
|
},
|
|
70
45
|
};
|
|
71
46
|
}
|
|
72
|
-
function parseJsonEnv(env, name) {
|
|
73
|
-
const raw = env[name];
|
|
74
|
-
if (!raw)
|
|
75
|
-
return undefined;
|
|
76
|
-
try {
|
|
77
|
-
return JSON.parse(raw);
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
throw new Error(`Invalid JSON in ${name}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
47
|
function isEnvDisabled(value) {
|
|
84
48
|
if (!value)
|
|
85
49
|
return false;
|
|
86
50
|
const normalized = value.trim().toLowerCase();
|
|
87
51
|
return normalized === "0" || normalized === "false" || normalized === "off";
|
|
88
52
|
}
|
|
89
|
-
function buildBrowserSessionName(projectRoot) {
|
|
90
|
-
const resolvedProjectRoot = resolve(projectRoot);
|
|
91
|
-
const base = basename(resolvedProjectRoot)
|
|
92
|
-
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
93
|
-
.replace(/^-+|-+$/g, "") || "project";
|
|
94
|
-
const hash = createHash("sha1").update(resolvedProjectRoot).digest("hex").slice(0, 8);
|
|
95
|
-
return `gsd-${base}-${hash}`;
|
|
96
|
-
}
|
|
97
53
|
export function buildProjectBrowserMcpServerConfig(projectRoot, env = process.env) {
|
|
98
54
|
return buildProjectBrowserMcpServerSpec(projectRoot, env)?.server ?? null;
|
|
99
55
|
}
|
|
100
56
|
function buildProjectBrowserMcpServerSpec(projectRoot, env = process.env) {
|
|
101
57
|
if (isEnvDisabled(env.GSD_BROWSER_MCP_ENABLED))
|
|
102
58
|
return null;
|
|
103
|
-
const
|
|
104
|
-
const serverName = env.GSD_BROWSER_MCP_NAME?.trim() || GSD_BROWSER_MCP_SERVER_NAME;
|
|
105
|
-
const explicitArgs = parseJsonEnv(env, "GSD_BROWSER_MCP_ARGS");
|
|
106
|
-
const explicitEnv = parseJsonEnv(env, "GSD_BROWSER_MCP_ENV");
|
|
107
|
-
const explicitCommand = env.GSD_BROWSER_MCP_COMMAND?.trim();
|
|
108
|
-
const explicitCliPath = env.GSD_BROWSER_CLI_PATH?.trim() || env.GSD_BROWSER_BIN_PATH?.trim();
|
|
109
|
-
const bundledCliPath = !explicitCommand && !explicitCliPath ? resolveBundledGsdBrowserCliPath(env) : null;
|
|
110
|
-
const command = explicitCommand
|
|
111
|
-
|| explicitCliPath
|
|
112
|
-
|| (bundledCliPath ? process.execPath : undefined)
|
|
113
|
-
|| "gsd-browser";
|
|
114
|
-
const args = Array.isArray(explicitArgs) && explicitArgs.length > 0
|
|
115
|
-
? explicitArgs.map(String)
|
|
116
|
-
: [
|
|
117
|
-
...(bundledCliPath ? [bundledCliPath] : []),
|
|
118
|
-
"mcp",
|
|
119
|
-
"--session",
|
|
120
|
-
buildBrowserSessionName(resolvedProjectRoot),
|
|
121
|
-
"--identity-scope",
|
|
122
|
-
"project",
|
|
123
|
-
"--identity-project",
|
|
124
|
-
resolvedProjectRoot,
|
|
125
|
-
];
|
|
126
|
-
const cwd = env.GSD_BROWSER_MCP_CWD?.trim() || resolvedProjectRoot;
|
|
59
|
+
const launch = resolveGsdBrowserMcpLaunchConfig(projectRoot, env);
|
|
127
60
|
return {
|
|
128
|
-
serverName,
|
|
61
|
+
serverName: launch.serverName,
|
|
129
62
|
server: {
|
|
130
|
-
command,
|
|
131
|
-
args,
|
|
132
|
-
cwd,
|
|
133
|
-
...(
|
|
63
|
+
command: launch.command,
|
|
64
|
+
args: launch.args,
|
|
65
|
+
cwd: launch.cwd,
|
|
66
|
+
...(launch.env ? { env: launch.env } : {}),
|
|
134
67
|
},
|
|
135
68
|
};
|
|
136
69
|
}
|
|
@@ -19,6 +19,15 @@ export function isRetryPending() {
|
|
|
19
19
|
export function consumeRetryTrigger() {
|
|
20
20
|
return getOrCreateRegistry().consumeRetryTrigger();
|
|
21
21
|
}
|
|
22
|
+
export function consumeHookFailure() {
|
|
23
|
+
return getOrCreateRegistry().consumeHookFailure();
|
|
24
|
+
}
|
|
25
|
+
export function isGateBlockPending() {
|
|
26
|
+
return getOrCreateRegistry().isGateBlockPending();
|
|
27
|
+
}
|
|
28
|
+
export function consumeGateBlock() {
|
|
29
|
+
return getOrCreateRegistry().consumeGateBlock();
|
|
30
|
+
}
|
|
22
31
|
export function resetHookState() {
|
|
23
32
|
getOrCreateRegistry().resetState();
|
|
24
33
|
}
|
|
@@ -15,6 +15,14 @@ const VALID_UOK_TURN_ACTIONS = new Set([
|
|
|
15
15
|
"snapshot",
|
|
16
16
|
"status-only",
|
|
17
17
|
]);
|
|
18
|
+
const VALID_POST_UNIT_HOOK_CRITICALITIES = new Set(["advisory", "blocking"]);
|
|
19
|
+
const VALID_POST_UNIT_HOOK_ON_BLOCK_ACTIONS = new Set([
|
|
20
|
+
"retry-unit",
|
|
21
|
+
"retry-task",
|
|
22
|
+
"queue-task",
|
|
23
|
+
"queue-slice",
|
|
24
|
+
"pause",
|
|
25
|
+
]);
|
|
18
26
|
export function validatePreferences(preferences) {
|
|
19
27
|
const errors = [];
|
|
20
28
|
const warnings = [];
|
|
@@ -474,9 +482,40 @@ export function validatePreferences(preferences) {
|
|
|
474
482
|
if (typeof hook.artifact === "string" && hook.artifact.trim()) {
|
|
475
483
|
validHook.artifact = hook.artifact.trim();
|
|
476
484
|
}
|
|
485
|
+
if (hook.criticality !== undefined) {
|
|
486
|
+
const criticality = typeof hook.criticality === "string" ? hook.criticality.trim() : "";
|
|
487
|
+
if (VALID_POST_UNIT_HOOK_CRITICALITIES.has(criticality)) {
|
|
488
|
+
validHook.criticality = criticality;
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
errors.push(`post_unit_hooks "${name}" invalid criticality: ${String(hook.criticality)}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
477
494
|
if (typeof hook.retry_on === "string" && hook.retry_on.trim()) {
|
|
478
495
|
validHook.retry_on = hook.retry_on.trim();
|
|
479
496
|
}
|
|
497
|
+
if (hook.on_block !== undefined) {
|
|
498
|
+
if (!hook.on_block || typeof hook.on_block !== "object") {
|
|
499
|
+
errors.push(`post_unit_hooks "${name}" on_block must be an object`);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
const onBlock = hook.on_block;
|
|
503
|
+
const action = typeof onBlock.action === "string" ? onBlock.action.trim() : "";
|
|
504
|
+
if (!VALID_POST_UNIT_HOOK_ON_BLOCK_ACTIONS.has(action)) {
|
|
505
|
+
errors.push(`post_unit_hooks "${name}" invalid on_block action: ${String(onBlock.action)}`);
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
validHook.on_block = { action: action };
|
|
509
|
+
if (typeof onBlock.artifact === "string" && onBlock.artifact.trim()) {
|
|
510
|
+
validHook.on_block.artifact = onBlock.artifact.trim();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (validHook.criticality === "blocking" && !validHook.artifact) {
|
|
516
|
+
errors.push(`post_unit_hooks "${name}" criticality blocking requires artifact`);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
480
519
|
if (typeof hook.agent === "string" && hook.agent.trim()) {
|
|
481
520
|
validHook.agent = hook.agent.trim();
|
|
482
521
|
}
|
|
@@ -26,9 +26,16 @@ function hasRequiredExtensionAssets(rootDir, exists = existsSync) {
|
|
|
26
26
|
return (exists(join(rootDir, "prompts")) &&
|
|
27
27
|
exists(join(rootDir, "templates", "task-summary.md")));
|
|
28
28
|
}
|
|
29
|
+
function isSourceExtensionDir(moduleDir) {
|
|
30
|
+
return moduleDir.replaceAll("\\", "/").endsWith("/src/resources/extensions/gsd");
|
|
31
|
+
}
|
|
29
32
|
export function resolveExtensionDirFromCandidates(moduleDir, agentGsdDir, exists = existsSync) {
|
|
30
33
|
const moduleUsable = hasRequiredExtensionAssets(moduleDir, exists);
|
|
31
34
|
const agentUsable = hasRequiredExtensionAssets(agentGsdDir, exists);
|
|
35
|
+
// Source checkouts must use their own prompt tree. Otherwise local tests and
|
|
36
|
+
// dev runs can silently render stale prompts from ~/.gsd/agent/extensions/gsd.
|
|
37
|
+
if (moduleUsable && isSourceExtensionDir(moduleDir))
|
|
38
|
+
return moduleDir;
|
|
32
39
|
// Prefer the user-local extension tree when both are valid. This avoids
|
|
33
40
|
// leaking npm/global-install paths into prompts on Windows.
|
|
34
41
|
if (agentUsable)
|