@jskit-ai/jskit-cli 0.2.88 → 0.2.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +130 -353
- package/src/server/sessionRuntime/io.js +2 -2
- package/src/server/sessionRuntime/preconditions.js +40 -144
- 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 +504 -295
- package/src/server/sessionRuntime.js +1300 -961
- package/src/server/sessionRuntime/prompts/issue_details.md +0 -49
- package/src/server/sessionRuntime/prompts/new_issue.md +0 -46
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHash
|
|
3
|
+
} from "node:crypto";
|
|
1
4
|
import {
|
|
2
5
|
appendFile,
|
|
3
6
|
mkdir,
|
|
@@ -10,13 +13,17 @@ import path from "node:path";
|
|
|
10
13
|
import {
|
|
11
14
|
BLUEPRINT_CODEX_HANDOFF,
|
|
12
15
|
AUTOMATED_CHECK_REPAIR_CODEX_HANDOFF,
|
|
13
|
-
|
|
16
|
+
DEPENDENCIES_INSTALL_RESULT_FILE,
|
|
14
17
|
DEEP_UI_CHECK_CODEX_HANDOFF,
|
|
15
|
-
|
|
18
|
+
ISSUE_DEFINITION_CODEX_HANDOFF,
|
|
19
|
+
ISSUE_FILE_CODEX_HANDOFF,
|
|
20
|
+
PLAN_CODEX_HANDOFF,
|
|
16
21
|
PLAN_EXECUTION_CODEX_HANDOFF,
|
|
22
|
+
PR_FILE_CODEX_HANDOFF,
|
|
17
23
|
PR_MERGE_PREP_CODEX_HANDOFF,
|
|
18
24
|
REVIEW_PASS_LIMIT,
|
|
19
25
|
REVIEW_EXECUTION_CODEX_HANDOFF,
|
|
26
|
+
RESOLVE_DESLOP_CODEX_HANDOFF,
|
|
20
27
|
SESSION_STATUS,
|
|
21
28
|
SESSION_WORKFLOW_VERSION,
|
|
22
29
|
STEP_DEFINITION_BY_ID,
|
|
@@ -32,7 +39,7 @@ import {
|
|
|
32
39
|
runCommand,
|
|
33
40
|
runGit,
|
|
34
41
|
runGitInWorktree,
|
|
35
|
-
|
|
42
|
+
timestampForStepRecord,
|
|
36
43
|
writeTextFile
|
|
37
44
|
} from "./sessionRuntime/io.js";
|
|
38
45
|
import {
|
|
@@ -54,13 +61,11 @@ import {
|
|
|
54
61
|
markStatus,
|
|
55
62
|
normalizeReviewPassNumber,
|
|
56
63
|
readActiveCycle,
|
|
57
|
-
|
|
64
|
+
readStepRecords,
|
|
58
65
|
readReviewPasses,
|
|
59
66
|
readSessionArtifacts,
|
|
60
67
|
reviewPassRoot,
|
|
61
|
-
|
|
62
|
-
writeCycleReceipt,
|
|
63
|
-
writeReceipt
|
|
68
|
+
writeStepRecord
|
|
64
69
|
} from "./sessionRuntime/responses.js";
|
|
65
70
|
import {
|
|
66
71
|
applyPreconditions,
|
|
@@ -70,22 +75,20 @@ import {
|
|
|
70
75
|
assertBlueprintUpdateSatisfied,
|
|
71
76
|
assertDeepUiCheckSatisfied,
|
|
72
77
|
assertDependenciesInstalled,
|
|
73
|
-
assertFinalReportExists,
|
|
74
78
|
assertGhAuth,
|
|
75
79
|
assertGitCurrentBranch,
|
|
76
80
|
assertGitRepository,
|
|
77
81
|
assertGithubOrigin,
|
|
78
|
-
assertIssueMetadataExists,
|
|
79
82
|
assertIssueTextExists,
|
|
80
83
|
assertIssueUrlExists,
|
|
81
84
|
assertAutomatedChecksPassed,
|
|
82
|
-
assertIssueDetailsExists,
|
|
83
85
|
assertMainCheckoutSyncSatisfied,
|
|
84
|
-
assertPlanTextExists,
|
|
85
86
|
assertPrUrlExists,
|
|
87
|
+
assertPullRequestFileExists,
|
|
86
88
|
assertReadyJskitApp,
|
|
87
89
|
assertSessionExists,
|
|
88
90
|
assertTargetRootWritable,
|
|
91
|
+
assertUserCheckPassed,
|
|
89
92
|
assertWorktreeExists,
|
|
90
93
|
ensureStudioGitExclude,
|
|
91
94
|
hasWorktree
|
|
@@ -173,15 +176,6 @@ function extractMarkedText(value = "", marker = "") {
|
|
|
173
176
|
return normalizeText(matches.length > 0 ? matches[matches.length - 1][1] : "");
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
function extractMarkedField(value = "", fieldName = "") {
|
|
177
|
-
const normalizedFieldName = normalizeText(fieldName);
|
|
178
|
-
if (!normalizedFieldName) {
|
|
179
|
-
return "";
|
|
180
|
-
}
|
|
181
|
-
const pattern = new RegExp(`^\\s*${normalizedFieldName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}\\s*:\\s*(.+)$`, "imu");
|
|
182
|
-
return normalizeText(pattern.exec(normalizeText(value))?.[1] || "");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
179
|
function extractIssueTitle(value = "") {
|
|
186
180
|
return extractMarkedText(value, "issue_title");
|
|
187
181
|
}
|
|
@@ -190,100 +184,10 @@ function extractIssueText(value = "") {
|
|
|
190
184
|
return extractMarkedText(value, "issue_text") || normalizeText(value);
|
|
191
185
|
}
|
|
192
186
|
|
|
193
|
-
function extractPlanText(value = "") {
|
|
194
|
-
return extractMarkedText(value, "plan") || normalizeText(value);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function extractIssueDetails(value = "") {
|
|
198
|
-
return extractMarkedText(value, "issue_details");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function extractIssueCategory(value = "") {
|
|
202
|
-
return extractMarkedText(value, "issue_category");
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function extractUiImpact(value = "") {
|
|
206
|
-
return extractMarkedText(value, "ui_impact");
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function extractAgentDecisions(value = "") {
|
|
210
|
-
return extractMarkedText(value, "agent_decisions");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function normalizeIssueCategory(value = "") {
|
|
214
|
-
const category = normalizeText(value).toLowerCase();
|
|
215
|
-
return ["client", "server", "client_server", "tooling", "unknown"].includes(category)
|
|
216
|
-
? category
|
|
217
|
-
: "";
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function normalizeUiImpact(value = "") {
|
|
221
|
-
const impact = normalizeText(value).toLowerCase();
|
|
222
|
-
return ["none", "possible", "definite", "unknown"].includes(impact)
|
|
223
|
-
? impact
|
|
224
|
-
: "";
|
|
225
|
-
}
|
|
226
|
-
|
|
227
187
|
async function writePromptArtifact(paths, fileName, prompt) {
|
|
228
188
|
await writeTextFile(path.join(paths.sessionRoot, "prompts", fileName), prompt);
|
|
229
189
|
}
|
|
230
190
|
|
|
231
|
-
async function codexResultPath(paths, stepId) {
|
|
232
|
-
if (CYCLE_STEP_IDS.includes(stepId)) {
|
|
233
|
-
const activeCycle = await readActiveCycle(paths);
|
|
234
|
-
return path.join(cycleRootPath(paths, activeCycle), "codex_results", `${stepId}.md`);
|
|
235
|
-
}
|
|
236
|
-
return path.join(paths.sessionRoot, "codex_results", `${stepId}.md`);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function codexResponseContractForStep(stepId) {
|
|
240
|
-
const contract = STEP_DEFINITION_BY_ID[stepId]?.codex?.responseContract;
|
|
241
|
-
return contract && typeof contract === "object" && !Array.isArray(contract) ? contract : null;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async function requireCodexStepResult(paths, stepId, result, preconditions = [], contractOverride = null) {
|
|
245
|
-
const contract = contractOverride || codexResponseContractForStep(stepId);
|
|
246
|
-
if (contract?.required !== true || !contract.marker) {
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const source = String(result || "");
|
|
251
|
-
if (!source.trim()) {
|
|
252
|
-
return failSession(paths, {
|
|
253
|
-
code: "codex_result_required",
|
|
254
|
-
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.`,
|
|
255
|
-
repairCommand: `jskit session ${paths.sessionId} step --codex-result -`,
|
|
256
|
-
preconditions
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const markedResult = extractMarkedText(source, contract.marker);
|
|
261
|
-
if (!markedResult) {
|
|
262
|
-
return failSession(paths, {
|
|
263
|
-
code: "codex_result_marker_missing",
|
|
264
|
-
message: `Codex output for ${STEP_DEFINITION_BY_ID[stepId]?.label || stepId} must include [${contract.marker}]... [/${contract.marker}] before JSKIT can advance.`,
|
|
265
|
-
repairCommand: `jskit session ${paths.sessionId} step --codex-result -`,
|
|
266
|
-
preconditions
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
const stepField = normalizeText(contract.stepField);
|
|
270
|
-
if (stepField) {
|
|
271
|
-
const resultStep = extractMarkedField(markedResult, stepField);
|
|
272
|
-
if (resultStep !== stepId) {
|
|
273
|
-
return failSession(paths, {
|
|
274
|
-
code: "codex_result_step_mismatch",
|
|
275
|
-
message: `Codex output for ${STEP_DEFINITION_BY_ID[stepId]?.label || stepId} must include ${stepField}: ${stepId} inside [${contract.marker}] before JSKIT can advance.`,
|
|
276
|
-
repairCommand: `jskit session ${paths.sessionId} step --codex-result -`,
|
|
277
|
-
preconditions
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
await writeTextFile(await codexResultPath(paths, stepId), source);
|
|
283
|
-
await appendAgentDecisions(paths, extractAgentDecisions(source));
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
191
|
function commandText(command, args = []) {
|
|
288
192
|
return [command, ...args].map((part) => {
|
|
289
193
|
const value = String(part || "");
|
|
@@ -297,28 +201,6 @@ function cycleRootPath(paths, cycle) {
|
|
|
297
201
|
return path.join(paths.sessionRoot, "cycles", `cycle_${cycle}`);
|
|
298
202
|
}
|
|
299
203
|
|
|
300
|
-
function cyclePlanPath(paths, cycle) {
|
|
301
|
-
return path.join(cycleRootPath(paths, cycle), "plan.md");
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function cyclePlanPromptFileName(cycle) {
|
|
305
|
-
return `cycle_${cycle}_plan_request.md`;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function cyclePlanExecutionPromptFileName(cycle) {
|
|
309
|
-
return `cycle_${cycle}_plan_execution.md`;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async function readCurrentPlan(paths) {
|
|
313
|
-
const activeCycle = await readActiveCycle(paths);
|
|
314
|
-
const planPath = cyclePlanPath(paths, activeCycle);
|
|
315
|
-
return {
|
|
316
|
-
activeCycle,
|
|
317
|
-
planPath,
|
|
318
|
-
planText: await readTrimmedFile(planPath)
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
204
|
function commandOutputSummary(output = "") {
|
|
323
205
|
const normalized = normalizeText(output);
|
|
324
206
|
if (normalized.length <= 1800) {
|
|
@@ -338,7 +220,7 @@ async function appendCommandLog(paths, {
|
|
|
338
220
|
return;
|
|
339
221
|
}
|
|
340
222
|
const entry = {
|
|
341
|
-
at:
|
|
223
|
+
at: timestampForStepRecord(),
|
|
342
224
|
command: commandText(command, args),
|
|
343
225
|
cwd,
|
|
344
226
|
exitCode: Number.isInteger(result.exitCode) ? result.exitCode : null,
|
|
@@ -417,14 +299,14 @@ function packageScriptRepairCommand(paths, command, args) {
|
|
|
417
299
|
return `cd ${paths.worktree} && ${command} ${args.join(" ")}`;
|
|
418
300
|
}
|
|
419
301
|
|
|
420
|
-
function
|
|
302
|
+
function packageScriptRecordName(scriptName) {
|
|
421
303
|
return normalizeText(scriptName).replace(/[^a-zA-Z0-9._-]+/gu, "_");
|
|
422
304
|
}
|
|
423
305
|
|
|
424
|
-
async function
|
|
306
|
+
async function writeSessionHookRecord(paths, scriptName, message) {
|
|
425
307
|
await writeTextFile(
|
|
426
|
-
path.join(paths.sessionRoot, "hooks",
|
|
427
|
-
`${
|
|
308
|
+
path.join(paths.sessionRoot, "hooks", packageScriptRecordName(scriptName)),
|
|
309
|
+
`${timestampForStepRecord()}\n${normalizeText(message) || `${scriptName} completed.`}`
|
|
428
310
|
);
|
|
429
311
|
}
|
|
430
312
|
|
|
@@ -462,7 +344,7 @@ async function runOptionalSessionPackageScript(paths, {
|
|
|
462
344
|
})
|
|
463
345
|
};
|
|
464
346
|
}
|
|
465
|
-
await
|
|
347
|
+
await writeSessionHookRecord(paths, scriptName, result.output || `${scriptName} completed.`);
|
|
466
348
|
return {
|
|
467
349
|
ok: true,
|
|
468
350
|
ran: true,
|
|
@@ -494,29 +376,6 @@ async function runSessionProvisioningHook(paths, {
|
|
|
494
376
|
});
|
|
495
377
|
}
|
|
496
378
|
|
|
497
|
-
async function readIssueMetadata(paths) {
|
|
498
|
-
const source = await readTextIfExists(path.join(paths.sessionRoot, "issue_metadata.json"));
|
|
499
|
-
if (!source) {
|
|
500
|
-
return {};
|
|
501
|
-
}
|
|
502
|
-
try {
|
|
503
|
-
const parsed = JSON.parse(source);
|
|
504
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
505
|
-
} catch {
|
|
506
|
-
return {};
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
async function writeIssueMetadata(paths, metadata = {}) {
|
|
511
|
-
const existing = await readIssueMetadata(paths);
|
|
512
|
-
const next = {
|
|
513
|
-
...existing,
|
|
514
|
-
...metadata
|
|
515
|
-
};
|
|
516
|
-
await writeTextFile(path.join(paths.sessionRoot, "issue_metadata.json"), `${JSON.stringify(next, null, 2)}\n`);
|
|
517
|
-
return next;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
379
|
async function readGithubComments(paths) {
|
|
521
380
|
const source = await readTextIfExists(path.join(paths.sessionRoot, "github_comments.json"));
|
|
522
381
|
if (!source) {
|
|
@@ -561,7 +420,7 @@ async function commentOnIssueOnce(paths, {
|
|
|
561
420
|
}
|
|
562
421
|
comments[normalizedPurpose] = {
|
|
563
422
|
bodyFile,
|
|
564
|
-
commentedAt:
|
|
423
|
+
commentedAt: timestampForStepRecord(),
|
|
565
424
|
issueUrl,
|
|
566
425
|
purpose: normalizedPurpose
|
|
567
426
|
};
|
|
@@ -572,61 +431,42 @@ async function commentOnIssueOnce(paths, {
|
|
|
572
431
|
};
|
|
573
432
|
}
|
|
574
433
|
|
|
575
|
-
async function appendAgentDecisions(paths, decisions = "") {
|
|
576
|
-
const normalized = normalizeText(decisions);
|
|
577
|
-
if (!normalized) {
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
const decisionsPath = path.join(paths.sessionRoot, "agent_decisions.md");
|
|
581
|
-
const existing = await readTextIfExists(decisionsPath);
|
|
582
|
-
await writeTextFile(
|
|
583
|
-
decisionsPath,
|
|
584
|
-
`${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${normalized}\n`
|
|
585
|
-
);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
async function appendAgentDecisionsInput(paths, options = {}) {
|
|
589
|
-
const source = normalizeText(options.agentDecisions || options["agent-decisions"]);
|
|
590
|
-
if (!source) {
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
await appendAgentDecisions(paths, extractAgentDecisions(source) || source);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
async function recordIssueInAgentDecisions(paths, issueUrl = "") {
|
|
597
|
-
const normalizedIssueUrl = normalizeText(issueUrl);
|
|
598
|
-
if (!normalizedIssueUrl) {
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
const decisionsPath = path.join(paths.sessionRoot, "agent_decisions.md");
|
|
602
|
-
const existing = await readTextIfExists(decisionsPath);
|
|
603
|
-
if (existing.includes(`Issue: ${normalizedIssueUrl}`)) {
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
await writeTextFile(
|
|
607
|
-
decisionsPath,
|
|
608
|
-
`${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}Issue: ${normalizedIssueUrl}\n\n`
|
|
609
|
-
);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
434
|
function issueMetadataFromUrl(issueUrl = "") {
|
|
613
|
-
const match = /^https:\/\/github\.com\/([
|
|
435
|
+
const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)(?:\b|$)/u.exec(String(issueUrl || "").trim());
|
|
614
436
|
if (!match) {
|
|
615
437
|
return {
|
|
616
438
|
issueNumber: "",
|
|
439
|
+
issueUrl: normalizeText(issueUrl),
|
|
617
440
|
owner: "",
|
|
618
441
|
repository: ""
|
|
619
442
|
};
|
|
620
443
|
}
|
|
621
444
|
return {
|
|
622
445
|
issueNumber: match[3],
|
|
446
|
+
issueUrl: normalizeText(issueUrl),
|
|
623
447
|
owner: match[1],
|
|
624
448
|
repository: match[2]
|
|
625
449
|
};
|
|
626
450
|
}
|
|
627
451
|
|
|
628
|
-
function
|
|
629
|
-
|
|
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
|
+
);
|
|
630
470
|
}
|
|
631
471
|
|
|
632
472
|
async function createSession({
|
|
@@ -664,11 +504,9 @@ async function createSession({
|
|
|
664
504
|
await ensureStudioGitExclude(initialPaths.targetRoot);
|
|
665
505
|
await mkdir(initialPaths.sessionRoot, { recursive: true });
|
|
666
506
|
await writeTextFile(path.join(initialPaths.sessionRoot, "transcript.log"), "");
|
|
667
|
-
await writeTextFile(path.join(initialPaths.sessionRoot, "agent_decisions.md"), `# Agent Decisions\n\nSession: ${initialPaths.sessionId}\nCreated: ${now.toISOString()}\n\n`);
|
|
668
507
|
await writeTextFile(path.join(initialPaths.sessionRoot, "workflow_version"), `${SESSION_WORKFLOW_VERSION}\n`);
|
|
669
|
-
await writeActiveCycle(initialPaths, "001");
|
|
670
508
|
await markStatus(initialPaths, SESSION_STATUS.PENDING);
|
|
671
|
-
await
|
|
509
|
+
await markCurrentStep(initialPaths, "worktree_created");
|
|
672
510
|
|
|
673
511
|
return buildSessionResponse(initialPaths, {
|
|
674
512
|
ok: true,
|
|
@@ -759,8 +597,7 @@ function emptySessionDetails(response) {
|
|
|
759
597
|
...response,
|
|
760
598
|
issueTitle: "",
|
|
761
599
|
issueText: "",
|
|
762
|
-
|
|
763
|
-
receipts: [],
|
|
600
|
+
stepRecords: [],
|
|
764
601
|
transcriptLog: ""
|
|
765
602
|
};
|
|
766
603
|
}
|
|
@@ -775,12 +612,10 @@ async function inspectSessionDetails({
|
|
|
775
612
|
}
|
|
776
613
|
const { paths, preconditions } = context;
|
|
777
614
|
const response = await buildSessionResponse(paths, { preconditions });
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
const [issueText, issueTitle, receipts, transcriptLog] = await Promise.all([
|
|
615
|
+
const [issueText, issueTitle, stepRecords, transcriptLog] = await Promise.all([
|
|
781
616
|
readTextIfExists(path.join(paths.sessionRoot, "issue.md")),
|
|
782
617
|
readTrimmedFile(path.join(paths.sessionRoot, "issue_title")),
|
|
783
|
-
|
|
618
|
+
readStepRecords(paths),
|
|
784
619
|
readTextIfExists(path.join(paths.sessionRoot, "transcript.log"))
|
|
785
620
|
]);
|
|
786
621
|
|
|
@@ -788,8 +623,7 @@ async function inspectSessionDetails({
|
|
|
788
623
|
...response,
|
|
789
624
|
issueTitle,
|
|
790
625
|
issueText: issueText.trim(),
|
|
791
|
-
|
|
792
|
-
receipts,
|
|
626
|
+
stepRecords,
|
|
793
627
|
transcriptLog
|
|
794
628
|
};
|
|
795
629
|
}
|
|
@@ -822,6 +656,7 @@ async function removeEmptyStaleWorktreeDirectory(paths) {
|
|
|
822
656
|
|
|
823
657
|
async function createWorktree(paths, _options = {}, context = {}) {
|
|
824
658
|
const preconditions = context.preconditions || [];
|
|
659
|
+
const completeStep = context.completeStep !== false;
|
|
825
660
|
const [baseBranchResult, baseCommitResult] = await Promise.all([
|
|
826
661
|
runGit(paths.targetRoot, ["branch", "--show-current"], { timeout: 15000 }),
|
|
827
662
|
runGit(paths.targetRoot, ["rev-parse", "--verify", "HEAD"], { timeout: 15000 })
|
|
@@ -835,7 +670,9 @@ async function createWorktree(paths, _options = {}, context = {}) {
|
|
|
835
670
|
if (baseCommit && !await readTrimmedFile(path.join(paths.sessionRoot, "base_commit"))) {
|
|
836
671
|
await writeTextFile(path.join(paths.sessionRoot, "base_commit"), `${baseCommit}\n`);
|
|
837
672
|
}
|
|
838
|
-
|
|
673
|
+
if (completeStep) {
|
|
674
|
+
await writeStepRecord(paths, "worktree_created", `Reused existing worktree ${paths.worktree}.`);
|
|
675
|
+
}
|
|
839
676
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
840
677
|
return buildSessionResponse(paths, {
|
|
841
678
|
preconditions
|
|
@@ -870,7 +707,9 @@ async function createWorktree(paths, _options = {}, context = {}) {
|
|
|
870
707
|
if (baseCommit) {
|
|
871
708
|
await writeTextFile(path.join(paths.sessionRoot, "base_commit"), `${baseCommit}\n`);
|
|
872
709
|
}
|
|
873
|
-
|
|
710
|
+
if (completeStep) {
|
|
711
|
+
await writeStepRecord(paths, "worktree_created", `Created worktree ${paths.worktree} on branch ${paths.branch}.`);
|
|
712
|
+
}
|
|
874
713
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
875
714
|
return buildSessionResponse(paths, {
|
|
876
715
|
preconditions
|
|
@@ -881,7 +720,21 @@ async function recordDependenciesInstalled(paths, {
|
|
|
881
720
|
message = "Installed Node dependencies in the session worktree.",
|
|
882
721
|
preconditions = []
|
|
883
722
|
} = {}) {
|
|
884
|
-
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
|
+
);
|
|
885
738
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
886
739
|
return buildSessionResponse(paths, {
|
|
887
740
|
preconditions
|
|
@@ -946,6 +799,7 @@ async function dependencyInstallCommandForWorktree(worktree) {
|
|
|
946
799
|
|
|
947
800
|
async function installDependencies(paths, _options = {}, context = {}) {
|
|
948
801
|
const preconditions = context.preconditions || [];
|
|
802
|
+
const completeStep = context.completeStep !== false;
|
|
949
803
|
const [command, args] = await dependencyInstallCommandForWorktree(paths.worktree);
|
|
950
804
|
const result = await runLoggedCommand(paths, "dependencies_install", command, args, {
|
|
951
805
|
cwd: paths.worktree,
|
|
@@ -967,7 +821,8 @@ async function installDependencies(paths, _options = {}, context = {}) {
|
|
|
967
821
|
return provisionResult.response;
|
|
968
822
|
}
|
|
969
823
|
const installMessage = result.output || `Installed Node dependencies in the session worktree with ${command} ${args.join(" ")}.`;
|
|
970
|
-
|
|
824
|
+
const recorder = completeStep ? recordDependenciesInstalled : recordDependencyInstallResult;
|
|
825
|
+
return recorder(paths, {
|
|
971
826
|
message: provisionResult.ran ? `${installMessage}\n${SESSION_PROVISION_PACKAGE_SCRIPT} completed.` : installMessage,
|
|
972
827
|
preconditions
|
|
973
828
|
});
|
|
@@ -999,18 +854,39 @@ async function adoptDependenciesInstalled({
|
|
|
999
854
|
if (!provisionResult.ok) {
|
|
1000
855
|
return provisionResult.response;
|
|
1001
856
|
}
|
|
1002
|
-
const
|
|
857
|
+
const hookMessage = provisionResult.ran
|
|
1003
858
|
? `${normalizeText(message) || "Installed Node dependencies in the session worktree."}\n${SESSION_PROVISION_PACKAGE_SCRIPT} completed.`
|
|
1004
859
|
: message;
|
|
1005
|
-
return
|
|
1006
|
-
message:
|
|
860
|
+
return recordDependencyInstallResult(paths, {
|
|
861
|
+
message: hookMessage,
|
|
1007
862
|
preconditions
|
|
1008
863
|
});
|
|
1009
864
|
});
|
|
1010
865
|
}
|
|
1011
866
|
|
|
1012
|
-
|
|
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 = {}) {
|
|
1013
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
|
+
}
|
|
1014
890
|
if (!userInput) {
|
|
1015
891
|
return failSession(paths, {
|
|
1016
892
|
code: "prompt_required",
|
|
@@ -1018,43 +894,18 @@ async function renderIssuePrompt(paths, options = {}) {
|
|
|
1018
894
|
repairCommand: `jskit session ${paths.sessionId} step --prompt "<what should change>"`
|
|
1019
895
|
});
|
|
1020
896
|
}
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
await writePromptArtifact(paths, "issue_draft.md", prompt);
|
|
1025
|
-
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");
|
|
1026
900
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
1027
901
|
return buildSessionResponse(paths, {
|
|
902
|
+
codex: ISSUE_DEFINITION_CODEX_HANDOFF,
|
|
1028
903
|
ok: true,
|
|
1029
904
|
prompt,
|
|
1030
905
|
status: SESSION_STATUS.WAITING_FOR_USER
|
|
1031
906
|
});
|
|
1032
907
|
}
|
|
1033
908
|
|
|
1034
|
-
async function draftIssue(paths, options = {}) {
|
|
1035
|
-
const issueText = extractIssueText(options.issue);
|
|
1036
|
-
if (!issueText) {
|
|
1037
|
-
return failSession(paths, {
|
|
1038
|
-
code: "issue_required",
|
|
1039
|
-
message: "The issue drafting step requires --issue, --issue-file, or --issue -.",
|
|
1040
|
-
repairCommand: `jskit session ${paths.sessionId} step --issue -`
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
const issueTitle = normalizeText(options.issueTitle) || extractIssueTitle(options.issue);
|
|
1044
|
-
if (!issueTitle) {
|
|
1045
|
-
return failSession(paths, {
|
|
1046
|
-
code: "issue_title_required",
|
|
1047
|
-
message: "The issue drafting step requires an approved issue title.",
|
|
1048
|
-
repairCommand: `jskit session ${paths.sessionId} step --issue-title "<title>" --issue -`
|
|
1049
|
-
});
|
|
1050
|
-
}
|
|
1051
|
-
await writeTextFile(path.join(paths.sessionRoot, "issue.md"), issueText);
|
|
1052
|
-
await writeTextFile(path.join(paths.sessionRoot, "issue_title"), issueTitle);
|
|
1053
|
-
await writeReceipt(paths, "issue_drafted", "Saved approved issue text.");
|
|
1054
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1055
|
-
return buildSessionResponse(paths);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
909
|
function titleFromIssue(issueText) {
|
|
1059
910
|
const firstMeaningfulLine = String(issueText || "")
|
|
1060
911
|
.split(/\r?\n/u)
|
|
@@ -1065,24 +916,56 @@ function titleFromIssue(issueText) {
|
|
|
1065
916
|
|
|
1066
917
|
async function createIssue(paths, _options = {}, context = {}) {
|
|
1067
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;
|
|
1068
936
|
const existingIssueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
1069
937
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
1070
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
|
+
}
|
|
1071
946
|
if (existingIssueUrl) {
|
|
1072
|
-
await
|
|
1073
|
-
...issueMetadataFromUrl(existingIssueUrl),
|
|
1074
|
-
issueBody: issueText,
|
|
1075
|
-
issueBodyPath: path.join(paths.sessionRoot, "issue.md"),
|
|
947
|
+
await writeIssueMetadataFiles(paths, {
|
|
1076
948
|
issueTitle,
|
|
1077
949
|
issueUrl: existingIssueUrl
|
|
1078
950
|
});
|
|
1079
|
-
|
|
1080
|
-
|
|
951
|
+
if (completeStep) {
|
|
952
|
+
await writeStepRecord(paths, "issue_submitted", `Reused GitHub issue ${existingIssueUrl}.`);
|
|
953
|
+
}
|
|
1081
954
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1082
955
|
return buildSessionResponse(paths, {
|
|
1083
956
|
preconditions
|
|
1084
957
|
});
|
|
1085
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
|
+
}
|
|
1086
969
|
const result = await runLoggedCommand(paths, "github_issue_create", "gh", [
|
|
1087
970
|
"issue",
|
|
1088
971
|
"create",
|
|
@@ -1104,40 +987,30 @@ async function createIssue(paths, _options = {}, context = {}) {
|
|
|
1104
987
|
}
|
|
1105
988
|
const issueUrl = result.stdout.split(/\r?\n/u).map((line) => line.trim()).find(Boolean) || result.stdout;
|
|
1106
989
|
await writeTextFile(path.join(paths.sessionRoot, "issue_url"), issueUrl);
|
|
1107
|
-
await
|
|
1108
|
-
...issueMetadataFromUrl(issueUrl),
|
|
1109
|
-
issueBody: issueText,
|
|
1110
|
-
issueBodyPath: path.join(paths.sessionRoot, "issue.md"),
|
|
990
|
+
await writeIssueMetadataFiles(paths, {
|
|
1111
991
|
issueTitle,
|
|
1112
992
|
issueUrl
|
|
1113
993
|
});
|
|
1114
|
-
|
|
1115
|
-
|
|
994
|
+
if (completeStep) {
|
|
995
|
+
await writeStepRecord(paths, "issue_submitted", `Created GitHub issue ${issueUrl}.`);
|
|
996
|
+
}
|
|
1116
997
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1117
998
|
return buildSessionResponse(paths, {
|
|
1118
999
|
preconditions
|
|
1119
1000
|
});
|
|
1120
1001
|
}
|
|
1121
1002
|
|
|
1122
|
-
async function
|
|
1003
|
+
async function renderIssueFilePrompt(paths, context = {}) {
|
|
1123
1004
|
const preconditions = context.preconditions || [];
|
|
1124
|
-
const
|
|
1125
|
-
const
|
|
1126
|
-
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
1127
|
-
const issueNumber = issueNumberFromUrl(issueUrl);
|
|
1128
|
-
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", {
|
|
1129
1007
|
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
1130
|
-
|
|
1131
|
-
issue_text: issueText,
|
|
1132
|
-
issue_title: issueTitle,
|
|
1133
|
-
issue_url: issueUrl,
|
|
1134
|
-
issue_details_file: path.join(paths.sessionRoot, "issue_details.md"),
|
|
1135
|
-
worktree: paths.worktree
|
|
1008
|
+
issue_title_file: path.join(paths.sessionRoot, "issue_title")
|
|
1136
1009
|
});
|
|
1137
|
-
await
|
|
1010
|
+
await writeTextFile(issueFileSentinelPath, "true\n");
|
|
1138
1011
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
1139
1012
|
return buildSessionResponse(paths, {
|
|
1140
|
-
codex:
|
|
1013
|
+
codex: ISSUE_FILE_CODEX_HANDOFF,
|
|
1141
1014
|
ok: true,
|
|
1142
1015
|
preconditions,
|
|
1143
1016
|
prompt,
|
|
@@ -1145,145 +1018,47 @@ async function renderIssueDetailsPrompt(paths, _options = {}, context = {}) {
|
|
|
1145
1018
|
});
|
|
1146
1019
|
}
|
|
1147
1020
|
|
|
1148
|
-
async function
|
|
1149
|
-
const preconditions = context.preconditions || [];
|
|
1150
|
-
const source = normalizeText(options.issueDetails || options["issue-details"]);
|
|
1151
|
-
const structuredIssueCategory = normalizeText(options.issueCategory || options["issue-category"]);
|
|
1152
|
-
const structuredUiImpact = normalizeText(options.uiImpact || options["ui-impact"]);
|
|
1153
|
-
const issueDetails = extractIssueDetails(source) || (
|
|
1154
|
-
structuredIssueCategory && structuredUiImpact ? source : ""
|
|
1155
|
-
);
|
|
1156
|
-
if (!source && !structuredIssueCategory && !structuredUiImpact) {
|
|
1157
|
-
return renderIssueDetailsPrompt(paths, options, context);
|
|
1158
|
-
}
|
|
1159
|
-
if (!issueDetails) {
|
|
1160
|
-
return failSession(paths, {
|
|
1161
|
-
code: "issue_details_required",
|
|
1162
|
-
message: "The details step requires confirmed issue details from Codex or the Studio form.",
|
|
1163
|
-
repairCommand: `jskit session ${paths.sessionId} step --issue-details -`,
|
|
1164
|
-
preconditions
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
const issueCategory = normalizeIssueCategory(structuredIssueCategory || extractIssueCategory(source));
|
|
1169
|
-
const uiImpact = normalizeUiImpact(structuredUiImpact || extractUiImpact(source));
|
|
1170
|
-
if (!issueCategory) {
|
|
1171
|
-
return failSession(paths, {
|
|
1172
|
-
code: "issue_category_invalid",
|
|
1173
|
-
message: "Issue details must include [issue_category]client, server, client_server, tooling, or unknown[/issue_category].",
|
|
1174
|
-
repairCommand: `jskit session ${paths.sessionId} step --issue-details -`,
|
|
1175
|
-
preconditions
|
|
1176
|
-
});
|
|
1177
|
-
}
|
|
1178
|
-
if (!uiImpact) {
|
|
1179
|
-
return failSession(paths, {
|
|
1180
|
-
code: "ui_impact_invalid",
|
|
1181
|
-
message: "Issue details must include [ui_impact]none, possible, definite, or unknown[/ui_impact].",
|
|
1182
|
-
repairCommand: `jskit session ${paths.sessionId} step --issue-details -`,
|
|
1183
|
-
preconditions
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
await writeTextFile(path.join(paths.sessionRoot, "issue_details.md"), issueDetails);
|
|
1188
|
-
await writeIssueMetadata(paths, {
|
|
1189
|
-
issueCategory,
|
|
1190
|
-
uiImpact,
|
|
1191
|
-
issueDetailsPath: path.join(paths.sessionRoot, "issue_details.md")
|
|
1192
|
-
});
|
|
1193
|
-
|
|
1194
|
-
const decisions = extractAgentDecisions(source);
|
|
1195
|
-
await appendAgentDecisions(paths, decisions);
|
|
1196
|
-
|
|
1197
|
-
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
1198
|
-
if (issueUrl) {
|
|
1199
|
-
const commentResult = await commentOnIssueOnce(paths, {
|
|
1200
|
-
bodyFile: path.join(paths.sessionRoot, "issue_details.md"),
|
|
1201
|
-
issueUrl,
|
|
1202
|
-
purpose: "issue_details"
|
|
1203
|
-
});
|
|
1204
|
-
if (!commentResult.ok) {
|
|
1205
|
-
return failSession(paths, {
|
|
1206
|
-
code: "issue_details_comment_failed",
|
|
1207
|
-
message: commentResult.output || "Failed to comment the issue details on the GitHub issue.",
|
|
1208
|
-
repairCommand: `gh issue comment ${issueUrl} --body-file ${path.join(paths.sessionRoot, "issue_details.md")}`,
|
|
1209
|
-
preconditions
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
await writeReceipt(paths, "issue_details_gathered", "Saved confirmed issue details and recorded the GitHub issue comment.");
|
|
1215
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1216
|
-
return buildSessionResponse(paths, {
|
|
1217
|
-
preconditions
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
async function makePlan(paths, options = {}, context = {}) {
|
|
1021
|
+
async function makePlan(paths, _options = {}, context = {}) {
|
|
1222
1022
|
const preconditions = context.preconditions || [];
|
|
1223
|
-
const
|
|
1023
|
+
const makePlanSentinelPath = path.join(paths.sessionRoot, "metadata", "make_plan_requested");
|
|
1224
1024
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
1225
1025
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
1226
1026
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
1227
1027
|
const issueNumber = issueNumberFromUrl(issueUrl);
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
const agentDecisionsText = await readTextIfExists(agentDecisionsPath);
|
|
1232
|
-
const currentCycleRoot = cycleRootPath(paths, activeCycle);
|
|
1233
|
-
const planPath = cyclePlanPath(paths, activeCycle);
|
|
1234
|
-
const reworkRequestPath = path.join(currentCycleRoot, "rework_request.md");
|
|
1235
|
-
const reworkRequest = await readTextIfExists(reworkRequestPath);
|
|
1236
|
-
|
|
1237
|
-
if (!planText) {
|
|
1238
|
-
const prompt = await renderPrompt(paths, "plan_issue.md", {
|
|
1239
|
-
active_cycle: activeCycle,
|
|
1240
|
-
agent_decisions_file: agentDecisionsPath,
|
|
1241
|
-
agent_decisions_text: agentDecisionsText,
|
|
1242
|
-
app_blueprint_file: path.join(paths.worktree, ".jskit", "APP_BLUEPRINT.md"),
|
|
1243
|
-
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
1244
|
-
issue_number: issueNumber,
|
|
1245
|
-
issue_text: issueText,
|
|
1246
|
-
issue_title: issueTitle,
|
|
1247
|
-
issue_title_file: path.join(paths.sessionRoot, "issue_title"),
|
|
1248
|
-
issue_url: issueUrl,
|
|
1249
|
-
issue_details_file: path.join(paths.sessionRoot, "issue_details.md"),
|
|
1250
|
-
issue_details_text: issueDetails,
|
|
1251
|
-
plan_file: planPath,
|
|
1252
|
-
plan_source: activeCycle === "001" ? "issue" : "rework",
|
|
1253
|
-
rework_request: reworkRequest,
|
|
1254
|
-
rework_request_file: reworkRequest ? reworkRequestPath : "",
|
|
1255
|
-
worktree: paths.worktree
|
|
1256
|
-
});
|
|
1257
|
-
await writePromptArtifact(paths, cyclePlanPromptFileName(activeCycle), prompt);
|
|
1258
|
-
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);
|
|
1259
1031
|
return buildSessionResponse(paths, {
|
|
1260
|
-
|
|
1261
|
-
preconditions,
|
|
1262
|
-
prompt,
|
|
1263
|
-
status: SESSION_STATUS.WAITING_FOR_USER
|
|
1032
|
+
preconditions
|
|
1264
1033
|
});
|
|
1265
1034
|
}
|
|
1266
1035
|
|
|
1267
|
-
await
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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);
|
|
1272
1048
|
return buildSessionResponse(paths, {
|
|
1273
|
-
|
|
1049
|
+
codex: PLAN_CODEX_HANDOFF,
|
|
1050
|
+
ok: true,
|
|
1051
|
+
preconditions,
|
|
1052
|
+
prompt,
|
|
1053
|
+
status: SESSION_STATUS.WAITING_FOR_USER
|
|
1274
1054
|
});
|
|
1275
1055
|
}
|
|
1276
1056
|
|
|
1277
|
-
async function renderPlanExecutionPrompt(paths,
|
|
1057
|
+
async function renderPlanExecutionPrompt(paths, _options = {}, context = {}) {
|
|
1278
1058
|
const preconditions = context.preconditions || [];
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
const codexResultFailure = await requireCodexStepResult(paths, "plan_executed", options.codexResult, preconditions);
|
|
1283
|
-
if (codexResultFailure) {
|
|
1284
|
-
return codexResultFailure;
|
|
1285
|
-
}
|
|
1286
|
-
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.");
|
|
1287
1062
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1288
1063
|
return buildSessionResponse(paths, {
|
|
1289
1064
|
preconditions
|
|
@@ -1294,22 +1069,14 @@ async function renderPlanExecutionPrompt(paths, options = {}, context = {}) {
|
|
|
1294
1069
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
1295
1070
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
1296
1071
|
const issueNumber = issueNumberFromUrl(issueUrl);
|
|
1297
|
-
const
|
|
1298
|
-
const issueDetailsPath = path.join(paths.sessionRoot, "issue_details.md");
|
|
1299
|
-
const issueDetails = await readTrimmedFile(issueDetailsPath);
|
|
1300
|
-
const executionPrompt = await renderPrompt(paths, "execute_plan.md", {
|
|
1301
|
-
active_cycle: activeCycle,
|
|
1072
|
+
const executionPrompt = await renderPrompt(paths, "plan_executed.md", {
|
|
1302
1073
|
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
1303
1074
|
issue_number: issueNumber,
|
|
1304
1075
|
issue_title: issueTitle,
|
|
1305
1076
|
issue_url: issueUrl,
|
|
1306
|
-
issue_details_file: issueDetailsPath,
|
|
1307
|
-
issue_details_text: issueDetails,
|
|
1308
|
-
plan_file: planPath,
|
|
1309
|
-
plan_text: planText,
|
|
1310
1077
|
worktree: paths.worktree
|
|
1311
1078
|
});
|
|
1312
|
-
await
|
|
1079
|
+
await writeTextFile(executePlanSentinelPath, "true\n");
|
|
1313
1080
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
1314
1081
|
return buildSessionResponse(paths, {
|
|
1315
1082
|
codex: PLAN_EXECUTION_CODEX_HANDOFF,
|
|
@@ -1440,7 +1207,6 @@ async function inspectSessionDiff({
|
|
|
1440
1207
|
}
|
|
1441
1208
|
|
|
1442
1209
|
const FIRST_REWINDABLE_STEP_ID = "dependencies_installed";
|
|
1443
|
-
const CYCLE_REWIND_TARGET_STEP_ID = "plan_made";
|
|
1444
1210
|
const REWIND_CLOSED_STATUSES = Object.freeze([
|
|
1445
1211
|
SESSION_STATUS.ABANDONED,
|
|
1446
1212
|
SESSION_STATUS.FINISHED
|
|
@@ -1462,7 +1228,29 @@ async function removePromptArtifact(paths, fileName) {
|
|
|
1462
1228
|
}
|
|
1463
1229
|
|
|
1464
1230
|
async function removeGlobalCodexResult(paths, stepId) {
|
|
1465
|
-
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
|
+
]);
|
|
1466
1254
|
}
|
|
1467
1255
|
|
|
1468
1256
|
async function removeGithubCommentPurpose(paths, purpose) {
|
|
@@ -1478,21 +1266,6 @@ async function removeGithubCommentPurpose(paths, purpose) {
|
|
|
1478
1266
|
await writeGithubComments(paths, comments);
|
|
1479
1267
|
}
|
|
1480
1268
|
|
|
1481
|
-
async function removeIssueDetailsMetadata(paths) {
|
|
1482
|
-
const metadata = await readIssueMetadata(paths);
|
|
1483
|
-
if (Object.keys(metadata).length === 0) {
|
|
1484
|
-
return;
|
|
1485
|
-
}
|
|
1486
|
-
delete metadata.issueCategory;
|
|
1487
|
-
delete metadata.issueDetailsPath;
|
|
1488
|
-
delete metadata.uiImpact;
|
|
1489
|
-
if (Object.keys(metadata).length === 0) {
|
|
1490
|
-
await removeSessionRootFile(paths, "issue_metadata.json");
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
await writeTextFile(path.join(paths.sessionRoot, "issue_metadata.json"), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
1269
|
async function removeCycleDirectories(paths) {
|
|
1497
1270
|
for (const rootName of ["steps", "cycles"]) {
|
|
1498
1271
|
const root = path.join(paths.sessionRoot, rootName);
|
|
@@ -1511,98 +1284,102 @@ async function removeCycleDirectories(paths) {
|
|
|
1511
1284
|
}
|
|
1512
1285
|
}
|
|
1513
1286
|
|
|
1514
|
-
async function
|
|
1515
|
-
|
|
1516
|
-
let entries = [];
|
|
1517
|
-
try {
|
|
1518
|
-
entries = await readdir(promptsRoot, { withFileTypes: true });
|
|
1519
|
-
} catch {
|
|
1520
|
-
entries = [];
|
|
1521
|
-
}
|
|
1522
|
-
const cyclePromptPattern = /^cycle_\d+_(?:plan_request|plan_execution)\.md$/u;
|
|
1523
|
-
const cyclePromptFiles = entries
|
|
1524
|
-
.filter((entry) => entry.isFile() && cyclePromptPattern.test(entry.name))
|
|
1525
|
-
.map((entry) => entry.name);
|
|
1526
|
-
await Promise.all([
|
|
1527
|
-
...cyclePromptFiles.map((fileName) => removePromptArtifact(paths, fileName)),
|
|
1528
|
-
removePromptArtifact(paths, "automated_checks_run.md"),
|
|
1529
|
-
removePromptArtifact(paths, "deep_ui_check_run.md"),
|
|
1530
|
-
removePromptArtifact(paths, "review.md"),
|
|
1531
|
-
removePromptArtifact(paths, "user_check.md")
|
|
1532
|
-
]);
|
|
1287
|
+
async function removePlanArtifacts(paths) {
|
|
1288
|
+
await removeSessionPath(paths, "metadata", "make_plan_requested");
|
|
1533
1289
|
}
|
|
1534
1290
|
|
|
1535
|
-
async function
|
|
1291
|
+
async function removePlanExecutionArtifacts(paths) {
|
|
1536
1292
|
await Promise.all([
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
removeSessionPath(paths, "checks"),
|
|
1540
|
-
removeSessionPath(paths, "ui_checks"),
|
|
1541
|
-
removeSessionPath(paths, "review_passes")
|
|
1293
|
+
removeSessionPath(paths, "metadata", "execute_plan_requested"),
|
|
1294
|
+
removeCodexResult(paths, "plan_executed")
|
|
1542
1295
|
]);
|
|
1543
|
-
await writeActiveCycle(paths, "001");
|
|
1544
1296
|
}
|
|
1545
1297
|
|
|
1546
1298
|
const STEP_CANCELERS = Object.freeze({
|
|
1547
1299
|
dependencies_installed: async () => {},
|
|
1548
1300
|
issue_prompt_rendered: async (paths) => {
|
|
1549
|
-
await
|
|
1301
|
+
await removeSessionPath(paths, "metadata", "issue_prompt_rendered_requested");
|
|
1550
1302
|
},
|
|
1551
|
-
|
|
1303
|
+
issue_created: async (paths) => {
|
|
1552
1304
|
await Promise.all([
|
|
1305
|
+
removeSessionPath(paths, "metadata", "issue_created_requested"),
|
|
1553
1306
|
removeSessionRootFile(paths, "issue.md"),
|
|
1554
1307
|
removeSessionRootFile(paths, "issue_title")
|
|
1555
1308
|
]);
|
|
1556
1309
|
},
|
|
1557
|
-
|
|
1310
|
+
issue_submitted: async (paths) => {
|
|
1558
1311
|
await Promise.all([
|
|
1559
1312
|
removeSessionRootFile(paths, "issue_url"),
|
|
1560
|
-
|
|
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")
|
|
1561
1335
|
]);
|
|
1562
1336
|
},
|
|
1563
|
-
|
|
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")
|
|
1344
|
+
]);
|
|
1345
|
+
},
|
|
1346
|
+
user_check_completed: async (paths) => {
|
|
1564
1347
|
await Promise.all([
|
|
1565
|
-
removePromptArtifact(paths, "
|
|
1566
|
-
|
|
1567
|
-
removeGithubCommentPurpose(paths, "issue_details"),
|
|
1568
|
-
removeIssueDetailsMetadata(paths)
|
|
1348
|
+
removePromptArtifact(paths, "user_check_completed"),
|
|
1349
|
+
removeSessionPath(paths, "steps", "user_check_failed")
|
|
1569
1350
|
]);
|
|
1570
1351
|
},
|
|
1571
|
-
plan_made: cancelAllCycleState,
|
|
1572
|
-
plan_executed: cancelAllCycleState,
|
|
1573
|
-
deep_ui_check_run: cancelAllCycleState,
|
|
1574
|
-
review_prompt_rendered: cancelAllCycleState,
|
|
1575
|
-
review_changes_accepted: cancelAllCycleState,
|
|
1576
|
-
automated_checks_run: cancelAllCycleState,
|
|
1577
|
-
user_check_completed: cancelAllCycleState,
|
|
1578
1352
|
changes_committed: async (paths) => {
|
|
1579
1353
|
await removeSessionRootFile(paths, "changes_committed.json");
|
|
1580
1354
|
},
|
|
1581
1355
|
blueprint_updated: async (paths) => {
|
|
1582
1356
|
await Promise.all([
|
|
1583
|
-
|
|
1584
|
-
|
|
1357
|
+
removeSessionPath(paths, "metadata", "blueprint_updated_requested"),
|
|
1358
|
+
removeSessionRootFile(paths, BLUEPRINT_BASELINE_FILE),
|
|
1359
|
+
removeCodexResult(paths, "blueprint_updated")
|
|
1585
1360
|
]);
|
|
1586
1361
|
},
|
|
1587
1362
|
final_report_created: async (paths) => {
|
|
1588
1363
|
await Promise.all([
|
|
1364
|
+
removeSessionPath(paths, "metadata", "pull_request_file_requested"),
|
|
1365
|
+
removeSessionRootFile(paths, "pull_request.md"),
|
|
1366
|
+
removeSessionRootFile(paths, "final_report"),
|
|
1589
1367
|
removeSessionRootFile(paths, "final_report.md"),
|
|
1590
1368
|
removeGithubCommentPurpose(paths, "final_report")
|
|
1591
1369
|
]);
|
|
1592
1370
|
},
|
|
1593
1371
|
pr_created: async (paths) => {
|
|
1594
1372
|
await Promise.all([
|
|
1595
|
-
removePromptArtifact(paths, "pr_create_failure
|
|
1373
|
+
removePromptArtifact(paths, "pr_create_failure"),
|
|
1596
1374
|
removeSessionRootFile(paths, "pr_body.md"),
|
|
1375
|
+
removeSessionRootFile(paths, "pull_request_body.md"),
|
|
1597
1376
|
removeSessionRootFile(paths, "pr_url")
|
|
1598
1377
|
]);
|
|
1599
1378
|
},
|
|
1600
1379
|
pr_merge_prepared: async (paths) => {
|
|
1601
|
-
await removePromptArtifact(paths, "prepare_pr_merge.md");
|
|
1602
|
-
},
|
|
1603
|
-
pr_finalized: async (paths) => {
|
|
1604
1380
|
await Promise.all([
|
|
1605
|
-
removePromptArtifact(paths, "
|
|
1381
|
+
removePromptArtifact(paths, "pr_merge_prepared"),
|
|
1382
|
+
removePromptArtifact(paths, "pr_merge_failure"),
|
|
1606
1383
|
removeSessionRootFile(paths, "pr_base_branch"),
|
|
1607
1384
|
removeSessionRootFile(paths, "pr_merge_completed"),
|
|
1608
1385
|
removeSessionRootFile(paths, "pr_outcome.json")
|
|
@@ -1616,27 +1393,18 @@ const STEP_CANCELERS = Object.freeze({
|
|
|
1616
1393
|
},
|
|
1617
1394
|
session_finished: async (paths) => {
|
|
1618
1395
|
await Promise.all([
|
|
1619
|
-
removeSessionRootFile(paths, "final_comment
|
|
1396
|
+
removeSessionRootFile(paths, "final_comment")
|
|
1620
1397
|
]);
|
|
1621
1398
|
}
|
|
1622
1399
|
});
|
|
1623
1400
|
|
|
1624
|
-
function targetRequiresCycleReset(stepId) {
|
|
1625
|
-
const targetIndex = STEP_IDS.indexOf(stepId);
|
|
1626
|
-
const planIndex = STEP_IDS.indexOf(CYCLE_REWIND_TARGET_STEP_ID);
|
|
1627
|
-
return targetIndex >= 0 && targetIndex <= planIndex;
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
1401
|
function targetIsAllowedRewindStep(stepId) {
|
|
1631
1402
|
if (!STEP_IDS.includes(stepId)) {
|
|
1632
1403
|
return false;
|
|
1633
1404
|
}
|
|
1634
|
-
if (stepId === "
|
|
1405
|
+
if (stepId === "worktree_created") {
|
|
1635
1406
|
return false;
|
|
1636
1407
|
}
|
|
1637
|
-
if (CYCLE_STEP_IDS.includes(stepId)) {
|
|
1638
|
-
return stepId === CYCLE_REWIND_TARGET_STEP_ID;
|
|
1639
|
-
}
|
|
1640
1408
|
return STEP_IDS.indexOf(stepId) >= STEP_IDS.indexOf(FIRST_REWINDABLE_STEP_ID);
|
|
1641
1409
|
}
|
|
1642
1410
|
|
|
@@ -1645,30 +1413,28 @@ function deletedStepIdsForRewindTarget(stepId) {
|
|
|
1645
1413
|
return targetIndex < 0 ? [] : STEP_IDS.slice(targetIndex);
|
|
1646
1414
|
}
|
|
1647
1415
|
|
|
1648
|
-
async function
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
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
|
+
]));
|
|
1652
1430
|
}
|
|
1653
1431
|
|
|
1654
|
-
async function cancelDeletedStepArtifacts(paths, deletedStepIds
|
|
1655
|
-
|
|
1656
|
-
} = {}) {
|
|
1657
|
-
const cancelerIds = cycleReset
|
|
1658
|
-
? deletedStepIds.filter((stepId) => !CYCLE_STEP_IDS.includes(stepId)).concat(CYCLE_REWIND_TARGET_STEP_ID)
|
|
1659
|
-
: deletedStepIds;
|
|
1660
|
-
const calledCycleCancel = new Set();
|
|
1661
|
-
for (const stepId of cancelerIds) {
|
|
1432
|
+
async function cancelDeletedStepArtifacts(paths, deletedStepIds) {
|
|
1433
|
+
for (const stepId of deletedStepIds) {
|
|
1662
1434
|
const canceler = STEP_CANCELERS[stepId];
|
|
1663
1435
|
if (typeof canceler !== "function") {
|
|
1664
1436
|
continue;
|
|
1665
1437
|
}
|
|
1666
|
-
if (CYCLE_STEP_IDS.includes(stepId)) {
|
|
1667
|
-
if (calledCycleCancel.has(CYCLE_REWIND_TARGET_STEP_ID)) {
|
|
1668
|
-
continue;
|
|
1669
|
-
}
|
|
1670
|
-
calledCycleCancel.add(CYCLE_REWIND_TARGET_STEP_ID);
|
|
1671
|
-
}
|
|
1672
1438
|
await canceler(paths);
|
|
1673
1439
|
}
|
|
1674
1440
|
}
|
|
@@ -1723,15 +1489,12 @@ async function rewindSession({
|
|
|
1723
1489
|
}
|
|
1724
1490
|
|
|
1725
1491
|
if (!targetIsAllowedRewindStep(normalizedStepId)) {
|
|
1726
|
-
const cycleHint = CYCLE_STEP_IDS.includes(normalizedStepId)
|
|
1727
|
-
? " Only Plan made can be used as a cycle rewind target; it resets all cycle/rework state."
|
|
1728
|
-
: "";
|
|
1729
1492
|
return buildSessionResponse(paths, {
|
|
1730
1493
|
ok: false,
|
|
1731
1494
|
errors: [
|
|
1732
1495
|
createError({
|
|
1733
1496
|
code: "rewind_step_not_allowed",
|
|
1734
|
-
message: `Cannot rewind session ${paths.sessionId} to ${normalizedStepId || "(missing)"}
|
|
1497
|
+
message: `Cannot rewind session ${paths.sessionId} to ${normalizedStepId || "(missing)"}.`
|
|
1735
1498
|
})
|
|
1736
1499
|
],
|
|
1737
1500
|
status: currentStatus
|
|
@@ -1752,12 +1515,8 @@ async function rewindSession({
|
|
|
1752
1515
|
}
|
|
1753
1516
|
|
|
1754
1517
|
const deletedStepIds = deletedStepIdsForRewindTarget(normalizedStepId);
|
|
1755
|
-
|
|
1756
|
-
await
|
|
1757
|
-
await cancelDeletedStepArtifacts(paths, deletedStepIds, { cycleReset });
|
|
1758
|
-
if (cycleReset) {
|
|
1759
|
-
await writeActiveCycle(paths, "001");
|
|
1760
|
-
}
|
|
1518
|
+
await removeStepRecordsForDeletedSteps(paths, deletedStepIds);
|
|
1519
|
+
await cancelDeletedStepArtifacts(paths, deletedStepIds);
|
|
1761
1520
|
await markCurrentStep(paths, normalizedStepId);
|
|
1762
1521
|
await markStatus(paths, SESSION_STATUS.PENDING);
|
|
1763
1522
|
return buildSessionResponse(paths, {
|
|
@@ -1830,6 +1589,79 @@ async function changedFilesInWorktree(paths) {
|
|
|
1830
1589
|
]);
|
|
1831
1590
|
}
|
|
1832
1591
|
|
|
1592
|
+
const BLUEPRINT_RELATIVE_PATH = ".jskit/APP_BLUEPRINT.md";
|
|
1593
|
+
const BLUEPRINT_BASELINE_FILE = "blueprint_update_baseline.json";
|
|
1594
|
+
|
|
1595
|
+
function blueprintBaselinePath(paths) {
|
|
1596
|
+
return path.join(paths.sessionRoot, BLUEPRINT_BASELINE_FILE);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function isBlueprintRelativePath(filePath = "") {
|
|
1600
|
+
return normalizeText(filePath) === BLUEPRINT_RELATIVE_PATH;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function nonBlueprintChangedFiles(files = []) {
|
|
1604
|
+
return files.filter((file) => !isBlueprintRelativePath(file));
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
async function hashWorktreeFile(paths, filePath) {
|
|
1608
|
+
try {
|
|
1609
|
+
const buffer = await readFile(path.join(paths.worktree, filePath));
|
|
1610
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
1611
|
+
} catch {
|
|
1612
|
+
return "missing";
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
async function buildDirtyFileSnapshot(paths, files = []) {
|
|
1617
|
+
const entries = await Promise.all(nonBlueprintChangedFiles(files).map(async (file) => [
|
|
1618
|
+
file,
|
|
1619
|
+
await hashWorktreeFile(paths, file)
|
|
1620
|
+
]));
|
|
1621
|
+
return Object.fromEntries(entries);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
async function writeBlueprintBaseline(paths) {
|
|
1625
|
+
const changedFiles = await changedFilesInWorktree(paths);
|
|
1626
|
+
const snapshot = await buildDirtyFileSnapshot(paths, changedFiles);
|
|
1627
|
+
const payload = {
|
|
1628
|
+
changedFiles: Object.keys(snapshot).sort((left, right) => left.localeCompare(right)),
|
|
1629
|
+
files: snapshot,
|
|
1630
|
+
recordedAt: timestampForStepRecord()
|
|
1631
|
+
};
|
|
1632
|
+
await writeTextFile(blueprintBaselinePath(paths), `${JSON.stringify(payload, null, 2)}\n`);
|
|
1633
|
+
return payload;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
async function readBlueprintBaseline(paths) {
|
|
1637
|
+
return parseJsonObject(await readTextIfExists(blueprintBaselinePath(paths))) || null;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
async function unexpectedBlueprintStepChanges(paths, changedFiles = []) {
|
|
1641
|
+
const baseline = await readBlueprintBaseline(paths);
|
|
1642
|
+
if (!baseline?.files || typeof baseline.files !== "object" || Array.isArray(baseline.files)) {
|
|
1643
|
+
return nonBlueprintChangedFiles(changedFiles);
|
|
1644
|
+
}
|
|
1645
|
+
const baselineFiles = baseline.files;
|
|
1646
|
+
const currentFiles = new Set(nonBlueprintChangedFiles(changedFiles));
|
|
1647
|
+
const candidates = new Set([
|
|
1648
|
+
...Object.keys(baselineFiles),
|
|
1649
|
+
...currentFiles
|
|
1650
|
+
]);
|
|
1651
|
+
const unexpected = [];
|
|
1652
|
+
for (const file of [...candidates].sort((left, right) => left.localeCompare(right))) {
|
|
1653
|
+
if (!Object.prototype.hasOwnProperty.call(baselineFiles, file)) {
|
|
1654
|
+
unexpected.push(file);
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
const currentHash = await hashWorktreeFile(paths, file);
|
|
1658
|
+
if (currentHash !== baselineFiles[file]) {
|
|
1659
|
+
unexpected.push(file);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
return unexpected;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1833
1665
|
async function changedFilesSinceBase(paths) {
|
|
1834
1666
|
const baseCommit = await readTrimmedFile(path.join(paths.sessionRoot, "base_commit"));
|
|
1835
1667
|
const args = baseCommit
|
|
@@ -1889,24 +1721,23 @@ async function renderReviewPrompt(paths) {
|
|
|
1889
1721
|
const reviewPass = await resolveReviewPassForPrompt(paths);
|
|
1890
1722
|
await writeCurrentReviewPass(paths, reviewPass);
|
|
1891
1723
|
const changedFiles = await changedFilesSinceBase(paths);
|
|
1892
|
-
const prompt = await renderPrompt(paths, "
|
|
1724
|
+
const prompt = await renderPrompt(paths, "review_prompt_rendered.md", {
|
|
1893
1725
|
changed_files: changedFiles,
|
|
1894
1726
|
review_pass_limit: String(REVIEW_PASS_LIMIT),
|
|
1895
1727
|
review_pass_number: reviewPass
|
|
1896
1728
|
});
|
|
1897
1729
|
const passRoot = reviewPassRoot(paths, reviewPass);
|
|
1898
|
-
await writePromptArtifact(paths, "
|
|
1730
|
+
await writePromptArtifact(paths, "review_prompt_rendered", prompt);
|
|
1899
1731
|
await mkdir(passRoot, { recursive: true });
|
|
1900
|
-
await writeTextFile(path.join(passRoot, "
|
|
1732
|
+
await writeTextFile(path.join(passRoot, "review_prompt_rendered"), prompt);
|
|
1901
1733
|
await writeReviewPassJson(paths, reviewPass, "prompt.json", {
|
|
1902
1734
|
changedFiles: changedFiles.split(/\r?\n/u).filter(Boolean),
|
|
1903
1735
|
maxPasses: REVIEW_PASS_LIMIT,
|
|
1904
1736
|
pass: reviewPass,
|
|
1905
|
-
promptPath: path.join(passRoot, "
|
|
1737
|
+
promptPath: path.join(passRoot, "review_prompt_rendered"),
|
|
1906
1738
|
status: "prompted",
|
|
1907
|
-
startedAt:
|
|
1739
|
+
startedAt: timestampForStepRecord()
|
|
1908
1740
|
});
|
|
1909
|
-
await writeReceipt(paths, "review_prompt_rendered", "Started code review.");
|
|
1910
1741
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
1911
1742
|
return buildSessionResponse(paths, {
|
|
1912
1743
|
codex: REVIEW_EXECUTION_CODEX_HANDOFF,
|
|
@@ -1915,13 +1746,42 @@ async function renderReviewPrompt(paths) {
|
|
|
1915
1746
|
});
|
|
1916
1747
|
}
|
|
1917
1748
|
|
|
1918
|
-
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
|
+
|
|
1919
1779
|
const reviewDecisionProvided = Object.hasOwn(options, "reviewFindingsRemaining") ||
|
|
1920
1780
|
Object.hasOwn(options, "review-findings-remaining");
|
|
1921
1781
|
if (!reviewDecisionProvided) {
|
|
1922
1782
|
return failSession(paths, {
|
|
1923
1783
|
code: "review_decision_required",
|
|
1924
|
-
message: "
|
|
1784
|
+
message: "Accept review/deslop requires an explicit decision: resolve review/deslop, run review/deslop again, or continue.",
|
|
1925
1785
|
repairCommand: `jskit session ${paths.sessionId} step --review-findings-remaining false`
|
|
1926
1786
|
});
|
|
1927
1787
|
}
|
|
@@ -1940,64 +1800,36 @@ async function acceptReviewChanges(paths, options = {}) {
|
|
|
1940
1800
|
const reviewPass = await readCurrentReviewPass(paths);
|
|
1941
1801
|
const findingsRemaining = options.reviewFindingsRemaining === true ||
|
|
1942
1802
|
normalizeText(options["review-findings-remaining"]).toLowerCase() === "true";
|
|
1943
|
-
const remainingFindings = normalizeText(options.reviewFindings || options["review-findings"]);
|
|
1944
1803
|
await writeReviewPassJson(paths, reviewPass, "accepted.json", {
|
|
1945
|
-
acceptedAt:
|
|
1804
|
+
acceptedAt: timestampForStepRecord(),
|
|
1946
1805
|
changedFiles: status.changedFiles || [],
|
|
1947
1806
|
findingsRemaining,
|
|
1948
|
-
remainingFindings,
|
|
1807
|
+
remainingFindings: "",
|
|
1949
1808
|
pass: reviewPass,
|
|
1950
1809
|
status: status.changedFiles?.length ? "accepted" : "no_changes"
|
|
1951
1810
|
});
|
|
1952
|
-
await
|
|
1811
|
+
await writeStepRecord(paths, "review_changes_accepted", message);
|
|
1953
1812
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1954
1813
|
return buildSessionResponse(paths);
|
|
1955
1814
|
}
|
|
1956
1815
|
|
|
1957
1816
|
async function runAutomatedChecks(paths, {
|
|
1958
|
-
stepId
|
|
1959
|
-
|
|
1960
|
-
}, options = {}, context = {}) {
|
|
1817
|
+
stepId
|
|
1818
|
+
}, _options = {}, context = {}) {
|
|
1961
1819
|
const preconditions = context.preconditions || [];
|
|
1962
1820
|
const [command, args] = await doctorCommandForWorktree(paths.worktree);
|
|
1963
|
-
const promptFileName = `${stepId}.md`;
|
|
1964
|
-
const promptPath = path.join(paths.sessionRoot, "prompts", promptFileName);
|
|
1965
1821
|
const checksRoot = path.join(paths.sessionRoot, "checks");
|
|
1966
1822
|
await mkdir(checksRoot, { recursive: true });
|
|
1967
1823
|
const checkCommand = [command, ...args].join(" ");
|
|
1968
1824
|
|
|
1969
|
-
|
|
1970
|
-
const codexResultFailure = await requireCodexStepResult(paths, stepId, options.codexResult, preconditions);
|
|
1971
|
-
if (codexResultFailure) {
|
|
1972
|
-
return codexResultFailure;
|
|
1973
|
-
}
|
|
1974
|
-
await writeTextFile(
|
|
1975
|
-
path.join(checksRoot, `${stepId}.json`),
|
|
1976
|
-
`${JSON.stringify({
|
|
1977
|
-
command: checkCommand,
|
|
1978
|
-
ok: true,
|
|
1979
|
-
promptPath,
|
|
1980
|
-
status: "completed_by_codex",
|
|
1981
|
-
stepId
|
|
1982
|
-
}, null, 2)}\n`
|
|
1983
|
-
);
|
|
1984
|
-
await writeReceipt(paths, stepId, `${label} completed by Codex: ${checkCommand}.`);
|
|
1985
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1986
|
-
return buildSessionResponse(paths, {
|
|
1987
|
-
preconditions
|
|
1988
|
-
});
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
const prompt = await renderPrompt(paths, "automated_checks.md", {
|
|
1825
|
+
const prompt = await renderPrompt(paths, "automated_checks_run.md", {
|
|
1992
1826
|
check_command: checkCommand
|
|
1993
1827
|
});
|
|
1994
|
-
await writePromptArtifact(paths, promptFileName, prompt);
|
|
1995
1828
|
await writeTextFile(
|
|
1996
1829
|
path.join(checksRoot, `${stepId}.json`),
|
|
1997
1830
|
`${JSON.stringify({
|
|
1998
1831
|
command: checkCommand,
|
|
1999
1832
|
ok: false,
|
|
2000
|
-
promptPath,
|
|
2001
1833
|
status: "prompted",
|
|
2002
1834
|
stepId
|
|
2003
1835
|
}, null, 2)}\n`
|
|
@@ -2019,86 +1851,26 @@ async function writeUiCheckJson(paths, fileName, payload) {
|
|
|
2019
1851
|
|
|
2020
1852
|
async function runDeepUiCheck(paths, {
|
|
2021
1853
|
stepId,
|
|
2022
|
-
label,
|
|
2023
1854
|
phase
|
|
2024
|
-
},
|
|
1855
|
+
}, _options = {}, context = {}) {
|
|
2025
1856
|
const preconditions = context.preconditions || [];
|
|
2026
|
-
const issueMetadata = await readIssueMetadata(paths);
|
|
2027
|
-
const uiImpact = normalizeUiImpact(issueMetadata.uiImpact) || "unknown";
|
|
2028
|
-
const skipRequested = options.skipUiCheck === true || normalizeText(options["skip-ui-check"]).toLowerCase() === "true";
|
|
2029
|
-
const skipReason = normalizeText(options.skipReason || options["skip-reason"]);
|
|
2030
|
-
const shouldSkip = uiImpact === "none" || skipRequested;
|
|
2031
|
-
if (shouldSkip) {
|
|
2032
|
-
if (skipRequested && uiImpact !== "possible") {
|
|
2033
|
-
return failSession(paths, {
|
|
2034
|
-
code: "ui_check_skip_not_allowed",
|
|
2035
|
-
message: `Deep UI check can only be manually skipped when uiImpact is possible. Current uiImpact is ${uiImpact}.`,
|
|
2036
|
-
repairCommand: `jskit session ${paths.sessionId} step`,
|
|
2037
|
-
preconditions
|
|
2038
|
-
});
|
|
2039
|
-
}
|
|
2040
|
-
if (skipRequested && !skipReason) {
|
|
2041
|
-
return failSession(paths, {
|
|
2042
|
-
code: "ui_check_skip_reason_required",
|
|
2043
|
-
message: "Skipping a possible Deep UI check requires --skip-reason.",
|
|
2044
|
-
repairCommand: `jskit session ${paths.sessionId} step --skip-ui-check --skip-reason "<reason>"`,
|
|
2045
|
-
preconditions
|
|
2046
|
-
});
|
|
2047
|
-
}
|
|
2048
|
-
const reason = uiImpact === "none" ? "uiImpact is none." : skipReason;
|
|
2049
|
-
await writeUiCheckJson(paths, stepId, {
|
|
2050
|
-
ok: true,
|
|
2051
|
-
phase,
|
|
2052
|
-
reason,
|
|
2053
|
-
status: "skipped",
|
|
2054
|
-
stepId,
|
|
2055
|
-
uiImpact
|
|
2056
|
-
});
|
|
2057
|
-
await writeReceipt(paths, stepId, `${label} skipped: ${reason}`);
|
|
2058
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2059
|
-
return buildSessionResponse(paths, {
|
|
2060
|
-
preconditions
|
|
2061
|
-
});
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
const promptFileName = `${stepId}.md`;
|
|
2065
|
-
const promptPath = path.join(paths.sessionRoot, "prompts", promptFileName);
|
|
2066
|
-
if (await fileExists(promptPath)) {
|
|
2067
|
-
const codexResultFailure = await requireCodexStepResult(paths, stepId, options.codexResult, preconditions);
|
|
2068
|
-
if (codexResultFailure) {
|
|
2069
|
-
return codexResultFailure;
|
|
2070
|
-
}
|
|
2071
|
-
await writeReceipt(paths, stepId, `${label} completed by Codex.`);
|
|
2072
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2073
|
-
return buildSessionResponse(paths, {
|
|
2074
|
-
preconditions
|
|
2075
|
-
});
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
1857
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
2079
1858
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
2080
1859
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
2081
|
-
const
|
|
2082
|
-
const prompt = await renderPrompt(paths, "deep_ui_check.md", {
|
|
1860
|
+
const prompt = await renderPrompt(paths, "deep_ui_check_run.md", {
|
|
2083
1861
|
changed_files: await changedFilesSinceBase(paths),
|
|
2084
1862
|
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
2085
1863
|
issue_number: issueNumberFromUrl(issueUrl),
|
|
2086
1864
|
issue_title: issueTitle,
|
|
2087
1865
|
issue_url: issueUrl,
|
|
2088
1866
|
phase,
|
|
2089
|
-
issue_details_file: path.join(paths.sessionRoot, "issue_details.md"),
|
|
2090
|
-
plan_file: planPath,
|
|
2091
|
-
ui_impact: uiImpact,
|
|
2092
1867
|
worktree: paths.worktree
|
|
2093
1868
|
});
|
|
2094
|
-
await writePromptArtifact(paths, promptFileName, prompt);
|
|
2095
1869
|
await writeUiCheckJson(paths, stepId, {
|
|
2096
1870
|
ok: true,
|
|
2097
1871
|
phase,
|
|
2098
|
-
promptPath,
|
|
2099
1872
|
status: "prompted",
|
|
2100
|
-
stepId
|
|
2101
|
-
uiImpact
|
|
1873
|
+
stepId
|
|
2102
1874
|
});
|
|
2103
1875
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
2104
1876
|
return buildSessionResponse(paths, {
|
|
@@ -2112,54 +1884,30 @@ async function runDeepUiCheck(paths, {
|
|
|
2112
1884
|
async function userCheck(paths, options = {}) {
|
|
2113
1885
|
const result = normalizeText(options.userCheck || options["user-check"]).toLowerCase();
|
|
2114
1886
|
if (result === "passed" || result === "pass" || result === "ok" || result === "yes") {
|
|
2115
|
-
await
|
|
1887
|
+
await writeStepRecord(paths, "user_check_completed", "User confirmed check passed.");
|
|
2116
1888
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2117
1889
|
return buildSessionResponse(paths);
|
|
2118
1890
|
}
|
|
2119
1891
|
if (result === "failed" || result === "fail" || result === "no") {
|
|
2120
|
-
const activeCycle = await readActiveCycle(paths);
|
|
2121
|
-
await writeCycleReceipt(paths, "user_check_failed", "User reported that manual verification failed.", {
|
|
2122
|
-
cycle: activeCycle
|
|
2123
|
-
});
|
|
2124
|
-
const reworkNotes = normalizeText(options.reworkNotes || options["rework-notes"]);
|
|
2125
|
-
if (!reworkNotes) {
|
|
2126
|
-
await markStatus(paths, SESSION_STATUS.BLOCKED);
|
|
2127
|
-
return buildSessionResponse(paths, {
|
|
2128
|
-
ok: false,
|
|
2129
|
-
errors: [
|
|
2130
|
-
createError({
|
|
2131
|
-
code: "user_check_failed",
|
|
2132
|
-
message: "User check failed. Provide rework notes to start a new plan cycle.",
|
|
2133
|
-
repairCommand: `jskit session ${paths.sessionId} step --user-check failed --rework-notes -`
|
|
2134
|
-
})
|
|
2135
|
-
],
|
|
2136
|
-
status: SESSION_STATUS.BLOCKED
|
|
2137
|
-
});
|
|
2138
|
-
}
|
|
2139
|
-
const nextCycle = nextCycleNumber(activeCycle);
|
|
2140
|
-
await writeTextFile(path.join(paths.sessionRoot, "cycles", `cycle_${nextCycle}`, "rework_request.md"), `${reworkNotes}\n`);
|
|
2141
|
-
await writeActiveCycle(paths, nextCycle);
|
|
2142
|
-
await writeCycleReceipt(paths, "cycle_started", `Started rework cycle ${nextCycle}.`, {
|
|
2143
|
-
cycle: nextCycle
|
|
2144
|
-
});
|
|
2145
|
-
await markCurrentStep(paths, "plan_made");
|
|
2146
1892
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2147
|
-
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
|
+
});
|
|
2148
1901
|
}
|
|
2149
1902
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
2150
1903
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
2151
1904
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
2152
|
-
const
|
|
2153
|
-
const prompt = await renderPrompt(paths, "user_check.md", {
|
|
1905
|
+
const prompt = await renderPrompt(paths, "user_check_completed.md", {
|
|
2154
1906
|
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
2155
1907
|
issue_title: issueTitle,
|
|
2156
|
-
issue_url: issueUrl
|
|
2157
|
-
issue_details_file: path.join(paths.sessionRoot, "issue_details.md"),
|
|
2158
|
-
issue_details_text: await readTrimmedFile(path.join(paths.sessionRoot, "issue_details.md")),
|
|
2159
|
-
plan_file: planPath,
|
|
2160
|
-
plan_text: planText
|
|
1908
|
+
issue_url: issueUrl
|
|
2161
1909
|
});
|
|
2162
|
-
await writePromptArtifact(paths, "
|
|
1910
|
+
await writePromptArtifact(paths, "user_check_completed", prompt);
|
|
2163
1911
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
2164
1912
|
return buildSessionResponse(paths, {
|
|
2165
1913
|
prompt,
|
|
@@ -2174,21 +1922,15 @@ async function readAcceptedChangesCommit(paths) {
|
|
|
2174
1922
|
|
|
2175
1923
|
async function commitAcceptedChanges(paths, _options = {}, context = {}) {
|
|
2176
1924
|
const preconditions = context.preconditions || [];
|
|
1925
|
+
const completeStep = context.completeStep !== false;
|
|
2177
1926
|
let commitInfo = await readAcceptedChangesCommit(paths);
|
|
2178
1927
|
|
|
2179
1928
|
if (!commitInfo?.commit) {
|
|
2180
1929
|
const result = await commitWorktree(paths, {
|
|
1930
|
+
allowNoChanges: true,
|
|
2181
1931
|
message: `Implement JSKIT session ${paths.sessionId}`
|
|
2182
1932
|
});
|
|
2183
1933
|
if (!result.ok) {
|
|
2184
|
-
if (result.output === "No changes found.") {
|
|
2185
|
-
return failSession(paths, {
|
|
2186
|
-
code: "accepted_changes_missing",
|
|
2187
|
-
message: "No accepted worktree changes found to commit.",
|
|
2188
|
-
repairCommand: `git -C ${paths.worktree} status --short`,
|
|
2189
|
-
preconditions
|
|
2190
|
-
});
|
|
2191
|
-
}
|
|
2192
1934
|
return failSession(paths, {
|
|
2193
1935
|
code: "accepted_changes_commit_failed",
|
|
2194
1936
|
message: result.output || "Failed to commit accepted changes.",
|
|
@@ -2199,41 +1941,56 @@ async function commitAcceptedChanges(paths, _options = {}, context = {}) {
|
|
|
2199
1941
|
commitInfo = {
|
|
2200
1942
|
changedFiles: result.changedFiles || [],
|
|
2201
1943
|
commit: await currentHead(paths),
|
|
2202
|
-
committedAt:
|
|
1944
|
+
committedAt: timestampForStepRecord(),
|
|
1945
|
+
noChanges: (result.changedFiles || []).length < 1
|
|
2203
1946
|
};
|
|
2204
1947
|
await writeTextFile(path.join(paths.sessionRoot, "changes_committed.json"), `${JSON.stringify(commitInfo, null, 2)}\n`);
|
|
2205
1948
|
}
|
|
2206
1949
|
|
|
2207
|
-
|
|
1950
|
+
const warnings = [];
|
|
1951
|
+
if (commitInfo.noChanges === true) {
|
|
1952
|
+
warnings.push({
|
|
1953
|
+
code: "accepted_changes_noop",
|
|
1954
|
+
message: "No accepted worktree changes were found; continuing without a new commit."
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
if (!completeStep) {
|
|
1958
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
1959
|
+
return buildSessionResponse(paths, {
|
|
1960
|
+
preconditions,
|
|
1961
|
+
warnings
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
await writeStepRecord(
|
|
1965
|
+
paths,
|
|
1966
|
+
"changes_committed",
|
|
1967
|
+
commitInfo.noChanges === true
|
|
1968
|
+
? "No accepted worktree changes were found; continued without a new commit."
|
|
1969
|
+
: `Committed accepted changes at ${commitInfo.commit || "unknown"}.`
|
|
1970
|
+
);
|
|
2208
1971
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2209
1972
|
return buildSessionResponse(paths, {
|
|
2210
|
-
preconditions
|
|
1973
|
+
preconditions,
|
|
1974
|
+
warnings
|
|
2211
1975
|
});
|
|
2212
1976
|
}
|
|
2213
1977
|
|
|
2214
|
-
async function updateBlueprint(paths,
|
|
1978
|
+
async function updateBlueprint(paths, _options = {}, context = {}) {
|
|
2215
1979
|
const preconditions = context.preconditions || [];
|
|
2216
1980
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
2217
1981
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
2218
1982
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
2219
1983
|
const issueNumber = issueNumberFromUrl(issueUrl);
|
|
2220
|
-
const
|
|
2221
|
-
const
|
|
2222
|
-
const agentDecisionsPath = path.join(paths.sessionRoot, "agent_decisions.md");
|
|
2223
|
-
const blueprintPath = path.join(paths.worktree, ".jskit", "APP_BLUEPRINT.md");
|
|
2224
|
-
const blueprintPromptPath = path.join(paths.sessionRoot, "prompts", "update_blueprint.md");
|
|
1984
|
+
const blueprintPath = path.join(paths.worktree, BLUEPRINT_RELATIVE_PATH);
|
|
1985
|
+
const blueprintSentinelPath = path.join(paths.sessionRoot, "metadata", "blueprint_updated_requested");
|
|
2225
1986
|
|
|
2226
|
-
if (await fileExists(
|
|
2227
|
-
const codexResultFailure = await requireCodexStepResult(paths, "blueprint_updated", options.codexResult, preconditions);
|
|
2228
|
-
if (codexResultFailure) {
|
|
2229
|
-
return codexResultFailure;
|
|
2230
|
-
}
|
|
1987
|
+
if (context.completeStep !== false && await fileExists(blueprintSentinelPath)) {
|
|
2231
1988
|
const changedFiles = await changedFilesInWorktree(paths);
|
|
2232
|
-
const unexpectedChanges =
|
|
1989
|
+
const unexpectedChanges = await unexpectedBlueprintStepChanges(paths, changedFiles);
|
|
2233
1990
|
if (unexpectedChanges.length > 0) {
|
|
2234
1991
|
return failSession(paths, {
|
|
2235
1992
|
code: "blueprint_unexpected_changes",
|
|
2236
|
-
message: `The blueprint step changed files outside
|
|
1993
|
+
message: `The blueprint step changed files outside ${BLUEPRINT_RELATIVE_PATH}: ${unexpectedChanges.join(", ")}`,
|
|
2237
1994
|
repairCommand: `git -C ${paths.worktree} status --short`,
|
|
2238
1995
|
preconditions
|
|
2239
1996
|
});
|
|
@@ -2249,32 +2006,10 @@ async function updateBlueprint(paths, options = {}, context = {}) {
|
|
|
2249
2006
|
});
|
|
2250
2007
|
}
|
|
2251
2008
|
|
|
2252
|
-
if (changedFiles.includes(
|
|
2253
|
-
|
|
2254
|
-
timeout: 15000
|
|
2255
|
-
});
|
|
2256
|
-
if (!addResult.ok) {
|
|
2257
|
-
return failSession(paths, {
|
|
2258
|
-
code: "blueprint_stage_failed",
|
|
2259
|
-
message: addResult.output || "Failed to stage app blueprint update.",
|
|
2260
|
-
repairCommand: `git -C ${paths.worktree} add .jskit/APP_BLUEPRINT.md`,
|
|
2261
|
-
preconditions
|
|
2262
|
-
});
|
|
2263
|
-
}
|
|
2264
|
-
const commitResult = await runGitInWorktree(paths.worktree, ["commit", "-m", `Update app blueprint for ${paths.sessionId}`], {
|
|
2265
|
-
timeout: 1000 * 60
|
|
2266
|
-
});
|
|
2267
|
-
if (!commitResult.ok) {
|
|
2268
|
-
return failSession(paths, {
|
|
2269
|
-
code: "blueprint_commit_failed",
|
|
2270
|
-
message: commitResult.output || "Failed to commit app blueprint update.",
|
|
2271
|
-
repairCommand: `git -C ${paths.worktree} status --short`,
|
|
2272
|
-
preconditions
|
|
2273
|
-
});
|
|
2274
|
-
}
|
|
2275
|
-
await writeReceipt(paths, "blueprint_updated", "Codex updated and JSKIT committed the app blueprint.");
|
|
2009
|
+
if (changedFiles.includes(BLUEPRINT_RELATIVE_PATH)) {
|
|
2010
|
+
await writeStepRecord(paths, "blueprint_updated", "Codex updated the app blueprint; JSKIT will include it in the accepted changes commit.");
|
|
2276
2011
|
} else {
|
|
2277
|
-
await
|
|
2012
|
+
await writeStepRecord(paths, "blueprint_updated", "Codex reviewed the app blueprint; no blueprint changes were needed.");
|
|
2278
2013
|
}
|
|
2279
2014
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2280
2015
|
return buildSessionResponse(paths, {
|
|
@@ -2283,19 +2018,17 @@ async function updateBlueprint(paths, options = {}, context = {}) {
|
|
|
2283
2018
|
});
|
|
2284
2019
|
}
|
|
2285
2020
|
|
|
2286
|
-
|
|
2287
|
-
|
|
2021
|
+
await writeBlueprintBaseline(paths);
|
|
2022
|
+
const prompt = await renderPrompt(paths, "blueprint_updated.md", {
|
|
2288
2023
|
app_blueprint_file: blueprintPath,
|
|
2289
2024
|
changed_files: await changedFilesSinceBase(paths),
|
|
2290
2025
|
issue_file: path.join(paths.sessionRoot, "issue.md"),
|
|
2291
2026
|
issue_number: issueNumber,
|
|
2292
2027
|
issue_title: issueTitle,
|
|
2293
2028
|
issue_url: issueUrl,
|
|
2294
|
-
issue_details_file: issueDetailsPath,
|
|
2295
|
-
plan_file: planPath,
|
|
2296
2029
|
worktree: paths.worktree
|
|
2297
2030
|
});
|
|
2298
|
-
await
|
|
2031
|
+
await writeTextFile(blueprintSentinelPath, "true\n");
|
|
2299
2032
|
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
2300
2033
|
return buildSessionResponse(paths, {
|
|
2301
2034
|
codex: BLUEPRINT_CODEX_HANDOFF,
|
|
@@ -2386,13 +2119,10 @@ async function readReviewPassSummaries(paths) {
|
|
|
2386
2119
|
.join("\n");
|
|
2387
2120
|
}
|
|
2388
2121
|
|
|
2389
|
-
async function
|
|
2122
|
+
async function renderPullRequestFilePrompt(paths, context = {}) {
|
|
2390
2123
|
const preconditions = context.preconditions || [];
|
|
2391
2124
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
2392
2125
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title"));
|
|
2393
|
-
const issueDetails = await readTrimmedFile(path.join(paths.sessionRoot, "issue_details.md"));
|
|
2394
|
-
const { planText } = await readCurrentPlan(paths);
|
|
2395
|
-
const agentDecisions = await readTextIfExists(path.join(paths.sessionRoot, "agent_decisions.md"));
|
|
2396
2126
|
const filesChanged = await changedFilesSinceBase(paths);
|
|
2397
2127
|
const commits = await commitLinesSinceBase(paths);
|
|
2398
2128
|
const checks = await readCheckSummaries(paths);
|
|
@@ -2400,80 +2130,43 @@ async function createFinalReport(paths, _options = {}, context = {}) {
|
|
|
2400
2130
|
const reviewPasses = await readReviewPassSummaries(paths);
|
|
2401
2131
|
const commandLogPath = path.join(paths.sessionRoot, "command_log.jsonl");
|
|
2402
2132
|
const blueprintStatus = await readTextIfExists(path.join(paths.sessionRoot, "steps", "blueprint_updated"));
|
|
2403
|
-
const userCheck = await readTextIfExists(path.join(paths.sessionRoot, "steps",
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
"",
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
"",
|
|
2410
|
-
"
|
|
2411
|
-
"",
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
"",
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
"
|
|
2419
|
-
"",
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
"## Command Log",
|
|
2439
|
-
"",
|
|
2440
|
-
await fileExists(commandLogPath) ? commandLogPath : "No command log recorded.",
|
|
2441
|
-
"",
|
|
2442
|
-
"## User Check",
|
|
2443
|
-
"",
|
|
2444
|
-
userCheck.trim() || "No user check receipt recorded.",
|
|
2445
|
-
"",
|
|
2446
|
-
"## Blueprint",
|
|
2447
|
-
"",
|
|
2448
|
-
blueprintStatus.trim() || "No blueprint receipt recorded.",
|
|
2449
|
-
"",
|
|
2450
|
-
"## Remaining Unverified Gaps",
|
|
2451
|
-
"",
|
|
2452
|
-
"Review the check and UI check sections above; no additional gaps were recorded by JSKIT.",
|
|
2453
|
-
"",
|
|
2454
|
-
"## Decisions",
|
|
2455
|
-
"",
|
|
2456
|
-
agentDecisions.trim() || "No decision log recorded.",
|
|
2457
|
-
""
|
|
2458
|
-
].join("\n");
|
|
2459
|
-
const reportPath = path.join(paths.sessionRoot, "final_report.md");
|
|
2460
|
-
await writeTextFile(reportPath, report);
|
|
2461
|
-
if (issueUrl) {
|
|
2462
|
-
const commentResult = await commentOnIssueOnce(paths, {
|
|
2463
|
-
bodyFile: reportPath,
|
|
2464
|
-
issueUrl,
|
|
2465
|
-
purpose: "final_report"
|
|
2466
|
-
});
|
|
2467
|
-
if (!commentResult.ok) {
|
|
2468
|
-
return failSession(paths, {
|
|
2469
|
-
code: "final_report_comment_failed",
|
|
2470
|
-
message: commentResult.output || "Failed to comment final report on the GitHub issue.",
|
|
2471
|
-
repairCommand: `gh issue comment ${issueUrl} --body-file ${reportPath}`,
|
|
2472
|
-
preconditions
|
|
2473
|
-
});
|
|
2474
|
-
}
|
|
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);
|
|
2475
2168
|
}
|
|
2476
|
-
await
|
|
2169
|
+
await writeStepRecord(paths, "final_report_created", "Pull request file is ready for review and submission.");
|
|
2477
2170
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2478
2171
|
return buildSessionResponse(paths, {
|
|
2479
2172
|
preconditions
|
|
@@ -2494,6 +2187,140 @@ function parseJsonObject(value) {
|
|
|
2494
2187
|
}
|
|
2495
2188
|
}
|
|
2496
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
|
+
|
|
2497
2324
|
async function readPrState(paths, prUrl) {
|
|
2498
2325
|
const prRef = normalizeText(prUrl);
|
|
2499
2326
|
const args = prRef
|
|
@@ -2597,7 +2424,7 @@ async function removeSessionWorktree(paths) {
|
|
|
2597
2424
|
|
|
2598
2425
|
async function writePrOutcome(paths, outcome) {
|
|
2599
2426
|
await writeTextFile(path.join(paths.sessionRoot, "pr_outcome.json"), `${JSON.stringify({
|
|
2600
|
-
recordedAt:
|
|
2427
|
+
recordedAt: timestampForStepRecord(),
|
|
2601
2428
|
...outcome
|
|
2602
2429
|
}, null, 2)}\n`);
|
|
2603
2430
|
}
|
|
@@ -2608,7 +2435,7 @@ function mainCheckoutSyncPath(paths) {
|
|
|
2608
2435
|
|
|
2609
2436
|
async function writeMainCheckoutSync(paths, payload = {}) {
|
|
2610
2437
|
await writeTextFile(mainCheckoutSyncPath(paths), `${JSON.stringify({
|
|
2611
|
-
recordedAt:
|
|
2438
|
+
recordedAt: timestampForStepRecord(),
|
|
2612
2439
|
...payload
|
|
2613
2440
|
}, null, 2)}\n`);
|
|
2614
2441
|
}
|
|
@@ -2674,36 +2501,33 @@ async function updateLocalBaseBranch(paths, baseBranch = "") {
|
|
|
2674
2501
|
}
|
|
2675
2502
|
|
|
2676
2503
|
async function syncMainCheckout(paths, options = {}, context = {}) {
|
|
2504
|
+
const prUrl = await readTrimmedFile(path.join(paths.sessionRoot, "pr_url"));
|
|
2677
2505
|
const prOutcome = parseJsonObject(await readTextIfExists(path.join(paths.sessionRoot, "pr_outcome.json")));
|
|
2678
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
|
+
}
|
|
2679
2515
|
if (!prOutcome?.outcome) {
|
|
2680
|
-
return
|
|
2516
|
+
return sessionStepError(paths, {
|
|
2681
2517
|
code: "pr_outcome_missing",
|
|
2682
|
-
message: "Cannot sync the main checkout before PR
|
|
2683
|
-
|
|
2684
|
-
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`
|
|
2685
2520
|
});
|
|
2686
2521
|
}
|
|
2687
2522
|
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
? skipReason
|
|
2695
|
-
: `PR outcome is ${prOutcome.outcome}; no main checkout sync is required.`;
|
|
2696
|
-
await writeMainCheckoutSync(paths, {
|
|
2697
|
-
branch: prOutcome.baseBranch || "",
|
|
2698
|
-
outcome: prOutcome.outcome,
|
|
2699
|
-
reason,
|
|
2700
|
-
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`
|
|
2701
2529
|
});
|
|
2702
|
-
await writeReceipt(paths, "main_checkout_synced", `Main checkout sync skipped: ${reason}`);
|
|
2703
|
-
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2704
|
-
return buildSessionResponse(paths);
|
|
2705
2530
|
}
|
|
2706
|
-
|
|
2707
2531
|
const baseBranch = prOutcome.baseBranch || await readTrimmedFile(path.join(paths.sessionRoot, "pr_base_branch"));
|
|
2708
2532
|
const syncFailure = await updateLocalBaseBranch(paths, baseBranch);
|
|
2709
2533
|
if (syncFailure) {
|
|
@@ -2716,9 +2540,13 @@ async function syncMainCheckout(paths, options = {}, context = {}) {
|
|
|
2716
2540
|
outcome: prOutcome.outcome,
|
|
2717
2541
|
status: "synced"
|
|
2718
2542
|
});
|
|
2719
|
-
|
|
2543
|
+
if (completeStep) {
|
|
2544
|
+
await writeStepRecord(paths, "main_checkout_synced", `Fast-forwarded target checkout branch ${branch}.`);
|
|
2545
|
+
}
|
|
2720
2546
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2721
|
-
return buildSessionResponse(paths
|
|
2547
|
+
return buildSessionResponse(paths, {
|
|
2548
|
+
preconditions
|
|
2549
|
+
});
|
|
2722
2550
|
}
|
|
2723
2551
|
|
|
2724
2552
|
async function updateHelperMapBeforePr(paths) {
|
|
@@ -2802,13 +2630,35 @@ async function updateHelperMapBeforePr(paths) {
|
|
|
2802
2630
|
};
|
|
2803
2631
|
}
|
|
2804
2632
|
|
|
2805
|
-
async function createPr(paths) {
|
|
2806
|
-
const
|
|
2807
|
-
|
|
2808
|
-
|
|
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
|
+
}
|
|
2655
|
+
const helperMapResult = await updateHelperMapBeforePr(paths);
|
|
2656
|
+
if (!helperMapResult.ok) {
|
|
2657
|
+
return failSession(paths, {
|
|
2809
2658
|
code: helperMapResult.code,
|
|
2810
2659
|
message: helperMapResult.message,
|
|
2811
|
-
repairCommand: helperMapResult.repairCommand
|
|
2660
|
+
repairCommand: helperMapResult.repairCommand,
|
|
2661
|
+
preconditions
|
|
2812
2662
|
});
|
|
2813
2663
|
}
|
|
2814
2664
|
|
|
@@ -2820,34 +2670,30 @@ async function createPr(paths) {
|
|
|
2820
2670
|
return failSession(paths, {
|
|
2821
2671
|
code: "branch_push_failed",
|
|
2822
2672
|
message: pushResult.output || "Failed to push session branch.",
|
|
2823
|
-
repairCommand: `git -C ${paths.worktree} push -u origin HEAD
|
|
2673
|
+
repairCommand: `git -C ${paths.worktree} push -u origin HEAD`,
|
|
2674
|
+
preconditions
|
|
2824
2675
|
});
|
|
2825
2676
|
}
|
|
2826
2677
|
const existingPrState = await readCurrentBranchPrState(paths);
|
|
2827
2678
|
if (existingPrState.ok && existingPrState.url && !prStateIsClosed(existingPrState)) {
|
|
2828
2679
|
await writeTextFile(path.join(paths.sessionRoot, "pr_url"), existingPrState.url);
|
|
2829
|
-
|
|
2680
|
+
if (completeStep) {
|
|
2681
|
+
await writeStepRecord(paths, "pr_created", `Pushed branch ${paths.branch} and reused existing PR ${existingPrState.url}. ${helperMapResult.message}`);
|
|
2682
|
+
}
|
|
2830
2683
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2831
|
-
return buildSessionResponse(paths
|
|
2684
|
+
return buildSessionResponse(paths, {
|
|
2685
|
+
preconditions
|
|
2686
|
+
});
|
|
2832
2687
|
}
|
|
2833
|
-
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
2834
2688
|
const issueText = await readTrimmedFile(path.join(paths.sessionRoot, "issue.md"));
|
|
2835
2689
|
const issueTitle = await readTrimmedFile(path.join(paths.sessionRoot, "issue_title")) || titleFromIssue(issueText);
|
|
2836
|
-
const issueNumber = issueNumberFromUrl(issueUrl);
|
|
2837
|
-
const body = [
|
|
2838
|
-
issueNumber ? `Closes #${issueNumber}` : "",
|
|
2839
|
-
"",
|
|
2840
|
-
issueText
|
|
2841
|
-
].join("\n").trim();
|
|
2842
|
-
const bodyPath = path.join(paths.sessionRoot, "pr_body.md");
|
|
2843
|
-
await writeTextFile(bodyPath, body);
|
|
2844
2690
|
const result = await runLoggedCommand(paths, "github_pr_create", "gh", [
|
|
2845
2691
|
"pr",
|
|
2846
2692
|
"create",
|
|
2847
2693
|
"--title",
|
|
2848
2694
|
issueTitle,
|
|
2849
2695
|
"--body-file",
|
|
2850
|
-
|
|
2696
|
+
pullRequestPath
|
|
2851
2697
|
], {
|
|
2852
2698
|
cwd: paths.worktree,
|
|
2853
2699
|
timeout: 1000 * 60
|
|
@@ -2856,144 +2702,87 @@ async function createPr(paths) {
|
|
|
2856
2702
|
const fallbackPrState = await readCurrentBranchPrState(paths);
|
|
2857
2703
|
if (fallbackPrState.ok && fallbackPrState.url && !prStateIsClosed(fallbackPrState)) {
|
|
2858
2704
|
await writeTextFile(path.join(paths.sessionRoot, "pr_url"), fallbackPrState.url);
|
|
2859
|
-
|
|
2705
|
+
if (completeStep) {
|
|
2706
|
+
await writeStepRecord(paths, "pr_created", `Pushed branch ${paths.branch} and reused existing PR ${fallbackPrState.url}. ${helperMapResult.message}`);
|
|
2707
|
+
}
|
|
2860
2708
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2861
|
-
return buildSessionResponse(paths
|
|
2709
|
+
return buildSessionResponse(paths, {
|
|
2710
|
+
preconditions
|
|
2711
|
+
});
|
|
2862
2712
|
}
|
|
2863
2713
|
const prompt = await renderPrompt(paths, "pr_failure.md", {
|
|
2864
2714
|
doctor_output: result.output
|
|
2865
2715
|
});
|
|
2866
|
-
await writePromptArtifact(paths, "pr_create_failure
|
|
2716
|
+
await writePromptArtifact(paths, "pr_create_failure", prompt);
|
|
2867
2717
|
return failSession(paths, {
|
|
2868
2718
|
code: "pr_create_failed",
|
|
2869
2719
|
message: result.output || "Failed to create PR.",
|
|
2870
2720
|
repairCommand: "gh pr create",
|
|
2721
|
+
preconditions,
|
|
2871
2722
|
prompt
|
|
2872
2723
|
});
|
|
2873
2724
|
}
|
|
2874
2725
|
const prUrl = result.stdout.split(/\r?\n/u).map((line) => line.trim()).find(Boolean) || result.stdout;
|
|
2875
2726
|
await writeTextFile(path.join(paths.sessionRoot, "pr_url"), prUrl);
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
return buildSessionResponse(paths);
|
|
2879
|
-
}
|
|
2880
|
-
|
|
2881
|
-
async function closePrWithoutMerge(paths, prUrl, options = {}) {
|
|
2882
|
-
const reason = normalizeText(options.closeReason || options["close-reason"]) || "User skipped merge in JSKIT Studio.";
|
|
2883
|
-
const prState = await readPrState(paths, prUrl);
|
|
2884
|
-
if (!prState.ok) {
|
|
2885
|
-
return failSession(paths, {
|
|
2886
|
-
code: "pr_state_failed",
|
|
2887
|
-
message: prState.output || "Failed to inspect PR before closing without merge.",
|
|
2888
|
-
repairCommand: `gh pr view ${prUrl} --json state,mergedAt,url,baseRefName`
|
|
2889
|
-
});
|
|
2890
|
-
}
|
|
2891
|
-
if (prStateIsMerged(prState)) {
|
|
2892
|
-
return failSession(paths, {
|
|
2893
|
-
code: "pr_already_merged",
|
|
2894
|
-
message: "Cannot finish without merging because the PR is already merged.",
|
|
2895
|
-
repairCommand: `jskit session ${paths.sessionId} step`
|
|
2896
|
-
});
|
|
2897
|
-
}
|
|
2898
|
-
if (!prStateIsClosed(prState)) {
|
|
2899
|
-
const commentResult = await runLoggedCommand(paths, "github_pr_comment", "gh", ["pr", "comment", prUrl, "--body", `JSKIT session ${paths.sessionId} finished without merging. Reason: ${reason}`], {
|
|
2900
|
-
cwd: paths.targetRoot,
|
|
2901
|
-
timeout: 1000 * 60
|
|
2902
|
-
});
|
|
2903
|
-
if (!commentResult.ok) {
|
|
2904
|
-
return failSession(paths, {
|
|
2905
|
-
code: "pr_comment_failed",
|
|
2906
|
-
message: commentResult.output || "Failed to comment on PR before finishing without merge.",
|
|
2907
|
-
repairCommand: `gh pr comment ${prUrl} --body "<reason>"`
|
|
2908
|
-
});
|
|
2909
|
-
}
|
|
2727
|
+
if (completeStep) {
|
|
2728
|
+
await writeStepRecord(paths, "pr_created", `Pushed branch ${paths.branch} and created PR ${prUrl}. ${helperMapResult.message}`);
|
|
2910
2729
|
}
|
|
2911
|
-
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
2912
|
-
if (issueUrl) {
|
|
2913
|
-
await runLoggedCommand(paths, "github_issue_comment", "gh", ["issue", "comment", issueUrl, "--body", `JSKIT session ${paths.sessionId} finished without merging PR ${prUrl}. Reason: ${reason}`], {
|
|
2914
|
-
cwd: paths.targetRoot,
|
|
2915
|
-
timeout: 1000 * 60
|
|
2916
|
-
});
|
|
2917
|
-
}
|
|
2918
|
-
await writePrOutcome(paths, {
|
|
2919
|
-
issueUrl,
|
|
2920
|
-
outcome: "closed_without_merge",
|
|
2921
|
-
prUrl,
|
|
2922
|
-
prState: prState.state,
|
|
2923
|
-
reason
|
|
2924
|
-
});
|
|
2925
|
-
await writeReceipt(paths, "pr_finalized", `Finished without merging PR ${prUrl}; PR left open. Reason: ${reason}`);
|
|
2926
2730
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2927
|
-
return buildSessionResponse(paths
|
|
2731
|
+
return buildSessionResponse(paths, {
|
|
2732
|
+
preconditions
|
|
2733
|
+
});
|
|
2928
2734
|
}
|
|
2929
2735
|
|
|
2930
2736
|
async function preparePrMerge(paths, options = {}, context = {}) {
|
|
2931
2737
|
const preconditions = context.preconditions || [];
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
const baseBranch = await readTrimmedFile(path.join(paths.sessionRoot, "pr_base_branch")) ||
|
|
2940
|
-
await readTrimmedFile(path.join(paths.sessionRoot, "base_branch")) ||
|
|
2941
|
-
await currentTargetBranch(paths.targetRoot);
|
|
2942
|
-
const prompt = await renderPrompt(paths, "prepare_pr_merge.md", {
|
|
2943
|
-
base_branch: baseBranch,
|
|
2944
|
-
final_report_file: path.join(paths.sessionRoot, "final_report.md"),
|
|
2945
|
-
issue_url: await readTrimmedFile(path.join(paths.sessionRoot, "issue_url")),
|
|
2946
|
-
pr_url: prUrl,
|
|
2947
|
-
target_root: paths.targetRoot
|
|
2948
|
-
});
|
|
2949
|
-
await writePromptArtifact(paths, "prepare_pr_merge.md", prompt);
|
|
2950
|
-
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
2951
|
-
return buildSessionResponse(paths, {
|
|
2952
|
-
codex: PR_MERGE_PREP_CODEX_HANDOFF,
|
|
2953
|
-
ok: true,
|
|
2954
|
-
preconditions,
|
|
2955
|
-
prompt,
|
|
2956
|
-
status: SESSION_STATUS.WAITING_FOR_USER
|
|
2957
|
-
});
|
|
2958
|
-
}
|
|
2959
|
-
|
|
2960
|
-
if (!continueToMerge) {
|
|
2961
|
-
return failSession(paths, {
|
|
2962
|
-
code: "pr_merge_prepare_decision_required",
|
|
2963
|
-
message: "Choose whether to ask Codex to prepare the PR for merge or continue to the merge decision.",
|
|
2964
|
-
repairCommand: `jskit session ${paths.sessionId} step --continue-to-merge true`,
|
|
2965
|
-
preconditions
|
|
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`
|
|
2966
2745
|
});
|
|
2967
2746
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
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);
|
|
2971
2759
|
return buildSessionResponse(paths, {
|
|
2972
|
-
|
|
2760
|
+
codex: PR_MERGE_PREP_CODEX_HANDOFF,
|
|
2761
|
+
ok: true,
|
|
2762
|
+
preconditions,
|
|
2763
|
+
prompt,
|
|
2764
|
+
status: SESSION_STATUS.WAITING_FOR_USER
|
|
2973
2765
|
});
|
|
2974
2766
|
}
|
|
2975
2767
|
|
|
2976
2768
|
async function finalizePr(paths, options = {}, context = {}) {
|
|
2977
2769
|
const preconditions = context.preconditions || [];
|
|
2770
|
+
const completeStep = context.completeStep !== false;
|
|
2978
2771
|
const prUrl = await readTrimmedFile(path.join(paths.sessionRoot, "pr_url"));
|
|
2979
|
-
const closeWithoutMerge = options.closeWithoutMerge === true ||
|
|
2980
|
-
normalizeText(options["close-without-merge"]).toLowerCase() === "true" ||
|
|
2981
|
-
options.skipMerge === true ||
|
|
2982
|
-
normalizeText(options["skip-merge"]).toLowerCase() === "true";
|
|
2983
|
-
if (closeWithoutMerge) {
|
|
2984
|
-
const guardResult = await runSessionFinalizationGuard(paths, preconditions);
|
|
2985
|
-
if (!guardResult.ok) {
|
|
2986
|
-
return guardResult.response;
|
|
2987
|
-
}
|
|
2988
|
-
return closePrWithoutMerge(paths, prUrl, options);
|
|
2989
|
-
}
|
|
2990
2772
|
const mergePr = options.mergePr === true ||
|
|
2991
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
|
+
}
|
|
2992
2781
|
if (!mergePr) {
|
|
2993
2782
|
return failSession(paths, {
|
|
2994
2783
|
code: "pr_finalize_decision_required",
|
|
2995
2784
|
message: "Choose whether to merge the PR or skip merge.",
|
|
2996
|
-
repairCommand: `jskit session ${paths.sessionId}
|
|
2785
|
+
repairCommand: `jskit session ${paths.sessionId} merge_pr`,
|
|
2997
2786
|
preconditions
|
|
2998
2787
|
});
|
|
2999
2788
|
}
|
|
@@ -3028,7 +2817,7 @@ async function finalizePr(paths, options = {}, context = {}) {
|
|
|
3028
2817
|
const prompt = await renderPrompt(paths, "pr_failure.md", {
|
|
3029
2818
|
doctor_output: mergeResult?.output || existingPrState.output
|
|
3030
2819
|
});
|
|
3031
|
-
await writePromptArtifact(paths, "pr_merge_failure
|
|
2820
|
+
await writePromptArtifact(paths, "pr_merge_failure", prompt);
|
|
3032
2821
|
return failSession(paths, {
|
|
3033
2822
|
code: "pr_merge_failed",
|
|
3034
2823
|
message: mergeResult?.output || existingPrState.output || "Failed to merge PR.",
|
|
@@ -3052,9 +2841,13 @@ async function finalizePr(paths, options = {}, context = {}) {
|
|
|
3052
2841
|
});
|
|
3053
2842
|
await writeTextFile(mergeMarkerPath, `${prUrl}\n`);
|
|
3054
2843
|
}
|
|
3055
|
-
|
|
2844
|
+
if (completeStep) {
|
|
2845
|
+
await writeStepRecord(paths, "pr_merge_prepared", `Merged PR ${prUrl}.`);
|
|
2846
|
+
}
|
|
3056
2847
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
3057
|
-
return buildSessionResponse(paths
|
|
2848
|
+
return buildSessionResponse(paths, {
|
|
2849
|
+
preconditions
|
|
2850
|
+
});
|
|
3058
2851
|
}
|
|
3059
2852
|
|
|
3060
2853
|
async function finishSession(paths) {
|
|
@@ -3075,14 +2868,15 @@ async function finishSession(paths) {
|
|
|
3075
2868
|
session_id: paths.sessionId,
|
|
3076
2869
|
transcript_log: path.join(paths.completedSessionRoot, "transcript.log")
|
|
3077
2870
|
});
|
|
3078
|
-
|
|
2871
|
+
const finalCommentPath = path.join(paths.sessionRoot, "final_comment");
|
|
2872
|
+
await writeTextFile(finalCommentPath, prompt);
|
|
3079
2873
|
if (issueUrl) {
|
|
3080
|
-
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], {
|
|
3081
2875
|
cwd: paths.targetRoot,
|
|
3082
2876
|
timeout: 1000 * 60
|
|
3083
2877
|
});
|
|
3084
2878
|
}
|
|
3085
|
-
await
|
|
2879
|
+
await writeStepRecord(paths, "session_finished", `Removed worktree ${paths.worktree} and finished session ${paths.sessionId} with PR outcome ${prOutcome?.outcome || "unknown"}.`);
|
|
3086
2880
|
await markStatus(paths, SESSION_STATUS.FINISHED);
|
|
3087
2881
|
await markCurrentStep(paths, "");
|
|
3088
2882
|
const archivedPaths = await archiveSession(paths, "completed");
|
|
@@ -3095,17 +2889,14 @@ const STEP_RUNNERS = Object.freeze({
|
|
|
3095
2889
|
worktree_created: createWorktree,
|
|
3096
2890
|
dependencies_installed: installDependencies,
|
|
3097
2891
|
issue_prompt_rendered: renderIssuePrompt,
|
|
3098
|
-
issue_drafted: draftIssue,
|
|
3099
2892
|
issue_created: createIssue,
|
|
3100
|
-
|
|
2893
|
+
issue_submitted: submitIssue,
|
|
3101
2894
|
plan_made: makePlan,
|
|
3102
2895
|
plan_executed: renderPlanExecutionPrompt,
|
|
3103
2896
|
automated_checks_run: (paths, options, context) => runAutomatedChecks(paths, {
|
|
3104
|
-
label: "Automated checks",
|
|
3105
2897
|
stepId: "automated_checks_run"
|
|
3106
2898
|
}, options, context),
|
|
3107
2899
|
deep_ui_check_run: (paths, options, context) => runDeepUiCheck(paths, {
|
|
3108
|
-
label: "Deep UI check",
|
|
3109
2900
|
phase: "pre_review",
|
|
3110
2901
|
stepId: "deep_ui_check_run"
|
|
3111
2902
|
}, options, context),
|
|
@@ -3114,10 +2905,9 @@ const STEP_RUNNERS = Object.freeze({
|
|
|
3114
2905
|
user_check_completed: userCheck,
|
|
3115
2906
|
changes_committed: commitAcceptedChanges,
|
|
3116
2907
|
blueprint_updated: updateBlueprint,
|
|
3117
|
-
final_report_created:
|
|
2908
|
+
final_report_created: createPullRequestFile,
|
|
3118
2909
|
pr_created: createPr,
|
|
3119
2910
|
pr_merge_prepared: preparePrMerge,
|
|
3120
|
-
pr_finalized: finalizePr,
|
|
3121
2911
|
main_checkout_synced: syncMainCheckout,
|
|
3122
2912
|
session_finished: finishSession
|
|
3123
2913
|
});
|
|
@@ -3126,21 +2916,19 @@ const PRECONDITION_RUNNERS = Object.freeze({
|
|
|
3126
2916
|
accepted_changes_committed: assertAcceptedChangesCommitted,
|
|
3127
2917
|
active_cycle_exists: assertActiveCycleExists,
|
|
3128
2918
|
active_cycle_user_check_passed: assertActiveCycleUserCheckPassed,
|
|
2919
|
+
user_check_passed: assertUserCheckPassed,
|
|
3129
2920
|
blueprint_update_satisfied: assertBlueprintUpdateSatisfied,
|
|
3130
2921
|
deep_ui_check_satisfied: assertDeepUiCheckSatisfied,
|
|
3131
2922
|
dependencies_installed: assertDependenciesInstalled,
|
|
3132
|
-
|
|
2923
|
+
pull_request_file_exists: assertPullRequestFileExists,
|
|
3133
2924
|
git_current_branch: (paths) => assertGitCurrentBranch(paths.targetRoot),
|
|
3134
2925
|
git_repository: (paths) => assertGitRepository(paths.targetRoot),
|
|
3135
2926
|
github_auth: (paths) => assertGhAuth(paths.targetRoot),
|
|
3136
2927
|
github_origin: (paths) => assertGithubOrigin(paths.targetRoot),
|
|
3137
|
-
issue_metadata_exists: assertIssueMetadataExists,
|
|
3138
2928
|
issue_text_exists: assertIssueTextExists,
|
|
3139
2929
|
issue_url_exists: assertIssueUrlExists,
|
|
3140
2930
|
automated_checks_passed: assertAutomatedChecksPassed,
|
|
3141
|
-
issue_details_exists: assertIssueDetailsExists,
|
|
3142
2931
|
main_checkout_sync_satisfied: assertMainCheckoutSyncSatisfied,
|
|
3143
|
-
plan_text_exists: assertPlanTextExists,
|
|
3144
2932
|
pr_url_exists: assertPrUrlExists,
|
|
3145
2933
|
ready_jskit_app: assertReadyJskitApp,
|
|
3146
2934
|
session_exists: assertSessionExists,
|
|
@@ -3156,10 +2944,171 @@ async function runNamedPreconditions(paths, names = []) {
|
|
|
3156
2944
|
);
|
|
3157
2945
|
}
|
|
3158
2946
|
|
|
3159
|
-
|
|
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({
|
|
3160
3061
|
targetRoot = process.cwd(),
|
|
3161
3062
|
sessionId,
|
|
3063
|
+
action,
|
|
3162
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
|
|
3163
3112
|
} = {}) {
|
|
3164
3113
|
return withExistingSession({ targetRoot, sessionId }, async (paths) => {
|
|
3165
3114
|
const artifacts = await readSessionArtifacts(paths);
|
|
@@ -3185,13 +3134,401 @@ async function runSessionStep({
|
|
|
3185
3134
|
if (!nextStep) {
|
|
3186
3135
|
return finishSession(paths);
|
|
3187
3136
|
}
|
|
3188
|
-
if (nextStep === "
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
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
|
|
3294
|
+
});
|
|
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`
|
|
3193
3493
|
});
|
|
3194
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
|
+
}
|
|
3195
3532
|
const runner = STEP_RUNNERS[nextStep];
|
|
3196
3533
|
if (typeof runner !== "function") {
|
|
3197
3534
|
return failSession(paths, {
|
|
@@ -3200,6 +3537,9 @@ async function runSessionStep({
|
|
|
3200
3537
|
status: SESSION_STATUS.FAILED
|
|
3201
3538
|
});
|
|
3202
3539
|
}
|
|
3540
|
+
if (skipStepRequested(options)) {
|
|
3541
|
+
return skipCurrentStep(paths, nextStep, options);
|
|
3542
|
+
}
|
|
3203
3543
|
const stepPreconditions = await runNamedPreconditions(paths, STEP_PRECONDITION_NAMES[nextStep] || ["session_exists"]);
|
|
3204
3544
|
if (!stepPreconditions.ok) {
|
|
3205
3545
|
return failSession(paths, {
|
|
@@ -3207,7 +3547,6 @@ async function runSessionStep({
|
|
|
3207
3547
|
preconditions: stepPreconditions.preconditions
|
|
3208
3548
|
});
|
|
3209
3549
|
}
|
|
3210
|
-
await appendAgentDecisionsInput(paths, options);
|
|
3211
3550
|
return runner(paths, options, {
|
|
3212
3551
|
preconditions: stepPreconditions.preconditions
|
|
3213
3552
|
});
|
|
@@ -3248,7 +3587,7 @@ async function abandonSession({
|
|
|
3248
3587
|
}
|
|
3249
3588
|
await writeTextFile(
|
|
3250
3589
|
path.join(paths.sessionRoot, "steps", "abandoned"),
|
|
3251
|
-
`${
|
|
3590
|
+
`${timestampForStepRecord()}\nAbandoned session ${paths.sessionId}.`
|
|
3252
3591
|
);
|
|
3253
3592
|
await markStatus(paths, SESSION_STATUS.ABANDONED);
|
|
3254
3593
|
await markCurrentStep(paths, "");
|
|
@@ -3297,6 +3636,7 @@ export {
|
|
|
3297
3636
|
STEP_IDS,
|
|
3298
3637
|
STEP_PRECONDITION_NAMES,
|
|
3299
3638
|
abandonSession,
|
|
3639
|
+
advanceSessionStep,
|
|
3300
3640
|
adoptDependenciesInstalled,
|
|
3301
3641
|
adoptCodexThreadId,
|
|
3302
3642
|
buildSessionResponse,
|
|
@@ -3305,8 +3645,6 @@ export {
|
|
|
3305
3645
|
createSessionId,
|
|
3306
3646
|
extractIssueTitle,
|
|
3307
3647
|
extractIssueText,
|
|
3308
|
-
extractIssueDetails,
|
|
3309
|
-
extractPlanText,
|
|
3310
3648
|
inspectSession,
|
|
3311
3649
|
inspectSessionDiff,
|
|
3312
3650
|
inspectSessionDetails,
|
|
@@ -3316,5 +3654,6 @@ export {
|
|
|
3316
3654
|
recordDependenciesInstalled,
|
|
3317
3655
|
rewindSession,
|
|
3318
3656
|
resolveSessionPaths,
|
|
3319
|
-
runSessionStep
|
|
3657
|
+
runSessionStep,
|
|
3658
|
+
runSessionStepAction
|
|
3320
3659
|
};
|