@jskit-ai/jskit-cli 0.2.81 → 0.2.83

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