@opengsd/gsd-pi 1.5.0-dev.4fb2d460 → 1.5.0-dev.c4c8885f
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/browser-tools/engine/managed-gsd-browser.js +114 -36
- package/dist/resources/extensions/gsd/activity-log.js +1 -1
- package/dist/resources/extensions/gsd/auto/orchestrator.js +16 -10
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +53 -15
- package/dist/resources/extensions/gsd/auto-model-selection.js +28 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +4 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +29 -117
- package/dist/resources/extensions/gsd/auto-start.js +53 -30
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +12 -2
- package/dist/resources/extensions/gsd/auto-timers.js +9 -2
- package/dist/resources/extensions/gsd/auto.js +97 -31
- package/dist/resources/extensions/gsd/compat/compat-marker.js +44 -0
- package/dist/resources/extensions/gsd/config-overlay.js +7 -2
- package/dist/resources/extensions/gsd/db/engine.js +33 -0
- package/dist/resources/extensions/gsd/db-adapter.js +30 -3
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +3 -3
- package/dist/resources/extensions/gsd/journal.js +1 -1
- package/dist/resources/extensions/gsd/markdown-renderer.js +34 -91
- package/dist/resources/extensions/gsd/metrics.js +2 -2
- package/dist/resources/extensions/gsd/paths.js +70 -49
- package/dist/resources/extensions/gsd/post-unit-hooks.js +27 -0
- package/dist/resources/extensions/gsd/preferences-models.js +66 -15
- package/dist/resources/extensions/gsd/preferences-validation.js +66 -10
- package/dist/resources/extensions/gsd/rule-registry.js +106 -10
- package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +1 -10
- package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +3 -24
- package/dist/resources/extensions/gsd/state-reconciliation/index.js +10 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -5
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +16 -10
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +3 -8
- package/dist/resources/extensions/gsd/uat-dispatch.js +94 -0
- package/dist/resources/extensions/gsd/uok/audit-toggle.js +31 -2
- package/dist/resources/extensions/gsd/uok/audit.js +3 -0
- package/dist/resources/extensions/gsd/uok/gate-runner.js +22 -30
- package/dist/resources/extensions/gsd/uok/gitops.js +18 -15
- package/dist/resources/extensions/gsd/uok/kernel.js +120 -46
- package/dist/resources/extensions/gsd/uok/model-policy.js +17 -14
- package/dist/resources/extensions/gsd/workflow-event-ledger.js +1 -1
- package/dist/resources/extensions/gsd/workflow-logger.js +1 -1
- package/dist/resources/extensions/gsd/workflow-projections.js +9 -26
- package/dist/resources/extensions/gsd/workspace.js +5 -14
- package/dist/resources/extensions/shared/gsd-browser-cli.js +29 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/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/boot/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/chunks/{1128.js → 1991.js} +1 -1
- 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/dist/worktree-cli-create.d.ts +10 -0
- package/dist/worktree-cli-create.js +11 -0
- package/dist/worktree-cli-format.d.ts +12 -0
- package/dist/worktree-cli-format.js +38 -0
- package/dist/worktree-cli-plan.d.ts +16 -0
- package/dist/worktree-cli-plan.js +19 -0
- package/dist/worktree-cli-session.d.ts +13 -0
- package/dist/worktree-cli-session.js +18 -0
- package/dist/worktree-cli-status.d.ts +35 -0
- package/dist/worktree-cli-status.js +54 -0
- package/dist/worktree-cli.d.ts +1 -16
- package/dist/worktree-cli.js +28 -122
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
- package/integrations/hermes/pyproject.toml +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/orchestrator-agent.d.ts +21 -0
- package/packages/daemon/dist/orchestrator-agent.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator-agent.js +113 -0
- package/packages/daemon/dist/orchestrator-agent.js.map +1 -0
- package/packages/daemon/dist/orchestrator-tools.d.ts +12 -0
- package/packages/daemon/dist/orchestrator-tools.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator-tools.js +137 -0
- package/packages/daemon/dist/orchestrator-tools.js.map +1 -0
- package/packages/daemon/dist/orchestrator.d.ts +4 -56
- package/packages/daemon/dist/orchestrator.d.ts.map +1 -1
- package/packages/daemon/dist/orchestrator.js +13 -303
- package/packages/daemon/dist/orchestrator.js.map +1 -1
- package/packages/daemon/dist/orchestrator.test.js +11 -1
- package/packages/daemon/dist/orchestrator.test.js.map +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +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 +2 -2
- 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 +146 -33
- package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +25 -0
- package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +93 -0
- package/src/resources/extensions/gsd/activity-log.ts +1 -1
- package/src/resources/extensions/gsd/auto/orchestrator.ts +17 -11
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +59 -14
- package/src/resources/extensions/gsd/auto-model-selection.ts +33 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +4 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +53 -113
- package/src/resources/extensions/gsd/auto-start.ts +57 -28
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +12 -2
- package/src/resources/extensions/gsd/auto-timers.ts +9 -2
- package/src/resources/extensions/gsd/auto.ts +103 -30
- package/src/resources/extensions/gsd/compat/compat-marker.ts +47 -0
- package/src/resources/extensions/gsd/config-overlay.ts +7 -1
- package/src/resources/extensions/gsd/db/engine.ts +36 -0
- package/src/resources/extensions/gsd/db-adapter.ts +29 -3
- package/src/resources/extensions/gsd/docs/preferences-reference.md +3 -3
- package/src/resources/extensions/gsd/journal.ts +1 -1
- package/src/resources/extensions/gsd/markdown-renderer.ts +52 -103
- package/src/resources/extensions/gsd/metrics.ts +2 -2
- package/src/resources/extensions/gsd/paths.ts +87 -48
- package/src/resources/extensions/gsd/post-unit-hooks.ts +32 -0
- package/src/resources/extensions/gsd/preferences-models.ts +74 -20
- package/src/resources/extensions/gsd/preferences-types.ts +24 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +77 -9
- package/src/resources/extensions/gsd/rule-registry.ts +111 -9
- package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +1 -9
- package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +2 -22
- package/src/resources/extensions/gsd/state-reconciliation/index.ts +14 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +41 -2
- package/src/resources/extensions/gsd/tests/auto-post-unit-artifact-diagnostic.test.ts +48 -1
- package/src/resources/extensions/gsd/tests/auto-start-project-milestone-reconcile.test.ts +29 -1
- package/src/resources/extensions/gsd/tests/auto-timeout-recovery-supervisor-inflight-guard.test.ts +159 -0
- package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/compat-marker.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/db-adapter.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/db-writable-probe.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/flat-phase-renderer.test.ts +22 -1
- package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +72 -32
- package/src/resources/extensions/gsd/tests/integration/paths.test.ts +88 -1
- package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +269 -0
- package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/reassess-handler.test.ts +49 -1
- package/src/resources/extensions/gsd/tests/rule-registry.test.ts +95 -1
- package/src/resources/extensions/gsd/tests/sibling-model-fallbacks.test.ts +189 -0
- package/src/resources/extensions/gsd/tests/single-field-model-thinking.test.ts +156 -0
- package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/supervisor-model-swap-inflight-guard.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/supervisor-model-swap-thinking-clamp.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/uat-dispatch.test.ts +211 -0
- package/src/resources/extensions/gsd/tests/uok-gate-runner.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/uok-kernel-path.test.ts +226 -2
- package/src/resources/extensions/gsd/tests/uok-model-policy.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +213 -2
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +9 -5
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +19 -8
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +13 -9
- package/src/resources/extensions/gsd/types.ts +23 -4
- package/src/resources/extensions/gsd/uat-dispatch.ts +176 -0
- package/src/resources/extensions/gsd/uok/audit-toggle.ts +32 -2
- package/src/resources/extensions/gsd/uok/audit.ts +2 -0
- package/src/resources/extensions/gsd/uok/gate-runner.ts +26 -36
- package/src/resources/extensions/gsd/uok/gitops.ts +21 -18
- package/src/resources/extensions/gsd/uok/kernel.ts +169 -48
- package/src/resources/extensions/gsd/uok/model-policy.ts +20 -17
- package/src/resources/extensions/gsd/workflow-event-ledger.ts +1 -1
- package/src/resources/extensions/gsd/workflow-logger.ts +1 -1
- package/src/resources/extensions/gsd/workflow-projections.ts +9 -24
- package/src/resources/extensions/gsd/workspace.ts +5 -21
- package/src/resources/extensions/shared/gsd-browser-cli.ts +29 -0
- /package/dist/web/standalone/.next/static/{gXoWec4UNqcR0tkuHwYCr → NPMNi2HV8BuK3baQsEUhk}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{gXoWec4UNqcR0tkuHwYCr → NPMNi2HV8BuK3baQsEUhk}/_ssgManifest.js +0 -0
|
@@ -25,6 +25,57 @@ const VALID_UOK_TURN_ACTIONS = new Set([
|
|
|
25
25
|
"snapshot",
|
|
26
26
|
"status-only",
|
|
27
27
|
]);
|
|
28
|
+
/**
|
|
29
|
+
* Sanitize a single model field that accepts either a bare model ID or the
|
|
30
|
+
* extended `{ model, provider?, fallbacks? }` object form used by phase buckets
|
|
31
|
+
* (#1229). Returns the sanitized value, or undefined when unset/invalid (in
|
|
32
|
+
* which case an error describing `label` is pushed onto `errors`).
|
|
33
|
+
*/
|
|
34
|
+
function sanitizeModelField(raw, label, errors, warnings) {
|
|
35
|
+
if (raw === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (typeof raw === "string") {
|
|
38
|
+
const trimmed = raw.trim();
|
|
39
|
+
if (!trimmed) {
|
|
40
|
+
errors.push(`${label} must not be an empty string`);
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return trimmed;
|
|
44
|
+
}
|
|
45
|
+
if (raw && typeof raw === "object") {
|
|
46
|
+
const obj = raw;
|
|
47
|
+
const model = typeof obj.model === "string" ? obj.model.trim() : "";
|
|
48
|
+
if (!model) {
|
|
49
|
+
errors.push(`${label} object form requires a non-empty "model"`);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const sanitized = { model };
|
|
53
|
+
if (typeof obj.provider === "string" && obj.provider.trim()) {
|
|
54
|
+
sanitized.provider = obj.provider.trim();
|
|
55
|
+
}
|
|
56
|
+
if (obj.fallbacks !== undefined) {
|
|
57
|
+
const fallbacks = normalizeStringArray(obj.fallbacks);
|
|
58
|
+
if (fallbacks.length > 0)
|
|
59
|
+
sanitized.fallbacks = fallbacks;
|
|
60
|
+
}
|
|
61
|
+
// Preserve the object form's `thinking` sub-field so the dispatch site can
|
|
62
|
+
// pin the hook/supervisor/subagent reasoning level (#1269). An illegal level
|
|
63
|
+
// is warned-and-stripped, mirroring inline per-phase thinking, so a typo can't
|
|
64
|
+
// reach the resolver and be treated as explicit configuration.
|
|
65
|
+
if (obj.thinking !== undefined) {
|
|
66
|
+
if (VALID_THINKING_LEVELS.has(obj.thinking)) {
|
|
67
|
+
sanitized.thinking = obj.thinking;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
warnings?.push(`${label} thinking "${String(obj.thinking)}" is not a valid thinking level ` +
|
|
71
|
+
`(off, minimal, low, medium, high, xhigh) — ignored`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return sanitized;
|
|
75
|
+
}
|
|
76
|
+
errors.push(`${label} must be a string or an object with a "model" field`);
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
28
79
|
const VALID_POST_UNIT_HOOK_CRITICALITIES = new Set(["advisory", "blocking"]);
|
|
29
80
|
const VALID_POST_UNIT_HOOK_ON_BLOCK_ACTIONS = new Set([
|
|
30
81
|
"retry-unit",
|
|
@@ -452,7 +503,15 @@ export function validatePreferences(preferences) {
|
|
|
452
503
|
// ─── Auto Supervisor ────────────────────────────────────────────────
|
|
453
504
|
if (preferences.auto_supervisor !== undefined) {
|
|
454
505
|
if (preferences.auto_supervisor && typeof preferences.auto_supervisor === "object") {
|
|
455
|
-
|
|
506
|
+
// Validate `model` through the shared sanitizer so the
|
|
507
|
+
// `{ model, provider?, fallbacks? }` object form is rejected on the same
|
|
508
|
+
// terms as its post_unit_hooks / reactive_execution siblings (#1229).
|
|
509
|
+
// Other supervisor fields pass through unchanged.
|
|
510
|
+
const supervisor = { ...preferences.auto_supervisor };
|
|
511
|
+
if (supervisor.model !== undefined) {
|
|
512
|
+
supervisor.model = sanitizeModelField(supervisor.model, "auto_supervisor.model", errors, warnings);
|
|
513
|
+
}
|
|
514
|
+
validated.auto_supervisor = supervisor;
|
|
456
515
|
}
|
|
457
516
|
else {
|
|
458
517
|
errors.push("auto_supervisor must be an object");
|
|
@@ -548,8 +607,9 @@ export function validatePreferences(preferences) {
|
|
|
548
607
|
const mc = typeof hook.max_cycles === "number" ? hook.max_cycles : Number(hook.max_cycles);
|
|
549
608
|
validHook.max_cycles = Number.isFinite(mc) ? Math.max(1, Math.min(10, Math.round(mc))) : 1;
|
|
550
609
|
}
|
|
551
|
-
|
|
552
|
-
|
|
610
|
+
const hookModel = sanitizeModelField(hook.model, `post_unit_hooks "${name}" model`, errors, warnings);
|
|
611
|
+
if (hookModel !== undefined) {
|
|
612
|
+
validHook.model = hookModel;
|
|
553
613
|
}
|
|
554
614
|
if (typeof hook.artifact === "string" && hook.artifact.trim()) {
|
|
555
615
|
validHook.artifact = hook.artifact.trim();
|
|
@@ -1050,13 +1110,9 @@ export function validatePreferences(preferences) {
|
|
|
1050
1110
|
errors.push('reactive_execution.isolation_mode must be "same-tree"');
|
|
1051
1111
|
}
|
|
1052
1112
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
}
|
|
1057
|
-
else {
|
|
1058
|
-
errors.push("reactive_execution.subagent_model must be a non-empty string");
|
|
1059
|
-
}
|
|
1113
|
+
const subagentModel = sanitizeModelField(re.subagent_model, "reactive_execution.subagent_model", errors, warnings);
|
|
1114
|
+
if (subagentModel !== undefined) {
|
|
1115
|
+
validRe.subagent_model = subagentModel;
|
|
1060
1116
|
}
|
|
1061
1117
|
const knownReKeys = new Set(["enabled", "max_parallel", "isolation_mode", "subagent_model"]);
|
|
1062
1118
|
for (const key of Object.keys(re)) {
|
|
@@ -8,21 +8,53 @@
|
|
|
8
8
|
import { logWarning } from "./workflow-logger.js";
|
|
9
9
|
import { resolvePostUnitHooks, resolvePreDispatchHooks } from "./preferences.js";
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
11
|
-
import { join } from "node:path";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
12
|
import { parseUnitId } from "./unit-id.js";
|
|
13
|
+
import { resolveMilestonePath } from "./paths.js";
|
|
13
14
|
import { queryJournal } from "./journal.js";
|
|
14
15
|
import { readUnitRuntimeRecord } from "./unit-runtime.js";
|
|
15
16
|
import { extractFrontmatterVerdict } from "./verdict-parser.js";
|
|
16
17
|
// ─── Artifact Path Resolution ──────────────────────────────────────────────
|
|
17
18
|
export function resolveHookArtifactPath(basePath, unitId, artifactName) {
|
|
18
19
|
const { milestone, slice, task } = parseUnitId(unitId);
|
|
20
|
+
// Prefer the active phase directory (flat-phase layout: .gsd/phases/<NN>-.../).
|
|
21
|
+
// Configured gate artifacts are canonical phase-level files (e.g.
|
|
22
|
+
// BROWSER-RUNTIME-EVIDENCE.md), so resolve the exact declared name first,
|
|
23
|
+
// then a task-prefixed variant for retry sentinels / rework briefs.
|
|
24
|
+
const candidates = [];
|
|
25
|
+
const phaseDir = resolveMilestonePath(basePath, milestone);
|
|
26
|
+
const isFlatPhaseDir = phaseDir !== null
|
|
27
|
+
&& !(dirname(phaseDir).endsWith("/milestones") || dirname(phaseDir).endsWith("\\milestones"));
|
|
28
|
+
if (isFlatPhaseDir) {
|
|
29
|
+
candidates.push(join(phaseDir, artifactName));
|
|
30
|
+
if (task !== undefined) {
|
|
31
|
+
candidates.push(join(phaseDir, `${task}-${artifactName}`));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Legacy nested layout fallbacks (.gsd/milestones/<M>/slices/<S>/tasks/...).
|
|
35
|
+
const legacyBase = join(basePath, ".gsd", "milestones", milestone);
|
|
36
|
+
let legacyDefault;
|
|
19
37
|
if (task !== undefined && slice !== undefined) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
38
|
+
legacyDefault = join(legacyBase, "slices", slice, "tasks", `${task}-${artifactName}`);
|
|
39
|
+
candidates.push(legacyDefault);
|
|
40
|
+
candidates.push(join(legacyBase, "slices", slice, "tasks", artifactName));
|
|
41
|
+
candidates.push(join(legacyBase, "slices", slice, artifactName));
|
|
42
|
+
}
|
|
43
|
+
else if (slice !== undefined) {
|
|
44
|
+
legacyDefault = join(legacyBase, "slices", slice, artifactName);
|
|
45
|
+
candidates.push(legacyDefault);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
legacyDefault = join(legacyBase, artifactName);
|
|
49
|
+
}
|
|
50
|
+
candidates.push(join(legacyBase, artifactName));
|
|
51
|
+
const existing = candidates.find((candidate) => existsSync(candidate));
|
|
52
|
+
if (existing)
|
|
53
|
+
return existing;
|
|
54
|
+
// Nothing on disk yet: prefer the active phase path when the phase dir is
|
|
55
|
+
// known, otherwise fall back to the legacy default (preserves pre-flat-phase
|
|
56
|
+
// behavior for missing-artifact diagnostics).
|
|
57
|
+
return isFlatPhaseDir ? join(phaseDir, artifactName) : legacyDefault;
|
|
26
58
|
}
|
|
27
59
|
// ─── Dispatch Rule Conversion ──────────────────────────────────────────────
|
|
28
60
|
/**
|
|
@@ -70,6 +102,13 @@ export class RuleRegistry {
|
|
|
70
102
|
activeHook = null;
|
|
71
103
|
hookQueue = [];
|
|
72
104
|
cycleCounts = new Map();
|
|
105
|
+
/**
|
|
106
|
+
* Cycle keys that have already been granted a one-shot re-dispatch after a
|
|
107
|
+
* lost/interrupted dispatch (hook charged a cycle but never produced its own
|
|
108
|
+
* unit-end). Bounds the dispatch-cycle refund to exactly one per gate so a
|
|
109
|
+
* hook that repeatedly fails to complete cannot loop forever.
|
|
110
|
+
*/
|
|
111
|
+
redispatchedGateKeys = new Set();
|
|
73
112
|
retryPending = false;
|
|
74
113
|
retryTrigger = null;
|
|
75
114
|
hookFailure = null;
|
|
@@ -269,6 +308,10 @@ export class RuleRegistry {
|
|
|
269
308
|
cycle: currentCycle,
|
|
270
309
|
pendingRetry: false,
|
|
271
310
|
};
|
|
311
|
+
return this._buildHookDispatch(config, triggerUnitId);
|
|
312
|
+
}
|
|
313
|
+
/** Construct the sidecar dispatch for a hook without mutating registry state. */
|
|
314
|
+
_buildHookDispatch(config, triggerUnitId) {
|
|
272
315
|
const { milestone: mid, slice: sid, task: tid } = parseUnitId(triggerUnitId);
|
|
273
316
|
let prompt = config.prompt
|
|
274
317
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
@@ -278,11 +321,27 @@ export class RuleRegistry {
|
|
|
278
321
|
return {
|
|
279
322
|
hookName: config.name,
|
|
280
323
|
prompt,
|
|
281
|
-
|
|
324
|
+
// Model selection (including fallbacks[]) is handled by
|
|
325
|
+
// resolveModelWithFallbacksForUnit for the `hook/<name>` unit type (#1229).
|
|
282
326
|
unitType: `hook/${config.name}`,
|
|
283
327
|
unitId: triggerUnitId,
|
|
284
328
|
};
|
|
285
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Reconstruct the in-flight hook's dispatch from the restored `activeHook`
|
|
332
|
+
* state, without mutating the registry. Used to reconcile a persisted
|
|
333
|
+
* `activeHook` whose session-local dispatch was lost across a pause/resume
|
|
334
|
+
* or crash-recovery, so the hook actually runs instead of being charged
|
|
335
|
+
* against the next unrelated unit's close-out (#1246).
|
|
336
|
+
*/
|
|
337
|
+
getPendingHookDispatch(basePath) {
|
|
338
|
+
if (!this.activeHook)
|
|
339
|
+
return null;
|
|
340
|
+
const config = resolvePostUnitHooks(basePath).find(h => h.name === this.activeHook.hookName);
|
|
341
|
+
if (!config)
|
|
342
|
+
return null;
|
|
343
|
+
return this._buildHookDispatch(config, this.activeHook.triggerUnitId);
|
|
344
|
+
}
|
|
286
345
|
_assessHookCompletion(basePath, hook) {
|
|
287
346
|
return this._assessConfiguredHookCompletion(basePath, hook.hookName, hook.triggerUnitId);
|
|
288
347
|
}
|
|
@@ -368,6 +427,30 @@ export class RuleRegistry {
|
|
|
368
427
|
}
|
|
369
428
|
_handleBlockingGateCompletion(config, hook, basePath, observedCleanExecution) {
|
|
370
429
|
if (!observedCleanExecution) {
|
|
430
|
+
// The hook was charged a cycle at dispatch but never produced its own
|
|
431
|
+
// unit-end — its dispatch was lost/interrupted (pause/resume, crash
|
|
432
|
+
// recovery). Refund that dispatch-consumed cycle exactly once so the gate
|
|
433
|
+
// is re-dispatched at least once instead of hard-blocking on a hook that
|
|
434
|
+
// never actually ran.
|
|
435
|
+
//
|
|
436
|
+
// This refund is intentionally UNCONDITIONAL on the first lost dispatch —
|
|
437
|
+
// it must NOT be gated on `currentCycle >= max_cycles`. A lost dispatch is
|
|
438
|
+
// a non-run: charging its cycle at dispatch time must not consume the
|
|
439
|
+
// hook's real-run budget. Gating the refund on "budget already exhausted"
|
|
440
|
+
// lets a lost dispatch below max_cycles silently eat a genuine cycle, so
|
|
441
|
+
// the hook gets one fewer real run than configured — reintroducing #1244
|
|
442
|
+
// for max_cycles >= 2 (see the "does not steal a real-run cycle" test).
|
|
443
|
+
// The redispatchedGateKeys flag bounds this to a single refund per gate,
|
|
444
|
+
// so repeated lost dispatches still terminate (total dispatches never
|
|
445
|
+
// exceed max_cycles + 1) and a hook that keeps failing blocks normally.
|
|
446
|
+
const cycleKey = hookCycleKey(config, hook);
|
|
447
|
+
if (!this.redispatchedGateKeys.has(cycleKey)) {
|
|
448
|
+
this.redispatchedGateKeys.add(cycleKey);
|
|
449
|
+
const currentCycle = this.cycleCounts.get(cycleKey);
|
|
450
|
+
if (typeof currentCycle === "number" && currentCycle > 0) {
|
|
451
|
+
this.cycleCounts.set(cycleKey, currentCycle - 1);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
371
454
|
return this._rerunGateOrBlock(config, hook, basePath, {
|
|
372
455
|
reason: `hook/${config.name} did not complete cleanly before the trigger unit resumed`,
|
|
373
456
|
});
|
|
@@ -639,6 +722,7 @@ export class RuleRegistry {
|
|
|
639
722
|
this.activeHook = null;
|
|
640
723
|
this.hookQueue = [];
|
|
641
724
|
this.cycleCounts.clear();
|
|
725
|
+
this.redispatchedGateKeys.clear();
|
|
642
726
|
this.retryPending = false;
|
|
643
727
|
this.retryTrigger = null;
|
|
644
728
|
this.hookFailure = null;
|
|
@@ -652,6 +736,7 @@ export class RuleRegistry {
|
|
|
652
736
|
persistState(basePath) {
|
|
653
737
|
const state = {
|
|
654
738
|
cycleCounts: Object.fromEntries(this.cycleCounts),
|
|
739
|
+
redispatchedGateKeys: Array.from(this.redispatchedGateKeys),
|
|
655
740
|
activeHook: this.activeHook ? { ...this.activeHook } : null,
|
|
656
741
|
hookQueue: this.hookQueue.map(entry => ({
|
|
657
742
|
hookName: entry.config.name,
|
|
@@ -687,6 +772,14 @@ export class RuleRegistry {
|
|
|
687
772
|
}
|
|
688
773
|
}
|
|
689
774
|
}
|
|
775
|
+
this.redispatchedGateKeys.clear();
|
|
776
|
+
if (Array.isArray(state.redispatchedGateKeys)) {
|
|
777
|
+
for (const key of state.redispatchedGateKeys) {
|
|
778
|
+
if (typeof key === "string") {
|
|
779
|
+
this.redispatchedGateKeys.add(key);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
690
783
|
this.activeHook = state.activeHook && typeof state.activeHook === "object"
|
|
691
784
|
? { ...state.activeHook }
|
|
692
785
|
: null;
|
|
@@ -715,7 +808,7 @@ export class RuleRegistry {
|
|
|
715
808
|
try {
|
|
716
809
|
const filePath = this._hookStatePath(basePath);
|
|
717
810
|
if (existsSync(filePath)) {
|
|
718
|
-
writeFileSync(filePath, JSON.stringify({ cycleCounts: {}, activeHook: null, hookQueue: [], savedAt: new Date().toISOString() }, null, 2), "utf-8");
|
|
811
|
+
writeFileSync(filePath, JSON.stringify({ cycleCounts: {}, redispatchedGateKeys: [], activeHook: null, hookQueue: [], savedAt: new Date().toISOString() }, null, 2), "utf-8");
|
|
719
812
|
}
|
|
720
813
|
}
|
|
721
814
|
catch (e) {
|
|
@@ -793,7 +886,10 @@ export class RuleRegistry {
|
|
|
793
886
|
return {
|
|
794
887
|
hookName: hook.name,
|
|
795
888
|
prompt,
|
|
796
|
-
|
|
889
|
+
// Model selection (including fallbacks[]) is resolved by dispatchHookUnit
|
|
890
|
+
// via resolveModelWithFallbacksForUnit for the `hook/<name>` unit type,
|
|
891
|
+
// matching the auto-mode path. Emitting the primary-only model here would
|
|
892
|
+
// discard the configured fallback chain (#1229).
|
|
797
893
|
unitType: `hook/${hook.name}`,
|
|
798
894
|
unitId,
|
|
799
895
|
};
|
|
@@ -140,16 +140,7 @@ function detectArtifactDbStatusDriftForMilestone(basePath, milestoneId) {
|
|
|
140
140
|
for (const task of tasks) {
|
|
141
141
|
if (isClosedStatus(task.status))
|
|
142
142
|
continue;
|
|
143
|
-
|
|
144
|
-
let diskTaskSummary = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY");
|
|
145
|
-
if (!diskTaskSummary) {
|
|
146
|
-
const phaseDir = resolveMilestonePath(basePath, milestoneId);
|
|
147
|
-
if (phaseDir) {
|
|
148
|
-
const flatSummary = join(phaseDir, `${task.id}-SUMMARY.md`);
|
|
149
|
-
if (existsSync(flatSummary))
|
|
150
|
-
diskTaskSummary = flatSummary;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
143
|
+
const diskTaskSummary = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY");
|
|
153
144
|
if (!diskTaskSummary || !existsSync(diskTaskSummary))
|
|
154
145
|
continue;
|
|
155
146
|
addUniqueDrift(drifts, seen, {
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
// same value).
|
|
8
8
|
import { existsSync, statSync } from "node:fs";
|
|
9
9
|
import { getAllMilestones, getMilestone, getMilestoneSlices, getSliceTasks, isDbAvailable, updateMilestoneStatus, updateSliceStatus, updateTaskStatus, } from "../../gsd-db.js";
|
|
10
|
-
import { resolveMilestoneFile,
|
|
11
|
-
import { join } from "node:path";
|
|
10
|
+
import { resolveMilestoneFile, resolveSliceFile, resolveTaskFile, } from "../../paths.js";
|
|
12
11
|
const COMPLETE_STATUSES = new Set(["complete", "done"]);
|
|
13
12
|
function summaryMtimeIso(path) {
|
|
14
13
|
try {
|
|
@@ -68,18 +67,7 @@ function collectMilestoneCompletionDrift(mid, ctx, drifts) {
|
|
|
68
67
|
continue;
|
|
69
68
|
if (task.completed_at !== null)
|
|
70
69
|
continue;
|
|
71
|
-
|
|
72
|
-
// Legacy: resolveTaskFile returns the tasks/TID/TID-SUMMARY.md path.
|
|
73
|
-
let taskSummary = resolveTaskFile(ctx.basePath, mid, slice.id, task.id, "SUMMARY");
|
|
74
|
-
// Flat-phase fallback: check the phase dir directly
|
|
75
|
-
if (!taskSummary) {
|
|
76
|
-
const phaseDir = resolveMilestonePath(ctx.basePath, mid);
|
|
77
|
-
if (phaseDir) {
|
|
78
|
-
const flatSummary = join(phaseDir, `${task.id}-SUMMARY.md`);
|
|
79
|
-
if (existsSync(flatSummary))
|
|
80
|
-
taskSummary = flatSummary;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
70
|
+
const taskSummary = resolveTaskFile(ctx.basePath, mid, slice.id, task.id, "SUMMARY");
|
|
83
71
|
if (taskSummary && existsSync(taskSummary)) {
|
|
84
72
|
drifts.push({
|
|
85
73
|
kind: "missing-completion-timestamp",
|
|
@@ -136,16 +124,7 @@ export function repairMissingCompletionTimestamp(record, ctx) {
|
|
|
136
124
|
task.completed_at !== null ||
|
|
137
125
|
!COMPLETE_STATUSES.has(task.status))
|
|
138
126
|
return;
|
|
139
|
-
|
|
140
|
-
let summary = resolveTaskFile(ctx.basePath, mid, sid, tid, "SUMMARY");
|
|
141
|
-
if (!summary) {
|
|
142
|
-
const phaseDir = resolveMilestonePath(ctx.basePath, mid);
|
|
143
|
-
if (phaseDir) {
|
|
144
|
-
const flatSummary = join(phaseDir, `${tid}-SUMMARY.md`);
|
|
145
|
-
if (existsSync(flatSummary))
|
|
146
|
-
summary = flatSummary;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
127
|
+
const summary = resolveTaskFile(ctx.basePath, mid, sid, tid, "SUMMARY");
|
|
149
128
|
const ts = summary ? summaryMtimeIso(summary) : null;
|
|
150
129
|
if (!ts)
|
|
151
130
|
return;
|
|
@@ -41,6 +41,16 @@ export async function reconcileBeforeDispatch(basePath, partialDeps = {}) {
|
|
|
41
41
|
if (!deps.dryRun) {
|
|
42
42
|
const { capturePlanningCompatIfNeeded } = await import("../compat/planning-compat.js");
|
|
43
43
|
await capturePlanningCompatIfNeeded(basePath);
|
|
44
|
+
// Self-heal phantom projection entries (#1257): a renamed/removed phase
|
|
45
|
+
// directory leaves stale `.compat.json` projection paths that no detector
|
|
46
|
+
// ever prunes (they all skip missing files), so the marker drifts from disk
|
|
47
|
+
// reality permanently. Drop entries whose backing file is gone before the
|
|
48
|
+
// detect passes run. Gated on !dryRun to keep detection read-only.
|
|
49
|
+
const { pruneOrphanedProjectionEntries } = await import("../compat/compat-marker.js");
|
|
50
|
+
const prunedCount = pruneOrphanedProjectionEntries(basePath);
|
|
51
|
+
if (prunedCount > 0) {
|
|
52
|
+
logWarning("reconcile", `pruned ${prunedCount} orphaned projection entr${prunedCount === 1 ? "y" : "ies"} from .compat.json (backing file no longer on disk)`);
|
|
53
|
+
}
|
|
44
54
|
}
|
|
45
55
|
for (let pass = 0; pass < MAX_PASSES; pass++) {
|
|
46
56
|
deps.invalidateStateCache();
|
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
* renders MILESTONE-SUMMARY.md to disk, stores rendered markdown in DB
|
|
9
9
|
* for recovery, and invalidates caches.
|
|
10
10
|
*/
|
|
11
|
-
import { join } from "node:path";
|
|
12
11
|
import { existsSync } from "node:fs";
|
|
13
12
|
import { transaction, getMilestone, getMilestoneSlices, getSliceTasks, getLatestAssessmentByScope, updateMilestoneStatus, } from "../gsd-db.js";
|
|
14
|
-
import { clearPathCache,
|
|
13
|
+
import { clearPathCache, resolveMilestoneFile, targetMilestoneFile } from "../paths.js";
|
|
15
14
|
import { resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
|
|
16
15
|
import { isClosedStatus, isDeferredStatus } from "../status-guards.js";
|
|
17
16
|
import { saveFile, clearParseCache } from "../files.js";
|
|
@@ -144,9 +143,8 @@ export async function handleCompleteMilestone(params, basePath) {
|
|
|
144
143
|
}
|
|
145
144
|
// ── Filesystem operations (outside transaction) ─────────────────────────
|
|
146
145
|
const summaryMd = renderMilestoneSummaryMarkdown(params, completedAt);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const summaryPath = join(artifactBasePath, relMilestoneFile(artifactBasePath, params.milestoneId, "SUMMARY"));
|
|
146
|
+
const summaryPath = resolveMilestoneFile(artifactBasePath, params.milestoneId, "SUMMARY") ??
|
|
147
|
+
targetMilestoneFile(artifactBasePath, params.milestoneId, "SUMMARY", getMilestone(params.milestoneId)?.title);
|
|
150
148
|
// Guard (#4598): if SUMMARY.md already exists on disk, do not overwrite it.
|
|
151
149
|
// This handles re-dispatch scenarios (DB/disk state divergence) where a prior
|
|
152
150
|
// completion already wrote the file. Overwriting would silently destroy the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
1
|
import { existsSync, unlinkSync } from "node:fs";
|
|
3
|
-
import {
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { relSliceFile, resolveMilestoneFile, resolveMilestonePath, targetMilestoneFile } from "../paths.js";
|
|
4
4
|
import { clearParseCache } from "../files.js";
|
|
5
5
|
import { isClosedStatus } from "../status-guards.js";
|
|
6
6
|
import { isNonEmptyString } from "../validation.js";
|
|
@@ -225,14 +225,20 @@ export async function handleReassessRoadmap(rawParams, basePath) {
|
|
|
225
225
|
params.sliceChanges.modified.length > 0 ||
|
|
226
226
|
params.sliceChanges.removed.length > 0;
|
|
227
227
|
if (hasStructuralChanges) {
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
228
|
+
const milestoneDir = resolveMilestonePath(basePath, params.milestoneId);
|
|
229
|
+
const validationFiles = new Set([
|
|
230
|
+
resolveMilestoneFile(basePath, params.milestoneId, "VALIDATION"),
|
|
231
|
+
targetMilestoneFile(basePath, params.milestoneId, "VALIDATION", getMilestone(params.milestoneId)?.title),
|
|
232
|
+
milestoneDir ? join(milestoneDir, `${params.milestoneId}-VALIDATION.md`) : null,
|
|
233
|
+
].filter((file) => Boolean(file)));
|
|
234
|
+
for (const validationFile of validationFiles) {
|
|
235
|
+
try {
|
|
236
|
+
if (existsSync(validationFile))
|
|
237
|
+
unlinkSync(validationFile);
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
logWarning("tool", `validation file cleanup failed: ${e.message}`);
|
|
241
|
+
}
|
|
236
242
|
}
|
|
237
243
|
}
|
|
238
244
|
// ── Invalidate caches ─────────────────────────────────────────
|
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
* were never persisted, causing M002+ milestones to have zero gate records
|
|
11
11
|
* despite passing validation.
|
|
12
12
|
*/
|
|
13
|
-
import { join } from "node:path";
|
|
14
13
|
import { transaction, insertAssessment, getMilestoneSlices, getMilestone, } from "../gsd-db.js";
|
|
15
|
-
import { clearPathCache,
|
|
14
|
+
import { clearPathCache, targetMilestoneFile } from "../paths.js";
|
|
16
15
|
import { resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
|
|
17
16
|
import { resolveWorktreeProjectRoot } from "../worktree-root.js";
|
|
18
17
|
import { saveFile, clearParseCache } from "../files.js";
|
|
@@ -113,11 +112,7 @@ export async function handleValidateMilestone(params, basePath, opts) {
|
|
|
113
112
|
// worktree exists for this milestone, validation reads/writes the
|
|
114
113
|
// worktree's artifacts instead of stale project-root state.
|
|
115
114
|
const validationMd = renderValidationMarkdown(effectiveParams);
|
|
116
|
-
|
|
117
|
-
// legacy → milestones/MID/MID-VALIDATION.md.
|
|
118
|
-
// join(artifactBasePath, relMilestoneFile) gives the absolute path; relMilestoneFile
|
|
119
|
-
// returns ".gsd/..."-relative so joining with basePath (project/worktree root) is correct.
|
|
120
|
-
const validationPath = join(artifactBasePath, relMilestoneFile(artifactBasePath, effectiveParams.milestoneId, "VALIDATION"));
|
|
115
|
+
const validationPath = targetMilestoneFile(artifactBasePath, effectiveParams.milestoneId, "VALIDATION", getMilestone(effectiveParams.milestoneId)?.title);
|
|
121
116
|
// ── DB write first — matches complete-task/complete-slice pattern ───
|
|
122
117
|
// Write DB before disk so a crash between the two leaves a recoverable
|
|
123
118
|
// state: the DB row exists but the file is missing, which projection
|
|
@@ -148,7 +143,7 @@ export async function handleValidateMilestone(params, basePath, opts) {
|
|
|
148
143
|
const projectRoot = resolveWorktreeProjectRoot(basePath);
|
|
149
144
|
if (projectRoot !== artifactBasePath) {
|
|
150
145
|
// Mirror to project root using the project root's layout (same logic as above).
|
|
151
|
-
const projectValidationPath =
|
|
146
|
+
const projectValidationPath = targetMilestoneFile(projectRoot, effectiveParams.milestoneId, "VALIDATION", getMilestone(effectiveParams.milestoneId)?.title);
|
|
152
147
|
try {
|
|
153
148
|
await saveFile(projectValidationPath, validationMd);
|
|
154
149
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Resolve whether a completed slice needs run-uat dispatch.
|
|
3
|
+
import { loadFile } from "./files.js";
|
|
4
|
+
import { resolveSliceFile } from "./paths.js";
|
|
5
|
+
import { classifyUatContentForRun, shouldDispatchUatForContent, } from "./uat-policy.js";
|
|
6
|
+
import { hasVerdict } from "./verdict-parser.js";
|
|
7
|
+
import { logWarning } from "./workflow-logger.js";
|
|
8
|
+
async function loadSliceFileContent(base, milestoneId, sliceId, kind) {
|
|
9
|
+
const filePath = resolveSliceFile(base, milestoneId, sliceId, kind);
|
|
10
|
+
return filePath ? ((await loadFile(filePath)) ?? "") : "";
|
|
11
|
+
}
|
|
12
|
+
async function resolveRunUatEffectiveType(base, milestoneId, sliceId, uatContent) {
|
|
13
|
+
let summaryContent = "";
|
|
14
|
+
try {
|
|
15
|
+
summaryContent = await loadSliceFileContent(base, milestoneId, sliceId, "SUMMARY");
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
logWarning("prompt", `resolveRunUatEffectiveType SUMMARY load failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
19
|
+
}
|
|
20
|
+
return classifyUatContentForRun(uatContent, summaryContent).effectiveType;
|
|
21
|
+
}
|
|
22
|
+
async function resolveCandidateRunUatDispatch(base, milestoneId, candidate, prefs) {
|
|
23
|
+
const uatContent = await loadSliceFileContent(base, milestoneId, candidate.sliceId, "UAT");
|
|
24
|
+
if (!uatContent)
|
|
25
|
+
return null;
|
|
26
|
+
if (hasVerdict(uatContent))
|
|
27
|
+
return null;
|
|
28
|
+
const assessmentContent = await loadSliceFileContent(base, milestoneId, candidate.sliceId, "ASSESSMENT");
|
|
29
|
+
if (assessmentContent && hasVerdict(assessmentContent))
|
|
30
|
+
return null;
|
|
31
|
+
if (!shouldDispatchUatForContent(uatContent, prefs))
|
|
32
|
+
return null;
|
|
33
|
+
return {
|
|
34
|
+
sliceId: candidate.sliceId,
|
|
35
|
+
uatType: await resolveRunUatEffectiveType(base, milestoneId, candidate.sliceId, uatContent),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function getDbCompletedSliceCandidates(milestoneId) {
|
|
39
|
+
const { isDbAvailable, getMilestoneSlices } = await import("./gsd-db.js");
|
|
40
|
+
if (!isDbAvailable())
|
|
41
|
+
return null;
|
|
42
|
+
const slices = getMilestoneSlices(milestoneId);
|
|
43
|
+
// No DB slice rows for this milestone: the DB has no authoritative view, so
|
|
44
|
+
// return null to let the caller defer to the roadmap fallback.
|
|
45
|
+
if (slices.length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
// The DB has slice rows and is therefore authoritative for this milestone.
|
|
48
|
+
// Return a (possibly empty) array rather than null: an empty result must NOT
|
|
49
|
+
// fall through to the roadmap fallback, because the DB, not stale roadmap
|
|
50
|
+
// checkboxes, is the source of truth for which slices are complete.
|
|
51
|
+
return slices
|
|
52
|
+
.filter((slice) => slice.status === "complete")
|
|
53
|
+
.map((slice) => ({ sliceId: slice.id }))
|
|
54
|
+
.reverse();
|
|
55
|
+
}
|
|
56
|
+
export async function findRunUatDispatchFromCandidates(base, milestoneId, candidates, prefs) {
|
|
57
|
+
for (const candidate of candidates) {
|
|
58
|
+
const dispatch = await resolveCandidateRunUatDispatch(base, milestoneId, candidate, prefs);
|
|
59
|
+
if (dispatch)
|
|
60
|
+
return dispatch;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if the most recently completed slice needs a UAT run.
|
|
66
|
+
* Returns { sliceId, uatType } if UAT should be dispatched, null otherwise.
|
|
67
|
+
*
|
|
68
|
+
* When the DB has slice rows for the milestone it is authoritative: only its
|
|
69
|
+
* completed slices are considered and the `fallbackCandidates` (roadmap-derived)
|
|
70
|
+
* are ignored, even when no DB slice is complete. The fallback candidates are
|
|
71
|
+
* only consulted when the DB has no slice rows for the milestone, is
|
|
72
|
+
* unavailable, or errors.
|
|
73
|
+
*
|
|
74
|
+
* Skips when:
|
|
75
|
+
* - No completed slices exist in DB or the caller-provided fallback candidates
|
|
76
|
+
* - uat_dispatch is not enabled and the UAT spec does not require runtime/browser evidence
|
|
77
|
+
* - No UAT file exists for the slice
|
|
78
|
+
* - UAT result already exists in the UAT or ASSESSMENT file
|
|
79
|
+
*/
|
|
80
|
+
export async function checkNeedsRunUat(base, milestoneId, prefs, fallbackCandidates = []) {
|
|
81
|
+
try {
|
|
82
|
+
const dbCandidates = await getDbCompletedSliceCandidates(milestoneId);
|
|
83
|
+
// A non-null result (including an empty array) means the DB is authoritative
|
|
84
|
+
// for this milestone; only fall through to the roadmap fallback when the DB
|
|
85
|
+
// has no slice rows at all (null).
|
|
86
|
+
if (dbCandidates !== null) {
|
|
87
|
+
return findRunUatDispatchFromCandidates(base, milestoneId, dbCandidates, prefs);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
logWarning("prompt", `checkNeedsRunUat DB lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
92
|
+
}
|
|
93
|
+
return findRunUatDispatchFromCandidates(base, milestoneId, fallbackCandidates, prefs);
|
|
94
|
+
}
|
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
const AUDIT_ENV_KEY = "GSD_UOK_AUDIT_UNIFIED";
|
|
2
|
+
const suppressedBasePaths = new Set();
|
|
2
3
|
export function setUnifiedAuditEnabled(enabled) {
|
|
3
4
|
process.env[AUDIT_ENV_KEY] = enabled ? "1" : "0";
|
|
4
5
|
}
|
|
5
|
-
export function
|
|
6
|
-
|
|
6
|
+
export function setUnifiedAuditSuppressedForBasePath(basePath, suppressed) {
|
|
7
|
+
if (suppressed) {
|
|
8
|
+
suppressedBasePaths.add(basePath);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
suppressedBasePaths.delete(basePath);
|
|
12
|
+
}
|
|
13
|
+
export function isUnifiedAuditEnabled(basePath) {
|
|
14
|
+
if (basePath && suppressedBasePaths.has(basePath))
|
|
15
|
+
return false;
|
|
16
|
+
return process.env[AUDIT_ENV_KEY] !== "0";
|
|
17
|
+
}
|
|
18
|
+
export function getUnifiedAuditOverride() {
|
|
19
|
+
const raw = process.env[AUDIT_ENV_KEY];
|
|
20
|
+
if (raw === "0")
|
|
21
|
+
return false;
|
|
22
|
+
if (raw === "1")
|
|
23
|
+
return true;
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
export function restoreUnifiedAuditOverride(override) {
|
|
27
|
+
if (override === undefined) {
|
|
28
|
+
delete process.env[AUDIT_ENV_KEY];
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
setUnifiedAuditEnabled(override);
|
|
32
|
+
}
|
|
33
|
+
export function clearUnifiedAuditOverrideForTests() {
|
|
34
|
+
delete process.env[AUDIT_ENV_KEY];
|
|
35
|
+
suppressedBasePaths.clear();
|
|
7
36
|
}
|
|
@@ -7,6 +7,7 @@ import { withFileLockSync } from "../file-lock.js";
|
|
|
7
7
|
import { gsdRoot } from "../paths.js";
|
|
8
8
|
import { isDbAvailable, insertAuditEvent } from "../gsd-db.js";
|
|
9
9
|
import { CURRENT_UOK_CONTRACT_VERSION, validateAuditEvent } from "./contracts.js";
|
|
10
|
+
import { isUnifiedAuditEnabled } from "./audit-toggle.js";
|
|
10
11
|
function auditLogPath(basePath) {
|
|
11
12
|
return join(gsdRoot(basePath), "audit", "events.jsonl");
|
|
12
13
|
}
|
|
@@ -30,6 +31,8 @@ export function emitUokAuditEvent(basePath, event) {
|
|
|
30
31
|
// Drop writes from a turn superseded by timeout recovery / cancellation.
|
|
31
32
|
if (isStaleWrite("uok-audit"))
|
|
32
33
|
return;
|
|
34
|
+
if (!isUnifiedAuditEnabled(basePath))
|
|
35
|
+
return;
|
|
33
36
|
const validation = validateAuditEvent(event);
|
|
34
37
|
if (!validation.ok) {
|
|
35
38
|
throw new Error(`Invalid UOK audit event: ${validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|