@jskit-ai/jskit-cli 0.2.80 → 0.2.82

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.
Files changed (32) hide show
  1. package/package.json +6 -4
  2. package/src/server/appBlueprint.js +1 -1
  3. package/src/server/commandHandlers/helperMap.js +104 -0
  4. package/src/server/commandHandlers/session.js +179 -4
  5. package/src/server/commandHandlers/show.js +169 -34
  6. package/src/server/core/argParser.js +20 -0
  7. package/src/server/core/commandCatalog.js +70 -7
  8. package/src/server/core/createCommandHandlers.js +4 -1
  9. package/src/server/helperMap.js +463 -0
  10. package/src/server/helperMapPaths.js +7 -0
  11. package/src/server/sessionRuntime/appReadiness.js +55 -0
  12. package/src/server/sessionRuntime/constants.js +298 -135
  13. package/src/server/sessionRuntime/paths.js +2 -4
  14. package/src/server/sessionRuntime/preconditions.js +393 -26
  15. package/src/server/sessionRuntime/promptRenderer.js +15 -2
  16. package/src/server/sessionRuntime/prompts/app_blueprint.md +26 -2
  17. package/src/server/sessionRuntime/prompts/automated_checks.md +42 -0
  18. package/src/server/sessionRuntime/prompts/deep_ui_check.md +53 -0
  19. package/src/server/sessionRuntime/prompts/doctor_failure.md +21 -1
  20. package/src/server/sessionRuntime/prompts/execute_plan.md +61 -0
  21. package/src/server/sessionRuntime/prompts/final_comment.md +3 -1
  22. package/src/server/sessionRuntime/prompts/issue_details.md +52 -0
  23. package/src/server/sessionRuntime/prompts/new_issue.md +34 -3
  24. package/src/server/sessionRuntime/prompts/plan_issue.md +81 -0
  25. package/src/server/sessionRuntime/prompts/pr_failure.md +14 -1
  26. package/src/server/sessionRuntime/prompts/review_changes.md +77 -15
  27. package/src/server/sessionRuntime/prompts/update_blueprint.md +36 -0
  28. package/src/server/sessionRuntime/prompts/user_check.md +22 -4
  29. package/src/server/sessionRuntime/responses.js +877 -30
  30. package/src/server/sessionRuntime/worktrees.js +31 -0
  31. package/src/server/sessionRuntime.js +2070 -244
  32. package/src/server/sessionRuntime/prompts/implement_issue.md +0 -25
@@ -1,6 +1,10 @@
1
1
  import { mkdir, readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import {
4
+ CYCLE_STEP_IDS,
5
+ JSKIT_CLI_SHELL_COMMAND,
6
+ REVIEW_PASS_LIMIT,
7
+ SESSION_WORKFLOW_VERSION,
4
8
  SESSION_STATUS,
5
9
  STEP_DEFINITION_BY_ID,
6
10
  STEP_DEFINITIONS,
@@ -12,12 +16,19 @@ import {
12
16
  normalizeText,
13
17
  readTextIfExists,
14
18
  readTrimmedFile,
19
+ runGitInWorktree,
15
20
  timestampForReceipt,
16
21
  writeTextFile
17
22
  } from "./io.js";
18
23
  import {
19
24
  pathsForExistingSession
20
25
  } from "./paths.js";
26
+ import {
27
+ hasWorktree
28
+ } from "./worktrees.js";
29
+ import {
30
+ inspectReadyJskitAppRoot
31
+ } from "./appReadiness.js";
21
32
 
22
33
  function createError({
23
34
  code,
@@ -43,30 +54,384 @@ function createPrecondition({
43
54
  });
44
55
  }
45
56
 
46
- async function readCompletedSteps(sessionRoot) {
47
- const stepsRoot = path.join(sessionRoot, "steps");
57
+ function normalizeStepId(stepId) {
58
+ return normalizeText(stepId);
59
+ }
60
+
61
+ function stepIndex(stepId) {
62
+ return STEP_IDS.indexOf(normalizeStepId(stepId));
63
+ }
64
+
65
+ function normalizeKnownStepIds(stepIds = []) {
66
+ return Array.from(
67
+ new Set(
68
+ stepIds
69
+ .map((stepId) => normalizeText(stepId))
70
+ .filter((stepId) => STEP_IDS.includes(stepId))
71
+ )
72
+ ).sort((left, right) => STEP_IDS.indexOf(left) - STEP_IDS.indexOf(right));
73
+ }
74
+
75
+ function stepCanExposeStoredPrompt(stepId) {
76
+ const step = STEP_DEFINITION_BY_ID[normalizeStepId(stepId)];
77
+ return Boolean(step?.codex || step?.kind === "codex_prompt" || step?.kind === "human_input");
78
+ }
79
+
80
+ const DEFAULT_ACTIVE_CYCLE = "001";
81
+ const DEFAULT_REVIEW_PASS = "001";
82
+
83
+ function normalizeCycleNumber(value = "") {
84
+ const normalized = normalizeText(value).replace(/^cycle_/u, "");
85
+ if (!/^\d+$/u.test(normalized)) {
86
+ return DEFAULT_ACTIVE_CYCLE;
87
+ }
88
+ return String(Number.parseInt(normalized, 10)).padStart(3, "0");
89
+ }
90
+
91
+ function cycleDirectoryName(cycle = DEFAULT_ACTIVE_CYCLE) {
92
+ return `cycle_${normalizeCycleNumber(cycle)}`;
93
+ }
94
+
95
+ function isCycleStepId(stepId = "") {
96
+ return CYCLE_STEP_IDS.includes(normalizeStepId(stepId));
97
+ }
98
+
99
+ async function readWorkflowVersion(paths) {
100
+ return readTrimmedFile(path.join(paths.sessionRoot, "workflow_version"));
101
+ }
102
+
103
+ async function readActiveCycle(paths) {
104
+ const cycle = await readTrimmedFile(path.join(paths.sessionRoot, "active_cycle"));
105
+ return normalizeCycleNumber(cycle || DEFAULT_ACTIVE_CYCLE);
106
+ }
107
+
108
+ async function writeActiveCycle(paths, cycle) {
109
+ await writeTextFile(path.join(paths.sessionRoot, "active_cycle"), `${normalizeCycleNumber(cycle)}\n`);
110
+ }
111
+
112
+ function cycleStepsRoot(paths, cycle) {
113
+ return path.join(paths.sessionRoot, "steps", cycleDirectoryName(cycle));
114
+ }
115
+
116
+ function cycleRoot(paths, cycle) {
117
+ return path.join(paths.sessionRoot, "cycles", cycleDirectoryName(cycle));
118
+ }
119
+
120
+ function cyclePlanPath(paths, cycle) {
121
+ return path.join(cycleRoot(paths, cycle), "plan.md");
122
+ }
123
+
124
+ function cyclePlanPromptFileName(cycle) {
125
+ return `cycle_${normalizeCycleNumber(cycle)}_plan_request.md`;
126
+ }
127
+
128
+ function cyclePlanExecutionPromptFileName(cycle) {
129
+ return `cycle_${normalizeCycleNumber(cycle)}_plan_execution.md`;
130
+ }
131
+
132
+ function normalizeReviewPassNumber(value = "") {
133
+ const normalized = normalizeText(value).replace(/^pass_/u, "");
134
+ if (!/^\d+$/u.test(normalized)) {
135
+ return DEFAULT_REVIEW_PASS;
136
+ }
137
+ return String(Number.parseInt(normalized, 10)).padStart(3, "0");
138
+ }
139
+
140
+ function reviewPassDirectoryName(pass = DEFAULT_REVIEW_PASS) {
141
+ return `pass_${normalizeReviewPassNumber(pass)}`;
142
+ }
143
+
144
+ function reviewPassRoot(paths, pass) {
145
+ return path.join(paths.sessionRoot, "review_passes", reviewPassDirectoryName(pass));
146
+ }
147
+
148
+ async function parseJsonFileIfExists(filePath) {
149
+ const source = await readTextIfExists(filePath);
150
+ if (!source) {
151
+ return null;
152
+ }
153
+ try {
154
+ const parsed = JSON.parse(source);
155
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+
161
+ async function readReviewPassNumbers(paths) {
162
+ try {
163
+ const entries = await readdir(path.join(paths.sessionRoot, "review_passes"), { withFileTypes: true });
164
+ return entries
165
+ .filter((entry) => entry.isDirectory() && /^pass_\d+$/u.test(entry.name))
166
+ .map((entry) => normalizeReviewPassNumber(entry.name))
167
+ .sort((left, right) => Number.parseInt(left, 10) - Number.parseInt(right, 10));
168
+ } catch {
169
+ return [];
170
+ }
171
+ }
172
+
173
+ async function readReviewPassInfo(paths, pass) {
174
+ const normalizedPass = normalizeReviewPassNumber(pass);
175
+ const root = reviewPassRoot(paths, normalizedPass);
176
+ const [prompt, accepted] = await Promise.all([
177
+ parseJsonFileIfExists(path.join(root, "prompt.json")),
178
+ parseJsonFileIfExists(path.join(root, "accepted.json"))
179
+ ]);
180
+ const status = accepted?.status || prompt?.status || "unknown";
181
+ const changedFiles = Array.isArray(accepted?.changedFiles) ? accepted.changedFiles : [];
182
+ return {
183
+ pass: normalizedPass,
184
+ label: reviewPassDirectoryName(normalizedPass),
185
+ status,
186
+ promptPath: prompt?.promptPath || path.join(root, "prompt.md"),
187
+ acceptedAt: accepted?.acceptedAt || "",
188
+ changedFiles,
189
+ commit: "",
190
+ committedAt: "",
191
+ findingsRemaining: accepted?.findingsRemaining === true,
192
+ maxPasses: REVIEW_PASS_LIMIT
193
+ };
194
+ }
195
+
196
+ async function readReviewPasses(paths) {
197
+ const passes = await readReviewPassNumbers(paths);
198
+ return Promise.all(passes.map((pass) => readReviewPassInfo(paths, pass)));
199
+ }
200
+
201
+ const PROMPT_ARTIFACT_BY_STEP_ID = Object.freeze({
202
+ issue_drafted: "issue_draft.md",
203
+ issue_details_gathered: "issue_details.md",
204
+ deep_ui_check_run: "deep_ui_check_run.md",
205
+ automated_checks_run: "automated_checks_run.md",
206
+ blueprint_updated: "update_blueprint.md",
207
+ user_check_completed: "user_check.md"
208
+ });
209
+
210
+ async function promptArtifactForStep(paths, stepId) {
211
+ const normalizedStepId = normalizeStepId(stepId);
212
+ if (normalizedStepId === "plan_made") {
213
+ return cyclePlanPromptFileName(await readActiveCycle(paths));
214
+ }
215
+ if (normalizedStepId === "plan_executed") {
216
+ return cyclePlanExecutionPromptFileName(await readActiveCycle(paths));
217
+ }
218
+ return PROMPT_ARTIFACT_BY_STEP_ID[normalizedStepId] || "";
219
+ }
220
+
221
+ async function readPromptForStep(paths, stepId) {
222
+ if (!stepCanExposeStoredPrompt(stepId)) {
223
+ return "";
224
+ }
225
+ const promptArtifact = await promptArtifactForStep(paths, stepId);
226
+ if (promptArtifact) {
227
+ const prompt = await readTextIfExists(path.join(paths.sessionRoot, "prompts", promptArtifact));
228
+ if (prompt) {
229
+ return prompt;
230
+ }
231
+ }
232
+ return readTextIfExists(path.join(paths.sessionRoot, "prompt.md"));
233
+ }
234
+
235
+ async function readStepFileNames(stepsRoot) {
48
236
  try {
49
237
  const entries = await readdir(stepsRoot, { withFileTypes: true });
50
238
  return entries
51
239
  .filter((entry) => entry.isFile())
52
- .map((entry) => entry.name)
53
- .filter((entry) => STEP_IDS.includes(entry))
54
- .sort((left, right) => STEP_IDS.indexOf(left) - STEP_IDS.indexOf(right));
240
+ .map((entry) => entry.name);
241
+ } catch {
242
+ return [];
243
+ }
244
+ }
245
+
246
+ async function readCompletedSteps(paths) {
247
+ const stepsRoot = path.join(paths.sessionRoot, "steps");
248
+ const activeCycle = await readActiveCycle(paths);
249
+ const globalStepIds = normalizeKnownStepIds(
250
+ (await readStepFileNames(stepsRoot)).filter((stepId) => !isCycleStepId(stepId))
251
+ );
252
+ const cycleStepIds = normalizeKnownStepIds(await readStepFileNames(cycleStepsRoot(paths, activeCycle)));
253
+ return applyReviewPassCompletionOverlay(paths, normalizeKnownStepIds([...globalStepIds, ...cycleStepIds]));
254
+ }
255
+
256
+ const REVIEW_STEP_IDS = Object.freeze([
257
+ "review_prompt_rendered",
258
+ "review_changes_accepted"
259
+ ]);
260
+
261
+ async function applyReviewPassCompletionOverlay(paths, completedSteps = []) {
262
+ const completed = new Set(completedSteps);
263
+ if (!REVIEW_STEP_IDS.some((stepId) => completed.has(stepId))) {
264
+ return normalizeKnownStepIds([...completed]);
265
+ }
266
+ const reviewPasses = await readReviewPasses(paths);
267
+ const latestPass = reviewPasses.at(-1);
268
+ if (!latestPass) {
269
+ REVIEW_STEP_IDS.forEach((stepId) => completed.delete(stepId));
270
+ return normalizeKnownStepIds([...completed]);
271
+ }
272
+ const latestPassAccepted = latestPass.status === "accepted" || latestPass.status === "no_changes";
273
+ const anotherPassRequired = latestPassAccepted && latestPass.findingsRemaining === true;
274
+ if (anotherPassRequired) {
275
+ REVIEW_STEP_IDS.forEach((stepId) => completed.delete(stepId));
276
+ return normalizeKnownStepIds([...completed]);
277
+ }
278
+ if (latestPass.status === "prompted") {
279
+ completed.add("review_prompt_rendered");
280
+ completed.delete("review_changes_accepted");
281
+ return normalizeKnownStepIds([...completed]);
282
+ }
283
+ if (latestPass.status === "accepted" || latestPass.status === "no_changes") {
284
+ REVIEW_STEP_IDS.forEach((stepId) => completed.add(stepId));
285
+ }
286
+ return normalizeKnownStepIds([...completed]);
287
+ }
288
+
289
+ async function readCycleInfo(paths, cycle) {
290
+ const normalizedCycle = normalizeCycleNumber(cycle);
291
+ const root = cycleRoot(paths, normalizedCycle);
292
+ const userCheckPassed = await readTextIfExists(path.join(cycleStepsRoot(paths, normalizedCycle), "user_check_completed"));
293
+ const userCheckFailed = await readTextIfExists(path.join(cycleStepsRoot(paths, normalizedCycle), "user_check_failed"));
294
+ const reworkRequestPath = path.join(root, "rework_request.md");
295
+ const reworkRequest = await readTextIfExists(reworkRequestPath);
296
+ return {
297
+ cycle: normalizedCycle,
298
+ label: cycleDirectoryName(normalizedCycle),
299
+ reworkRequest: reworkRequest.trim(),
300
+ reworkRequestPath: reworkRequest ? reworkRequestPath : "",
301
+ status: userCheckPassed ? "passed" : userCheckFailed ? "failed" : "active",
302
+ userCheckResult: userCheckPassed ? "passed" : userCheckFailed ? "failed" : "",
303
+ userCheckReceipt: (userCheckPassed || userCheckFailed).trim()
304
+ };
305
+ }
306
+
307
+ async function readCycles(paths, activeCycle) {
308
+ const cycles = new Set([normalizeCycleNumber(activeCycle || DEFAULT_ACTIVE_CYCLE)]);
309
+ for (const root of [path.join(paths.sessionRoot, "steps"), path.join(paths.sessionRoot, "cycles")]) {
310
+ try {
311
+ const entries = await readdir(root, { withFileTypes: true });
312
+ for (const entry of entries) {
313
+ if (entry.isDirectory() && /^cycle_\d+$/u.test(entry.name)) {
314
+ cycles.add(normalizeCycleNumber(entry.name));
315
+ }
316
+ }
317
+ } catch {
318
+ // Missing cycle directories are normal for older sessions.
319
+ }
320
+ }
321
+ return Promise.all([...cycles]
322
+ .sort((left, right) => Number.parseInt(left, 10) - Number.parseInt(right, 10))
323
+ .map((cycle) => readCycleInfo(paths, cycle)));
324
+ }
325
+
326
+ async function readStructuredChecks(paths) {
327
+ const checksRoot = path.join(paths.sessionRoot, "checks");
328
+ try {
329
+ const entries = await readdir(checksRoot, { withFileTypes: true });
330
+ const checks = [];
331
+ for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name))) {
332
+ const source = await readTextIfExists(path.join(checksRoot, entry.name));
333
+ try {
334
+ const parsed = JSON.parse(source);
335
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
336
+ checks.push(parsed);
337
+ }
338
+ } catch {
339
+ // Ignore malformed check metadata; the raw log remains on disk.
340
+ }
341
+ }
342
+ return checks;
343
+ } catch {
344
+ return [];
345
+ }
346
+ }
347
+
348
+ async function readStructuredUiChecks(paths) {
349
+ const uiChecksRoot = path.join(paths.sessionRoot, "ui_checks");
350
+ try {
351
+ const entries = await readdir(uiChecksRoot, { withFileTypes: true });
352
+ const checks = [];
353
+ for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name))) {
354
+ const source = await readTextIfExists(path.join(uiChecksRoot, entry.name));
355
+ try {
356
+ const parsed = JSON.parse(source);
357
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
358
+ checks.push(parsed);
359
+ }
360
+ } catch {
361
+ // Ignore malformed UI check metadata; the raw prompt/log remains on disk.
362
+ }
363
+ }
364
+ return checks;
55
365
  } catch {
56
366
  return [];
57
367
  }
58
368
  }
59
369
 
370
+ async function readWorktreeStatus(paths, worktreeReady) {
371
+ if (!worktreeReady) {
372
+ return {
373
+ changedFiles: [],
374
+ dirty: false,
375
+ ok: true,
376
+ status: "missing",
377
+ statusText: ""
378
+ };
379
+ }
380
+ const result = await runGitInWorktree(paths.worktree, ["status", "--porcelain=v1"], {
381
+ timeout: 15000
382
+ });
383
+ if (!result.ok) {
384
+ return {
385
+ changedFiles: [],
386
+ dirty: false,
387
+ ok: false,
388
+ status: "unknown",
389
+ statusText: result.output
390
+ };
391
+ }
392
+ const changedFiles = result.stdout
393
+ .split(/\r?\n/u)
394
+ .map((line) => line.trim())
395
+ .filter(Boolean);
396
+ return {
397
+ changedFiles,
398
+ dirty: changedFiles.length > 0,
399
+ ok: true,
400
+ status: changedFiles.length > 0 ? "dirty" : "clean",
401
+ statusText: result.stdout
402
+ };
403
+ }
404
+
60
405
  async function readReceiptSteps(paths) {
61
406
  const stepsRoot = path.join(paths.sessionRoot, "steps");
62
407
  try {
63
408
  const entries = await readdir(stepsRoot, { withFileTypes: true });
64
- const stepNames = entries
409
+ const knownStepRows = new Map();
410
+ const unknownStepRows = [];
411
+ entries
65
412
  .filter((entry) => entry.isFile())
66
413
  .map((entry) => entry.name)
414
+ .forEach((receiptName) => {
415
+ const stepId = normalizeStepId(receiptName);
416
+ if (STEP_IDS.includes(stepId)) {
417
+ if (!knownStepRows.has(stepId) || receiptName === stepId) {
418
+ knownStepRows.set(stepId, {
419
+ receiptName,
420
+ stepId
421
+ });
422
+ }
423
+ return;
424
+ }
425
+ unknownStepRows.push({
426
+ receiptName,
427
+ stepId
428
+ });
429
+ });
430
+
431
+ const stepRows = [...knownStepRows.values(), ...unknownStepRows]
67
432
  .sort((left, right) => {
68
- const leftIndex = STEP_IDS.indexOf(left);
69
- const rightIndex = STEP_IDS.indexOf(right);
433
+ const leftIndex = stepIndex(left.stepId);
434
+ const rightIndex = stepIndex(right.stepId);
70
435
  if (leftIndex >= 0 && rightIndex >= 0) {
71
436
  return leftIndex - rightIndex;
72
437
  }
@@ -76,14 +441,37 @@ async function readReceiptSteps(paths) {
76
441
  if (rightIndex >= 0) {
77
442
  return 1;
78
443
  }
79
- return left.localeCompare(right);
444
+ return left.stepId.localeCompare(right.stepId);
80
445
  });
81
446
 
82
- return Promise.all(stepNames.map(async (stepId) => ({
447
+ const globalReceipts = await Promise.all(stepRows.map(async ({ receiptName, stepId }) => ({
448
+ cycle: "",
83
449
  label: STEP_LABEL_BY_ID[stepId] || stepId,
84
- receipt: (await readTextIfExists(path.join(stepsRoot, stepId))).trim(),
450
+ receipt: (await readTextIfExists(path.join(stepsRoot, receiptName))).trim(),
85
451
  stepId
86
452
  })));
453
+
454
+ const cycleReceipts = [];
455
+ const cycleDirectories = entries
456
+ .filter((entry) => entry.isDirectory() && /^cycle_\d+$/u.test(entry.name))
457
+ .map((entry) => entry.name)
458
+ .sort();
459
+ for (const cycleDirectory of cycleDirectories) {
460
+ const cycle = normalizeCycleNumber(cycleDirectory);
461
+ const cycleRootPath = path.join(stepsRoot, cycleDirectory);
462
+ const cycleStepIds = await readStepFileNames(cycleRootPath);
463
+ for (const receiptName of cycleStepIds) {
464
+ const stepId = normalizeStepId(receiptName);
465
+ cycleReceipts.push({
466
+ cycle,
467
+ label: STEP_LABEL_BY_ID[stepId] || stepId,
468
+ receipt: (await readTextIfExists(path.join(cycleRootPath, receiptName))).trim(),
469
+ stepId
470
+ });
471
+ }
472
+ }
473
+
474
+ return [...globalReceipts, ...cycleReceipts];
87
475
  } catch {
88
476
  return [];
89
477
  }
@@ -106,14 +494,36 @@ function cloneContractValue(value) {
106
494
  );
107
495
  }
108
496
 
497
+ function stepRepeatabilityContract(stepId) {
498
+ if (!CYCLE_STEP_IDS.includes(normalizeStepId(stepId))) {
499
+ return {
500
+ repeatable: false,
501
+ repeatableGroupId: "",
502
+ repeatableGroupLabel: "",
503
+ repeatableLabel: ""
504
+ };
505
+ }
506
+ return {
507
+ repeatable: true,
508
+ repeatableGroupId: "rework_cycle",
509
+ repeatableGroupLabel: "Rework cycle",
510
+ repeatableLabel: "Cycle step"
511
+ };
512
+ }
513
+
109
514
  function publicStepDefinition(step, index) {
110
515
  return {
111
516
  description: step.description,
517
+ displayGroupId: step.displayGroupId || "",
518
+ displayGroupLabel: step.displayGroupLabel || "",
112
519
  id: step.id,
113
520
  index,
114
521
  input: cloneContractValue(step.input),
115
522
  kind: step.kind,
116
- label: step.label
523
+ label: step.label,
524
+ ...stepRepeatabilityContract(step.id),
525
+ requiresExplicitRun: step.requiresExplicitRun === true,
526
+ utilityActions: cloneContractValue(step.utilityActions || [])
117
527
  };
118
528
  }
119
529
 
@@ -121,49 +531,332 @@ function buildStepDefinitions() {
121
531
  return STEP_DEFINITIONS.map((step, index) => publicStepDefinition(step, index));
122
532
  }
123
533
 
124
- function buildCurrentStepAction(stepId) {
534
+ function stepIsRetryableWhenBlocked(stepId) {
535
+ return [
536
+ "automated_checks_run",
537
+ "deep_ui_check_run",
538
+ "doctor_run"
539
+ ].includes(normalizeStepId(stepId));
540
+ }
541
+
542
+ function stepIsConditional(stepId) {
543
+ return [
544
+ "deep_ui_check_run"
545
+ ].includes(normalizeStepId(stepId));
546
+ }
547
+
548
+ function activeCycleInfoFromArtifacts(artifacts = {}) {
549
+ const activeCycle = normalizeCycleNumber(artifacts.activeCycle || "");
550
+ return (artifacts.cycles || []).find((cycle) => cycle?.cycle === activeCycle) || null;
551
+ }
552
+
553
+ function uiCheckPromptedForStep(artifacts = {}, stepId = "") {
554
+ const normalizedStepId = normalizeStepId(stepId);
555
+ return (artifacts.uiChecks || []).some((entry) => {
556
+ return normalizeStepId(entry?.stepId || "") === normalizedStepId &&
557
+ normalizeText(entry?.status || "") === "prompted";
558
+ });
559
+ }
560
+
561
+ function skipReasonForStep(stepId, artifacts = {}) {
562
+ const normalizedStepId = normalizeStepId(stepId);
563
+ if (normalizedStepId === "deep_ui_check_run" && artifacts.uiImpact === "none") {
564
+ return "uiImpact is none.";
565
+ }
566
+ return "";
567
+ }
568
+
569
+ function buildCurrentStepAction(stepId, artifacts = {}) {
125
570
  const step = STEP_DEFINITION_BY_ID[stepId];
126
571
  if (!step) {
127
572
  return null;
128
573
  }
574
+ const activeCycleInfo = activeCycleInfoFromArtifacts(artifacts);
575
+ const planExecutionPrompted = artifacts.planExecution?.prompted === true;
576
+ const planExecutionSubmitted = artifacts.planExecution?.submitted === true;
577
+ const planReworkMode = step.id === "plan_made" && normalizeCycleNumber(artifacts.activeCycle || "") !== "001";
578
+ const deepUiCheckPrompted = step.id === "deep_ui_check_run" && uiCheckPromptedForStep(artifacts, "deep_ui_check_run");
579
+ const hasActiveReworkRequest = Boolean(activeCycleInfo?.reworkRequestPath);
580
+ const promptPhaseButtonLabel = step.kind === "codex_output" &&
581
+ step.codex?.mode === "inject_prompt" &&
582
+ !artifacts.prompt
583
+ ? step.codex.promptActionLabel || ""
584
+ : "";
585
+ const buttonLabel = promptPhaseButtonLabel || step.buttonLabel;
586
+ const alternateActions = [];
587
+ if (step.id === "user_check_completed") {
588
+ alternateActions.push({
589
+ id: "return_to_plan_made",
590
+ input: {
591
+ formatHint: "markdown",
592
+ label: "What needs to be reworked?",
593
+ multiline: true,
594
+ name: "reworkNotes",
595
+ required: true,
596
+ type: "text"
597
+ },
598
+ label: "Return to Plan made",
599
+ presentation: "exclusive",
600
+ requiredErrorCode: "user_check_failed",
601
+ submitOptions: {
602
+ userCheck: "failed"
603
+ },
604
+ targetStep: "plan_made"
605
+ });
606
+ }
607
+ if (step.id === "review_changes_accepted") {
608
+ alternateActions.push({
609
+ id: "request_another_review_pass",
610
+ input: {
611
+ formatHint: "markdown",
612
+ label: "What important findings remain?",
613
+ multiline: true,
614
+ name: "reviewFindings",
615
+ required: true,
616
+ type: "text"
617
+ },
618
+ label: "Run another review pass",
619
+ presentation: "secondary",
620
+ submitOptions: {
621
+ reviewFindingsRemaining: true
622
+ },
623
+ targetStep: "review_prompt_rendered"
624
+ });
625
+ }
626
+ if (step.id === "pr_finalized") {
627
+ alternateActions.push({
628
+ id: "close_without_merge",
629
+ input: {
630
+ formatHint: "markdown",
631
+ label: "Why is this session finishing without merge?",
632
+ multiline: true,
633
+ name: "closeReason",
634
+ required: true,
635
+ type: "text"
636
+ },
637
+ label: "Finish without merge",
638
+ presentation: "secondary",
639
+ submitOptions: {
640
+ closeWithoutMerge: true
641
+ },
642
+ targetStep: "pr_finalized"
643
+ });
644
+ }
645
+ const dynamicButtonLabel = (() => {
646
+ if (step.id === "plan_executed" && planExecutionPrompted && !planExecutionSubmitted) {
647
+ return "Go to next step";
648
+ }
649
+ if (step.id === "deep_ui_check_run" && deepUiCheckPrompted) {
650
+ return "Go to next step";
651
+ }
652
+ if (step.id === "automated_checks_run" && artifacts.prompt) {
653
+ return "Go to next step";
654
+ }
655
+ if (step.id === "plan_made" && planReworkMode && !artifacts.prompt && hasActiveReworkRequest) {
656
+ return "Get Codex to create revised plan";
657
+ }
658
+ return buttonLabel;
659
+ })();
660
+ const dynamicDescription = (() => {
661
+ if (step.id === "plan_executed" && planExecutionPrompted && !planExecutionSubmitted) {
662
+ return "Codex has the execution prompt. Studio advances when Codex finishes.";
663
+ }
664
+ if (step.id === "deep_ui_check_run" && deepUiCheckPrompted) {
665
+ return "Codex has the Deep UI check prompt. Studio advances when Codex finishes.";
666
+ }
667
+ if (step.id === "automated_checks_run" && artifacts.prompt) {
668
+ return "Codex has the automated-checks prompt. Studio advances when Codex finishes.";
669
+ }
670
+ if (step.id === "plan_made" && planReworkMode && hasActiveReworkRequest) {
671
+ return "Codex writes a revised implementation plan from the user's rework notes for this cycle.";
672
+ }
673
+ return step.description;
674
+ })();
675
+ const dynamicUtilityActions = (() => {
676
+ return step.utilityActions || [];
677
+ })();
129
678
  return {
130
- buttonLabel: step.buttonLabel,
131
- description: step.description,
679
+ alternateActions,
680
+ buttonLabel: dynamicButtonLabel,
681
+ description: dynamicDescription,
132
682
  index: STEP_IDS.indexOf(step.id),
133
683
  input: cloneContractValue(step.input),
134
684
  kind: step.kind,
135
- stepId: step.id
685
+ label: dynamicButtonLabel,
686
+ ...stepRepeatabilityContract(step.id),
687
+ requiredInput: cloneContractValue(step.input),
688
+ requiresExplicitRun: step.requiresExplicitRun === true,
689
+ conditional: stepIsConditional(step.id),
690
+ retryable: artifacts.status === SESSION_STATUS.BLOCKED && stepIsRetryableWhenBlocked(step.id),
691
+ skipReason: skipReasonForStep(step.id, artifacts),
692
+ stepId: step.id,
693
+ utilityActions: cloneContractValue(dynamicUtilityActions)
136
694
  };
137
695
  }
138
696
 
139
- function buildCodexHandoff(stepId) {
697
+ function buildCodexHandoff(stepId, _artifacts = {}) {
140
698
  const step = STEP_DEFINITION_BY_ID[stepId];
141
699
  return step?.codex ? cloneContractValue(step.codex) : null;
142
700
  }
143
701
 
144
702
  async function readSessionArtifacts(paths) {
145
- const [status, currentStep, issueUrl, prUrl, prompt, issueText, codexThreadId] = await Promise.all([
703
+ const activeCycle = await readActiveCycle(paths);
704
+ const [
705
+ status,
706
+ rawCurrentStep,
707
+ issueUrl,
708
+ prUrl,
709
+ issueText,
710
+ issueTitle,
711
+ planText,
712
+ issueDetails,
713
+ agentDecisions,
714
+ finalReportText,
715
+ githubCommentsText,
716
+ codexThreadId,
717
+ workflowVersion,
718
+ baseBranch,
719
+ baseCommit,
720
+ issueMetadataText,
721
+ planExecutionReceipt,
722
+ prOutcomeText
723
+ ] = await Promise.all([
146
724
  readTrimmedFile(path.join(paths.sessionRoot, "status")),
147
725
  readTrimmedFile(path.join(paths.sessionRoot, "current_step")),
148
726
  readTrimmedFile(path.join(paths.sessionRoot, "issue_url")),
149
727
  readTrimmedFile(path.join(paths.sessionRoot, "pr_url")),
150
- readTextIfExists(path.join(paths.sessionRoot, "prompt.md")),
151
728
  readTextIfExists(path.join(paths.sessionRoot, "issue.md")),
152
- readTrimmedFile(path.join(paths.sessionRoot, "codex_thread_id"))
729
+ readTrimmedFile(path.join(paths.sessionRoot, "issue_title")),
730
+ readTextIfExists(cyclePlanPath(paths, activeCycle)),
731
+ readTextIfExists(path.join(paths.sessionRoot, "issue_details.md")),
732
+ readTextIfExists(path.join(paths.sessionRoot, "agent_decisions.md")),
733
+ readTextIfExists(path.join(paths.sessionRoot, "final_report.md")),
734
+ readTextIfExists(path.join(paths.sessionRoot, "github_comments.json")),
735
+ readTrimmedFile(path.join(paths.sessionRoot, "codex_thread_id")),
736
+ readWorkflowVersion(paths),
737
+ readTrimmedFile(path.join(paths.sessionRoot, "base_branch")),
738
+ readTrimmedFile(path.join(paths.sessionRoot, "base_commit")),
739
+ readTextIfExists(path.join(paths.sessionRoot, "issue_metadata.json")),
740
+ readTextIfExists(path.join(cycleStepsRoot(paths, activeCycle), "plan_executed")),
741
+ readTextIfExists(path.join(paths.sessionRoot, "pr_outcome.json"))
153
742
  ]);
154
- const completedSteps = await readCompletedSteps(paths.sessionRoot);
743
+ let issueMetadata = null;
744
+ if (issueMetadataText) {
745
+ try {
746
+ issueMetadata = JSON.parse(issueMetadataText);
747
+ } catch {
748
+ issueMetadata = null;
749
+ }
750
+ }
751
+ let githubComments = {};
752
+ if (githubCommentsText) {
753
+ try {
754
+ const parsed = JSON.parse(githubCommentsText);
755
+ githubComments = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
756
+ } catch {
757
+ githubComments = {};
758
+ }
759
+ }
760
+ let prOutcome = null;
761
+ if (prOutcomeText) {
762
+ try {
763
+ const parsed = JSON.parse(prOutcomeText);
764
+ prOutcome = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
765
+ } catch {
766
+ prOutcome = null;
767
+ }
768
+ }
769
+ const cycles = await readCycles(paths, activeCycle);
770
+ const checks = await readStructuredChecks(paths);
771
+ const uiChecks = await readStructuredUiChecks(paths);
772
+ const reviewPasses = await readReviewPasses(paths);
773
+ const worktreeReady = await hasWorktree(paths);
774
+ const worktreeStatus = await readWorktreeStatus(paths, worktreeReady);
775
+ const commandLogPath = path.join(paths.sessionRoot, "command_log.jsonl");
776
+ const dependencyInstallReceipt = await readTextIfExists(path.join(paths.sessionRoot, "steps", "dependencies_installed"));
777
+ const planExecutionPromptPath = path.join(paths.sessionRoot, "prompts", cyclePlanExecutionPromptFileName(activeCycle));
778
+ const planExecutionPromptExists = await fileExists(planExecutionPromptPath);
779
+ const appRootForArtifacts = worktreeReady ? paths.worktree : paths.targetRoot;
780
+ const appReady = await inspectReadyJskitAppRoot(appRootForArtifacts);
781
+ const blueprintPath = path.join(appRootForArtifacts, ".jskit", "APP_BLUEPRINT.md");
782
+ const helperMapPath = path.join(appRootForArtifacts, ".jskit", "helper-map.md");
783
+ const currentStep = normalizeStepId(rawCurrentStep);
784
+ let completedSteps = await readCompletedSteps(paths);
785
+ const worktreeRemovalCompleted = completedSteps.includes("pr_finalized");
786
+ const worktreeReceiptInvalid = !worktreeReady &&
787
+ completedSteps.includes("worktree_created") &&
788
+ !worktreeRemovalCompleted &&
789
+ status !== SESSION_STATUS.FINISHED &&
790
+ status !== SESSION_STATUS.ABANDONED;
791
+ if (worktreeReceiptInvalid) {
792
+ completedSteps = completedSteps.filter((stepId) => !["worktree_created", "dependencies_installed"].includes(stepId));
793
+ }
155
794
  const nextStep = resolveNextStep(completedSteps);
795
+ const currentStepIndex = stepIndex(currentStep);
796
+ const nextStepIndex = stepIndex(nextStep);
797
+ const effectiveCurrentStep = nextStep &&
798
+ (completedSteps.includes(currentStep) || currentStepIndex < 0 || currentStepIndex > nextStepIndex)
799
+ ? nextStep
800
+ : currentStep || nextStep;
801
+ const prompt = await readPromptForStep(paths, effectiveCurrentStep);
156
802
 
157
803
  return {
158
804
  codexThreadId,
159
805
  completedSteps,
160
- currentStep: currentStep || nextStep,
806
+ currentStep: effectiveCurrentStep,
807
+ activeCycle,
808
+ appReady,
809
+ baseBranch,
810
+ baseCommit,
811
+ blueprintExists: await fileExists(blueprintPath),
812
+ blueprintPath,
813
+ cycles,
814
+ checks,
815
+ uiChecks,
816
+ reviewPasses,
817
+ currentReviewPass: reviewPasses.at(-1)?.pass || "",
818
+ commandLogExists: await fileExists(commandLogPath),
819
+ commandLogPath,
820
+ dependencyInstall: {
821
+ installed: Boolean(dependencyInstallReceipt.trim()),
822
+ receipt: dependencyInstallReceipt.trim(),
823
+ status: dependencyInstallReceipt.trim()
824
+ ? "installed"
825
+ : worktreeReady ? "pending" : "waiting_for_worktree"
826
+ },
827
+ helperMapExists: await fileExists(helperMapPath),
828
+ helperMapPath,
829
+ githubComments,
830
+ issueMetadata,
831
+ issueCategory: normalizeText(issueMetadata?.issueCategory || ""),
832
+ uiImpact: normalizeText(issueMetadata?.uiImpact || ""),
833
+ agentDecisions: agentDecisions.trim(),
834
+ agentDecisionsLatest: agentDecisions
835
+ .split(/\r?\n/u)
836
+ .map((line) => line.trim())
837
+ .filter((line) => line && !line.startsWith("#") && !line.startsWith("Session:"))
838
+ .slice(-5)
839
+ .join("\n"),
840
+ issueTitle,
161
841
  issueText: issueText.trim(),
162
842
  issueUrl,
163
843
  nextStep,
164
844
  prUrl,
845
+ prOutcome,
846
+ planExecution: {
847
+ prompted: planExecutionPromptExists,
848
+ promptPath: planExecutionPromptExists ? planExecutionPromptPath : "",
849
+ receipt: planExecutionReceipt.trim(),
850
+ submitted: Boolean(planExecutionReceipt.trim())
851
+ },
852
+ planText: planText.trim(),
853
+ issueDetails: issueDetails.trim(),
854
+ finalReportText: finalReportText.trim(),
165
855
  prompt: prompt.trim(),
166
- status: status || SESSION_STATUS.PENDING
856
+ status: status || SESSION_STATUS.PENDING,
857
+ workflowVersion,
858
+ worktreeReady,
859
+ worktreeStatus
167
860
  };
168
861
  }
169
862
 
@@ -171,11 +864,12 @@ function buildNextCommand(sessionId, stepId) {
171
864
  if (!stepId) {
172
865
  return "";
173
866
  }
174
- const template = STEP_DEFINITION_BY_ID[stepId]?.nextCommandTemplate || "jskit session {{session_id}} step";
867
+ const template = STEP_DEFINITION_BY_ID[stepId]?.nextCommandTemplate || `${JSKIT_CLI_SHELL_COMMAND} session {{session_id}} step`;
175
868
  return template.replaceAll("{{session_id}}", sessionId);
176
869
  }
177
870
 
178
871
  async function buildSessionResponse(paths, {
872
+ codex = undefined,
179
873
  ok = true,
180
874
  errors = [],
181
875
  preconditions = [],
@@ -185,8 +879,72 @@ async function buildSessionResponse(paths, {
185
879
  const responsePaths = paths.sessionId ? await pathsForExistingSession(paths) : paths;
186
880
  const artifacts = responsePaths.sessionRoot ? await readSessionArtifacts(responsePaths) : {};
187
881
  const resolvedStatus = status || artifacts.status || (ok ? SESSION_STATUS.PENDING : SESSION_STATUS.BLOCKED);
882
+ if (responsePaths.sessionRoot && await fileExists(responsePaths.sessionRoot) && artifacts.workflowVersion !== SESSION_WORKFLOW_VERSION) {
883
+ return {
884
+ ok: false,
885
+ sessionId: paths.sessionId || "",
886
+ status: SESSION_STATUS.BLOCKED,
887
+ currentStep: "",
888
+ completedSteps: artifacts.completedSteps || [],
889
+ workflowVersion: artifacts.workflowVersion || "",
890
+ baseBranch: artifacts.baseBranch || "",
891
+ baseCommit: artifacts.baseCommit || "",
892
+ blueprintPath: artifacts.blueprintPath || "",
893
+ blueprintExists: artifacts.blueprintExists === true,
894
+ activeCycle: artifacts.activeCycle || "",
895
+ appReady: cloneContractValue(artifacts.appReady || null),
896
+ cycles: cloneContractValue(artifacts.cycles || []),
897
+ checks: cloneContractValue(artifacts.checks || []),
898
+ dependencyInstall: cloneContractValue(artifacts.dependencyInstall || null),
899
+ uiChecks: cloneContractValue(artifacts.uiChecks || []),
900
+ reviewPasses: cloneContractValue(artifacts.reviewPasses || []),
901
+ currentReviewPass: artifacts.currentReviewPass || "",
902
+ commandLogExists: artifacts.commandLogExists === true,
903
+ commandLogPath: artifacts.commandLogPath || "",
904
+ stepDefinitions: buildStepDefinitions(),
905
+ currentStepAction: null,
906
+ codex: null,
907
+ prompt: "",
908
+ nextCommand: "",
909
+ issueUrl: artifacts.issueUrl || "",
910
+ issueTitle: artifacts.issueTitle || "",
911
+ issueText: artifacts.issueText || "",
912
+ issueMetadata: cloneContractValue(artifacts.issueMetadata || null),
913
+ githubComments: cloneContractValue(artifacts.githubComments || {}),
914
+ issueCategory: artifacts.issueCategory || "",
915
+ uiImpact: artifacts.uiImpact || "",
916
+ agentDecisionsPath: artifacts.agentDecisions ? path.join(responsePaths.sessionRoot, "agent_decisions.md") : "",
917
+ agentDecisionsLatest: artifacts.agentDecisionsLatest || "",
918
+ planExecution: cloneContractValue(artifacts.planExecution || null),
919
+ planText: artifacts.planText || "",
920
+ issueDetails: artifacts.issueDetails || "",
921
+ issueDetailsPath: artifacts.issueDetails ? path.join(responsePaths.sessionRoot, "issue_details.md") : "",
922
+ finalReportPath: artifacts.finalReportText ? path.join(responsePaths.sessionRoot, "final_report.md") : "",
923
+ finalReportText: artifacts.finalReportText || "",
924
+ helperMapPath: artifacts.helperMapPath || "",
925
+ helperMapExists: artifacts.helperMapExists === true,
926
+ prUrl: artifacts.prUrl || "",
927
+ prOutcome: cloneContractValue(artifacts.prOutcome || null),
928
+ preconditions,
929
+ errors: [
930
+ createError({
931
+ code: "unsupported_workflow_version",
932
+ message: `Session ${paths.sessionId || ""} uses workflow version ${artifacts.workflowVersion || "missing"}, but this JSKIT runtime expects ${SESSION_WORKFLOW_VERSION}.`
933
+ })
934
+ ],
935
+ archive: responsePaths.archive || "active",
936
+ sessionRoot: responsePaths.sessionRoot || "",
937
+ worktree: paths.worktree || "",
938
+ worktreeReady: artifacts.worktreeReady === true,
939
+ worktreeStatus: cloneContractValue(artifacts.worktreeStatus || null),
940
+ branch: paths.branch || "",
941
+ codexThreadId: artifacts.codexThreadId || ""
942
+ };
943
+ }
188
944
  const currentStep = artifacts.currentStep || artifacts.nextStep || "";
189
- const responsePrompt = typeof prompt === "string" ? prompt : artifacts.prompt || "";
945
+ const responsePrompt = typeof prompt === "string"
946
+ ? prompt
947
+ : stepCanExposeStoredPrompt(currentStep) ? artifacts.prompt || "" : "";
190
948
 
191
949
  return {
192
950
  ok: ok === true,
@@ -194,18 +952,52 @@ async function buildSessionResponse(paths, {
194
952
  status: resolvedStatus,
195
953
  currentStep,
196
954
  completedSteps: artifacts.completedSteps || [],
955
+ workflowVersion: artifacts.workflowVersion || "",
956
+ baseBranch: artifacts.baseBranch || "",
957
+ baseCommit: artifacts.baseCommit || "",
958
+ blueprintPath: artifacts.blueprintPath || "",
959
+ blueprintExists: artifacts.blueprintExists === true,
960
+ activeCycle: artifacts.activeCycle || "",
961
+ appReady: cloneContractValue(artifacts.appReady || null),
962
+ cycles: cloneContractValue(artifacts.cycles || []),
963
+ checks: cloneContractValue(artifacts.checks || []),
964
+ dependencyInstall: cloneContractValue(artifacts.dependencyInstall || null),
965
+ uiChecks: cloneContractValue(artifacts.uiChecks || []),
966
+ reviewPasses: cloneContractValue(artifacts.reviewPasses || []),
967
+ currentReviewPass: artifacts.currentReviewPass || "",
968
+ commandLogExists: artifacts.commandLogExists === true,
969
+ commandLogPath: artifacts.commandLogPath || "",
197
970
  stepDefinitions: buildStepDefinitions(),
198
- currentStepAction: buildCurrentStepAction(currentStep),
199
- codex: buildCodexHandoff(currentStep),
971
+ currentStepAction: buildCurrentStepAction(currentStep, artifacts),
972
+ codex: codex === undefined ? buildCodexHandoff(currentStep, artifacts) : cloneContractValue(codex),
200
973
  prompt: responsePrompt,
201
974
  nextCommand: buildNextCommand(paths.sessionId || "", currentStep),
202
975
  issueUrl: artifacts.issueUrl || "",
976
+ issueTitle: artifacts.issueTitle || "",
977
+ issueText: artifacts.issueText || "",
978
+ issueMetadata: cloneContractValue(artifacts.issueMetadata || null),
979
+ githubComments: cloneContractValue(artifacts.githubComments || {}),
980
+ issueCategory: artifacts.issueCategory || "",
981
+ uiImpact: artifacts.uiImpact || "",
982
+ agentDecisionsPath: artifacts.agentDecisions ? path.join(responsePaths.sessionRoot, "agent_decisions.md") : "",
983
+ agentDecisionsLatest: artifacts.agentDecisionsLatest || "",
984
+ planExecution: cloneContractValue(artifacts.planExecution || null),
985
+ planText: artifacts.planText || "",
986
+ issueDetails: artifacts.issueDetails || "",
987
+ issueDetailsPath: artifacts.issueDetails ? path.join(responsePaths.sessionRoot, "issue_details.md") : "",
988
+ finalReportPath: artifacts.finalReportText ? path.join(responsePaths.sessionRoot, "final_report.md") : "",
989
+ finalReportText: artifacts.finalReportText || "",
990
+ helperMapPath: artifacts.helperMapPath || "",
991
+ helperMapExists: artifacts.helperMapExists === true,
203
992
  prUrl: artifacts.prUrl || "",
993
+ prOutcome: cloneContractValue(artifacts.prOutcome || null),
204
994
  preconditions,
205
995
  errors,
206
996
  archive: responsePaths.archive || (resolvedStatus === SESSION_STATUS.FINISHED ? "completed" : resolvedStatus === SESSION_STATUS.ABANDONED ? "abandoned" : "active"),
207
997
  sessionRoot: responsePaths.sessionRoot || "",
208
998
  worktree: paths.worktree || "",
999
+ worktreeReady: artifacts.worktreeReady === true,
1000
+ worktreeStatus: cloneContractValue(artifacts.worktreeStatus || null),
209
1001
  branch: paths.branch || "",
210
1002
  codexThreadId: artifacts.codexThreadId || ""
211
1003
  };
@@ -238,18 +1030,52 @@ function buildSessionErrorResponse({
238
1030
  status,
239
1031
  currentStep: "",
240
1032
  completedSteps: [],
1033
+ workflowVersion: "",
1034
+ baseBranch: "",
1035
+ baseCommit: "",
1036
+ blueprintPath: "",
1037
+ blueprintExists: false,
1038
+ activeCycle: "",
1039
+ appReady: null,
1040
+ cycles: [],
1041
+ checks: [],
1042
+ dependencyInstall: null,
1043
+ uiChecks: [],
1044
+ reviewPasses: [],
1045
+ currentReviewPass: "",
1046
+ commandLogExists: false,
1047
+ commandLogPath: "",
241
1048
  stepDefinitions: buildStepDefinitions(),
242
1049
  currentStepAction: null,
243
1050
  codex: null,
244
1051
  prompt: "",
245
1052
  nextCommand: "",
1053
+ issueTitle: "",
1054
+ issueText: "",
1055
+ issueMetadata: null,
1056
+ githubComments: {},
1057
+ issueCategory: "",
1058
+ uiImpact: "",
1059
+ agentDecisionsPath: "",
1060
+ agentDecisionsLatest: "",
1061
+ planExecution: null,
1062
+ planText: "",
1063
+ issueDetails: "",
1064
+ issueDetailsPath: "",
1065
+ finalReportPath: "",
1066
+ finalReportText: "",
1067
+ helperMapPath: "",
1068
+ helperMapExists: false,
246
1069
  issueUrl: "",
247
1070
  prUrl: "",
1071
+ prOutcome: null,
248
1072
  preconditions,
249
1073
  errors: errorList,
250
1074
  archive: "",
251
1075
  sessionRoot: "",
252
1076
  worktree: "",
1077
+ worktreeReady: false,
1078
+ worktreeStatus: null,
253
1079
  branch: "",
254
1080
  codexThreadId: "",
255
1081
  targetRoot: normalizedTargetRoot
@@ -265,15 +1091,29 @@ async function markCurrentStep(paths, stepId) {
265
1091
  }
266
1092
 
267
1093
  async function writeReceipt(paths, stepId, message) {
268
- await mkdir(path.join(paths.sessionRoot, "steps"), { recursive: true });
1094
+ const activeCycle = await readActiveCycle(paths);
1095
+ const root = isCycleStepId(stepId) ? cycleStepsRoot(paths, activeCycle) : path.join(paths.sessionRoot, "steps");
1096
+ await mkdir(root, { recursive: true });
269
1097
  await writeTextFile(
270
- path.join(paths.sessionRoot, "steps", stepId),
1098
+ path.join(root, stepId),
271
1099
  `${timestampForReceipt()}\n${normalizeText(message) || STEP_LABEL_BY_ID[stepId] || stepId}`
272
1100
  );
273
- const completedSteps = await readCompletedSteps(paths.sessionRoot);
1101
+ const completedSteps = await readCompletedSteps(paths);
274
1102
  await markCurrentStep(paths, resolveNextStep(completedSteps));
275
1103
  }
276
1104
 
1105
+ async function writeCycleReceipt(paths, receiptName, message, {
1106
+ cycle = ""
1107
+ } = {}) {
1108
+ const activeCycle = normalizeCycleNumber(cycle || await readActiveCycle(paths));
1109
+ const root = cycleStepsRoot(paths, activeCycle);
1110
+ await mkdir(root, { recursive: true });
1111
+ await writeTextFile(
1112
+ path.join(root, normalizeText(receiptName)),
1113
+ `${timestampForReceipt()}\n${normalizeText(message) || normalizeText(receiptName)}`
1114
+ );
1115
+ }
1116
+
277
1117
  async function failSession(paths, {
278
1118
  code,
279
1119
  message,
@@ -309,7 +1149,14 @@ export {
309
1149
  failSession,
310
1150
  markCurrentStep,
311
1151
  markStatus,
1152
+ normalizeReviewPassNumber,
1153
+ readActiveCycle,
312
1154
  readReceiptSteps,
1155
+ readReviewPasses,
313
1156
  readSessionArtifacts,
1157
+ reviewPassDirectoryName,
1158
+ reviewPassRoot,
1159
+ writeActiveCycle,
1160
+ writeCycleReceipt,
314
1161
  writeReceipt
315
1162
  };