@jskit-ai/jskit-cli 0.2.96 → 0.2.97

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 (31) hide show
  1. package/package.json +4 -4
  2. package/src/server/appBlueprint.js +37 -14
  3. package/src/server/core/argParser.js +0 -12
  4. package/src/server/core/commandCatalog.js +2 -92
  5. package/src/server/core/createCommandHandlers.js +0 -3
  6. package/src/server/index.js +0 -1
  7. package/src/server/{sessionRuntime/prompts → prompts}/app_blueprint.md +1 -1
  8. package/src/server/commandHandlers/session.js +0 -471
  9. package/src/server/sessionRuntime/appReadiness.js +0 -55
  10. package/src/server/sessionRuntime/constants.js +0 -377
  11. package/src/server/sessionRuntime/io.js +0 -97
  12. package/src/server/sessionRuntime/paths.js +0 -163
  13. package/src/server/sessionRuntime/preconditions.js +0 -663
  14. package/src/server/sessionRuntime/promptRenderer.js +0 -41
  15. package/src/server/sessionRuntime/prompts/automated_checks_run.md +0 -28
  16. package/src/server/sessionRuntime/prompts/blueprint_updated.md +0 -29
  17. package/src/server/sessionRuntime/prompts/deep_ui_check_run.md +0 -40
  18. package/src/server/sessionRuntime/prompts/final_comment.md +0 -10
  19. package/src/server/sessionRuntime/prompts/final_report_created.md +0 -44
  20. package/src/server/sessionRuntime/prompts/issue_created.md +0 -26
  21. package/src/server/sessionRuntime/prompts/issue_prompt_rendered.md +0 -1
  22. package/src/server/sessionRuntime/prompts/make_plan.md +0 -57
  23. package/src/server/sessionRuntime/prompts/plan_executed.md +0 -39
  24. package/src/server/sessionRuntime/prompts/pr_failure.md +0 -28
  25. package/src/server/sessionRuntime/prompts/pr_merge_prepared.md +0 -22
  26. package/src/server/sessionRuntime/prompts/review_changes_accepted_resolve.md +0 -12
  27. package/src/server/sessionRuntime/prompts/review_prompt_rendered.md +0 -61
  28. package/src/server/sessionRuntime/prompts/user_check_completed.md +0 -17
  29. package/src/server/sessionRuntime/responses.js +0 -1481
  30. package/src/server/sessionRuntime/worktrees.js +0 -31
  31. package/src/server/sessionRuntime.js +0 -3659
@@ -1,1481 +0,0 @@
1
- import { mkdir, readdir } from "node:fs/promises";
2
- import path from "node:path";
3
- import {
4
- CYCLE_STEP_IDS,
5
- DEPENDENCIES_INSTALL_RESULT_FILE,
6
- ISSUE_FILE_CODEX_HANDOFF,
7
- ISSUE_DEFINITION_CODEX_HANDOFF,
8
- JSKIT_CLI_SHELL_COMMAND,
9
- REVIEW_EXECUTION_CODEX_HANDOFF,
10
- REVIEW_PASS_LIMIT,
11
- SESSION_WORKFLOW_VERSION,
12
- SESSION_STATUS,
13
- STEP_DEFINITION_BY_ID,
14
- STEP_DEFINITIONS,
15
- STEP_IDS,
16
- STEP_LABEL_BY_ID
17
- } from "./constants.js";
18
- import {
19
- fileExists,
20
- normalizeText,
21
- readTextIfExists,
22
- readTrimmedFile,
23
- runGitInWorktree,
24
- timestampForStepRecord,
25
- writeTextFile
26
- } from "./io.js";
27
- import {
28
- pathsForExistingSession
29
- } from "./paths.js";
30
- import {
31
- hasWorktree
32
- } from "./worktrees.js";
33
- import {
34
- inspectReadyJskitAppRoot
35
- } from "./appReadiness.js";
36
-
37
- function createError({
38
- code,
39
- message,
40
- repairCommand = ""
41
- }) {
42
- return Object.freeze({
43
- code: normalizeText(code),
44
- message: normalizeText(message),
45
- repairCommand: normalizeText(repairCommand)
46
- });
47
- }
48
-
49
- function createPrecondition({
50
- id,
51
- ok,
52
- message
53
- }) {
54
- return Object.freeze({
55
- id: normalizeText(id),
56
- ok: ok === true,
57
- message: normalizeText(message)
58
- });
59
- }
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
-
79
- function normalizeStepId(stepId) {
80
- return normalizeText(stepId);
81
- }
82
-
83
- function stepIndex(stepId) {
84
- return STEP_IDS.indexOf(normalizeStepId(stepId));
85
- }
86
-
87
- function normalizeKnownStepIds(stepIds = []) {
88
- return Array.from(
89
- new Set(
90
- stepIds
91
- .map((stepId) => normalizeText(stepId))
92
- .filter((stepId) => STEP_IDS.includes(stepId))
93
- )
94
- ).sort((left, right) => STEP_IDS.indexOf(left) - STEP_IDS.indexOf(right));
95
- }
96
-
97
- function stepCanExposeStoredPrompt(stepId) {
98
- const normalizedStepId = normalizeStepId(stepId);
99
- const step = STEP_DEFINITION_BY_ID[normalizedStepId];
100
- return Boolean(
101
- normalizedStepId === "review_changes_accepted" ||
102
- step?.codex ||
103
- step?.kind === "codex_prompt" ||
104
- step?.kind === "human_input"
105
- );
106
- }
107
-
108
- const DEFAULT_ACTIVE_CYCLE = "001";
109
- const DEFAULT_REVIEW_PASS = "001";
110
-
111
- function normalizeCycleNumber(value = "") {
112
- const normalized = normalizeText(value).replace(/^cycle_/u, "");
113
- if (!/^\d+$/u.test(normalized)) {
114
- return DEFAULT_ACTIVE_CYCLE;
115
- }
116
- return String(Number.parseInt(normalized, 10)).padStart(3, "0");
117
- }
118
-
119
- function cycleDirectoryName(cycle = DEFAULT_ACTIVE_CYCLE) {
120
- return `cycle_${normalizeCycleNumber(cycle)}`;
121
- }
122
-
123
- function isCycleStepId(stepId = "") {
124
- return CYCLE_STEP_IDS.includes(normalizeStepId(stepId));
125
- }
126
-
127
- async function readWorkflowVersion(paths) {
128
- return readTrimmedFile(path.join(paths.sessionRoot, "workflow_version"));
129
- }
130
-
131
- async function readActiveCycle(paths) {
132
- const cycle = await readTrimmedFile(path.join(paths.sessionRoot, "active_cycle"));
133
- return normalizeCycleNumber(cycle || DEFAULT_ACTIVE_CYCLE);
134
- }
135
-
136
- function cycleStepsRoot(paths, cycle) {
137
- return path.join(paths.sessionRoot, "steps", cycleDirectoryName(cycle));
138
- }
139
-
140
- function cycleRoot(paths, cycle) {
141
- return path.join(paths.sessionRoot, "cycles", cycleDirectoryName(cycle));
142
- }
143
-
144
- function normalizeReviewPassNumber(value = "") {
145
- const normalized = normalizeText(value).replace(/^pass_/u, "");
146
- if (!/^\d+$/u.test(normalized)) {
147
- return DEFAULT_REVIEW_PASS;
148
- }
149
- return String(Number.parseInt(normalized, 10)).padStart(3, "0");
150
- }
151
-
152
- function reviewPassDirectoryName(pass = DEFAULT_REVIEW_PASS) {
153
- return `pass_${normalizeReviewPassNumber(pass)}`;
154
- }
155
-
156
- function reviewPassRoot(paths, pass) {
157
- return path.join(paths.sessionRoot, "review_passes", reviewPassDirectoryName(pass));
158
- }
159
-
160
- async function parseJsonFileIfExists(filePath) {
161
- const source = await readTextIfExists(filePath);
162
- if (!source) {
163
- return null;
164
- }
165
- try {
166
- const parsed = JSON.parse(source);
167
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
168
- } catch {
169
- return null;
170
- }
171
- }
172
-
173
- async function readReviewPassNumbers(paths) {
174
- try {
175
- const entries = await readdir(path.join(paths.sessionRoot, "review_passes"), { withFileTypes: true });
176
- return entries
177
- .filter((entry) => entry.isDirectory() && /^pass_\d+$/u.test(entry.name))
178
- .map((entry) => normalizeReviewPassNumber(entry.name))
179
- .sort((left, right) => Number.parseInt(left, 10) - Number.parseInt(right, 10));
180
- } catch {
181
- return [];
182
- }
183
- }
184
-
185
- async function readReviewPassInfo(paths, pass) {
186
- const normalizedPass = normalizeReviewPassNumber(pass);
187
- const root = reviewPassRoot(paths, normalizedPass);
188
- const [prompt, accepted] = await Promise.all([
189
- parseJsonFileIfExists(path.join(root, "prompt.json")),
190
- parseJsonFileIfExists(path.join(root, "accepted.json"))
191
- ]);
192
- const status = accepted?.status || prompt?.status || "unknown";
193
- const changedFiles = Array.isArray(accepted?.changedFiles) ? accepted.changedFiles : [];
194
- return {
195
- pass: normalizedPass,
196
- label: reviewPassDirectoryName(normalizedPass),
197
- status,
198
- promptPath: prompt?.promptPath || path.join(root, "review_prompt_rendered"),
199
- acceptedAt: accepted?.acceptedAt || "",
200
- changedFiles,
201
- commit: "",
202
- committedAt: "",
203
- findingsRemaining: accepted?.findingsRemaining === true,
204
- maxPasses: REVIEW_PASS_LIMIT
205
- };
206
- }
207
-
208
- async function readReviewPasses(paths) {
209
- const passes = await readReviewPassNumbers(paths);
210
- return Promise.all(passes.map((pass) => readReviewPassInfo(paths, pass)));
211
- }
212
-
213
- const REVIEW_STEP_IDS = Object.freeze([
214
- "review_prompt_rendered",
215
- "review_changes_accepted"
216
- ]);
217
-
218
- function latestReviewPass(artifacts = {}) {
219
- const passes = Array.isArray(artifacts.reviewPasses) ? artifacts.reviewPasses : [];
220
- return passes.at(-1) || null;
221
- }
222
-
223
- function latestReviewPassIsPrompted(artifacts = {}) {
224
- return latestReviewPass(artifacts)?.status === "prompted";
225
- }
226
-
227
- async function readPromptFromAbsolutePath(filePath = "") {
228
- return filePath ? readTextIfExists(filePath) : "";
229
- }
230
-
231
- async function readReviewPromptForStep(paths, artifacts = {}) {
232
- const latestPass = latestReviewPass(artifacts);
233
- if (latestPass?.status === "prompted") {
234
- const prompt = await readPromptFromAbsolutePath(latestPass.promptPath);
235
- if (prompt) {
236
- return prompt;
237
- }
238
- }
239
- return "";
240
- }
241
-
242
- async function readPromptForStep(paths, stepId, artifacts = {}) {
243
- if (!stepCanExposeStoredPrompt(stepId)) {
244
- return "";
245
- }
246
- if (REVIEW_STEP_IDS.includes(normalizeStepId(stepId))) {
247
- return readReviewPromptForStep(paths, artifacts);
248
- }
249
- return "";
250
- }
251
-
252
- async function readStepFileNames(stepsRoot) {
253
- try {
254
- const entries = await readdir(stepsRoot, { withFileTypes: true });
255
- return entries
256
- .filter((entry) => entry.isFile())
257
- .map((entry) => entry.name);
258
- } catch {
259
- return [];
260
- }
261
- }
262
-
263
- async function readCompletedSteps(paths) {
264
- const stepsRoot = path.join(paths.sessionRoot, "steps");
265
- const globalStepIds = normalizeKnownStepIds(
266
- (await readStepFileNames(stepsRoot)).filter((stepId) => !isCycleStepId(stepId))
267
- );
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
- }
277
- return applyReviewPassCompletionOverlay(paths, normalizeKnownStepIds([...globalStepIds, ...cycleStepIds]));
278
- }
279
-
280
- async function applyReviewPassCompletionOverlay(paths, completedSteps = []) {
281
- const completed = new Set(completedSteps);
282
- if (!REVIEW_STEP_IDS.some((stepId) => completed.has(stepId))) {
283
- return normalizeKnownStepIds([...completed]);
284
- }
285
- const reviewPasses = await readReviewPasses(paths);
286
- const latestPass = reviewPasses.at(-1);
287
- if (!latestPass) {
288
- return normalizeKnownStepIds([...completed]);
289
- }
290
- const latestPassAccepted = latestPass.status === "accepted" || latestPass.status === "no_changes";
291
- const anotherPassRequired = latestPassAccepted && latestPass.findingsRemaining === true;
292
- if (anotherPassRequired) {
293
- REVIEW_STEP_IDS.forEach((stepId) => completed.delete(stepId));
294
- return normalizeKnownStepIds([...completed]);
295
- }
296
- if (latestPass.status === "accepted" || latestPass.status === "no_changes") {
297
- REVIEW_STEP_IDS.forEach((stepId) => completed.add(stepId));
298
- }
299
- return normalizeKnownStepIds([...completed]);
300
- }
301
-
302
- async function readCycleInfo(paths, cycle) {
303
- const normalizedCycle = normalizeCycleNumber(cycle);
304
- const root = cycleRoot(paths, normalizedCycle);
305
- const userCheckPassed = await readTextIfExists(path.join(cycleStepsRoot(paths, normalizedCycle), "user_check_completed"));
306
- const userCheckFailed = await readTextIfExists(path.join(cycleStepsRoot(paths, normalizedCycle), "user_check_failed"));
307
- const reworkRequestPath = path.join(root, "rework_request");
308
- const reworkRequest = await readTextIfExists(reworkRequestPath);
309
- return {
310
- cycle: normalizedCycle,
311
- label: cycleDirectoryName(normalizedCycle),
312
- reworkRequest: reworkRequest.trim(),
313
- reworkRequestPath: reworkRequest ? reworkRequestPath : "",
314
- status: userCheckPassed ? "passed" : userCheckFailed ? "failed" : "active",
315
- userCheckResult: userCheckPassed ? "passed" : userCheckFailed ? "failed" : "",
316
- userCheckRecord: (userCheckPassed || userCheckFailed).trim()
317
- };
318
- }
319
-
320
- async function readCycles(paths, activeCycle) {
321
- const cycles = new Set([normalizeCycleNumber(activeCycle || DEFAULT_ACTIVE_CYCLE)]);
322
- for (const root of [path.join(paths.sessionRoot, "steps"), path.join(paths.sessionRoot, "cycles")]) {
323
- try {
324
- const entries = await readdir(root, { withFileTypes: true });
325
- for (const entry of entries) {
326
- if (entry.isDirectory() && /^cycle_\d+$/u.test(entry.name)) {
327
- cycles.add(normalizeCycleNumber(entry.name));
328
- }
329
- }
330
- } catch {
331
- // No cycle directory exists until a session enters a repeatable work cycle.
332
- }
333
- }
334
- return Promise.all([...cycles]
335
- .sort((left, right) => Number.parseInt(left, 10) - Number.parseInt(right, 10))
336
- .map((cycle) => readCycleInfo(paths, cycle)));
337
- }
338
-
339
- async function readStructuredChecks(paths) {
340
- const checksRoot = path.join(paths.sessionRoot, "checks");
341
- try {
342
- const entries = await readdir(checksRoot, { withFileTypes: true });
343
- const checks = [];
344
- for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name))) {
345
- const source = await readTextIfExists(path.join(checksRoot, entry.name));
346
- try {
347
- const parsed = JSON.parse(source);
348
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
349
- checks.push(parsed);
350
- }
351
- } catch {
352
- // Ignore malformed check metadata; the raw log remains on disk.
353
- }
354
- }
355
- return checks;
356
- } catch {
357
- return [];
358
- }
359
- }
360
-
361
- async function readStructuredUiChecks(paths) {
362
- const uiChecksRoot = path.join(paths.sessionRoot, "ui_checks");
363
- try {
364
- const entries = await readdir(uiChecksRoot, { 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(uiChecksRoot, 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 UI check metadata; the raw prompt/log remains on disk.
375
- }
376
- }
377
- return checks;
378
- } catch {
379
- return [];
380
- }
381
- }
382
-
383
- async function readWorktreeStatus(paths, worktreeReady) {
384
- if (!worktreeReady) {
385
- return {
386
- changedFiles: [],
387
- dirty: false,
388
- ok: true,
389
- status: "missing",
390
- statusText: ""
391
- };
392
- }
393
- const result = await runGitInWorktree(paths.worktree, ["status", "--porcelain=v1"], {
394
- timeout: 15000
395
- });
396
- if (!result.ok) {
397
- return {
398
- changedFiles: [],
399
- dirty: false,
400
- ok: false,
401
- status: "unknown",
402
- statusText: result.output
403
- };
404
- }
405
- const changedFiles = result.stdout
406
- .split(/\r?\n/u)
407
- .map((line) => line.trim())
408
- .filter(Boolean);
409
- return {
410
- changedFiles,
411
- dirty: changedFiles.length > 0,
412
- ok: true,
413
- status: changedFiles.length > 0 ? "dirty" : "clean",
414
- statusText: result.stdout
415
- };
416
- }
417
-
418
- async function readStepRecords(paths) {
419
- const stepsRoot = path.join(paths.sessionRoot, "steps");
420
- try {
421
- const entries = await readdir(stepsRoot, { withFileTypes: true });
422
- const knownStepRows = new Map();
423
- const unknownStepRows = [];
424
- entries
425
- .filter((entry) => entry.isFile())
426
- .map((entry) => entry.name)
427
- .forEach((recordName) => {
428
- const stepId = normalizeStepId(recordName);
429
- if (STEP_IDS.includes(stepId)) {
430
- if (!knownStepRows.has(stepId) || recordName === stepId) {
431
- knownStepRows.set(stepId, {
432
- recordName,
433
- stepId
434
- });
435
- }
436
- return;
437
- }
438
- unknownStepRows.push({
439
- recordName,
440
- stepId
441
- });
442
- });
443
-
444
- const stepRows = [...knownStepRows.values(), ...unknownStepRows]
445
- .sort((left, right) => {
446
- const leftIndex = stepIndex(left.stepId);
447
- const rightIndex = stepIndex(right.stepId);
448
- if (leftIndex >= 0 && rightIndex >= 0) {
449
- return leftIndex - rightIndex;
450
- }
451
- if (leftIndex >= 0) {
452
- return -1;
453
- }
454
- if (rightIndex >= 0) {
455
- return 1;
456
- }
457
- return left.stepId.localeCompare(right.stepId);
458
- });
459
-
460
- const globalRecords = await Promise.all(stepRows.map(async ({ recordName, stepId }) => ({
461
- cycle: "",
462
- details: (await readTextIfExists(path.join(stepsRoot, recordName))).trim(),
463
- label: STEP_LABEL_BY_ID[stepId] || stepId,
464
- stepId
465
- })));
466
-
467
- const cycleRecords = [];
468
- const cycleDirectories = entries
469
- .filter((entry) => entry.isDirectory() && /^cycle_\d+$/u.test(entry.name))
470
- .map((entry) => entry.name)
471
- .sort();
472
- for (const cycleDirectory of cycleDirectories) {
473
- const cycle = normalizeCycleNumber(cycleDirectory);
474
- const cycleRootPath = path.join(stepsRoot, cycleDirectory);
475
- const cycleStepIds = await readStepFileNames(cycleRootPath);
476
- for (const recordName of cycleStepIds) {
477
- const stepId = normalizeStepId(recordName);
478
- cycleRecords.push({
479
- cycle,
480
- details: (await readTextIfExists(path.join(cycleRootPath, recordName))).trim(),
481
- label: STEP_LABEL_BY_ID[stepId] || stepId,
482
- stepId
483
- });
484
- }
485
- }
486
-
487
- return [...globalRecords, ...cycleRecords];
488
- } catch {
489
- return [];
490
- }
491
- }
492
-
493
- function resolveNextStep(completedSteps = []) {
494
- const completed = new Set(completedSteps);
495
- return STEP_IDS.find((stepId) => !completed.has(stepId)) || "";
496
- }
497
-
498
- function cloneContractValue(value) {
499
- if (!value || typeof value !== "object") {
500
- return value;
501
- }
502
- if (Array.isArray(value)) {
503
- return value.map((entry) => cloneContractValue(entry));
504
- }
505
- return Object.fromEntries(
506
- Object.entries(value).map(([key, entry]) => [key, cloneContractValue(entry)])
507
- );
508
- }
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
-
547
- async function publicCodexContract(codex = null) {
548
- if (!codex || typeof codex !== "object" || Array.isArray(codex)) {
549
- return null;
550
- }
551
- return cloneContractValue(codex);
552
- }
553
-
554
- function stepRepeatabilityContract(stepId) {
555
- return {
556
- repeatable: false,
557
- repeatableGroupId: "",
558
- repeatableGroupLabel: "",
559
- repeatableLabel: ""
560
- };
561
- }
562
-
563
- function publicStepDefinition(step, index) {
564
- return {
565
- automation: cloneContractValue(step.automation || { mode: "manual" }),
566
- ...(step.codex ? { codex: cloneContractValue(step.codex) } : {}),
567
- description: step.description,
568
- displayGroupId: step.displayGroupId || "",
569
- displayGroupLabel: step.displayGroupLabel || "",
570
- id: step.id,
571
- index,
572
- input: cloneContractValue(step.input),
573
- kind: step.kind,
574
- label: step.label,
575
- ...stepRepeatabilityContract(step.id),
576
- requiresExplicitRun: step.requiresExplicitRun === true,
577
- submitOptions: cloneContractValue(step.submitOptions || {}),
578
- utilityActions: cloneContractValue(step.utilityActions || [])
579
- };
580
- }
581
-
582
- function buildStepDefinitions() {
583
- return STEP_DEFINITIONS.map((step, index) => publicStepDefinition(step, index));
584
- }
585
-
586
- function stepIsRetryableWhenBlocked(stepId) {
587
- return [
588
- "automated_checks_run",
589
- "deep_ui_check_run",
590
- "main_checkout_synced"
591
- ].includes(normalizeStepId(stepId));
592
- }
593
-
594
- function uiCheckPromptedForStep(artifacts = {}, stepId = "") {
595
- const normalizedStepId = normalizeStepId(stepId);
596
- return (artifacts.uiChecks || []).some((entry) => {
597
- return normalizeStepId(entry?.stepId || "") === normalizedStepId &&
598
- normalizeText(entry?.status || "") === "prompted";
599
- });
600
- }
601
-
602
- function skipReasonForStep(stepId, artifacts = {}) {
603
- void stepId;
604
- void artifacts;
605
- return "";
606
- }
607
-
608
- function buildCurrentStepAction(stepId, artifacts = {}) {
609
- const step = STEP_DEFINITION_BY_ID[stepId];
610
- if (!step) {
611
- return null;
612
- }
613
- const planExecutionPrompted = artifacts.planExecution?.prompted === true;
614
- const planExecutionSubmitted = artifacts.planExecution?.submitted === true;
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";
620
- const deepUiCheckPrompted = step.id === "deep_ui_check_run" && uiCheckPromptedForStep(artifacts, "deep_ui_check_run");
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
- });
625
- const alternateActions = [];
626
- if (step.id === "review_changes_accepted") {
627
- alternateActions.push({
628
- id: "request_another_review_pass",
629
- helpText: "Run another explicit deslop prompt before continuing.",
630
- input: {
631
- type: "none"
632
- },
633
- label: "Run deslop",
634
- presentation: "secondary",
635
- submitOptions: {
636
- reviewFindingsRemaining: true
637
- },
638
- targetStep: "review_prompt_rendered"
639
- });
640
- }
641
- const dynamicButtonLabel = (() => {
642
- if (step.id === "issue_created" && !artifacts.issueText) {
643
- return "Create issue file";
644
- }
645
- if (step.id === "issue_submitted") {
646
- return "Create issue on GH";
647
- }
648
- if (step.id === "pr_created") {
649
- return "Create PR on GH";
650
- }
651
- if (step.id === "pr_merge_prepared") {
652
- return "Merge";
653
- }
654
- if (step.id === "main_checkout_synced" && artifacts.prOutcome?.outcome && artifacts.prOutcome.outcome !== "merged") {
655
- return "Sync main checkout";
656
- }
657
- return step.buttonLabel;
658
- })();
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
- }
678
- if (step.id === "plan_executed" && planExecutionPrompted && !planExecutionSubmitted) {
679
- return "Codex has the execution prompt. Review the result, then use Next when ready.";
680
- }
681
- if (step.id === "deep_ui_check_run" && deepUiCheckPrompted) {
682
- return "Codex has the run deep UI check prompt. Review the result, then use Next when ready.";
683
- }
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.";
686
- }
687
- if (step.id === "main_checkout_synced" && artifacts.prOutcome?.outcome && artifacts.prOutcome.outcome !== "merged") {
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.";
692
- }
693
- return step.description;
694
- })();
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
- }
710
- return step.utilityActions || [];
711
- })();
712
- return {
713
- alternateActions,
714
- buttonLabel: dynamicButtonLabel,
715
- description: dynamicDescription,
716
- displayGroupId: step.displayGroupId,
717
- displayGroupLabel: step.displayGroupLabel,
718
- index: STEP_IDS.indexOf(step.id),
719
- input: cloneContractValue(issueDefinitionPrompted || issueFilePromptAction || pullRequestFileAction ? { type: "none" } : step.input),
720
- kind: issueDefinitionPrompted || issueFilePromptAction || pullRequestFileAction ? "codex_prompt" : step.kind,
721
- label: dynamicButtonLabel,
722
- automation: cloneContractValue(issueDefinitionPrompted || issueFilePromptAction || pullRequestFileAction ? { mode: "codex_prompt" } : step.automation || { mode: "manual" }),
723
- ...stepRepeatabilityContract(step.id),
724
- requiredInput: cloneContractValue(issueDefinitionPrompted || issueFilePromptAction || pullRequestFileAction ? { type: "none" } : step.input),
725
- requiresExplicitRun: step.requiresExplicitRun === true,
726
- retryable: artifacts.status === SESSION_STATUS.BLOCKED && stepIsRetryableWhenBlocked(step.id),
727
- skipReason: skipReasonForStep(step.id, artifacts),
728
- stepId: step.id,
729
- submitOptions: cloneContractValue(step.submitOptions || {}),
730
- utilityActions: cloneContractValue(dynamicUtilityActions)
731
- };
732
- }
733
-
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
- }
741
- if (normalizeStepId(stepId) === "review_changes_accepted" && latestReviewPassIsPrompted(artifacts)) {
742
- return cloneContractValue(REVIEW_EXECUTION_CODEX_HANDOFF);
743
- }
744
- const step = STEP_DEFINITION_BY_ID[stepId];
745
- return step?.codex ? cloneContractValue(step.codex) : null;
746
- }
747
-
748
- async function buildCodexHandoff(stepId, artifacts = {}) {
749
- return publicCodexContract(rawCodexHandoff(stepId, artifacts));
750
- }
751
-
752
- async function readSessionArtifacts(paths) {
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");
756
- const [
757
- status,
758
- rawCurrentStep,
759
- issueUrl,
760
- issueNumber,
761
- prUrl,
762
- issueText,
763
- issueTitle,
764
- finalReportText,
765
- pullRequestText,
766
- githubCommentsText,
767
- codexThreadId,
768
- workflowVersion,
769
- baseBranch,
770
- baseCommit,
771
- planExecutionRecord,
772
- issueDefinitionRequested,
773
- issueFileRequested,
774
- pullRequestFileRequested,
775
- makePlanRequested,
776
- blueprintUpdateRequested,
777
- executePlanRequested,
778
- prOutcomeText,
779
- mainCheckoutSyncText,
780
- changesCommittedText
781
- ] = await Promise.all([
782
- readTrimmedFile(path.join(paths.sessionRoot, "status")),
783
- readTrimmedFile(path.join(paths.sessionRoot, "current_step")),
784
- readTrimmedFile(path.join(paths.sessionRoot, "issue_url")),
785
- readTrimmedFile(path.join(paths.sessionRoot, "metadata", "issue_number")),
786
- readTrimmedFile(path.join(paths.sessionRoot, "pr_url")),
787
- readTextIfExists(path.join(paths.sessionRoot, "issue.md")),
788
- readTrimmedFile(path.join(paths.sessionRoot, "issue_title")),
789
- readTextIfExists(path.join(paths.sessionRoot, "final_report")),
790
- readTextIfExists(path.join(paths.sessionRoot, "pull_request.md")),
791
- readTextIfExists(path.join(paths.sessionRoot, "github_comments.json")),
792
- readTrimmedFile(path.join(paths.sessionRoot, "codex_thread_id")),
793
- readWorkflowVersion(paths),
794
- readTrimmedFile(path.join(paths.sessionRoot, "base_branch")),
795
- readTrimmedFile(path.join(paths.sessionRoot, "base_commit")),
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")),
803
- readTextIfExists(path.join(paths.sessionRoot, "pr_outcome.json")),
804
- readTextIfExists(path.join(paths.sessionRoot, "main_checkout_sync.json")),
805
- readTextIfExists(path.join(paths.sessionRoot, "changes_committed.json"))
806
- ]);
807
- let githubComments = {};
808
- if (githubCommentsText) {
809
- try {
810
- const parsed = JSON.parse(githubCommentsText);
811
- githubComments = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
812
- } catch {
813
- githubComments = {};
814
- }
815
- }
816
- let prOutcome = null;
817
- if (prOutcomeText) {
818
- try {
819
- const parsed = JSON.parse(prOutcomeText);
820
- prOutcome = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
821
- } catch {
822
- prOutcome = null;
823
- }
824
- }
825
- let mainCheckoutSync = null;
826
- if (mainCheckoutSyncText) {
827
- try {
828
- const parsed = JSON.parse(mainCheckoutSyncText);
829
- mainCheckoutSync = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
830
- } catch {
831
- mainCheckoutSync = null;
832
- }
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
- : [];
846
- const cycles = await readCycles(paths, activeCycle);
847
- const checks = await readStructuredChecks(paths);
848
- const uiChecks = await readStructuredUiChecks(paths);
849
- const reviewPasses = await readReviewPasses(paths);
850
- const worktreeReady = await hasWorktree(paths);
851
- const worktreeStatus = await readWorktreeStatus(paths, worktreeReady);
852
- const commandLogPath = path.join(paths.sessionRoot, "command_log.jsonl");
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));
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("session_finished");
862
- const worktreeStepRecordInvalid = !worktreeReady &&
863
- completedSteps.includes("worktree_created") &&
864
- !worktreeRemovalCompleted &&
865
- status !== SESSION_STATUS.FINISHED &&
866
- status !== SESSION_STATUS.ABANDONED;
867
- if (worktreeStepRecordInvalid) {
868
- completedSteps = completedSteps.filter((stepId) => !["worktree_created", "dependencies_installed"].includes(stepId));
869
- }
870
- const nextStep = resolveNextStep(completedSteps);
871
- const currentStepIndex = stepIndex(currentStep);
872
- const nextStepIndex = stepIndex(nextStep);
873
- const effectiveCurrentStep = nextStep &&
874
- (completedSteps.includes(currentStep) || currentStepIndex < 0 || currentStepIndex > nextStepIndex)
875
- ? nextStep
876
- : currentStep || nextStep;
877
- const prompt = await readPromptForStep(paths, effectiveCurrentStep, { reviewPasses });
878
- const dependencyInstallDetails = dependencyInstallRecord.trim() || dependencyInstallResult.trim();
879
- const dependencyInstallReady = Boolean(dependencyInstallRecord.trim() || dependencyInstallResult.trim());
880
-
881
- return {
882
- codexThreadId,
883
- completedSteps,
884
- currentStep: effectiveCurrentStep,
885
- activeCycle,
886
- appReady,
887
- baseBranch,
888
- baseCommit,
889
- blueprintExists: await fileExists(blueprintPath),
890
- blueprintPath,
891
- cycles,
892
- checks,
893
- uiChecks,
894
- reviewPasses,
895
- currentReviewPass: reviewPasses.at(-1)?.pass || "",
896
- commandLogExists: await fileExists(commandLogPath),
897
- commandLogPath,
898
- dependencyInstall: {
899
- installed: Boolean(dependencyInstallRecord.trim()),
900
- ready: dependencyInstallReady,
901
- details: dependencyInstallDetails,
902
- status: dependencyInstallRecord.trim()
903
- ? "installed"
904
- : dependencyInstallResult.trim() ? "ready_to_advance"
905
- : worktreeReady ? "pending" : "waiting_for_worktree"
906
- },
907
- helperMapExists: await fileExists(helperMapPath),
908
- helperMapPath,
909
- githubComments,
910
- issueNumber: issueNumber || issueNumberFromUrl(issueUrl),
911
- issueTitle,
912
- issueText: issueText.trim(),
913
- issueUrl,
914
- nextStep,
915
- pullRequestPath: path.join(paths.sessionRoot, "pull_request.md"),
916
- pullRequestText: pullRequestText.trim(),
917
- prUrl,
918
- prOutcome,
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()),
927
- planExecution: {
928
- prompted: Boolean(executePlanRequested.trim()),
929
- promptPath: "",
930
- details: planExecutionRecord.trim(),
931
- submitted: Boolean(planExecutionRecord.trim())
932
- },
933
- finalReportText: finalReportText.trim(),
934
- prompt: prompt.trim(),
935
- status: status || SESSION_STATUS.PENDING,
936
- warnings,
937
- workflowVersion,
938
- worktreeReady,
939
- worktreeStatus
940
- };
941
- }
942
-
943
- function stepCanExposeNextCommand(stepId, artifacts = {}) {
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)) {
982
- return "";
983
- }
984
- const template = STEP_DEFINITION_BY_ID[stepId]?.nextCommandTemplate || `${JSKIT_CLI_SHELL_COMMAND} session {{session_id}} next`;
985
- return template.replaceAll("{{session_id}}", sessionId);
986
- }
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
-
1181
- async function buildSessionResponse(paths, {
1182
- codex = undefined,
1183
- ok = true,
1184
- errors = [],
1185
- preconditions = [],
1186
- prompt = undefined,
1187
- status = undefined,
1188
- warnings = []
1189
- } = {}) {
1190
- const responsePaths = paths.sessionId ? await pathsForExistingSession(paths) : paths;
1191
- const artifacts = responsePaths.sessionRoot ? await readSessionArtifacts(responsePaths) : {};
1192
- const resolvedStatus = status || artifacts.status || (ok ? SESSION_STATUS.PENDING : SESSION_STATUS.BLOCKED);
1193
- if (responsePaths.sessionRoot && await fileExists(responsePaths.sessionRoot) && artifacts.workflowVersion !== SESSION_WORKFLOW_VERSION) {
1194
- return {
1195
- ok: false,
1196
- sessionId: paths.sessionId || "",
1197
- status: SESSION_STATUS.BLOCKED,
1198
- currentStep: "",
1199
- completedSteps: artifacts.completedSteps || [],
1200
- workflowVersion: artifacts.workflowVersion || "",
1201
- baseBranch: artifacts.baseBranch || "",
1202
- baseCommit: artifacts.baseCommit || "",
1203
- blueprintPath: artifacts.blueprintPath || "",
1204
- blueprintExists: artifacts.blueprintExists === true,
1205
- activeCycle: artifacts.activeCycle || "",
1206
- appReady: cloneContractValue(artifacts.appReady || null),
1207
- cycles: cloneContractValue(artifacts.cycles || []),
1208
- checks: cloneContractValue(artifacts.checks || []),
1209
- dependencyInstall: cloneContractValue(artifacts.dependencyInstall || null),
1210
- uiChecks: cloneContractValue(artifacts.uiChecks || []),
1211
- reviewPasses: cloneContractValue(artifacts.reviewPasses || []),
1212
- currentReviewPass: artifacts.currentReviewPass || "",
1213
- commandLogExists: artifacts.commandLogExists === true,
1214
- commandLogPath: artifacts.commandLogPath || "",
1215
- stepDefinitions: buildStepDefinitions(),
1216
- currentStepAction: null,
1217
- codex: null,
1218
- prompt: "",
1219
- nextCommand: "",
1220
- issueDefinitionRequested: artifacts.issueDefinitionRequested === true,
1221
- issueFileRequested: artifacts.issueFileRequested === true,
1222
- issueNumber: artifacts.issueNumber || "",
1223
- issueUrl: artifacts.issueUrl || "",
1224
- issueTitle: artifacts.issueTitle || "",
1225
- issueText: artifacts.issueText || "",
1226
- pullRequestFileRequested: artifacts.pullRequestFileRequested === true,
1227
- pullRequestPath: artifacts.pullRequestPath || "",
1228
- pullRequestText: artifacts.pullRequestText || "",
1229
- githubComments: cloneContractValue(artifacts.githubComments || {}),
1230
- makePlanRequested: artifacts.makePlanRequested === true,
1231
- blueprintUpdateRequested: artifacts.blueprintUpdateRequested === true,
1232
- executePlanRequested: artifacts.executePlanRequested === true,
1233
- planExecution: cloneContractValue(artifacts.planExecution || null),
1234
- finalReportPath: artifacts.finalReportText ? path.join(responsePaths.sessionRoot, "final_report") : "",
1235
- finalReportText: artifacts.finalReportText || "",
1236
- helperMapPath: artifacts.helperMapPath || "",
1237
- helperMapExists: artifacts.helperMapExists === true,
1238
- prUrl: artifacts.prUrl || "",
1239
- prOutcome: cloneContractValue(artifacts.prOutcome || null),
1240
- mainCheckoutSync: cloneContractValue(artifacts.mainCheckoutSync || null),
1241
- acceptedChangesCommit: cloneContractValue(artifacts.acceptedChangesCommit || null),
1242
- preconditions,
1243
- errors: [
1244
- createError({
1245
- code: "unsupported_workflow_version",
1246
- message: `Session ${paths.sessionId || ""} uses workflow version ${artifacts.workflowVersion || "missing"}, but this JSKIT runtime expects ${SESSION_WORKFLOW_VERSION}.`
1247
- })
1248
- ],
1249
- warnings: [],
1250
- archive: responsePaths.archive || "active",
1251
- sessionRoot: responsePaths.sessionRoot || "",
1252
- worktree: paths.worktree || "",
1253
- worktreeReady: artifacts.worktreeReady === true,
1254
- worktreeStatus: cloneContractValue(artifacts.worktreeStatus || null),
1255
- branch: paths.branch || "",
1256
- codexThreadId: artifacts.codexThreadId || ""
1257
- };
1258
- }
1259
- const currentStep = artifacts.currentStep || artifacts.nextStep || "";
1260
- const responsePrompt = typeof prompt === "string"
1261
- ? prompt
1262
- : stepCanExposeStoredPrompt(currentStep) ? artifacts.prompt || "" : "";
1263
- const responseWarnings = mergeWarnings(artifacts.warnings || [], warnings);
1264
-
1265
- return {
1266
- ok: ok === true,
1267
- sessionId: paths.sessionId || "",
1268
- status: resolvedStatus,
1269
- currentStep,
1270
- completedSteps: artifacts.completedSteps || [],
1271
- workflowVersion: artifacts.workflowVersion || "",
1272
- baseBranch: artifacts.baseBranch || "",
1273
- baseCommit: artifacts.baseCommit || "",
1274
- blueprintPath: artifacts.blueprintPath || "",
1275
- blueprintExists: artifacts.blueprintExists === true,
1276
- activeCycle: artifacts.activeCycle || "",
1277
- appReady: cloneContractValue(artifacts.appReady || null),
1278
- cycles: cloneContractValue(artifacts.cycles || []),
1279
- checks: cloneContractValue(artifacts.checks || []),
1280
- dependencyInstall: cloneContractValue(artifacts.dependencyInstall || null),
1281
- uiChecks: cloneContractValue(artifacts.uiChecks || []),
1282
- reviewPasses: cloneContractValue(artifacts.reviewPasses || []),
1283
- currentReviewPass: artifacts.currentReviewPass || "",
1284
- commandLogExists: artifacts.commandLogExists === true,
1285
- commandLogPath: artifacts.commandLogPath || "",
1286
- stepDefinitions: buildStepDefinitions(),
1287
- currentStepAction: buildCurrentStepAction(currentStep, artifacts),
1288
- actionCommands: buildStepActionCommands(paths.sessionId || "", currentStep, artifacts),
1289
- codex: codex === undefined ? await buildCodexHandoff(currentStep, artifacts) : await publicCodexContract(codex),
1290
- prompt: responsePrompt,
1291
- nextCommand: buildNextCommand(paths.sessionId || "", currentStep, artifacts),
1292
- issueDefinitionRequested: artifacts.issueDefinitionRequested === true,
1293
- issueFileRequested: artifacts.issueFileRequested === true,
1294
- issueNumber: artifacts.issueNumber || "",
1295
- issueUrl: artifacts.issueUrl || "",
1296
- issueTitle: artifacts.issueTitle || "",
1297
- issueText: artifacts.issueText || "",
1298
- pullRequestFileRequested: artifacts.pullRequestFileRequested === true,
1299
- pullRequestPath: artifacts.pullRequestPath || "",
1300
- pullRequestText: artifacts.pullRequestText || "",
1301
- githubComments: cloneContractValue(artifacts.githubComments || {}),
1302
- makePlanRequested: artifacts.makePlanRequested === true,
1303
- blueprintUpdateRequested: artifacts.blueprintUpdateRequested === true,
1304
- executePlanRequested: artifacts.executePlanRequested === true,
1305
- planExecution: cloneContractValue(artifacts.planExecution || null),
1306
- finalReportPath: artifacts.finalReportText ? path.join(responsePaths.sessionRoot, "final_report") : "",
1307
- finalReportText: artifacts.finalReportText || "",
1308
- helperMapPath: artifacts.helperMapPath || "",
1309
- helperMapExists: artifacts.helperMapExists === true,
1310
- prUrl: artifacts.prUrl || "",
1311
- prOutcome: cloneContractValue(artifacts.prOutcome || null),
1312
- mainCheckoutSync: cloneContractValue(artifacts.mainCheckoutSync || null),
1313
- acceptedChangesCommit: cloneContractValue(artifacts.acceptedChangesCommit || null),
1314
- preconditions,
1315
- errors,
1316
- warnings: responseWarnings,
1317
- archive: responsePaths.archive || (resolvedStatus === SESSION_STATUS.FINISHED ? "completed" : resolvedStatus === SESSION_STATUS.ABANDONED ? "abandoned" : "active"),
1318
- sessionRoot: responsePaths.sessionRoot || "",
1319
- worktree: paths.worktree || "",
1320
- worktreeReady: artifacts.worktreeReady === true,
1321
- worktreeStatus: cloneContractValue(artifacts.worktreeStatus || null),
1322
- branch: paths.branch || "",
1323
- codexThreadId: artifacts.codexThreadId || ""
1324
- };
1325
- }
1326
-
1327
- function buildSessionErrorResponse({
1328
- targetRoot = process.cwd(),
1329
- sessionId = "",
1330
- code,
1331
- message,
1332
- repairCommand = "",
1333
- status = SESSION_STATUS.BLOCKED,
1334
- preconditions = [],
1335
- errors = undefined
1336
- } = {}) {
1337
- const normalizedTargetRoot = path.resolve(normalizeText(targetRoot) || process.cwd());
1338
- const errorList = Array.isArray(errors)
1339
- ? errors
1340
- : [
1341
- createError({
1342
- code,
1343
- message,
1344
- repairCommand
1345
- })
1346
- ];
1347
-
1348
- return {
1349
- ok: false,
1350
- sessionId: normalizeText(sessionId),
1351
- status,
1352
- currentStep: "",
1353
- completedSteps: [],
1354
- workflowVersion: "",
1355
- baseBranch: "",
1356
- baseCommit: "",
1357
- blueprintPath: "",
1358
- blueprintExists: false,
1359
- activeCycle: "",
1360
- appReady: null,
1361
- cycles: [],
1362
- checks: [],
1363
- dependencyInstall: null,
1364
- uiChecks: [],
1365
- reviewPasses: [],
1366
- currentReviewPass: "",
1367
- commandLogExists: false,
1368
- commandLogPath: "",
1369
- stepDefinitions: buildStepDefinitions(),
1370
- currentStepAction: null,
1371
- codex: null,
1372
- prompt: "",
1373
- nextCommand: "",
1374
- issueTitle: "",
1375
- issueText: "",
1376
- pullRequestFileRequested: false,
1377
- pullRequestPath: "",
1378
- pullRequestText: "",
1379
- githubComments: {},
1380
- planExecution: null,
1381
- finalReportPath: "",
1382
- finalReportText: "",
1383
- helperMapPath: "",
1384
- helperMapExists: false,
1385
- issueUrl: "",
1386
- prUrl: "",
1387
- prOutcome: null,
1388
- preconditions,
1389
- errors: errorList,
1390
- warnings: [],
1391
- archive: "",
1392
- sessionRoot: "",
1393
- worktree: "",
1394
- worktreeReady: false,
1395
- worktreeStatus: null,
1396
- branch: "",
1397
- codexThreadId: "",
1398
- targetRoot: normalizedTargetRoot
1399
- };
1400
- }
1401
-
1402
- async function markStatus(paths, status) {
1403
- await writeTextFile(path.join(paths.sessionRoot, "status"), status);
1404
- }
1405
-
1406
- async function markCurrentStep(paths, stepId) {
1407
- await writeTextFile(path.join(paths.sessionRoot, "current_step"), stepId);
1408
- }
1409
-
1410
- async function writeStepRecord(paths, stepId, message) {
1411
- const root = isCycleStepId(stepId)
1412
- ? cycleStepsRoot(paths, await readActiveCycle(paths))
1413
- : path.join(paths.sessionRoot, "steps");
1414
- await mkdir(root, { recursive: true });
1415
- await writeTextFile(
1416
- path.join(root, stepId),
1417
- `${timestampForStepRecord()}\n${normalizeText(message) || STEP_LABEL_BY_ID[stepId] || stepId}`
1418
- );
1419
- const completedSteps = await readCompletedSteps(paths);
1420
- await markCurrentStep(paths, resolveNextStep(completedSteps));
1421
- }
1422
-
1423
- async function writeCycleStepRecord(paths, recordName, message, {
1424
- cycle = ""
1425
- } = {}) {
1426
- const activeCycle = normalizeCycleNumber(cycle || await readActiveCycle(paths));
1427
- const root = cycleStepsRoot(paths, activeCycle);
1428
- await mkdir(root, { recursive: true });
1429
- await writeTextFile(
1430
- path.join(root, normalizeText(recordName)),
1431
- `${timestampForStepRecord()}\n${normalizeText(message) || normalizeText(recordName)}`
1432
- );
1433
- }
1434
-
1435
- async function failSession(paths, {
1436
- code,
1437
- message,
1438
- repairCommand = "",
1439
- preconditions = [],
1440
- status = SESSION_STATUS.BLOCKED,
1441
- prompt = "",
1442
- codex = undefined
1443
- }) {
1444
- if (paths.sessionRoot && await fileExists(paths.sessionRoot)) {
1445
- await markStatus(paths, status);
1446
- }
1447
- return buildSessionResponse(paths, {
1448
- ok: false,
1449
- status,
1450
- prompt,
1451
- codex,
1452
- preconditions,
1453
- errors: [
1454
- createError({
1455
- code,
1456
- message,
1457
- repairCommand
1458
- })
1459
- ]
1460
- });
1461
- }
1462
-
1463
- export {
1464
- buildSessionErrorResponse,
1465
- buildSessionResponse,
1466
- buildStepDefinitions,
1467
- createError,
1468
- createPrecondition,
1469
- failSession,
1470
- markCurrentStep,
1471
- markStatus,
1472
- normalizeReviewPassNumber,
1473
- readActiveCycle,
1474
- readStepRecords,
1475
- readReviewPasses,
1476
- readSessionArtifacts,
1477
- reviewPassDirectoryName,
1478
- reviewPassRoot,
1479
- writeCycleStepRecord,
1480
- writeStepRecord
1481
- };