@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
|
@@ -9,7 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
10
|
import test from "node:test";
|
|
11
11
|
|
|
12
|
-
import { relMilestoneFile, resolveMilestoneFile } from "../paths.ts";
|
|
12
|
+
import { normalizeRealPath, relMilestoneFile, relSliceFile, resolveMilestoneFile, targetMilestoneFile } from "../paths.ts";
|
|
13
13
|
|
|
14
14
|
function makeFlatPhaseFixture(): { basePath: string; cleanup: () => void } {
|
|
15
15
|
const basePath = mkdtempSync(join(tmpdir(), "flat-phase-validation-"));
|
|
@@ -33,42 +33,82 @@ function makeFlatPhaseFixture(): { basePath: string; cleanup: () => void } {
|
|
|
33
33
|
return { basePath, cleanup: () => rmSync(basePath, { recursive: true, force: true }) };
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
test("flat-phase: relMilestoneFile resolves to the layout-aware path", () => {
|
|
36
|
+
test("flat-phase: relMilestoneFile resolves to the layout-aware path", (t) => {
|
|
37
37
|
const { basePath, cleanup } = makeFlatPhaseFixture();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
t.after(cleanup);
|
|
39
|
+
|
|
40
|
+
const rel = relMilestoneFile(basePath, "M001", "VALIDATION");
|
|
41
|
+
assert.equal(rel, ".gsd/phases/01-foo/01-VALIDATION.md");
|
|
42
|
+
const abs = join(basePath, rel);
|
|
43
|
+
assert.equal(existsSync(abs), true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("flat-phase: relMilestoneFile fallback uses the milestone title when no directory exists", (t) => {
|
|
47
|
+
const basePath = mkdtempSync(join(tmpdir(), "flat-phase-missing-dir-"));
|
|
48
|
+
t.after(() => rmSync(basePath, { recursive: true, force: true }));
|
|
49
|
+
|
|
50
|
+
const rel = relMilestoneFile(basePath, "M001", "ROADMAP", "Milestone");
|
|
51
|
+
assert.equal(rel, ".gsd/phases/01-milestone/01-ROADMAP.md");
|
|
46
52
|
});
|
|
47
53
|
|
|
48
|
-
test("
|
|
54
|
+
test("legacy layout: relMilestoneFile fallback stays in milestones when the target dir is missing", (t) => {
|
|
55
|
+
const basePath = mkdtempSync(join(tmpdir(), "legacy-missing-dir-"));
|
|
56
|
+
t.after(() => rmSync(basePath, { recursive: true, force: true }));
|
|
57
|
+
const existingLegacyDir = join(basePath, ".gsd", "milestones", "M002");
|
|
58
|
+
mkdirSync(existingLegacyDir, { recursive: true });
|
|
59
|
+
writeFileSync(join(existingLegacyDir, "M002-ROADMAP.md"), "# existing legacy roadmap\n");
|
|
60
|
+
|
|
61
|
+
const rel = relMilestoneFile(basePath, "M001", "ROADMAP", "Milestone");
|
|
62
|
+
|
|
63
|
+
assert.equal(rel, ".gsd/milestones/M001/M001-ROADMAP.md");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("legacy layout: relSliceFile fallback uses slices directory when target milestone is missing", (t) => {
|
|
67
|
+
const basePath = mkdtempSync(join(tmpdir(), "legacy-missing-slice-"));
|
|
68
|
+
t.after(() => rmSync(basePath, { recursive: true, force: true }));
|
|
69
|
+
const existingLegacyDir = join(basePath, ".gsd", "milestones", "M002");
|
|
70
|
+
mkdirSync(existingLegacyDir, { recursive: true });
|
|
71
|
+
writeFileSync(join(existingLegacyDir, "M002-ROADMAP.md"), "# existing legacy roadmap\n");
|
|
72
|
+
|
|
73
|
+
const rel = relSliceFile(basePath, "M001", "S01", "PLAN", "Milestone");
|
|
74
|
+
|
|
75
|
+
assert.equal(rel, ".gsd/milestones/M001/slices/S01/S01-PLAN.md");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("flat-phase: targetMilestoneFile ignores legacy-named compatibility files when writing", (t) => {
|
|
79
|
+
const basePath = mkdtempSync(join(tmpdir(), "flat-phase-legacy-named-file-"));
|
|
80
|
+
t.after(() => rmSync(basePath, { recursive: true, force: true }));
|
|
81
|
+
const phaseDir = join(basePath, ".gsd", "phases", "01-foo");
|
|
82
|
+
mkdirSync(phaseDir, { recursive: true });
|
|
83
|
+
writeFileSync(join(phaseDir, "M001-ROADMAP.md"), "# legacy named roadmap\n");
|
|
84
|
+
|
|
85
|
+
assert.equal(
|
|
86
|
+
targetMilestoneFile(basePath, "M001", "ROADMAP", "Foo"),
|
|
87
|
+
join(normalizeRealPath(phaseDir), "01-ROADMAP.md"),
|
|
88
|
+
);
|
|
89
|
+
assert.equal(relMilestoneFile(basePath, "M001", "ROADMAP", "Foo"), ".gsd/phases/01-foo/M001-ROADMAP.md");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("flat-phase: resolveMilestoneFile finds the on-disk file", (t) => {
|
|
49
93
|
const { basePath, cleanup } = makeFlatPhaseFixture();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
cleanup();
|
|
57
|
-
}
|
|
94
|
+
t.after(cleanup);
|
|
95
|
+
|
|
96
|
+
const abs = resolveMilestoneFile(basePath, "M001", "VALIDATION");
|
|
97
|
+
assert.ok(abs, "resolveMilestoneFile must return a path for the flat-phase fixture");
|
|
98
|
+
assert.equal(abs!.endsWith("/.gsd/phases/01-foo/01-VALIDATION.md"), true);
|
|
99
|
+
assert.equal(existsSync(abs!), true);
|
|
58
100
|
});
|
|
59
101
|
|
|
60
|
-
test("flat-phase: the legacy hardcoded path does NOT resolve (regression for #876)", () => {
|
|
102
|
+
test("flat-phase: the legacy hardcoded path does NOT resolve (regression for #876)", (t) => {
|
|
61
103
|
const { basePath, cleanup } = makeFlatPhaseFixture();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
cleanup();
|
|
73
|
-
}
|
|
104
|
+
t.after(cleanup);
|
|
105
|
+
|
|
106
|
+
// This is the path that auto-verification.ts:264-270 used to construct.
|
|
107
|
+
// It must NOT exist on a flat-phase project — that's the whole bug.
|
|
108
|
+
const legacyHardcoded = join(basePath, ".gsd", "milestones", "M001", "M001-VALIDATION.md");
|
|
109
|
+
assert.equal(
|
|
110
|
+
existsSync(legacyHardcoded),
|
|
111
|
+
false,
|
|
112
|
+
"legacy hardcoded path must not exist in flat-phase fixture — if it does, the test fixture is wrong",
|
|
113
|
+
);
|
|
74
114
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, mkdirSync, rmSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, realpathSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
gsdProjectionRoot,
|
|
10
10
|
gsdRoot,
|
|
11
11
|
milestonesDir,
|
|
12
|
+
resolveGsdPathContract,
|
|
12
13
|
resolveSliceFile,
|
|
13
14
|
resolveTaskFile,
|
|
14
15
|
_clearGsdRootCache,
|
|
@@ -166,4 +167,90 @@ describe('paths', () => {
|
|
|
166
167
|
cleanup(root);
|
|
167
168
|
}
|
|
168
169
|
});
|
|
170
|
+
|
|
171
|
+
test('Case 9: flat-phase task SUMMARY resolves at phase root despite a stray slices/SID/ dir', () => {
|
|
172
|
+
const root = tmp();
|
|
173
|
+
try {
|
|
174
|
+
// Hybrid flat-phase milestone: phase dir holds flat TID-SUMMARY.md, but a
|
|
175
|
+
// stray slices/S01/ folder also exists on disk. resolveSlicePath then points
|
|
176
|
+
// under slices/, so layout (phases/ vs milestones/) — not slicePath===phaseDir
|
|
177
|
+
// — must decide the flat-phase task summary location.
|
|
178
|
+
const phaseDir = join(root, ".gsd", "phases", "01-foo");
|
|
179
|
+
mkdirSync(join(phaseDir, "slices", "S01"), { recursive: true });
|
|
180
|
+
const flatSummary = join(phaseDir, "T01-SUMMARY.md");
|
|
181
|
+
writeFileSync(flatSummary, "# task summary\n");
|
|
182
|
+
|
|
183
|
+
_clearGsdRootCache();
|
|
184
|
+
|
|
185
|
+
assert.deepStrictEqual(
|
|
186
|
+
resolveTaskFile(root, "M001", "S01", "T01", "SUMMARY"),
|
|
187
|
+
flatSummary,
|
|
188
|
+
"flat-phase TID-SUMMARY.md at phase root is found even when slices/S01/ exists",
|
|
189
|
+
);
|
|
190
|
+
} finally { cleanup(root); }
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('Case 10: resolveGsdPathContract canonicalizes a symlinked .gsd root (regression #1239)', () => {
|
|
194
|
+
// WSL / external-state shape: `<projectRoot>/.gsd` is a symlink pointing at a
|
|
195
|
+
// real directory on another volume. Before #1239 the DB path was built from
|
|
196
|
+
// the unresolved symlink, so the workflow SQLite handle opened through a
|
|
197
|
+
// move-prone path and could pick the wrong journal mode / SQLITE_READONLY_DBMOVED.
|
|
198
|
+
const root = tmp();
|
|
199
|
+
try {
|
|
200
|
+
const realState = join(root, "real-state"); // canonical symlink target
|
|
201
|
+
mkdirSync(join(realState, "milestones"), { recursive: true });
|
|
202
|
+
const projectRoot = join(root, "project");
|
|
203
|
+
mkdirSync(projectRoot, { recursive: true });
|
|
204
|
+
symlinkSync(realState, join(projectRoot, ".gsd"), "dir");
|
|
205
|
+
|
|
206
|
+
_clearGsdRootCache();
|
|
207
|
+
const contract = resolveGsdPathContract(projectRoot);
|
|
208
|
+
|
|
209
|
+
const canonicalGsd = realpathSync.native(join(projectRoot, ".gsd"));
|
|
210
|
+
assert.deepStrictEqual(contract.projectGsd, canonicalGsd,
|
|
211
|
+
"projectGsd is the realpath'd target, not the unresolved .gsd symlink");
|
|
212
|
+
assert.deepStrictEqual(contract.projectDb, join(canonicalGsd, "gsd.db"),
|
|
213
|
+
"projectDb is built from the canonical .gsd root");
|
|
214
|
+
assert.deepStrictEqual(contract.projectGsd, gsdProjectionRoot(projectRoot),
|
|
215
|
+
"projectGsd agrees with gsdProjectionRoot on the canonical root");
|
|
216
|
+
assert.notStrictEqual(contract.projectDb, join(projectRoot, ".gsd", "gsd.db"),
|
|
217
|
+
"regression #1239: DB must not open through the unresolved .gsd symlink");
|
|
218
|
+
} finally { cleanup(root); }
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('Case 11: resolveGsdPathContract canonicalizes the external-state worktree DB path through a symlinked store (regression #1239)', () => {
|
|
222
|
+
const root = tmp();
|
|
223
|
+
const originalStateDir = process.env.GSD_STATE_DIR;
|
|
224
|
+
try {
|
|
225
|
+
const stateDir = join(root, "state");
|
|
226
|
+
process.env.GSD_STATE_DIR = stateDir;
|
|
227
|
+
|
|
228
|
+
// The external-state project store `<state>/projects/<hash>` is itself a
|
|
229
|
+
// symlink to a real directory on another volume (the WSL `.gsd` shape).
|
|
230
|
+
const realProjectGsd = join(root, "real-projects", "abc123");
|
|
231
|
+
mkdirSync(realProjectGsd, { recursive: true });
|
|
232
|
+
mkdirSync(join(stateDir, "projects"), { recursive: true });
|
|
233
|
+
const storeSymlink = join(stateDir, "projects", "abc123");
|
|
234
|
+
symlinkSync(realProjectGsd, storeSymlink, "dir");
|
|
235
|
+
|
|
236
|
+
const wtRoot = join(storeSymlink, "worktrees", "M002");
|
|
237
|
+
mkdirSync(join(wtRoot, ".gsd"), { recursive: true });
|
|
238
|
+
|
|
239
|
+
_clearGsdRootCache();
|
|
240
|
+
const contract = resolveGsdPathContract(wtRoot);
|
|
241
|
+
|
|
242
|
+
const canonicalStore = realpathSync.native(storeSymlink);
|
|
243
|
+
assert.ok(contract.isWorktree, "recognized as an external-state worktree layout");
|
|
244
|
+
assert.deepStrictEqual(contract.projectGsd, canonicalStore,
|
|
245
|
+
"projectGsd is the realpath'd project store, not the unresolved symlink");
|
|
246
|
+
assert.deepStrictEqual(contract.projectDb, join(canonicalStore, "gsd.db"),
|
|
247
|
+
"projectDb is built from the canonical project store");
|
|
248
|
+
assert.notStrictEqual(contract.projectDb, join(storeSymlink, "gsd.db"),
|
|
249
|
+
"regression #1239: DB must not open through the unresolved external-state symlink");
|
|
250
|
+
} finally {
|
|
251
|
+
if (originalStateDir === undefined) delete process.env.GSD_STATE_DIR;
|
|
252
|
+
else process.env.GSD_STATE_DIR = originalStateDir;
|
|
253
|
+
cleanup(root);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
169
256
|
});
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
isCompletedMilestoneTerminal,
|
|
14
14
|
repairMissingMilestoneSummaryProjection,
|
|
15
15
|
} from "../milestone-closeout.js";
|
|
16
|
+
import { targetMilestoneFile } from "../paths.js";
|
|
16
17
|
import type { DispatchContext } from "../auto-dispatch.js";
|
|
17
18
|
|
|
18
19
|
/** Build a minimal DispatchContext for the dispatch-policy branches under test. */
|
|
@@ -196,7 +197,7 @@ test("repairMissingMilestoneSummaryProjection succeeds when milestone dir does n
|
|
|
196
197
|
const repair = await repairMissingMilestoneSummaryProjection(base, "M042");
|
|
197
198
|
assert.equal(repair.ok, true, "repair should report success when handler creates the SUMMARY");
|
|
198
199
|
assert.ok(
|
|
199
|
-
existsSync(
|
|
200
|
+
existsSync(targetMilestoneFile(base, "M042", "SUMMARY", "Done")),
|
|
200
201
|
"repair should write the SUMMARY artifact to the canonical projection path",
|
|
201
202
|
);
|
|
202
203
|
});
|
|
@@ -65,7 +65,6 @@ const ALLOWED_IMPORTERS = new Set([
|
|
|
65
65
|
"gsd/visualizer-data.ts",
|
|
66
66
|
// prompt context text (display strings injected into unit prompts)
|
|
67
67
|
"gsd/auto-prompts.ts",
|
|
68
|
-
// cold-path maintenance command (branch cleanup messaging)
|
|
69
68
|
"gsd/commands-maintenance.ts",
|
|
70
69
|
// display-only GitHub issue/PR body sync
|
|
71
70
|
"github-sync/sync.ts",
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
runPreDispatchHooks,
|
|
17
17
|
persistHookState,
|
|
18
18
|
restoreHookState,
|
|
19
|
+
reconcileRestoredHookDispatch,
|
|
19
20
|
clearPersistedHookState,
|
|
20
21
|
getHookStatus,
|
|
21
22
|
formatHookStatus,
|
|
@@ -225,6 +226,233 @@ test('Blocking hook restored from disk does not trust artifact without clean hoo
|
|
|
225
226
|
}
|
|
226
227
|
});
|
|
227
228
|
|
|
229
|
+
test('Restore reconciliation re-enqueues the lost hook dispatch (#1246)', () => {
|
|
230
|
+
resetHookState();
|
|
231
|
+
const base = createFixtureBase();
|
|
232
|
+
try {
|
|
233
|
+
writeHookPreferences(base, ` - name: plan-review
|
|
234
|
+
after:
|
|
235
|
+
- plan-slice
|
|
236
|
+
prompt: Review the plan for {milestoneId}/{sliceId}
|
|
237
|
+
artifact: PLAN-REVIEW.md
|
|
238
|
+
criticality: blocking
|
|
239
|
+
max_cycles: 2
|
|
240
|
+
`);
|
|
241
|
+
// Trigger unit completes: activeHook set + persisted, dispatch enqueued.
|
|
242
|
+
const dispatch = checkPostUnitHooks("plan-slice", "M002/S01", base);
|
|
243
|
+
assert.ok(dispatch, "gate dispatches on trigger unit completion");
|
|
244
|
+
assert.equal(dispatch.unitType, "hook/plan-review");
|
|
245
|
+
persistHookState(base);
|
|
246
|
+
|
|
247
|
+
// Pause/resume: activeHook restored, but the session-local sidecar queue is
|
|
248
|
+
// gone (never persisted).
|
|
249
|
+
resetHookState();
|
|
250
|
+
restoreHookState(base);
|
|
251
|
+
assert.ok(getActiveHook(), "activeHook restored from disk");
|
|
252
|
+
|
|
253
|
+
// Reconciliation re-enqueues the missing dispatch so the hook actually runs.
|
|
254
|
+
const sidecarQueue: any[] = [];
|
|
255
|
+
reconcileRestoredHookDispatch(base, sidecarQueue);
|
|
256
|
+
assert.equal(sidecarQueue.length, 1, "lost hook dispatch is re-enqueued");
|
|
257
|
+
assert.equal(sidecarQueue[0].kind, "hook");
|
|
258
|
+
assert.equal(sidecarQueue[0].unitType, "hook/plan-review");
|
|
259
|
+
assert.equal(sidecarQueue[0].unitId, "M002/S01");
|
|
260
|
+
assert.match(sidecarQueue[0].prompt, /Review the plan for M002\/S01/);
|
|
261
|
+
} finally {
|
|
262
|
+
resetHookState();
|
|
263
|
+
invalidateAllCaches();
|
|
264
|
+
rmSync(base, { recursive: true, force: true });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('Restore reconciliation is a no-op when the dispatch is already queued (#1246)', () => {
|
|
269
|
+
resetHookState();
|
|
270
|
+
const base = createFixtureBase();
|
|
271
|
+
try {
|
|
272
|
+
writeHookPreferences(base, ` - name: plan-review
|
|
273
|
+
after:
|
|
274
|
+
- plan-slice
|
|
275
|
+
prompt: Review the plan
|
|
276
|
+
artifact: PLAN-REVIEW.md
|
|
277
|
+
criticality: blocking
|
|
278
|
+
max_cycles: 2
|
|
279
|
+
`);
|
|
280
|
+
const dispatch = checkPostUnitHooks("plan-slice", "M002/S01", base);
|
|
281
|
+
assert.ok(dispatch, "gate dispatches on trigger unit completion");
|
|
282
|
+
persistHookState(base);
|
|
283
|
+
resetHookState();
|
|
284
|
+
restoreHookState(base);
|
|
285
|
+
|
|
286
|
+
const sidecarQueue: any[] = [
|
|
287
|
+
{ kind: "hook", unitType: "hook/plan-review", unitId: "M002/S01", prompt: "already here" },
|
|
288
|
+
];
|
|
289
|
+
reconcileRestoredHookDispatch(base, sidecarQueue);
|
|
290
|
+
assert.equal(sidecarQueue.length, 1, "does not duplicate an existing hook dispatch");
|
|
291
|
+
assert.equal(sidecarQueue[0].prompt, "already here");
|
|
292
|
+
} finally {
|
|
293
|
+
resetHookState();
|
|
294
|
+
invalidateAllCaches();
|
|
295
|
+
rmSync(base, { recursive: true, force: true });
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('Restore reconciliation is a no-op with no active hook (#1246)', () => {
|
|
300
|
+
resetHookState();
|
|
301
|
+
const base = createFixtureBase();
|
|
302
|
+
try {
|
|
303
|
+
const sidecarQueue: any[] = [];
|
|
304
|
+
reconcileRestoredHookDispatch(base, sidecarQueue);
|
|
305
|
+
assert.equal(sidecarQueue.length, 0, "nothing enqueued when no active hook");
|
|
306
|
+
} finally {
|
|
307
|
+
resetHookState();
|
|
308
|
+
invalidateAllCaches();
|
|
309
|
+
rmSync(base, { recursive: true, force: true });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('Blocking hook re-dispatches once after a lost dispatch at default max_cycles', () => {
|
|
314
|
+
resetHookState();
|
|
315
|
+
const base = createFixtureBase();
|
|
316
|
+
try {
|
|
317
|
+
// Default max_cycles (1): a cycle is charged at dispatch time.
|
|
318
|
+
writeHookPreferences(base, ` - name: plan-review
|
|
319
|
+
after:
|
|
320
|
+
- plan-slice
|
|
321
|
+
prompt: Review plan
|
|
322
|
+
artifact: PLAN-REVIEW.md
|
|
323
|
+
criticality: blocking
|
|
324
|
+
`);
|
|
325
|
+
|
|
326
|
+
const firstDispatch = checkPostUnitHooks("plan-slice", "M002/S01", base);
|
|
327
|
+
assert.ok(firstDispatch, "gate dispatches and consumes its cycle at dispatch");
|
|
328
|
+
assert.equal(firstDispatch.unitType, "hook/plan-review");
|
|
329
|
+
persistHookState(base);
|
|
330
|
+
|
|
331
|
+
// Simulate a lost dispatch: the hook never produced its own unit-end, the
|
|
332
|
+
// trigger unit resumes instead. The dispatch-consumed cycle is refunded once
|
|
333
|
+
// so the gate re-dispatches rather than hard-blocking on a hook that never ran.
|
|
334
|
+
resetHookState();
|
|
335
|
+
restoreHookState(base);
|
|
336
|
+
const redispatch = checkPostUnitHooks("plan-slice", "M002/S01", base);
|
|
337
|
+
assert.ok(redispatch, "lost dispatch re-dispatches the gate instead of blocking");
|
|
338
|
+
assert.equal(redispatch.unitType, "hook/plan-review");
|
|
339
|
+
assert.deepStrictEqual(consumeGateBlock(), null, "no gate block on the first lost dispatch");
|
|
340
|
+
persistHookState(base);
|
|
341
|
+
|
|
342
|
+
// A second lost dispatch consumes the budget and blocks — the refund is one-shot.
|
|
343
|
+
resetHookState();
|
|
344
|
+
restoreHookState(base);
|
|
345
|
+
const blocked = checkPostUnitHooks("plan-slice", "M002/S01", base);
|
|
346
|
+
assert.deepStrictEqual(blocked, null, "second lost dispatch exhausts the budget");
|
|
347
|
+
const block = consumeGateBlock();
|
|
348
|
+
assert.ok(block, "gate block recorded after the one-shot re-dispatch is used");
|
|
349
|
+
assert.equal(block.hookName, "plan-review");
|
|
350
|
+
assert.match(block.reason, /gate cycle budget exhausted/);
|
|
351
|
+
} finally {
|
|
352
|
+
resetHookState();
|
|
353
|
+
invalidateAllCaches();
|
|
354
|
+
rmSync(base, { recursive: true, force: true });
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('Blocking hook lost-dispatch refund is one-shot and bounded at max_cycles=2', () => {
|
|
359
|
+
resetHookState();
|
|
360
|
+
const base = createFixtureBase();
|
|
361
|
+
try {
|
|
362
|
+
writeHookPreferences(base, ` - name: plan-review
|
|
363
|
+
after:
|
|
364
|
+
- plan-slice
|
|
365
|
+
prompt: Review plan
|
|
366
|
+
artifact: PLAN-REVIEW.md
|
|
367
|
+
criticality: blocking
|
|
368
|
+
max_cycles: 2
|
|
369
|
+
`);
|
|
370
|
+
|
|
371
|
+
// Simulate a lost dispatch: persist, drop in-memory state, restore, then the
|
|
372
|
+
// trigger unit resumes instead of the hook producing its own unit-end.
|
|
373
|
+
const lose = () => {
|
|
374
|
+
persistHookState(base);
|
|
375
|
+
resetHookState();
|
|
376
|
+
restoreHookState(base);
|
|
377
|
+
return checkPostUnitHooks("plan-slice", "M002/S01", base);
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
assert.ok(checkPostUnitHooks("plan-slice", "M002/S01", base), "initial dispatch");
|
|
381
|
+
|
|
382
|
+
// Refund is one-shot, but max_cycles=2 still has real budget, so lost#2 also
|
|
383
|
+
// re-dispatches by consuming a genuine cycle (not another refund).
|
|
384
|
+
assert.ok(lose(), "lost#1 re-dispatches (one-shot refund)");
|
|
385
|
+
assert.equal(consumeGateBlock(), null, "no block after lost#1");
|
|
386
|
+
assert.ok(lose(), "lost#2 re-dispatches from real budget");
|
|
387
|
+
assert.equal(consumeGateBlock(), null, "no block after lost#2");
|
|
388
|
+
|
|
389
|
+
// Total dispatches are bounded to max_cycles + 1 (the single forgiven loss),
|
|
390
|
+
// so the third lost dispatch blocks. redispatchedGateKeys guarantees this
|
|
391
|
+
// terminates rather than looping forever on repeated lost resumes.
|
|
392
|
+
assert.equal(lose(), null, "lost#3 exhausts the bounded budget");
|
|
393
|
+
const block = consumeGateBlock();
|
|
394
|
+
assert.ok(block, "gate block recorded on lost#3");
|
|
395
|
+
assert.match(block.reason, /gate cycle budget exhausted/);
|
|
396
|
+
} finally {
|
|
397
|
+
resetHookState();
|
|
398
|
+
invalidateAllCaches();
|
|
399
|
+
rmSync(base, { recursive: true, force: true });
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('Blocking hook lost dispatch does not steal a real-run cycle at max_cycles=2', () => {
|
|
404
|
+
resetHookState();
|
|
405
|
+
const base = createFixtureBase();
|
|
406
|
+
try {
|
|
407
|
+
writeHookPreferences(base, ` - name: plan-review
|
|
408
|
+
after:
|
|
409
|
+
- plan-slice
|
|
410
|
+
prompt: Review plan
|
|
411
|
+
artifact: PLAN-REVIEW.md
|
|
412
|
+
criticality: blocking
|
|
413
|
+
max_cycles: 2
|
|
414
|
+
`);
|
|
415
|
+
|
|
416
|
+
// Dispatch, then lose it once (the one-shot refund is spent here).
|
|
417
|
+
assert.ok(checkPostUnitHooks("plan-slice", "M002/S01", base), "initial dispatch");
|
|
418
|
+
persistHookState(base);
|
|
419
|
+
resetHookState();
|
|
420
|
+
restoreHookState(base);
|
|
421
|
+
assert.ok(
|
|
422
|
+
checkPostUnitHooks("plan-slice", "M002/S01", base),
|
|
423
|
+
"lost dispatch re-dispatches (refund spent)",
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// The hook now actually runs, each run reporting a failed verdict. Because a
|
|
427
|
+
// lost dispatch must not consume the hook's real-run budget, max_cycles=2
|
|
428
|
+
// still permits two genuine runs before the gate blocks — spending the
|
|
429
|
+
// refund early did not rob a later real cycle.
|
|
430
|
+
mkdirSync(join(base, ".gsd", "milestones", "M002", "slices", "S01"), { recursive: true });
|
|
431
|
+
writeFileSync(
|
|
432
|
+
resolveHookArtifactPath(base, "M002/S01", "PLAN-REVIEW.md"),
|
|
433
|
+
"---\nverdict: failed\n---\n\nRejected.\n",
|
|
434
|
+
"utf-8",
|
|
435
|
+
);
|
|
436
|
+
assert.ok(
|
|
437
|
+
checkPostUnitHooks("hook/plan-review", "M002/S01", base),
|
|
438
|
+
"real run #1 (failed verdict) re-dispatches",
|
|
439
|
+
);
|
|
440
|
+
assert.equal(consumeGateBlock(), null, "no block after the first real run");
|
|
441
|
+
assert.equal(
|
|
442
|
+
checkPostUnitHooks("hook/plan-review", "M002/S01", base),
|
|
443
|
+
null,
|
|
444
|
+
"real run #2 (failed verdict) exhausts the real budget",
|
|
445
|
+
);
|
|
446
|
+
const block = consumeGateBlock();
|
|
447
|
+
assert.ok(block, "gate block recorded only after two real runs");
|
|
448
|
+
assert.match(block.reason, /gate cycle budget exhausted/);
|
|
449
|
+
} finally {
|
|
450
|
+
resetHookState();
|
|
451
|
+
invalidateAllCaches();
|
|
452
|
+
rmSync(base, { recursive: true, force: true });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
228
456
|
test('Blocking hook needs-rework verdict requests trigger unit retry', () => {
|
|
229
457
|
resetHookState();
|
|
230
458
|
const base = createFixtureBase();
|
|
@@ -454,4 +682,45 @@ test('triggerHookManually: with configured hook', () => {
|
|
|
454
682
|
}
|
|
455
683
|
});
|
|
456
684
|
|
|
685
|
+
// ─── Hook dispatch results omit a primary-only model (#1229) ───────────────
|
|
686
|
+
// Both the auto-mode (checkPostUnitHooks → _startHook) and manual
|
|
687
|
+
// (triggerHookManually) dispatch paths must NOT carry a resolved primary model
|
|
688
|
+
// on the dispatch result. Emitting the primary alone discards the configured
|
|
689
|
+
// fallbacks[] chain: the auto path would re-apply it over a fallback already
|
|
690
|
+
// selected by selectAndApplyModel, and the manual path (dispatchHookUnit) now
|
|
691
|
+
// resolves the full chain itself. The dispatch result therefore leaves `model`
|
|
692
|
+
// unset so downstream resolution honors fallbacks.
|
|
693
|
+
test('Dispatch results omit primary-only model so fallbacks survive (#1229)', () => {
|
|
694
|
+
resetHookState();
|
|
695
|
+
const base = createFixtureBase();
|
|
696
|
+
try {
|
|
697
|
+
writeHookPreferences(base, ` - name: code-review
|
|
698
|
+
after:
|
|
699
|
+
- execute-task
|
|
700
|
+
prompt: Review the change
|
|
701
|
+
artifact: REVIEW-PASS.md
|
|
702
|
+
model:
|
|
703
|
+
model: primary-model
|
|
704
|
+
fallbacks:
|
|
705
|
+
- fallback-a
|
|
706
|
+
- fallback-b
|
|
707
|
+
`);
|
|
708
|
+
|
|
709
|
+
const autoDispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
|
|
710
|
+
assert.ok(autoDispatch, "auto-mode dispatches the configured hook");
|
|
711
|
+
assert.equal(autoDispatch.unitType, "hook/code-review", "auto dispatch is hook-prefixed");
|
|
712
|
+
assert.equal(autoDispatch.model, undefined, "auto dispatch does not carry a primary-only model");
|
|
713
|
+
|
|
714
|
+
resetHookState();
|
|
715
|
+
|
|
716
|
+
const manualDispatch = triggerHookManually("code-review", "execute-task", "M001/S01/T01", base);
|
|
717
|
+
assert.ok(manualDispatch, "manual trigger dispatches the configured hook");
|
|
718
|
+
assert.equal(manualDispatch.unitType, "hook/code-review", "manual dispatch is hook-prefixed");
|
|
719
|
+
assert.equal(manualDispatch.model, undefined, "manual dispatch does not carry a primary-only model");
|
|
720
|
+
} finally {
|
|
721
|
+
resetHookState();
|
|
722
|
+
rmSync(base, { recursive: true, force: true });
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
457
726
|
});
|
|
@@ -260,7 +260,14 @@ test("post-unit blocking gate pauses auto-mode on needs-attention verdict", asyn
|
|
|
260
260
|
const result = await postUnitPostVerification(pctx);
|
|
261
261
|
assert.equal(result, "stopped");
|
|
262
262
|
assert.equal(pauseAuto.mock.callCount(), 1);
|
|
263
|
-
|
|
263
|
+
// Regression (#1245): the block message must name BOTH the hook's trigger
|
|
264
|
+
// unit (execute-task M001/S01/T01) and the just-completed unit that surfaced
|
|
265
|
+
// the block (s.currentUnit = hook/review-arbiter M001/S01/T01), so the pause
|
|
266
|
+
// is unambiguous to triage. The pre-fix message stopped at the trigger unit.
|
|
267
|
+
assert.match(
|
|
268
|
+
notifications.join("\n"),
|
|
269
|
+
/Post-unit gate "review-arbiter" blocked execute-task M001\/S01\/T01 \(detected on completion of hook\/review-arbiter M001\/S01\/T01\)/,
|
|
270
|
+
);
|
|
264
271
|
assert.match(notifications.join("\n"), /\/gsd status/);
|
|
265
272
|
} finally {
|
|
266
273
|
process.chdir(originalCwd);
|
|
@@ -160,6 +160,60 @@ describe('readUatGateVerdict — DB fallback for orphaned ASSESSMENT', () => {
|
|
|
160
160
|
assert.equal(result, null, 'roadmap-scoped assessments must not satisfy the UAT gate');
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
+
test('a backfilled assessment does NOT satisfy the UAT gate (#1258)', async () => {
|
|
164
|
+
// The milestone-validation backfill fabricates a `verdict: PASS` ASSESSMENT
|
|
165
|
+
// for completed slices that never produced a real UAT result (e.g.
|
|
166
|
+
// artifact-driven UAT that was never dispatched). Filed under scope
|
|
167
|
+
// 'backfill', that placeholder must never be read as a genuine UAT sign-off —
|
|
168
|
+
// otherwise "never checked" is silently treated as "passed". Even with the
|
|
169
|
+
// fabricated file present on disk, the gate must fall through to the
|
|
170
|
+
// authoritative run-uat row (absent here) and return null.
|
|
171
|
+
const file = canonicalAssessmentPath(basePath);
|
|
172
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
173
|
+
fs.writeFileSync(file, RUNTIME_PASS_BODY);
|
|
174
|
+
insertAssessment({
|
|
175
|
+
path: `.gsd/milestones/${MID}/slices/${SLICE}/${SLICE}-ASSESSMENT.md`,
|
|
176
|
+
milestoneId: MID,
|
|
177
|
+
sliceId: SLICE,
|
|
178
|
+
status: 'pass',
|
|
179
|
+
scope: 'backfill',
|
|
180
|
+
fullContent: RUNTIME_PASS_BODY,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const result = await readUatGateVerdict(basePath, MID, SLICE);
|
|
184
|
+
|
|
185
|
+
assert.equal(result, null, 'backfilled assessments must not satisfy the UAT gate');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('a genuine run-uat row still resolves even when a backfilled file exists (#1258)', async () => {
|
|
189
|
+
// If a real UAT was later recorded, its authoritative run-uat row must win
|
|
190
|
+
// over the fabricated placeholder rather than being masked by it.
|
|
191
|
+
const file = canonicalAssessmentPath(basePath);
|
|
192
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
193
|
+
fs.writeFileSync(file, RUNTIME_PASS_BODY);
|
|
194
|
+
insertAssessment({
|
|
195
|
+
path: `.gsd/milestones/${MID}/slices/${SLICE}/${SLICE}-ASSESSMENT.md`,
|
|
196
|
+
milestoneId: MID,
|
|
197
|
+
sliceId: SLICE,
|
|
198
|
+
status: 'pass',
|
|
199
|
+
scope: 'backfill',
|
|
200
|
+
fullContent: RUNTIME_PASS_BODY,
|
|
201
|
+
});
|
|
202
|
+
insertAssessment({
|
|
203
|
+
path: `.gsd/phases/01-some-feature/01-01-ASSESSMENT.md`,
|
|
204
|
+
milestoneId: MID,
|
|
205
|
+
sliceId: SLICE,
|
|
206
|
+
status: 'pass',
|
|
207
|
+
scope: 'run-uat',
|
|
208
|
+
fullContent: RUNTIME_PASS_BODY,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const result = await readUatGateVerdict(basePath, MID, SLICE);
|
|
212
|
+
|
|
213
|
+
assert.ok(result, 'a genuine run-uat sign-off must resolve');
|
|
214
|
+
assert.equal(result!.verdict, 'pass');
|
|
215
|
+
});
|
|
216
|
+
|
|
163
217
|
test('returns null when no assessment and no file exist (fallback does not hallucinate)', async () => {
|
|
164
218
|
const result = await readUatGateVerdict(basePath, MID, SLICE);
|
|
165
219
|
assert.equal(result, null);
|