gsd-pi 2.79.0-dev.579e14e9b → 2.79.0-dev.8f814f3af
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +6 -1
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-start.js +3 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -8
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +8 -8
- package/dist/resources/extensions/gsd/guided-flow.js +40 -0
- package/dist/resources/extensions/gsd/paths.js +5 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +45 -4
- package/dist/resources/extensions/gsd/uok/audit.js +23 -9
- package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
- package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
- package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
- package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
- package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
- package/dist/resources/extensions/shared/interview-ui.js +15 -4
- 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 +15 -15
- 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/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
- 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/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +13 -2
- package/src/resources/extensions/gsd/auto/phases.ts +6 -1
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-start.ts +3 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +11 -8
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-shouldblock-basepath.test.ts +97 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +8 -4
- package/src/resources/extensions/gsd/guided-flow.ts +47 -0
- package/src/resources/extensions/gsd/paths.ts +6 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +148 -0
- package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
- package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +109 -0
- package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
- package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +36 -7
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +47 -4
- package/src/resources/extensions/gsd/uok/audit.ts +25 -9
- package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
- package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
- package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
- package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
- package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
- package/src/resources/extensions/shared/interview-ui.ts +18 -5
- package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
- package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +41 -0
- /package/dist/web/standalone/.next/static/{X6D0ObmOxuQCMG5piZpbE → -O_omrDQQ0SfyCjHy578c}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{X6D0ObmOxuQCMG5piZpbE → -O_omrDQQ0SfyCjHy578c}/_ssgManifest.js +0 -0
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
DISPATCH_RULES,
|
|
15
15
|
getDeepStageGate,
|
|
16
16
|
hasPendingDeepStage,
|
|
17
|
+
resolveDispatch,
|
|
17
18
|
setResearchProjectPromptBuilderForTest,
|
|
18
19
|
type DispatchContext,
|
|
19
20
|
} from "../auto-dispatch.ts";
|
|
@@ -248,6 +249,11 @@ function writeCapturedDeepPrefs(base: string): void {
|
|
|
248
249
|
);
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
function writeSkippedProjectResearchDecision(base: string): void {
|
|
253
|
+
mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
|
|
254
|
+
writeFileSync(join(base, ".gsd", "runtime", "research-decision.json"), JSON.stringify({ decision: "skip" }));
|
|
255
|
+
}
|
|
256
|
+
|
|
251
257
|
function makeCtx(
|
|
252
258
|
basePath: string,
|
|
253
259
|
prefs: GSDPreferences | undefined,
|
|
@@ -785,6 +791,42 @@ test("Deep mode: research-project DOES dispatch when only 3 of 4 research files
|
|
|
785
791
|
assert.ok(result && result.action === "dispatch", "any missing dimension must trigger re-run");
|
|
786
792
|
});
|
|
787
793
|
|
|
794
|
+
test("Deep mode: queued milestone without CONTEXT.md routes to milestone research after project setup", async (t) => {
|
|
795
|
+
const base = makeIsolatedBaseWithCleanup(t);
|
|
796
|
+
|
|
797
|
+
writeCapturedDeepPrefs(base);
|
|
798
|
+
writeValidProject(base);
|
|
799
|
+
writeValidRequirements(base);
|
|
800
|
+
writeSkippedProjectResearchDecision(base);
|
|
801
|
+
|
|
802
|
+
const prefs = { planning_depth: "deep" } as GSDPreferences;
|
|
803
|
+
const result = await resolveDispatch(makeCtx(base, prefs));
|
|
804
|
+
|
|
805
|
+
assert.equal(result.action, "dispatch");
|
|
806
|
+
if (result.action === "dispatch") {
|
|
807
|
+
assert.equal(result.unitType, "research-milestone");
|
|
808
|
+
assert.equal(result.unitId, "M001");
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
test("Deep mode: queued milestone without CONTEXT.md can route directly to milestone planning", async (t) => {
|
|
813
|
+
const base = makeIsolatedBaseWithCleanup(t);
|
|
814
|
+
|
|
815
|
+
writeCapturedDeepPrefs(base);
|
|
816
|
+
writeValidProject(base);
|
|
817
|
+
writeValidRequirements(base);
|
|
818
|
+
writeSkippedProjectResearchDecision(base);
|
|
819
|
+
|
|
820
|
+
const prefs = { planning_depth: "deep", phases: { skip_research: true } } as GSDPreferences;
|
|
821
|
+
const result = await resolveDispatch(makeCtx(base, prefs));
|
|
822
|
+
|
|
823
|
+
assert.equal(result.action, "dispatch");
|
|
824
|
+
if (result.action === "dispatch") {
|
|
825
|
+
assert.equal(result.unitType, "plan-milestone");
|
|
826
|
+
assert.equal(result.unitId, "M001");
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
|
|
788
830
|
// ─── centralized deep-stage gate ─────────────────────────────────────────
|
|
789
831
|
|
|
790
832
|
test("Deep mode gate reports the earliest missing section", (t) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
@@ -23,6 +23,11 @@ import {
|
|
|
23
23
|
showSmartEntry,
|
|
24
24
|
startDeepProjectSetupForeground,
|
|
25
25
|
} from "../guided-flow.ts";
|
|
26
|
+
import {
|
|
27
|
+
closeDatabase,
|
|
28
|
+
insertMilestone,
|
|
29
|
+
openDatabase,
|
|
30
|
+
} from "../gsd-db.ts";
|
|
26
31
|
import type { GSDPreferences } from "../preferences.ts";
|
|
27
32
|
import type { GSDState } from "../types.ts";
|
|
28
33
|
|
|
@@ -342,6 +347,62 @@ test("deep project setup: pre-dispatch can run before the first milestone exists
|
|
|
342
347
|
}
|
|
343
348
|
});
|
|
344
349
|
|
|
350
|
+
test("deep project setup: bootstrap continues queued M002 without milestone context", async () => {
|
|
351
|
+
const base = makeRepo();
|
|
352
|
+
try {
|
|
353
|
+
writeCapturedDeepPrefs(base);
|
|
354
|
+
writeValidProjectAndRequirements(base);
|
|
355
|
+
mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
|
|
356
|
+
writeFileSync(join(base, ".gsd", "runtime", "research-decision.json"), '{"decision":"skip"}\n');
|
|
357
|
+
|
|
358
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
359
|
+
insertMilestone({ id: "M001", title: "First milestone", status: "complete" });
|
|
360
|
+
insertMilestone({ id: "M002", title: "Second milestone", status: "queued" });
|
|
361
|
+
closeDatabase();
|
|
362
|
+
|
|
363
|
+
const messages: unknown[] = [];
|
|
364
|
+
const pi = {
|
|
365
|
+
...makePi(messages),
|
|
366
|
+
getThinkingLevel: () => "medium",
|
|
367
|
+
};
|
|
368
|
+
const s = new AutoSession();
|
|
369
|
+
const ready = await bootstrapAutoSession(
|
|
370
|
+
s,
|
|
371
|
+
makeCtx(`queued-${randomUUID()}`) as any,
|
|
372
|
+
pi as any,
|
|
373
|
+
base,
|
|
374
|
+
false,
|
|
375
|
+
false,
|
|
376
|
+
{
|
|
377
|
+
shouldUseWorktreeIsolation: () => false,
|
|
378
|
+
registerSigtermHandler: () => {},
|
|
379
|
+
lockBase: () => base,
|
|
380
|
+
buildResolver: () => ({}) as any,
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
classification: "none",
|
|
384
|
+
lock: null,
|
|
385
|
+
pausedSession: null,
|
|
386
|
+
state: null,
|
|
387
|
+
recovery: null,
|
|
388
|
+
recoveryPrompt: null,
|
|
389
|
+
recoveryToolCallCount: 0,
|
|
390
|
+
artifactSatisfied: false,
|
|
391
|
+
hasResumableDiskState: false,
|
|
392
|
+
isBootstrapCrash: false,
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
assert.equal(ready, true);
|
|
397
|
+
assert.equal(s.active, true);
|
|
398
|
+
assert.equal(s.currentMilestoneId, "M002");
|
|
399
|
+
assert.equal(messages.length, 0, "queued deep milestone must not re-enter smart new-milestone discussion");
|
|
400
|
+
} finally {
|
|
401
|
+
try { closeDatabase(); } catch {}
|
|
402
|
+
rmSync(base, { recursive: true, force: true });
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
345
406
|
test("deep project setup: pre-dispatch takes precedence over an existing draft milestone", async () => {
|
|
346
407
|
const base = makeBase();
|
|
347
408
|
try {
|
|
@@ -1020,7 +1081,7 @@ test("deep project setup: research-project blocker placeholder is a file, not th
|
|
|
1020
1081
|
const base = makeBase();
|
|
1021
1082
|
try {
|
|
1022
1083
|
const expectedPath = resolveExpectedArtifactPath("research-project", "PROJECT-RESEARCH", base);
|
|
1023
|
-
assert.equal(expectedPath, join(base, ".gsd", "research", "PROJECT-RESEARCH-BLOCKER.md"));
|
|
1084
|
+
assert.equal(expectedPath, join(realpathSync(base), ".gsd", "research", "PROJECT-RESEARCH-BLOCKER.md"));
|
|
1024
1085
|
|
|
1025
1086
|
mkdirSync(join(base, ".gsd", "research"), { recursive: true });
|
|
1026
1087
|
const diagnosis = writeBlockerPlaceholder(
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// gsd-2 / execute-summary-save PROJECT registration hard-fail tests
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
import { openDatabase, closeDatabase, getAllMilestones } from "../gsd-db.ts";
|
|
10
|
+
import { markApprovalGateVerified, clearDiscussionFlowState } from "../bootstrap/write-gate.ts";
|
|
11
|
+
import { executeSummarySave } from "../tools/workflow-tool-executors.ts";
|
|
12
|
+
|
|
13
|
+
function makeTmpBase(): string {
|
|
14
|
+
const base = join(tmpdir(), `gsd-summary-save-empty-project-${randomUUID()}`);
|
|
15
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
16
|
+
return base;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function cleanup(base: string): void {
|
|
20
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* swallow */ }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function openTestDb(base: string): void {
|
|
24
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function inProjectDir<T>(dir: string, fn: () => Promise<T>): Promise<T> {
|
|
28
|
+
const originalCwd = process.cwd();
|
|
29
|
+
try {
|
|
30
|
+
process.chdir(dir);
|
|
31
|
+
return await fn();
|
|
32
|
+
} finally {
|
|
33
|
+
process.chdir(originalCwd);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setupBase(t: { after: (fn: () => void) => void }): string {
|
|
38
|
+
const base = makeTmpBase();
|
|
39
|
+
// Force deep planning so the root-artifact guard requires a verified approval gate,
|
|
40
|
+
// matching the production flow that surfaces the regression.
|
|
41
|
+
writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "---\nplanning_depth: deep\n---\n");
|
|
42
|
+
openTestDb(base);
|
|
43
|
+
markApprovalGateVerified("depth_verification_project_confirm", base);
|
|
44
|
+
t.after(() => {
|
|
45
|
+
clearDiscussionFlowState(base);
|
|
46
|
+
closeDatabase();
|
|
47
|
+
cleanup(base);
|
|
48
|
+
});
|
|
49
|
+
return base;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("executeSummarySave returns isError when PROJECT.md content has zero parseable milestone lines", async (t) => {
|
|
53
|
+
const base = setupBase(t);
|
|
54
|
+
|
|
55
|
+
const content = [
|
|
56
|
+
"# Project",
|
|
57
|
+
"",
|
|
58
|
+
"## What This Is",
|
|
59
|
+
"",
|
|
60
|
+
"Bad-separator regression fixture.",
|
|
61
|
+
"",
|
|
62
|
+
"## Milestone Sequence",
|
|
63
|
+
"",
|
|
64
|
+
// Wrong separator: " : " instead of em-dash / -- / - → MILESTONE_LINE_RE matches zero lines.
|
|
65
|
+
"- [ ] M001: Foundation : Establish the first runnable slice.",
|
|
66
|
+
"",
|
|
67
|
+
"## Next Section",
|
|
68
|
+
"",
|
|
69
|
+
"Trailing prose with no list bullets so MILESTONE_LINE_RE cannot bridge across lines.",
|
|
70
|
+
"",
|
|
71
|
+
].join("\n");
|
|
72
|
+
|
|
73
|
+
const result = await inProjectDir(base, () => executeSummarySave({
|
|
74
|
+
artifact_type: "PROJECT",
|
|
75
|
+
content,
|
|
76
|
+
}, base));
|
|
77
|
+
|
|
78
|
+
assert.equal(result.isError, true);
|
|
79
|
+
assert.equal(result.details.error, "milestone_registration_empty_parse");
|
|
80
|
+
assert.match(result.content[0].text, /zero parseable milestone lines/);
|
|
81
|
+
assert.equal(getAllMilestones().length, 0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("executeSummarySave registers milestones when PROJECT.md uses canonical em-dash format", async (t) => {
|
|
85
|
+
const base = setupBase(t);
|
|
86
|
+
|
|
87
|
+
const content = [
|
|
88
|
+
"# Project",
|
|
89
|
+
"",
|
|
90
|
+
"## What This Is",
|
|
91
|
+
"",
|
|
92
|
+
"Canonical milestone-sequence fixture.",
|
|
93
|
+
"",
|
|
94
|
+
"## Milestone Sequence",
|
|
95
|
+
"",
|
|
96
|
+
"- [ ] M001: Foo — bar",
|
|
97
|
+
"- [ ] M002: Baz — qux",
|
|
98
|
+
"",
|
|
99
|
+
].join("\n");
|
|
100
|
+
|
|
101
|
+
const result = await inProjectDir(base, () => executeSummarySave({
|
|
102
|
+
artifact_type: "PROJECT",
|
|
103
|
+
content,
|
|
104
|
+
}, base));
|
|
105
|
+
|
|
106
|
+
assert.notEqual(result.isError, true);
|
|
107
|
+
assert.deepEqual(result.details.registeredMilestones, ["M001", "M002"]);
|
|
108
|
+
assert.equal(getAllMilestones().length, 2);
|
|
109
|
+
});
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
// GSD2 UOK Contract Versioning and DB Authority Tests
|
|
2
|
+
|
|
1
3
|
import test from "node:test";
|
|
2
4
|
import assert from "node:assert/strict";
|
|
5
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
3
8
|
|
|
4
9
|
import type {
|
|
5
10
|
AuditEventEnvelope,
|
|
@@ -11,8 +16,17 @@ import type {
|
|
|
11
16
|
WriteRecord,
|
|
12
17
|
WriterToken,
|
|
13
18
|
} from "../uok/contracts.ts";
|
|
14
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
CURRENT_UOK_CONTRACT_VERSION,
|
|
21
|
+
normalizeAuditEvent,
|
|
22
|
+
validateAuditEvent,
|
|
23
|
+
validateDispatchEnvelope,
|
|
24
|
+
validateTurnResult,
|
|
25
|
+
} from "../uok/contracts.ts";
|
|
26
|
+
import { buildAuditEnvelope, emitUokAuditEvent } from "../uok/audit.ts";
|
|
15
27
|
import { buildDispatchEnvelope, explainDispatch } from "../uok/dispatch-envelope.ts";
|
|
28
|
+
import { buildTurnTimeline } from "../uok/timeline.ts";
|
|
29
|
+
import { _getAdapter, closeDatabase, openDatabase } from "../gsd-db.ts";
|
|
16
30
|
|
|
17
31
|
test("uok contracts serialize/deserialize turn envelopes", () => {
|
|
18
32
|
const contract: TurnContract = {
|
|
@@ -37,6 +51,7 @@ test("uok contracts serialize/deserialize turn envelopes", () => {
|
|
|
37
51
|
};
|
|
38
52
|
|
|
39
53
|
const result: TurnResult = {
|
|
54
|
+
version: CURRENT_UOK_CONTRACT_VERSION,
|
|
40
55
|
traceId: contract.traceId,
|
|
41
56
|
turnId: contract.turnId,
|
|
42
57
|
iteration: contract.iteration,
|
|
@@ -56,8 +71,10 @@ test("uok contracts serialize/deserialize turn envelopes", () => {
|
|
|
56
71
|
|
|
57
72
|
const roundTrip = JSON.parse(JSON.stringify(result)) as TurnResult;
|
|
58
73
|
assert.equal(roundTrip.turnId, "turn-1");
|
|
74
|
+
assert.equal(roundTrip.version, CURRENT_UOK_CONTRACT_VERSION);
|
|
59
75
|
assert.equal(roundTrip.gateResults?.[0]?.gateId, "Q3");
|
|
60
76
|
assert.equal(roundTrip.phaseResults.length, 3);
|
|
77
|
+
assert.equal(validateTurnResult(roundTrip).ok, true);
|
|
61
78
|
});
|
|
62
79
|
|
|
63
80
|
test("uok contracts include required DAG node kinds", () => {
|
|
@@ -84,9 +101,11 @@ test("uok audit envelope includes trace/turn/causality fields", () => {
|
|
|
84
101
|
});
|
|
85
102
|
|
|
86
103
|
assert.equal(event.traceId, "trace-xyz");
|
|
104
|
+
assert.equal(event.version, CURRENT_UOK_CONTRACT_VERSION);
|
|
87
105
|
assert.equal(event.turnId, "turn-xyz");
|
|
88
106
|
assert.equal(event.causedBy, "turn-start");
|
|
89
107
|
assert.equal(event.payload.status, "completed");
|
|
108
|
+
assert.equal(validateAuditEvent(event).ok, true);
|
|
90
109
|
});
|
|
91
110
|
|
|
92
111
|
test("uok dispatch envelope carries scheduler reason and constraints", () => {
|
|
@@ -107,9 +126,98 @@ test("uok dispatch envelope carries scheduler reason and constraints", () => {
|
|
|
107
126
|
});
|
|
108
127
|
|
|
109
128
|
assert.equal(envelope.nodeKind, "unit");
|
|
129
|
+
assert.equal(envelope.version, CURRENT_UOK_CONTRACT_VERSION);
|
|
110
130
|
assert.equal(envelope.reason.reasonCode, "dependency");
|
|
111
131
|
assert.deepEqual(envelope.constraints?.dependsOn, ["plan-gate"]);
|
|
112
132
|
assert.ok(explainDispatch(envelope).includes("execute-task M001/S01/T01"));
|
|
133
|
+
assert.equal(validateDispatchEnvelope(envelope).ok, true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("uok contracts normalize legacy records without losing payload fields", () => {
|
|
137
|
+
const legacy = {
|
|
138
|
+
eventId: "event-legacy",
|
|
139
|
+
traceId: "trace-legacy",
|
|
140
|
+
category: "orchestration",
|
|
141
|
+
type: "turn-result",
|
|
142
|
+
ts: new Date().toISOString(),
|
|
143
|
+
payload: { status: "completed", extra: "preserved" },
|
|
144
|
+
} as AuditEventEnvelope;
|
|
145
|
+
|
|
146
|
+
const normalized = normalizeAuditEvent(legacy);
|
|
147
|
+
assert.equal(normalized.version, "0");
|
|
148
|
+
assert.equal(normalized.payload.extra, "preserved");
|
|
149
|
+
assert.equal(validateAuditEvent(legacy).ok, true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("uok audit emission writes DB as authoritative before jsonl projection", (t) => {
|
|
153
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-uok-db-audit-"));
|
|
154
|
+
mkdirSync(join(basePath, ".gsd"), { recursive: true });
|
|
155
|
+
t.after(() => {
|
|
156
|
+
closeDatabase();
|
|
157
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
assert.equal(openDatabase(join(basePath, ".gsd", "gsd.db")), true);
|
|
161
|
+
emitUokAuditEvent(
|
|
162
|
+
basePath,
|
|
163
|
+
buildAuditEnvelope({
|
|
164
|
+
traceId: "trace-db",
|
|
165
|
+
turnId: "turn-db",
|
|
166
|
+
category: "orchestration",
|
|
167
|
+
type: "turn-start",
|
|
168
|
+
payload: { unitType: "execute-task" },
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const row = _getAdapter()!.prepare(
|
|
173
|
+
"SELECT payload_json FROM audit_events WHERE trace_id = 'trace-db' AND turn_id = 'turn-db'",
|
|
174
|
+
).get() as { payload_json: string } | undefined;
|
|
175
|
+
assert.ok(row, "DB audit row should be written");
|
|
176
|
+
assert.equal(JSON.parse(row.payload_json).contractVersion, CURRENT_UOK_CONTRACT_VERSION);
|
|
177
|
+
|
|
178
|
+
const projection = readFileSync(join(basePath, ".gsd", "audit", "events.jsonl"), "utf-8");
|
|
179
|
+
assert.ok(projection.includes("trace-db"), "jsonl projection should still be written");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("uok timeline prefers DB records over jsonl projection when DB is available", (t) => {
|
|
183
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-uok-timeline-"));
|
|
184
|
+
const auditDir = join(basePath, ".gsd", "audit");
|
|
185
|
+
mkdirSync(auditDir, { recursive: true });
|
|
186
|
+
writeFileSync(
|
|
187
|
+
join(auditDir, "events.jsonl"),
|
|
188
|
+
`${JSON.stringify({
|
|
189
|
+
version: CURRENT_UOK_CONTRACT_VERSION,
|
|
190
|
+
eventId: "jsonl-only",
|
|
191
|
+
traceId: "trace-timeline",
|
|
192
|
+
turnId: "turn-timeline",
|
|
193
|
+
category: "orchestration",
|
|
194
|
+
type: "jsonl-projection",
|
|
195
|
+
ts: "2026-01-01T00:00:00.000Z",
|
|
196
|
+
payload: {},
|
|
197
|
+
})}\n`,
|
|
198
|
+
);
|
|
199
|
+
t.after(() => {
|
|
200
|
+
closeDatabase();
|
|
201
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
assert.equal(openDatabase(join(basePath, ".gsd", "gsd.db")), true);
|
|
205
|
+
emitUokAuditEvent(
|
|
206
|
+
basePath,
|
|
207
|
+
buildAuditEnvelope({
|
|
208
|
+
traceId: "trace-timeline",
|
|
209
|
+
turnId: "turn-timeline",
|
|
210
|
+
category: "orchestration",
|
|
211
|
+
type: "db-authoritative",
|
|
212
|
+
payload: {},
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const timeline = buildTurnTimeline(basePath, { traceId: "trace-timeline", turnId: "turn-timeline" });
|
|
217
|
+
assert.equal(timeline.authoritative, "db");
|
|
218
|
+
assert.equal(timeline.degraded, false);
|
|
219
|
+
assert.ok(timeline.entries.some((entry) => entry.type === "db-authoritative"));
|
|
220
|
+
assert.equal(timeline.entries.some((entry) => entry.type === "jsonl-projection"), false);
|
|
113
221
|
});
|
|
114
222
|
|
|
115
223
|
test("uok writer records serialize sequence metadata", () => {
|
|
@@ -63,3 +63,101 @@ test("uok turn observer adds writer sequence metadata to audit events", (t) => {
|
|
|
63
63
|
assert.equal(payloads[1]?.writeSequence, 2);
|
|
64
64
|
assert.equal(typeof payloads[0]?.writerTokenId, "string");
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
test("uok turn observer releases writer token when validation throws", (t) => {
|
|
68
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-uok-loop-writer-throw-"));
|
|
69
|
+
resetWriterTokensForTests();
|
|
70
|
+
t.after(() => {
|
|
71
|
+
resetWriterTokensForTests();
|
|
72
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const observer = createTurnObserver({
|
|
76
|
+
basePath,
|
|
77
|
+
gitAction: "status-only",
|
|
78
|
+
gitPush: false,
|
|
79
|
+
enableAudit: false,
|
|
80
|
+
enableGitops: false,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
observer.onTurnStart({
|
|
84
|
+
basePath,
|
|
85
|
+
traceId: "trace-throw",
|
|
86
|
+
turnId: "turn-throw",
|
|
87
|
+
iteration: 1,
|
|
88
|
+
unitType: "execute-task",
|
|
89
|
+
unitId: "M001/S01/T01",
|
|
90
|
+
startedAt: new Date().toISOString(),
|
|
91
|
+
});
|
|
92
|
+
assert.equal(hasActiveWriterToken(basePath, "turn-throw"), true);
|
|
93
|
+
|
|
94
|
+
// Invalid payload (missing required fields like status/finishedAt) should
|
|
95
|
+
// trigger validateTurnResult to fail and throw.
|
|
96
|
+
assert.throws(() => {
|
|
97
|
+
observer.onTurnResult({
|
|
98
|
+
traceId: "trace-throw",
|
|
99
|
+
turnId: "turn-throw",
|
|
100
|
+
// @ts-expect-error intentionally invalid for test
|
|
101
|
+
iteration: "not-a-number",
|
|
102
|
+
unitType: "execute-task",
|
|
103
|
+
unitId: "M001/S01/T01",
|
|
104
|
+
status: "completed",
|
|
105
|
+
failureClass: "none",
|
|
106
|
+
phaseResults: [],
|
|
107
|
+
startedAt: new Date().toISOString(),
|
|
108
|
+
finishedAt: new Date().toISOString(),
|
|
109
|
+
});
|
|
110
|
+
}, /Invalid UOK turn result/);
|
|
111
|
+
|
|
112
|
+
// Cleanup must run in finally — token released, no leaked state.
|
|
113
|
+
assert.equal(hasActiveWriterToken(basePath, "turn-throw"), false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("uok turn observer falls back to cached phaseResults when result.phaseResults is missing", (t) => {
|
|
117
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-uok-loop-writer-missing-"));
|
|
118
|
+
resetWriterTokensForTests();
|
|
119
|
+
t.after(() => {
|
|
120
|
+
resetWriterTokensForTests();
|
|
121
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const observer = createTurnObserver({
|
|
125
|
+
basePath,
|
|
126
|
+
gitAction: "status-only",
|
|
127
|
+
gitPush: false,
|
|
128
|
+
enableAudit: false,
|
|
129
|
+
enableGitops: false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
observer.onTurnStart({
|
|
133
|
+
basePath,
|
|
134
|
+
traceId: "trace-missing",
|
|
135
|
+
turnId: "turn-missing",
|
|
136
|
+
iteration: 1,
|
|
137
|
+
unitType: "execute-task",
|
|
138
|
+
unitId: "M001/S01/T01",
|
|
139
|
+
startedAt: new Date().toISOString(),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Without the Array.isArray guard, accessing result.phaseResults.length on a
|
|
143
|
+
// payload where phaseResults is undefined would throw TypeError before
|
|
144
|
+
// validateTurnResult could surface a structured error. The guard must defer
|
|
145
|
+
// to the cached phaseResults fallback so the turn completes cleanly.
|
|
146
|
+
assert.doesNotThrow(() => {
|
|
147
|
+
observer.onTurnResult({
|
|
148
|
+
traceId: "trace-missing",
|
|
149
|
+
turnId: "turn-missing",
|
|
150
|
+
iteration: 1,
|
|
151
|
+
unitType: "execute-task",
|
|
152
|
+
unitId: "M001/S01/T01",
|
|
153
|
+
status: "completed",
|
|
154
|
+
failureClass: "none",
|
|
155
|
+
// @ts-expect-error intentionally missing for test
|
|
156
|
+
phaseResults: undefined,
|
|
157
|
+
startedAt: new Date().toISOString(),
|
|
158
|
+
finishedAt: new Date().toISOString(),
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
assert.equal(hasActiveWriterToken(basePath, "turn-missing"), false);
|
|
163
|
+
});
|
|
@@ -692,7 +692,18 @@ test("executeSummarySave supports root-level deep planning artifacts", async ()
|
|
|
692
692
|
|
|
693
693
|
const project = await inProjectDir(base, () => executeSummarySave({
|
|
694
694
|
artifact_type: "PROJECT",
|
|
695
|
-
content:
|
|
695
|
+
content: [
|
|
696
|
+
"# Project",
|
|
697
|
+
"",
|
|
698
|
+
"## What This Is",
|
|
699
|
+
"",
|
|
700
|
+
"A root project artifact.",
|
|
701
|
+
"",
|
|
702
|
+
"## Milestone Sequence",
|
|
703
|
+
"",
|
|
704
|
+
"- [ ] M001: Foundation - Establish the first runnable slice.",
|
|
705
|
+
"",
|
|
706
|
+
].join("\n"),
|
|
696
707
|
}, base));
|
|
697
708
|
assert.equal(project.isError, undefined);
|
|
698
709
|
assert.equal(project.details.path, "PROJECT.md");
|
|
@@ -794,7 +805,7 @@ test("executeSummarySave registers PROJECT milestone sequence for the next run",
|
|
|
794
805
|
}
|
|
795
806
|
});
|
|
796
807
|
|
|
797
|
-
test("executeSummarySave
|
|
808
|
+
test("executeSummarySave hard-fails when milestone registration throws so silent No-Active-Milestone is impossible", async () => {
|
|
798
809
|
const base = makeTmpBase();
|
|
799
810
|
try {
|
|
800
811
|
openTestDb(base);
|
|
@@ -824,10 +835,15 @@ test("executeSummarySave keeps PROJECT artifact save successful if milestone reg
|
|
|
824
835
|
].join("\n"),
|
|
825
836
|
}, base));
|
|
826
837
|
|
|
827
|
-
|
|
838
|
+
// The artifact is persisted before registration runs, but registration must
|
|
839
|
+
// surface as isError so the LLM retries (INSERT OR IGNORE makes it idempotent)
|
|
840
|
+
// instead of announcing "ready" while the DB has zero milestone rows.
|
|
841
|
+
assert.equal(result.isError, true);
|
|
828
842
|
assert.equal(result.details.path, "PROJECT.md");
|
|
829
|
-
assert.equal(result.details.
|
|
830
|
-
assert.match(String(result.details.
|
|
843
|
+
assert.equal(result.details.error, "milestone_registration_threw");
|
|
844
|
+
assert.match(String(result.details.registration_error), /simulated milestone registration failure/);
|
|
845
|
+
assert.match(result.content[0].text, /milestone registration failed/);
|
|
846
|
+
assert.match(result.content[0].text, /idempotent/);
|
|
831
847
|
assert.ok(existsSync(join(base, ".gsd", "PROJECT.md")));
|
|
832
848
|
const artifact = originalPrepare("SELECT path FROM artifacts WHERE path = ?").get("PROJECT.md");
|
|
833
849
|
assert.equal(artifact?.path, "PROJECT.md");
|
|
@@ -872,9 +888,22 @@ test("executeSummarySave requires verified root approval in deep mode", async ()
|
|
|
872
888
|
writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "---\nplanning_depth: deep\n---\n");
|
|
873
889
|
openTestDb(base);
|
|
874
890
|
|
|
891
|
+
const projectFixture = [
|
|
892
|
+
"# Project",
|
|
893
|
+
"",
|
|
894
|
+
"## What This Is",
|
|
895
|
+
"",
|
|
896
|
+
"A root project artifact.",
|
|
897
|
+
"",
|
|
898
|
+
"## Milestone Sequence",
|
|
899
|
+
"",
|
|
900
|
+
"- [ ] M001: Foundation - Establish the first runnable slice.",
|
|
901
|
+
"",
|
|
902
|
+
].join("\n");
|
|
903
|
+
|
|
875
904
|
const blocked = await inProjectDir(base, () => executeSummarySave({
|
|
876
905
|
artifact_type: "PROJECT",
|
|
877
|
-
content:
|
|
906
|
+
content: projectFixture,
|
|
878
907
|
}, base));
|
|
879
908
|
|
|
880
909
|
assert.equal(blocked.isError, true);
|
|
@@ -886,7 +915,7 @@ test("executeSummarySave requires verified root approval in deep mode", async ()
|
|
|
886
915
|
|
|
887
916
|
const unblocked = await inProjectDir(base, () => executeSummarySave({
|
|
888
917
|
artifact_type: "PROJECT",
|
|
889
|
-
content:
|
|
918
|
+
content: projectFixture,
|
|
890
919
|
}, base));
|
|
891
920
|
|
|
892
921
|
assert.equal(unblocked.isError, undefined);
|
|
@@ -190,19 +190,63 @@ export async function executeSummarySave(
|
|
|
190
190
|
);
|
|
191
191
|
|
|
192
192
|
let registeredMilestones: string[] = [];
|
|
193
|
-
let registrationWarning: string | undefined;
|
|
194
193
|
if (params.artifact_type === "PROJECT") {
|
|
195
194
|
try {
|
|
196
195
|
registeredMilestones = registerProjectMilestoneSequence(contentToSave);
|
|
197
196
|
if (registeredMilestones.length > 0) invalidateStateCache();
|
|
198
197
|
} catch (err) {
|
|
199
198
|
const msg = err instanceof Error ? err.message : String(err);
|
|
200
|
-
|
|
201
|
-
logWarning("tool", registrationWarning, {
|
|
199
|
+
logError("tool", `gsd_summary_save: PROJECT artifact persisted but milestone registration threw: ${msg}`, {
|
|
202
200
|
tool: "gsd_summary_save",
|
|
203
201
|
error: String(err),
|
|
204
202
|
stack: err instanceof Error ? err.stack ?? "" : "",
|
|
205
203
|
});
|
|
204
|
+
// PROJECT.md was persisted by saveArtifactToDb above; the artifacts row
|
|
205
|
+
// changed even though no milestones registered. Invalidate so subsequent
|
|
206
|
+
// /gsd reads see the persisted artifact instead of the pre-save cache.
|
|
207
|
+
invalidateStateCache();
|
|
208
|
+
return {
|
|
209
|
+
content: [{
|
|
210
|
+
type: "text",
|
|
211
|
+
text:
|
|
212
|
+
`Error: PROJECT.md was saved to ${relativePath} but milestone registration failed: ${msg}. ` +
|
|
213
|
+
`The DB has no milestone rows for this project, so /gsd will report "No Active Milestone". ` +
|
|
214
|
+
`Re-call gsd_summary_save(PROJECT) once the underlying error is resolved — INSERT OR IGNORE makes registration idempotent.`,
|
|
215
|
+
}],
|
|
216
|
+
details: {
|
|
217
|
+
operation: "save_summary",
|
|
218
|
+
path: relativePath,
|
|
219
|
+
artifact_type: params.artifact_type,
|
|
220
|
+
error: "milestone_registration_threw",
|
|
221
|
+
registration_error: msg,
|
|
222
|
+
},
|
|
223
|
+
isError: true,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (registeredMilestones.length === 0) {
|
|
227
|
+
logError("tool", `gsd_summary_save: PROJECT.md saved to ${relativePath} but parsed zero milestones — registration produced no DB rows`, {
|
|
228
|
+
tool: "gsd_summary_save",
|
|
229
|
+
});
|
|
230
|
+
// PROJECT.md was persisted; invalidate so subsequent reads see the new
|
|
231
|
+
// artifacts row even though no milestones registered.
|
|
232
|
+
invalidateStateCache();
|
|
233
|
+
return {
|
|
234
|
+
content: [{
|
|
235
|
+
type: "text",
|
|
236
|
+
text:
|
|
237
|
+
`Error: PROJECT.md was saved to ${relativePath} but contains zero parseable milestone lines, ` +
|
|
238
|
+
`so no milestones were registered in the DB. /gsd will report "No Active Milestone". ` +
|
|
239
|
+
`Rewrite PROJECT.md so the "Milestone Sequence" section uses canonical lines: ` +
|
|
240
|
+
`\`- [ ] M001: <Title> — <One-liner>\` (em-dash, double-dash \`--\`, or single-dash \`-\` separator), then re-call gsd_summary_save(PROJECT).`,
|
|
241
|
+
}],
|
|
242
|
+
details: {
|
|
243
|
+
operation: "save_summary",
|
|
244
|
+
path: relativePath,
|
|
245
|
+
artifact_type: params.artifact_type,
|
|
246
|
+
error: "milestone_registration_empty_parse",
|
|
247
|
+
},
|
|
248
|
+
isError: true,
|
|
249
|
+
};
|
|
206
250
|
}
|
|
207
251
|
}
|
|
208
252
|
|
|
@@ -225,7 +269,6 @@ export async function executeSummarySave(
|
|
|
225
269
|
artifact_type: params.artifact_type,
|
|
226
270
|
content_source: contentSource,
|
|
227
271
|
...(registeredMilestones.length > 0 ? { registeredMilestones } : {}),
|
|
228
|
-
...(registrationWarning ? { warning: registrationWarning } : {}),
|
|
229
272
|
},
|
|
230
273
|
};
|
|
231
274
|
} catch (err) {
|