@jskit-ai/jskit-cli 0.2.88 → 0.2.90

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 (25) hide show
  1. package/package.json +4 -4
  2. package/src/server/commandHandlers/session.js +173 -142
  3. package/src/server/core/argParser.js +0 -4
  4. package/src/server/core/commandCatalog.js +47 -35
  5. package/src/server/sessionRuntime/constants.js +130 -353
  6. package/src/server/sessionRuntime/io.js +2 -2
  7. package/src/server/sessionRuntime/preconditions.js +40 -144
  8. package/src/server/sessionRuntime/promptRenderer.js +2 -15
  9. package/src/server/sessionRuntime/prompts/app_blueprint.md +2 -7
  10. package/src/server/sessionRuntime/prompts/{automated_checks.md → automated_checks_run.md} +2 -17
  11. package/src/server/sessionRuntime/prompts/{update_blueprint.md → blueprint_updated.md} +2 -11
  12. package/src/server/sessionRuntime/prompts/{deep_ui_check.md → deep_ui_check_run.md} +2 -19
  13. package/src/server/sessionRuntime/prompts/final_report_created.md +44 -0
  14. package/src/server/sessionRuntime/prompts/issue_created.md +26 -0
  15. package/src/server/sessionRuntime/prompts/issue_prompt_rendered.md +1 -0
  16. package/src/server/sessionRuntime/prompts/{plan_issue.md → make_plan.md} +8 -37
  17. package/src/server/sessionRuntime/prompts/{execute_plan.md → plan_executed.md} +4 -29
  18. package/src/server/sessionRuntime/prompts/{prepare_pr_merge.md → pr_merge_prepared.md} +3 -3
  19. package/src/server/sessionRuntime/prompts/{resolve_deslop_findings.md → review_changes_accepted_resolve.md} +2 -6
  20. package/src/server/sessionRuntime/prompts/{review_changes.md → review_prompt_rendered.md} +3 -28
  21. package/src/server/sessionRuntime/prompts/{user_check.md → user_check_completed.md} +1 -11
  22. package/src/server/sessionRuntime/responses.js +504 -295
  23. package/src/server/sessionRuntime.js +1300 -961
  24. package/src/server/sessionRuntime/prompts/issue_details.md +0 -49
  25. package/src/server/sessionRuntime/prompts/new_issue.md +0 -46
@@ -2,8 +2,10 @@ import { mkdir, readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import {
4
4
  CYCLE_STEP_IDS,
5
+ DEPENDENCIES_INSTALL_RESULT_FILE,
6
+ ISSUE_FILE_CODEX_HANDOFF,
7
+ ISSUE_DEFINITION_CODEX_HANDOFF,
5
8
  JSKIT_CLI_SHELL_COMMAND,
6
- PROMPT_DIRECTORY,
7
9
  REVIEW_EXECUTION_CODEX_HANDOFF,
8
10
  REVIEW_PASS_LIMIT,
9
11
  SESSION_WORKFLOW_VERSION,
@@ -19,7 +21,7 @@ import {
19
21
  readTextIfExists,
20
22
  readTrimmedFile,
21
23
  runGitInWorktree,
22
- timestampForReceipt,
24
+ timestampForStepRecord,
23
25
  writeTextFile
24
26
  } from "./io.js";
25
27
  import {
@@ -56,6 +58,24 @@ function createPrecondition({
56
58
  });
57
59
  }
58
60
 
61
+ function createWarning({
62
+ code,
63
+ message,
64
+ repairCommand = ""
65
+ }) {
66
+ return createError({ code, message, repairCommand });
67
+ }
68
+
69
+ const ACCEPTED_CHANGES_NOOP_WARNING = createWarning({
70
+ code: "accepted_changes_noop",
71
+ message: "No accepted worktree changes were found; continuing without a new commit."
72
+ });
73
+
74
+ function issueNumberFromUrl(issueUrl = "") {
75
+ const match = /\/issues\/(\d+)(?:\b|$)/u.exec(String(issueUrl || ""));
76
+ return match ? match[1] : "";
77
+ }
78
+
59
79
  function normalizeStepId(stepId) {
60
80
  return normalizeText(stepId);
61
81
  }
@@ -113,10 +133,6 @@ async function readActiveCycle(paths) {
113
133
  return normalizeCycleNumber(cycle || DEFAULT_ACTIVE_CYCLE);
114
134
  }
115
135
 
116
- async function writeActiveCycle(paths, cycle) {
117
- await writeTextFile(path.join(paths.sessionRoot, "active_cycle"), `${normalizeCycleNumber(cycle)}\n`);
118
- }
119
-
120
136
  function cycleStepsRoot(paths, cycle) {
121
137
  return path.join(paths.sessionRoot, "steps", cycleDirectoryName(cycle));
122
138
  }
@@ -125,18 +141,6 @@ function cycleRoot(paths, cycle) {
125
141
  return path.join(paths.sessionRoot, "cycles", cycleDirectoryName(cycle));
126
142
  }
127
143
 
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
144
  function normalizeReviewPassNumber(value = "") {
141
145
  const normalized = normalizeText(value).replace(/^pass_/u, "");
142
146
  if (!/^\d+$/u.test(normalized)) {
@@ -191,7 +195,7 @@ async function readReviewPassInfo(paths, pass) {
191
195
  pass: normalizedPass,
192
196
  label: reviewPassDirectoryName(normalizedPass),
193
197
  status,
194
- promptPath: prompt?.promptPath || path.join(root, "prompt.md"),
198
+ promptPath: prompt?.promptPath || path.join(root, "review_prompt_rendered"),
195
199
  acceptedAt: accepted?.acceptedAt || "",
196
200
  changedFiles,
197
201
  commit: "",
@@ -235,27 +239,6 @@ async function readReviewPromptForStep(paths, artifacts = {}) {
235
239
  return "";
236
240
  }
237
241
 
238
- const PROMPT_ARTIFACT_BY_STEP_ID = Object.freeze({
239
- issue_drafted: "issue_draft.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",
244
- pr_merge_prepared: "prepare_pr_merge.md",
245
- user_check_completed: "user_check.md"
246
- });
247
-
248
- async function promptArtifactForStep(paths, stepId) {
249
- const normalizedStepId = normalizeStepId(stepId);
250
- if (normalizedStepId === "plan_made") {
251
- return cyclePlanPromptFileName(await readActiveCycle(paths));
252
- }
253
- if (normalizedStepId === "plan_executed") {
254
- return cyclePlanExecutionPromptFileName(await readActiveCycle(paths));
255
- }
256
- return PROMPT_ARTIFACT_BY_STEP_ID[normalizedStepId] || "";
257
- }
258
-
259
242
  async function readPromptForStep(paths, stepId, artifacts = {}) {
260
243
  if (!stepCanExposeStoredPrompt(stepId)) {
261
244
  return "";
@@ -263,13 +246,6 @@ async function readPromptForStep(paths, stepId, artifacts = {}) {
263
246
  if (REVIEW_STEP_IDS.includes(normalizeStepId(stepId))) {
264
247
  return readReviewPromptForStep(paths, artifacts);
265
248
  }
266
- const promptArtifact = await promptArtifactForStep(paths, stepId);
267
- if (promptArtifact) {
268
- const prompt = await readTextIfExists(path.join(paths.sessionRoot, "prompts", promptArtifact));
269
- if (prompt) {
270
- return prompt;
271
- }
272
- }
273
249
  return "";
274
250
  }
275
251
 
@@ -286,11 +262,18 @@ async function readStepFileNames(stepsRoot) {
286
262
 
287
263
  async function readCompletedSteps(paths) {
288
264
  const stepsRoot = path.join(paths.sessionRoot, "steps");
289
- const activeCycle = await readActiveCycle(paths);
290
265
  const globalStepIds = normalizeKnownStepIds(
291
266
  (await readStepFileNames(stepsRoot)).filter((stepId) => !isCycleStepId(stepId))
292
267
  );
293
- const cycleStepIds = normalizeKnownStepIds(await readStepFileNames(cycleStepsRoot(paths, activeCycle)));
268
+ const cycleStepIds = [];
269
+ try {
270
+ const entries = await readdir(stepsRoot, { withFileTypes: true });
271
+ for (const entry of entries.filter((item) => item.isDirectory() && /^cycle_\d+$/u.test(item.name)).sort((left, right) => left.name.localeCompare(right.name))) {
272
+ cycleStepIds.push(...await readStepFileNames(path.join(stepsRoot, entry.name)));
273
+ }
274
+ } catch {
275
+ // Legacy sessions may not have cycle record directories.
276
+ }
294
277
  return applyReviewPassCompletionOverlay(paths, normalizeKnownStepIds([...globalStepIds, ...cycleStepIds]));
295
278
  }
296
279
 
@@ -302,7 +285,6 @@ async function applyReviewPassCompletionOverlay(paths, completedSteps = []) {
302
285
  const reviewPasses = await readReviewPasses(paths);
303
286
  const latestPass = reviewPasses.at(-1);
304
287
  if (!latestPass) {
305
- REVIEW_STEP_IDS.forEach((stepId) => completed.delete(stepId));
306
288
  return normalizeKnownStepIds([...completed]);
307
289
  }
308
290
  const latestPassAccepted = latestPass.status === "accepted" || latestPass.status === "no_changes";
@@ -311,11 +293,6 @@ async function applyReviewPassCompletionOverlay(paths, completedSteps = []) {
311
293
  REVIEW_STEP_IDS.forEach((stepId) => completed.delete(stepId));
312
294
  return normalizeKnownStepIds([...completed]);
313
295
  }
314
- if (latestPass.status === "prompted") {
315
- completed.add("review_prompt_rendered");
316
- completed.delete("review_changes_accepted");
317
- return normalizeKnownStepIds([...completed]);
318
- }
319
296
  if (latestPass.status === "accepted" || latestPass.status === "no_changes") {
320
297
  REVIEW_STEP_IDS.forEach((stepId) => completed.add(stepId));
321
298
  }
@@ -327,7 +304,7 @@ async function readCycleInfo(paths, cycle) {
327
304
  const root = cycleRoot(paths, normalizedCycle);
328
305
  const userCheckPassed = await readTextIfExists(path.join(cycleStepsRoot(paths, normalizedCycle), "user_check_completed"));
329
306
  const userCheckFailed = await readTextIfExists(path.join(cycleStepsRoot(paths, normalizedCycle), "user_check_failed"));
330
- const reworkRequestPath = path.join(root, "rework_request.md");
307
+ const reworkRequestPath = path.join(root, "rework_request");
331
308
  const reworkRequest = await readTextIfExists(reworkRequestPath);
332
309
  return {
333
310
  cycle: normalizedCycle,
@@ -336,7 +313,7 @@ async function readCycleInfo(paths, cycle) {
336
313
  reworkRequestPath: reworkRequest ? reworkRequestPath : "",
337
314
  status: userCheckPassed ? "passed" : userCheckFailed ? "failed" : "active",
338
315
  userCheckResult: userCheckPassed ? "passed" : userCheckFailed ? "failed" : "",
339
- userCheckReceipt: (userCheckPassed || userCheckFailed).trim()
316
+ userCheckRecord: (userCheckPassed || userCheckFailed).trim()
340
317
  };
341
318
  }
342
319
 
@@ -438,7 +415,7 @@ async function readWorktreeStatus(paths, worktreeReady) {
438
415
  };
439
416
  }
440
417
 
441
- async function readReceiptSteps(paths) {
418
+ async function readStepRecords(paths) {
442
419
  const stepsRoot = path.join(paths.sessionRoot, "steps");
443
420
  try {
444
421
  const entries = await readdir(stepsRoot, { withFileTypes: true });
@@ -447,19 +424,19 @@ async function readReceiptSteps(paths) {
447
424
  entries
448
425
  .filter((entry) => entry.isFile())
449
426
  .map((entry) => entry.name)
450
- .forEach((receiptName) => {
451
- const stepId = normalizeStepId(receiptName);
427
+ .forEach((recordName) => {
428
+ const stepId = normalizeStepId(recordName);
452
429
  if (STEP_IDS.includes(stepId)) {
453
- if (!knownStepRows.has(stepId) || receiptName === stepId) {
430
+ if (!knownStepRows.has(stepId) || recordName === stepId) {
454
431
  knownStepRows.set(stepId, {
455
- receiptName,
432
+ recordName,
456
433
  stepId
457
434
  });
458
435
  }
459
436
  return;
460
437
  }
461
438
  unknownStepRows.push({
462
- receiptName,
439
+ recordName,
463
440
  stepId
464
441
  });
465
442
  });
@@ -480,14 +457,14 @@ async function readReceiptSteps(paths) {
480
457
  return left.stepId.localeCompare(right.stepId);
481
458
  });
482
459
 
483
- const globalReceipts = await Promise.all(stepRows.map(async ({ receiptName, stepId }) => ({
460
+ const globalRecords = await Promise.all(stepRows.map(async ({ recordName, stepId }) => ({
484
461
  cycle: "",
462
+ details: (await readTextIfExists(path.join(stepsRoot, recordName))).trim(),
485
463
  label: STEP_LABEL_BY_ID[stepId] || stepId,
486
- receipt: (await readTextIfExists(path.join(stepsRoot, receiptName))).trim(),
487
464
  stepId
488
465
  })));
489
466
 
490
- const cycleReceipts = [];
467
+ const cycleRecords = [];
491
468
  const cycleDirectories = entries
492
469
  .filter((entry) => entry.isDirectory() && /^cycle_\d+$/u.test(entry.name))
493
470
  .map((entry) => entry.name)
@@ -496,18 +473,18 @@ async function readReceiptSteps(paths) {
496
473
  const cycle = normalizeCycleNumber(cycleDirectory);
497
474
  const cycleRootPath = path.join(stepsRoot, cycleDirectory);
498
475
  const cycleStepIds = await readStepFileNames(cycleRootPath);
499
- for (const receiptName of cycleStepIds) {
500
- const stepId = normalizeStepId(receiptName);
501
- cycleReceipts.push({
476
+ for (const recordName of cycleStepIds) {
477
+ const stepId = normalizeStepId(recordName);
478
+ cycleRecords.push({
502
479
  cycle,
480
+ details: (await readTextIfExists(path.join(cycleRootPath, recordName))).trim(),
503
481
  label: STEP_LABEL_BY_ID[stepId] || stepId,
504
- receipt: (await readTextIfExists(path.join(cycleRootPath, receiptName))).trim(),
505
482
  stepId
506
483
  });
507
484
  }
508
485
  }
509
486
 
510
- return [...globalReceipts, ...cycleReceipts];
487
+ return [...globalRecords, ...cycleRecords];
511
488
  } catch {
512
489
  return [];
513
490
  }
@@ -530,37 +507,56 @@ function cloneContractValue(value) {
530
507
  );
531
508
  }
532
509
 
510
+ function normalizeWarning(warning) {
511
+ if (typeof warning === "string") {
512
+ return createWarning({
513
+ code: "session_warning",
514
+ message: warning
515
+ });
516
+ }
517
+ if (!warning || typeof warning !== "object" || Array.isArray(warning)) {
518
+ return null;
519
+ }
520
+ return createWarning({
521
+ code: warning.code || "session_warning",
522
+ message: warning.message || "",
523
+ repairCommand: warning.repairCommand || ""
524
+ });
525
+ }
526
+
527
+ function mergeWarnings(...warningLists) {
528
+ const merged = [];
529
+ const seen = new Set();
530
+ for (const warnings of warningLists) {
531
+ for (const warning of Array.isArray(warnings) ? warnings : []) {
532
+ const normalized = normalizeWarning(warning);
533
+ if (!normalized?.message) {
534
+ continue;
535
+ }
536
+ const key = `${normalized.code}\n${normalized.message}`;
537
+ if (seen.has(key)) {
538
+ continue;
539
+ }
540
+ seen.add(key);
541
+ merged.push(normalized);
542
+ }
543
+ }
544
+ return merged;
545
+ }
546
+
533
547
  async function publicCodexContract(codex = null) {
534
548
  if (!codex || typeof codex !== "object" || Array.isArray(codex)) {
535
549
  return null;
536
550
  }
537
- const clonedCodex = cloneContractValue(codex);
538
- const resolvePrompt = clonedCodex.responseContract?.resolvePrompt;
539
- const templateFile = normalizeText(resolvePrompt?.templateFile || "");
540
- if (templateFile) {
541
- const template = await readTextIfExists(path.join(PROMPT_DIRECTORY, templateFile));
542
- clonedCodex.responseContract.resolvePrompt = {
543
- ...resolvePrompt,
544
- template
545
- };
546
- }
547
- return clonedCodex;
551
+ return cloneContractValue(codex);
548
552
  }
549
553
 
550
554
  function stepRepeatabilityContract(stepId) {
551
- if (!CYCLE_STEP_IDS.includes(normalizeStepId(stepId))) {
552
- return {
553
- repeatable: false,
554
- repeatableGroupId: "",
555
- repeatableGroupLabel: "",
556
- repeatableLabel: ""
557
- };
558
- }
559
555
  return {
560
- repeatable: true,
561
- repeatableGroupId: "rework_cycle",
562
- repeatableGroupLabel: "Rework cycle",
563
- repeatableLabel: "Cycle step"
556
+ repeatable: false,
557
+ repeatableGroupId: "",
558
+ repeatableGroupLabel: "",
559
+ repeatableLabel: ""
564
560
  };
565
561
  }
566
562
 
@@ -595,17 +591,6 @@ function stepIsRetryableWhenBlocked(stepId) {
595
591
  ].includes(normalizeStepId(stepId));
596
592
  }
597
593
 
598
- function stepIsConditional(stepId) {
599
- return [
600
- "deep_ui_check_run"
601
- ].includes(normalizeStepId(stepId));
602
- }
603
-
604
- function activeCycleInfoFromArtifacts(artifacts = {}) {
605
- const activeCycle = normalizeCycleNumber(artifacts.activeCycle || "");
606
- return (artifacts.cycles || []).find((cycle) => cycle?.cycle === activeCycle) || null;
607
- }
608
-
609
594
  function uiCheckPromptedForStep(artifacts = {}, stepId = "") {
610
595
  const normalizedStepId = normalizeStepId(stepId);
611
596
  return (artifacts.uiChecks || []).some((entry) => {
@@ -615,10 +600,8 @@ function uiCheckPromptedForStep(artifacts = {}, stepId = "") {
615
600
  }
616
601
 
617
602
  function skipReasonForStep(stepId, artifacts = {}) {
618
- const normalizedStepId = normalizeStepId(stepId);
619
- if (normalizedStepId === "deep_ui_check_run" && artifacts.uiImpact === "none") {
620
- return "uiImpact is none.";
621
- }
603
+ void stepId;
604
+ void artifacts;
622
605
  return "";
623
606
  }
624
607
 
@@ -627,53 +610,27 @@ function buildCurrentStepAction(stepId, artifacts = {}) {
627
610
  if (!step) {
628
611
  return null;
629
612
  }
630
- const activeCycleInfo = activeCycleInfoFromArtifacts(artifacts);
631
613
  const planExecutionPrompted = artifacts.planExecution?.prompted === true;
632
614
  const planExecutionSubmitted = artifacts.planExecution?.submitted === true;
633
- const planReworkMode = step.id === "plan_made" && normalizeCycleNumber(artifacts.activeCycle || "") !== "001";
615
+ const issueDefinitionPrompted = step.id === "issue_prompt_rendered" && artifacts.issueDefinitionRequested === true;
616
+ const issueFilePromptAction = step.id === "issue_created";
617
+ const issueSubmissionAction = step.id === "issue_submitted";
618
+ const pullRequestFileAction = step.id === "final_report_created";
619
+ const prSubmissionAction = step.id === "pr_created";
634
620
  const deepUiCheckPrompted = step.id === "deep_ui_check_run" && uiCheckPromptedForStep(artifacts, "deep_ui_check_run");
635
- const hasActiveReworkRequest = Boolean(activeCycleInfo?.reworkRequestPath);
636
- const promptPhaseButtonLabel = step.kind === "codex_output" &&
637
- step.codex?.mode === "inject_prompt" &&
638
- !artifacts.prompt
639
- ? step.codex.promptActionLabel || ""
640
- : "";
641
- const buttonLabel = promptPhaseButtonLabel || step.buttonLabel;
621
+ const automatedChecksPrompted = step.id === "automated_checks_run" && (artifacts.checks || []).some((entry) => {
622
+ return normalizeStepId(entry?.stepId || "") === "automated_checks_run" &&
623
+ normalizeText(entry?.status || "") === "prompted";
624
+ });
642
625
  const alternateActions = [];
643
- if (step.id === "user_check_completed") {
644
- alternateActions.push({
645
- id: "return_to_plan_made",
646
- input: {
647
- formatHint: "markdown",
648
- label: "What needs to be reworked?",
649
- multiline: true,
650
- name: "reworkNotes",
651
- required: true,
652
- type: "text"
653
- },
654
- label: "Return to Plan made",
655
- presentation: "exclusive",
656
- requiredErrorCode: "user_check_failed",
657
- submitOptions: {
658
- userCheck: "failed"
659
- },
660
- targetStep: "plan_made"
661
- });
662
- }
663
626
  if (step.id === "review_changes_accepted") {
664
627
  alternateActions.push({
665
628
  id: "request_another_review_pass",
666
- helpText: "Use this only when important review findings remain. Studio will record these notes and loop back to Codex review.",
629
+ helpText: "Run another explicit deslop prompt before continuing.",
667
630
  input: {
668
- formatHint: "markdown",
669
- label: "What important findings remain?",
670
- multiline: true,
671
- name: "reviewFindings",
672
- placeholder: "List the specific findings Codex still needs to address.",
673
- required: true,
674
- type: "text"
631
+ type: "none"
675
632
  },
676
- label: "Run another review pass",
633
+ label: "Run deslop",
677
634
  presentation: "secondary",
678
635
  submitOptions: {
679
636
  reviewFindingsRemaining: true
@@ -681,73 +638,75 @@ function buildCurrentStepAction(stepId, artifacts = {}) {
681
638
  targetStep: "review_prompt_rendered"
682
639
  });
683
640
  }
684
- if (step.id === "pr_finalized") {
685
- alternateActions.push({
686
- id: "skip_merge",
687
- helpText: "Leave the PR open and record the skipped-merge outcome; final cleanup removes the session worktree.",
688
- input: {
689
- type: "none"
690
- },
691
- label: "Skip merge",
692
- presentation: "secondary",
693
- submitOptions: {
694
- closeWithoutMerge: true
695
- },
696
- targetStep: "pr_finalized"
697
- });
698
- }
699
- if (step.id === "main_checkout_synced") {
700
- alternateActions.push({
701
- id: "skip_main_checkout_sync",
702
- helpText: "Leave the main checkout untouched and continue with session cleanup.",
703
- input: {
704
- type: "none"
705
- },
706
- label: "Skip sync",
707
- presentation: "secondary",
708
- submitOptions: {
709
- skipMainSync: true
710
- },
711
- targetStep: "main_checkout_synced"
712
- });
713
- }
714
641
  const dynamicButtonLabel = (() => {
715
- if (step.id === "plan_executed" && planExecutionPrompted && !planExecutionSubmitted) {
716
- return "Go to next step";
642
+ if (step.id === "issue_created" && !artifacts.issueText) {
643
+ return "Create issue file";
717
644
  }
718
- if (step.id === "deep_ui_check_run" && deepUiCheckPrompted) {
719
- return "Go to next step";
645
+ if (step.id === "issue_submitted") {
646
+ return "Create issue on GH";
720
647
  }
721
- if (step.id === "automated_checks_run" && artifacts.prompt) {
722
- return "Go to next step";
648
+ if (step.id === "pr_created") {
649
+ return "Create PR on GH";
723
650
  }
724
- if (step.id === "plan_made" && planReworkMode && !artifacts.prompt && hasActiveReworkRequest) {
725
- return "Get Codex to create revised plan";
651
+ if (step.id === "pr_merge_prepared") {
652
+ return "Merge";
726
653
  }
727
654
  if (step.id === "main_checkout_synced" && artifacts.prOutcome?.outcome && artifacts.prOutcome.outcome !== "merged") {
728
- return "Record no sync needed";
655
+ return "Sync main checkout";
729
656
  }
730
- return buttonLabel;
657
+ return step.buttonLabel;
731
658
  })();
732
659
  const dynamicDescription = (() => {
660
+ if (issueDefinitionPrompted) {
661
+ return "Codex has the issue-definition prompt. Continue after the issue is scoped clearly enough.";
662
+ }
663
+ if (issueFilePromptAction && artifacts.issueFileRequested) {
664
+ return "Codex has the issue-file prompt. Review issue.md and issue_title, then continue when ready.";
665
+ }
666
+ if (issueSubmissionAction && artifacts.issueUrl) {
667
+ return "The GitHub issue has been created. Continue when ready.";
668
+ }
669
+ if (pullRequestFileAction && artifacts.pullRequestFileRequested) {
670
+ return "Codex has the PR-file prompt. Review pull_request.md, then continue when ready.";
671
+ }
672
+ if (prSubmissionAction && artifacts.prUrl) {
673
+ return "The GitHub pull request has been created. Continue when ready.";
674
+ }
675
+ if (step.id === "pr_merge_prepared" && artifacts.prOutcome?.outcome === "merged") {
676
+ return "The pull request was merged. Use Next when ready.";
677
+ }
733
678
  if (step.id === "plan_executed" && planExecutionPrompted && !planExecutionSubmitted) {
734
- return "Codex has the execution prompt. Review the result, then use Go to next step when ready.";
679
+ return "Codex has the execution prompt. Review the result, then use Next when ready.";
735
680
  }
736
681
  if (step.id === "deep_ui_check_run" && deepUiCheckPrompted) {
737
- return "Codex has the Deep UI check prompt. Studio advances when Codex finishes.";
738
- }
739
- if (step.id === "automated_checks_run" && artifacts.prompt) {
740
- return "Codex has the automated-checks prompt. Studio advances when Codex finishes.";
682
+ return "Codex has the run deep UI check prompt. Review the result, then use Next when ready.";
741
683
  }
742
- if (step.id === "plan_made" && planReworkMode && hasActiveReworkRequest) {
743
- return "Codex writes a revised implementation plan from the user's rework notes for this cycle.";
684
+ if (step.id === "automated_checks_run" && automatedChecksPrompted) {
685
+ return "Codex has the run automated checks prompt. Review the result, then use Next when ready.";
744
686
  }
745
687
  if (step.id === "main_checkout_synced" && artifacts.prOutcome?.outcome && artifacts.prOutcome.outcome !== "merged") {
746
- return "The PR was not merged, so JSKIT will record main checkout sync as skipped before cleanup.";
688
+ return "Main checkout sync is only available after a successful merge. Use Next to continue.";
689
+ }
690
+ if (step.id === "main_checkout_synced" && artifacts.mainCheckoutSync?.status === "synced") {
691
+ return "The main checkout has been synced. Use Next when ready.";
747
692
  }
748
693
  return step.description;
749
694
  })();
750
695
  const dynamicUtilityActions = (() => {
696
+ if (step.id === "review_prompt_rendered" || step.id === "review_changes_accepted") {
697
+ return [
698
+ {
699
+ id: "resolve_deslop",
700
+ helpText: "Send Codex the explicit resolve deslop prompt. Nothing advances automatically after it finishes.",
701
+ kind: "codex_prompt",
702
+ label: "Resolve deslop",
703
+ submitOptions: {
704
+ actionCommand: "resolve_deslop"
705
+ }
706
+ },
707
+ ...(step.utilityActions || [])
708
+ ];
709
+ }
751
710
  return step.utilityActions || [];
752
711
  })();
753
712
  return {
@@ -757,14 +716,13 @@ function buildCurrentStepAction(stepId, artifacts = {}) {
757
716
  displayGroupId: step.displayGroupId,
758
717
  displayGroupLabel: step.displayGroupLabel,
759
718
  index: STEP_IDS.indexOf(step.id),
760
- input: cloneContractValue(step.input),
761
- kind: step.kind,
719
+ input: cloneContractValue(issueDefinitionPrompted || issueFilePromptAction || pullRequestFileAction ? { type: "none" } : step.input),
720
+ kind: issueDefinitionPrompted || issueFilePromptAction || pullRequestFileAction ? "codex_prompt" : step.kind,
762
721
  label: dynamicButtonLabel,
763
- automation: cloneContractValue(step.automation || { mode: "manual" }),
722
+ automation: cloneContractValue(issueDefinitionPrompted || issueFilePromptAction || pullRequestFileAction ? { mode: "codex_prompt" } : step.automation || { mode: "manual" }),
764
723
  ...stepRepeatabilityContract(step.id),
765
- requiredInput: cloneContractValue(step.input),
724
+ requiredInput: cloneContractValue(issueDefinitionPrompted || issueFilePromptAction || pullRequestFileAction ? { type: "none" } : step.input),
766
725
  requiresExplicitRun: step.requiresExplicitRun === true,
767
- conditional: stepIsConditional(step.id),
768
726
  retryable: artifacts.status === SESSION_STATUS.BLOCKED && stepIsRetryableWhenBlocked(step.id),
769
727
  skipReason: skipReasonForStep(step.id, artifacts),
770
728
  stepId: step.id,
@@ -774,19 +732,17 @@ function buildCurrentStepAction(stepId, artifacts = {}) {
774
732
  }
775
733
 
776
734
  function rawCodexHandoff(stepId, artifacts = {}) {
735
+ if (normalizeStepId(stepId) === "issue_prompt_rendered" && artifacts.issueDefinitionRequested) {
736
+ return cloneContractValue(ISSUE_DEFINITION_CODEX_HANDOFF);
737
+ }
738
+ if (normalizeStepId(stepId) === "issue_created" && artifacts.issueFileRequested) {
739
+ return cloneContractValue(ISSUE_FILE_CODEX_HANDOFF);
740
+ }
777
741
  if (normalizeStepId(stepId) === "review_changes_accepted" && latestReviewPassIsPrompted(artifacts)) {
778
742
  return cloneContractValue(REVIEW_EXECUTION_CODEX_HANDOFF);
779
743
  }
780
744
  const step = STEP_DEFINITION_BY_ID[stepId];
781
- const codex = step?.codex ? cloneContractValue(step.codex) : null;
782
- if (
783
- codex &&
784
- normalizeStepId(stepId) === "plan_made" &&
785
- normalizeCycleNumber(artifacts.activeCycle || "") !== "001"
786
- ) {
787
- codex.promptIntroText = "Codex will create a revised implementation plan based on the rework notes.";
788
- }
789
- return codex;
745
+ return step?.codex ? cloneContractValue(step.codex) : null;
790
746
  }
791
747
 
792
748
  async function buildCodexHandoff(stepId, artifacts = {}) {
@@ -795,55 +751,59 @@ async function buildCodexHandoff(stepId, artifacts = {}) {
795
751
 
796
752
  async function readSessionArtifacts(paths) {
797
753
  const activeCycle = await readActiveCycle(paths);
754
+ const globalPlanExecutionRecordPath = path.join(paths.sessionRoot, "steps", "plan_executed");
755
+ const legacyPlanExecutionRecordPath = path.join(cycleStepsRoot(paths, activeCycle), "plan_executed");
798
756
  const [
799
757
  status,
800
758
  rawCurrentStep,
801
759
  issueUrl,
760
+ issueNumber,
802
761
  prUrl,
803
762
  issueText,
804
763
  issueTitle,
805
- planText,
806
- issueDetails,
807
- agentDecisions,
808
764
  finalReportText,
765
+ pullRequestText,
809
766
  githubCommentsText,
810
767
  codexThreadId,
811
768
  workflowVersion,
812
769
  baseBranch,
813
770
  baseCommit,
814
- issueMetadataText,
815
- planExecutionReceipt,
771
+ planExecutionRecord,
772
+ issueDefinitionRequested,
773
+ issueFileRequested,
774
+ pullRequestFileRequested,
775
+ makePlanRequested,
776
+ blueprintUpdateRequested,
777
+ executePlanRequested,
816
778
  prOutcomeText,
817
- mainCheckoutSyncText
779
+ mainCheckoutSyncText,
780
+ changesCommittedText
818
781
  ] = await Promise.all([
819
782
  readTrimmedFile(path.join(paths.sessionRoot, "status")),
820
783
  readTrimmedFile(path.join(paths.sessionRoot, "current_step")),
821
784
  readTrimmedFile(path.join(paths.sessionRoot, "issue_url")),
785
+ readTrimmedFile(path.join(paths.sessionRoot, "metadata", "issue_number")),
822
786
  readTrimmedFile(path.join(paths.sessionRoot, "pr_url")),
823
787
  readTextIfExists(path.join(paths.sessionRoot, "issue.md")),
824
788
  readTrimmedFile(path.join(paths.sessionRoot, "issue_title")),
825
- readTextIfExists(cyclePlanPath(paths, activeCycle)),
826
- readTextIfExists(path.join(paths.sessionRoot, "issue_details.md")),
827
- readTextIfExists(path.join(paths.sessionRoot, "agent_decisions.md")),
828
- readTextIfExists(path.join(paths.sessionRoot, "final_report.md")),
789
+ readTextIfExists(path.join(paths.sessionRoot, "final_report")),
790
+ readTextIfExists(path.join(paths.sessionRoot, "pull_request.md")),
829
791
  readTextIfExists(path.join(paths.sessionRoot, "github_comments.json")),
830
792
  readTrimmedFile(path.join(paths.sessionRoot, "codex_thread_id")),
831
793
  readWorkflowVersion(paths),
832
794
  readTrimmedFile(path.join(paths.sessionRoot, "base_branch")),
833
795
  readTrimmedFile(path.join(paths.sessionRoot, "base_commit")),
834
- readTextIfExists(path.join(paths.sessionRoot, "issue_metadata.json")),
835
- readTextIfExists(path.join(cycleStepsRoot(paths, activeCycle), "plan_executed")),
796
+ readTextIfExists(globalPlanExecutionRecordPath).then(async (text) => text || await readTextIfExists(legacyPlanExecutionRecordPath)),
797
+ readTrimmedFile(path.join(paths.sessionRoot, "metadata", "issue_prompt_rendered_requested")),
798
+ readTrimmedFile(path.join(paths.sessionRoot, "metadata", "issue_created_requested")),
799
+ readTrimmedFile(path.join(paths.sessionRoot, "metadata", "pull_request_file_requested")),
800
+ readTrimmedFile(path.join(paths.sessionRoot, "metadata", "make_plan_requested")),
801
+ readTrimmedFile(path.join(paths.sessionRoot, "metadata", "blueprint_updated_requested")),
802
+ readTrimmedFile(path.join(paths.sessionRoot, "metadata", "execute_plan_requested")),
836
803
  readTextIfExists(path.join(paths.sessionRoot, "pr_outcome.json")),
837
- readTextIfExists(path.join(paths.sessionRoot, "main_checkout_sync.json"))
804
+ readTextIfExists(path.join(paths.sessionRoot, "main_checkout_sync.json")),
805
+ readTextIfExists(path.join(paths.sessionRoot, "changes_committed.json"))
838
806
  ]);
839
- let issueMetadata = null;
840
- if (issueMetadataText) {
841
- try {
842
- issueMetadata = JSON.parse(issueMetadataText);
843
- } catch {
844
- issueMetadata = null;
845
- }
846
- }
847
807
  let githubComments = {};
848
808
  if (githubCommentsText) {
849
809
  try {
@@ -871,6 +831,18 @@ async function readSessionArtifacts(paths) {
871
831
  mainCheckoutSync = null;
872
832
  }
873
833
  }
834
+ let acceptedChangesCommit = null;
835
+ if (changesCommittedText) {
836
+ try {
837
+ const parsed = JSON.parse(changesCommittedText);
838
+ acceptedChangesCommit = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
839
+ } catch {
840
+ acceptedChangesCommit = null;
841
+ }
842
+ }
843
+ const warnings = acceptedChangesCommit?.noChanges === true
844
+ ? [ACCEPTED_CHANGES_NOOP_WARNING]
845
+ : [];
874
846
  const cycles = await readCycles(paths, activeCycle);
875
847
  const checks = await readStructuredChecks(paths);
876
848
  const uiChecks = await readStructuredUiChecks(paths);
@@ -878,9 +850,8 @@ async function readSessionArtifacts(paths) {
878
850
  const worktreeReady = await hasWorktree(paths);
879
851
  const worktreeStatus = await readWorktreeStatus(paths, worktreeReady);
880
852
  const commandLogPath = path.join(paths.sessionRoot, "command_log.jsonl");
881
- const dependencyInstallReceipt = await readTextIfExists(path.join(paths.sessionRoot, "steps", "dependencies_installed"));
882
- const planExecutionPromptPath = path.join(paths.sessionRoot, "prompts", cyclePlanExecutionPromptFileName(activeCycle));
883
- const planExecutionPromptExists = await fileExists(planExecutionPromptPath);
853
+ const dependencyInstallRecord = await readTextIfExists(path.join(paths.sessionRoot, "steps", "dependencies_installed"));
854
+ const dependencyInstallResult = await readTextIfExists(path.join(paths.sessionRoot, DEPENDENCIES_INSTALL_RESULT_FILE));
884
855
  const appRootForArtifacts = worktreeReady ? paths.worktree : paths.targetRoot;
885
856
  const appReady = await inspectReadyJskitAppRoot(appRootForArtifacts);
886
857
  const blueprintPath = path.join(appRootForArtifacts, ".jskit", "APP_BLUEPRINT.md");
@@ -888,12 +859,12 @@ async function readSessionArtifacts(paths) {
888
859
  const currentStep = normalizeStepId(rawCurrentStep);
889
860
  let completedSteps = await readCompletedSteps(paths);
890
861
  const worktreeRemovalCompleted = completedSteps.includes("session_finished");
891
- const worktreeReceiptInvalid = !worktreeReady &&
862
+ const worktreeStepRecordInvalid = !worktreeReady &&
892
863
  completedSteps.includes("worktree_created") &&
893
864
  !worktreeRemovalCompleted &&
894
865
  status !== SESSION_STATUS.FINISHED &&
895
866
  status !== SESSION_STATUS.ABANDONED;
896
- if (worktreeReceiptInvalid) {
867
+ if (worktreeStepRecordInvalid) {
897
868
  completedSteps = completedSteps.filter((stepId) => !["worktree_created", "dependencies_installed"].includes(stepId));
898
869
  }
899
870
  const nextStep = resolveNextStep(completedSteps);
@@ -904,6 +875,8 @@ async function readSessionArtifacts(paths) {
904
875
  ? nextStep
905
876
  : currentStep || nextStep;
906
877
  const prompt = await readPromptForStep(paths, effectiveCurrentStep, { reviewPasses });
878
+ const dependencyInstallDetails = dependencyInstallRecord.trim() || dependencyInstallResult.trim();
879
+ const dependencyInstallReady = Boolean(dependencyInstallRecord.trim() || dependencyInstallResult.trim());
907
880
 
908
881
  return {
909
882
  codexThreadId,
@@ -923,64 +896,296 @@ async function readSessionArtifacts(paths) {
923
896
  commandLogExists: await fileExists(commandLogPath),
924
897
  commandLogPath,
925
898
  dependencyInstall: {
926
- installed: Boolean(dependencyInstallReceipt.trim()),
927
- receipt: dependencyInstallReceipt.trim(),
928
- status: dependencyInstallReceipt.trim()
899
+ installed: Boolean(dependencyInstallRecord.trim()),
900
+ ready: dependencyInstallReady,
901
+ details: dependencyInstallDetails,
902
+ status: dependencyInstallRecord.trim()
929
903
  ? "installed"
904
+ : dependencyInstallResult.trim() ? "ready_to_advance"
930
905
  : worktreeReady ? "pending" : "waiting_for_worktree"
931
906
  },
932
907
  helperMapExists: await fileExists(helperMapPath),
933
908
  helperMapPath,
934
909
  githubComments,
935
- issueMetadata,
936
- issueCategory: normalizeText(issueMetadata?.issueCategory || ""),
937
- uiImpact: normalizeText(issueMetadata?.uiImpact || ""),
938
- agentDecisions: agentDecisions.trim(),
939
- agentDecisionsLatest: agentDecisions
940
- .split(/\r?\n/u)
941
- .map((line) => line.trim())
942
- .filter((line) => line && !line.startsWith("#") && !line.startsWith("Session:"))
943
- .slice(-5)
944
- .join("\n"),
910
+ issueNumber: issueNumber || issueNumberFromUrl(issueUrl),
945
911
  issueTitle,
946
912
  issueText: issueText.trim(),
947
913
  issueUrl,
948
914
  nextStep,
915
+ pullRequestPath: path.join(paths.sessionRoot, "pull_request.md"),
916
+ pullRequestText: pullRequestText.trim(),
949
917
  prUrl,
950
918
  prOutcome,
951
919
  mainCheckoutSync,
920
+ acceptedChangesCommit,
921
+ issueDefinitionRequested: Boolean(issueDefinitionRequested.trim()),
922
+ issueFileRequested: Boolean(issueFileRequested.trim()),
923
+ pullRequestFileRequested: Boolean(pullRequestFileRequested.trim()),
924
+ makePlanRequested: Boolean(makePlanRequested.trim()),
925
+ blueprintUpdateRequested: Boolean(blueprintUpdateRequested.trim()),
926
+ executePlanRequested: Boolean(executePlanRequested.trim()),
952
927
  planExecution: {
953
- prompted: planExecutionPromptExists,
954
- promptPath: planExecutionPromptExists ? planExecutionPromptPath : "",
955
- receipt: planExecutionReceipt.trim(),
956
- submitted: Boolean(planExecutionReceipt.trim())
928
+ prompted: Boolean(executePlanRequested.trim()),
929
+ promptPath: "",
930
+ details: planExecutionRecord.trim(),
931
+ submitted: Boolean(planExecutionRecord.trim())
957
932
  },
958
- planText: planText.trim(),
959
- issueDetails: issueDetails.trim(),
960
933
  finalReportText: finalReportText.trim(),
961
934
  prompt: prompt.trim(),
962
935
  status: status || SESSION_STATUS.PENDING,
936
+ warnings,
963
937
  workflowVersion,
964
938
  worktreeReady,
965
939
  worktreeStatus
966
940
  };
967
941
  }
968
942
 
969
- function buildNextCommand(sessionId, stepId) {
943
+ function stepCanExposeNextCommand(stepId, artifacts = {}) {
970
944
  if (!stepId) {
945
+ return false;
946
+ }
947
+ if (stepId === "worktree_created") {
948
+ return artifacts.worktreeReady === true;
949
+ }
950
+ if (stepId === "dependencies_installed") {
951
+ return artifacts.dependencyInstall?.ready === true;
952
+ }
953
+ if (stepId === "issue_prompt_rendered") {
954
+ return Boolean(artifacts.issueText);
955
+ }
956
+ if (stepId === "issue_created") {
957
+ return Boolean(artifacts.issueText);
958
+ }
959
+ if (stepId === "issue_submitted") {
960
+ return Boolean(artifacts.issueUrl);
961
+ }
962
+ if (stepId === "changes_committed") {
963
+ return Boolean(artifacts.acceptedChangesCommit?.commit);
964
+ }
965
+ if (stepId === "final_report_created") {
966
+ return Boolean(artifacts.pullRequestText);
967
+ }
968
+ if (stepId === "session_finished") {
969
+ return false;
970
+ }
971
+ if (stepId === "plan_made") {
972
+ return artifacts.makePlanRequested === true;
973
+ }
974
+ if (stepId === "plan_executed") {
975
+ return artifacts.executePlanRequested === true;
976
+ }
977
+ return true;
978
+ }
979
+
980
+ function buildNextCommand(sessionId, stepId, artifacts = {}) {
981
+ if (!stepCanExposeNextCommand(stepId, artifacts)) {
971
982
  return "";
972
983
  }
973
- const template = STEP_DEFINITION_BY_ID[stepId]?.nextCommandTemplate || `${JSKIT_CLI_SHELL_COMMAND} session {{session_id}} step`;
984
+ const template = STEP_DEFINITION_BY_ID[stepId]?.nextCommandTemplate || `${JSKIT_CLI_SHELL_COMMAND} session {{session_id}} next`;
974
985
  return template.replaceAll("{{session_id}}", sessionId);
975
986
  }
976
987
 
988
+ function buildStepActionCommands(sessionId, stepId, artifacts = {}) {
989
+ const commandBase = `${JSKIT_CLI_SHELL_COMMAND} session ${sessionId}`;
990
+ if (stepId === "worktree_created") {
991
+ return artifacts.worktreeReady === true
992
+ ? []
993
+ : [
994
+ {
995
+ command: `${commandBase} create_worktree`,
996
+ id: "create_worktree",
997
+ label: "Create worktree"
998
+ }
999
+ ];
1000
+ }
1001
+ if (stepId === "dependencies_installed") {
1002
+ return artifacts.dependencyInstall?.ready === true
1003
+ ? []
1004
+ : [
1005
+ {
1006
+ command: `${commandBase} run_npm_install`,
1007
+ id: "run_npm_install",
1008
+ label: "Run npm install"
1009
+ }
1010
+ ];
1011
+ }
1012
+ if (stepId === "issue_prompt_rendered") {
1013
+ return [
1014
+ {
1015
+ command: `${commandBase} define_issue --prompt "<what should change>"`,
1016
+ id: "define_issue",
1017
+ label: "Define issue"
1018
+ },
1019
+ ...(artifacts.issueDefinitionRequested
1020
+ ? [
1021
+ {
1022
+ command: `${commandBase} create_issue_file`,
1023
+ id: "create_issue_file",
1024
+ label: "Create issue file"
1025
+ }
1026
+ ]
1027
+ : [])
1028
+ ];
1029
+ }
1030
+ if (stepId === "issue_created") {
1031
+ return [
1032
+ {
1033
+ command: `${commandBase} create_issue_file`,
1034
+ id: "create_issue_file",
1035
+ label: "Create issue file"
1036
+ }
1037
+ ];
1038
+ }
1039
+ if (stepId === "issue_submitted") {
1040
+ return artifacts.issueUrl
1041
+ ? []
1042
+ : [
1043
+ {
1044
+ command: `${commandBase} create_issue_on_gh`,
1045
+ id: "create_issue_on_gh",
1046
+ label: "Create issue on GH"
1047
+ }
1048
+ ];
1049
+ }
1050
+ if (stepId === "plan_made") {
1051
+ return [
1052
+ {
1053
+ command: `${commandBase} make_plan`,
1054
+ id: "make_plan",
1055
+ label: "Make plan"
1056
+ }
1057
+ ];
1058
+ }
1059
+ if (stepId === "plan_executed") {
1060
+ return [
1061
+ {
1062
+ command: `${commandBase} execute_plan`,
1063
+ id: "execute_plan",
1064
+ label: "Execute plan"
1065
+ }
1066
+ ];
1067
+ }
1068
+ if (stepId === "deep_ui_check_run") {
1069
+ return [
1070
+ {
1071
+ command: `${commandBase} run_deep_ui_check`,
1072
+ id: "run_deep_ui_check",
1073
+ label: "Run deep UI check"
1074
+ }
1075
+ ];
1076
+ }
1077
+ if (stepId === "automated_checks_run") {
1078
+ return [
1079
+ {
1080
+ command: `${commandBase} run_automated_checks`,
1081
+ id: "run_automated_checks",
1082
+ label: "Run automated checks"
1083
+ }
1084
+ ];
1085
+ }
1086
+ if (stepId === "review_prompt_rendered") {
1087
+ return [
1088
+ {
1089
+ command: `${commandBase} deslop`,
1090
+ id: "deslop",
1091
+ label: "Run deslop"
1092
+ },
1093
+ {
1094
+ command: `${commandBase} resolve-deslop`,
1095
+ id: "resolve_deslop",
1096
+ label: "Resolve deslop"
1097
+ }
1098
+ ];
1099
+ }
1100
+ if (stepId === "blueprint_updated") {
1101
+ return [
1102
+ {
1103
+ command: `${commandBase} update_blueprint`,
1104
+ id: "update_blueprint",
1105
+ label: "Update blueprint"
1106
+ }
1107
+ ];
1108
+ }
1109
+ if (stepId === "changes_committed") {
1110
+ return artifacts.acceptedChangesCommit?.commit
1111
+ ? []
1112
+ : [
1113
+ {
1114
+ command: `${commandBase} commit_changes`,
1115
+ id: "commit_changes",
1116
+ label: "Commit changes"
1117
+ }
1118
+ ];
1119
+ }
1120
+ if (stepId === "final_report_created") {
1121
+ return artifacts.pullRequestText
1122
+ ? []
1123
+ : [
1124
+ {
1125
+ command: `${commandBase} create_pull_request_file`,
1126
+ id: "create_pull_request_file",
1127
+ label: "Create PR file"
1128
+ }
1129
+ ];
1130
+ }
1131
+ if (stepId === "pr_created") {
1132
+ return artifacts.prUrl
1133
+ ? []
1134
+ : [
1135
+ {
1136
+ command: `${commandBase} create_pr_on_gh`,
1137
+ id: "create_pr_on_gh",
1138
+ label: "Create PR on GH"
1139
+ }
1140
+ ];
1141
+ }
1142
+ if (stepId === "pr_merge_prepared") {
1143
+ return artifacts.prOutcome?.outcome === "merged" || !artifacts.prUrl
1144
+ ? []
1145
+ : [
1146
+ {
1147
+ command: `${commandBase} prepare_for_merge`,
1148
+ id: "prepare_for_merge",
1149
+ label: "Prepare for merge"
1150
+ },
1151
+ {
1152
+ command: `${commandBase} merge_pr`,
1153
+ id: "merge_pr",
1154
+ label: "Merge"
1155
+ }
1156
+ ];
1157
+ }
1158
+ if (stepId === "main_checkout_synced") {
1159
+ return artifacts.prUrl && artifacts.prOutcome?.outcome === "merged" && !artifacts.mainCheckoutSync?.status
1160
+ ? [
1161
+ {
1162
+ command: `${commandBase} sync_main_checkout`,
1163
+ id: "sync_main_checkout",
1164
+ label: "Sync main checkout"
1165
+ }
1166
+ ]
1167
+ : [];
1168
+ }
1169
+ if (stepId === "session_finished") {
1170
+ return [
1171
+ {
1172
+ command: `${commandBase} finish_session`,
1173
+ id: "finish_session",
1174
+ label: "Finish"
1175
+ }
1176
+ ];
1177
+ }
1178
+ return [];
1179
+ }
1180
+
977
1181
  async function buildSessionResponse(paths, {
978
1182
  codex = undefined,
979
1183
  ok = true,
980
1184
  errors = [],
981
1185
  preconditions = [],
982
1186
  prompt = undefined,
983
- status = undefined
1187
+ status = undefined,
1188
+ warnings = []
984
1189
  } = {}) {
985
1190
  const responsePaths = paths.sessionId ? await pathsForExistingSession(paths) : paths;
986
1191
  const artifacts = responsePaths.sessionRoot ? await readSessionArtifacts(responsePaths) : {};
@@ -1012,26 +1217,28 @@ async function buildSessionResponse(paths, {
1012
1217
  codex: null,
1013
1218
  prompt: "",
1014
1219
  nextCommand: "",
1220
+ issueDefinitionRequested: artifacts.issueDefinitionRequested === true,
1221
+ issueFileRequested: artifacts.issueFileRequested === true,
1222
+ issueNumber: artifacts.issueNumber || "",
1015
1223
  issueUrl: artifacts.issueUrl || "",
1016
1224
  issueTitle: artifacts.issueTitle || "",
1017
1225
  issueText: artifacts.issueText || "",
1018
- issueMetadata: cloneContractValue(artifacts.issueMetadata || null),
1226
+ pullRequestFileRequested: artifacts.pullRequestFileRequested === true,
1227
+ pullRequestPath: artifacts.pullRequestPath || "",
1228
+ pullRequestText: artifacts.pullRequestText || "",
1019
1229
  githubComments: cloneContractValue(artifacts.githubComments || {}),
1020
- issueCategory: artifacts.issueCategory || "",
1021
- uiImpact: artifacts.uiImpact || "",
1022
- agentDecisionsPath: artifacts.agentDecisions ? path.join(responsePaths.sessionRoot, "agent_decisions.md") : "",
1023
- agentDecisionsLatest: artifacts.agentDecisionsLatest || "",
1230
+ makePlanRequested: artifacts.makePlanRequested === true,
1231
+ blueprintUpdateRequested: artifacts.blueprintUpdateRequested === true,
1232
+ executePlanRequested: artifacts.executePlanRequested === true,
1024
1233
  planExecution: cloneContractValue(artifacts.planExecution || null),
1025
- planText: artifacts.planText || "",
1026
- issueDetails: artifacts.issueDetails || "",
1027
- issueDetailsPath: artifacts.issueDetails ? path.join(responsePaths.sessionRoot, "issue_details.md") : "",
1028
- finalReportPath: artifacts.finalReportText ? path.join(responsePaths.sessionRoot, "final_report.md") : "",
1234
+ finalReportPath: artifacts.finalReportText ? path.join(responsePaths.sessionRoot, "final_report") : "",
1029
1235
  finalReportText: artifacts.finalReportText || "",
1030
1236
  helperMapPath: artifacts.helperMapPath || "",
1031
1237
  helperMapExists: artifacts.helperMapExists === true,
1032
1238
  prUrl: artifacts.prUrl || "",
1033
1239
  prOutcome: cloneContractValue(artifacts.prOutcome || null),
1034
1240
  mainCheckoutSync: cloneContractValue(artifacts.mainCheckoutSync || null),
1241
+ acceptedChangesCommit: cloneContractValue(artifacts.acceptedChangesCommit || null),
1035
1242
  preconditions,
1036
1243
  errors: [
1037
1244
  createError({
@@ -1039,6 +1246,7 @@ async function buildSessionResponse(paths, {
1039
1246
  message: `Session ${paths.sessionId || ""} uses workflow version ${artifacts.workflowVersion || "missing"}, but this JSKIT runtime expects ${SESSION_WORKFLOW_VERSION}.`
1040
1247
  })
1041
1248
  ],
1249
+ warnings: [],
1042
1250
  archive: responsePaths.archive || "active",
1043
1251
  sessionRoot: responsePaths.sessionRoot || "",
1044
1252
  worktree: paths.worktree || "",
@@ -1052,6 +1260,7 @@ async function buildSessionResponse(paths, {
1052
1260
  const responsePrompt = typeof prompt === "string"
1053
1261
  ? prompt
1054
1262
  : stepCanExposeStoredPrompt(currentStep) ? artifacts.prompt || "" : "";
1263
+ const responseWarnings = mergeWarnings(artifacts.warnings || [], warnings);
1055
1264
 
1056
1265
  return {
1057
1266
  ok: ok === true,
@@ -1076,31 +1285,35 @@ async function buildSessionResponse(paths, {
1076
1285
  commandLogPath: artifacts.commandLogPath || "",
1077
1286
  stepDefinitions: buildStepDefinitions(),
1078
1287
  currentStepAction: buildCurrentStepAction(currentStep, artifacts),
1288
+ actionCommands: buildStepActionCommands(paths.sessionId || "", currentStep, artifacts),
1079
1289
  codex: codex === undefined ? await buildCodexHandoff(currentStep, artifacts) : await publicCodexContract(codex),
1080
1290
  prompt: responsePrompt,
1081
- nextCommand: buildNextCommand(paths.sessionId || "", currentStep),
1291
+ nextCommand: buildNextCommand(paths.sessionId || "", currentStep, artifacts),
1292
+ issueDefinitionRequested: artifacts.issueDefinitionRequested === true,
1293
+ issueFileRequested: artifacts.issueFileRequested === true,
1294
+ issueNumber: artifacts.issueNumber || "",
1082
1295
  issueUrl: artifacts.issueUrl || "",
1083
1296
  issueTitle: artifacts.issueTitle || "",
1084
1297
  issueText: artifacts.issueText || "",
1085
- issueMetadata: cloneContractValue(artifacts.issueMetadata || null),
1298
+ pullRequestFileRequested: artifacts.pullRequestFileRequested === true,
1299
+ pullRequestPath: artifacts.pullRequestPath || "",
1300
+ pullRequestText: artifacts.pullRequestText || "",
1086
1301
  githubComments: cloneContractValue(artifacts.githubComments || {}),
1087
- issueCategory: artifacts.issueCategory || "",
1088
- uiImpact: artifacts.uiImpact || "",
1089
- agentDecisionsPath: artifacts.agentDecisions ? path.join(responsePaths.sessionRoot, "agent_decisions.md") : "",
1090
- agentDecisionsLatest: artifacts.agentDecisionsLatest || "",
1302
+ makePlanRequested: artifacts.makePlanRequested === true,
1303
+ blueprintUpdateRequested: artifacts.blueprintUpdateRequested === true,
1304
+ executePlanRequested: artifacts.executePlanRequested === true,
1091
1305
  planExecution: cloneContractValue(artifacts.planExecution || null),
1092
- planText: artifacts.planText || "",
1093
- issueDetails: artifacts.issueDetails || "",
1094
- issueDetailsPath: artifacts.issueDetails ? path.join(responsePaths.sessionRoot, "issue_details.md") : "",
1095
- finalReportPath: artifacts.finalReportText ? path.join(responsePaths.sessionRoot, "final_report.md") : "",
1306
+ finalReportPath: artifacts.finalReportText ? path.join(responsePaths.sessionRoot, "final_report") : "",
1096
1307
  finalReportText: artifacts.finalReportText || "",
1097
1308
  helperMapPath: artifacts.helperMapPath || "",
1098
1309
  helperMapExists: artifacts.helperMapExists === true,
1099
1310
  prUrl: artifacts.prUrl || "",
1100
1311
  prOutcome: cloneContractValue(artifacts.prOutcome || null),
1101
1312
  mainCheckoutSync: cloneContractValue(artifacts.mainCheckoutSync || null),
1313
+ acceptedChangesCommit: cloneContractValue(artifacts.acceptedChangesCommit || null),
1102
1314
  preconditions,
1103
1315
  errors,
1316
+ warnings: responseWarnings,
1104
1317
  archive: responsePaths.archive || (resolvedStatus === SESSION_STATUS.FINISHED ? "completed" : resolvedStatus === SESSION_STATUS.ABANDONED ? "abandoned" : "active"),
1105
1318
  sessionRoot: responsePaths.sessionRoot || "",
1106
1319
  worktree: paths.worktree || "",
@@ -1160,16 +1373,11 @@ function buildSessionErrorResponse({
1160
1373
  nextCommand: "",
1161
1374
  issueTitle: "",
1162
1375
  issueText: "",
1163
- issueMetadata: null,
1376
+ pullRequestFileRequested: false,
1377
+ pullRequestPath: "",
1378
+ pullRequestText: "",
1164
1379
  githubComments: {},
1165
- issueCategory: "",
1166
- uiImpact: "",
1167
- agentDecisionsPath: "",
1168
- agentDecisionsLatest: "",
1169
1380
  planExecution: null,
1170
- planText: "",
1171
- issueDetails: "",
1172
- issueDetailsPath: "",
1173
1381
  finalReportPath: "",
1174
1382
  finalReportText: "",
1175
1383
  helperMapPath: "",
@@ -1179,6 +1387,7 @@ function buildSessionErrorResponse({
1179
1387
  prOutcome: null,
1180
1388
  preconditions,
1181
1389
  errors: errorList,
1390
+ warnings: [],
1182
1391
  archive: "",
1183
1392
  sessionRoot: "",
1184
1393
  worktree: "",
@@ -1198,27 +1407,28 @@ async function markCurrentStep(paths, stepId) {
1198
1407
  await writeTextFile(path.join(paths.sessionRoot, "current_step"), stepId);
1199
1408
  }
1200
1409
 
1201
- async function writeReceipt(paths, stepId, message) {
1202
- const activeCycle = await readActiveCycle(paths);
1203
- const root = isCycleStepId(stepId) ? cycleStepsRoot(paths, activeCycle) : path.join(paths.sessionRoot, "steps");
1410
+ async function writeStepRecord(paths, stepId, message) {
1411
+ const root = isCycleStepId(stepId)
1412
+ ? cycleStepsRoot(paths, await readActiveCycle(paths))
1413
+ : path.join(paths.sessionRoot, "steps");
1204
1414
  await mkdir(root, { recursive: true });
1205
1415
  await writeTextFile(
1206
1416
  path.join(root, stepId),
1207
- `${timestampForReceipt()}\n${normalizeText(message) || STEP_LABEL_BY_ID[stepId] || stepId}`
1417
+ `${timestampForStepRecord()}\n${normalizeText(message) || STEP_LABEL_BY_ID[stepId] || stepId}`
1208
1418
  );
1209
1419
  const completedSteps = await readCompletedSteps(paths);
1210
1420
  await markCurrentStep(paths, resolveNextStep(completedSteps));
1211
1421
  }
1212
1422
 
1213
- async function writeCycleReceipt(paths, receiptName, message, {
1423
+ async function writeCycleStepRecord(paths, recordName, message, {
1214
1424
  cycle = ""
1215
1425
  } = {}) {
1216
1426
  const activeCycle = normalizeCycleNumber(cycle || await readActiveCycle(paths));
1217
1427
  const root = cycleStepsRoot(paths, activeCycle);
1218
1428
  await mkdir(root, { recursive: true });
1219
1429
  await writeTextFile(
1220
- path.join(root, normalizeText(receiptName)),
1221
- `${timestampForReceipt()}\n${normalizeText(message) || normalizeText(receiptName)}`
1430
+ path.join(root, normalizeText(recordName)),
1431
+ `${timestampForStepRecord()}\n${normalizeText(message) || normalizeText(recordName)}`
1222
1432
  );
1223
1433
  }
1224
1434
 
@@ -1261,12 +1471,11 @@ export {
1261
1471
  markStatus,
1262
1472
  normalizeReviewPassNumber,
1263
1473
  readActiveCycle,
1264
- readReceiptSteps,
1474
+ readStepRecords,
1265
1475
  readReviewPasses,
1266
1476
  readSessionArtifacts,
1267
1477
  reviewPassDirectoryName,
1268
1478
  reviewPassRoot,
1269
- writeActiveCycle,
1270
- writeCycleReceipt,
1271
- writeReceipt
1479
+ writeCycleStepRecord,
1480
+ writeStepRecord
1272
1481
  };