@jskit-ai/jskit-cli 0.2.89 → 0.2.91
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.
- package/package.json +4 -4
- package/src/server/commandHandlers/session.js +173 -142
- package/src/server/core/argParser.js +0 -4
- package/src/server/core/commandCatalog.js +47 -35
- package/src/server/sessionRuntime/constants.js +125 -348
- package/src/server/sessionRuntime/io.js +2 -2
- package/src/server/sessionRuntime/preconditions.js +39 -143
- package/src/server/sessionRuntime/promptRenderer.js +2 -15
- package/src/server/sessionRuntime/prompts/app_blueprint.md +2 -7
- package/src/server/sessionRuntime/prompts/{automated_checks.md → automated_checks_run.md} +2 -17
- package/src/server/sessionRuntime/prompts/{update_blueprint.md → blueprint_updated.md} +2 -11
- package/src/server/sessionRuntime/prompts/{deep_ui_check.md → deep_ui_check_run.md} +2 -19
- package/src/server/sessionRuntime/prompts/final_report_created.md +44 -0
- package/src/server/sessionRuntime/prompts/issue_created.md +26 -0
- package/src/server/sessionRuntime/prompts/issue_prompt_rendered.md +1 -0
- package/src/server/sessionRuntime/prompts/{plan_issue.md → make_plan.md} +8 -37
- package/src/server/sessionRuntime/prompts/{execute_plan.md → plan_executed.md} +4 -29
- package/src/server/sessionRuntime/prompts/{prepare_pr_merge.md → pr_merge_prepared.md} +3 -3
- package/src/server/sessionRuntime/prompts/{resolve_deslop_findings.md → review_changes_accepted_resolve.md} +2 -6
- package/src/server/sessionRuntime/prompts/{review_changes.md → review_prompt_rendered.md} +3 -28
- package/src/server/sessionRuntime/prompts/{user_check.md → user_check_completed.md} +1 -11
- package/src/server/sessionRuntime/responses.js +431 -292
- package/src/server/sessionRuntime.js +1201 -926
- package/src/server/sessionRuntime/prompts/issue_details.md +0 -49
- package/src/server/sessionRuntime/prompts/new_issue.md +0 -46
|
@@ -13,13 +13,17 @@ import path from "node:path";
|
|
|
13
13
|
import {
|
|
14
14
|
BLUEPRINT_CODEX_HANDOFF,
|
|
15
15
|
AUTOMATED_CHECK_REPAIR_CODEX_HANDOFF,
|
|
16
|
-
|
|
16
|
+
DEPENDENCIES_INSTALL_RESULT_FILE,
|
|
17
17
|
DEEP_UI_CHECK_CODEX_HANDOFF,
|
|
18
|
-
|
|
18
|
+
ISSUE_DEFINITION_CODEX_HANDOFF,
|
|
19
|
+
ISSUE_FILE_CODEX_HANDOFF,
|
|
20
|
+
PLAN_CODEX_HANDOFF,
|
|
19
21
|
PLAN_EXECUTION_CODEX_HANDOFF,
|
|
22
|
+
PR_FILE_CODEX_HANDOFF,
|
|
20
23
|
PR_MERGE_PREP_CODEX_HANDOFF,
|
|
21
24
|
REVIEW_PASS_LIMIT,
|
|
22
25
|
REVIEW_EXECUTION_CODEX_HANDOFF,
|
|
26
|
+
RESOLVE_DESLOP_CODEX_HANDOFF,
|
|
23
27
|
SESSION_STATUS,
|
|
24
28
|
SESSION_WORKFLOW_VERSION,
|
|
25
29
|
STEP_DEFINITION_BY_ID,
|
|
@@ -35,7 +39,7 @@ import {
|
|
|
35
39
|
runCommand,
|
|
36
40
|
runGit,
|
|
37
41
|
runGitInWorktree,
|
|
38
|
-
|
|
42
|
+
timestampForStepRecord,
|
|
39
43
|
writeTextFile
|
|
40
44
|
} from "./sessionRuntime/io.js";
|
|
41
45
|
import {
|
|
@@ -57,13 +61,11 @@ import {
|
|
|
57
61
|
markStatus,
|
|
58
62
|
normalizeReviewPassNumber,
|
|
59
63
|
readActiveCycle,
|
|
60
|
-
|
|
64
|
+
readStepRecords,
|
|
61
65
|
readReviewPasses,
|
|
62
66
|
readSessionArtifacts,
|
|
63
67
|
reviewPassRoot,
|
|
64
|
-
|
|
65
|
-
writeCycleReceipt,
|
|
66
|
-
writeReceipt
|
|
68
|
+
writeStepRecord
|
|
67
69
|
} from "./sessionRuntime/responses.js";
|
|
68
70
|
import {
|
|
69
71
|
applyPreconditions,
|
|
@@ -73,22 +75,20 @@ import {
|
|
|
73
75
|
assertBlueprintUpdateSatisfied,
|
|
74
76
|
assertDeepUiCheckSatisfied,
|
|
75
77
|
assertDependenciesInstalled,
|
|
76
|
-
assertFinalReportExists,
|
|
77
78
|
assertGhAuth,
|
|
78
79
|
assertGitCurrentBranch,
|
|
79
80
|
assertGitRepository,
|
|
80
81
|
assertGithubOrigin,
|
|
81
|
-
assertIssueMetadataExists,
|
|
82
82
|
assertIssueTextExists,
|
|
83
83
|
assertIssueUrlExists,
|
|
84
84
|
assertAutomatedChecksPassed,
|
|
85
|
-
assertIssueDetailsExists,
|
|
86
85
|
assertMainCheckoutSyncSatisfied,
|
|
87
|
-
assertPlanTextExists,
|
|
88
86
|
assertPrUrlExists,
|
|
87
|
+
assertPullRequestFileExists,
|
|
89
88
|
assertReadyJskitApp,
|
|
90
89
|
assertSessionExists,
|
|
91
90
|
assertTargetRootWritable,
|
|
91
|
+
assertUserCheckPassed,
|
|
92
92
|
assertWorktreeExists,
|
|
93
93
|
ensureStudioGitExclude,
|
|
94
94
|
hasWorktree
|
|
@@ -176,15 +176,6 @@ function extractMarkedText(value = "", marker = "") {
|
|
|
176
176
|
return normalizeText(matches.length > 0 ? matches[matches.length - 1][1] : "");
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
function extractMarkedField(value = "", fieldName = "") {
|
|
180
|
-
const normalizedFieldName = normalizeText(fieldName);
|
|
181
|
-
if (!normalizedFieldName) {
|
|
182
|
-
return "";
|
|
183
|
-
}
|
|
184
|
-
const pattern = new RegExp(`^\\s*${normalizedFieldName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}\\s*:\\s*(.+)$`, "imu");
|
|
185
|
-
return normalizeText(pattern.exec(normalizeText(value))?.[1] || "");
|
|
186
|
-
}
|
|
187
|
-
|
|
188
179
|
function extractIssueTitle(value = "") {
|
|
189
180
|
return extractMarkedText(value, "issue_title");
|
|
190
181
|
}
|
|
@@ -193,100 +184,10 @@ function extractIssueText(value = "") {
|
|
|
193
184
|
return extractMarkedText(value, "issue_text") || normalizeText(value);
|
|
194
185
|
}
|
|
195
186
|
|
|
196
|
-
function extractPlanText(value = "") {
|
|
197
|
-
return extractMarkedText(value, "plan") || normalizeText(value);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function extractIssueDetails(value = "") {
|
|
201
|
-
return extractMarkedText(value, "issue_details");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function extractIssueCategory(value = "") {
|
|
205
|
-
return extractMarkedText(value, "issue_category");
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function extractUiImpact(value = "") {
|
|
209
|
-
return extractMarkedText(value, "ui_impact");
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function extractAgentDecisions(value = "") {
|
|
213
|
-
return extractMarkedText(value, "agent_decisions");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function normalizeIssueCategory(value = "") {
|
|
217
|
-
const category = normalizeText(value).toLowerCase();
|
|
218
|
-
return ["client", "server", "client_server", "tooling", "unknown"].includes(category)
|
|
219
|
-
? category
|
|
220
|
-
: "";
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function normalizeUiImpact(value = "") {
|
|
224
|
-
const impact = normalizeText(value).toLowerCase();
|
|
225
|
-
return ["none", "possible", "definite", "unknown"].includes(impact)
|
|
226
|
-
? impact
|
|
227
|
-
: "";
|
|
228
|
-
}
|
|
229
|
-
|
|
230
187
|
async function writePromptArtifact(paths, fileName, prompt) {
|
|
231
188
|
await writeTextFile(path.join(paths.sessionRoot, "prompts", fileName), prompt);
|
|
232
189
|
}
|
|
233
190
|
|
|
234
|
-
async function codexResultPath(paths, stepId) {
|
|
235
|
-
if (CYCLE_STEP_IDS.includes(stepId)) {
|
|
236
|
-
const activeCycle = await readActiveCycle(paths);
|
|
237
|
-
return path.join(cycleRootPath(paths, activeCycle), "codex_results", `${stepId}.md`);
|
|
238
|
-
}
|
|
239
|
-
return path.join(paths.sessionRoot, "codex_results", `${stepId}.md`);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function codexResponseContractForStep(stepId) {
|
|
243
|
-
const contract = STEP_DEFINITION_BY_ID[stepId]?.codex?.responseContract;
|
|
244
|
-
return contract && typeof contract === "object" && !Array.isArray(contract) ? contract : null;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async function requireCodexStepResult(paths, stepId, result, preconditions = [], contractOverride = null) {
|
|
248
|
-
const contract = contractOverride || codexResponseContractForStep(stepId);
|
|
249
|
-
if (contract?.required !== true || !contract.marker) {
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const source = String(result || "");
|
|
254
|
-
if (!source.trim()) {
|
|
255
|
-
return failSession(paths, {
|
|
256
|
-
code: "codex_result_required",
|
|
257
|
-
message: `The ${STEP_DEFINITION_BY_ID[stepId]?.label || stepId} step has already produced a Codex prompt. Paste Codex's final marked result with --codex-result - before JSKIT records the receipt.`,
|
|
258
|
-
repairCommand: `jskit session ${paths.sessionId} step --codex-result -`,
|
|
259
|
-
preconditions
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const markedResult = extractMarkedText(source, contract.marker);
|
|
264
|
-
if (!markedResult) {
|
|
265
|
-
return failSession(paths, {
|
|
266
|
-
code: "codex_result_marker_missing",
|
|
267
|
-
message: `Codex output for ${STEP_DEFINITION_BY_ID[stepId]?.label || stepId} must include [${contract.marker}]... [/${contract.marker}] before JSKIT can advance.`,
|
|
268
|
-
repairCommand: `jskit session ${paths.sessionId} step --codex-result -`,
|
|
269
|
-
preconditions
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
const stepField = normalizeText(contract.stepField);
|
|
273
|
-
if (stepField) {
|
|
274
|
-
const resultStep = extractMarkedField(markedResult, stepField);
|
|
275
|
-
if (resultStep !== stepId) {
|
|
276
|
-
return failSession(paths, {
|
|
277
|
-
code: "codex_result_step_mismatch",
|
|
278
|
-
message: `Codex output for ${STEP_DEFINITION_BY_ID[stepId]?.label || stepId} must include ${stepField}: ${stepId} inside [${contract.marker}] before JSKIT can advance.`,
|
|
279
|
-
repairCommand: `jskit session ${paths.sessionId} step --codex-result -`,
|
|
280
|
-
preconditions
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
await writeTextFile(await codexResultPath(paths, stepId), source);
|
|
286
|
-
await appendAgentDecisions(paths, extractAgentDecisions(source));
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
191
|
function commandText(command, args = []) {
|
|
291
192
|
return [command, ...args].map((part) => {
|
|
292
193
|
const value = String(part || "");
|
|
@@ -300,28 +201,6 @@ function cycleRootPath(paths, cycle) {
|
|
|
300
201
|
return path.join(paths.sessionRoot, "cycles", `cycle_${cycle}`);
|
|
301
202
|
}
|
|
302
203
|
|
|
303
|
-
function cyclePlanPath(paths, cycle) {
|
|
304
|
-
return path.join(cycleRootPath(paths, cycle), "plan.md");
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function cyclePlanPromptFileName(cycle) {
|
|
308
|
-
return `cycle_${cycle}_plan_request.md`;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function cyclePlanExecutionPromptFileName(cycle) {
|
|
312
|
-
return `cycle_${cycle}_plan_execution.md`;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async function readCurrentPlan(paths) {
|
|
316
|
-
const activeCycle = await readActiveCycle(paths);
|
|
317
|
-
const planPath = cyclePlanPath(paths, activeCycle);
|
|
318
|
-
return {
|
|
319
|
-
activeCycle,
|
|
320
|
-
planPath,
|
|
321
|
-
planText: await readTrimmedFile(planPath)
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
204
|
function commandOutputSummary(output = "") {
|
|
326
205
|
const normalized = normalizeText(output);
|
|
327
206
|
if (normalized.length <= 1800) {
|
|
@@ -341,7 +220,7 @@ async function appendCommandLog(paths, {
|
|
|
341
220
|
return;
|
|
342
221
|
}
|
|
343
222
|
const entry = {
|
|
344
|
-
at:
|
|
223
|
+
at: timestampForStepRecord(),
|
|
345
224
|
command: commandText(command, args),
|
|
346
225
|
cwd,
|
|
347
226
|
exitCode: Number.isInteger(result.exitCode) ? result.exitCode : null,
|
|
@@ -420,14 +299,14 @@ function packageScriptRepairCommand(paths, command, args) {
|
|
|
420
299
|
return `cd ${paths.worktree} && ${command} ${args.join(" ")}`;
|
|
421
300
|
}
|
|
422
301
|
|
|
423
|
-
function
|
|
302
|
+
function packageScriptRecordName(scriptName) {
|
|
424
303
|
return normalizeText(scriptName).replace(/[^a-zA-Z0-9._-]+/gu, "_");
|
|
425
304
|
}
|
|
426
305
|
|
|
427
|
-
async function
|
|
306
|
+
async function writeSessionHookRecord(paths, scriptName, message) {
|
|
428
307
|
await writeTextFile(
|
|
429
|
-
path.join(paths.sessionRoot, "hooks",
|
|
430
|
-
`${
|
|
308
|
+
path.join(paths.sessionRoot, "hooks", packageScriptRecordName(scriptName)),
|
|
309
|
+
`${timestampForStepRecord()}\n${normalizeText(message) || `${scriptName} completed.`}`
|
|
431
310
|
);
|
|
432
311
|
}
|
|
433
312
|
|
|
@@ -465,7 +344,7 @@ async function runOptionalSessionPackageScript(paths, {
|
|
|
465
344
|
})
|
|
466
345
|
};
|
|
467
346
|
}
|
|
468
|
-
await
|
|
347
|
+
await writeSessionHookRecord(paths, scriptName, result.output || `${scriptName} completed.`);
|
|
469
348
|
return {
|
|
470
349
|
ok: true,
|
|
471
350
|
ran: true,
|
|
@@ -497,29 +376,6 @@ async function runSessionProvisioningHook(paths, {
|
|
|
497
376
|
});
|
|
498
377
|
}
|
|
499
378
|
|
|
500
|
-
async function readIssueMetadata(paths) {
|
|
501
|
-
const source = await readTextIfExists(path.join(paths.sessionRoot, "issue_metadata.json"));
|
|
502
|
-
if (!source) {
|
|
503
|
-
return {};
|
|
504
|
-
}
|
|
505
|
-
try {
|
|
506
|
-
const parsed = JSON.parse(source);
|
|
507
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
508
|
-
} catch {
|
|
509
|
-
return {};
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
async function writeIssueMetadata(paths, metadata = {}) {
|
|
514
|
-
const existing = await readIssueMetadata(paths);
|
|
515
|
-
const next = {
|
|
516
|
-
...existing,
|
|
517
|
-
...metadata
|
|
518
|
-
};
|
|
519
|
-
await writeTextFile(path.join(paths.sessionRoot, "issue_metadata.json"), `${JSON.stringify(next, null, 2)}\n`);
|
|
520
|
-
return next;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
379
|
async function readGithubComments(paths) {
|
|
524
380
|
const source = await readTextIfExists(path.join(paths.sessionRoot, "github_comments.json"));
|
|
525
381
|
if (!source) {
|
|
@@ -564,7 +420,7 @@ async function commentOnIssueOnce(paths, {
|
|
|
564
420
|
}
|
|
565
421
|
comments[normalizedPurpose] = {
|
|
566
422
|
bodyFile,
|
|
567
|
-
commentedAt:
|
|
423
|
+
commentedAt: timestampForStepRecord(),
|
|
568
424
|
issueUrl,
|
|
569
425
|
purpose: normalizedPurpose
|
|
570
426
|
};
|
|
@@ -575,61 +431,42 @@ async function commentOnIssueOnce(paths, {
|
|
|
575
431
|
};
|
|
576
432
|
}
|
|
577
433
|
|
|
578
|
-
async function appendAgentDecisions(paths, decisions = "") {
|
|
579
|
-
const normalized = normalizeText(decisions);
|
|
580
|
-
if (!normalized) {
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
const decisionsPath = path.join(paths.sessionRoot, "agent_decisions.md");
|
|
584
|
-
const existing = await readTextIfExists(decisionsPath);
|
|
585
|
-
await writeTextFile(
|
|
586
|
-
decisionsPath,
|
|
587
|
-
`${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${normalized}\n`
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
async function appendAgentDecisionsInput(paths, options = {}) {
|
|
592
|
-
const source = normalizeText(options.agentDecisions || options["agent-decisions"]);
|
|
593
|
-
if (!source) {
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
await appendAgentDecisions(paths, extractAgentDecisions(source) || source);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
async function recordIssueInAgentDecisions(paths, issueUrl = "") {
|
|
600
|
-
const normalizedIssueUrl = normalizeText(issueUrl);
|
|
601
|
-
if (!normalizedIssueUrl) {
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
const decisionsPath = path.join(paths.sessionRoot, "agent_decisions.md");
|
|
605
|
-
const existing = await readTextIfExists(decisionsPath);
|
|
606
|
-
if (existing.includes(`Issue: ${normalizedIssueUrl}`)) {
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
await writeTextFile(
|
|
610
|
-
decisionsPath,
|
|
611
|
-
`${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}Issue: ${normalizedIssueUrl}\n\n`
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
434
|
function issueMetadataFromUrl(issueUrl = "") {
|
|
616
|
-
const match = /^https:\/\/github\.com\/([
|
|
435
|
+
const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)(?:\b|$)/u.exec(String(issueUrl || "").trim());
|
|
617
436
|
if (!match) {
|
|
618
437
|
return {
|
|
619
438
|
issueNumber: "",
|
|
439
|
+
issueUrl: normalizeText(issueUrl),
|
|
620
440
|
owner: "",
|
|
621
441
|
repository: ""
|
|
622
442
|
};
|
|
623
443
|
}
|
|
624
444
|
return {
|
|
625
445
|
issueNumber: match[3],
|
|
446
|
+
issueUrl: normalizeText(issueUrl),
|
|
626
447
|
owner: match[1],
|
|
627
448
|
repository: match[2]
|
|
628
449
|
};
|
|
629
450
|
}
|
|
630
451
|
|
|
631
|
-
function
|
|
632
|
-
|
|
452
|
+
async function writeIssueMetadataFiles(paths, {
|
|
453
|
+
issueTitle = "",
|
|
454
|
+
issueUrl = ""
|
|
455
|
+
} = {}) {
|
|
456
|
+
const issueMetadata = issueMetadataFromUrl(issueUrl);
|
|
457
|
+
const metadataValues = {
|
|
458
|
+
issue_body_path: path.join(paths.sessionRoot, "issue.md"),
|
|
459
|
+
issue_number: issueMetadata.issueNumber,
|
|
460
|
+
issue_owner: issueMetadata.owner,
|
|
461
|
+
issue_repository: issueMetadata.repository,
|
|
462
|
+
issue_title: normalizeText(issueTitle),
|
|
463
|
+
issue_url: issueMetadata.issueUrl
|
|
464
|
+
};
|
|
465
|
+
await Promise.all(
|
|
466
|
+
Object.entries(metadataValues)
|
|
467
|
+
.filter(([, value]) => normalizeText(value))
|
|
468
|
+
.map(([name, value]) => writeTextFile(path.join(paths.sessionRoot, "metadata", name), value))
|
|
469
|
+
);
|
|
633
470
|
}
|
|
634
471
|
|
|
635
472
|
async function createSession({
|
|
@@ -667,11 +504,9 @@ async function createSession({
|
|
|
667
504
|
await ensureStudioGitExclude(initialPaths.targetRoot);
|
|
668
505
|
await mkdir(initialPaths.sessionRoot, { recursive: true });
|
|
669
506
|
await writeTextFile(path.join(initialPaths.sessionRoot, "transcript.log"), "");
|
|
670
|
-
await writeTextFile(path.join(initialPaths.sessionRoot, "agent_decisions.md"), `# Agent Decisions\n\nSession: ${initialPaths.sessionId}\nCreated: ${now.toISOString()}\n\n`);
|
|
671
507
|
await writeTextFile(path.join(initialPaths.sessionRoot, "workflow_version"), `${SESSION_WORKFLOW_VERSION}\n`);
|
|
672
|
-
await writeActiveCycle(initialPaths, "001");
|
|
673
508
|
await markStatus(initialPaths, SESSION_STATUS.PENDING);
|
|
674
|
-
await
|
|
509
|
+
await markCurrentStep(initialPaths, "worktree_created");
|
|
675
510
|
|
|
676
511
|
return buildSessionResponse(initialPaths, {
|
|
677
512
|
ok: true,
|
|
@@ -762,8 +597,7 @@ function emptySessionDetails(response) {
|
|
|
762
597
|
...response,
|
|
763
598
|
issueTitle: "",
|
|
764
599
|
issueText: "",
|
|
765
|
-
|
|
766
|
-
receipts: [],
|
|
600
|
+
stepRecords: [],
|
|
767
601
|
transcriptLog: ""
|
|
768
602
|
};
|
|
769
603
|
}
|
|
@@ -778,12 +612,10 @@ async function inspectSessionDetails({
|
|
|
778
612
|
}
|
|
779
613
|
const { paths, preconditions } = context;
|
|
780
614
|
const response = await buildSessionResponse(paths, { preconditions });
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
const [issueText, issueTitle, receipts, transcriptLog] = await Promise.all([
|
|
615
|
+
const [issueText, issueTitle, stepRecords, transcriptLog] = await Promise.all([
|
|
784
616
|
readTextIfExists(path.join(paths.sessionRoot, "issue.md")),
|
|
785
617
|
readTrimmedFile(path.join(paths.sessionRoot, "issue_title")),
|
|
786
|
-
|
|
618
|
+
readStepRecords(paths),
|
|
787
619
|
readTextIfExists(path.join(paths.sessionRoot, "transcript.log"))
|
|
788
620
|
]);
|
|
789
621
|
|
|
@@ -791,8 +623,7 @@ async function inspectSessionDetails({
|
|
|
791
623
|
...response,
|
|
792
624
|
issueTitle,
|
|
793
625
|
issueText: issueText.trim(),
|
|
794
|
-
|
|
795
|
-
receipts,
|
|
626
|
+
stepRecords,
|
|
796
627
|
transcriptLog
|
|
797
628
|
};
|
|
798
629
|
}
|
|
@@ -825,6 +656,7 @@ async function removeEmptyStaleWorktreeDirectory(paths) {
|
|
|
825
656
|
|
|
826
657
|
async function createWorktree(paths, _options = {}, context = {}) {
|
|
827
658
|
const preconditions = context.preconditions || [];
|
|
659
|
+
const completeStep = context.completeStep !== false;
|
|
828
660
|
const [baseBranchResult, baseCommitResult] = await Promise.all([
|
|
829
661
|
runGit(paths.targetRoot, ["branch", "--show-current"], { timeout: 15000 }),
|
|
830
662
|
runGit(paths.targetRoot, ["rev-parse", "--verify", "HEAD"], { timeout: 15000 })
|
|
@@ -838,7 +670,9 @@ async function createWorktree(paths, _options = {}, context = {}) {
|
|
|
838
670
|
if (baseCommit && !await readTrimmedFile(path.join(paths.sessionRoot, "base_commit"))) {
|
|
839
671
|
await writeTextFile(path.join(paths.sessionRoot, "base_commit"), `${baseCommit}\n`);
|
|
840
672
|
}
|
|
841
|
-
|
|
673
|
+
if (completeStep) {
|
|
674
|
+
await writeStepRecord(paths, "worktree_created", `Reused existing worktree ${paths.worktree}.`);
|
|
675
|
+
}
|
|
842
676
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
843
677
|
return buildSessionResponse(paths, {
|
|
844
678
|
preconditions
|
|
@@ -873,7 +707,9 @@ async function createWorktree(paths, _options = {}, context = {}) {
|
|
|
873
707
|
if (baseCommit) {
|
|
874
708
|
await writeTextFile(path.join(paths.sessionRoot, "base_commit"), `${baseCommit}\n`);
|
|
875
709
|
}
|
|
876
|
-
|
|
710
|
+
if (completeStep) {
|
|
711
|
+
await writeStepRecord(paths, "worktree_created", `Created worktree ${paths.worktree} on branch ${paths.branch}.`);
|
|
712
|
+
}
|
|
877
713
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
878
714
|
return buildSessionResponse(paths, {
|
|
879
715
|
preconditions
|
|
@@ -884,7 +720,21 @@ async function recordDependenciesInstalled(paths, {
|
|
|
884
720
|
message = "Installed Node dependencies in the session worktree.",
|
|
885
721
|
preconditions = []
|
|
886
722
|
} = {}) {
|
|
887
|
-
await
|
|
723
|
+
await writeStepRecord(paths, "dependencies_installed", message);
|
|
724
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
725
|
+
return buildSessionResponse(paths, {
|
|
726
|
+
preconditions
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function recordDependencyInstallResult(paths, {
|
|
731
|
+
message = "Installed Node dependencies in the session worktree.",
|
|
732
|
+
preconditions = []
|
|
733
|
+
} = {}) {
|
|
734
|
+
await writeTextFile(
|
|
735
|
+
path.join(paths.sessionRoot, DEPENDENCIES_INSTALL_RESULT_FILE),
|
|
736
|
+
`${timestampForStepRecord()}\n${normalizeText(message) || "Installed Node dependencies in the session worktree."}`
|
|
737
|
+
);
|
|
888
738
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
889
739
|
return buildSessionResponse(paths, {
|
|
890
740
|
preconditions
|
|
@@ -949,6 +799,7 @@ async function dependencyInstallCommandForWorktree(worktree) {
|
|
|
949
799
|
|
|
950
800
|
async function installDependencies(paths, _options = {}, context = {}) {
|
|
951
801
|
const preconditions = context.preconditions || [];
|
|
802
|
+
const completeStep = context.completeStep !== false;
|
|
952
803
|
const [command, args] = await dependencyInstallCommandForWorktree(paths.worktree);
|
|
953
804
|
const result = await runLoggedCommand(paths, "dependencies_install", command, args, {
|
|
954
805
|
cwd: paths.worktree,
|
|
@@ -970,7 +821,8 @@ async function installDependencies(paths, _options = {}, context = {}) {
|
|
|
970
821
|
return provisionResult.response;
|
|
971
822
|
}
|
|
972
823
|
const installMessage = result.output || `Installed Node dependencies in the session worktree with ${command} ${args.join(" ")}.`;
|
|
973
|
-
|
|
824
|
+
const recorder = completeStep ? recordDependenciesInstalled : recordDependencyInstallResult;
|
|
825
|
+
return recorder(paths, {
|
|
974
826
|
message: provisionResult.ran ? `${installMessage}\n${SESSION_PROVISION_PACKAGE_SCRIPT} completed.` : installMessage,
|
|
975
827
|
preconditions
|
|
976
828
|
});
|
|
@@ -1002,18 +854,39 @@ async function adoptDependenciesInstalled({
|
|
|
1002
854
|
if (!provisionResult.ok) {
|
|
1003
855
|
return provisionResult.response;
|
|
1004
856
|
}
|
|
1005
|
-
const
|
|
857
|
+
const hookMessage = provisionResult.ran
|
|
1006
858
|
? `${normalizeText(message) || "Installed Node dependencies in the session worktree."}\n${SESSION_PROVISION_PACKAGE_SCRIPT} completed.`
|
|
1007
859
|
: message;
|
|
1008
|
-
return
|
|
1009
|
-
message:
|
|
860
|
+
return recordDependencyInstallResult(paths, {
|
|
861
|
+
message: hookMessage,
|
|
1010
862
|
preconditions
|
|
1011
863
|
});
|
|
1012
864
|
});
|
|
1013
865
|
}
|
|
1014
866
|
|
|
1015
|
-
|
|
867
|
+
const STUDIO_CONTEXT_START_MARKER = "[[JSKIT_STUDIO_CONTEXT_START]]";
|
|
868
|
+
const STUDIO_CONTEXT_END_MARKER = "[[JSKIT_STUDIO_CONTEXT_END]]";
|
|
869
|
+
|
|
870
|
+
function issueDefinitionPrompt(userInput, context) {
|
|
871
|
+
return [
|
|
872
|
+
userInput,
|
|
873
|
+
"",
|
|
874
|
+
STUDIO_CONTEXT_START_MARKER,
|
|
875
|
+
"JSKIT Studio context marker: follow the instructions inside this context block normally, but ignore the surrounding JSKIT_STUDIO_CONTEXT markers.",
|
|
876
|
+
"",
|
|
877
|
+
context,
|
|
878
|
+
STUDIO_CONTEXT_END_MARKER
|
|
879
|
+
].join("\n").trim();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
async function renderIssuePrompt(paths, options = {}, context = {}) {
|
|
1016
883
|
const userInput = normalizeText(options.prompt);
|
|
884
|
+
const issueDefinitionSentinelPath = path.join(paths.sessionRoot, "metadata", "issue_prompt_rendered_requested");
|
|
885
|
+
if (!userInput && context.completeStep !== false && await fileExists(issueDefinitionSentinelPath)) {
|
|
886
|
+
await writeStepRecord(paths, "issue_prompt_rendered", "Issue scoped in Codex terminal.");
|
|
887
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
888
|
+
return buildSessionResponse(paths);
|
|
889
|
+
}
|
|
1017
890
|
if (!userInput) {
|
|
1018
891
|
return failSession(paths, {
|
|
1019
892
|
code: "prompt_required",
|
|
@@ -1021,43 +894,18 @@ async function renderIssuePrompt(paths, options = {}) {
|
|
|
1021
894
|
repairCommand: `jskit session ${paths.sessionId} step --prompt "<what should change>"`
|
|
1022
895
|
});
|
|
1023
896
|
}
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
await writePromptArtifact(paths, "issue_draft.md", prompt);
|
|
1028
|
-
await writeReceipt(paths, "issue_prompt_rendered", "Rendered the issue drafting prompt.");
|
|
897
|
+
const promptContext = await renderPrompt(paths, "issue_prompt_rendered.md");
|
|
898
|
+
const prompt = issueDefinitionPrompt(userInput, promptContext);
|
|
899
|
+
await writeTextFile(issueDefinitionSentinelPath, "true\n");
|
|
1029
900
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
1030
901
|
return buildSessionResponse(paths, {
|
|
902
|
+
codex: ISSUE_DEFINITION_CODEX_HANDOFF,
|
|
1031
903
|
ok: true,
|
|
1032
904
|
prompt,
|
|
1033
905
|
status: SESSION_STATUS.WAITING_FOR_USER
|
|
1034
906
|
});
|
|
1035
907
|
}
|
|
1036
908
|
|
|
1037
|
-
async function draftIssue(paths, options = {}) {
|
|
1038
|
-
const issueText = extractIssueText(options.issue);
|
|
1039
|
-
if (!issueText) {
|
|
1040
|
-
return failSession(paths, {
|
|
1041
|
-
code: "issue_required",
|
|
1042
|
-
message: "The issue drafting step requires --issue, --issue-file, or --issue -.",
|
|
1043
|
-
repairCommand: `jskit session ${paths.sessionId} step --issue -`
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
const issueTitle = normalizeText(options.issueTitle) || extractIssueTitle(options.issue);
|
|
1047
|
-
if (!issueTitle) {
|
|
1048
|
-
return failSession(paths, {
|
|
1049
|
-
code: "issue_title_required",
|
|
1050
|
-
message: "The issue drafting step requires an approved issue title.",
|
|
1051
|
-
repairCommand: `jskit session ${paths.sessionId} step --issue-title "<title>" --issue -`
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
await writeTextFile(path.join(paths.sessionRoot, "issue.md"), issueText);
|
|
1055
|
-
await writeTextFile(path.join(paths.sessionRoot, "issue_title"), issueTitle);
|
|
1056
|
-
await writeReceipt(paths, "issue_drafted", "Saved approved issue text.");
|
|
1057
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1058
|
-
return buildSessionResponse(paths);
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
909
|
function titleFromIssue(issueText) {
|
|
1062
910
|
const firstMeaningfulLine = String(issueText || "")
|
|
1063
911
|
.split(/\r?\n/u)
|
|
@@ -1068,24 +916,56 @@ function titleFromIssue(issueText) {
|
|
|
1068
916
|
|
|
1069
917
|
async function createIssue(paths, _options = {}, context = {}) {
|
|
1070
918
|
const preconditions = context.preconditions || [];
|
|
919
|
+
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
920
|
+
if (!issueText) {
|
|
921
|
+
return renderIssueFilePrompt(paths, context);
|
|
922
|
+
}
|
|
923
|
+
if (context.completeStep === false) {
|
|
924
|
+
return renderIssueFilePrompt(paths, context);
|
|
925
|
+
}
|
|
926
|
+
await writeStepRecord(paths, "issue_created", "Issue files are ready for review and submission.");
|
|
927
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
928
|
+
return buildSessionResponse(paths, {
|
|
929
|
+
preconditions
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async function submitIssue(paths, _options = {}, context = {}) {
|
|
934
|
+
const preconditions = context.preconditions || [];
|
|
935
|
+
const completeStep = context.completeStep !== false;
|
|
1071
936
|
const existingIssueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
1072
937
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
1073
938
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
939
|
+
if (!issueText) {
|
|
940
|
+
return sessionStepError(paths, {
|
|
941
|
+
code: "issue_file_missing",
|
|
942
|
+
message: "Cannot create the GitHub issue until issue.md exists.",
|
|
943
|
+
repairCommand: `jskit session ${paths.sessionId} create_issue_file`
|
|
944
|
+
});
|
|
945
|
+
}
|
|
1074
946
|
if (existingIssueUrl) {
|
|
1075
|
-
await
|
|
1076
|
-
...issueMetadataFromUrl(existingIssueUrl),
|
|
1077
|
-
issueBody: issueText,
|
|
1078
|
-
issueBodyPath: path.join(paths.sessionRoot, "issue.md"),
|
|
947
|
+
await writeIssueMetadataFiles(paths, {
|
|
1079
948
|
issueTitle,
|
|
1080
949
|
issueUrl: existingIssueUrl
|
|
1081
950
|
});
|
|
1082
|
-
|
|
1083
|
-
|
|
951
|
+
if (completeStep) {
|
|
952
|
+
await writeStepRecord(paths, "issue_submitted", `Reused GitHub issue ${existingIssueUrl}.`);
|
|
953
|
+
}
|
|
1084
954
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1085
955
|
return buildSessionResponse(paths, {
|
|
1086
956
|
preconditions
|
|
1087
957
|
});
|
|
1088
958
|
}
|
|
959
|
+
const githubPreconditions = await runNamedPreconditions(paths, ["github_auth", "github_origin"]);
|
|
960
|
+
if (!githubPreconditions.ok) {
|
|
961
|
+
return failSession(paths, {
|
|
962
|
+
...githubPreconditions.error,
|
|
963
|
+
preconditions: [
|
|
964
|
+
...preconditions,
|
|
965
|
+
...githubPreconditions.preconditions
|
|
966
|
+
]
|
|
967
|
+
});
|
|
968
|
+
}
|
|
1089
969
|
const result = await runLoggedCommand(paths, "github_issue_create", "gh", [
|
|
1090
970
|
"issue",
|
|
1091
971
|
"create",
|
|
@@ -1107,40 +987,30 @@ async function createIssue(paths, _options = {}, context = {}) {
|
|
|
1107
987
|
}
|
|
1108
988
|
const issueUrl = result.stdout.split(/\r?\n/u).map((line) => line.trim()).find(Boolean) || result.stdout;
|
|
1109
989
|
await writeTextFile(path.join(paths.sessionRoot, "issue_url"), issueUrl);
|
|
1110
|
-
await
|
|
1111
|
-
...issueMetadataFromUrl(issueUrl),
|
|
1112
|
-
issueBody: issueText,
|
|
1113
|
-
issueBodyPath: path.join(paths.sessionRoot, "issue.md"),
|
|
990
|
+
await writeIssueMetadataFiles(paths, {
|
|
1114
991
|
issueTitle,
|
|
1115
992
|
issueUrl
|
|
1116
993
|
});
|
|
1117
|
-
|
|
1118
|
-
|
|
994
|
+
if (completeStep) {
|
|
995
|
+
await writeStepRecord(paths, "issue_submitted", `Created GitHub issue ${issueUrl}.`);
|
|
996
|
+
}
|
|
1119
997
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1120
998
|
return buildSessionResponse(paths, {
|
|
1121
999
|
preconditions
|
|
1122
1000
|
});
|
|
1123
1001
|
}
|
|
1124
1002
|
|
|
1125
|
-
async function
|
|
1003
|
+
async function renderIssueFilePrompt(paths, context = {}) {
|
|
1126
1004
|
const preconditions = context.preconditions || [];
|
|
1127
|
-
const
|
|
1128
|
-
const
|
|
1129
|
-
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
1130
|
-
const issueNumber = issueNumberFromUrl(issueUrl);
|
|
1131
|
-
const prompt = await renderPrompt(paths, "issue_details.md", {
|
|
1005
|
+
const issueFileSentinelPath = path.join(paths.sessionRoot, "metadata", "issue_created_requested");
|
|
1006
|
+
const prompt = await renderPrompt(paths, "issue_created.md", {
|
|
1132
1007
|
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
1133
|
-
|
|
1134
|
-
issue_text: issueText,
|
|
1135
|
-
issue_title: issueTitle,
|
|
1136
|
-
issue_url: issueUrl,
|
|
1137
|
-
issue_details_file: path.join(paths.sessionRoot, "issue_details.md"),
|
|
1138
|
-
worktree: paths.worktree
|
|
1008
|
+
issue_title_file: path.join(paths.sessionRoot, "issue_title")
|
|
1139
1009
|
});
|
|
1140
|
-
await
|
|
1010
|
+
await writeTextFile(issueFileSentinelPath, "true\n");
|
|
1141
1011
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
1142
1012
|
return buildSessionResponse(paths, {
|
|
1143
|
-
codex:
|
|
1013
|
+
codex: ISSUE_FILE_CODEX_HANDOFF,
|
|
1144
1014
|
ok: true,
|
|
1145
1015
|
preconditions,
|
|
1146
1016
|
prompt,
|
|
@@ -1148,145 +1018,47 @@ async function renderIssueDetailsPrompt(paths, _options = {}, context = {}) {
|
|
|
1148
1018
|
});
|
|
1149
1019
|
}
|
|
1150
1020
|
|
|
1151
|
-
async function
|
|
1152
|
-
const preconditions = context.preconditions || [];
|
|
1153
|
-
const source = normalizeText(options.issueDetails || options["issue-details"]);
|
|
1154
|
-
const structuredIssueCategory = normalizeText(options.issueCategory || options["issue-category"]);
|
|
1155
|
-
const structuredUiImpact = normalizeText(options.uiImpact || options["ui-impact"]);
|
|
1156
|
-
const issueDetails = extractIssueDetails(source) || (
|
|
1157
|
-
structuredIssueCategory && structuredUiImpact ? source : ""
|
|
1158
|
-
);
|
|
1159
|
-
if (!source && !structuredIssueCategory && !structuredUiImpact) {
|
|
1160
|
-
return renderIssueDetailsPrompt(paths, options, context);
|
|
1161
|
-
}
|
|
1162
|
-
if (!issueDetails) {
|
|
1163
|
-
return failSession(paths, {
|
|
1164
|
-
code: "issue_details_required",
|
|
1165
|
-
message: "The details step requires confirmed issue details from Codex or the Studio form.",
|
|
1166
|
-
repairCommand: `jskit session ${paths.sessionId} step --issue-details -`,
|
|
1167
|
-
preconditions
|
|
1168
|
-
});
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
const issueCategory = normalizeIssueCategory(structuredIssueCategory || extractIssueCategory(source));
|
|
1172
|
-
const uiImpact = normalizeUiImpact(structuredUiImpact || extractUiImpact(source));
|
|
1173
|
-
if (!issueCategory) {
|
|
1174
|
-
return failSession(paths, {
|
|
1175
|
-
code: "issue_category_invalid",
|
|
1176
|
-
message: "Issue details must include [issue_category]client, server, client_server, tooling, or unknown[/issue_category].",
|
|
1177
|
-
repairCommand: `jskit session ${paths.sessionId} step --issue-details -`,
|
|
1178
|
-
preconditions
|
|
1179
|
-
});
|
|
1180
|
-
}
|
|
1181
|
-
if (!uiImpact) {
|
|
1182
|
-
return failSession(paths, {
|
|
1183
|
-
code: "ui_impact_invalid",
|
|
1184
|
-
message: "Issue details must include [ui_impact]none, possible, definite, or unknown[/ui_impact].",
|
|
1185
|
-
repairCommand: `jskit session ${paths.sessionId} step --issue-details -`,
|
|
1186
|
-
preconditions
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
await writeTextFile(path.join(paths.sessionRoot, "issue_details.md"), issueDetails);
|
|
1191
|
-
await writeIssueMetadata(paths, {
|
|
1192
|
-
issueCategory,
|
|
1193
|
-
uiImpact,
|
|
1194
|
-
issueDetailsPath: path.join(paths.sessionRoot, "issue_details.md")
|
|
1195
|
-
});
|
|
1196
|
-
|
|
1197
|
-
const decisions = extractAgentDecisions(source);
|
|
1198
|
-
await appendAgentDecisions(paths, decisions);
|
|
1199
|
-
|
|
1200
|
-
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
1201
|
-
if (issueUrl) {
|
|
1202
|
-
const commentResult = await commentOnIssueOnce(paths, {
|
|
1203
|
-
bodyFile: path.join(paths.sessionRoot, "issue_details.md"),
|
|
1204
|
-
issueUrl,
|
|
1205
|
-
purpose: "issue_details"
|
|
1206
|
-
});
|
|
1207
|
-
if (!commentResult.ok) {
|
|
1208
|
-
return failSession(paths, {
|
|
1209
|
-
code: "issue_details_comment_failed",
|
|
1210
|
-
message: commentResult.output || "Failed to comment the issue details on the GitHub issue.",
|
|
1211
|
-
repairCommand: `gh issue comment ${issueUrl} --body-file ${path.join(paths.sessionRoot, "issue_details.md")}`,
|
|
1212
|
-
preconditions
|
|
1213
|
-
});
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
await writeReceipt(paths, "issue_details_gathered", "Saved confirmed issue details and recorded the GitHub issue comment.");
|
|
1218
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1219
|
-
return buildSessionResponse(paths, {
|
|
1220
|
-
preconditions
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
async function makePlan(paths, options = {}, context = {}) {
|
|
1021
|
+
async function makePlan(paths, _options = {}, context = {}) {
|
|
1225
1022
|
const preconditions = context.preconditions || [];
|
|
1226
|
-
const
|
|
1023
|
+
const makePlanSentinelPath = path.join(paths.sessionRoot, "metadata", "make_plan_requested");
|
|
1227
1024
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
1228
1025
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
1229
1026
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
1230
1027
|
const issueNumber = issueNumberFromUrl(issueUrl);
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
const agentDecisionsText = await readTextIfExists(agentDecisionsPath);
|
|
1235
|
-
const currentCycleRoot = cycleRootPath(paths, activeCycle);
|
|
1236
|
-
const planPath = cyclePlanPath(paths, activeCycle);
|
|
1237
|
-
const reworkRequestPath = path.join(currentCycleRoot, "rework_request.md");
|
|
1238
|
-
const reworkRequest = await readTextIfExists(reworkRequestPath);
|
|
1239
|
-
|
|
1240
|
-
if (!planText) {
|
|
1241
|
-
const prompt = await renderPrompt(paths, "plan_issue.md", {
|
|
1242
|
-
active_cycle: activeCycle,
|
|
1243
|
-
agent_decisions_file: agentDecisionsPath,
|
|
1244
|
-
agent_decisions_text: agentDecisionsText,
|
|
1245
|
-
app_blueprint_file: path.join(paths.worktree, ".jskit", "APP_BLUEPRINT.md"),
|
|
1246
|
-
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
1247
|
-
issue_number: issueNumber,
|
|
1248
|
-
issue_text: issueText,
|
|
1249
|
-
issue_title: issueTitle,
|
|
1250
|
-
issue_title_file: path.join(paths.sessionRoot, "issue_title"),
|
|
1251
|
-
issue_url: issueUrl,
|
|
1252
|
-
issue_details_file: path.join(paths.sessionRoot, "issue_details.md"),
|
|
1253
|
-
issue_details_text: issueDetails,
|
|
1254
|
-
plan_file: planPath,
|
|
1255
|
-
plan_source: activeCycle === "001" ? "issue" : "rework",
|
|
1256
|
-
rework_request: reworkRequest,
|
|
1257
|
-
rework_request_file: reworkRequest ? reworkRequestPath : "",
|
|
1258
|
-
worktree: paths.worktree
|
|
1259
|
-
});
|
|
1260
|
-
await writePromptArtifact(paths, cyclePlanPromptFileName(activeCycle), prompt);
|
|
1261
|
-
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
1028
|
+
if (context.completeStep !== false && await fileExists(makePlanSentinelPath)) {
|
|
1029
|
+
await writeStepRecord(paths, "plan_made", "Plan reviewed in Codex terminal.");
|
|
1030
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1262
1031
|
return buildSessionResponse(paths, {
|
|
1263
|
-
|
|
1264
|
-
preconditions,
|
|
1265
|
-
prompt,
|
|
1266
|
-
status: SESSION_STATUS.WAITING_FOR_USER
|
|
1032
|
+
preconditions
|
|
1267
1033
|
});
|
|
1268
1034
|
}
|
|
1269
1035
|
|
|
1270
|
-
await
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1036
|
+
const prompt = await renderPrompt(paths, "make_plan.md", {
|
|
1037
|
+
app_blueprint_file: path.join(paths.worktree, ".jskit", "APP_BLUEPRINT.md"),
|
|
1038
|
+
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
1039
|
+
issue_number: issueNumber,
|
|
1040
|
+
issue_text: issueText,
|
|
1041
|
+
issue_title: issueTitle,
|
|
1042
|
+
issue_title_file: path.join(paths.sessionRoot, "issue_title"),
|
|
1043
|
+
issue_url: issueUrl,
|
|
1044
|
+
worktree: paths.worktree
|
|
1045
|
+
});
|
|
1046
|
+
await writeTextFile(makePlanSentinelPath, "true\n");
|
|
1047
|
+
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
1275
1048
|
return buildSessionResponse(paths, {
|
|
1276
|
-
|
|
1049
|
+
codex: PLAN_CODEX_HANDOFF,
|
|
1050
|
+
ok: true,
|
|
1051
|
+
preconditions,
|
|
1052
|
+
prompt,
|
|
1053
|
+
status: SESSION_STATUS.WAITING_FOR_USER
|
|
1277
1054
|
});
|
|
1278
1055
|
}
|
|
1279
1056
|
|
|
1280
|
-
async function renderPlanExecutionPrompt(paths,
|
|
1057
|
+
async function renderPlanExecutionPrompt(paths, _options = {}, context = {}) {
|
|
1281
1058
|
const preconditions = context.preconditions || [];
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const codexResultFailure = await requireCodexStepResult(paths, "plan_executed", options.codexResult, preconditions);
|
|
1286
|
-
if (codexResultFailure) {
|
|
1287
|
-
return codexResultFailure;
|
|
1288
|
-
}
|
|
1289
|
-
await writeReceipt(paths, "plan_executed", `Cycle ${activeCycle} plan execution completed by Codex.`);
|
|
1059
|
+
const executePlanSentinelPath = path.join(paths.sessionRoot, "metadata", "execute_plan_requested");
|
|
1060
|
+
if (context.completeStep !== false && await fileExists(executePlanSentinelPath)) {
|
|
1061
|
+
await writeStepRecord(paths, "plan_executed", "Plan execution completed by Codex.");
|
|
1290
1062
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1291
1063
|
return buildSessionResponse(paths, {
|
|
1292
1064
|
preconditions
|
|
@@ -1297,22 +1069,14 @@ async function renderPlanExecutionPrompt(paths, options = {}, context = {}) {
|
|
|
1297
1069
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
1298
1070
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
1299
1071
|
const issueNumber = issueNumberFromUrl(issueUrl);
|
|
1300
|
-
const
|
|
1301
|
-
const issueDetailsPath = path.join(paths.sessionRoot, "issue_details.md");
|
|
1302
|
-
const issueDetails = await readTrimmedFile(issueDetailsPath);
|
|
1303
|
-
const executionPrompt = await renderPrompt(paths, "execute_plan.md", {
|
|
1304
|
-
active_cycle: activeCycle,
|
|
1072
|
+
const executionPrompt = await renderPrompt(paths, "plan_executed.md", {
|
|
1305
1073
|
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
1306
1074
|
issue_number: issueNumber,
|
|
1307
1075
|
issue_title: issueTitle,
|
|
1308
1076
|
issue_url: issueUrl,
|
|
1309
|
-
issue_details_file: issueDetailsPath,
|
|
1310
|
-
issue_details_text: issueDetails,
|
|
1311
|
-
plan_file: planPath,
|
|
1312
|
-
plan_text: planText,
|
|
1313
1077
|
worktree: paths.worktree
|
|
1314
1078
|
});
|
|
1315
|
-
await
|
|
1079
|
+
await writeTextFile(executePlanSentinelPath, "true\n");
|
|
1316
1080
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
1317
1081
|
return buildSessionResponse(paths, {
|
|
1318
1082
|
codex: PLAN_EXECUTION_CODEX_HANDOFF,
|
|
@@ -1443,7 +1207,6 @@ async function inspectSessionDiff({
|
|
|
1443
1207
|
}
|
|
1444
1208
|
|
|
1445
1209
|
const FIRST_REWINDABLE_STEP_ID = "dependencies_installed";
|
|
1446
|
-
const CYCLE_REWIND_TARGET_STEP_ID = "plan_made";
|
|
1447
1210
|
const REWIND_CLOSED_STATUSES = Object.freeze([
|
|
1448
1211
|
SESSION_STATUS.ABANDONED,
|
|
1449
1212
|
SESSION_STATUS.FINISHED
|
|
@@ -1465,7 +1228,29 @@ async function removePromptArtifact(paths, fileName) {
|
|
|
1465
1228
|
}
|
|
1466
1229
|
|
|
1467
1230
|
async function removeGlobalCodexResult(paths, stepId) {
|
|
1468
|
-
await removeSessionPath(paths, "codex_results",
|
|
1231
|
+
await removeSessionPath(paths, "codex_results", stepId);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
async function removeCycleCodexResults(paths, stepId) {
|
|
1235
|
+
const cyclesRoot = path.join(paths.sessionRoot, "cycles");
|
|
1236
|
+
let cycleDirectories = [];
|
|
1237
|
+
try {
|
|
1238
|
+
cycleDirectories = (await readdir(cyclesRoot, { withFileTypes: true }))
|
|
1239
|
+
.filter((entry) => entry.isDirectory() && /^cycle_\d+$/u.test(entry.name))
|
|
1240
|
+
.map((entry) => entry.name);
|
|
1241
|
+
} catch {
|
|
1242
|
+
cycleDirectories = [];
|
|
1243
|
+
}
|
|
1244
|
+
await Promise.all(cycleDirectories.map((cycleDirectory) => {
|
|
1245
|
+
return removeSessionPath(paths, "cycles", cycleDirectory, "codex_results", stepId);
|
|
1246
|
+
}));
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
async function removeCodexResult(paths, stepId) {
|
|
1250
|
+
await Promise.all([
|
|
1251
|
+
removeGlobalCodexResult(paths, stepId),
|
|
1252
|
+
removeCycleCodexResults(paths, stepId)
|
|
1253
|
+
]);
|
|
1469
1254
|
}
|
|
1470
1255
|
|
|
1471
1256
|
async function removeGithubCommentPurpose(paths, purpose) {
|
|
@@ -1481,21 +1266,6 @@ async function removeGithubCommentPurpose(paths, purpose) {
|
|
|
1481
1266
|
await writeGithubComments(paths, comments);
|
|
1482
1267
|
}
|
|
1483
1268
|
|
|
1484
|
-
async function removeIssueDetailsMetadata(paths) {
|
|
1485
|
-
const metadata = await readIssueMetadata(paths);
|
|
1486
|
-
if (Object.keys(metadata).length === 0) {
|
|
1487
|
-
return;
|
|
1488
|
-
}
|
|
1489
|
-
delete metadata.issueCategory;
|
|
1490
|
-
delete metadata.issueDetailsPath;
|
|
1491
|
-
delete metadata.uiImpact;
|
|
1492
|
-
if (Object.keys(metadata).length === 0) {
|
|
1493
|
-
await removeSessionRootFile(paths, "issue_metadata.json");
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
await writeTextFile(path.join(paths.sessionRoot, "issue_metadata.json"), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
1269
|
async function removeCycleDirectories(paths) {
|
|
1500
1270
|
for (const rootName of ["steps", "cycles"]) {
|
|
1501
1271
|
const root = path.join(paths.sessionRoot, rootName);
|
|
@@ -1514,99 +1284,102 @@ async function removeCycleDirectories(paths) {
|
|
|
1514
1284
|
}
|
|
1515
1285
|
}
|
|
1516
1286
|
|
|
1517
|
-
async function
|
|
1518
|
-
|
|
1519
|
-
let entries = [];
|
|
1520
|
-
try {
|
|
1521
|
-
entries = await readdir(promptsRoot, { withFileTypes: true });
|
|
1522
|
-
} catch {
|
|
1523
|
-
entries = [];
|
|
1524
|
-
}
|
|
1525
|
-
const cyclePromptPattern = /^cycle_\d+_(?:plan_request|plan_execution)\.md$/u;
|
|
1526
|
-
const cyclePromptFiles = entries
|
|
1527
|
-
.filter((entry) => entry.isFile() && cyclePromptPattern.test(entry.name))
|
|
1528
|
-
.map((entry) => entry.name);
|
|
1529
|
-
await Promise.all([
|
|
1530
|
-
...cyclePromptFiles.map((fileName) => removePromptArtifact(paths, fileName)),
|
|
1531
|
-
removePromptArtifact(paths, "automated_checks_run.md"),
|
|
1532
|
-
removePromptArtifact(paths, "deep_ui_check_run.md"),
|
|
1533
|
-
removePromptArtifact(paths, "review.md"),
|
|
1534
|
-
removePromptArtifact(paths, "user_check.md")
|
|
1535
|
-
]);
|
|
1287
|
+
async function removePlanArtifacts(paths) {
|
|
1288
|
+
await removeSessionPath(paths, "metadata", "make_plan_requested");
|
|
1536
1289
|
}
|
|
1537
1290
|
|
|
1538
|
-
async function
|
|
1291
|
+
async function removePlanExecutionArtifacts(paths) {
|
|
1539
1292
|
await Promise.all([
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
removeSessionPath(paths, "checks"),
|
|
1543
|
-
removeSessionPath(paths, "ui_checks"),
|
|
1544
|
-
removeSessionPath(paths, "review_passes")
|
|
1293
|
+
removeSessionPath(paths, "metadata", "execute_plan_requested"),
|
|
1294
|
+
removeCodexResult(paths, "plan_executed")
|
|
1545
1295
|
]);
|
|
1546
|
-
await writeActiveCycle(paths, "001");
|
|
1547
1296
|
}
|
|
1548
1297
|
|
|
1549
1298
|
const STEP_CANCELERS = Object.freeze({
|
|
1550
1299
|
dependencies_installed: async () => {},
|
|
1551
1300
|
issue_prompt_rendered: async (paths) => {
|
|
1552
|
-
await
|
|
1301
|
+
await removeSessionPath(paths, "metadata", "issue_prompt_rendered_requested");
|
|
1553
1302
|
},
|
|
1554
|
-
|
|
1303
|
+
issue_created: async (paths) => {
|
|
1555
1304
|
await Promise.all([
|
|
1305
|
+
removeSessionPath(paths, "metadata", "issue_created_requested"),
|
|
1556
1306
|
removeSessionRootFile(paths, "issue.md"),
|
|
1557
1307
|
removeSessionRootFile(paths, "issue_title")
|
|
1558
1308
|
]);
|
|
1559
1309
|
},
|
|
1560
|
-
|
|
1310
|
+
issue_submitted: async (paths) => {
|
|
1561
1311
|
await Promise.all([
|
|
1562
1312
|
removeSessionRootFile(paths, "issue_url"),
|
|
1563
|
-
|
|
1313
|
+
removeSessionPath(paths, "metadata", "issue_body_path"),
|
|
1314
|
+
removeSessionPath(paths, "metadata", "issue_details_path"),
|
|
1315
|
+
removeSessionPath(paths, "metadata", "issue_number"),
|
|
1316
|
+
removeSessionPath(paths, "metadata", "issue_owner"),
|
|
1317
|
+
removeSessionPath(paths, "metadata", "issue_repository"),
|
|
1318
|
+
removeSessionPath(paths, "metadata", "issue_title"),
|
|
1319
|
+
removeSessionPath(paths, "metadata", "issue_url")
|
|
1320
|
+
]);
|
|
1321
|
+
},
|
|
1322
|
+
plan_made: removePlanArtifacts,
|
|
1323
|
+
plan_executed: removePlanExecutionArtifacts,
|
|
1324
|
+
deep_ui_check_run: async (paths) => {
|
|
1325
|
+
await Promise.all([
|
|
1326
|
+
removeSessionPath(paths, "ui_checks"),
|
|
1327
|
+
removeCodexResult(paths, "deep_ui_check_run")
|
|
1328
|
+
]);
|
|
1329
|
+
},
|
|
1330
|
+
review_prompt_rendered: async (paths) => {
|
|
1331
|
+
await Promise.all([
|
|
1332
|
+
removePromptArtifact(paths, "review_prompt_rendered"),
|
|
1333
|
+
removeSessionPath(paths, "review_passes"),
|
|
1334
|
+
removeCodexResult(paths, "review_prompt_rendered")
|
|
1335
|
+
]);
|
|
1336
|
+
},
|
|
1337
|
+
review_changes_accepted: async (paths) => {
|
|
1338
|
+
await removeSessionPath(paths, "review_passes");
|
|
1339
|
+
},
|
|
1340
|
+
automated_checks_run: async (paths) => {
|
|
1341
|
+
await Promise.all([
|
|
1342
|
+
removeSessionPath(paths, "checks"),
|
|
1343
|
+
removeCodexResult(paths, "automated_checks_run")
|
|
1564
1344
|
]);
|
|
1565
1345
|
},
|
|
1566
|
-
|
|
1346
|
+
user_check_completed: async (paths) => {
|
|
1567
1347
|
await Promise.all([
|
|
1568
|
-
removePromptArtifact(paths, "
|
|
1569
|
-
|
|
1570
|
-
removeGithubCommentPurpose(paths, "issue_details"),
|
|
1571
|
-
removeIssueDetailsMetadata(paths)
|
|
1348
|
+
removePromptArtifact(paths, "user_check_completed"),
|
|
1349
|
+
removeSessionPath(paths, "steps", "user_check_failed")
|
|
1572
1350
|
]);
|
|
1573
1351
|
},
|
|
1574
|
-
plan_made: cancelAllCycleState,
|
|
1575
|
-
plan_executed: cancelAllCycleState,
|
|
1576
|
-
deep_ui_check_run: cancelAllCycleState,
|
|
1577
|
-
review_prompt_rendered: cancelAllCycleState,
|
|
1578
|
-
review_changes_accepted: cancelAllCycleState,
|
|
1579
|
-
automated_checks_run: cancelAllCycleState,
|
|
1580
|
-
user_check_completed: cancelAllCycleState,
|
|
1581
1352
|
changes_committed: async (paths) => {
|
|
1582
1353
|
await removeSessionRootFile(paths, "changes_committed.json");
|
|
1583
1354
|
},
|
|
1584
1355
|
blueprint_updated: async (paths) => {
|
|
1585
1356
|
await Promise.all([
|
|
1586
|
-
|
|
1357
|
+
removeSessionPath(paths, "metadata", "blueprint_updated_requested"),
|
|
1587
1358
|
removeSessionRootFile(paths, BLUEPRINT_BASELINE_FILE),
|
|
1588
|
-
|
|
1359
|
+
removeCodexResult(paths, "blueprint_updated")
|
|
1589
1360
|
]);
|
|
1590
1361
|
},
|
|
1591
1362
|
final_report_created: async (paths) => {
|
|
1592
1363
|
await Promise.all([
|
|
1364
|
+
removeSessionPath(paths, "metadata", "pull_request_file_requested"),
|
|
1365
|
+
removeSessionRootFile(paths, "pull_request.md"),
|
|
1366
|
+
removeSessionRootFile(paths, "final_report"),
|
|
1593
1367
|
removeSessionRootFile(paths, "final_report.md"),
|
|
1594
1368
|
removeGithubCommentPurpose(paths, "final_report")
|
|
1595
1369
|
]);
|
|
1596
1370
|
},
|
|
1597
1371
|
pr_created: async (paths) => {
|
|
1598
1372
|
await Promise.all([
|
|
1599
|
-
removePromptArtifact(paths, "pr_create_failure
|
|
1373
|
+
removePromptArtifact(paths, "pr_create_failure"),
|
|
1600
1374
|
removeSessionRootFile(paths, "pr_body.md"),
|
|
1375
|
+
removeSessionRootFile(paths, "pull_request_body.md"),
|
|
1601
1376
|
removeSessionRootFile(paths, "pr_url")
|
|
1602
1377
|
]);
|
|
1603
1378
|
},
|
|
1604
1379
|
pr_merge_prepared: async (paths) => {
|
|
1605
|
-
await removePromptArtifact(paths, "prepare_pr_merge.md");
|
|
1606
|
-
},
|
|
1607
|
-
pr_finalized: async (paths) => {
|
|
1608
1380
|
await Promise.all([
|
|
1609
|
-
removePromptArtifact(paths, "
|
|
1381
|
+
removePromptArtifact(paths, "pr_merge_prepared"),
|
|
1382
|
+
removePromptArtifact(paths, "pr_merge_failure"),
|
|
1610
1383
|
removeSessionRootFile(paths, "pr_base_branch"),
|
|
1611
1384
|
removeSessionRootFile(paths, "pr_merge_completed"),
|
|
1612
1385
|
removeSessionRootFile(paths, "pr_outcome.json")
|
|
@@ -1620,27 +1393,18 @@ const STEP_CANCELERS = Object.freeze({
|
|
|
1620
1393
|
},
|
|
1621
1394
|
session_finished: async (paths) => {
|
|
1622
1395
|
await Promise.all([
|
|
1623
|
-
removeSessionRootFile(paths, "final_comment
|
|
1396
|
+
removeSessionRootFile(paths, "final_comment")
|
|
1624
1397
|
]);
|
|
1625
1398
|
}
|
|
1626
1399
|
});
|
|
1627
1400
|
|
|
1628
|
-
function targetRequiresCycleReset(stepId) {
|
|
1629
|
-
const targetIndex = STEP_IDS.indexOf(stepId);
|
|
1630
|
-
const planIndex = STEP_IDS.indexOf(CYCLE_REWIND_TARGET_STEP_ID);
|
|
1631
|
-
return targetIndex >= 0 && targetIndex <= planIndex;
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
1401
|
function targetIsAllowedRewindStep(stepId) {
|
|
1635
1402
|
if (!STEP_IDS.includes(stepId)) {
|
|
1636
1403
|
return false;
|
|
1637
1404
|
}
|
|
1638
|
-
if (stepId === "
|
|
1405
|
+
if (stepId === "worktree_created") {
|
|
1639
1406
|
return false;
|
|
1640
1407
|
}
|
|
1641
|
-
if (CYCLE_STEP_IDS.includes(stepId)) {
|
|
1642
|
-
return stepId === CYCLE_REWIND_TARGET_STEP_ID;
|
|
1643
|
-
}
|
|
1644
1408
|
return STEP_IDS.indexOf(stepId) >= STEP_IDS.indexOf(FIRST_REWINDABLE_STEP_ID);
|
|
1645
1409
|
}
|
|
1646
1410
|
|
|
@@ -1649,30 +1413,28 @@ function deletedStepIdsForRewindTarget(stepId) {
|
|
|
1649
1413
|
return targetIndex < 0 ? [] : STEP_IDS.slice(targetIndex);
|
|
1650
1414
|
}
|
|
1651
1415
|
|
|
1652
|
-
async function
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1416
|
+
async function removeStepRecordsForDeletedSteps(paths, deletedStepIds) {
|
|
1417
|
+
const stepsRoot = path.join(paths.sessionRoot, "steps");
|
|
1418
|
+
let cycleDirectories = [];
|
|
1419
|
+
try {
|
|
1420
|
+
cycleDirectories = (await readdir(stepsRoot, { withFileTypes: true }))
|
|
1421
|
+
.filter((entry) => entry.isDirectory() && /^cycle_\d+$/u.test(entry.name))
|
|
1422
|
+
.map((entry) => entry.name);
|
|
1423
|
+
} catch {
|
|
1424
|
+
cycleDirectories = [];
|
|
1425
|
+
}
|
|
1426
|
+
await Promise.all(deletedStepIds.flatMap((stepId) => [
|
|
1427
|
+
removeSessionPath(paths, "steps", stepId),
|
|
1428
|
+
...cycleDirectories.map((cycleDirectory) => removeSessionPath(paths, "steps", cycleDirectory, stepId))
|
|
1429
|
+
]));
|
|
1656
1430
|
}
|
|
1657
1431
|
|
|
1658
|
-
async function cancelDeletedStepArtifacts(paths, deletedStepIds
|
|
1659
|
-
|
|
1660
|
-
} = {}) {
|
|
1661
|
-
const cancelerIds = cycleReset
|
|
1662
|
-
? deletedStepIds.filter((stepId) => !CYCLE_STEP_IDS.includes(stepId)).concat(CYCLE_REWIND_TARGET_STEP_ID)
|
|
1663
|
-
: deletedStepIds;
|
|
1664
|
-
const calledCycleCancel = new Set();
|
|
1665
|
-
for (const stepId of cancelerIds) {
|
|
1432
|
+
async function cancelDeletedStepArtifacts(paths, deletedStepIds) {
|
|
1433
|
+
for (const stepId of deletedStepIds) {
|
|
1666
1434
|
const canceler = STEP_CANCELERS[stepId];
|
|
1667
1435
|
if (typeof canceler !== "function") {
|
|
1668
1436
|
continue;
|
|
1669
1437
|
}
|
|
1670
|
-
if (CYCLE_STEP_IDS.includes(stepId)) {
|
|
1671
|
-
if (calledCycleCancel.has(CYCLE_REWIND_TARGET_STEP_ID)) {
|
|
1672
|
-
continue;
|
|
1673
|
-
}
|
|
1674
|
-
calledCycleCancel.add(CYCLE_REWIND_TARGET_STEP_ID);
|
|
1675
|
-
}
|
|
1676
1438
|
await canceler(paths);
|
|
1677
1439
|
}
|
|
1678
1440
|
}
|
|
@@ -1727,15 +1489,12 @@ async function rewindSession({
|
|
|
1727
1489
|
}
|
|
1728
1490
|
|
|
1729
1491
|
if (!targetIsAllowedRewindStep(normalizedStepId)) {
|
|
1730
|
-
const cycleHint = CYCLE_STEP_IDS.includes(normalizedStepId)
|
|
1731
|
-
? " Only Plan made can be used as a cycle rewind target; it resets all cycle/rework state."
|
|
1732
|
-
: "";
|
|
1733
1492
|
return buildSessionResponse(paths, {
|
|
1734
1493
|
ok: false,
|
|
1735
1494
|
errors: [
|
|
1736
1495
|
createError({
|
|
1737
1496
|
code: "rewind_step_not_allowed",
|
|
1738
|
-
message: `Cannot rewind session ${paths.sessionId} to ${normalizedStepId || "(missing)"}
|
|
1497
|
+
message: `Cannot rewind session ${paths.sessionId} to ${normalizedStepId || "(missing)"}.`
|
|
1739
1498
|
})
|
|
1740
1499
|
],
|
|
1741
1500
|
status: currentStatus
|
|
@@ -1756,12 +1515,8 @@ async function rewindSession({
|
|
|
1756
1515
|
}
|
|
1757
1516
|
|
|
1758
1517
|
const deletedStepIds = deletedStepIdsForRewindTarget(normalizedStepId);
|
|
1759
|
-
|
|
1760
|
-
await
|
|
1761
|
-
await cancelDeletedStepArtifacts(paths, deletedStepIds, { cycleReset });
|
|
1762
|
-
if (cycleReset) {
|
|
1763
|
-
await writeActiveCycle(paths, "001");
|
|
1764
|
-
}
|
|
1518
|
+
await removeStepRecordsForDeletedSteps(paths, deletedStepIds);
|
|
1519
|
+
await cancelDeletedStepArtifacts(paths, deletedStepIds);
|
|
1765
1520
|
await markCurrentStep(paths, normalizedStepId);
|
|
1766
1521
|
await markStatus(paths, SESSION_STATUS.PENDING);
|
|
1767
1522
|
return buildSessionResponse(paths, {
|
|
@@ -1872,7 +1627,7 @@ async function writeBlueprintBaseline(paths) {
|
|
|
1872
1627
|
const payload = {
|
|
1873
1628
|
changedFiles: Object.keys(snapshot).sort((left, right) => left.localeCompare(right)),
|
|
1874
1629
|
files: snapshot,
|
|
1875
|
-
recordedAt:
|
|
1630
|
+
recordedAt: timestampForStepRecord()
|
|
1876
1631
|
};
|
|
1877
1632
|
await writeTextFile(blueprintBaselinePath(paths), `${JSON.stringify(payload, null, 2)}\n`);
|
|
1878
1633
|
return payload;
|
|
@@ -1966,24 +1721,23 @@ async function renderReviewPrompt(paths) {
|
|
|
1966
1721
|
const reviewPass = await resolveReviewPassForPrompt(paths);
|
|
1967
1722
|
await writeCurrentReviewPass(paths, reviewPass);
|
|
1968
1723
|
const changedFiles = await changedFilesSinceBase(paths);
|
|
1969
|
-
const prompt = await renderPrompt(paths, "
|
|
1724
|
+
const prompt = await renderPrompt(paths, "review_prompt_rendered.md", {
|
|
1970
1725
|
changed_files: changedFiles,
|
|
1971
1726
|
review_pass_limit: String(REVIEW_PASS_LIMIT),
|
|
1972
1727
|
review_pass_number: reviewPass
|
|
1973
1728
|
});
|
|
1974
1729
|
const passRoot = reviewPassRoot(paths, reviewPass);
|
|
1975
|
-
await writePromptArtifact(paths, "
|
|
1730
|
+
await writePromptArtifact(paths, "review_prompt_rendered", prompt);
|
|
1976
1731
|
await mkdir(passRoot, { recursive: true });
|
|
1977
|
-
await writeTextFile(path.join(passRoot, "
|
|
1732
|
+
await writeTextFile(path.join(passRoot, "review_prompt_rendered"), prompt);
|
|
1978
1733
|
await writeReviewPassJson(paths, reviewPass, "prompt.json", {
|
|
1979
1734
|
changedFiles: changedFiles.split(/\r?\n/u).filter(Boolean),
|
|
1980
1735
|
maxPasses: REVIEW_PASS_LIMIT,
|
|
1981
1736
|
pass: reviewPass,
|
|
1982
|
-
promptPath: path.join(passRoot, "
|
|
1737
|
+
promptPath: path.join(passRoot, "review_prompt_rendered"),
|
|
1983
1738
|
status: "prompted",
|
|
1984
|
-
startedAt:
|
|
1739
|
+
startedAt: timestampForStepRecord()
|
|
1985
1740
|
});
|
|
1986
|
-
await writeReceipt(paths, "review_prompt_rendered", "Started code review.");
|
|
1987
1741
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
1988
1742
|
return buildSessionResponse(paths, {
|
|
1989
1743
|
codex: REVIEW_EXECUTION_CODEX_HANDOFF,
|
|
@@ -1992,13 +1746,42 @@ async function renderReviewPrompt(paths) {
|
|
|
1992
1746
|
});
|
|
1993
1747
|
}
|
|
1994
1748
|
|
|
1995
|
-
async function
|
|
1749
|
+
async function renderResolveDeslopPrompt(paths, context = {}) {
|
|
1750
|
+
const preconditions = context.preconditions || [];
|
|
1751
|
+
const reviewPass = await readCurrentReviewPass(paths);
|
|
1752
|
+
const prompt = await renderPrompt(paths, "review_changes_accepted_resolve.md", {});
|
|
1753
|
+
await writePromptArtifact(paths, "review_changes_accepted_resolve", prompt);
|
|
1754
|
+
if (reviewPass) {
|
|
1755
|
+
await writeReviewPassJson(paths, reviewPass, "resolve_prompt.json", {
|
|
1756
|
+
pass: reviewPass,
|
|
1757
|
+
promptPath: path.join(paths.sessionRoot, "prompts", "review_changes_accepted_resolve"),
|
|
1758
|
+
status: "resolve_prompted",
|
|
1759
|
+
startedAt: timestampForStepRecord()
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
1763
|
+
return buildSessionResponse(paths, {
|
|
1764
|
+
codex: RESOLVE_DESLOP_CODEX_HANDOFF,
|
|
1765
|
+
ok: true,
|
|
1766
|
+
preconditions,
|
|
1767
|
+
prompt,
|
|
1768
|
+
status: SESSION_STATUS.WAITING_FOR_USER
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
async function acceptReviewChanges(paths, options = {}, context = {}) {
|
|
1773
|
+
const resolveDeslop = options.resolveDeslop === true ||
|
|
1774
|
+
normalizeText(options["resolve-deslop"]).toLowerCase() === "true";
|
|
1775
|
+
if (resolveDeslop) {
|
|
1776
|
+
return renderResolveDeslopPrompt(paths, context);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1996
1779
|
const reviewDecisionProvided = Object.hasOwn(options, "reviewFindingsRemaining") ||
|
|
1997
1780
|
Object.hasOwn(options, "review-findings-remaining");
|
|
1998
1781
|
if (!reviewDecisionProvided) {
|
|
1999
1782
|
return failSession(paths, {
|
|
2000
1783
|
code: "review_decision_required",
|
|
2001
|
-
message: "
|
|
1784
|
+
message: "Accept review/deslop requires an explicit decision: resolve review/deslop, run review/deslop again, or continue.",
|
|
2002
1785
|
repairCommand: `jskit session ${paths.sessionId} step --review-findings-remaining false`
|
|
2003
1786
|
});
|
|
2004
1787
|
}
|
|
@@ -2017,64 +1800,36 @@ async function acceptReviewChanges(paths, options = {}) {
|
|
|
2017
1800
|
const reviewPass = await readCurrentReviewPass(paths);
|
|
2018
1801
|
const findingsRemaining = options.reviewFindingsRemaining === true ||
|
|
2019
1802
|
normalizeText(options["review-findings-remaining"]).toLowerCase() === "true";
|
|
2020
|
-
const remainingFindings = normalizeText(options.reviewFindings || options["review-findings"]);
|
|
2021
1803
|
await writeReviewPassJson(paths, reviewPass, "accepted.json", {
|
|
2022
|
-
acceptedAt:
|
|
1804
|
+
acceptedAt: timestampForStepRecord(),
|
|
2023
1805
|
changedFiles: status.changedFiles || [],
|
|
2024
1806
|
findingsRemaining,
|
|
2025
|
-
remainingFindings,
|
|
1807
|
+
remainingFindings: "",
|
|
2026
1808
|
pass: reviewPass,
|
|
2027
1809
|
status: status.changedFiles?.length ? "accepted" : "no_changes"
|
|
2028
1810
|
});
|
|
2029
|
-
await
|
|
1811
|
+
await writeStepRecord(paths, "review_changes_accepted", message);
|
|
2030
1812
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2031
1813
|
return buildSessionResponse(paths);
|
|
2032
1814
|
}
|
|
2033
1815
|
|
|
2034
1816
|
async function runAutomatedChecks(paths, {
|
|
2035
|
-
stepId
|
|
2036
|
-
|
|
2037
|
-
}, options = {}, context = {}) {
|
|
1817
|
+
stepId
|
|
1818
|
+
}, _options = {}, context = {}) {
|
|
2038
1819
|
const preconditions = context.preconditions || [];
|
|
2039
1820
|
const [command, args] = await doctorCommandForWorktree(paths.worktree);
|
|
2040
|
-
const promptFileName = `${stepId}.md`;
|
|
2041
|
-
const promptPath = path.join(paths.sessionRoot, "prompts", promptFileName);
|
|
2042
1821
|
const checksRoot = path.join(paths.sessionRoot, "checks");
|
|
2043
1822
|
await mkdir(checksRoot, { recursive: true });
|
|
2044
1823
|
const checkCommand = [command, ...args].join(" ");
|
|
2045
1824
|
|
|
2046
|
-
|
|
2047
|
-
const codexResultFailure = await requireCodexStepResult(paths, stepId, options.codexResult, preconditions);
|
|
2048
|
-
if (codexResultFailure) {
|
|
2049
|
-
return codexResultFailure;
|
|
2050
|
-
}
|
|
2051
|
-
await writeTextFile(
|
|
2052
|
-
path.join(checksRoot, `${stepId}.json`),
|
|
2053
|
-
`${JSON.stringify({
|
|
2054
|
-
command: checkCommand,
|
|
2055
|
-
ok: true,
|
|
2056
|
-
promptPath,
|
|
2057
|
-
status: "completed_by_codex",
|
|
2058
|
-
stepId
|
|
2059
|
-
}, null, 2)}\n`
|
|
2060
|
-
);
|
|
2061
|
-
await writeReceipt(paths, stepId, `${label} completed by Codex: ${checkCommand}.`);
|
|
2062
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2063
|
-
return buildSessionResponse(paths, {
|
|
2064
|
-
preconditions
|
|
2065
|
-
});
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
const prompt = await renderPrompt(paths, "automated_checks.md", {
|
|
1825
|
+
const prompt = await renderPrompt(paths, "automated_checks_run.md", {
|
|
2069
1826
|
check_command: checkCommand
|
|
2070
1827
|
});
|
|
2071
|
-
await writePromptArtifact(paths, promptFileName, prompt);
|
|
2072
1828
|
await writeTextFile(
|
|
2073
1829
|
path.join(checksRoot, `${stepId}.json`),
|
|
2074
1830
|
`${JSON.stringify({
|
|
2075
1831
|
command: checkCommand,
|
|
2076
1832
|
ok: false,
|
|
2077
|
-
promptPath,
|
|
2078
1833
|
status: "prompted",
|
|
2079
1834
|
stepId
|
|
2080
1835
|
}, null, 2)}\n`
|
|
@@ -2096,86 +1851,26 @@ async function writeUiCheckJson(paths, fileName, payload) {
|
|
|
2096
1851
|
|
|
2097
1852
|
async function runDeepUiCheck(paths, {
|
|
2098
1853
|
stepId,
|
|
2099
|
-
label,
|
|
2100
1854
|
phase
|
|
2101
|
-
},
|
|
1855
|
+
}, _options = {}, context = {}) {
|
|
2102
1856
|
const preconditions = context.preconditions || [];
|
|
2103
|
-
const issueMetadata = await readIssueMetadata(paths);
|
|
2104
|
-
const uiImpact = normalizeUiImpact(issueMetadata.uiImpact) || "unknown";
|
|
2105
|
-
const skipRequested = options.skipUiCheck === true || normalizeText(options["skip-ui-check"]).toLowerCase() === "true";
|
|
2106
|
-
const skipReason = normalizeText(options.skipReason || options["skip-reason"]);
|
|
2107
|
-
const shouldSkip = uiImpact === "none" || skipRequested;
|
|
2108
|
-
if (shouldSkip) {
|
|
2109
|
-
if (skipRequested && uiImpact !== "possible") {
|
|
2110
|
-
return failSession(paths, {
|
|
2111
|
-
code: "ui_check_skip_not_allowed",
|
|
2112
|
-
message: `Deep UI check can only be manually skipped when uiImpact is possible. Current uiImpact is ${uiImpact}.`,
|
|
2113
|
-
repairCommand: `jskit session ${paths.sessionId} step`,
|
|
2114
|
-
preconditions
|
|
2115
|
-
});
|
|
2116
|
-
}
|
|
2117
|
-
if (skipRequested && !skipReason) {
|
|
2118
|
-
return failSession(paths, {
|
|
2119
|
-
code: "ui_check_skip_reason_required",
|
|
2120
|
-
message: "Skipping a possible Deep UI check requires --skip-reason.",
|
|
2121
|
-
repairCommand: `jskit session ${paths.sessionId} step --skip-ui-check --skip-reason "<reason>"`,
|
|
2122
|
-
preconditions
|
|
2123
|
-
});
|
|
2124
|
-
}
|
|
2125
|
-
const reason = uiImpact === "none" ? "uiImpact is none." : skipReason;
|
|
2126
|
-
await writeUiCheckJson(paths, stepId, {
|
|
2127
|
-
ok: true,
|
|
2128
|
-
phase,
|
|
2129
|
-
reason,
|
|
2130
|
-
status: "skipped",
|
|
2131
|
-
stepId,
|
|
2132
|
-
uiImpact
|
|
2133
|
-
});
|
|
2134
|
-
await writeReceipt(paths, stepId, `${label} skipped: ${reason}`);
|
|
2135
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2136
|
-
return buildSessionResponse(paths, {
|
|
2137
|
-
preconditions
|
|
2138
|
-
});
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
const promptFileName = `${stepId}.md`;
|
|
2142
|
-
const promptPath = path.join(paths.sessionRoot, "prompts", promptFileName);
|
|
2143
|
-
if (await fileExists(promptPath)) {
|
|
2144
|
-
const codexResultFailure = await requireCodexStepResult(paths, stepId, options.codexResult, preconditions);
|
|
2145
|
-
if (codexResultFailure) {
|
|
2146
|
-
return codexResultFailure;
|
|
2147
|
-
}
|
|
2148
|
-
await writeReceipt(paths, stepId, `${label} completed by Codex.`);
|
|
2149
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2150
|
-
return buildSessionResponse(paths, {
|
|
2151
|
-
preconditions
|
|
2152
|
-
});
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
1857
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
2156
1858
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
2157
1859
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
2158
|
-
const
|
|
2159
|
-
const prompt = await renderPrompt(paths, "deep_ui_check.md", {
|
|
1860
|
+
const prompt = await renderPrompt(paths, "deep_ui_check_run.md", {
|
|
2160
1861
|
changed_files: await changedFilesSinceBase(paths),
|
|
2161
1862
|
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
2162
1863
|
issue_number: issueNumberFromUrl(issueUrl),
|
|
2163
1864
|
issue_title: issueTitle,
|
|
2164
1865
|
issue_url: issueUrl,
|
|
2165
1866
|
phase,
|
|
2166
|
-
issue_details_file: path.join(paths.sessionRoot, "issue_details.md"),
|
|
2167
|
-
plan_file: planPath,
|
|
2168
|
-
ui_impact: uiImpact,
|
|
2169
1867
|
worktree: paths.worktree
|
|
2170
1868
|
});
|
|
2171
|
-
await writePromptArtifact(paths, promptFileName, prompt);
|
|
2172
1869
|
await writeUiCheckJson(paths, stepId, {
|
|
2173
1870
|
ok: true,
|
|
2174
1871
|
phase,
|
|
2175
|
-
promptPath,
|
|
2176
1872
|
status: "prompted",
|
|
2177
|
-
stepId
|
|
2178
|
-
uiImpact
|
|
1873
|
+
stepId
|
|
2179
1874
|
});
|
|
2180
1875
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
2181
1876
|
return buildSessionResponse(paths, {
|
|
@@ -2189,54 +1884,30 @@ async function runDeepUiCheck(paths, {
|
|
|
2189
1884
|
async function userCheck(paths, options = {}) {
|
|
2190
1885
|
const result = normalizeText(options.userCheck || options["user-check"]).toLowerCase();
|
|
2191
1886
|
if (result === "passed" || result === "pass" || result === "ok" || result === "yes") {
|
|
2192
|
-
await
|
|
1887
|
+
await writeStepRecord(paths, "user_check_completed", "User confirmed check passed.");
|
|
2193
1888
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2194
1889
|
return buildSessionResponse(paths);
|
|
2195
1890
|
}
|
|
2196
1891
|
if (result === "failed" || result === "fail" || result === "no") {
|
|
2197
|
-
const activeCycle = await readActiveCycle(paths);
|
|
2198
|
-
await writeCycleReceipt(paths, "user_check_failed", "User reported that manual verification failed.", {
|
|
2199
|
-
cycle: activeCycle
|
|
2200
|
-
});
|
|
2201
|
-
const reworkNotes = normalizeText(options.reworkNotes || options["rework-notes"]);
|
|
2202
|
-
if (!reworkNotes) {
|
|
2203
|
-
await markStatus(paths, SESSION_STATUS.BLOCKED);
|
|
2204
|
-
return buildSessionResponse(paths, {
|
|
2205
|
-
ok: false,
|
|
2206
|
-
errors: [
|
|
2207
|
-
createError({
|
|
2208
|
-
code: "user_check_failed",
|
|
2209
|
-
message: "User check failed. Provide rework notes to start a new plan cycle.",
|
|
2210
|
-
repairCommand: `jskit session ${paths.sessionId} step --user-check failed --rework-notes -`
|
|
2211
|
-
})
|
|
2212
|
-
],
|
|
2213
|
-
status: SESSION_STATUS.BLOCKED
|
|
2214
|
-
});
|
|
2215
|
-
}
|
|
2216
|
-
const nextCycle = nextCycleNumber(activeCycle);
|
|
2217
|
-
await writeTextFile(path.join(paths.sessionRoot, "cycles", `cycle_${nextCycle}`, "rework_request.md"), `${reworkNotes}\n`);
|
|
2218
|
-
await writeActiveCycle(paths, nextCycle);
|
|
2219
|
-
await writeCycleReceipt(paths, "cycle_started", `Started rework cycle ${nextCycle}.`, {
|
|
2220
|
-
cycle: nextCycle
|
|
2221
|
-
});
|
|
2222
|
-
await markCurrentStep(paths, "plan_made");
|
|
2223
1892
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2224
|
-
return buildSessionResponse(paths
|
|
1893
|
+
return buildSessionResponse(paths, {
|
|
1894
|
+
warnings: [
|
|
1895
|
+
{
|
|
1896
|
+
code: "user_check_failed",
|
|
1897
|
+
message: "Complete user check failed. Rewind to the step that should be redone."
|
|
1898
|
+
}
|
|
1899
|
+
]
|
|
1900
|
+
});
|
|
2225
1901
|
}
|
|
2226
1902
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
2227
1903
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
2228
1904
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
2229
|
-
const
|
|
2230
|
-
const prompt = await renderPrompt(paths, "user_check.md", {
|
|
1905
|
+
const prompt = await renderPrompt(paths, "user_check_completed.md", {
|
|
2231
1906
|
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
2232
1907
|
issue_title: issueTitle,
|
|
2233
|
-
issue_url: issueUrl
|
|
2234
|
-
issue_details_file: path.join(paths.sessionRoot, "issue_details.md"),
|
|
2235
|
-
issue_details_text: await readTrimmedFile(path.join(paths.sessionRoot, "issue_details.md")),
|
|
2236
|
-
plan_file: planPath,
|
|
2237
|
-
plan_text: planText
|
|
1908
|
+
issue_url: issueUrl
|
|
2238
1909
|
});
|
|
2239
|
-
await writePromptArtifact(paths, "
|
|
1910
|
+
await writePromptArtifact(paths, "user_check_completed", prompt);
|
|
2240
1911
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
2241
1912
|
return buildSessionResponse(paths, {
|
|
2242
1913
|
prompt,
|
|
@@ -2251,6 +1922,7 @@ async function readAcceptedChangesCommit(paths) {
|
|
|
2251
1922
|
|
|
2252
1923
|
async function commitAcceptedChanges(paths, _options = {}, context = {}) {
|
|
2253
1924
|
const preconditions = context.preconditions || [];
|
|
1925
|
+
const completeStep = context.completeStep !== false;
|
|
2254
1926
|
let commitInfo = await readAcceptedChangesCommit(paths);
|
|
2255
1927
|
|
|
2256
1928
|
if (!commitInfo?.commit) {
|
|
@@ -2269,7 +1941,7 @@ async function commitAcceptedChanges(paths, _options = {}, context = {}) {
|
|
|
2269
1941
|
commitInfo = {
|
|
2270
1942
|
changedFiles: result.changedFiles || [],
|
|
2271
1943
|
commit: await currentHead(paths),
|
|
2272
|
-
committedAt:
|
|
1944
|
+
committedAt: timestampForStepRecord(),
|
|
2273
1945
|
noChanges: (result.changedFiles || []).length < 1
|
|
2274
1946
|
};
|
|
2275
1947
|
await writeTextFile(path.join(paths.sessionRoot, "changes_committed.json"), `${JSON.stringify(commitInfo, null, 2)}\n`);
|
|
@@ -2282,7 +1954,14 @@ async function commitAcceptedChanges(paths, _options = {}, context = {}) {
|
|
|
2282
1954
|
message: "No accepted worktree changes were found; continuing without a new commit."
|
|
2283
1955
|
});
|
|
2284
1956
|
}
|
|
2285
|
-
|
|
1957
|
+
if (!completeStep) {
|
|
1958
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1959
|
+
return buildSessionResponse(paths, {
|
|
1960
|
+
preconditions,
|
|
1961
|
+
warnings
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
await writeStepRecord(
|
|
2286
1965
|
paths,
|
|
2287
1966
|
"changes_committed",
|
|
2288
1967
|
commitInfo.noChanges === true
|
|
@@ -2296,23 +1975,16 @@ async function commitAcceptedChanges(paths, _options = {}, context = {}) {
|
|
|
2296
1975
|
});
|
|
2297
1976
|
}
|
|
2298
1977
|
|
|
2299
|
-
async function updateBlueprint(paths,
|
|
1978
|
+
async function updateBlueprint(paths, _options = {}, context = {}) {
|
|
2300
1979
|
const preconditions = context.preconditions || [];
|
|
2301
1980
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
2302
1981
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
2303
1982
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
2304
1983
|
const issueNumber = issueNumberFromUrl(issueUrl);
|
|
2305
|
-
const { planPath } = await readCurrentPlan(paths);
|
|
2306
|
-
const issueDetailsPath = path.join(paths.sessionRoot, "issue_details.md");
|
|
2307
|
-
const agentDecisionsPath = path.join(paths.sessionRoot, "agent_decisions.md");
|
|
2308
1984
|
const blueprintPath = path.join(paths.worktree, BLUEPRINT_RELATIVE_PATH);
|
|
2309
|
-
const
|
|
1985
|
+
const blueprintSentinelPath = path.join(paths.sessionRoot, "metadata", "blueprint_updated_requested");
|
|
2310
1986
|
|
|
2311
|
-
if (await fileExists(
|
|
2312
|
-
const codexResultFailure = await requireCodexStepResult(paths, "blueprint_updated", options.codexResult, preconditions);
|
|
2313
|
-
if (codexResultFailure) {
|
|
2314
|
-
return codexResultFailure;
|
|
2315
|
-
}
|
|
1987
|
+
if (context.completeStep !== false && await fileExists(blueprintSentinelPath)) {
|
|
2316
1988
|
const changedFiles = await changedFilesInWorktree(paths);
|
|
2317
1989
|
const unexpectedChanges = await unexpectedBlueprintStepChanges(paths, changedFiles);
|
|
2318
1990
|
if (unexpectedChanges.length > 0) {
|
|
@@ -2335,9 +2007,9 @@ async function updateBlueprint(paths, options = {}, context = {}) {
|
|
|
2335
2007
|
}
|
|
2336
2008
|
|
|
2337
2009
|
if (changedFiles.includes(BLUEPRINT_RELATIVE_PATH)) {
|
|
2338
|
-
await
|
|
2010
|
+
await writeStepRecord(paths, "blueprint_updated", "Codex updated the app blueprint; JSKIT will include it in the accepted changes commit.");
|
|
2339
2011
|
} else {
|
|
2340
|
-
await
|
|
2012
|
+
await writeStepRecord(paths, "blueprint_updated", "Codex reviewed the app blueprint; no blueprint changes were needed.");
|
|
2341
2013
|
}
|
|
2342
2014
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2343
2015
|
return buildSessionResponse(paths, {
|
|
@@ -2347,19 +2019,16 @@ async function updateBlueprint(paths, options = {}, context = {}) {
|
|
|
2347
2019
|
}
|
|
2348
2020
|
|
|
2349
2021
|
await writeBlueprintBaseline(paths);
|
|
2350
|
-
const prompt = await renderPrompt(paths, "
|
|
2351
|
-
agent_decisions_file: agentDecisionsPath,
|
|
2022
|
+
const prompt = await renderPrompt(paths, "blueprint_updated.md", {
|
|
2352
2023
|
app_blueprint_file: blueprintPath,
|
|
2353
2024
|
changed_files: await changedFilesSinceBase(paths),
|
|
2354
2025
|
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
2355
2026
|
issue_number: issueNumber,
|
|
2356
2027
|
issue_title: issueTitle,
|
|
2357
2028
|
issue_url: issueUrl,
|
|
2358
|
-
issue_details_file: issueDetailsPath,
|
|
2359
|
-
plan_file: planPath,
|
|
2360
2029
|
worktree: paths.worktree
|
|
2361
2030
|
});
|
|
2362
|
-
await
|
|
2031
|
+
await writeTextFile(blueprintSentinelPath, "true\n");
|
|
2363
2032
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
2364
2033
|
return buildSessionResponse(paths, {
|
|
2365
2034
|
codex: BLUEPRINT_CODEX_HANDOFF,
|
|
@@ -2450,13 +2119,10 @@ async function readReviewPassSummaries(paths) {
|
|
|
2450
2119
|
.join("\n");
|
|
2451
2120
|
}
|
|
2452
2121
|
|
|
2453
|
-
async function
|
|
2122
|
+
async function renderPullRequestFilePrompt(paths, context = {}) {
|
|
2454
2123
|
const preconditions = context.preconditions || [];
|
|
2455
2124
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
2456
2125
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title"));
|
|
2457
|
-
const issueDetails = await readTrimmedFile(path.join(paths.sessionRoot, "issue_details.md"));
|
|
2458
|
-
const { planText } = await readCurrentPlan(paths);
|
|
2459
|
-
const agentDecisions = await readTextIfExists(path.join(paths.sessionRoot, "agent_decisions.md"));
|
|
2460
2126
|
const filesChanged = await changedFilesSinceBase(paths);
|
|
2461
2127
|
const commits = await commitLinesSinceBase(paths);
|
|
2462
2128
|
const checks = await readCheckSummaries(paths);
|
|
@@ -2464,80 +2130,43 @@ async function createFinalReport(paths, _options = {}, context = {}) {
|
|
|
2464
2130
|
const reviewPasses = await readReviewPassSummaries(paths);
|
|
2465
2131
|
const commandLogPath = path.join(paths.sessionRoot, "command_log.jsonl");
|
|
2466
2132
|
const blueprintStatus = await readTextIfExists(path.join(paths.sessionRoot, "steps", "blueprint_updated"));
|
|
2467
|
-
const userCheck = await readTextIfExists(path.join(paths.sessionRoot, "steps",
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
"",
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
"",
|
|
2474
|
-
"
|
|
2475
|
-
"",
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
"",
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
"
|
|
2483
|
-
"",
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
"## Command Log",
|
|
2503
|
-
"",
|
|
2504
|
-
await fileExists(commandLogPath) ? commandLogPath : "No command log recorded.",
|
|
2505
|
-
"",
|
|
2506
|
-
"## User Check",
|
|
2507
|
-
"",
|
|
2508
|
-
userCheck.trim() || "No user check receipt recorded.",
|
|
2509
|
-
"",
|
|
2510
|
-
"## Blueprint",
|
|
2511
|
-
"",
|
|
2512
|
-
blueprintStatus.trim() || "No blueprint receipt recorded.",
|
|
2513
|
-
"",
|
|
2514
|
-
"## Remaining Unverified Gaps",
|
|
2515
|
-
"",
|
|
2516
|
-
"Review the check and UI check sections above; no additional gaps were recorded by JSKIT.",
|
|
2517
|
-
"",
|
|
2518
|
-
"## Decisions",
|
|
2519
|
-
"",
|
|
2520
|
-
agentDecisions.trim() || "No decision log recorded.",
|
|
2521
|
-
""
|
|
2522
|
-
].join("\n");
|
|
2523
|
-
const reportPath = path.join(paths.sessionRoot, "final_report.md");
|
|
2524
|
-
await writeTextFile(reportPath, report);
|
|
2525
|
-
if (issueUrl) {
|
|
2526
|
-
const commentResult = await commentOnIssueOnce(paths, {
|
|
2527
|
-
bodyFile: reportPath,
|
|
2528
|
-
issueUrl,
|
|
2529
|
-
purpose: "final_report"
|
|
2530
|
-
});
|
|
2531
|
-
if (!commentResult.ok) {
|
|
2532
|
-
return failSession(paths, {
|
|
2533
|
-
code: "final_report_comment_failed",
|
|
2534
|
-
message: commentResult.output || "Failed to comment final report on the GitHub issue.",
|
|
2535
|
-
repairCommand: `gh issue comment ${issueUrl} --body-file ${reportPath}`,
|
|
2536
|
-
preconditions
|
|
2537
|
-
});
|
|
2538
|
-
}
|
|
2133
|
+
const userCheck = await readTextIfExists(path.join(paths.sessionRoot, "steps", "user_check_completed")) ||
|
|
2134
|
+
await readTextIfExists(path.join(paths.sessionRoot, "steps", `cycle_${await readActiveCycle(paths)}`, "user_check_completed"));
|
|
2135
|
+
const prompt = await renderPrompt(paths, "final_report_created.md", {
|
|
2136
|
+
base_branch: await readTrimmedFile(path.join(paths.sessionRoot, "base_branch")),
|
|
2137
|
+
blueprint_status: blueprintStatus.trim() || "No blueprint update recorded.",
|
|
2138
|
+
checks: checks || "No structured checks recorded.",
|
|
2139
|
+
command_log: await fileExists(commandLogPath) ? commandLogPath : "No command log recorded.",
|
|
2140
|
+
commits: commits || "No commits detected against the session base.",
|
|
2141
|
+
files_changed: filesChanged || "No changed files detected against the session base.",
|
|
2142
|
+
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
2143
|
+
issue_title: issueTitle || paths.sessionId,
|
|
2144
|
+
issue_url: issueUrl || "",
|
|
2145
|
+
pull_request_file: path.join(paths.sessionRoot, "pull_request.md"),
|
|
2146
|
+
review_passes: reviewPasses || "No structured review passes recorded.",
|
|
2147
|
+
session_id: paths.sessionId,
|
|
2148
|
+
ui_checks: uiChecks || "No structured UI checks recorded.",
|
|
2149
|
+
user_check: userCheck.trim() || "No user check recorded.",
|
|
2150
|
+
worktree: paths.worktree
|
|
2151
|
+
});
|
|
2152
|
+
await writeTextFile(path.join(paths.sessionRoot, "metadata", "pull_request_file_requested"), "true\n");
|
|
2153
|
+
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
2154
|
+
return buildSessionResponse(paths, {
|
|
2155
|
+
codex: PR_FILE_CODEX_HANDOFF,
|
|
2156
|
+
ok: true,
|
|
2157
|
+
preconditions,
|
|
2158
|
+
prompt,
|
|
2159
|
+
status: SESSION_STATUS.WAITING_FOR_USER
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
async function createPullRequestFile(paths, _options = {}, context = {}) {
|
|
2164
|
+
const preconditions = context.preconditions || [];
|
|
2165
|
+
const pullRequestText = await readTrimmedFile(path.join(paths.sessionRoot, "pull_request.md"));
|
|
2166
|
+
if (!pullRequestText || context.completeStep === false) {
|
|
2167
|
+
return renderPullRequestFilePrompt(paths, context);
|
|
2539
2168
|
}
|
|
2540
|
-
await
|
|
2169
|
+
await writeStepRecord(paths, "final_report_created", "Pull request file is ready for review and submission.");
|
|
2541
2170
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2542
2171
|
return buildSessionResponse(paths, {
|
|
2543
2172
|
preconditions
|
|
@@ -2558,6 +2187,140 @@ function parseJsonObject(value) {
|
|
|
2558
2187
|
}
|
|
2559
2188
|
}
|
|
2560
2189
|
|
|
2190
|
+
function booleanOption(options = {}, ...names) {
|
|
2191
|
+
return names.some((name) => {
|
|
2192
|
+
const value = options[name];
|
|
2193
|
+
return value === true || normalizeText(value).toLowerCase() === "true";
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
function skipStepRequested(options = {}) {
|
|
2198
|
+
return booleanOption(options, "skipStep", "skip-step", "skip");
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
function skipStepReason(options = {}, stepId = "") {
|
|
2202
|
+
return normalizeText(options.skipReason || options["skip-reason"]) ||
|
|
2203
|
+
`User skipped ${STEP_DEFINITION_BY_ID[stepId]?.label || stepId}.`;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
async function writeJsonFile(filePath, payload) {
|
|
2207
|
+
await writeTextFile(filePath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
async function writeTextIfMissing(filePath, value) {
|
|
2211
|
+
if (await fileExists(filePath)) {
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
await writeTextFile(filePath, value);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
async function writeSkippedIssueDraft(paths, reason) {
|
|
2218
|
+
await writeTextIfMissing(path.join(paths.sessionRoot, "issue_title"), `Skipped issue draft for ${paths.sessionId}\n`);
|
|
2219
|
+
await writeTextIfMissing(
|
|
2220
|
+
path.join(paths.sessionRoot, "issue.md"),
|
|
2221
|
+
`# Skipped issue draft\n\n${reason}\n`
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
async function writeSkippedReviewPass(paths, reason) {
|
|
2226
|
+
const reviewPass = normalizeReviewPassNumber(await readTrimmedFile(path.join(paths.sessionRoot, "review_passes", "current_pass")) || "001");
|
|
2227
|
+
await writeCurrentReviewPass(paths, reviewPass);
|
|
2228
|
+
await writeReviewPassJson(paths, reviewPass, "accepted.json", {
|
|
2229
|
+
acceptedAt: timestampForStepRecord(),
|
|
2230
|
+
changedFiles: [],
|
|
2231
|
+
findingsRemaining: false,
|
|
2232
|
+
pass: reviewPass,
|
|
2233
|
+
reason,
|
|
2234
|
+
status: "skipped"
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
async function writeSkippedStepArtifacts(paths, stepId, reason) {
|
|
2239
|
+
if (stepId === "issue_created") {
|
|
2240
|
+
await writeSkippedIssueDraft(paths, reason);
|
|
2241
|
+
}
|
|
2242
|
+
if (stepId === "issue_submitted") {
|
|
2243
|
+
await writeTextIfMissing(path.join(paths.sessionRoot, "issue_url"), `skipped://${paths.sessionId}/issue\n`);
|
|
2244
|
+
}
|
|
2245
|
+
if (stepId === "deep_ui_check_run") {
|
|
2246
|
+
await writeUiCheckJson(paths, stepId, {
|
|
2247
|
+
ok: true,
|
|
2248
|
+
reason,
|
|
2249
|
+
status: "skipped",
|
|
2250
|
+
stepId
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
if (stepId === "automated_checks_run") {
|
|
2254
|
+
await mkdir(path.join(paths.sessionRoot, "checks"), { recursive: true });
|
|
2255
|
+
await writeJsonFile(path.join(paths.sessionRoot, "checks", `${stepId}.json`), {
|
|
2256
|
+
ok: true,
|
|
2257
|
+
reason,
|
|
2258
|
+
status: "skipped",
|
|
2259
|
+
stepId
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
if (stepId === "review_prompt_rendered") {
|
|
2263
|
+
await writeSkippedReviewPass(paths, reason);
|
|
2264
|
+
}
|
|
2265
|
+
if (stepId === "review_changes_accepted") {
|
|
2266
|
+
await writeSkippedReviewPass(paths, reason);
|
|
2267
|
+
}
|
|
2268
|
+
if (stepId === "changes_committed") {
|
|
2269
|
+
await writeJsonFile(path.join(paths.sessionRoot, "changes_committed.json"), {
|
|
2270
|
+
changedFiles: [],
|
|
2271
|
+
commit: await currentHead(paths),
|
|
2272
|
+
committedAt: timestampForStepRecord(),
|
|
2273
|
+
noChanges: true,
|
|
2274
|
+
reason
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
if (stepId === "final_report_created") {
|
|
2278
|
+
await writeTextIfMissing(
|
|
2279
|
+
path.join(paths.sessionRoot, "pull_request.md"),
|
|
2280
|
+
`# Pull Request: ${paths.sessionId}\n\nPull request file step skipped.\n\n${reason}\n`
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
if (stepId === "pr_created") {
|
|
2284
|
+
await writeTextIfMissing(path.join(paths.sessionRoot, "pr_url"), `skipped://${paths.sessionId}/pr\n`);
|
|
2285
|
+
}
|
|
2286
|
+
if (stepId === "pr_merge_prepared") {
|
|
2287
|
+
const prUrl = await readTrimmedFile(path.join(paths.sessionRoot, "pr_url"));
|
|
2288
|
+
await writePrOutcome(paths, {
|
|
2289
|
+
outcome: "skipped",
|
|
2290
|
+
prUrl,
|
|
2291
|
+
reason
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
if (stepId === "main_checkout_synced") {
|
|
2295
|
+
await writeMainCheckoutSync(paths, {
|
|
2296
|
+
reason,
|
|
2297
|
+
status: "skipped"
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
async function skipCurrentStep(paths, stepId, options = {}) {
|
|
2303
|
+
if (["worktree_created", "dependencies_installed", "issue_prompt_rendered", "session_finished"].includes(stepId)) {
|
|
2304
|
+
return failSession(paths, {
|
|
2305
|
+
code: "session_step_skip_not_allowed",
|
|
2306
|
+
message: `Step ${stepId} cannot be skipped.`,
|
|
2307
|
+
repairCommand: `jskit session ${paths.sessionId} step`
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
const reason = skipStepReason(options, stepId);
|
|
2311
|
+
await writeSkippedStepArtifacts(paths, stepId, reason);
|
|
2312
|
+
await writeStepRecord(paths, stepId, `Skipped: ${reason}`);
|
|
2313
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2314
|
+
return buildSessionResponse(paths, {
|
|
2315
|
+
warnings: [
|
|
2316
|
+
{
|
|
2317
|
+
code: "session_step_skipped",
|
|
2318
|
+
message: `${STEP_DEFINITION_BY_ID[stepId]?.label || stepId} was skipped.`
|
|
2319
|
+
}
|
|
2320
|
+
]
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2561
2324
|
async function readPrState(paths, prUrl) {
|
|
2562
2325
|
const prRef = normalizeText(prUrl);
|
|
2563
2326
|
const args = prRef
|
|
@@ -2661,7 +2424,7 @@ async function removeSessionWorktree(paths) {
|
|
|
2661
2424
|
|
|
2662
2425
|
async function writePrOutcome(paths, outcome) {
|
|
2663
2426
|
await writeTextFile(path.join(paths.sessionRoot, "pr_outcome.json"), `${JSON.stringify({
|
|
2664
|
-
recordedAt:
|
|
2427
|
+
recordedAt: timestampForStepRecord(),
|
|
2665
2428
|
...outcome
|
|
2666
2429
|
}, null, 2)}\n`);
|
|
2667
2430
|
}
|
|
@@ -2672,7 +2435,7 @@ function mainCheckoutSyncPath(paths) {
|
|
|
2672
2435
|
|
|
2673
2436
|
async function writeMainCheckoutSync(paths, payload = {}) {
|
|
2674
2437
|
await writeTextFile(mainCheckoutSyncPath(paths), `${JSON.stringify({
|
|
2675
|
-
recordedAt:
|
|
2438
|
+
recordedAt: timestampForStepRecord(),
|
|
2676
2439
|
...payload
|
|
2677
2440
|
}, null, 2)}\n`);
|
|
2678
2441
|
}
|
|
@@ -2738,36 +2501,33 @@ async function updateLocalBaseBranch(paths, baseBranch = "") {
|
|
|
2738
2501
|
}
|
|
2739
2502
|
|
|
2740
2503
|
async function syncMainCheckout(paths, options = {}, context = {}) {
|
|
2504
|
+
const prUrl = await readTrimmedFile(path.join(paths.sessionRoot, "pr_url"));
|
|
2741
2505
|
const prOutcome = parseJsonObject(await readTextIfExists(path.join(paths.sessionRoot, "pr_outcome.json")));
|
|
2742
2506
|
const preconditions = context.preconditions || [];
|
|
2507
|
+
const completeStep = context.completeStep !== false;
|
|
2508
|
+
if (!prUrl) {
|
|
2509
|
+
return sessionStepError(paths, {
|
|
2510
|
+
code: "pr_url_missing",
|
|
2511
|
+
message: "Cannot sync the main checkout until the GitHub pull request exists.",
|
|
2512
|
+
repairCommand: `jskit session ${paths.sessionId} create_pr_on_gh`
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2743
2515
|
if (!prOutcome?.outcome) {
|
|
2744
|
-
return
|
|
2516
|
+
return sessionStepError(paths, {
|
|
2745
2517
|
code: "pr_outcome_missing",
|
|
2746
|
-
message: "Cannot sync the main checkout before PR
|
|
2747
|
-
|
|
2748
|
-
repairCommand: `jskit session ${paths.sessionId} step`
|
|
2518
|
+
message: "Cannot sync the main checkout before the PR merge step records an outcome.",
|
|
2519
|
+
repairCommand: `jskit session ${paths.sessionId} next`
|
|
2749
2520
|
});
|
|
2750
2521
|
}
|
|
2751
2522
|
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
? skipReason
|
|
2759
|
-
: `PR outcome is ${prOutcome.outcome}; no main checkout sync is required.`;
|
|
2760
|
-
await writeMainCheckoutSync(paths, {
|
|
2761
|
-
branch: prOutcome.baseBranch || "",
|
|
2762
|
-
outcome: prOutcome.outcome,
|
|
2763
|
-
reason,
|
|
2764
|
-
status: "skipped"
|
|
2523
|
+
void options;
|
|
2524
|
+
if (prOutcome.outcome !== "merged") {
|
|
2525
|
+
return sessionStepError(paths, {
|
|
2526
|
+
code: "main_checkout_sync_unavailable",
|
|
2527
|
+
message: `Cannot sync the main checkout because the PR outcome is ${prOutcome.outcome}.`,
|
|
2528
|
+
repairCommand: `jskit session ${paths.sessionId} next`
|
|
2765
2529
|
});
|
|
2766
|
-
await writeReceipt(paths, "main_checkout_synced", `Main checkout sync skipped: ${reason}`);
|
|
2767
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2768
|
-
return buildSessionResponse(paths);
|
|
2769
2530
|
}
|
|
2770
|
-
|
|
2771
2531
|
const baseBranch = prOutcome.baseBranch || await readTrimmedFile(path.join(paths.sessionRoot, "pr_base_branch"));
|
|
2772
2532
|
const syncFailure = await updateLocalBaseBranch(paths, baseBranch);
|
|
2773
2533
|
if (syncFailure) {
|
|
@@ -2780,9 +2540,13 @@ async function syncMainCheckout(paths, options = {}, context = {}) {
|
|
|
2780
2540
|
outcome: prOutcome.outcome,
|
|
2781
2541
|
status: "synced"
|
|
2782
2542
|
});
|
|
2783
|
-
|
|
2543
|
+
if (completeStep) {
|
|
2544
|
+
await writeStepRecord(paths, "main_checkout_synced", `Fast-forwarded target checkout branch ${branch}.`);
|
|
2545
|
+
}
|
|
2784
2546
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2785
|
-
return buildSessionResponse(paths
|
|
2547
|
+
return buildSessionResponse(paths, {
|
|
2548
|
+
preconditions
|
|
2549
|
+
});
|
|
2786
2550
|
}
|
|
2787
2551
|
|
|
2788
2552
|
async function updateHelperMapBeforePr(paths) {
|
|
@@ -2866,13 +2630,35 @@ async function updateHelperMapBeforePr(paths) {
|
|
|
2866
2630
|
};
|
|
2867
2631
|
}
|
|
2868
2632
|
|
|
2869
|
-
async function createPr(paths) {
|
|
2633
|
+
async function createPr(paths, _options = {}, context = {}) {
|
|
2634
|
+
const preconditions = context.preconditions || [];
|
|
2635
|
+
const completeStep = context.completeStep !== false;
|
|
2636
|
+
const existingPrUrl = await readTrimmedFile(path.join(paths.sessionRoot, "pr_url"));
|
|
2637
|
+
if (existingPrUrl) {
|
|
2638
|
+
if (completeStep) {
|
|
2639
|
+
await writeStepRecord(paths, "pr_created", `Reused existing PR ${existingPrUrl}.`);
|
|
2640
|
+
}
|
|
2641
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2642
|
+
return buildSessionResponse(paths, {
|
|
2643
|
+
preconditions
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
const pullRequestPath = path.join(paths.sessionRoot, "pull_request.md");
|
|
2647
|
+
const pullRequestText = await readTrimmedFile(pullRequestPath);
|
|
2648
|
+
if (!pullRequestText) {
|
|
2649
|
+
return sessionStepError(paths, {
|
|
2650
|
+
code: "pull_request_file_missing",
|
|
2651
|
+
message: "Cannot create the GitHub pull request until pull_request.md exists.",
|
|
2652
|
+
repairCommand: `jskit session ${paths.sessionId} create_pull_request_file`
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2870
2655
|
const helperMapResult = await updateHelperMapBeforePr(paths);
|
|
2871
2656
|
if (!helperMapResult.ok) {
|
|
2872
2657
|
return failSession(paths, {
|
|
2873
2658
|
code: helperMapResult.code,
|
|
2874
2659
|
message: helperMapResult.message,
|
|
2875
|
-
repairCommand: helperMapResult.repairCommand
|
|
2660
|
+
repairCommand: helperMapResult.repairCommand,
|
|
2661
|
+
preconditions
|
|
2876
2662
|
});
|
|
2877
2663
|
}
|
|
2878
2664
|
|
|
@@ -2884,34 +2670,30 @@ async function createPr(paths) {
|
|
|
2884
2670
|
return failSession(paths, {
|
|
2885
2671
|
code: "branch_push_failed",
|
|
2886
2672
|
message: pushResult.output || "Failed to push session branch.",
|
|
2887
|
-
repairCommand: `git -C ${paths.worktree} push -u origin HEAD
|
|
2673
|
+
repairCommand: `git -C ${paths.worktree} push -u origin HEAD`,
|
|
2674
|
+
preconditions
|
|
2888
2675
|
});
|
|
2889
2676
|
}
|
|
2890
2677
|
const existingPrState = await readCurrentBranchPrState(paths);
|
|
2891
2678
|
if (existingPrState.ok && existingPrState.url && !prStateIsClosed(existingPrState)) {
|
|
2892
2679
|
await writeTextFile(path.join(paths.sessionRoot, "pr_url"), existingPrState.url);
|
|
2893
|
-
|
|
2680
|
+
if (completeStep) {
|
|
2681
|
+
await writeStepRecord(paths, "pr_created", `Pushed branch ${paths.branch} and reused existing PR ${existingPrState.url}. ${helperMapResult.message}`);
|
|
2682
|
+
}
|
|
2894
2683
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2895
|
-
return buildSessionResponse(paths
|
|
2684
|
+
return buildSessionResponse(paths, {
|
|
2685
|
+
preconditions
|
|
2686
|
+
});
|
|
2896
2687
|
}
|
|
2897
|
-
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
2898
2688
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
2899
2689
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
2900
|
-
const issueNumber = issueNumberFromUrl(issueUrl);
|
|
2901
|
-
const body = [
|
|
2902
|
-
issueNumber ? `Closes #${issueNumber}` : "",
|
|
2903
|
-
"",
|
|
2904
|
-
issueText
|
|
2905
|
-
].join("\n").trim();
|
|
2906
|
-
const bodyPath = path.join(paths.sessionRoot, "pr_body.md");
|
|
2907
|
-
await writeTextFile(bodyPath, body);
|
|
2908
2690
|
const result = await runLoggedCommand(paths, "github_pr_create", "gh", [
|
|
2909
2691
|
"pr",
|
|
2910
2692
|
"create",
|
|
2911
2693
|
"--title",
|
|
2912
2694
|
issueTitle,
|
|
2913
2695
|
"--body-file",
|
|
2914
|
-
|
|
2696
|
+
pullRequestPath
|
|
2915
2697
|
], {
|
|
2916
2698
|
cwd: paths.worktree,
|
|
2917
2699
|
timeout: 1000 * 60
|
|
@@ -2920,144 +2702,87 @@ async function createPr(paths) {
|
|
|
2920
2702
|
const fallbackPrState = await readCurrentBranchPrState(paths);
|
|
2921
2703
|
if (fallbackPrState.ok && fallbackPrState.url && !prStateIsClosed(fallbackPrState)) {
|
|
2922
2704
|
await writeTextFile(path.join(paths.sessionRoot, "pr_url"), fallbackPrState.url);
|
|
2923
|
-
|
|
2705
|
+
if (completeStep) {
|
|
2706
|
+
await writeStepRecord(paths, "pr_created", `Pushed branch ${paths.branch} and reused existing PR ${fallbackPrState.url}. ${helperMapResult.message}`);
|
|
2707
|
+
}
|
|
2924
2708
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2925
|
-
return buildSessionResponse(paths
|
|
2709
|
+
return buildSessionResponse(paths, {
|
|
2710
|
+
preconditions
|
|
2711
|
+
});
|
|
2926
2712
|
}
|
|
2927
2713
|
const prompt = await renderPrompt(paths, "pr_failure.md", {
|
|
2928
2714
|
doctor_output: result.output
|
|
2929
2715
|
});
|
|
2930
|
-
await writePromptArtifact(paths, "pr_create_failure
|
|
2716
|
+
await writePromptArtifact(paths, "pr_create_failure", prompt);
|
|
2931
2717
|
return failSession(paths, {
|
|
2932
2718
|
code: "pr_create_failed",
|
|
2933
2719
|
message: result.output || "Failed to create PR.",
|
|
2934
2720
|
repairCommand: "gh pr create",
|
|
2721
|
+
preconditions,
|
|
2935
2722
|
prompt
|
|
2936
2723
|
});
|
|
2937
2724
|
}
|
|
2938
2725
|
const prUrl = result.stdout.split(/\r?\n/u).map((line) => line.trim()).find(Boolean) || result.stdout;
|
|
2939
2726
|
await writeTextFile(path.join(paths.sessionRoot, "pr_url"), prUrl);
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
return buildSessionResponse(paths);
|
|
2943
|
-
}
|
|
2944
|
-
|
|
2945
|
-
async function closePrWithoutMerge(paths, prUrl, options = {}) {
|
|
2946
|
-
const reason = normalizeText(options.closeReason || options["close-reason"]) || "User skipped merge in JSKIT Studio.";
|
|
2947
|
-
const prState = await readPrState(paths, prUrl);
|
|
2948
|
-
if (!prState.ok) {
|
|
2949
|
-
return failSession(paths, {
|
|
2950
|
-
code: "pr_state_failed",
|
|
2951
|
-
message: prState.output || "Failed to inspect PR before closing without merge.",
|
|
2952
|
-
repairCommand: `gh pr view ${prUrl} --json state,mergedAt,url,baseRefName`
|
|
2953
|
-
});
|
|
2954
|
-
}
|
|
2955
|
-
if (prStateIsMerged(prState)) {
|
|
2956
|
-
return failSession(paths, {
|
|
2957
|
-
code: "pr_already_merged",
|
|
2958
|
-
message: "Cannot finish without merging because the PR is already merged.",
|
|
2959
|
-
repairCommand: `jskit session ${paths.sessionId} step`
|
|
2960
|
-
});
|
|
2961
|
-
}
|
|
2962
|
-
if (!prStateIsClosed(prState)) {
|
|
2963
|
-
const commentResult = await runLoggedCommand(paths, "github_pr_comment", "gh", ["pr", "comment", prUrl, "--body", `JSKIT session ${paths.sessionId} finished without merging. Reason: ${reason}`], {
|
|
2964
|
-
cwd: paths.targetRoot,
|
|
2965
|
-
timeout: 1000 * 60
|
|
2966
|
-
});
|
|
2967
|
-
if (!commentResult.ok) {
|
|
2968
|
-
return failSession(paths, {
|
|
2969
|
-
code: "pr_comment_failed",
|
|
2970
|
-
message: commentResult.output || "Failed to comment on PR before finishing without merge.",
|
|
2971
|
-
repairCommand: `gh pr comment ${prUrl} --body "<reason>"`
|
|
2972
|
-
});
|
|
2973
|
-
}
|
|
2727
|
+
if (completeStep) {
|
|
2728
|
+
await writeStepRecord(paths, "pr_created", `Pushed branch ${paths.branch} and created PR ${prUrl}. ${helperMapResult.message}`);
|
|
2974
2729
|
}
|
|
2975
|
-
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
2976
|
-
if (issueUrl) {
|
|
2977
|
-
await runLoggedCommand(paths, "github_issue_comment", "gh", ["issue", "comment", issueUrl, "--body", `JSKIT session ${paths.sessionId} finished without merging PR ${prUrl}. Reason: ${reason}`], {
|
|
2978
|
-
cwd: paths.targetRoot,
|
|
2979
|
-
timeout: 1000 * 60
|
|
2980
|
-
});
|
|
2981
|
-
}
|
|
2982
|
-
await writePrOutcome(paths, {
|
|
2983
|
-
issueUrl,
|
|
2984
|
-
outcome: "closed_without_merge",
|
|
2985
|
-
prUrl,
|
|
2986
|
-
prState: prState.state,
|
|
2987
|
-
reason
|
|
2988
|
-
});
|
|
2989
|
-
await writeReceipt(paths, "pr_finalized", `Finished without merging PR ${prUrl}; PR left open. Reason: ${reason}`);
|
|
2990
2730
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2991
|
-
return buildSessionResponse(paths
|
|
2731
|
+
return buildSessionResponse(paths, {
|
|
2732
|
+
preconditions
|
|
2733
|
+
});
|
|
2992
2734
|
}
|
|
2993
2735
|
|
|
2994
2736
|
async function preparePrMerge(paths, options = {}, context = {}) {
|
|
2995
2737
|
const preconditions = context.preconditions || [];
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
ok: true,
|
|
3018
|
-
preconditions,
|
|
3019
|
-
prompt,
|
|
3020
|
-
status: SESSION_STATUS.WAITING_FOR_USER
|
|
3021
|
-
});
|
|
3022
|
-
}
|
|
3023
|
-
|
|
3024
|
-
if (!continueToMerge) {
|
|
3025
|
-
return failSession(paths, {
|
|
3026
|
-
code: "pr_merge_prepare_decision_required",
|
|
3027
|
-
message: "Choose whether to ask Codex to prepare the PR for merge or continue to the merge decision.",
|
|
3028
|
-
repairCommand: `jskit session ${paths.sessionId} step --continue-to-merge true`,
|
|
3029
|
-
preconditions
|
|
3030
|
-
});
|
|
3031
|
-
}
|
|
3032
|
-
|
|
3033
|
-
await writeReceipt(paths, "pr_merge_prepared", "User continued from PR merge preparation to the merge decision.");
|
|
3034
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2738
|
+
void options;
|
|
2739
|
+
const prUrl = await readTrimmedFile(path.join(paths.sessionRoot, "pr_url"));
|
|
2740
|
+
if (!prUrl) {
|
|
2741
|
+
return sessionStepError(paths, {
|
|
2742
|
+
code: "pr_url_missing",
|
|
2743
|
+
message: "Cannot prepare the pull request for merge until the GitHub pull request exists.",
|
|
2744
|
+
repairCommand: `jskit session ${paths.sessionId} create_pr_on_gh`
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
const baseBranch = await readTrimmedFile(path.join(paths.sessionRoot, "pr_base_branch")) ||
|
|
2748
|
+
await readTrimmedFile(path.join(paths.sessionRoot, "base_branch")) ||
|
|
2749
|
+
await currentTargetBranch(paths.targetRoot);
|
|
2750
|
+
const prompt = await renderPrompt(paths, "pr_merge_prepared.md", {
|
|
2751
|
+
base_branch: baseBranch,
|
|
2752
|
+
issue_url: await readTrimmedFile(path.join(paths.sessionRoot, "issue_url")),
|
|
2753
|
+
pull_request_file: path.join(paths.sessionRoot, "pull_request.md"),
|
|
2754
|
+
pr_url: prUrl,
|
|
2755
|
+
target_root: paths.targetRoot
|
|
2756
|
+
});
|
|
2757
|
+
await writePromptArtifact(paths, "pr_merge_prepared", prompt);
|
|
2758
|
+
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
3035
2759
|
return buildSessionResponse(paths, {
|
|
3036
|
-
|
|
2760
|
+
codex: PR_MERGE_PREP_CODEX_HANDOFF,
|
|
2761
|
+
ok: true,
|
|
2762
|
+
preconditions,
|
|
2763
|
+
prompt,
|
|
2764
|
+
status: SESSION_STATUS.WAITING_FOR_USER
|
|
3037
2765
|
});
|
|
3038
2766
|
}
|
|
3039
2767
|
|
|
3040
2768
|
async function finalizePr(paths, options = {}, context = {}) {
|
|
3041
2769
|
const preconditions = context.preconditions || [];
|
|
2770
|
+
const completeStep = context.completeStep !== false;
|
|
3042
2771
|
const prUrl = await readTrimmedFile(path.join(paths.sessionRoot, "pr_url"));
|
|
3043
|
-
const closeWithoutMerge = options.closeWithoutMerge === true ||
|
|
3044
|
-
normalizeText(options["close-without-merge"]).toLowerCase() === "true" ||
|
|
3045
|
-
options.skipMerge === true ||
|
|
3046
|
-
normalizeText(options["skip-merge"]).toLowerCase() === "true";
|
|
3047
|
-
if (closeWithoutMerge) {
|
|
3048
|
-
const guardResult = await runSessionFinalizationGuard(paths, preconditions);
|
|
3049
|
-
if (!guardResult.ok) {
|
|
3050
|
-
return guardResult.response;
|
|
3051
|
-
}
|
|
3052
|
-
return closePrWithoutMerge(paths, prUrl, options);
|
|
3053
|
-
}
|
|
3054
2772
|
const mergePr = options.mergePr === true ||
|
|
3055
2773
|
normalizeText(options["merge-pr"]).toLowerCase() === "true";
|
|
2774
|
+
if (!prUrl) {
|
|
2775
|
+
return sessionStepError(paths, {
|
|
2776
|
+
code: "pr_url_missing",
|
|
2777
|
+
message: "Cannot merge the pull request until the GitHub pull request exists.",
|
|
2778
|
+
repairCommand: `jskit session ${paths.sessionId} create_pr_on_gh`
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
3056
2781
|
if (!mergePr) {
|
|
3057
2782
|
return failSession(paths, {
|
|
3058
2783
|
code: "pr_finalize_decision_required",
|
|
3059
2784
|
message: "Choose whether to merge the PR or skip merge.",
|
|
3060
|
-
repairCommand: `jskit session ${paths.sessionId}
|
|
2785
|
+
repairCommand: `jskit session ${paths.sessionId} merge_pr`,
|
|
3061
2786
|
preconditions
|
|
3062
2787
|
});
|
|
3063
2788
|
}
|
|
@@ -3092,7 +2817,7 @@ async function finalizePr(paths, options = {}, context = {}) {
|
|
|
3092
2817
|
const prompt = await renderPrompt(paths, "pr_failure.md", {
|
|
3093
2818
|
doctor_output: mergeResult?.output || existingPrState.output
|
|
3094
2819
|
});
|
|
3095
|
-
await writePromptArtifact(paths, "pr_merge_failure
|
|
2820
|
+
await writePromptArtifact(paths, "pr_merge_failure", prompt);
|
|
3096
2821
|
return failSession(paths, {
|
|
3097
2822
|
code: "pr_merge_failed",
|
|
3098
2823
|
message: mergeResult?.output || existingPrState.output || "Failed to merge PR.",
|
|
@@ -3116,9 +2841,13 @@ async function finalizePr(paths, options = {}, context = {}) {
|
|
|
3116
2841
|
});
|
|
3117
2842
|
await writeTextFile(mergeMarkerPath, `${prUrl}\n`);
|
|
3118
2843
|
}
|
|
3119
|
-
|
|
2844
|
+
if (completeStep) {
|
|
2845
|
+
await writeStepRecord(paths, "pr_merge_prepared", `Merged PR ${prUrl}.`);
|
|
2846
|
+
}
|
|
3120
2847
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3121
|
-
return buildSessionResponse(paths
|
|
2848
|
+
return buildSessionResponse(paths, {
|
|
2849
|
+
preconditions
|
|
2850
|
+
});
|
|
3122
2851
|
}
|
|
3123
2852
|
|
|
3124
2853
|
async function finishSession(paths) {
|
|
@@ -3139,14 +2868,15 @@ async function finishSession(paths) {
|
|
|
3139
2868
|
session_id: paths.sessionId,
|
|
3140
2869
|
transcript_log: path.join(paths.completedSessionRoot, "transcript.log")
|
|
3141
2870
|
});
|
|
3142
|
-
|
|
2871
|
+
const finalCommentPath = path.join(paths.sessionRoot, "final_comment");
|
|
2872
|
+
await writeTextFile(finalCommentPath, prompt);
|
|
3143
2873
|
if (issueUrl) {
|
|
3144
|
-
await runLoggedCommand(paths, "github_issue_comment", "gh", ["issue", "comment", issueUrl, "--body-file",
|
|
2874
|
+
await runLoggedCommand(paths, "github_issue_comment", "gh", ["issue", "comment", issueUrl, "--body-file", finalCommentPath], {
|
|
3145
2875
|
cwd: paths.targetRoot,
|
|
3146
2876
|
timeout: 1000 * 60
|
|
3147
2877
|
});
|
|
3148
2878
|
}
|
|
3149
|
-
await
|
|
2879
|
+
await writeStepRecord(paths, "session_finished", `Removed worktree ${paths.worktree} and finished session ${paths.sessionId} with PR outcome ${prOutcome?.outcome || "unknown"}.`);
|
|
3150
2880
|
await markStatus(paths, SESSION_STATUS.FINISHED);
|
|
3151
2881
|
await markCurrentStep(paths, "");
|
|
3152
2882
|
const archivedPaths = await archiveSession(paths, "completed");
|
|
@@ -3159,17 +2889,14 @@ const STEP_RUNNERS = Object.freeze({
|
|
|
3159
2889
|
worktree_created: createWorktree,
|
|
3160
2890
|
dependencies_installed: installDependencies,
|
|
3161
2891
|
issue_prompt_rendered: renderIssuePrompt,
|
|
3162
|
-
issue_drafted: draftIssue,
|
|
3163
2892
|
issue_created: createIssue,
|
|
3164
|
-
|
|
2893
|
+
issue_submitted: submitIssue,
|
|
3165
2894
|
plan_made: makePlan,
|
|
3166
2895
|
plan_executed: renderPlanExecutionPrompt,
|
|
3167
2896
|
automated_checks_run: (paths, options, context) => runAutomatedChecks(paths, {
|
|
3168
|
-
label: "Automated checks",
|
|
3169
2897
|
stepId: "automated_checks_run"
|
|
3170
2898
|
}, options, context),
|
|
3171
2899
|
deep_ui_check_run: (paths, options, context) => runDeepUiCheck(paths, {
|
|
3172
|
-
label: "Deep UI check",
|
|
3173
2900
|
phase: "pre_review",
|
|
3174
2901
|
stepId: "deep_ui_check_run"
|
|
3175
2902
|
}, options, context),
|
|
@@ -3178,10 +2905,9 @@ const STEP_RUNNERS = Object.freeze({
|
|
|
3178
2905
|
user_check_completed: userCheck,
|
|
3179
2906
|
changes_committed: commitAcceptedChanges,
|
|
3180
2907
|
blueprint_updated: updateBlueprint,
|
|
3181
|
-
final_report_created:
|
|
2908
|
+
final_report_created: createPullRequestFile,
|
|
3182
2909
|
pr_created: createPr,
|
|
3183
2910
|
pr_merge_prepared: preparePrMerge,
|
|
3184
|
-
pr_finalized: finalizePr,
|
|
3185
2911
|
main_checkout_synced: syncMainCheckout,
|
|
3186
2912
|
session_finished: finishSession
|
|
3187
2913
|
});
|
|
@@ -3190,21 +2916,19 @@ const PRECONDITION_RUNNERS = Object.freeze({
|
|
|
3190
2916
|
accepted_changes_committed: assertAcceptedChangesCommitted,
|
|
3191
2917
|
active_cycle_exists: assertActiveCycleExists,
|
|
3192
2918
|
active_cycle_user_check_passed: assertActiveCycleUserCheckPassed,
|
|
2919
|
+
user_check_passed: assertUserCheckPassed,
|
|
3193
2920
|
blueprint_update_satisfied: assertBlueprintUpdateSatisfied,
|
|
3194
2921
|
deep_ui_check_satisfied: assertDeepUiCheckSatisfied,
|
|
3195
2922
|
dependencies_installed: assertDependenciesInstalled,
|
|
3196
|
-
|
|
2923
|
+
pull_request_file_exists: assertPullRequestFileExists,
|
|
3197
2924
|
git_current_branch: (paths) => assertGitCurrentBranch(paths.targetRoot),
|
|
3198
2925
|
git_repository: (paths) => assertGitRepository(paths.targetRoot),
|
|
3199
2926
|
github_auth: (paths) => assertGhAuth(paths.targetRoot),
|
|
3200
2927
|
github_origin: (paths) => assertGithubOrigin(paths.targetRoot),
|
|
3201
|
-
issue_metadata_exists: assertIssueMetadataExists,
|
|
3202
2928
|
issue_text_exists: assertIssueTextExists,
|
|
3203
2929
|
issue_url_exists: assertIssueUrlExists,
|
|
3204
2930
|
automated_checks_passed: assertAutomatedChecksPassed,
|
|
3205
|
-
issue_details_exists: assertIssueDetailsExists,
|
|
3206
2931
|
main_checkout_sync_satisfied: assertMainCheckoutSyncSatisfied,
|
|
3207
|
-
plan_text_exists: assertPlanTextExists,
|
|
3208
2932
|
pr_url_exists: assertPrUrlExists,
|
|
3209
2933
|
ready_jskit_app: assertReadyJskitApp,
|
|
3210
2934
|
session_exists: assertSessionExists,
|
|
@@ -3220,10 +2944,171 @@ async function runNamedPreconditions(paths, names = []) {
|
|
|
3220
2944
|
);
|
|
3221
2945
|
}
|
|
3222
2946
|
|
|
3223
|
-
|
|
2947
|
+
function sessionStepError(paths, {
|
|
2948
|
+
code,
|
|
2949
|
+
message,
|
|
2950
|
+
repairCommand = ""
|
|
2951
|
+
} = {}) {
|
|
2952
|
+
return buildSessionResponse(paths, {
|
|
2953
|
+
ok: false,
|
|
2954
|
+
errors: [
|
|
2955
|
+
createError({
|
|
2956
|
+
code,
|
|
2957
|
+
message,
|
|
2958
|
+
repairCommand
|
|
2959
|
+
})
|
|
2960
|
+
]
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
async function createIssueFileAction(paths, options = {}, context = {}) {
|
|
2965
|
+
void options;
|
|
2966
|
+
const artifacts = await readSessionArtifacts(paths);
|
|
2967
|
+
if (artifacts.nextStep === "issue_prompt_rendered") {
|
|
2968
|
+
if (!artifacts.issueDefinitionRequested) {
|
|
2969
|
+
return sessionStepError(paths, {
|
|
2970
|
+
code: "issue_prompt_missing",
|
|
2971
|
+
message: "Cannot create the issue-file prompt until the issue-definition prompt has been created.",
|
|
2972
|
+
repairCommand: `jskit session ${paths.sessionId} define_issue --prompt "<what should change>"`
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
await writeStepRecord(paths, "issue_prompt_rendered", "Issue scoped in Codex terminal.");
|
|
2976
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2977
|
+
}
|
|
2978
|
+
return renderIssueFilePrompt(paths, context);
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
async function createGithubIssueAction(paths, options = {}, context = {}) {
|
|
2982
|
+
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
2983
|
+
if (!issueText) {
|
|
2984
|
+
return sessionStepError(paths, {
|
|
2985
|
+
code: "issue_file_missing",
|
|
2986
|
+
message: "Cannot create the GitHub issue until issue.md exists.",
|
|
2987
|
+
repairCommand: `jskit session ${paths.sessionId} create_issue_file`
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
return submitIssue(paths, options, context);
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
const STEP_ACTION_RUNNERS = Object.freeze({
|
|
2994
|
+
worktree_created: Object.freeze({
|
|
2995
|
+
create_worktree: createWorktree
|
|
2996
|
+
}),
|
|
2997
|
+
dependencies_installed: Object.freeze({
|
|
2998
|
+
run_npm_install: installDependencies
|
|
2999
|
+
}),
|
|
3000
|
+
issue_prompt_rendered: Object.freeze({
|
|
3001
|
+
create_issue_file: createIssueFileAction,
|
|
3002
|
+
define_issue: renderIssuePrompt
|
|
3003
|
+
}),
|
|
3004
|
+
issue_created: Object.freeze({
|
|
3005
|
+
create_issue_file: createIssueFileAction
|
|
3006
|
+
}),
|
|
3007
|
+
issue_submitted: Object.freeze({
|
|
3008
|
+
create_issue_on_gh: createGithubIssueAction
|
|
3009
|
+
}),
|
|
3010
|
+
plan_made: Object.freeze({
|
|
3011
|
+
make_plan: makePlan
|
|
3012
|
+
}),
|
|
3013
|
+
plan_executed: Object.freeze({
|
|
3014
|
+
execute_plan: renderPlanExecutionPrompt
|
|
3015
|
+
}),
|
|
3016
|
+
deep_ui_check_run: Object.freeze({
|
|
3017
|
+
run_deep_ui_check: (paths, options, context) => runDeepUiCheck(paths, {
|
|
3018
|
+
phase: "pre_review",
|
|
3019
|
+
stepId: "deep_ui_check_run"
|
|
3020
|
+
}, options, context)
|
|
3021
|
+
}),
|
|
3022
|
+
review_prompt_rendered: Object.freeze({
|
|
3023
|
+
resolve_deslop: (paths, _options, context) => renderResolveDeslopPrompt(paths, context)
|
|
3024
|
+
}),
|
|
3025
|
+
review_changes_accepted: Object.freeze({
|
|
3026
|
+
resolve_deslop: (paths, _options, context) => renderResolveDeslopPrompt(paths, context)
|
|
3027
|
+
}),
|
|
3028
|
+
automated_checks_run: Object.freeze({
|
|
3029
|
+
run_automated_checks: (paths, options, context) => runAutomatedChecks(paths, {
|
|
3030
|
+
stepId: "automated_checks_run"
|
|
3031
|
+
}, options, context)
|
|
3032
|
+
}),
|
|
3033
|
+
blueprint_updated: Object.freeze({
|
|
3034
|
+
update_blueprint: updateBlueprint
|
|
3035
|
+
}),
|
|
3036
|
+
changes_committed: Object.freeze({
|
|
3037
|
+
commit_changes: commitAcceptedChanges
|
|
3038
|
+
}),
|
|
3039
|
+
final_report_created: Object.freeze({
|
|
3040
|
+
create_pull_request_file: createPullRequestFile
|
|
3041
|
+
}),
|
|
3042
|
+
pr_created: Object.freeze({
|
|
3043
|
+
create_pr_on_gh: createPr
|
|
3044
|
+
}),
|
|
3045
|
+
pr_merge_prepared: Object.freeze({
|
|
3046
|
+
merge_pr: (paths, options, context) => finalizePr(paths, {
|
|
3047
|
+
...options,
|
|
3048
|
+
mergePr: true
|
|
3049
|
+
}, context),
|
|
3050
|
+
prepare_for_merge: preparePrMerge
|
|
3051
|
+
}),
|
|
3052
|
+
main_checkout_synced: Object.freeze({
|
|
3053
|
+
sync_main_checkout: syncMainCheckout
|
|
3054
|
+
}),
|
|
3055
|
+
session_finished: Object.freeze({
|
|
3056
|
+
finish_session: finishSession
|
|
3057
|
+
})
|
|
3058
|
+
});
|
|
3059
|
+
|
|
3060
|
+
async function runSessionStepAction({
|
|
3224
3061
|
targetRoot = process.cwd(),
|
|
3225
3062
|
sessionId,
|
|
3063
|
+
action,
|
|
3226
3064
|
options = {}
|
|
3065
|
+
} = {}) {
|
|
3066
|
+
return withExistingSession({ targetRoot, sessionId }, async (paths) => {
|
|
3067
|
+
const artifacts = await readSessionArtifacts(paths);
|
|
3068
|
+
if (artifacts.status === SESSION_STATUS.FINISHED || artifacts.status === SESSION_STATUS.ABANDONED) {
|
|
3069
|
+
return buildSessionResponse(paths, {
|
|
3070
|
+
ok: true,
|
|
3071
|
+
status: artifacts.status
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
if (artifacts.workflowVersion !== SESSION_WORKFLOW_VERSION) {
|
|
3075
|
+
return buildSessionResponse(paths, {
|
|
3076
|
+
ok: false,
|
|
3077
|
+
errors: [
|
|
3078
|
+
createError({
|
|
3079
|
+
code: "unsupported_workflow_version",
|
|
3080
|
+
message: `Session ${paths.sessionId} uses workflow version ${artifacts.workflowVersion || "unknown"}, but this JSKIT runtime expects ${SESSION_WORKFLOW_VERSION}.`
|
|
3081
|
+
})
|
|
3082
|
+
],
|
|
3083
|
+
status: SESSION_STATUS.BLOCKED
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
const nextStep = artifacts.nextStep;
|
|
3087
|
+
const runner = STEP_ACTION_RUNNERS[nextStep]?.[normalizeText(action)];
|
|
3088
|
+
if (typeof runner !== "function") {
|
|
3089
|
+
return sessionStepError(paths, {
|
|
3090
|
+
code: "session_action_not_available",
|
|
3091
|
+
message: `Action ${normalizeText(action) || "(missing)"} is not available while the current step is ${nextStep || "complete"}.`,
|
|
3092
|
+
repairCommand: `jskit session ${paths.sessionId}`
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
3095
|
+
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3096
|
+
if (!stepPreconditions.ok) {
|
|
3097
|
+
return sessionStepError(paths, {
|
|
3098
|
+
...stepPreconditions.error,
|
|
3099
|
+
repairCommand: stepPreconditions.error?.repairCommand || `jskit session ${paths.sessionId}`
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
return runner(paths, options, {
|
|
3103
|
+
completeStep: false,
|
|
3104
|
+
preconditions: stepPreconditions.preconditions
|
|
3105
|
+
});
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
async function advanceSessionStep({
|
|
3110
|
+
targetRoot = process.cwd(),
|
|
3111
|
+
sessionId
|
|
3227
3112
|
} = {}) {
|
|
3228
3113
|
return withExistingSession({ targetRoot, sessionId }, async (paths) => {
|
|
3229
3114
|
const artifacts = await readSessionArtifacts(paths);
|
|
@@ -3249,13 +3134,401 @@ async function runSessionStep({
|
|
|
3249
3134
|
if (!nextStep) {
|
|
3250
3135
|
return finishSession(paths);
|
|
3251
3136
|
}
|
|
3252
|
-
if (nextStep === "
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3137
|
+
if (nextStep === "worktree_created") {
|
|
3138
|
+
if (!await hasWorktree(paths)) {
|
|
3139
|
+
return sessionStepError(paths, {
|
|
3140
|
+
code: "worktree_not_created",
|
|
3141
|
+
message: "Cannot move to the next step until the session worktree exists.",
|
|
3142
|
+
repairCommand: `jskit session ${paths.sessionId} create_worktree`
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
await writeStepRecord(paths, "worktree_created", `Session worktree is ready at ${paths.worktree}.`);
|
|
3146
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3147
|
+
return buildSessionResponse(paths);
|
|
3148
|
+
}
|
|
3149
|
+
if (nextStep === "dependencies_installed") {
|
|
3150
|
+
const installResult = await readTextIfExists(path.join(paths.sessionRoot, DEPENDENCIES_INSTALL_RESULT_FILE));
|
|
3151
|
+
if (!installResult.trim()) {
|
|
3152
|
+
return sessionStepError(paths, {
|
|
3153
|
+
code: "dependencies_not_installed",
|
|
3154
|
+
message: "Cannot move to the next step until dependencies have been installed in the session worktree.",
|
|
3155
|
+
repairCommand: `jskit session ${paths.sessionId} run_npm_install`
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
return recordDependenciesInstalled(paths, {
|
|
3159
|
+
message: installResult.trim()
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
if (nextStep === "issue_prompt_rendered") {
|
|
3163
|
+
if (!artifacts.issueDefinitionRequested) {
|
|
3164
|
+
return sessionStepError(paths, {
|
|
3165
|
+
code: "issue_prompt_missing",
|
|
3166
|
+
message: "Cannot move to the next step until the issue-definition prompt has been created.",
|
|
3167
|
+
repairCommand: `jskit session ${paths.sessionId} define_issue --prompt "<what should change>"`
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
3170
|
+
if (!artifacts.issueText) {
|
|
3171
|
+
return sessionStepError(paths, {
|
|
3172
|
+
code: "issue_file_missing",
|
|
3173
|
+
message: "Cannot move to the next step until issue.md exists.",
|
|
3174
|
+
repairCommand: `jskit session ${paths.sessionId} create_issue_file`
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
await writeStepRecord(paths, "issue_prompt_rendered", "Issue scoped in Codex terminal.");
|
|
3178
|
+
await writeStepRecord(paths, "issue_created", "Issue files are ready for review and submission.");
|
|
3179
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3180
|
+
return buildSessionResponse(paths);
|
|
3181
|
+
}
|
|
3182
|
+
if (nextStep === "issue_created") {
|
|
3183
|
+
if (!artifacts.issueText) {
|
|
3184
|
+
return sessionStepError(paths, {
|
|
3185
|
+
code: "issue_file_missing",
|
|
3186
|
+
message: "Cannot move to the next step until issue.md exists.",
|
|
3187
|
+
repairCommand: `jskit session ${paths.sessionId} create_issue_file`
|
|
3188
|
+
});
|
|
3189
|
+
}
|
|
3190
|
+
await writeStepRecord(paths, "issue_created", "Issue files are ready for review and submission.");
|
|
3191
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3192
|
+
return buildSessionResponse(paths);
|
|
3193
|
+
}
|
|
3194
|
+
if (nextStep === "issue_submitted") {
|
|
3195
|
+
if (!artifacts.issueUrl) {
|
|
3196
|
+
return sessionStepError(paths, {
|
|
3197
|
+
code: "issue_url_missing",
|
|
3198
|
+
message: "Cannot move to the next step until the GitHub issue exists.",
|
|
3199
|
+
repairCommand: `jskit session ${paths.sessionId} create_issue_on_gh`
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
await writeIssueMetadataFiles(paths, {
|
|
3203
|
+
issueTitle: artifacts.issueTitle || titleFromIssue(artifacts.issueText),
|
|
3204
|
+
issueUrl: artifacts.issueUrl
|
|
3205
|
+
});
|
|
3206
|
+
await writeStepRecord(paths, "issue_submitted", `Created GitHub issue ${artifacts.issueUrl}.`);
|
|
3207
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3208
|
+
return buildSessionResponse(paths);
|
|
3209
|
+
}
|
|
3210
|
+
if (nextStep === "plan_made") {
|
|
3211
|
+
if (!artifacts.makePlanRequested) {
|
|
3212
|
+
return sessionStepError(paths, {
|
|
3213
|
+
code: "session_step_not_ready",
|
|
3214
|
+
message: "Current step plan_made is not ready to advance.",
|
|
3215
|
+
repairCommand: `jskit session ${paths.sessionId} make_plan`
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3219
|
+
if (!stepPreconditions.ok) {
|
|
3220
|
+
return sessionStepError(paths, {
|
|
3221
|
+
...stepPreconditions.error,
|
|
3222
|
+
repairCommand: stepPreconditions.error?.repairCommand || `jskit session ${paths.sessionId}`
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
return makePlan(paths, {}, {
|
|
3226
|
+
preconditions: stepPreconditions.preconditions
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
if (nextStep === "plan_executed") {
|
|
3230
|
+
if (!artifacts.executePlanRequested) {
|
|
3231
|
+
return sessionStepError(paths, {
|
|
3232
|
+
code: "session_step_not_ready",
|
|
3233
|
+
message: "Current step plan_executed is not ready to advance.",
|
|
3234
|
+
repairCommand: `jskit session ${paths.sessionId} execute_plan`
|
|
3235
|
+
});
|
|
3236
|
+
}
|
|
3237
|
+
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3238
|
+
if (!stepPreconditions.ok) {
|
|
3239
|
+
return sessionStepError(paths, {
|
|
3240
|
+
...stepPreconditions.error,
|
|
3241
|
+
repairCommand: stepPreconditions.error?.repairCommand || `jskit session ${paths.sessionId}`
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
return renderPlanExecutionPrompt(paths, {}, {
|
|
3245
|
+
preconditions: stepPreconditions.preconditions
|
|
3246
|
+
});
|
|
3247
|
+
}
|
|
3248
|
+
if (nextStep === "deep_ui_check_run") {
|
|
3249
|
+
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3250
|
+
if (!stepPreconditions.ok) {
|
|
3251
|
+
return sessionStepError(paths, {
|
|
3252
|
+
...stepPreconditions.error,
|
|
3253
|
+
repairCommand: stepPreconditions.error?.repairCommand || `jskit session ${paths.sessionId}`
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
const deepUiCheckPrompted = (artifacts.uiChecks || []).some((entry) => {
|
|
3257
|
+
return normalizeText(entry?.stepId) === "deep_ui_check_run" &&
|
|
3258
|
+
normalizeText(entry?.status) === "prompted";
|
|
3259
|
+
});
|
|
3260
|
+
await writeStepRecord(
|
|
3261
|
+
paths,
|
|
3262
|
+
"deep_ui_check_run",
|
|
3263
|
+
deepUiCheckPrompted ? "Run deep UI check completed by Codex." : "Deep UI check skipped."
|
|
3264
|
+
);
|
|
3265
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3266
|
+
return buildSessionResponse(paths, {
|
|
3267
|
+
preconditions: stepPreconditions.preconditions
|
|
3268
|
+
});
|
|
3269
|
+
}
|
|
3270
|
+
if (nextStep === "review_prompt_rendered") {
|
|
3271
|
+
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3272
|
+
if (!stepPreconditions.ok) {
|
|
3273
|
+
return sessionStepError(paths, {
|
|
3274
|
+
...stepPreconditions.error,
|
|
3275
|
+
repairCommand: stepPreconditions.error?.repairCommand || `jskit session ${paths.sessionId}`
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
const reviewPrompted = (artifacts.reviewPasses || []).some((entry) => {
|
|
3279
|
+
return normalizeText(entry?.status) === "prompted";
|
|
3280
|
+
});
|
|
3281
|
+
await writeStepRecord(
|
|
3282
|
+
paths,
|
|
3283
|
+
"review_prompt_rendered",
|
|
3284
|
+
reviewPrompted ? "Review/deslop completed by Codex." : "Review/deslop skipped."
|
|
3285
|
+
);
|
|
3286
|
+
await writeStepRecord(
|
|
3287
|
+
paths,
|
|
3288
|
+
"review_changes_accepted",
|
|
3289
|
+
reviewPrompted ? "Review/deslop accepted." : "No review/deslop pass was requested."
|
|
3290
|
+
);
|
|
3291
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3292
|
+
return buildSessionResponse(paths, {
|
|
3293
|
+
preconditions: stepPreconditions.preconditions
|
|
3257
3294
|
});
|
|
3258
3295
|
}
|
|
3296
|
+
if (nextStep === "automated_checks_run") {
|
|
3297
|
+
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3298
|
+
if (!stepPreconditions.ok) {
|
|
3299
|
+
return sessionStepError(paths, {
|
|
3300
|
+
...stepPreconditions.error,
|
|
3301
|
+
repairCommand: stepPreconditions.error?.repairCommand || `jskit session ${paths.sessionId}`
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
const [command, args] = await doctorCommandForWorktree(paths.worktree);
|
|
3305
|
+
const checkCommand = [command, ...args].join(" ");
|
|
3306
|
+
const automatedChecksPrompted = (artifacts.checks || []).some((entry) => {
|
|
3307
|
+
return normalizeText(entry?.stepId) === "automated_checks_run" &&
|
|
3308
|
+
normalizeText(entry?.status) === "prompted";
|
|
3309
|
+
});
|
|
3310
|
+
if (automatedChecksPrompted) {
|
|
3311
|
+
const checksRoot = path.join(paths.sessionRoot, "checks");
|
|
3312
|
+
await mkdir(checksRoot, { recursive: true });
|
|
3313
|
+
await writeTextFile(
|
|
3314
|
+
path.join(checksRoot, "automated_checks_run.json"),
|
|
3315
|
+
`${JSON.stringify({
|
|
3316
|
+
command: checkCommand,
|
|
3317
|
+
ok: true,
|
|
3318
|
+
status: "completed_by_codex",
|
|
3319
|
+
stepId: "automated_checks_run"
|
|
3320
|
+
}, null, 2)}\n`
|
|
3321
|
+
);
|
|
3322
|
+
}
|
|
3323
|
+
await writeStepRecord(
|
|
3324
|
+
paths,
|
|
3325
|
+
"automated_checks_run",
|
|
3326
|
+
automatedChecksPrompted ? `Run automated checks completed by Codex: ${checkCommand}.` : "Automated checks skipped."
|
|
3327
|
+
);
|
|
3328
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3329
|
+
return buildSessionResponse(paths, {
|
|
3330
|
+
preconditions: stepPreconditions.preconditions
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
if (nextStep === "blueprint_updated") {
|
|
3334
|
+
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3335
|
+
if (!stepPreconditions.ok) {
|
|
3336
|
+
return sessionStepError(paths, {
|
|
3337
|
+
...stepPreconditions.error,
|
|
3338
|
+
repairCommand: stepPreconditions.error?.repairCommand || `jskit session ${paths.sessionId}`
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
await writeStepRecord(paths, "blueprint_updated", "Blueprint update step completed.");
|
|
3342
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3343
|
+
return buildSessionResponse(paths, {
|
|
3344
|
+
preconditions: stepPreconditions.preconditions,
|
|
3345
|
+
status: SESSION_STATUS.RUNNING
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
if (nextStep === "changes_committed") {
|
|
3349
|
+
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3350
|
+
if (!stepPreconditions.ok) {
|
|
3351
|
+
return sessionStepError(paths, {
|
|
3352
|
+
...stepPreconditions.error,
|
|
3353
|
+
repairCommand: stepPreconditions.error?.repairCommand || `jskit session ${paths.sessionId}`
|
|
3354
|
+
});
|
|
3355
|
+
}
|
|
3356
|
+
const commitInfo = await readAcceptedChangesCommit(paths);
|
|
3357
|
+
if (!commitInfo?.commit) {
|
|
3358
|
+
return sessionStepError(paths, {
|
|
3359
|
+
code: "changes_not_committed",
|
|
3360
|
+
message: "Cannot move to the next step until accepted changes have been committed.",
|
|
3361
|
+
repairCommand: `jskit session ${paths.sessionId} commit_changes`
|
|
3362
|
+
});
|
|
3363
|
+
}
|
|
3364
|
+
const warnings = [];
|
|
3365
|
+
if (commitInfo.noChanges === true) {
|
|
3366
|
+
warnings.push({
|
|
3367
|
+
code: "accepted_changes_noop",
|
|
3368
|
+
message: "No accepted worktree changes were found; continuing without a new commit."
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
await writeStepRecord(
|
|
3372
|
+
paths,
|
|
3373
|
+
"changes_committed",
|
|
3374
|
+
commitInfo.noChanges === true
|
|
3375
|
+
? "No accepted worktree changes were found; continued without a new commit."
|
|
3376
|
+
: `Committed accepted changes at ${commitInfo.commit || "unknown"}.`
|
|
3377
|
+
);
|
|
3378
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3379
|
+
return buildSessionResponse(paths, {
|
|
3380
|
+
preconditions: stepPreconditions.preconditions,
|
|
3381
|
+
warnings
|
|
3382
|
+
});
|
|
3383
|
+
}
|
|
3384
|
+
if (nextStep === "final_report_created") {
|
|
3385
|
+
const pullRequestText = await readTrimmedFile(path.join(paths.sessionRoot, "pull_request.md"));
|
|
3386
|
+
if (!pullRequestText) {
|
|
3387
|
+
return sessionStepError(paths, {
|
|
3388
|
+
code: "pull_request_file_missing",
|
|
3389
|
+
message: "Cannot move to the next step until pull_request.md exists.",
|
|
3390
|
+
repairCommand: `jskit session ${paths.sessionId} create_pull_request_file`
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3394
|
+
if (!stepPreconditions.ok) {
|
|
3395
|
+
return sessionStepError(paths, {
|
|
3396
|
+
...stepPreconditions.error,
|
|
3397
|
+
repairCommand: stepPreconditions.error?.repairCommand || `jskit session ${paths.sessionId}`
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
await writeStepRecord(paths, "final_report_created", "Pull request file is ready for review and submission.");
|
|
3401
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3402
|
+
return buildSessionResponse(paths, {
|
|
3403
|
+
preconditions: stepPreconditions.preconditions
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
if (nextStep === "pr_created") {
|
|
3407
|
+
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3408
|
+
if (!stepPreconditions.ok) {
|
|
3409
|
+
return sessionStepError(paths, {
|
|
3410
|
+
...stepPreconditions.error,
|
|
3411
|
+
repairCommand: stepPreconditions.error?.repairCommand || `jskit session ${paths.sessionId}`
|
|
3412
|
+
});
|
|
3413
|
+
}
|
|
3414
|
+
await writeStepRecord(
|
|
3415
|
+
paths,
|
|
3416
|
+
"pr_created",
|
|
3417
|
+
artifacts.prUrl
|
|
3418
|
+
? `Created GitHub pull request ${artifacts.prUrl}.`
|
|
3419
|
+
: "Continued without creating a GitHub pull request."
|
|
3420
|
+
);
|
|
3421
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3422
|
+
return buildSessionResponse(paths, {
|
|
3423
|
+
preconditions: stepPreconditions.preconditions
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
if (nextStep === "pr_merge_prepared") {
|
|
3427
|
+
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3428
|
+
if (!stepPreconditions.ok) {
|
|
3429
|
+
return sessionStepError(paths, {
|
|
3430
|
+
...stepPreconditions.error,
|
|
3431
|
+
repairCommand: stepPreconditions.error?.repairCommand || `jskit session ${paths.sessionId}`
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
let prOutcome = artifacts.prOutcome || parseJsonObject(await readTextIfExists(path.join(paths.sessionRoot, "pr_outcome.json")));
|
|
3435
|
+
if (!prOutcome?.outcome) {
|
|
3436
|
+
prOutcome = {
|
|
3437
|
+
outcome: "skipped",
|
|
3438
|
+
prUrl: artifacts.prUrl || await readTrimmedFile(path.join(paths.sessionRoot, "pr_url")),
|
|
3439
|
+
reason: "User continued without merging the pull request."
|
|
3440
|
+
};
|
|
3441
|
+
await writePrOutcome(paths, prOutcome);
|
|
3442
|
+
}
|
|
3443
|
+
await writeStepRecord(
|
|
3444
|
+
paths,
|
|
3445
|
+
"pr_merge_prepared",
|
|
3446
|
+
prOutcome.outcome === "merged"
|
|
3447
|
+
? `Merged PR ${prOutcome.prUrl || artifacts.prUrl || "unknown"}.`
|
|
3448
|
+
: `Merge PR skipped: ${prOutcome.reason || `PR outcome is ${prOutcome.outcome}.`}`
|
|
3449
|
+
);
|
|
3450
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3451
|
+
return buildSessionResponse(paths, {
|
|
3452
|
+
preconditions: stepPreconditions.preconditions
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
if (nextStep === "main_checkout_synced") {
|
|
3456
|
+
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3457
|
+
if (!stepPreconditions.ok) {
|
|
3458
|
+
return sessionStepError(paths, {
|
|
3459
|
+
...stepPreconditions.error,
|
|
3460
|
+
repairCommand: stepPreconditions.error?.repairCommand || `jskit session ${paths.sessionId}`
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
let mainCheckoutSync = artifacts.mainCheckoutSync || parseJsonObject(await readTextIfExists(path.join(paths.sessionRoot, "main_checkout_sync.json")));
|
|
3464
|
+
if (!mainCheckoutSync?.status) {
|
|
3465
|
+
const prOutcome = artifacts.prOutcome || parseJsonObject(await readTextIfExists(path.join(paths.sessionRoot, "pr_outcome.json"))) || {};
|
|
3466
|
+
const reason = prOutcome.outcome === "merged"
|
|
3467
|
+
? "User skipped main checkout sync."
|
|
3468
|
+
: prOutcome.outcome
|
|
3469
|
+
? `PR outcome is ${prOutcome.outcome}; no main checkout sync is required.`
|
|
3470
|
+
: "The pull request was not merged; no main checkout sync is required.";
|
|
3471
|
+
mainCheckoutSync = {
|
|
3472
|
+
branch: prOutcome.baseBranch || "",
|
|
3473
|
+
outcome: prOutcome.outcome || "skipped",
|
|
3474
|
+
reason,
|
|
3475
|
+
status: "skipped"
|
|
3476
|
+
};
|
|
3477
|
+
await writeMainCheckoutSync(paths, mainCheckoutSync);
|
|
3478
|
+
}
|
|
3479
|
+
const message = mainCheckoutSync.status === "synced"
|
|
3480
|
+
? `Fast-forwarded target checkout branch ${mainCheckoutSync.branch || "unknown"}.`
|
|
3481
|
+
: `Main checkout sync skipped: ${mainCheckoutSync.reason || "No sync was required."}`;
|
|
3482
|
+
await writeStepRecord(paths, "main_checkout_synced", message);
|
|
3483
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3484
|
+
return buildSessionResponse(paths, {
|
|
3485
|
+
preconditions: stepPreconditions.preconditions
|
|
3486
|
+
});
|
|
3487
|
+
}
|
|
3488
|
+
if (nextStep === "session_finished") {
|
|
3489
|
+
return sessionStepError(paths, {
|
|
3490
|
+
code: "finish_session_required",
|
|
3491
|
+
message: "Use the Finish action to complete and archive the session.",
|
|
3492
|
+
repairCommand: `jskit session ${paths.sessionId} finish_session`
|
|
3493
|
+
});
|
|
3494
|
+
}
|
|
3495
|
+
return sessionStepError(paths, {
|
|
3496
|
+
code: "session_step_not_ready",
|
|
3497
|
+
message: `Current step ${nextStep} is not ready to advance.`,
|
|
3498
|
+
repairCommand: `jskit session ${paths.sessionId}`
|
|
3499
|
+
});
|
|
3500
|
+
});
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
async function runSessionStep({
|
|
3504
|
+
targetRoot = process.cwd(),
|
|
3505
|
+
sessionId,
|
|
3506
|
+
options = {}
|
|
3507
|
+
} = {}) {
|
|
3508
|
+
return withExistingSession({ targetRoot, sessionId }, async (paths) => {
|
|
3509
|
+
const artifacts = await readSessionArtifacts(paths);
|
|
3510
|
+
if (artifacts.status === SESSION_STATUS.FINISHED || artifacts.status === SESSION_STATUS.ABANDONED) {
|
|
3511
|
+
return buildSessionResponse(paths, {
|
|
3512
|
+
ok: true,
|
|
3513
|
+
status: artifacts.status
|
|
3514
|
+
});
|
|
3515
|
+
}
|
|
3516
|
+
if (artifacts.workflowVersion !== SESSION_WORKFLOW_VERSION) {
|
|
3517
|
+
return buildSessionResponse(paths, {
|
|
3518
|
+
ok: false,
|
|
3519
|
+
errors: [
|
|
3520
|
+
createError({
|
|
3521
|
+
code: "unsupported_workflow_version",
|
|
3522
|
+
message: `Session ${paths.sessionId} uses workflow version ${artifacts.workflowVersion || "unknown"}, but this JSKIT runtime expects ${SESSION_WORKFLOW_VERSION}.`
|
|
3523
|
+
})
|
|
3524
|
+
],
|
|
3525
|
+
status: SESSION_STATUS.BLOCKED
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
const nextStep = artifacts.nextStep;
|
|
3529
|
+
if (!nextStep) {
|
|
3530
|
+
return finishSession(paths);
|
|
3531
|
+
}
|
|
3259
3532
|
const runner = STEP_RUNNERS[nextStep];
|
|
3260
3533
|
if (typeof runner !== "function") {
|
|
3261
3534
|
return failSession(paths, {
|
|
@@ -3264,6 +3537,9 @@ async function runSessionStep({
|
|
|
3264
3537
|
status: SESSION_STATUS.FAILED
|
|
3265
3538
|
});
|
|
3266
3539
|
}
|
|
3540
|
+
if (skipStepRequested(options)) {
|
|
3541
|
+
return skipCurrentStep(paths, nextStep, options);
|
|
3542
|
+
}
|
|
3267
3543
|
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3268
3544
|
if (!stepPreconditions.ok) {
|
|
3269
3545
|
return failSession(paths, {
|
|
@@ -3271,7 +3547,6 @@ async function runSessionStep({
|
|
|
3271
3547
|
preconditions: stepPreconditions.preconditions
|
|
3272
3548
|
});
|
|
3273
3549
|
}
|
|
3274
|
-
await appendAgentDecisionsInput(paths, options);
|
|
3275
3550
|
return runner(paths, options, {
|
|
3276
3551
|
preconditions: stepPreconditions.preconditions
|
|
3277
3552
|
});
|
|
@@ -3312,7 +3587,7 @@ async function abandonSession({
|
|
|
3312
3587
|
}
|
|
3313
3588
|
await writeTextFile(
|
|
3314
3589
|
path.join(paths.sessionRoot, "steps", "abandoned"),
|
|
3315
|
-
`${
|
|
3590
|
+
`${timestampForStepRecord()}\nAbandoned session ${paths.sessionId}.`
|
|
3316
3591
|
);
|
|
3317
3592
|
await markStatus(paths, SESSION_STATUS.ABANDONED);
|
|
3318
3593
|
await markCurrentStep(paths, "");
|
|
@@ -3361,6 +3636,7 @@ export {
|
|
|
3361
3636
|
STEP_IDS,
|
|
3362
3637
|
STEP_PRECONDITION_NAMES,
|
|
3363
3638
|
abandonSession,
|
|
3639
|
+
advanceSessionStep,
|
|
3364
3640
|
adoptDependenciesInstalled,
|
|
3365
3641
|
adoptCodexThreadId,
|
|
3366
3642
|
buildSessionResponse,
|
|
@@ -3369,8 +3645,6 @@ export {
|
|
|
3369
3645
|
createSessionId,
|
|
3370
3646
|
extractIssueTitle,
|
|
3371
3647
|
extractIssueText,
|
|
3372
|
-
extractIssueDetails,
|
|
3373
|
-
extractPlanText,
|
|
3374
3648
|
inspectSession,
|
|
3375
3649
|
inspectSessionDiff,
|
|
3376
3650
|
inspectSessionDetails,
|
|
@@ -3380,5 +3654,6 @@ export {
|
|
|
3380
3654
|
recordDependenciesInstalled,
|
|
3381
3655
|
rewindSession,
|
|
3382
3656
|
resolveSessionPaths,
|
|
3383
|
-
runSessionStep
|
|
3657
|
+
runSessionStep,
|
|
3658
|
+
runSessionStepAction
|
|
3384
3659
|
};
|