gsd-pi 2.78.1-dev.eccf86e27 → 2.79.0-dev.487011134
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/README.md +94 -47
- 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-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +52 -29
- package/dist/resources/extensions/gsd/auto-recovery.js +18 -3
- package/dist/resources/extensions/gsd/auto-start.js +3 -2
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +33 -37
- package/dist/resources/extensions/gsd/commands/context.js +1 -1
- package/dist/resources/extensions/gsd/preferences-types.js +20 -2
- package/dist/resources/extensions/gsd/preferences-validation.js +3 -3
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +41 -2
- package/dist/resources/extensions/gsd/unit-context-composer.js +32 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +21 -0
- 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/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
- 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/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 +12 -12
- 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/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +1 -0
- package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +42 -0
- package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +30 -0
- package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/LICENSE +191 -0
- package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
- package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +46 -0
- package/package.json +1 -1
- package/packages/daemon/package.json +2 -2
- package/packages/mcp-server/dist/workflow-tools.d.ts +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +53 -0
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +2 -2
- package/packages/mcp-server/src/workflow-tools.test.ts +116 -0
- package/packages/mcp-server/src/workflow-tools.ts +81 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- 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 +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +6 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +106 -28
- package/src/resources/extensions/gsd/auto-recovery.ts +17 -3
- package/src/resources/extensions/gsd/auto-start.ts +3 -2
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +38 -38
- package/src/resources/extensions/gsd/commands/context.ts +1 -1
- package/src/resources/extensions/gsd/preferences-types.ts +23 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +68 -1
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/current-directory-root-homedir-fallback.test.ts +63 -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/guided-flow-prompt-consolidation.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +32 -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 +100 -0
- package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +3 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +41 -1
- package/src/resources/extensions/gsd/unit-context-composer.ts +49 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +34 -0
- 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/tests/gsd-phase-state.test.ts +43 -1
- /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → SVXdEZJVE2uo2osZ3EE21}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → SVXdEZJVE2uo2osZ3EE21}/_ssgManifest.js +0 -0
|
@@ -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
|
+
});
|
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
_getAdapter,
|
|
12
12
|
insertGateRow,
|
|
13
13
|
upsertRequirement,
|
|
14
|
+
getAllMilestones,
|
|
14
15
|
} from "../gsd-db.ts";
|
|
16
|
+
import { deriveState, invalidateStateCache } from "../state.ts";
|
|
15
17
|
import { markApprovalGateVerified, markDepthVerified, clearDiscussionFlowState, loadWriteGateSnapshot, setPendingGate } from "../bootstrap/write-gate.ts";
|
|
16
18
|
import {
|
|
17
19
|
executeCompleteMilestone,
|
|
@@ -737,6 +739,104 @@ test("executeSummarySave supports root-level deep planning artifacts", async ()
|
|
|
737
739
|
}
|
|
738
740
|
});
|
|
739
741
|
|
|
742
|
+
test("executeSummarySave registers PROJECT milestone sequence for the next run", async () => {
|
|
743
|
+
const base = makeTmpBase();
|
|
744
|
+
try {
|
|
745
|
+
openTestDb(base);
|
|
746
|
+
|
|
747
|
+
const result = await inProjectDir(base, () => executeSummarySave({
|
|
748
|
+
artifact_type: "PROJECT",
|
|
749
|
+
content: [
|
|
750
|
+
"# Project",
|
|
751
|
+
"",
|
|
752
|
+
"## What This Is",
|
|
753
|
+
"",
|
|
754
|
+
"Deep project setup output.",
|
|
755
|
+
"",
|
|
756
|
+
"## Project Shape",
|
|
757
|
+
"",
|
|
758
|
+
"**Complexity:** complex",
|
|
759
|
+
"**Why:** It spans multiple delivery steps.",
|
|
760
|
+
"",
|
|
761
|
+
"## Capability Contract",
|
|
762
|
+
"",
|
|
763
|
+
"See .gsd/REQUIREMENTS.md.",
|
|
764
|
+
"",
|
|
765
|
+
"## Milestone Sequence",
|
|
766
|
+
"",
|
|
767
|
+
"- [ ] M001: Foundation - Establish the first runnable slice.",
|
|
768
|
+
"- [ ] M002: Polish - Follow-up experience work.",
|
|
769
|
+
"",
|
|
770
|
+
].join("\n"),
|
|
771
|
+
}, base));
|
|
772
|
+
|
|
773
|
+
assert.equal(result.isError, undefined);
|
|
774
|
+
assert.deepEqual(result.details.registeredMilestones, ["M001", "M002"]);
|
|
775
|
+
|
|
776
|
+
const milestones = getAllMilestones();
|
|
777
|
+
assert.deepEqual(
|
|
778
|
+
milestones.map((m) => [m.id, m.title, m.status]),
|
|
779
|
+
[
|
|
780
|
+
["M001", "Foundation", "queued"],
|
|
781
|
+
["M002", "Polish", "queued"],
|
|
782
|
+
],
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
invalidateStateCache();
|
|
786
|
+
const state = await deriveState(base);
|
|
787
|
+
assert.equal(state.activeMilestone?.id, "M001");
|
|
788
|
+
assert.equal(state.phase, "pre-planning");
|
|
789
|
+
assert.equal(state.registry[0]?.status, "active");
|
|
790
|
+
assert.equal(state.registry[1]?.status, "pending");
|
|
791
|
+
} finally {
|
|
792
|
+
closeDatabase();
|
|
793
|
+
cleanup(base);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test("executeSummarySave keeps PROJECT artifact save successful if milestone registration fails", async () => {
|
|
798
|
+
const base = makeTmpBase();
|
|
799
|
+
try {
|
|
800
|
+
openTestDb(base);
|
|
801
|
+
const db = _getAdapter();
|
|
802
|
+
assert.ok(db, "DB should be open");
|
|
803
|
+
const originalPrepare = db.prepare.bind(db);
|
|
804
|
+
(db as any).prepare = (sql: string) => {
|
|
805
|
+
if (sql.includes("INSERT OR IGNORE INTO milestones")) {
|
|
806
|
+
throw new Error("simulated milestone registration failure");
|
|
807
|
+
}
|
|
808
|
+
return originalPrepare(sql);
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
const result = await inProjectDir(base, () => executeSummarySave({
|
|
812
|
+
artifact_type: "PROJECT",
|
|
813
|
+
content: [
|
|
814
|
+
"# Project",
|
|
815
|
+
"",
|
|
816
|
+
"## What This Is",
|
|
817
|
+
"",
|
|
818
|
+
"Deep project setup output.",
|
|
819
|
+
"",
|
|
820
|
+
"## Milestone Sequence",
|
|
821
|
+
"",
|
|
822
|
+
"- [ ] M001: Foundation - Establish the first runnable slice.",
|
|
823
|
+
"",
|
|
824
|
+
].join("\n"),
|
|
825
|
+
}, base));
|
|
826
|
+
|
|
827
|
+
assert.equal(result.isError, undefined);
|
|
828
|
+
assert.equal(result.details.path, "PROJECT.md");
|
|
829
|
+
assert.equal(result.details.registeredMilestones, undefined);
|
|
830
|
+
assert.match(String(result.details.warning), /milestone registration failed/);
|
|
831
|
+
assert.ok(existsSync(join(base, ".gsd", "PROJECT.md")));
|
|
832
|
+
const artifact = originalPrepare("SELECT path FROM artifacts WHERE path = ?").get("PROJECT.md");
|
|
833
|
+
assert.equal(artifact?.path, "PROJECT.md");
|
|
834
|
+
} finally {
|
|
835
|
+
closeDatabase();
|
|
836
|
+
cleanup(base);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
|
|
740
840
|
test("executeSummarySave blocks final root artifacts while approval gate is pending", async () => {
|
|
741
841
|
const base = makeTmpBase();
|
|
742
842
|
try {
|
|
@@ -229,6 +229,9 @@ test("worktree-aware prompt builders include the explicit working directory", as
|
|
|
229
229
|
),
|
|
230
230
|
]);
|
|
231
231
|
|
|
232
|
+
assert.ok(prompts[0].includes("## Context Mode"), "discuss-milestone should include standalone Context Mode guidance");
|
|
233
|
+
assert.ok(prompts[0].includes("interview lane"), "discuss-milestone should render the interview lane");
|
|
234
|
+
|
|
232
235
|
for (const prompt of prompts) {
|
|
233
236
|
assert.match(prompt, /working directory/i);
|
|
234
237
|
assert.ok(prompt.includes(base), "prompt should include the provided working directory");
|
|
@@ -3,6 +3,7 @@ import { sanitizeCompleteMilestoneParams } from "../bootstrap/sanitize-complete-
|
|
|
3
3
|
import { loadWriteGateSnapshot, shouldBlockContextArtifactSaveInSnapshot, shouldBlockRootArtifactSaveInSnapshot } from "../bootstrap/write-gate.js";
|
|
4
4
|
import {
|
|
5
5
|
getActiveRequirements,
|
|
6
|
+
insertMilestone,
|
|
6
7
|
getMilestone,
|
|
7
8
|
getSliceStatusSummary,
|
|
8
9
|
getSliceTaskCounts,
|
|
@@ -31,6 +32,7 @@ import { handleValidateMilestone } from "./validate-milestone.js";
|
|
|
31
32
|
import { logError, logWarning } from "../workflow-logger.js";
|
|
32
33
|
import { invalidateStateCache } from "../state.js";
|
|
33
34
|
import { loadEffectiveGSDPreferences } from "../preferences.js";
|
|
35
|
+
import { parseProject } from "../schemas/parsers.js";
|
|
34
36
|
|
|
35
37
|
export const SUPPORTED_SUMMARY_ARTIFACT_TYPES = [
|
|
36
38
|
"SUMMARY",
|
|
@@ -71,6 +73,20 @@ export interface SummarySaveParams {
|
|
|
71
73
|
content: string;
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
function registerProjectMilestoneSequence(content: string): string[] {
|
|
77
|
+
const parsed = parseProject(content);
|
|
78
|
+
const registered: string[] = [];
|
|
79
|
+
for (const milestone of parsed.milestones) {
|
|
80
|
+
insertMilestone({
|
|
81
|
+
id: milestone.id,
|
|
82
|
+
title: milestone.title,
|
|
83
|
+
status: milestone.done ? "complete" : "queued",
|
|
84
|
+
});
|
|
85
|
+
registered.push(milestone.id);
|
|
86
|
+
}
|
|
87
|
+
return registered;
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
export async function executeSummarySave(
|
|
75
91
|
params: SummarySaveParams,
|
|
76
92
|
basePath: string = process.cwd(),
|
|
@@ -173,6 +189,23 @@ export async function executeSummarySave(
|
|
|
173
189
|
basePath,
|
|
174
190
|
);
|
|
175
191
|
|
|
192
|
+
let registeredMilestones: string[] = [];
|
|
193
|
+
let registrationWarning: string | undefined;
|
|
194
|
+
if (params.artifact_type === "PROJECT") {
|
|
195
|
+
try {
|
|
196
|
+
registeredMilestones = registerProjectMilestoneSequence(contentToSave);
|
|
197
|
+
if (registeredMilestones.length > 0) invalidateStateCache();
|
|
198
|
+
} catch (err) {
|
|
199
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
200
|
+
registrationWarning = `PROJECT artifact saved, but milestone registration failed: ${msg}`;
|
|
201
|
+
logWarning("tool", registrationWarning, {
|
|
202
|
+
tool: "gsd_summary_save",
|
|
203
|
+
error: String(err),
|
|
204
|
+
stack: err instanceof Error ? err.stack ?? "" : "",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
176
209
|
if (params.artifact_type === "CONTEXT" && !params.task_id) {
|
|
177
210
|
try {
|
|
178
211
|
const draftFile = params.slice_id
|
|
@@ -186,7 +219,14 @@ export async function executeSummarySave(
|
|
|
186
219
|
|
|
187
220
|
return {
|
|
188
221
|
content: [{ type: "text", text: `Saved ${params.artifact_type} artifact to ${relativePath}` }],
|
|
189
|
-
details: {
|
|
222
|
+
details: {
|
|
223
|
+
operation: "save_summary",
|
|
224
|
+
path: relativePath,
|
|
225
|
+
artifact_type: params.artifact_type,
|
|
226
|
+
content_source: contentSource,
|
|
227
|
+
...(registeredMilestones.length > 0 ? { registeredMilestones } : {}),
|
|
228
|
+
...(registrationWarning ? { warning: registrationWarning } : {}),
|
|
229
|
+
},
|
|
190
230
|
};
|
|
191
231
|
} catch (err) {
|
|
192
232
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
type BaseResolverContext,
|
|
43
43
|
type ComputedArtifactId,
|
|
44
44
|
type ComputedArtifactRegistry,
|
|
45
|
+
type ContextModePolicy,
|
|
45
46
|
type UnitContextManifest,
|
|
46
47
|
} from "./unit-context-manifest.js";
|
|
47
48
|
|
|
@@ -92,6 +93,54 @@ export function manifestBudgetChars(unitType: string): number | null {
|
|
|
92
93
|
return manifest ? manifest.maxSystemPromptChars : null;
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
// ─── Context Mode lane guidance ──────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export type ContextModeRenderMode = "standalone" | "nested";
|
|
99
|
+
|
|
100
|
+
export interface ComposeContextModeInstructionOptions {
|
|
101
|
+
readonly enabled: boolean;
|
|
102
|
+
readonly renderMode: ContextModeRenderMode;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const CONTEXT_MODE_LANE_LABELS: Record<Exclude<ContextModePolicy, "none">, string> = {
|
|
106
|
+
interview: "interview",
|
|
107
|
+
research: "research",
|
|
108
|
+
planning: "planning",
|
|
109
|
+
execution: "execution",
|
|
110
|
+
verification: "verification",
|
|
111
|
+
orchestration: "orchestration",
|
|
112
|
+
docs: "documentation",
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const CONTEXT_MODE_GUIDANCE =
|
|
116
|
+
"Use `gsd_exec` for noisy scans, builds, and tests so full output stays out of prompt context; call `gsd_exec_search` before repeating prior runs; call `gsd_resume` after compaction or resume to recover stored execution context.";
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Render the Context Mode instruction lane for a unit type. Unknown unit
|
|
120
|
+
* types, disabled config, and explicit `contextMode: "none"` all omit the
|
|
121
|
+
* block so callers can prefix this safely without extra branching.
|
|
122
|
+
*/
|
|
123
|
+
export function composeContextModeInstructions(
|
|
124
|
+
unitType: string,
|
|
125
|
+
opts: ComposeContextModeInstructionOptions,
|
|
126
|
+
): string {
|
|
127
|
+
if (!opts.enabled) return "";
|
|
128
|
+
const manifest = resolveManifest(unitType);
|
|
129
|
+
if (!manifest || manifest.contextMode === "none") return "";
|
|
130
|
+
|
|
131
|
+
const lane = CONTEXT_MODE_LANE_LABELS[manifest.contextMode];
|
|
132
|
+
if (opts.renderMode === "nested") {
|
|
133
|
+
return `Context Mode (${lane} lane): ${CONTEXT_MODE_GUIDANCE}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return [
|
|
137
|
+
"## Context Mode",
|
|
138
|
+
"",
|
|
139
|
+
`Lane: **${lane} lane**.`,
|
|
140
|
+
CONTEXT_MODE_GUIDANCE,
|
|
141
|
+
].join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
95
144
|
// ─── v2 surface (#4924) ───────────────────────────────────────────────────
|
|
96
145
|
|
|
97
146
|
/**
|