@jskit-ai/jskit-cli 0.2.81 → 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 (27) 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 +110 -3
  5. package/src/server/commandHandlers/show.js +169 -34
  6. package/src/server/core/argParser.js +8 -0
  7. package/src/server/core/commandCatalog.js +58 -2
  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 +217 -78
  13. package/src/server/sessionRuntime/preconditions.js +382 -5
  14. package/src/server/sessionRuntime/promptRenderer.js +15 -2
  15. package/src/server/sessionRuntime/prompts/automated_checks.md +42 -0
  16. package/src/server/sessionRuntime/prompts/deep_ui_check.md +53 -0
  17. package/src/server/sessionRuntime/prompts/doctor_failure.md +11 -2
  18. package/src/server/sessionRuntime/prompts/execute_plan.md +32 -6
  19. package/src/server/sessionRuntime/prompts/final_comment.md +3 -1
  20. package/src/server/sessionRuntime/prompts/issue_details.md +52 -0
  21. package/src/server/sessionRuntime/prompts/new_issue.md +15 -2
  22. package/src/server/sessionRuntime/prompts/plan_issue.md +40 -9
  23. package/src/server/sessionRuntime/prompts/review_changes.md +46 -5
  24. package/src/server/sessionRuntime/prompts/update_blueprint.md +36 -0
  25. package/src/server/sessionRuntime/prompts/user_check.md +15 -1
  26. package/src/server/sessionRuntime/responses.js +776 -56
  27. package/src/server/sessionRuntime.js +1658 -123
@@ -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,6 +16,7 @@ import {
12
16
  normalizeText,
13
17
  readTextIfExists,
14
18
  readTrimmedFile,
19
+ runGitInWorktree,
15
20
  timestampForReceipt,
16
21
  writeTextFile
17
22
  } from "./io.js";
@@ -21,6 +26,9 @@ import {
21
26
  import {
22
27
  hasWorktree
23
28
  } from "./worktrees.js";
29
+ import {
30
+ inspectReadyJskitAppRoot
31
+ } from "./appReadiness.js";
24
32
 
25
33
  function createError({
26
34
  code,
@@ -46,34 +54,8 @@ function createPrecondition({
46
54
  });
47
55
  }
48
56
 
49
- const LEGACY_CURRENT_STEP_ID_ALIASES = Object.freeze({
50
- implementation_changes_detected: "implementation_changes_accepted",
51
- review_changes_detected: "review_changes_accepted"
52
- });
53
-
54
- const LEGACY_COMPLETED_STEP_ID_ALIASES = Object.freeze({
55
- implementation_changes_detected: Object.freeze(["implementation_changes_accepted"]),
56
- review_changes_detected: Object.freeze(["review_changes_accepted", "review_changes_committed"])
57
- });
58
-
59
- const LEGACY_RECEIPT_STEP_ID_ALIASES = Object.freeze({
60
- implementation_changes_detected: "implementation_changes_accepted",
61
- review_changes_detected: "review_changes_committed"
62
- });
63
-
64
57
  function normalizeStepId(stepId) {
65
- const normalized = normalizeText(stepId);
66
- return LEGACY_CURRENT_STEP_ID_ALIASES[normalized] || normalized;
67
- }
68
-
69
- function completedStepIdsForReceipt(stepId) {
70
- const normalized = normalizeText(stepId);
71
- return LEGACY_COMPLETED_STEP_ID_ALIASES[normalized] || [normalized];
72
- }
73
-
74
- function receiptStepId(stepId) {
75
- const normalized = normalizeText(stepId);
76
- return LEGACY_RECEIPT_STEP_ID_ALIASES[normalized] || normalizeStepId(normalized);
58
+ return normalizeText(stepId);
77
59
  }
78
60
 
79
61
  function stepIndex(stepId) {
@@ -84,7 +66,6 @@ function normalizeKnownStepIds(stepIds = []) {
84
66
  return Array.from(
85
67
  new Set(
86
68
  stepIds
87
- .flatMap((stepId) => completedStepIdsForReceipt(stepId))
88
69
  .map((stepId) => normalizeText(stepId))
89
70
  .filter((stepId) => STEP_IDS.includes(stepId))
90
71
  )
@@ -93,20 +74,155 @@ function normalizeKnownStepIds(stepIds = []) {
93
74
 
94
75
  function stepCanExposeStoredPrompt(stepId) {
95
76
  const step = STEP_DEFINITION_BY_ID[normalizeStepId(stepId)];
96
- return Boolean(step?.codex || step?.kind === "human_input");
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)));
97
199
  }
98
200
 
99
201
  const PROMPT_ARTIFACT_BY_STEP_ID = Object.freeze({
100
202
  issue_drafted: "issue_draft.md",
101
- plan_made: "plan_request.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",
102
207
  user_check_completed: "user_check.md"
103
208
  });
104
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
+
105
221
  async function readPromptForStep(paths, stepId) {
106
222
  if (!stepCanExposeStoredPrompt(stepId)) {
107
223
  return "";
108
224
  }
109
- const promptArtifact = PROMPT_ARTIFACT_BY_STEP_ID[normalizeStepId(stepId)];
225
+ const promptArtifact = await promptArtifactForStep(paths, stepId);
110
226
  if (promptArtifact) {
111
227
  const prompt = await readTextIfExists(path.join(paths.sessionRoot, "prompts", promptArtifact));
112
228
  if (prompt) {
@@ -116,18 +232,176 @@ async function readPromptForStep(paths, stepId) {
116
232
  return readTextIfExists(path.join(paths.sessionRoot, "prompt.md"));
117
233
  }
118
234
 
119
- async function readCompletedSteps(sessionRoot) {
120
- const stepsRoot = path.join(sessionRoot, "steps");
235
+ async function readStepFileNames(stepsRoot) {
121
236
  try {
122
237
  const entries = await readdir(stepsRoot, { withFileTypes: true });
123
- return normalizeKnownStepIds(entries
238
+ return entries
124
239
  .filter((entry) => entry.isFile())
125
- .map((entry) => entry.name));
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;
126
343
  } catch {
127
344
  return [];
128
345
  }
129
346
  }
130
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;
365
+ } catch {
366
+ return [];
367
+ }
368
+ }
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
+
131
405
  async function readReceiptSteps(paths) {
132
406
  const stepsRoot = path.join(paths.sessionRoot, "steps");
133
407
  try {
@@ -138,7 +412,7 @@ async function readReceiptSteps(paths) {
138
412
  .filter((entry) => entry.isFile())
139
413
  .map((entry) => entry.name)
140
414
  .forEach((receiptName) => {
141
- const stepId = receiptStepId(receiptName);
415
+ const stepId = normalizeStepId(receiptName);
142
416
  if (STEP_IDS.includes(stepId)) {
143
417
  if (!knownStepRows.has(stepId) || receiptName === stepId) {
144
418
  knownStepRows.set(stepId, {
@@ -170,11 +444,34 @@ async function readReceiptSteps(paths) {
170
444
  return left.stepId.localeCompare(right.stepId);
171
445
  });
172
446
 
173
- return Promise.all(stepRows.map(async ({ receiptName, stepId }) => ({
447
+ const globalReceipts = await Promise.all(stepRows.map(async ({ receiptName, stepId }) => ({
448
+ cycle: "",
174
449
  label: STEP_LABEL_BY_ID[stepId] || stepId,
175
450
  receipt: (await readTextIfExists(path.join(stepsRoot, receiptName))).trim(),
176
451
  stepId
177
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];
178
475
  } catch {
179
476
  return [];
180
477
  }
@@ -197,14 +494,35 @@ function cloneContractValue(value) {
197
494
  );
198
495
  }
199
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
+
200
514
  function publicStepDefinition(step, index) {
201
515
  return {
202
516
  description: step.description,
517
+ displayGroupId: step.displayGroupId || "",
518
+ displayGroupLabel: step.displayGroupLabel || "",
203
519
  id: step.id,
204
520
  index,
205
521
  input: cloneContractValue(step.input),
206
522
  kind: step.kind,
207
523
  label: step.label,
524
+ ...stepRepeatabilityContract(step.id),
525
+ requiresExplicitRun: step.requiresExplicitRun === true,
208
526
  utilityActions: cloneContractValue(step.utilityActions || [])
209
527
  };
210
528
  }
@@ -213,43 +531,258 @@ function buildStepDefinitions() {
213
531
  return STEP_DEFINITIONS.map((step, index) => publicStepDefinition(step, index));
214
532
  }
215
533
 
216
- 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 = {}) {
217
570
  const step = STEP_DEFINITION_BY_ID[stepId];
218
571
  if (!step) {
219
572
  return null;
220
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
+ })();
221
678
  return {
222
- buttonLabel: step.buttonLabel,
223
- description: step.description,
679
+ alternateActions,
680
+ buttonLabel: dynamicButtonLabel,
681
+ description: dynamicDescription,
224
682
  index: STEP_IDS.indexOf(step.id),
225
683
  input: cloneContractValue(step.input),
226
684
  kind: step.kind,
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),
227
692
  stepId: step.id,
228
- utilityActions: cloneContractValue(step.utilityActions || [])
693
+ utilityActions: cloneContractValue(dynamicUtilityActions)
229
694
  };
230
695
  }
231
696
 
232
- function buildCodexHandoff(stepId) {
697
+ function buildCodexHandoff(stepId, _artifacts = {}) {
233
698
  const step = STEP_DEFINITION_BY_ID[stepId];
234
699
  return step?.codex ? cloneContractValue(step.codex) : null;
235
700
  }
236
701
 
237
702
  async function readSessionArtifacts(paths) {
238
- const [status, rawCurrentStep, issueUrl, prUrl, issueText, issueTitle, planText, 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([
239
724
  readTrimmedFile(path.join(paths.sessionRoot, "status")),
240
725
  readTrimmedFile(path.join(paths.sessionRoot, "current_step")),
241
726
  readTrimmedFile(path.join(paths.sessionRoot, "issue_url")),
242
727
  readTrimmedFile(path.join(paths.sessionRoot, "pr_url")),
243
728
  readTextIfExists(path.join(paths.sessionRoot, "issue.md")),
244
729
  readTrimmedFile(path.join(paths.sessionRoot, "issue_title")),
245
- readTextIfExists(path.join(paths.sessionRoot, "plan.md")),
246
- readTrimmedFile(path.join(paths.sessionRoot, "codex_thread_id"))
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"))
247
742
  ]);
248
- const currentStep = normalizeStepId(rawCurrentStep);
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);
249
773
  const worktreeReady = await hasWorktree(paths);
250
- let completedSteps = await readCompletedSteps(paths.sessionRoot);
251
- const worktreeRemovalCompleted = completedSteps.includes("pr_merged") ||
252
- completedSteps.includes("worktree_removed");
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");
253
786
  const worktreeReceiptInvalid = !worktreeReady &&
254
787
  completedSteps.includes("worktree_created") &&
255
788
  !worktreeRemovalCompleted &&
@@ -271,15 +804,59 @@ async function readSessionArtifacts(paths) {
271
804
  codexThreadId,
272
805
  completedSteps,
273
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"),
274
840
  issueTitle,
275
841
  issueText: issueText.trim(),
276
842
  issueUrl,
277
843
  nextStep,
278
844
  prUrl,
845
+ prOutcome,
846
+ planExecution: {
847
+ prompted: planExecutionPromptExists,
848
+ promptPath: planExecutionPromptExists ? planExecutionPromptPath : "",
849
+ receipt: planExecutionReceipt.trim(),
850
+ submitted: Boolean(planExecutionReceipt.trim())
851
+ },
279
852
  planText: planText.trim(),
853
+ issueDetails: issueDetails.trim(),
854
+ finalReportText: finalReportText.trim(),
280
855
  prompt: prompt.trim(),
281
856
  status: status || SESSION_STATUS.PENDING,
282
- worktreeReady
857
+ workflowVersion,
858
+ worktreeReady,
859
+ worktreeStatus
283
860
  };
284
861
  }
285
862
 
@@ -287,7 +864,7 @@ function buildNextCommand(sessionId, stepId) {
287
864
  if (!stepId) {
288
865
  return "";
289
866
  }
290
- 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`;
291
868
  return template.replaceAll("{{session_id}}", sessionId);
292
869
  }
293
870
 
@@ -302,6 +879,68 @@ async function buildSessionResponse(paths, {
302
879
  const responsePaths = paths.sessionId ? await pathsForExistingSession(paths) : paths;
303
880
  const artifacts = responsePaths.sessionRoot ? await readSessionArtifacts(responsePaths) : {};
304
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
+ }
305
944
  const currentStep = artifacts.currentStep || artifacts.nextStep || "";
306
945
  const responsePrompt = typeof prompt === "string"
307
946
  ? prompt
@@ -313,22 +952,52 @@ async function buildSessionResponse(paths, {
313
952
  status: resolvedStatus,
314
953
  currentStep,
315
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 || "",
316
970
  stepDefinitions: buildStepDefinitions(),
317
- currentStepAction: buildCurrentStepAction(currentStep),
318
- codex: codex === undefined ? buildCodexHandoff(currentStep) : cloneContractValue(codex),
971
+ currentStepAction: buildCurrentStepAction(currentStep, artifacts),
972
+ codex: codex === undefined ? buildCodexHandoff(currentStep, artifacts) : cloneContractValue(codex),
319
973
  prompt: responsePrompt,
320
974
  nextCommand: buildNextCommand(paths.sessionId || "", currentStep),
321
975
  issueUrl: artifacts.issueUrl || "",
322
976
  issueTitle: artifacts.issueTitle || "",
323
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),
324
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,
325
992
  prUrl: artifacts.prUrl || "",
993
+ prOutcome: cloneContractValue(artifacts.prOutcome || null),
326
994
  preconditions,
327
995
  errors,
328
996
  archive: responsePaths.archive || (resolvedStatus === SESSION_STATUS.FINISHED ? "completed" : resolvedStatus === SESSION_STATUS.ABANDONED ? "abandoned" : "active"),
329
997
  sessionRoot: responsePaths.sessionRoot || "",
330
998
  worktree: paths.worktree || "",
331
999
  worktreeReady: artifacts.worktreeReady === true,
1000
+ worktreeStatus: cloneContractValue(artifacts.worktreeStatus || null),
332
1001
  branch: paths.branch || "",
333
1002
  codexThreadId: artifacts.codexThreadId || ""
334
1003
  };
@@ -361,6 +1030,21 @@ function buildSessionErrorResponse({
361
1030
  status,
362
1031
  currentStep: "",
363
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: "",
364
1048
  stepDefinitions: buildStepDefinitions(),
365
1049
  currentStepAction: null,
366
1050
  codex: null,
@@ -368,15 +1052,30 @@ function buildSessionErrorResponse({
368
1052
  nextCommand: "",
369
1053
  issueTitle: "",
370
1054
  issueText: "",
1055
+ issueMetadata: null,
1056
+ githubComments: {},
1057
+ issueCategory: "",
1058
+ uiImpact: "",
1059
+ agentDecisionsPath: "",
1060
+ agentDecisionsLatest: "",
1061
+ planExecution: null,
371
1062
  planText: "",
1063
+ issueDetails: "",
1064
+ issueDetailsPath: "",
1065
+ finalReportPath: "",
1066
+ finalReportText: "",
1067
+ helperMapPath: "",
1068
+ helperMapExists: false,
372
1069
  issueUrl: "",
373
1070
  prUrl: "",
1071
+ prOutcome: null,
374
1072
  preconditions,
375
1073
  errors: errorList,
376
1074
  archive: "",
377
1075
  sessionRoot: "",
378
1076
  worktree: "",
379
1077
  worktreeReady: false,
1078
+ worktreeStatus: null,
380
1079
  branch: "",
381
1080
  codexThreadId: "",
382
1081
  targetRoot: normalizedTargetRoot
@@ -392,15 +1091,29 @@ async function markCurrentStep(paths, stepId) {
392
1091
  }
393
1092
 
394
1093
  async function writeReceipt(paths, stepId, message) {
395
- 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 });
396
1097
  await writeTextFile(
397
- path.join(paths.sessionRoot, "steps", stepId),
1098
+ path.join(root, stepId),
398
1099
  `${timestampForReceipt()}\n${normalizeText(message) || STEP_LABEL_BY_ID[stepId] || stepId}`
399
1100
  );
400
- const completedSteps = await readCompletedSteps(paths.sessionRoot);
1101
+ const completedSteps = await readCompletedSteps(paths);
401
1102
  await markCurrentStep(paths, resolveNextStep(completedSteps));
402
1103
  }
403
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
+
404
1117
  async function failSession(paths, {
405
1118
  code,
406
1119
  message,
@@ -436,7 +1149,14 @@ export {
436
1149
  failSession,
437
1150
  markCurrentStep,
438
1151
  markStatus,
1152
+ normalizeReviewPassNumber,
1153
+ readActiveCycle,
439
1154
  readReceiptSteps,
1155
+ readReviewPasses,
440
1156
  readSessionArtifacts,
1157
+ reviewPassDirectoryName,
1158
+ reviewPassRoot,
1159
+ writeActiveCycle,
1160
+ writeCycleReceipt,
441
1161
  writeReceipt
442
1162
  };