@duckwalk/mcp-server 0.1.2
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/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/chunk-IIW4JPFI.js +1295 -0
- package/dist/chunk-IIW4JPFI.js.map +1 -0
- package/dist/index.d.ts +568 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +239 -0
- package/dist/server.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1295 @@
|
|
|
1
|
+
// src/contract.ts
|
|
2
|
+
function getDuckWalkContract() {
|
|
3
|
+
return {
|
|
4
|
+
server: {
|
|
5
|
+
name: "duckwalk-mcp",
|
|
6
|
+
version: "0.1.2"
|
|
7
|
+
},
|
|
8
|
+
guidance: {
|
|
9
|
+
summary: "Use this contract instead of searching the duckWalk repo or the user's home directory for examples.",
|
|
10
|
+
workspaceRoot: "Always pass workspaceRoot as the absolute target task workspace path when creating, reading, or updating guided sessions.",
|
|
11
|
+
gitignore: "Session creation automatically adds .guided-implementation/ to the target workspace .gitignore when no equivalent ignore rule already exists.",
|
|
12
|
+
commentStyle: "For functions or non-trivial logic in ghostCode, include short pragmatic comments that say what the code does. Only use an `Important:` comment when the behavior is safety-critical, stateful, or easy to misuse.",
|
|
13
|
+
pathfinderAuthoringHints: [
|
|
14
|
+
"Start at the real entrypoint for the user's question, such as middleware, controller, route, job handler, or event subscriber.",
|
|
15
|
+
"Follow calls, hooks, guards, and data handoffs in execution order until the question is answered.",
|
|
16
|
+
"Use one step per touchpoint, not one step per file.",
|
|
17
|
+
"Use named subranges so the walkthrough can distinguish action ranges from supporting context.",
|
|
18
|
+
"Fill step links explicitly so the graph and story can show why control moves to the next touchpoint."
|
|
19
|
+
],
|
|
20
|
+
recommendedFlow: [
|
|
21
|
+
"Inspect only the current task workspace to choose file targets.",
|
|
22
|
+
"Call get_duckwalk_contract when you need the session shape or examples.",
|
|
23
|
+
"Call create_guided_session for implementation playback or create_pr_review_session for review playback.",
|
|
24
|
+
"Call pathfinder for question-driven codebase walkthroughs that explain architecture flow.",
|
|
25
|
+
"Only call get_guided_session when you explicitly need current session data.",
|
|
26
|
+
"Only call update_step_status when the user explicitly wants step state changed from Codex."
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
tools: {
|
|
30
|
+
get_duckwalk_contract: {
|
|
31
|
+
description: "Returns the duckWalk contract, rules, and example payloads."
|
|
32
|
+
},
|
|
33
|
+
create_guided_session: {
|
|
34
|
+
description: "Validate and persist an implementation guided session.",
|
|
35
|
+
input: {
|
|
36
|
+
workspaceRoot: "string, absolute path, recommended",
|
|
37
|
+
session: "GuidedSession with mode implementation"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
create_pr_review_session: {
|
|
41
|
+
description: "Validate and persist a PR review guided session.",
|
|
42
|
+
input: {
|
|
43
|
+
workspaceRoot: "string, absolute path, recommended",
|
|
44
|
+
session: "GuidedSession with mode pr_review"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
pathfinder: {
|
|
48
|
+
description: "Validate and persist a question-driven codebase walkthrough guided session.",
|
|
49
|
+
input: {
|
|
50
|
+
workspaceRoot: "string, absolute path, recommended",
|
|
51
|
+
session: "GuidedSession with mode codebase_walkthrough"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
get_guided_session: {
|
|
55
|
+
description: "Read the current session or a specific session by ID.",
|
|
56
|
+
input: {
|
|
57
|
+
workspaceRoot: "string, absolute path, recommended",
|
|
58
|
+
sessionId: "optional string"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
update_step_status: {
|
|
62
|
+
description: "Update one step status in the current guided session state.",
|
|
63
|
+
input: {
|
|
64
|
+
workspaceRoot: "string, absolute path, recommended",
|
|
65
|
+
sessionId: "string",
|
|
66
|
+
stepId: "string",
|
|
67
|
+
status: ["pending", "active", "complete", "skipped"]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
schema: {
|
|
72
|
+
sessionModes: ["implementation", "pr_review", "codebase_walkthrough"],
|
|
73
|
+
implementationStepRequiredFields: [
|
|
74
|
+
"id",
|
|
75
|
+
"order",
|
|
76
|
+
"mode",
|
|
77
|
+
"file",
|
|
78
|
+
"location",
|
|
79
|
+
"explanation",
|
|
80
|
+
"ghostCode"
|
|
81
|
+
],
|
|
82
|
+
prReviewStepRequiredFields: [
|
|
83
|
+
"id",
|
|
84
|
+
"order",
|
|
85
|
+
"mode",
|
|
86
|
+
"file",
|
|
87
|
+
"location",
|
|
88
|
+
"explanation",
|
|
89
|
+
"review"
|
|
90
|
+
],
|
|
91
|
+
codebaseWalkthroughRequiredFields: [
|
|
92
|
+
"question",
|
|
93
|
+
"lens",
|
|
94
|
+
"flow",
|
|
95
|
+
"id",
|
|
96
|
+
"order",
|
|
97
|
+
"mode",
|
|
98
|
+
"touchpoint",
|
|
99
|
+
"confidence",
|
|
100
|
+
"evidenceQuality",
|
|
101
|
+
"fileRationale",
|
|
102
|
+
"file",
|
|
103
|
+
"location",
|
|
104
|
+
"explanation",
|
|
105
|
+
"snippet",
|
|
106
|
+
"subranges"
|
|
107
|
+
],
|
|
108
|
+
walkthroughFlowFields: ["summary", "path", "entrypoint?", "outcome?"],
|
|
109
|
+
walkthroughSubrangeFields: [
|
|
110
|
+
"id",
|
|
111
|
+
"label",
|
|
112
|
+
"role",
|
|
113
|
+
"range",
|
|
114
|
+
"summary?",
|
|
115
|
+
"snippet?",
|
|
116
|
+
"symbols?"
|
|
117
|
+
],
|
|
118
|
+
walkthroughLinkFields: ["stepId", "subrangeId?", "type", "why", "viaSymbol?"],
|
|
119
|
+
walkthroughBranchFields: [
|
|
120
|
+
"id",
|
|
121
|
+
"label",
|
|
122
|
+
"condition",
|
|
123
|
+
"outcome",
|
|
124
|
+
"targetStepId?",
|
|
125
|
+
"targetSubrangeId?"
|
|
126
|
+
],
|
|
127
|
+
walkthroughFollowUpFields: ["id", "kind", "label", "description", "stepId?", "file?"],
|
|
128
|
+
optionalStepFields: ["links?", "branches?", "symbols?"],
|
|
129
|
+
guidedFileTargetFields: ["path", "exists?", "createIfMissing?"],
|
|
130
|
+
locationStrategies: ["create_file", "line", "range", "after_text", "before_text"],
|
|
131
|
+
explanationFields: ["title", "what", "why", "how?", "impact?", "risk?", "narration?"],
|
|
132
|
+
validation: {
|
|
133
|
+
default: {
|
|
134
|
+
type: "normalised_match"
|
|
135
|
+
},
|
|
136
|
+
optionalFields: ["expectedText", "scope"]
|
|
137
|
+
},
|
|
138
|
+
prReviewRangeRule: "Each pr_review step must include a usable range through location.range or review.changedRange.",
|
|
139
|
+
codebaseWalkthroughRangeRule: "Each codebase_walkthrough step must include location.strategy = range and a usable location.range.",
|
|
140
|
+
codebaseWalkthroughSubrangeRules: [
|
|
141
|
+
"Each codebase_walkthrough step must include named subranges.",
|
|
142
|
+
"Exactly one subrange must use role primary and it must match location.range.",
|
|
143
|
+
"Additional subranges should usually use role action or context.",
|
|
144
|
+
"Subrange IDs and ranges must be unique within the step.",
|
|
145
|
+
"Links must point to real step IDs in the same walkthrough.",
|
|
146
|
+
"When links or branches target subranges, the target subrange ID must exist on the target step."
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
examples: {
|
|
150
|
+
create_guided_session: {
|
|
151
|
+
workspaceRoot: "/absolute/path/to/task/workspace",
|
|
152
|
+
session: {
|
|
153
|
+
id: "feature-auth-middleware",
|
|
154
|
+
mode: "implementation",
|
|
155
|
+
title: "Create auth middleware",
|
|
156
|
+
summary: "Adds a reusable auth middleware and wires it into routes.",
|
|
157
|
+
createdAt: "2026-06-18T00:00:00.000Z",
|
|
158
|
+
steps: [
|
|
159
|
+
{
|
|
160
|
+
id: "step-1",
|
|
161
|
+
order: 1,
|
|
162
|
+
mode: "implementation",
|
|
163
|
+
file: {
|
|
164
|
+
path: "src/middleware/auth.ts",
|
|
165
|
+
createIfMissing: true
|
|
166
|
+
},
|
|
167
|
+
location: {
|
|
168
|
+
strategy: "create_file"
|
|
169
|
+
},
|
|
170
|
+
explanation: {
|
|
171
|
+
title: "Create the auth middleware",
|
|
172
|
+
what: "Adds a reusable authorization middleware.",
|
|
173
|
+
why: "Route handlers should not repeat auth checks."
|
|
174
|
+
},
|
|
175
|
+
ghostCode: "import type { FastifyReply, FastifyRequest } from 'fastify';\n\n// Rejects unauthenticated requests before route handlers run.\nexport async function authMiddleware(request: FastifyRequest, reply: FastifyReply) {\n const authHeader = request.headers.authorization;\n\n // Important: fail fast so protected handlers never run without auth.\n if (!authHeader) {\n return reply.code(401).send({ error: 'Missing authorization header' });\n }\n}\n",
|
|
176
|
+
validation: {
|
|
177
|
+
type: "normalised_match"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
create_pr_review_session: {
|
|
184
|
+
workspaceRoot: "/absolute/path/to/task/workspace",
|
|
185
|
+
session: {
|
|
186
|
+
id: "review-auth-middleware",
|
|
187
|
+
mode: "pr_review",
|
|
188
|
+
title: "Review auth middleware changes",
|
|
189
|
+
summary: "Walks through the middleware and route wiring changes.",
|
|
190
|
+
createdAt: "2026-06-18T00:00:00.000Z",
|
|
191
|
+
steps: [
|
|
192
|
+
{
|
|
193
|
+
id: "review-step-1",
|
|
194
|
+
order: 1,
|
|
195
|
+
mode: "pr_review",
|
|
196
|
+
file: {
|
|
197
|
+
path: "src/middleware/auth.ts"
|
|
198
|
+
},
|
|
199
|
+
location: {
|
|
200
|
+
strategy: "range",
|
|
201
|
+
range: {
|
|
202
|
+
startLine: 1,
|
|
203
|
+
startCharacter: 0,
|
|
204
|
+
endLine: 12,
|
|
205
|
+
endCharacter: 0
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
explanation: {
|
|
209
|
+
title: "Review the middleware implementation",
|
|
210
|
+
what: "Adds a reusable auth check.",
|
|
211
|
+
why: "Routes should fail fast before business logic.",
|
|
212
|
+
impact: "Protected handlers now reject missing authorization headers."
|
|
213
|
+
},
|
|
214
|
+
review: {
|
|
215
|
+
beforeCode: "",
|
|
216
|
+
afterCode: "export async function authMiddleware() {}\n",
|
|
217
|
+
changedRange: {
|
|
218
|
+
startLine: 1,
|
|
219
|
+
startCharacter: 0,
|
|
220
|
+
endLine: 12,
|
|
221
|
+
endCharacter: 0
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
pathfinder: {
|
|
229
|
+
workspaceRoot: "/absolute/path/to/task/workspace",
|
|
230
|
+
session: {
|
|
231
|
+
id: "walkthrough-authentication-flow",
|
|
232
|
+
mode: "codebase_walkthrough",
|
|
233
|
+
title: "Trace backend authentication flow",
|
|
234
|
+
summary: "Shows how a protected request moves from middleware into token validation.",
|
|
235
|
+
question: "How does authentication work in this backend project?",
|
|
236
|
+
lens: "permission_flow",
|
|
237
|
+
flow: {
|
|
238
|
+
summary: "Request -> auth middleware -> auth service -> route guard",
|
|
239
|
+
path: ["Request", "authMiddleware", "resolveAuthenticatedUser", "requireRole"],
|
|
240
|
+
entrypoint: "HTTP request to a protected route",
|
|
241
|
+
outcome: "Only authenticated requests with the right role reach the handler."
|
|
242
|
+
},
|
|
243
|
+
followUps: [
|
|
244
|
+
{
|
|
245
|
+
id: "follow-up-tests",
|
|
246
|
+
kind: "tests",
|
|
247
|
+
label: "Inspect auth tests",
|
|
248
|
+
description: "Open the middleware tests to confirm the success path and failure branches.",
|
|
249
|
+
file: "tests/auth/middleware.test.ts"
|
|
250
|
+
}
|
|
251
|
+
],
|
|
252
|
+
createdAt: "2026-06-18T00:00:00.000Z",
|
|
253
|
+
steps: [
|
|
254
|
+
{
|
|
255
|
+
id: "walkthrough-step-1",
|
|
256
|
+
order: 1,
|
|
257
|
+
mode: "codebase_walkthrough",
|
|
258
|
+
touchpoint: "entry",
|
|
259
|
+
confidence: "direct",
|
|
260
|
+
evidenceQuality: "high",
|
|
261
|
+
fileRationale: "This file is the first protected-route touchpoint where authentication begins.",
|
|
262
|
+
file: {
|
|
263
|
+
path: "src/auth/middleware.ts"
|
|
264
|
+
},
|
|
265
|
+
location: {
|
|
266
|
+
strategy: "range",
|
|
267
|
+
range: {
|
|
268
|
+
startLine: 1,
|
|
269
|
+
startCharacter: 0,
|
|
270
|
+
endLine: 12,
|
|
271
|
+
endCharacter: 0
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
subranges: [
|
|
275
|
+
{
|
|
276
|
+
id: "middleware-entry",
|
|
277
|
+
label: "Middleware entry",
|
|
278
|
+
role: "primary",
|
|
279
|
+
range: {
|
|
280
|
+
startLine: 1,
|
|
281
|
+
startCharacter: 0,
|
|
282
|
+
endLine: 12,
|
|
283
|
+
endCharacter: 0
|
|
284
|
+
},
|
|
285
|
+
summary: "Reads the Authorization header and extracts the bearer token.",
|
|
286
|
+
symbols: ["authMiddleware"]
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "downstream-policy-context",
|
|
290
|
+
label: "Downstream policy context",
|
|
291
|
+
role: "context",
|
|
292
|
+
range: {
|
|
293
|
+
startLine: 130,
|
|
294
|
+
startCharacter: 0,
|
|
295
|
+
endLine: 190,
|
|
296
|
+
endCharacter: 0
|
|
297
|
+
},
|
|
298
|
+
summary: "Later role checks depend on the authenticated user injected here.",
|
|
299
|
+
symbols: ["requireRole"]
|
|
300
|
+
}
|
|
301
|
+
],
|
|
302
|
+
symbols: ["authMiddleware", "resolveAuthenticatedUser", "requireRole"],
|
|
303
|
+
explanation: {
|
|
304
|
+
title: "Start at the auth middleware",
|
|
305
|
+
what: "This middleware extracts the bearer token from the request.",
|
|
306
|
+
why: "Every protected route enters the authentication flow here.",
|
|
307
|
+
how: "The request header is parsed and the token is passed to the downstream auth service.",
|
|
308
|
+
impact: "Requests without a token fail before route handlers run."
|
|
309
|
+
},
|
|
310
|
+
snippet: "export async function authMiddleware(request, reply) {\n const authHeader = request.headers.authorization;\n const token = authHeader?.replace('Bearer ', '');\n}\n",
|
|
311
|
+
links: [
|
|
312
|
+
{
|
|
313
|
+
stepId: "walkthrough-step-2",
|
|
314
|
+
subrangeId: "token-validate",
|
|
315
|
+
type: "calls",
|
|
316
|
+
why: "The extracted token is validated by the auth service before the request can continue.",
|
|
317
|
+
viaSymbol: "resolveAuthenticatedUser"
|
|
318
|
+
}
|
|
319
|
+
],
|
|
320
|
+
branches: [
|
|
321
|
+
{
|
|
322
|
+
id: "missing-token",
|
|
323
|
+
label: "Missing token",
|
|
324
|
+
condition: "The Authorization header is missing or malformed.",
|
|
325
|
+
outcome: "The request fails before the route handler runs."
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: "success-path",
|
|
329
|
+
label: "Success path",
|
|
330
|
+
condition: "A bearer token is present.",
|
|
331
|
+
outcome: "The request continues into the token validation step.",
|
|
332
|
+
targetStepId: "walkthrough-step-2",
|
|
333
|
+
targetSubrangeId: "token-validate"
|
|
334
|
+
}
|
|
335
|
+
]
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: "walkthrough-step-2",
|
|
339
|
+
order: 2,
|
|
340
|
+
mode: "codebase_walkthrough",
|
|
341
|
+
touchpoint: "transform",
|
|
342
|
+
confidence: "direct",
|
|
343
|
+
evidenceQuality: "high",
|
|
344
|
+
fileRationale: "This file turns the raw token into a trusted identity object for downstream guards.",
|
|
345
|
+
file: {
|
|
346
|
+
path: "src/auth/service.ts"
|
|
347
|
+
},
|
|
348
|
+
location: {
|
|
349
|
+
strategy: "range",
|
|
350
|
+
range: {
|
|
351
|
+
startLine: 10,
|
|
352
|
+
startCharacter: 0,
|
|
353
|
+
endLine: 24,
|
|
354
|
+
endCharacter: 0
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
subranges: [
|
|
358
|
+
{
|
|
359
|
+
id: "token-validate",
|
|
360
|
+
label: "Token validation",
|
|
361
|
+
role: "primary",
|
|
362
|
+
range: {
|
|
363
|
+
startLine: 10,
|
|
364
|
+
startCharacter: 0,
|
|
365
|
+
endLine: 24,
|
|
366
|
+
endCharacter: 0
|
|
367
|
+
},
|
|
368
|
+
summary: "Verifies the token and resolves the authenticated user.",
|
|
369
|
+
symbols: ["resolveAuthenticatedUser", "verifyToken"]
|
|
370
|
+
}
|
|
371
|
+
],
|
|
372
|
+
symbols: ["resolveAuthenticatedUser", "verifyToken"],
|
|
373
|
+
explanation: {
|
|
374
|
+
title: "Validate the token and resolve the user",
|
|
375
|
+
what: "The auth service verifies the token and builds the authenticated user context.",
|
|
376
|
+
why: "Downstream guards can only make authorization decisions from a trusted identity.",
|
|
377
|
+
how: "The token verifier decodes claims and returns a resolved actor for later guards."
|
|
378
|
+
},
|
|
379
|
+
snippet: "export async function resolveAuthenticatedUser(token) {\n const payload = await verifyToken(token);\n return { userId: payload.sub, roles: payload.roles };\n}\n"
|
|
380
|
+
}
|
|
381
|
+
]
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ../../packages/core/src/markdown-writer.ts
|
|
389
|
+
function formatStepLocation(step) {
|
|
390
|
+
const labels = [];
|
|
391
|
+
if (step.mode === "pr_review" && step.review.changedRange) {
|
|
392
|
+
labels.push(
|
|
393
|
+
`${step.review.changedRange.startLine}:${step.review.changedRange.startCharacter} - ${step.review.changedRange.endLine}:${step.review.changedRange.endCharacter}`
|
|
394
|
+
);
|
|
395
|
+
} else if (step.location.strategy === "range" && step.location.range) {
|
|
396
|
+
labels.push(
|
|
397
|
+
`${step.location.range.startLine}:${step.location.range.startCharacter} - ${step.location.range.endLine}:${step.location.range.endCharacter}`
|
|
398
|
+
);
|
|
399
|
+
} else if (step.location.strategy === "line" && step.location.line) {
|
|
400
|
+
labels.push(`${step.location.line}:${step.location.column ?? 0}`);
|
|
401
|
+
} else if ((step.location.strategy === "after_text" || step.location.strategy === "before_text") && step.location.anchorText) {
|
|
402
|
+
labels.push(step.location.anchorText);
|
|
403
|
+
}
|
|
404
|
+
for (const subrange of step.subranges ?? []) {
|
|
405
|
+
labels.push(
|
|
406
|
+
`${subrange.label} (${subrange.role}): ${subrange.range.startLine}:${subrange.range.startCharacter} - ${subrange.range.endLine}:${subrange.range.endCharacter}`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return labels.length > 0 ? labels.join("; ") : null;
|
|
410
|
+
}
|
|
411
|
+
function renderSessionMarkdown(session) {
|
|
412
|
+
const lines = [
|
|
413
|
+
`# ${session.title}`,
|
|
414
|
+
"",
|
|
415
|
+
`- Session ID: \`${session.id}\``,
|
|
416
|
+
`- Mode: \`${session.mode}\``,
|
|
417
|
+
`- Created At: \`${session.createdAt}\``,
|
|
418
|
+
"",
|
|
419
|
+
session.summary
|
|
420
|
+
];
|
|
421
|
+
if (session.mode === "codebase_walkthrough" && session.question) {
|
|
422
|
+
lines.push("", `Question: ${session.question}`);
|
|
423
|
+
}
|
|
424
|
+
if (session.mode === "codebase_walkthrough" && session.lens) {
|
|
425
|
+
lines.push(`Lens: ${session.lens}`);
|
|
426
|
+
}
|
|
427
|
+
if (session.mode === "codebase_walkthrough" && session.flow) {
|
|
428
|
+
lines.push("", "## Flow Summary", "");
|
|
429
|
+
lines.push(`- Summary: ${session.flow.summary}`);
|
|
430
|
+
lines.push(`- Path: ${session.flow.path.join(" -> ")}`);
|
|
431
|
+
if (session.flow.entrypoint) {
|
|
432
|
+
lines.push(`- Entrypoint: ${session.flow.entrypoint}`);
|
|
433
|
+
}
|
|
434
|
+
if (session.flow.outcome) {
|
|
435
|
+
lines.push(`- Outcome: ${session.flow.outcome}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
lines.push("");
|
|
439
|
+
for (const step of [...session.steps].sort((left, right) => left.order - right.order)) {
|
|
440
|
+
lines.push(`## Step ${step.order}: ${step.explanation.title}`, "");
|
|
441
|
+
lines.push(`- File: \`${step.file.path}\``);
|
|
442
|
+
lines.push(`- Location strategy: \`${step.location.strategy}\``);
|
|
443
|
+
const locationDetail = formatStepLocation(step);
|
|
444
|
+
if (locationDetail) {
|
|
445
|
+
lines.push(`- Where: ${locationDetail}`);
|
|
446
|
+
}
|
|
447
|
+
if (step.mode === "codebase_walkthrough") {
|
|
448
|
+
lines.push(`- Touchpoint: ${step.touchpoint}`);
|
|
449
|
+
lines.push(`- Confidence: ${step.confidence}`);
|
|
450
|
+
lines.push(`- Evidence quality: ${step.evidenceQuality}`);
|
|
451
|
+
lines.push(`- File rationale: ${step.fileRationale}`);
|
|
452
|
+
}
|
|
453
|
+
lines.push(`- What: ${step.explanation.what}`);
|
|
454
|
+
lines.push(`- Why: ${step.explanation.why}`);
|
|
455
|
+
if (step.explanation.how) {
|
|
456
|
+
lines.push(`- How: ${step.explanation.how}`);
|
|
457
|
+
}
|
|
458
|
+
if (step.explanation.impact) {
|
|
459
|
+
lines.push(`- Impact: ${step.explanation.impact}`);
|
|
460
|
+
}
|
|
461
|
+
if (step.explanation.risk) {
|
|
462
|
+
lines.push(`- Risk: ${step.explanation.risk}`);
|
|
463
|
+
}
|
|
464
|
+
if (step.symbols?.length) {
|
|
465
|
+
lines.push(`- Symbols: ${step.symbols.join(", ")}`);
|
|
466
|
+
}
|
|
467
|
+
lines.push("");
|
|
468
|
+
if (step.mode === "implementation") {
|
|
469
|
+
lines.push("```ts", step.ghostCode, "```", "");
|
|
470
|
+
} else if (step.mode === "pr_review") {
|
|
471
|
+
if (step.review.beforeCode) {
|
|
472
|
+
lines.push("### Before", "", "```ts", step.review.beforeCode, "```", "");
|
|
473
|
+
}
|
|
474
|
+
if (step.review.afterCode) {
|
|
475
|
+
lines.push("### After", "", "```ts", step.review.afterCode, "```", "");
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
lines.push("### Snippet", "", "```ts", step.snippet, "```", "");
|
|
479
|
+
if (step.subranges?.length) {
|
|
480
|
+
lines.push("### Evidence", "");
|
|
481
|
+
for (const subrange of step.subranges) {
|
|
482
|
+
lines.push(
|
|
483
|
+
`- ${subrange.label} [${subrange.role}] ${subrange.range.startLine}:${subrange.range.startCharacter} - ${subrange.range.endLine}:${subrange.range.endCharacter}`
|
|
484
|
+
);
|
|
485
|
+
if (subrange.summary) {
|
|
486
|
+
lines.push(` Summary: ${subrange.summary}`);
|
|
487
|
+
}
|
|
488
|
+
if (subrange.symbols?.length) {
|
|
489
|
+
lines.push(` Symbols: ${subrange.symbols.join(", ")}`);
|
|
490
|
+
}
|
|
491
|
+
if (subrange.snippet) {
|
|
492
|
+
lines.push("", "```ts", subrange.snippet, "```");
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
lines.push("");
|
|
496
|
+
}
|
|
497
|
+
if (step.links?.length) {
|
|
498
|
+
lines.push("### Links", "");
|
|
499
|
+
for (const link of step.links) {
|
|
500
|
+
const via = link.viaSymbol ? ` via ${link.viaSymbol}` : "";
|
|
501
|
+
const target = link.subrangeId ? `${link.stepId}#${link.subrangeId}` : link.stepId;
|
|
502
|
+
lines.push(`- ${link.type} -> ${target}${via}: ${link.why}`);
|
|
503
|
+
}
|
|
504
|
+
lines.push("");
|
|
505
|
+
}
|
|
506
|
+
if (step.branches?.length) {
|
|
507
|
+
lines.push("### Branches", "");
|
|
508
|
+
for (const branch of step.branches) {
|
|
509
|
+
const target = branch.targetStepId ? ` -> ${branch.targetStepId}${branch.targetSubrangeId ? `#${branch.targetSubrangeId}` : ""}` : "";
|
|
510
|
+
lines.push(`- ${branch.label}${target}`);
|
|
511
|
+
lines.push(` Condition: ${branch.condition}`);
|
|
512
|
+
lines.push(` Outcome: ${branch.outcome}`);
|
|
513
|
+
}
|
|
514
|
+
lines.push("");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (session.mode === "codebase_walkthrough" && session.followUps?.length) {
|
|
519
|
+
lines.push("## Follow-ups", "");
|
|
520
|
+
for (const followUp of session.followUps) {
|
|
521
|
+
const target = followUp.stepId ? `step ${followUp.stepId}` : followUp.file ? `file ${followUp.file}` : "general";
|
|
522
|
+
lines.push(`- ${followUp.label} [${followUp.kind}] (${target})`);
|
|
523
|
+
lines.push(` ${followUp.description}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return `${lines.join("\n").trim()}
|
|
527
|
+
`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ../../packages/core/src/recipe-writer.ts
|
|
531
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
532
|
+
import path2 from "path";
|
|
533
|
+
|
|
534
|
+
// ../../packages/core/src/state.ts
|
|
535
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
536
|
+
import path from "path";
|
|
537
|
+
import { z } from "zod";
|
|
538
|
+
var guidedPaths = {
|
|
539
|
+
directoryName: ".guided-implementation",
|
|
540
|
+
currentRecipeName: "current.recipe.json",
|
|
541
|
+
currentMarkdownName: "current.recipe.md",
|
|
542
|
+
stateName: "state.json",
|
|
543
|
+
sessionsDirectoryName: "sessions"
|
|
544
|
+
};
|
|
545
|
+
var stepStateSchema = z.object({
|
|
546
|
+
status: z.enum(["pending", "active", "complete", "skipped"]),
|
|
547
|
+
completedAt: z.string().optional()
|
|
548
|
+
});
|
|
549
|
+
var guidedSessionStateSchema = z.object({
|
|
550
|
+
sessionId: z.string().min(1),
|
|
551
|
+
activeStepId: z.string().min(1).nullable(),
|
|
552
|
+
activeStepOrder: z.number().int().positive().nullable(),
|
|
553
|
+
updatedAt: z.string().min(1),
|
|
554
|
+
steps: z.record(stepStateSchema)
|
|
555
|
+
});
|
|
556
|
+
function resolveGuidedPaths(rootDir) {
|
|
557
|
+
const baseDir = path.join(rootDir, guidedPaths.directoryName);
|
|
558
|
+
return {
|
|
559
|
+
baseDir,
|
|
560
|
+
currentRecipePath: path.join(baseDir, guidedPaths.currentRecipeName),
|
|
561
|
+
currentMarkdownPath: path.join(baseDir, guidedPaths.currentMarkdownName),
|
|
562
|
+
statePath: path.join(baseDir, guidedPaths.stateName),
|
|
563
|
+
sessionsDir: path.join(baseDir, guidedPaths.sessionsDirectoryName)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function getSessionStatePath(paths, sessionId) {
|
|
567
|
+
return path.join(paths.sessionsDir, `${sessionId}.state.json`);
|
|
568
|
+
}
|
|
569
|
+
function createInitialSessionState(session) {
|
|
570
|
+
const orderedSteps = [...session.steps].sort((left, right) => left.order - right.order);
|
|
571
|
+
const firstStep = orderedSteps[0] ?? null;
|
|
572
|
+
const steps = {};
|
|
573
|
+
for (const step of orderedSteps) {
|
|
574
|
+
steps[step.id] = {
|
|
575
|
+
status: step.id === firstStep?.id ? "active" : "pending"
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
sessionId: session.id,
|
|
580
|
+
activeStepId: firstStep?.id ?? null,
|
|
581
|
+
activeStepOrder: firstStep?.order ?? null,
|
|
582
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
583
|
+
steps
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
async function ensureGuidedDirectories(rootDir) {
|
|
587
|
+
const paths = resolveGuidedPaths(rootDir);
|
|
588
|
+
await mkdir(paths.baseDir, { recursive: true });
|
|
589
|
+
await mkdir(paths.sessionsDir, { recursive: true });
|
|
590
|
+
return paths;
|
|
591
|
+
}
|
|
592
|
+
async function readGuidedState(rootDir, sessionId) {
|
|
593
|
+
const paths = resolveGuidedPaths(rootDir);
|
|
594
|
+
const statePath = sessionId ? getSessionStatePath(paths, sessionId) : paths.statePath;
|
|
595
|
+
try {
|
|
596
|
+
const raw = await readFile(statePath, "utf8");
|
|
597
|
+
return guidedSessionStateSchema.parse(JSON.parse(raw));
|
|
598
|
+
} catch {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async function writeGuidedState(rootDir, state, options = {}) {
|
|
603
|
+
const paths = await ensureGuidedDirectories(rootDir);
|
|
604
|
+
const nextState = {
|
|
605
|
+
...state,
|
|
606
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
607
|
+
};
|
|
608
|
+
const payload = JSON.stringify(nextState, null, 2);
|
|
609
|
+
const sessionStatePath = getSessionStatePath(paths, nextState.sessionId);
|
|
610
|
+
await writeFile(sessionStatePath, payload);
|
|
611
|
+
if (options.writeCurrent ?? true) {
|
|
612
|
+
await writeFile(paths.statePath, payload);
|
|
613
|
+
return paths.statePath;
|
|
614
|
+
}
|
|
615
|
+
return sessionStatePath;
|
|
616
|
+
}
|
|
617
|
+
async function updateGuidedStepStatus(rootDir, session, stepId, status, options = {}) {
|
|
618
|
+
const existing = await readGuidedState(rootDir, session.id) ?? createInitialSessionState(session);
|
|
619
|
+
const orderedSteps = [...session.steps].sort((left, right) => left.order - right.order);
|
|
620
|
+
const currentIndex = orderedSteps.findIndex((step) => step.id === stepId);
|
|
621
|
+
if (currentIndex === -1) {
|
|
622
|
+
throw new Error(`Unknown step ID: ${stepId}`);
|
|
623
|
+
}
|
|
624
|
+
const nextSteps = {
|
|
625
|
+
...existing.steps,
|
|
626
|
+
[stepId]: {
|
|
627
|
+
status,
|
|
628
|
+
completedAt: status === "complete" ? (/* @__PURE__ */ new Date()).toISOString() : void 0
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
let activeStepId = existing.activeStepId;
|
|
632
|
+
let activeStepOrder = existing.activeStepOrder;
|
|
633
|
+
if (status === "complete") {
|
|
634
|
+
const nextStep = orderedSteps[currentIndex + 1];
|
|
635
|
+
if (nextStep) {
|
|
636
|
+
if (nextSteps[nextStep.id]?.status !== "complete") {
|
|
637
|
+
nextSteps[nextStep.id] = { status: "active" };
|
|
638
|
+
}
|
|
639
|
+
activeStepId = nextStep.id;
|
|
640
|
+
activeStepOrder = nextStep.order;
|
|
641
|
+
} else {
|
|
642
|
+
activeStepId = null;
|
|
643
|
+
activeStepOrder = null;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const nextState = {
|
|
647
|
+
sessionId: session.id,
|
|
648
|
+
activeStepId,
|
|
649
|
+
activeStepOrder,
|
|
650
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
651
|
+
steps: nextSteps
|
|
652
|
+
};
|
|
653
|
+
await writeGuidedState(rootDir, nextState, options);
|
|
654
|
+
return nextState;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ../../packages/core/src/recipe-writer.ts
|
|
658
|
+
var guidedImplementationIgnoreRule = ".guided-implementation/";
|
|
659
|
+
async function ensureGuidedImplementationGitignore(rootDir) {
|
|
660
|
+
const gitignorePath = path2.join(rootDir, ".gitignore");
|
|
661
|
+
let existing = "";
|
|
662
|
+
try {
|
|
663
|
+
existing = await readFile2(gitignorePath, "utf8");
|
|
664
|
+
} catch {
|
|
665
|
+
existing = "";
|
|
666
|
+
}
|
|
667
|
+
const hasRule = existing.split(/\r?\n/).map((line) => line.trim()).some(
|
|
668
|
+
(line) => line === guidedImplementationIgnoreRule || line === ".guided-implementation" || line === ".guided-implementation/*"
|
|
669
|
+
);
|
|
670
|
+
if (hasRule) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
674
|
+
await writeFile2(gitignorePath, `${existing}${prefix}${guidedImplementationIgnoreRule}
|
|
675
|
+
`);
|
|
676
|
+
}
|
|
677
|
+
async function writeRecipeFiles(rootDir, session) {
|
|
678
|
+
const paths = await ensureGuidedDirectories(rootDir);
|
|
679
|
+
const recipePayload = `${JSON.stringify(session, null, 2)}
|
|
680
|
+
`;
|
|
681
|
+
const markdownPayload = renderSessionMarkdown(session);
|
|
682
|
+
const sessionRecipePath = path2.join(paths.sessionsDir, `${session.id}.recipe.json`);
|
|
683
|
+
const sessionMarkdownPath = path2.join(paths.sessionsDir, `${session.id}.recipe.md`);
|
|
684
|
+
await ensureGuidedImplementationGitignore(rootDir);
|
|
685
|
+
await writeFile2(paths.currentRecipePath, recipePayload);
|
|
686
|
+
await writeFile2(paths.currentMarkdownPath, markdownPayload);
|
|
687
|
+
await writeFile2(sessionRecipePath, recipePayload);
|
|
688
|
+
await writeFile2(sessionMarkdownPath, markdownPayload);
|
|
689
|
+
await writeGuidedState(rootDir, createInitialSessionState(session));
|
|
690
|
+
return {
|
|
691
|
+
recipePath: paths.currentRecipePath,
|
|
692
|
+
markdownPath: paths.currentMarkdownPath,
|
|
693
|
+
statePath: paths.statePath,
|
|
694
|
+
sessionRecipePath,
|
|
695
|
+
sessionMarkdownPath
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
async function readGuidedSession(rootDir, sessionId) {
|
|
699
|
+
const paths = await ensureGuidedDirectories(rootDir);
|
|
700
|
+
const recipePath = sessionId ? path2.join(paths.sessionsDir, `${sessionId}.recipe.json`) : paths.currentRecipePath;
|
|
701
|
+
const raw = await readFile2(recipePath, "utf8");
|
|
702
|
+
return JSON.parse(raw);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ../../packages/core/src/session.ts
|
|
706
|
+
function getOrderedSteps(session) {
|
|
707
|
+
return [...session.steps].sort((left, right) => left.order - right.order);
|
|
708
|
+
}
|
|
709
|
+
function validateDuplicateStepIds(session) {
|
|
710
|
+
const seen = /* @__PURE__ */ new Set();
|
|
711
|
+
for (const step of session.steps) {
|
|
712
|
+
if (seen.has(step.id)) {
|
|
713
|
+
throw new Error(`Duplicate step ID detected: ${step.id}`);
|
|
714
|
+
}
|
|
715
|
+
seen.add(step.id);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function validateSessionOrder(session) {
|
|
719
|
+
const ordered = getOrderedSteps(session);
|
|
720
|
+
ordered.forEach((step, index) => {
|
|
721
|
+
const expectedOrder = index + 1;
|
|
722
|
+
if (step.order !== expectedOrder) {
|
|
723
|
+
throw new Error(
|
|
724
|
+
`Invalid step ordering. Expected step ${step.id} to have order ${expectedOrder}, received ${step.order}`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
function validateSessionIntegrity(session) {
|
|
730
|
+
validateDuplicateStepIds(session);
|
|
731
|
+
validateSessionOrder(session);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ../../packages/core/src/session-history.ts
|
|
735
|
+
import { readdir, writeFile as writeFile3 } from "fs/promises";
|
|
736
|
+
|
|
737
|
+
// ../../packages/schema/src/guided-session.ts
|
|
738
|
+
import { z as z2 } from "zod";
|
|
739
|
+
var sessionModeSchema = z2.enum([
|
|
740
|
+
"implementation",
|
|
741
|
+
"pr_review",
|
|
742
|
+
"codebase_walkthrough"
|
|
743
|
+
]);
|
|
744
|
+
var stepStatusSchema = z2.enum(["pending", "active", "complete", "skipped"]);
|
|
745
|
+
var narrationSchema = z2.object({
|
|
746
|
+
short: z2.string().min(1),
|
|
747
|
+
detailed: z2.string().min(1).optional()
|
|
748
|
+
});
|
|
749
|
+
var stepExplanationSchema = z2.object({
|
|
750
|
+
title: z2.string().min(1),
|
|
751
|
+
what: z2.string().min(1),
|
|
752
|
+
why: z2.string().min(1),
|
|
753
|
+
how: z2.string().min(1).optional(),
|
|
754
|
+
impact: z2.string().min(1).optional(),
|
|
755
|
+
risk: z2.string().min(1).optional(),
|
|
756
|
+
narration: narrationSchema.optional()
|
|
757
|
+
});
|
|
758
|
+
var guidedFileTargetSchema = z2.object({
|
|
759
|
+
path: z2.string().min(1),
|
|
760
|
+
exists: z2.boolean().optional(),
|
|
761
|
+
createIfMissing: z2.boolean().optional()
|
|
762
|
+
});
|
|
763
|
+
var guidedRangeSchema = z2.object({
|
|
764
|
+
startLine: z2.number().int().positive(),
|
|
765
|
+
startCharacter: z2.number().int().nonnegative().default(0),
|
|
766
|
+
endLine: z2.number().int().positive(),
|
|
767
|
+
endCharacter: z2.number().int().nonnegative().default(0)
|
|
768
|
+
});
|
|
769
|
+
var guidedLocationSchema = z2.object({
|
|
770
|
+
strategy: z2.enum(["create_file", "line", "range", "after_text", "before_text"]),
|
|
771
|
+
line: z2.number().int().positive().optional(),
|
|
772
|
+
column: z2.number().int().nonnegative().optional(),
|
|
773
|
+
range: guidedRangeSchema.optional(),
|
|
774
|
+
anchorText: z2.string().min(1).optional()
|
|
775
|
+
});
|
|
776
|
+
var walkthroughSubrangeRoleSchema = z2.enum(["primary", "action", "context"]);
|
|
777
|
+
var walkthroughLinkTypeSchema = z2.enum([
|
|
778
|
+
"calls",
|
|
779
|
+
"returns",
|
|
780
|
+
"guards",
|
|
781
|
+
"dispatches",
|
|
782
|
+
"reads",
|
|
783
|
+
"writes",
|
|
784
|
+
"configures",
|
|
785
|
+
"emits"
|
|
786
|
+
]);
|
|
787
|
+
var walkthroughTouchpointTypeSchema = z2.enum([
|
|
788
|
+
"entry",
|
|
789
|
+
"guard",
|
|
790
|
+
"read",
|
|
791
|
+
"write",
|
|
792
|
+
"transform",
|
|
793
|
+
"emit",
|
|
794
|
+
"respond",
|
|
795
|
+
"config"
|
|
796
|
+
]);
|
|
797
|
+
var walkthroughConfidenceSchema = z2.enum(["direct", "mixed", "inferred"]);
|
|
798
|
+
var walkthroughEvidenceQualitySchema = z2.enum(["high", "medium", "low"]);
|
|
799
|
+
var walkthroughLensSchema = z2.enum([
|
|
800
|
+
"request_flow",
|
|
801
|
+
"data_flow",
|
|
802
|
+
"permission_flow",
|
|
803
|
+
"error_path",
|
|
804
|
+
"config_dependency_flow"
|
|
805
|
+
]);
|
|
806
|
+
var walkthroughFollowUpKindSchema = z2.enum([
|
|
807
|
+
"implementation",
|
|
808
|
+
"tests",
|
|
809
|
+
"config",
|
|
810
|
+
"docs",
|
|
811
|
+
"investigate"
|
|
812
|
+
]);
|
|
813
|
+
var walkthroughSubrangeSchema = z2.object({
|
|
814
|
+
id: z2.string().min(1),
|
|
815
|
+
label: z2.string().min(1),
|
|
816
|
+
role: walkthroughSubrangeRoleSchema,
|
|
817
|
+
range: guidedRangeSchema,
|
|
818
|
+
summary: z2.string().min(1).optional(),
|
|
819
|
+
snippet: z2.string().min(1).optional(),
|
|
820
|
+
symbols: z2.array(z2.string().min(1)).min(1).optional()
|
|
821
|
+
});
|
|
822
|
+
var walkthroughLinkSchema = z2.object({
|
|
823
|
+
stepId: z2.string().min(1),
|
|
824
|
+
subrangeId: z2.string().min(1).optional(),
|
|
825
|
+
type: walkthroughLinkTypeSchema,
|
|
826
|
+
why: z2.string().min(1),
|
|
827
|
+
viaSymbol: z2.string().min(1).optional()
|
|
828
|
+
});
|
|
829
|
+
var walkthroughBranchSchema = z2.object({
|
|
830
|
+
id: z2.string().min(1),
|
|
831
|
+
label: z2.string().min(1),
|
|
832
|
+
condition: z2.string().min(1),
|
|
833
|
+
outcome: z2.string().min(1),
|
|
834
|
+
targetStepId: z2.string().min(1).optional(),
|
|
835
|
+
targetSubrangeId: z2.string().min(1).optional()
|
|
836
|
+
});
|
|
837
|
+
var walkthroughFlowSchema = z2.object({
|
|
838
|
+
summary: z2.string().min(1),
|
|
839
|
+
path: z2.array(z2.string().min(1)).min(2),
|
|
840
|
+
entrypoint: z2.string().min(1).optional(),
|
|
841
|
+
outcome: z2.string().min(1).optional()
|
|
842
|
+
});
|
|
843
|
+
var walkthroughFollowUpSchema = z2.object({
|
|
844
|
+
id: z2.string().min(1),
|
|
845
|
+
kind: walkthroughFollowUpKindSchema,
|
|
846
|
+
label: z2.string().min(1),
|
|
847
|
+
description: z2.string().min(1),
|
|
848
|
+
stepId: z2.string().min(1).optional(),
|
|
849
|
+
file: z2.string().min(1).optional()
|
|
850
|
+
});
|
|
851
|
+
var stepValidationSchema = z2.object({
|
|
852
|
+
type: z2.literal("normalised_match"),
|
|
853
|
+
expectedText: z2.string().optional(),
|
|
854
|
+
scope: z2.enum(["file", "range"]).optional()
|
|
855
|
+
});
|
|
856
|
+
var reviewPlaybackSchema = z2.object({
|
|
857
|
+
beforeCode: z2.string().optional(),
|
|
858
|
+
afterCode: z2.string().optional(),
|
|
859
|
+
changedRange: guidedRangeSchema.optional()
|
|
860
|
+
}).refine(
|
|
861
|
+
(value) => Boolean(value.beforeCode) || Boolean(value.afterCode) || Boolean(value.changedRange),
|
|
862
|
+
{
|
|
863
|
+
message: "review playback requires beforeCode, afterCode, or changedRange"
|
|
864
|
+
}
|
|
865
|
+
);
|
|
866
|
+
var baseStepSchema = z2.object({
|
|
867
|
+
id: z2.string().min(1),
|
|
868
|
+
order: z2.number().int().positive(),
|
|
869
|
+
mode: sessionModeSchema,
|
|
870
|
+
file: guidedFileTargetSchema,
|
|
871
|
+
location: guidedLocationSchema,
|
|
872
|
+
subranges: z2.array(walkthroughSubrangeSchema).min(1).optional(),
|
|
873
|
+
symbols: z2.array(z2.string().min(1)).min(1).optional(),
|
|
874
|
+
explanation: stepExplanationSchema,
|
|
875
|
+
validation: stepValidationSchema.optional(),
|
|
876
|
+
status: stepStatusSchema.optional()
|
|
877
|
+
});
|
|
878
|
+
var implementationStepSchema = baseStepSchema.extend({
|
|
879
|
+
mode: z2.literal("implementation"),
|
|
880
|
+
ghostCode: z2.string().min(1)
|
|
881
|
+
});
|
|
882
|
+
var prReviewStepSchema = baseStepSchema.extend({
|
|
883
|
+
mode: z2.literal("pr_review"),
|
|
884
|
+
review: reviewPlaybackSchema
|
|
885
|
+
});
|
|
886
|
+
var codebaseWalkthroughStepSchema = baseStepSchema.extend({
|
|
887
|
+
mode: z2.literal("codebase_walkthrough"),
|
|
888
|
+
touchpoint: walkthroughTouchpointTypeSchema,
|
|
889
|
+
confidence: walkthroughConfidenceSchema,
|
|
890
|
+
evidenceQuality: walkthroughEvidenceQualitySchema,
|
|
891
|
+
fileRationale: z2.string().min(1),
|
|
892
|
+
snippet: z2.string().min(1),
|
|
893
|
+
links: z2.array(walkthroughLinkSchema).optional(),
|
|
894
|
+
branches: z2.array(walkthroughBranchSchema).optional()
|
|
895
|
+
}).superRefine((step, context) => {
|
|
896
|
+
if (step.location.strategy !== "range" || !step.location.range) {
|
|
897
|
+
context.addIssue({
|
|
898
|
+
code: z2.ZodIssueCode.custom,
|
|
899
|
+
path: ["location"],
|
|
900
|
+
message: "codebase walkthrough steps require a location range"
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
if (!step.explanation.how) {
|
|
904
|
+
context.addIssue({
|
|
905
|
+
code: z2.ZodIssueCode.custom,
|
|
906
|
+
path: ["explanation", "how"],
|
|
907
|
+
message: "codebase walkthrough steps require explanation.how"
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
if (!step.subranges?.length) {
|
|
911
|
+
context.addIssue({
|
|
912
|
+
code: z2.ZodIssueCode.custom,
|
|
913
|
+
path: ["subranges"],
|
|
914
|
+
message: "codebase walkthrough steps require named subranges"
|
|
915
|
+
});
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const primarySubranges = step.subranges.filter((subrange) => subrange.role === "primary");
|
|
919
|
+
if (primarySubranges.length !== 1) {
|
|
920
|
+
context.addIssue({
|
|
921
|
+
code: z2.ZodIssueCode.custom,
|
|
922
|
+
path: ["subranges"],
|
|
923
|
+
message: "codebase walkthrough steps require exactly one primary subrange"
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
var guidedStepSchema = z2.union([
|
|
928
|
+
implementationStepSchema,
|
|
929
|
+
prReviewStepSchema,
|
|
930
|
+
codebaseWalkthroughStepSchema
|
|
931
|
+
]);
|
|
932
|
+
var guidedSessionSchema = z2.object({
|
|
933
|
+
id: z2.string().min(1),
|
|
934
|
+
mode: sessionModeSchema,
|
|
935
|
+
title: z2.string().min(1),
|
|
936
|
+
summary: z2.string().min(1),
|
|
937
|
+
createdAt: z2.string().min(1),
|
|
938
|
+
question: z2.string().min(1).optional(),
|
|
939
|
+
lens: walkthroughLensSchema.optional(),
|
|
940
|
+
flow: walkthroughFlowSchema.optional(),
|
|
941
|
+
followUps: z2.array(walkthroughFollowUpSchema).optional(),
|
|
942
|
+
steps: z2.array(guidedStepSchema).min(1)
|
|
943
|
+
}).superRefine((session, context) => {
|
|
944
|
+
if (session.mode === "codebase_walkthrough" && !session.question) {
|
|
945
|
+
context.addIssue({
|
|
946
|
+
code: z2.ZodIssueCode.custom,
|
|
947
|
+
path: ["question"],
|
|
948
|
+
message: "codebase walkthrough sessions require a question"
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
if (session.mode === "codebase_walkthrough" && !session.flow) {
|
|
952
|
+
context.addIssue({
|
|
953
|
+
code: z2.ZodIssueCode.custom,
|
|
954
|
+
path: ["flow"],
|
|
955
|
+
message: "codebase walkthrough sessions require a flow summary"
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
if (session.mode === "codebase_walkthrough" && !session.lens) {
|
|
959
|
+
context.addIssue({
|
|
960
|
+
code: z2.ZodIssueCode.custom,
|
|
961
|
+
path: ["lens"],
|
|
962
|
+
message: "codebase walkthrough sessions require a lens"
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
session.steps.forEach((step, index) => {
|
|
966
|
+
if (step.mode !== session.mode) {
|
|
967
|
+
context.addIssue({
|
|
968
|
+
code: z2.ZodIssueCode.custom,
|
|
969
|
+
path: ["steps", index, "mode"],
|
|
970
|
+
message: `step mode ${step.mode} does not match session mode ${session.mode}`
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// ../../packages/core/src/validation.ts
|
|
977
|
+
function normaliseCode(input) {
|
|
978
|
+
return input.replace(/\r\n/g, "\n").split("\n").map((line) => line.replace(/[ \t]+$/g, "")).join("\n").trim();
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/service.ts
|
|
982
|
+
import { z as z3 } from "zod";
|
|
983
|
+
|
|
984
|
+
// src/walkthrough-validation.ts
|
|
985
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
986
|
+
import path3 from "path";
|
|
987
|
+
function validatePrReviewStepRanges(session) {
|
|
988
|
+
for (const step of session.steps) {
|
|
989
|
+
if (step.mode !== "pr_review" || (step.location.strategy !== "range" || !step.location.range) && !step.review.changedRange) {
|
|
990
|
+
throw new Error(
|
|
991
|
+
`PR review step ${step.id} requires a location range or review.changedRange`
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
function validateCodebaseWalkthroughSession(session) {
|
|
997
|
+
const walkthroughSteps = session.steps.map(parseWalkthroughStep);
|
|
998
|
+
validateWalkthroughGraph(session, walkthroughSteps);
|
|
999
|
+
return walkthroughSteps;
|
|
1000
|
+
}
|
|
1001
|
+
async function validateCodebaseWalkthroughWorkspace(rootDir, walkthroughSteps) {
|
|
1002
|
+
for (const step of walkthroughSteps) {
|
|
1003
|
+
await validateWalkthroughFileEvidence(rootDir, step);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function parseWalkthroughStep(step) {
|
|
1007
|
+
if (step.mode !== "codebase_walkthrough") {
|
|
1008
|
+
throw new Error(`Codebase walkthrough step ${step.id} requires mode codebase_walkthrough`);
|
|
1009
|
+
}
|
|
1010
|
+
if (step.location.strategy !== "range" || !step.location.range) {
|
|
1011
|
+
throw new Error(`Codebase walkthrough step ${step.id} requires a location range`);
|
|
1012
|
+
}
|
|
1013
|
+
const walkthroughStep = codebaseWalkthroughStepSchema.parse(step);
|
|
1014
|
+
const locationRange = walkthroughStep.location.range;
|
|
1015
|
+
if (!locationRange) {
|
|
1016
|
+
throw new Error(`Codebase walkthrough step ${walkthroughStep.id} requires a location range`);
|
|
1017
|
+
}
|
|
1018
|
+
if (!walkthroughStep.subranges?.length) {
|
|
1019
|
+
throw new Error(`Codebase walkthrough step ${walkthroughStep.id} requires named subranges`);
|
|
1020
|
+
}
|
|
1021
|
+
const primarySubranges = walkthroughStep.subranges.filter(
|
|
1022
|
+
(subrange) => subrange.role === "primary"
|
|
1023
|
+
);
|
|
1024
|
+
const primarySubrange = primarySubranges[0];
|
|
1025
|
+
if (!primarySubrange || !rangeMatches(primarySubrange.range, locationRange)) {
|
|
1026
|
+
throw new Error(
|
|
1027
|
+
`Codebase walkthrough step ${walkthroughStep.id} primary subrange must match location.range`
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
const seenSubrangeIds = /* @__PURE__ */ new Set();
|
|
1031
|
+
const seenRanges = /* @__PURE__ */ new Set();
|
|
1032
|
+
for (const subrange of walkthroughStep.subranges) {
|
|
1033
|
+
if (seenSubrangeIds.has(subrange.id)) {
|
|
1034
|
+
throw new Error(
|
|
1035
|
+
`Codebase walkthrough step ${walkthroughStep.id} contains duplicate subrange id ${subrange.id}`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
seenSubrangeIds.add(subrange.id);
|
|
1039
|
+
const key = rangeKey(subrange.range);
|
|
1040
|
+
if (seenRanges.has(key)) {
|
|
1041
|
+
throw new Error(
|
|
1042
|
+
`Codebase walkthrough step ${walkthroughStep.id} contains duplicate subrange range ${key}`
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
seenRanges.add(key);
|
|
1046
|
+
}
|
|
1047
|
+
return walkthroughStep;
|
|
1048
|
+
}
|
|
1049
|
+
function rangeKey(range) {
|
|
1050
|
+
return `${range.startLine}:${range.startCharacter}-${range.endLine}:${range.endCharacter}`;
|
|
1051
|
+
}
|
|
1052
|
+
function rangeMatches(left, right) {
|
|
1053
|
+
return left.startLine === right.startLine && left.startCharacter === right.startCharacter && left.endLine === right.endLine && left.endCharacter === right.endCharacter;
|
|
1054
|
+
}
|
|
1055
|
+
function getRangeEvidenceText(content, range) {
|
|
1056
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
1057
|
+
const lines = normalized.split("\n");
|
|
1058
|
+
return lines.slice(range.startLine - 1, range.endLine).join("\n");
|
|
1059
|
+
}
|
|
1060
|
+
function snippetsOverlap(actualRangeText, expectedSnippet) {
|
|
1061
|
+
const normalizedActual = normaliseCode(actualRangeText);
|
|
1062
|
+
const expectedLines = normaliseCode(expectedSnippet).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1063
|
+
if (!expectedLines.length) {
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
const matchingLineCount = expectedLines.filter((line) => normalizedActual.includes(line)).length;
|
|
1067
|
+
const requiredMatches = Math.min(expectedLines.length, 2);
|
|
1068
|
+
return matchingLineCount >= requiredMatches;
|
|
1069
|
+
}
|
|
1070
|
+
function validateWalkthroughLinks(steps) {
|
|
1071
|
+
const stepIds = new Set(steps.map((step) => step.id));
|
|
1072
|
+
const subrangesByStepId = new Map(
|
|
1073
|
+
steps.map((step) => [step.id, new Set(step.subranges.map((subrange) => subrange.id))])
|
|
1074
|
+
);
|
|
1075
|
+
steps.forEach((step, index) => {
|
|
1076
|
+
const nextOrderedStep = steps[index + 1];
|
|
1077
|
+
if (nextOrderedStep && (!step.links || step.links.length === 0)) {
|
|
1078
|
+
throw new Error(`Codebase walkthrough step ${step.id} requires at least one outgoing link`);
|
|
1079
|
+
}
|
|
1080
|
+
const seenTargetIds = /* @__PURE__ */ new Set();
|
|
1081
|
+
for (const link of step.links ?? []) {
|
|
1082
|
+
if (!stepIds.has(link.stepId)) {
|
|
1083
|
+
throw new Error(`Codebase walkthrough step ${step.id} links to unknown step ${link.stepId}`);
|
|
1084
|
+
}
|
|
1085
|
+
if (link.subrangeId && !subrangesByStepId.get(link.stepId)?.has(link.subrangeId)) {
|
|
1086
|
+
throw new Error(
|
|
1087
|
+
`Codebase walkthrough step ${step.id} links to unknown subrange ${link.subrangeId} on ${link.stepId}`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
if (seenTargetIds.has(link.stepId)) {
|
|
1091
|
+
throw new Error(`Codebase walkthrough step ${step.id} links to ${link.stepId} more than once`);
|
|
1092
|
+
}
|
|
1093
|
+
seenTargetIds.add(link.stepId);
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
function validateWalkthroughBranches(steps) {
|
|
1098
|
+
const stepIds = new Set(steps.map((step) => step.id));
|
|
1099
|
+
const subrangesByStepId = new Map(
|
|
1100
|
+
steps.map((step) => [step.id, new Set(step.subranges.map((subrange) => subrange.id))])
|
|
1101
|
+
);
|
|
1102
|
+
for (const step of steps) {
|
|
1103
|
+
const seenBranchIds = /* @__PURE__ */ new Set();
|
|
1104
|
+
for (const branch of step.branches ?? []) {
|
|
1105
|
+
if (seenBranchIds.has(branch.id)) {
|
|
1106
|
+
throw new Error(`Codebase walkthrough step ${step.id} contains duplicate branch id ${branch.id}`);
|
|
1107
|
+
}
|
|
1108
|
+
seenBranchIds.add(branch.id);
|
|
1109
|
+
if (branch.targetSubrangeId && !branch.targetStepId) {
|
|
1110
|
+
throw new Error(
|
|
1111
|
+
`Codebase walkthrough step ${step.id} branch ${branch.id} requires targetStepId when targetSubrangeId is present`
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
if (branch.targetStepId && !stepIds.has(branch.targetStepId)) {
|
|
1115
|
+
throw new Error(
|
|
1116
|
+
`Codebase walkthrough step ${step.id} branch ${branch.id} points to unknown step ${branch.targetStepId}`
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
if (branch.targetStepId && branch.targetSubrangeId && !subrangesByStepId.get(branch.targetStepId)?.has(branch.targetSubrangeId)) {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
`Codebase walkthrough step ${step.id} branch ${branch.id} points to unknown subrange ${branch.targetSubrangeId} on ${branch.targetStepId}`
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function validateWalkthroughFollowUps(session, steps) {
|
|
1128
|
+
const stepIds = new Set(steps.map((step) => step.id));
|
|
1129
|
+
for (const followUp of session.followUps ?? []) {
|
|
1130
|
+
if (followUp.stepId && !stepIds.has(followUp.stepId)) {
|
|
1131
|
+
throw new Error(
|
|
1132
|
+
`Codebase walkthrough follow-up ${followUp.id} points to unknown step ${followUp.stepId}`
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function validateWalkthroughGraph(session, steps) {
|
|
1138
|
+
validateWalkthroughLinks(steps);
|
|
1139
|
+
validateWalkthroughBranches(steps);
|
|
1140
|
+
validateWalkthroughFollowUps(session, steps);
|
|
1141
|
+
}
|
|
1142
|
+
async function validateWalkthroughFileEvidence(rootDir, step) {
|
|
1143
|
+
const filePath = path3.join(rootDir, step.file.path);
|
|
1144
|
+
const fileContent = await readFile3(filePath, "utf8");
|
|
1145
|
+
const snippetMatchesAnyRange = step.subranges.some(
|
|
1146
|
+
(subrange) => snippetsOverlap(getRangeEvidenceText(fileContent, subrange.range), step.snippet)
|
|
1147
|
+
);
|
|
1148
|
+
if (!snippetMatchesAnyRange) {
|
|
1149
|
+
throw new Error(
|
|
1150
|
+
`Codebase walkthrough step ${step.id} snippet must overlap one of its declared subranges`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
for (const subrange of step.subranges) {
|
|
1154
|
+
if (!subrange.snippet) {
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
const rangeText = getRangeEvidenceText(fileContent, subrange.range);
|
|
1158
|
+
if (!snippetsOverlap(rangeText, subrange.snippet)) {
|
|
1159
|
+
throw new Error(
|
|
1160
|
+
`Codebase walkthrough step ${step.id} subrange ${subrange.id} snippet must overlap its range`
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/service.ts
|
|
1167
|
+
var updateStepStatusInputSchema = z3.object({
|
|
1168
|
+
sessionId: z3.string().min(1),
|
|
1169
|
+
stepId: z3.string().min(1),
|
|
1170
|
+
status: stepStatusSchema
|
|
1171
|
+
});
|
|
1172
|
+
var validateGuidedSessionInputSchema = z3.object({
|
|
1173
|
+
session: guidedSessionSchema,
|
|
1174
|
+
expectMode: sessionModeSchema.optional()
|
|
1175
|
+
});
|
|
1176
|
+
function validateGuidedSessionInput(input) {
|
|
1177
|
+
const payload = validateGuidedSessionInputSchema.parse(input);
|
|
1178
|
+
const session = guidedSessionSchema.parse(payload.session);
|
|
1179
|
+
if (payload.expectMode && session.mode !== payload.expectMode) {
|
|
1180
|
+
throw new Error(`Expected session mode "${payload.expectMode}" but received "${session.mode}"`);
|
|
1181
|
+
}
|
|
1182
|
+
validateSessionIntegrity(session);
|
|
1183
|
+
if (session.mode === "pr_review") {
|
|
1184
|
+
validatePrReviewStepRanges(session);
|
|
1185
|
+
}
|
|
1186
|
+
if (session.mode === "codebase_walkthrough") {
|
|
1187
|
+
validateCodebaseWalkthroughSession(session);
|
|
1188
|
+
}
|
|
1189
|
+
return {
|
|
1190
|
+
valid: true,
|
|
1191
|
+
session: {
|
|
1192
|
+
id: session.id,
|
|
1193
|
+
mode: session.mode,
|
|
1194
|
+
title: session.title,
|
|
1195
|
+
summary: session.summary,
|
|
1196
|
+
stepCount: session.steps.length,
|
|
1197
|
+
files: session.steps.map((step) => step.file.path),
|
|
1198
|
+
locationStrategies: [...new Set(session.steps.map((step) => step.location.strategy))]
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
async function isCurrentSession(rootDir, sessionId) {
|
|
1203
|
+
try {
|
|
1204
|
+
const currentSession = await readGuidedSession(rootDir);
|
|
1205
|
+
return currentSession.id === sessionId;
|
|
1206
|
+
} catch {
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
async function createGuidedSession(rootDir, sessionInput) {
|
|
1211
|
+
const session = guidedSessionSchema.parse(sessionInput);
|
|
1212
|
+
validateSessionIntegrity(session);
|
|
1213
|
+
const files = await writeRecipeFiles(rootDir, session);
|
|
1214
|
+
return {
|
|
1215
|
+
sessionId: session.id,
|
|
1216
|
+
recipePath: files.recipePath,
|
|
1217
|
+
markdownPath: files.markdownPath,
|
|
1218
|
+
statePath: files.statePath
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
async function createPrReviewSession(rootDir, sessionInput) {
|
|
1222
|
+
const session = guidedSessionSchema.parse(sessionInput);
|
|
1223
|
+
if (session.mode !== "pr_review") {
|
|
1224
|
+
throw new Error('create_pr_review_session requires mode "pr_review"');
|
|
1225
|
+
}
|
|
1226
|
+
validateSessionIntegrity(session);
|
|
1227
|
+
validatePrReviewStepRanges(session);
|
|
1228
|
+
const files = await writeRecipeFiles(rootDir, session);
|
|
1229
|
+
return {
|
|
1230
|
+
sessionId: session.id,
|
|
1231
|
+
recipePath: files.recipePath,
|
|
1232
|
+
markdownPath: files.markdownPath,
|
|
1233
|
+
statePath: files.statePath
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
async function pathfinder(rootDir, sessionInput) {
|
|
1237
|
+
const session = guidedSessionSchema.parse(sessionInput);
|
|
1238
|
+
if (session.mode !== "codebase_walkthrough") {
|
|
1239
|
+
throw new Error('pathfinder requires mode "codebase_walkthrough"');
|
|
1240
|
+
}
|
|
1241
|
+
validateSessionIntegrity(session);
|
|
1242
|
+
const walkthroughSteps = validateCodebaseWalkthroughSession(session);
|
|
1243
|
+
await validateCodebaseWalkthroughWorkspace(rootDir, walkthroughSteps);
|
|
1244
|
+
const files = await writeRecipeFiles(rootDir, session);
|
|
1245
|
+
return {
|
|
1246
|
+
sessionId: session.id,
|
|
1247
|
+
recipePath: files.recipePath,
|
|
1248
|
+
markdownPath: files.markdownPath,
|
|
1249
|
+
statePath: files.statePath
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
async function getGuidedSession(rootDir, sessionId) {
|
|
1253
|
+
const session = guidedSessionSchema.parse(await readGuidedSession(rootDir, sessionId));
|
|
1254
|
+
let state = await readGuidedState(rootDir, sessionId);
|
|
1255
|
+
if (!state && sessionId && await isCurrentSession(rootDir, sessionId)) {
|
|
1256
|
+
state = await readGuidedState(rootDir);
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
session,
|
|
1260
|
+
state: state ? guidedSessionStateSchema.parse(state) : null
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
async function updateStepStatus(rootDir, input) {
|
|
1264
|
+
const payload = updateStepStatusInputSchema.parse(input);
|
|
1265
|
+
const session = guidedSessionSchema.parse(await readGuidedSession(rootDir, payload.sessionId));
|
|
1266
|
+
if (session.id !== payload.sessionId) {
|
|
1267
|
+
throw new Error(`Session mismatch for ${payload.sessionId}`);
|
|
1268
|
+
}
|
|
1269
|
+
const state = await updateGuidedStepStatus(
|
|
1270
|
+
rootDir,
|
|
1271
|
+
session,
|
|
1272
|
+
payload.stepId,
|
|
1273
|
+
payload.status,
|
|
1274
|
+
{
|
|
1275
|
+
writeCurrent: await isCurrentSession(rootDir, session.id)
|
|
1276
|
+
}
|
|
1277
|
+
);
|
|
1278
|
+
return {
|
|
1279
|
+
sessionId: session.id,
|
|
1280
|
+
stepId: payload.stepId,
|
|
1281
|
+
status: payload.status,
|
|
1282
|
+
state: guidedSessionStateSchema.parse(state)
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
export {
|
|
1287
|
+
getDuckWalkContract,
|
|
1288
|
+
validateGuidedSessionInput,
|
|
1289
|
+
createGuidedSession,
|
|
1290
|
+
createPrReviewSession,
|
|
1291
|
+
pathfinder,
|
|
1292
|
+
getGuidedSession,
|
|
1293
|
+
updateStepStatus
|
|
1294
|
+
};
|
|
1295
|
+
//# sourceMappingURL=chunk-IIW4JPFI.js.map
|