@jskit-ai/jskit-cli 0.2.83 → 0.2.85
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/appCommandCatalog.js +2 -1
- package/src/server/commandHandlers/session.js +13 -2
- package/src/server/core/argParser.js +8 -0
- package/src/server/core/commandCatalog.js +13 -5
- package/src/server/sessionRuntime/constants.js +42 -5
- package/src/server/sessionRuntime/preconditions.js +28 -0
- package/src/server/sessionRuntime/prompts/deep_ui_check.md +2 -0
- package/src/server/sessionRuntime/prompts/execute_plan.md +1 -0
- package/src/server/sessionRuntime/prompts/prepare_pr_merge.md +22 -0
- package/src/server/sessionRuntime/prompts/review_changes.md +1 -0
- package/src/server/sessionRuntime/responses.js +45 -13
- package/src/server/sessionRuntime.js +599 -76
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/jskit-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.85",
|
|
4
4
|
"description": "Bundle and package orchestration CLI for JSKIT apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"test": "node --test"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@jskit-ai/jskit-catalog": "0.1.
|
|
24
|
-
"@jskit-ai/kernel": "0.1.
|
|
25
|
-
"@jskit-ai/shell-web": "0.1.
|
|
23
|
+
"@jskit-ai/jskit-catalog": "0.1.84",
|
|
24
|
+
"@jskit-ai/kernel": "0.1.76",
|
|
25
|
+
"@jskit-ai/shell-web": "0.1.75",
|
|
26
26
|
"@vue/compiler-sfc": "^3.5.29",
|
|
27
27
|
"ts-morph": "^28.0.0"
|
|
28
28
|
},
|
|
@@ -51,7 +51,7 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
|
|
|
51
51
|
options: Object.freeze([
|
|
52
52
|
Object.freeze({
|
|
53
53
|
label: "--command <shell-command>",
|
|
54
|
-
description: "Targeted
|
|
54
|
+
description: "Targeted UI verification command to run. If it uses Playwright against a local app, the command/environment must start or reuse a reachable app server first."
|
|
55
55
|
}),
|
|
56
56
|
Object.freeze({
|
|
57
57
|
label: "--feature <label>",
|
|
@@ -68,6 +68,7 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
|
|
|
68
68
|
]),
|
|
69
69
|
defaults: Object.freeze([
|
|
70
70
|
"Requires a git working tree so the receipt can record the currently changed UI files.",
|
|
71
|
+
"Does not start the app server; make --command self-contained for UI tests that need one.",
|
|
71
72
|
"Writes .jskit/verification/ui.json after the command succeeds.",
|
|
72
73
|
"Doctor expects the receipt to match the current dirty UI file set, or the same --against <base-ref> delta when used."
|
|
73
74
|
])
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
inspectSessionDiff,
|
|
8
8
|
inspectSessionDetails,
|
|
9
9
|
listSessions,
|
|
10
|
+
rewindSession,
|
|
10
11
|
runSessionStep
|
|
11
12
|
} from "../sessionRuntime.js";
|
|
12
13
|
|
|
@@ -270,7 +271,7 @@ async function resolveStepInputs({
|
|
|
270
271
|
fileOption: "close-reason-file",
|
|
271
272
|
inlineOptions,
|
|
272
273
|
io,
|
|
273
|
-
repairCommand: `jskit session ${sessionId} step --
|
|
274
|
+
repairCommand: `jskit session ${sessionId} step --skip-merge --close-reason "<reason>"`,
|
|
274
275
|
cwd,
|
|
275
276
|
sessionId,
|
|
276
277
|
stdinOption: "-",
|
|
@@ -312,10 +313,14 @@ async function resolveStepInputs({
|
|
|
312
313
|
function normalizeStepOptions(inlineOptions = {}) {
|
|
313
314
|
const options = {
|
|
314
315
|
...inlineOptions,
|
|
315
|
-
closeWithoutMerge: inlineOptions["close-without-merge"] === "true" ||
|
|
316
|
+
closeWithoutMerge: inlineOptions["close-without-merge"] === "true" ||
|
|
317
|
+
inlineOptions.closeWithoutMerge === true ||
|
|
318
|
+
inlineOptions["skip-merge"] === "true" ||
|
|
319
|
+
inlineOptions.skipMerge === true,
|
|
316
320
|
mergePr: inlineOptions["merge-pr"] === "true" || inlineOptions.mergePr === true,
|
|
317
321
|
prompt: inlineOptions.prompt,
|
|
318
322
|
reviewFindings: inlineOptions["review-findings"] || inlineOptions.reviewFindings,
|
|
323
|
+
skipMainSync: inlineOptions["skip-main-sync"] === "true" || inlineOptions.skipMainSync === true,
|
|
319
324
|
skipUiCheck: inlineOptions["skip-ui-check"] === "true" || inlineOptions.skipUiCheck === true,
|
|
320
325
|
userCheck: inlineOptions["user-check"] || inlineOptions.userCheck
|
|
321
326
|
};
|
|
@@ -395,6 +400,12 @@ function createSessionCommands() {
|
|
|
395
400
|
targetRoot: cwd,
|
|
396
401
|
sessionId: first
|
|
397
402
|
});
|
|
403
|
+
} else if (second === "rewind") {
|
|
404
|
+
payload = await rewindSession({
|
|
405
|
+
targetRoot: cwd,
|
|
406
|
+
sessionId: first,
|
|
407
|
+
stepId: inlineOptions.step
|
|
408
|
+
});
|
|
398
409
|
} else if (second === "adopt-codex-thread") {
|
|
399
410
|
payload = await adoptCodexThreadId({
|
|
400
411
|
targetRoot: cwd,
|
|
@@ -145,6 +145,14 @@ function parseArgs(argv, { createCliError } = {}) {
|
|
|
145
145
|
options.inlineOptions["close-without-merge"] = "true";
|
|
146
146
|
continue;
|
|
147
147
|
}
|
|
148
|
+
if (token === "--skip-merge") {
|
|
149
|
+
options.inlineOptions["skip-merge"] = "true";
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (token === "--skip-main-sync") {
|
|
153
|
+
options.inlineOptions["skip-main-sync"] = "true";
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
148
156
|
|
|
149
157
|
if (token.startsWith("--")) {
|
|
150
158
|
const withoutPrefix = token.slice(2);
|
|
@@ -212,8 +212,8 @@ const COMMAND_DESCRIPTORS = Object.freeze({
|
|
|
212
212
|
description: "Create a session, inspect a session, or run a session subcommand."
|
|
213
213
|
}),
|
|
214
214
|
Object.freeze({
|
|
215
|
-
name: "[step|abandon|adopt-codex-thread]",
|
|
216
|
-
description: "Run the next step, inspect a diff, abandon a session, or attach a Codex thread id."
|
|
215
|
+
name: "[step|diff|rewind|abandon|adopt-codex-thread]",
|
|
216
|
+
description: "Run the next step, inspect a diff, rewind a session, abandon a session, or attach a Codex thread id."
|
|
217
217
|
})
|
|
218
218
|
]),
|
|
219
219
|
defaults: Object.freeze([
|
|
@@ -227,13 +227,17 @@ const COMMAND_DESCRIPTORS = Object.freeze({
|
|
|
227
227
|
"Use --issue-details - to read confirmed issue details from stdin.",
|
|
228
228
|
"Use --plan - to read the approved implementation plan from stdin.",
|
|
229
229
|
"Use --codex-result - after Codex prompt steps to read the final marked Codex result from stdin.",
|
|
230
|
+
"Use rewind --step <step_id> to delete a completed step and later JSKIT-owned session artifacts; only plan_made is allowed inside the repeatable cycle.",
|
|
230
231
|
"Use --rework-notes - with --user-check failed to start the next plan cycle.",
|
|
231
232
|
"Use --agent-decisions - to append session-local decision log entries from implementation, UI review, verification, or repair phases.",
|
|
232
233
|
"Use --review-findings-remaining true --review-findings \"<findings>\" when an accepted review pass needs another pass.",
|
|
233
234
|
"Use --review-findings-remaining false only when the review/deslop loop is done.",
|
|
234
235
|
"Use --skip-ui-check --skip-reason \"<reason>\" only when uiImpact is possible and the Deep UI Check is intentionally skipped.",
|
|
236
|
+
"Use --prepare-merge true at PR merge preparation to render the Codex prep prompt.",
|
|
237
|
+
"Use --continue-to-merge true when the user decides to advance from preparation to merge decision.",
|
|
235
238
|
"Use --merge-pr true at PR finalization to merge the pull request.",
|
|
236
|
-
"Use --
|
|
239
|
+
"Use --skip-merge at PR finalization to complete the session without merging.",
|
|
240
|
+
"Use --skip-main-sync at Main checkout synced to explicitly leave the main checkout untouched.",
|
|
237
241
|
"Run the blueprint step once to render its Codex prompt, then again after Codex updates .jskit/APP_BLUEPRINT.md."
|
|
238
242
|
]),
|
|
239
243
|
examples: Object.freeze([
|
|
@@ -252,15 +256,19 @@ const COMMAND_DESCRIPTORS = Object.freeze({
|
|
|
252
256
|
"jskit session 2026-05-11_21-42-08 step --review-findings-remaining false",
|
|
253
257
|
"jskit session 2026-05-11_21-42-08 step --skip-ui-check --skip-reason \"No user-facing UI changes\"",
|
|
254
258
|
"jskit session 2026-05-11_21-42-08 step",
|
|
259
|
+
"jskit session 2026-05-11_21-42-08 step --prepare-merge true",
|
|
260
|
+
"jskit session 2026-05-11_21-42-08 step --continue-to-merge true",
|
|
255
261
|
"jskit session 2026-05-11_21-42-08 step --merge-pr true",
|
|
256
|
-
"jskit session 2026-05-11_21-42-08 step --
|
|
262
|
+
"jskit session 2026-05-11_21-42-08 step --skip-merge",
|
|
263
|
+
"jskit session 2026-05-11_21-42-08 step --skip-main-sync",
|
|
257
264
|
"jskit session 2026-05-11_21-42-08 step --user-check failed --rework-notes -",
|
|
265
|
+
"jskit session 2026-05-11_21-42-08 rewind --step plan_made --json",
|
|
258
266
|
"jskit session 2026-05-11_21-42-08 diff --json"
|
|
259
267
|
])
|
|
260
268
|
})
|
|
261
269
|
]),
|
|
262
270
|
fullUse:
|
|
263
|
-
"jskit session [create|<sessionId>] [step|diff|abandon|adopt-codex-thread] [--prompt <text>] [--issue-title <text>|--issue-title-file <path>] [--issue <text>|--issue-file <path>] [--issue-details <text>|--issue-details-file <path>] [--plan <text>|--plan-file <path>] [--codex-result <text>|--codex-result-file <path>] [--agent-decisions <text>|--agent-decisions-file <path>] [--review-findings-remaining true --review-findings <text>|--review-findings-remaining false] [--skip-ui-check --skip-reason <text>] [--merge
|
|
271
|
+
"jskit session [create|<sessionId>] [step|diff|rewind|abandon|adopt-codex-thread] [--step <step_id>] [--prompt <text>] [--issue-title <text>|--issue-title-file <path>] [--issue <text>|--issue-file <path>] [--issue-details <text>|--issue-details-file <path>] [--plan <text>|--plan-file <path>] [--codex-result <text>|--codex-result-file <path>] [--agent-decisions <text>|--agent-decisions-file <path>] [--review-findings-remaining true --review-findings <text>|--review-findings-remaining false] [--skip-ui-check --skip-reason <text>] [--prepare-merge true|--continue-to-merge true|--merge-pr true|--skip-merge|--skip-main-sync] [--user-check <passed|failed>] [--rework-notes <text>|--rework-notes-file <path>] [--codex-thread-id <id>] [--abandoned|--completed|--all] [--json]",
|
|
264
272
|
showHelpOnBareInvocation: false,
|
|
265
273
|
handlerName: "commandSession",
|
|
266
274
|
allowedFlagKeys: Object.freeze(["json", "abandoned", "completed", "all"]),
|
|
@@ -263,6 +263,12 @@ const BLUEPRINT_CODEX_HANDOFF = codexHandoff([], {
|
|
|
263
263
|
promptActionLabel: "Update blueprint",
|
|
264
264
|
responseContract: JSKIT_STEP_RESULT_CONTRACT
|
|
265
265
|
});
|
|
266
|
+
const PR_MERGE_PREP_CODEX_HANDOFF = codexHandoff([], {
|
|
267
|
+
autoInject: true,
|
|
268
|
+
promptActionLabel: "Get Codex ready to merge PR",
|
|
269
|
+
promptWaitingText: "Codex is preparing the PR for merge. Continue only when you decide the PR is ready.",
|
|
270
|
+
responseContract: null
|
|
271
|
+
});
|
|
266
272
|
|
|
267
273
|
function defineStep({
|
|
268
274
|
buttonLabel,
|
|
@@ -497,23 +503,53 @@ const STEP_DEFINITIONS = Object.freeze([
|
|
|
497
503
|
label: "Branch pushed, PR created",
|
|
498
504
|
preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "issue_metadata_exists", "active_cycle_exists", "automated_checks_passed", "deep_ui_check_satisfied", "active_cycle_user_check_passed", "accepted_changes_committed", "blueprint_update_satisfied", "final_report_exists"]
|
|
499
505
|
}),
|
|
506
|
+
defineStep({
|
|
507
|
+
buttonLabel: "Continue to merge",
|
|
508
|
+
codex: PR_MERGE_PREP_CODEX_HANDOFF,
|
|
509
|
+
description: "User can ask Codex to prepare the pull request for merge, then explicitly continue to the merge decision.",
|
|
510
|
+
id: "pr_merge_prepared",
|
|
511
|
+
label: "PR merge prepared",
|
|
512
|
+
preconditions: ["session_exists", "pr_url_exists", "worktree_exists"],
|
|
513
|
+
requiresExplicitRun: true,
|
|
514
|
+
submitOptions: Object.freeze({
|
|
515
|
+
continueToMerge: true
|
|
516
|
+
}),
|
|
517
|
+
utilityActions: Object.freeze([
|
|
518
|
+
Object.freeze({
|
|
519
|
+
id: "prepare_pr_merge",
|
|
520
|
+
kind: "codex_prompt",
|
|
521
|
+
label: "Get Codex ready to merge PR",
|
|
522
|
+
submitOptions: Object.freeze({
|
|
523
|
+
prepareMerge: true
|
|
524
|
+
})
|
|
525
|
+
})
|
|
526
|
+
])
|
|
527
|
+
}),
|
|
500
528
|
defineStep({
|
|
501
529
|
buttonLabel: "Merge PR",
|
|
502
|
-
description: "User chooses whether JSKIT merges the pull request or
|
|
530
|
+
description: "User chooses whether JSKIT merges the pull request or skips merge; JSKIT records the PR outcome.",
|
|
503
531
|
id: "pr_finalized",
|
|
504
|
-
label: "PR finalized
|
|
532
|
+
label: "PR finalized",
|
|
505
533
|
preconditions: ["session_exists", "pr_url_exists", "worktree_exists"],
|
|
506
534
|
requiresExplicitRun: true,
|
|
507
535
|
submitOptions: Object.freeze({
|
|
508
536
|
mergePr: true
|
|
509
537
|
})
|
|
510
538
|
}),
|
|
539
|
+
defineStep({
|
|
540
|
+
buttonLabel: "Sync main checkout",
|
|
541
|
+
description: "JSKIT fast-forwards the main checkout after a merged PR, or records an explicit skip before cleanup.",
|
|
542
|
+
id: "main_checkout_synced",
|
|
543
|
+
label: "Main checkout synced",
|
|
544
|
+
preconditions: ["session_exists", "worktree_exists"],
|
|
545
|
+
requiresExplicitRun: true
|
|
546
|
+
}),
|
|
511
547
|
defineStep({
|
|
512
548
|
buttonLabel: "Finish session",
|
|
513
|
-
description: "JSKIT writes the final receipt and archives the completed session.",
|
|
549
|
+
description: "JSKIT removes the session worktree, writes the final receipt, and archives the completed session.",
|
|
514
550
|
id: "session_finished",
|
|
515
|
-
label: "
|
|
516
|
-
preconditions: ["session_exists"]
|
|
551
|
+
label: "Worktree removed, session finished",
|
|
552
|
+
preconditions: ["session_exists", "main_checkout_sync_satisfied"]
|
|
517
553
|
})
|
|
518
554
|
]);
|
|
519
555
|
|
|
@@ -553,6 +589,7 @@ export {
|
|
|
553
589
|
DEEP_UI_CHECK_CODEX_HANDOFF,
|
|
554
590
|
AUTOMATED_CHECK_REPAIR_CODEX_HANDOFF,
|
|
555
591
|
BLUEPRINT_CODEX_HANDOFF,
|
|
592
|
+
PR_MERGE_PREP_CODEX_HANDOFF,
|
|
556
593
|
JSKIT_CLI_SHELL_COMMAND,
|
|
557
594
|
JSKIT_CLI_SHELL_RULE,
|
|
558
595
|
SESSION_STATE_RELATIVE_PATH
|
|
@@ -710,6 +710,33 @@ async function assertPrUrlExists(paths) {
|
|
|
710
710
|
};
|
|
711
711
|
}
|
|
712
712
|
|
|
713
|
+
async function assertMainCheckoutSyncSatisfied(paths) {
|
|
714
|
+
const receiptPath = path.join(paths.sessionRoot, "steps", "main_checkout_synced");
|
|
715
|
+
if (await pathExists(receiptPath)) {
|
|
716
|
+
return {
|
|
717
|
+
ok: true,
|
|
718
|
+
precondition: createPrecondition({
|
|
719
|
+
id: "main_checkout_sync_satisfied",
|
|
720
|
+
ok: true,
|
|
721
|
+
message: "Main checkout sync has been completed or skipped."
|
|
722
|
+
})
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
ok: false,
|
|
727
|
+
error: createError({
|
|
728
|
+
code: "main_checkout_sync_required",
|
|
729
|
+
message: "Main checkout sync must be completed or explicitly skipped before session cleanup.",
|
|
730
|
+
repairCommand: jskitCommand(`session ${paths.sessionId} step`)
|
|
731
|
+
}),
|
|
732
|
+
precondition: createPrecondition({
|
|
733
|
+
id: "main_checkout_sync_satisfied",
|
|
734
|
+
ok: false,
|
|
735
|
+
message: "Main checkout sync has been completed or skipped."
|
|
736
|
+
})
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
713
740
|
export {
|
|
714
741
|
applyPreconditions,
|
|
715
742
|
assertAcceptedChangesCommitted,
|
|
@@ -728,6 +755,7 @@ export {
|
|
|
728
755
|
assertIssueUrlExists,
|
|
729
756
|
assertAutomatedChecksPassed,
|
|
730
757
|
assertIssueDetailsExists,
|
|
758
|
+
assertMainCheckoutSyncSatisfied,
|
|
731
759
|
assertPlanTextExists,
|
|
732
760
|
assertPrUrlExists,
|
|
733
761
|
assertReadyJskitApp,
|
|
@@ -34,6 +34,8 @@ Use Playwright for a meaningful route check when possible. If login is required,
|
|
|
34
34
|
|
|
35
35
|
`npx --no-install jskit app verify-ui --command "<playwright command>" --feature "<label>" --auth-mode <mode>`
|
|
36
36
|
|
|
37
|
+
Important: `npx --no-install jskit app verify-ui` does not start the app server. Before running it, make sure the app is reachable by Playwright. If a local server is needed, inspect `.jskit/config/testrun_command` when present and use the app's documented server command, or start the server explicitly and wait for it before invoking `verify-ui`. Do not first run a bare Playwright command against a stopped server.
|
|
38
|
+
|
|
37
39
|
Do not create commits, branches, issues, pull requests, merges, or worktree cleanup. JSKIT session owns those steps.
|
|
38
40
|
|
|
39
41
|
If this pass makes UI fixes, intentionally skips UI work after inspection, changes a design direction, or leaves a meaningful UI verification gap, include concise decision entries with reasons in this exact marker block:
|
|
@@ -41,6 +41,7 @@ After making changes:
|
|
|
41
41
|
- Review for repeated code, unnecessary helpers, fragmented functions, placeholder work, missing states, broken route wiring, ownership mistakes, and weak JSKIT reuse.
|
|
42
42
|
- Run the smallest relevant checks you can run safely in the worktree.
|
|
43
43
|
- For changed user-facing UI, run or clearly identify the Playwright verification path. When possible, record UI verification with `npx --no-install jskit app verify-ui --command "<playwright command>" --feature "<label>" --auth-mode <mode>`.
|
|
44
|
+
- `npx --no-install jskit app verify-ui` does not start the app server. Before using it for Playwright, make sure the app is reachable. If a local server is needed, inspect `.jskit/config/testrun_command` when present and use the app's documented server command, or start the server explicitly and wait for it before invoking `verify-ui`. Do not first run a bare Playwright command against a stopped server.
|
|
44
45
|
- Summarize changed files and checks run.
|
|
45
46
|
- If implementation deviated from the approved plan, generator choices, package ownership, helper reuse, UI verification path, or data ownership, include concise decision entries with reasons in this exact marker block:
|
|
46
47
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Prepare the JSKIT session pull request for merge.
|
|
2
|
+
|
|
3
|
+
Session: {{session_id}}
|
|
4
|
+
Worktree: {{worktree}}
|
|
5
|
+
Target root: {{target_root}}
|
|
6
|
+
Branch: {{branch}}
|
|
7
|
+
Base branch: {{base_branch}}
|
|
8
|
+
Pull request: {{pr_url}}
|
|
9
|
+
Issue: {{issue_url}}
|
|
10
|
+
Final report: {{final_report_file}}
|
|
11
|
+
|
|
12
|
+
Your job is to get the pull request into a state where the user can decide whether to press the JSKIT Merge PR button.
|
|
13
|
+
|
|
14
|
+
Required boundaries:
|
|
15
|
+
|
|
16
|
+
- Inspect the pull request state, checks, and branch status.
|
|
17
|
+
- Resolve merge conflicts or failing checks if the fix is clear and in scope.
|
|
18
|
+
- Commit and push any preparation changes to the session branch.
|
|
19
|
+
- Do not merge the pull request.
|
|
20
|
+
- Do not remove the worktree, archive the session, update the local base branch, or write JSKIT receipts. JSKIT owns deterministic cleanup after the user decides.
|
|
21
|
+
|
|
22
|
+
When you are done, summarize what you checked, what changed, and whether you believe the PR is ready for the user to merge.
|
|
@@ -51,6 +51,7 @@ Use four passes:
|
|
|
51
51
|
- run the smallest relevant verification commands for the changed scope
|
|
52
52
|
- any changed user-facing UI should be exercised with Playwright when possible
|
|
53
53
|
- UI verification should normally be recorded through `npx --no-install jskit app verify-ui`
|
|
54
|
+
- `npx --no-install jskit app verify-ui` does not start the app server; if Playwright needs a local server, inspect `.jskit/config/testrun_command` when present and start or wrap the documented server command before invoking `verify-ui`
|
|
54
55
|
- if login is required, use the chosen local test-auth path instead of live external auth
|
|
55
56
|
- if there is no usable local auth bootstrap path, record it as a blocking testability gap
|
|
56
57
|
|
|
@@ -241,6 +241,7 @@ const PROMPT_ARTIFACT_BY_STEP_ID = Object.freeze({
|
|
|
241
241
|
deep_ui_check_run: "deep_ui_check_run.md",
|
|
242
242
|
automated_checks_run: "automated_checks_run.md",
|
|
243
243
|
blueprint_updated: "update_blueprint.md",
|
|
244
|
+
pr_merge_prepared: "prepare_pr_merge.md",
|
|
244
245
|
user_check_completed: "user_check.md"
|
|
245
246
|
});
|
|
246
247
|
|
|
@@ -589,7 +590,8 @@ function buildStepDefinitions() {
|
|
|
589
590
|
function stepIsRetryableWhenBlocked(stepId) {
|
|
590
591
|
return [
|
|
591
592
|
"automated_checks_run",
|
|
592
|
-
"deep_ui_check_run"
|
|
593
|
+
"deep_ui_check_run",
|
|
594
|
+
"main_checkout_synced"
|
|
593
595
|
].includes(normalizeStepId(stepId));
|
|
594
596
|
}
|
|
595
597
|
|
|
@@ -681,17 +683,12 @@ function buildCurrentStepAction(stepId, artifacts = {}) {
|
|
|
681
683
|
}
|
|
682
684
|
if (step.id === "pr_finalized") {
|
|
683
685
|
alternateActions.push({
|
|
684
|
-
id: "
|
|
685
|
-
helpText: "Leave the PR open
|
|
686
|
+
id: "skip_merge",
|
|
687
|
+
helpText: "Leave the PR open and record the skipped-merge outcome; final cleanup removes the session worktree.",
|
|
686
688
|
input: {
|
|
687
|
-
|
|
688
|
-
label: "Why is this session finishing without merge?",
|
|
689
|
-
multiline: true,
|
|
690
|
-
name: "closeReason",
|
|
691
|
-
required: true,
|
|
692
|
-
type: "text"
|
|
689
|
+
type: "none"
|
|
693
690
|
},
|
|
694
|
-
label: "
|
|
691
|
+
label: "Skip merge",
|
|
695
692
|
presentation: "secondary",
|
|
696
693
|
submitOptions: {
|
|
697
694
|
closeWithoutMerge: true
|
|
@@ -699,6 +696,21 @@ function buildCurrentStepAction(stepId, artifacts = {}) {
|
|
|
699
696
|
targetStep: "pr_finalized"
|
|
700
697
|
});
|
|
701
698
|
}
|
|
699
|
+
if (step.id === "main_checkout_synced") {
|
|
700
|
+
alternateActions.push({
|
|
701
|
+
id: "skip_main_checkout_sync",
|
|
702
|
+
helpText: "Leave the main checkout untouched and continue with session cleanup.",
|
|
703
|
+
input: {
|
|
704
|
+
type: "none"
|
|
705
|
+
},
|
|
706
|
+
label: "Skip sync",
|
|
707
|
+
presentation: "secondary",
|
|
708
|
+
submitOptions: {
|
|
709
|
+
skipMainSync: true
|
|
710
|
+
},
|
|
711
|
+
targetStep: "main_checkout_synced"
|
|
712
|
+
});
|
|
713
|
+
}
|
|
702
714
|
const dynamicButtonLabel = (() => {
|
|
703
715
|
if (step.id === "plan_executed" && planExecutionPrompted && !planExecutionSubmitted) {
|
|
704
716
|
return "Go to next step";
|
|
@@ -712,6 +724,9 @@ function buildCurrentStepAction(stepId, artifacts = {}) {
|
|
|
712
724
|
if (step.id === "plan_made" && planReworkMode && !artifacts.prompt && hasActiveReworkRequest) {
|
|
713
725
|
return "Get Codex to create revised plan";
|
|
714
726
|
}
|
|
727
|
+
if (step.id === "main_checkout_synced" && artifacts.prOutcome?.outcome && artifacts.prOutcome.outcome !== "merged") {
|
|
728
|
+
return "Record no sync needed";
|
|
729
|
+
}
|
|
715
730
|
return buttonLabel;
|
|
716
731
|
})();
|
|
717
732
|
const dynamicDescription = (() => {
|
|
@@ -727,6 +742,9 @@ function buildCurrentStepAction(stepId, artifacts = {}) {
|
|
|
727
742
|
if (step.id === "plan_made" && planReworkMode && hasActiveReworkRequest) {
|
|
728
743
|
return "Codex writes a revised implementation plan from the user's rework notes for this cycle.";
|
|
729
744
|
}
|
|
745
|
+
if (step.id === "main_checkout_synced" && artifacts.prOutcome?.outcome && artifacts.prOutcome.outcome !== "merged") {
|
|
746
|
+
return "The PR was not merged, so JSKIT will record main checkout sync as skipped before cleanup.";
|
|
747
|
+
}
|
|
730
748
|
return step.description;
|
|
731
749
|
})();
|
|
732
750
|
const dynamicUtilityActions = (() => {
|
|
@@ -795,7 +813,8 @@ async function readSessionArtifacts(paths) {
|
|
|
795
813
|
baseCommit,
|
|
796
814
|
issueMetadataText,
|
|
797
815
|
planExecutionReceipt,
|
|
798
|
-
prOutcomeText
|
|
816
|
+
prOutcomeText,
|
|
817
|
+
mainCheckoutSyncText
|
|
799
818
|
] = await Promise.all([
|
|
800
819
|
readTrimmedFile(path.join(paths.sessionRoot, "status")),
|
|
801
820
|
readTrimmedFile(path.join(paths.sessionRoot, "current_step")),
|
|
@@ -814,7 +833,8 @@ async function readSessionArtifacts(paths) {
|
|
|
814
833
|
readTrimmedFile(path.join(paths.sessionRoot, "base_commit")),
|
|
815
834
|
readTextIfExists(path.join(paths.sessionRoot, "issue_metadata.json")),
|
|
816
835
|
readTextIfExists(path.join(cycleStepsRoot(paths, activeCycle), "plan_executed")),
|
|
817
|
-
readTextIfExists(path.join(paths.sessionRoot, "pr_outcome.json"))
|
|
836
|
+
readTextIfExists(path.join(paths.sessionRoot, "pr_outcome.json")),
|
|
837
|
+
readTextIfExists(path.join(paths.sessionRoot, "main_checkout_sync.json"))
|
|
818
838
|
]);
|
|
819
839
|
let issueMetadata = null;
|
|
820
840
|
if (issueMetadataText) {
|
|
@@ -842,6 +862,15 @@ async function readSessionArtifacts(paths) {
|
|
|
842
862
|
prOutcome = null;
|
|
843
863
|
}
|
|
844
864
|
}
|
|
865
|
+
let mainCheckoutSync = null;
|
|
866
|
+
if (mainCheckoutSyncText) {
|
|
867
|
+
try {
|
|
868
|
+
const parsed = JSON.parse(mainCheckoutSyncText);
|
|
869
|
+
mainCheckoutSync = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
870
|
+
} catch {
|
|
871
|
+
mainCheckoutSync = null;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
845
874
|
const cycles = await readCycles(paths, activeCycle);
|
|
846
875
|
const checks = await readStructuredChecks(paths);
|
|
847
876
|
const uiChecks = await readStructuredUiChecks(paths);
|
|
@@ -858,7 +887,7 @@ async function readSessionArtifacts(paths) {
|
|
|
858
887
|
const helperMapPath = path.join(appRootForArtifacts, ".jskit", "helper-map.md");
|
|
859
888
|
const currentStep = normalizeStepId(rawCurrentStep);
|
|
860
889
|
let completedSteps = await readCompletedSteps(paths);
|
|
861
|
-
const worktreeRemovalCompleted = completedSteps.includes("
|
|
890
|
+
const worktreeRemovalCompleted = completedSteps.includes("session_finished");
|
|
862
891
|
const worktreeReceiptInvalid = !worktreeReady &&
|
|
863
892
|
completedSteps.includes("worktree_created") &&
|
|
864
893
|
!worktreeRemovalCompleted &&
|
|
@@ -919,6 +948,7 @@ async function readSessionArtifacts(paths) {
|
|
|
919
948
|
nextStep,
|
|
920
949
|
prUrl,
|
|
921
950
|
prOutcome,
|
|
951
|
+
mainCheckoutSync,
|
|
922
952
|
planExecution: {
|
|
923
953
|
prompted: planExecutionPromptExists,
|
|
924
954
|
promptPath: planExecutionPromptExists ? planExecutionPromptPath : "",
|
|
@@ -1001,6 +1031,7 @@ async function buildSessionResponse(paths, {
|
|
|
1001
1031
|
helperMapExists: artifacts.helperMapExists === true,
|
|
1002
1032
|
prUrl: artifacts.prUrl || "",
|
|
1003
1033
|
prOutcome: cloneContractValue(artifacts.prOutcome || null),
|
|
1034
|
+
mainCheckoutSync: cloneContractValue(artifacts.mainCheckoutSync || null),
|
|
1004
1035
|
preconditions,
|
|
1005
1036
|
errors: [
|
|
1006
1037
|
createError({
|
|
@@ -1067,6 +1098,7 @@ async function buildSessionResponse(paths, {
|
|
|
1067
1098
|
helperMapExists: artifacts.helperMapExists === true,
|
|
1068
1099
|
prUrl: artifacts.prUrl || "",
|
|
1069
1100
|
prOutcome: cloneContractValue(artifacts.prOutcome || null),
|
|
1101
|
+
mainCheckoutSync: cloneContractValue(artifacts.mainCheckoutSync || null),
|
|
1070
1102
|
preconditions,
|
|
1071
1103
|
errors,
|
|
1072
1104
|
archive: responsePaths.archive || (resolvedStatus === SESSION_STATUS.FINISHED ? "completed" : resolvedStatus === SESSION_STATUS.ABANDONED ? "abandoned" : "active"),
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
mkdir,
|
|
4
4
|
readFile,
|
|
5
5
|
readdir,
|
|
6
|
+
rm,
|
|
6
7
|
rmdir
|
|
7
8
|
} from "node:fs/promises";
|
|
8
9
|
import path from "node:path";
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
DEEP_UI_CHECK_CODEX_HANDOFF,
|
|
14
15
|
ISSUE_DETAILS_CODEX_HANDOFF,
|
|
15
16
|
PLAN_EXECUTION_CODEX_HANDOFF,
|
|
17
|
+
PR_MERGE_PREP_CODEX_HANDOFF,
|
|
16
18
|
REVIEW_PASS_LIMIT,
|
|
17
19
|
REVIEW_EXECUTION_CODEX_HANDOFF,
|
|
18
20
|
SESSION_STATUS,
|
|
@@ -78,6 +80,7 @@ import {
|
|
|
78
80
|
assertIssueUrlExists,
|
|
79
81
|
assertAutomatedChecksPassed,
|
|
80
82
|
assertIssueDetailsExists,
|
|
83
|
+
assertMainCheckoutSyncSatisfied,
|
|
81
84
|
assertPlanTextExists,
|
|
82
85
|
assertPrUrlExists,
|
|
83
86
|
assertReadyJskitApp,
|
|
@@ -96,6 +99,9 @@ import {
|
|
|
96
99
|
HELPER_MAP_MARKDOWN_RELATIVE_PATH
|
|
97
100
|
} from "./helperMapPaths.js";
|
|
98
101
|
|
|
102
|
+
const SESSION_PROVISION_PACKAGE_SCRIPT = "jskit:provision-session";
|
|
103
|
+
const SESSION_FINALIZATION_GUARD_PACKAGE_SCRIPT = "jskit:finalization-guard";
|
|
104
|
+
|
|
99
105
|
function invalidSessionIdError(sessionId = "") {
|
|
100
106
|
return createError({
|
|
101
107
|
code: "invalid_session_id",
|
|
@@ -355,6 +361,125 @@ async function runLoggedCommand(paths, kind, command, args = [], options = {}) {
|
|
|
355
361
|
return result;
|
|
356
362
|
}
|
|
357
363
|
|
|
364
|
+
async function readWorktreePackageJson(worktree) {
|
|
365
|
+
const source = await readTextIfExists(path.join(worktree, "package.json"));
|
|
366
|
+
if (!source) {
|
|
367
|
+
return {};
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
const parsed = JSON.parse(source);
|
|
371
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
372
|
+
} catch {
|
|
373
|
+
return {};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function packageScriptRunArgs(packageManager, scriptName) {
|
|
378
|
+
if (packageManager === "pnpm") {
|
|
379
|
+
return ["pnpm", ["run", scriptName]];
|
|
380
|
+
}
|
|
381
|
+
if (packageManager === "yarn") {
|
|
382
|
+
return ["yarn", ["run", scriptName]];
|
|
383
|
+
}
|
|
384
|
+
if (packageManager === "bun") {
|
|
385
|
+
return ["bun", ["run", scriptName]];
|
|
386
|
+
}
|
|
387
|
+
return ["npm", ["run", "--silent", scriptName]];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function packageScriptCommandForWorktree(worktree, scriptName, {
|
|
391
|
+
preferredPackageManager = ""
|
|
392
|
+
} = {}) {
|
|
393
|
+
const packageJson = await readWorktreePackageJson(worktree);
|
|
394
|
+
const script = packageJson?.scripts?.[scriptName];
|
|
395
|
+
if (typeof script !== "string" || !normalizeText(script)) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
const packageManager = preferredPackageManager || (await dependencyInstallCommandForWorktree(worktree))[0];
|
|
399
|
+
const [command, args] = packageScriptRunArgs(packageManager, scriptName);
|
|
400
|
+
return {
|
|
401
|
+
args,
|
|
402
|
+
command
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function sessionPackageScriptEnv(paths, scriptName) {
|
|
407
|
+
return {
|
|
408
|
+
JSKIT_SESSION_ID: paths.sessionId,
|
|
409
|
+
JSKIT_SESSION_PACKAGE_SCRIPT: scriptName,
|
|
410
|
+
JSKIT_SESSION_ROOT: paths.sessionRoot,
|
|
411
|
+
JSKIT_TARGET_ROOT: paths.targetRoot,
|
|
412
|
+
JSKIT_WORKTREE_ROOT: paths.worktree
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function packageScriptRepairCommand(paths, command, args) {
|
|
417
|
+
return `cd ${paths.worktree} && ${command} ${args.join(" ")}`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function packageScriptReceiptName(scriptName) {
|
|
421
|
+
return normalizeText(scriptName).replace(/[^a-zA-Z0-9._-]+/gu, "_");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function writeSessionHookReceipt(paths, scriptName, message) {
|
|
425
|
+
await writeTextFile(
|
|
426
|
+
path.join(paths.sessionRoot, "hooks", packageScriptReceiptName(scriptName)),
|
|
427
|
+
`${timestampForReceipt()}\n${normalizeText(message) || `${scriptName} completed.`}`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function runOptionalSessionPackageScript(paths, {
|
|
432
|
+
failureCode,
|
|
433
|
+
failureMessage,
|
|
434
|
+
kind,
|
|
435
|
+
preferredPackageManager = "",
|
|
436
|
+
preconditions = [],
|
|
437
|
+
scriptName,
|
|
438
|
+
timeout = 1000 * 60 * 10
|
|
439
|
+
} = {}) {
|
|
440
|
+
const scriptCommand = await packageScriptCommandForWorktree(paths.worktree, scriptName, {
|
|
441
|
+
preferredPackageManager
|
|
442
|
+
});
|
|
443
|
+
if (!scriptCommand) {
|
|
444
|
+
return {
|
|
445
|
+
ok: true,
|
|
446
|
+
ran: false
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const result = await runLoggedCommand(paths, kind, scriptCommand.command, scriptCommand.args, {
|
|
450
|
+
cwd: paths.worktree,
|
|
451
|
+
env: sessionPackageScriptEnv(paths, scriptName),
|
|
452
|
+
timeout
|
|
453
|
+
});
|
|
454
|
+
if (!result.ok) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
response: await failSession(paths, {
|
|
458
|
+
code: failureCode,
|
|
459
|
+
message: result.output || failureMessage,
|
|
460
|
+
preconditions,
|
|
461
|
+
repairCommand: packageScriptRepairCommand(paths, scriptCommand.command, scriptCommand.args)
|
|
462
|
+
})
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
await writeSessionHookReceipt(paths, scriptName, result.output || `${scriptName} completed.`);
|
|
466
|
+
return {
|
|
467
|
+
ok: true,
|
|
468
|
+
ran: true,
|
|
469
|
+
result
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function runSessionFinalizationGuard(paths, preconditions = []) {
|
|
474
|
+
return runOptionalSessionPackageScript(paths, {
|
|
475
|
+
failureCode: "session_finalization_guard_failed",
|
|
476
|
+
failureMessage: `${SESSION_FINALIZATION_GUARD_PACKAGE_SCRIPT} failed in the session worktree.`,
|
|
477
|
+
kind: "session_finalization_guard",
|
|
478
|
+
preconditions,
|
|
479
|
+
scriptName: SESSION_FINALIZATION_GUARD_PACKAGE_SCRIPT
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
358
483
|
async function readIssueMetadata(paths) {
|
|
359
484
|
const source = await readTextIfExists(path.join(paths.sessionRoot, "issue_metadata.json"));
|
|
360
485
|
if (!source) {
|
|
@@ -820,8 +945,20 @@ async function installDependencies(paths, _options = {}, context = {}) {
|
|
|
820
945
|
preconditions
|
|
821
946
|
});
|
|
822
947
|
}
|
|
948
|
+
const provisionResult = await runOptionalSessionPackageScript(paths, {
|
|
949
|
+
failureCode: "session_provision_failed",
|
|
950
|
+
failureMessage: `${SESSION_PROVISION_PACKAGE_SCRIPT} failed in the session worktree.`,
|
|
951
|
+
kind: "session_provision",
|
|
952
|
+
preferredPackageManager: command,
|
|
953
|
+
preconditions,
|
|
954
|
+
scriptName: SESSION_PROVISION_PACKAGE_SCRIPT
|
|
955
|
+
});
|
|
956
|
+
if (!provisionResult.ok) {
|
|
957
|
+
return provisionResult.response;
|
|
958
|
+
}
|
|
959
|
+
const installMessage = result.output || `Installed Node dependencies in the session worktree with ${command} ${args.join(" ")}.`;
|
|
823
960
|
return recordDependenciesInstalled(paths, {
|
|
824
|
-
message:
|
|
961
|
+
message: provisionResult.ran ? `${installMessage}\n${SESSION_PROVISION_PACKAGE_SCRIPT} completed.` : installMessage,
|
|
825
962
|
preconditions
|
|
826
963
|
});
|
|
827
964
|
}
|
|
@@ -1282,6 +1419,333 @@ async function inspectSessionDiff({
|
|
|
1282
1419
|
});
|
|
1283
1420
|
}
|
|
1284
1421
|
|
|
1422
|
+
const FIRST_REWINDABLE_STEP_ID = "dependencies_installed";
|
|
1423
|
+
const CYCLE_REWIND_TARGET_STEP_ID = "plan_made";
|
|
1424
|
+
const REWIND_CLOSED_STATUSES = Object.freeze([
|
|
1425
|
+
SESSION_STATUS.ABANDONED,
|
|
1426
|
+
SESSION_STATUS.FINISHED
|
|
1427
|
+
]);
|
|
1428
|
+
|
|
1429
|
+
async function removeSessionPath(paths, ...parts) {
|
|
1430
|
+
await rm(path.join(paths.sessionRoot, ...parts), {
|
|
1431
|
+
force: true,
|
|
1432
|
+
recursive: true
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
async function removeSessionRootFile(paths, fileName) {
|
|
1437
|
+
await removeSessionPath(paths, fileName);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
async function removePromptArtifact(paths, fileName) {
|
|
1441
|
+
await removeSessionPath(paths, "prompts", fileName);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
async function removeGlobalCodexResult(paths, stepId) {
|
|
1445
|
+
await removeSessionPath(paths, "codex_results", `${stepId}.md`);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
async function removeGithubCommentPurpose(paths, purpose) {
|
|
1449
|
+
const comments = await readGithubComments(paths);
|
|
1450
|
+
if (!Object.hasOwn(comments, purpose)) {
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
delete comments[purpose];
|
|
1454
|
+
if (Object.keys(comments).length === 0) {
|
|
1455
|
+
await removeSessionRootFile(paths, "github_comments.json");
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
await writeGithubComments(paths, comments);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
async function removeIssueDetailsMetadata(paths) {
|
|
1462
|
+
const metadata = await readIssueMetadata(paths);
|
|
1463
|
+
if (Object.keys(metadata).length === 0) {
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
delete metadata.issueCategory;
|
|
1467
|
+
delete metadata.issueDetailsPath;
|
|
1468
|
+
delete metadata.uiImpact;
|
|
1469
|
+
if (Object.keys(metadata).length === 0) {
|
|
1470
|
+
await removeSessionRootFile(paths, "issue_metadata.json");
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
await writeTextFile(path.join(paths.sessionRoot, "issue_metadata.json"), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
async function removeCycleDirectories(paths) {
|
|
1477
|
+
for (const rootName of ["steps", "cycles"]) {
|
|
1478
|
+
const root = path.join(paths.sessionRoot, rootName);
|
|
1479
|
+
let entries = [];
|
|
1480
|
+
try {
|
|
1481
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
1482
|
+
} catch {
|
|
1483
|
+
entries = [];
|
|
1484
|
+
}
|
|
1485
|
+
await Promise.all(entries
|
|
1486
|
+
.filter((entry) => entry.isDirectory() && /^cycle_\d+$/u.test(entry.name))
|
|
1487
|
+
.map((entry) => rm(path.join(root, entry.name), {
|
|
1488
|
+
force: true,
|
|
1489
|
+
recursive: true
|
|
1490
|
+
})));
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
async function removeCyclePromptArtifacts(paths) {
|
|
1495
|
+
const promptsRoot = path.join(paths.sessionRoot, "prompts");
|
|
1496
|
+
let entries = [];
|
|
1497
|
+
try {
|
|
1498
|
+
entries = await readdir(promptsRoot, { withFileTypes: true });
|
|
1499
|
+
} catch {
|
|
1500
|
+
entries = [];
|
|
1501
|
+
}
|
|
1502
|
+
const cyclePromptPattern = /^cycle_\d+_(?:plan_request|plan_execution)\.md$/u;
|
|
1503
|
+
const cyclePromptFiles = entries
|
|
1504
|
+
.filter((entry) => entry.isFile() && cyclePromptPattern.test(entry.name))
|
|
1505
|
+
.map((entry) => entry.name);
|
|
1506
|
+
await Promise.all([
|
|
1507
|
+
...cyclePromptFiles.map((fileName) => removePromptArtifact(paths, fileName)),
|
|
1508
|
+
removePromptArtifact(paths, "automated_checks_run.md"),
|
|
1509
|
+
removePromptArtifact(paths, "deep_ui_check_run.md"),
|
|
1510
|
+
removePromptArtifact(paths, "review.md"),
|
|
1511
|
+
removePromptArtifact(paths, "user_check.md")
|
|
1512
|
+
]);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
async function cancelAllCycleState(paths) {
|
|
1516
|
+
await Promise.all([
|
|
1517
|
+
removeCycleDirectories(paths),
|
|
1518
|
+
removeCyclePromptArtifacts(paths),
|
|
1519
|
+
removeSessionPath(paths, "checks"),
|
|
1520
|
+
removeSessionPath(paths, "ui_checks"),
|
|
1521
|
+
removeSessionPath(paths, "review_passes")
|
|
1522
|
+
]);
|
|
1523
|
+
await writeActiveCycle(paths, "001");
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const STEP_CANCELERS = Object.freeze({
|
|
1527
|
+
dependencies_installed: async () => {},
|
|
1528
|
+
issue_prompt_rendered: async (paths) => {
|
|
1529
|
+
await removePromptArtifact(paths, "issue_draft.md");
|
|
1530
|
+
},
|
|
1531
|
+
issue_drafted: async (paths) => {
|
|
1532
|
+
await Promise.all([
|
|
1533
|
+
removeSessionRootFile(paths, "issue.md"),
|
|
1534
|
+
removeSessionRootFile(paths, "issue_title")
|
|
1535
|
+
]);
|
|
1536
|
+
},
|
|
1537
|
+
issue_created: async (paths) => {
|
|
1538
|
+
await Promise.all([
|
|
1539
|
+
removeSessionRootFile(paths, "issue_url"),
|
|
1540
|
+
removeSessionRootFile(paths, "issue_metadata.json")
|
|
1541
|
+
]);
|
|
1542
|
+
},
|
|
1543
|
+
issue_details_gathered: async (paths) => {
|
|
1544
|
+
await Promise.all([
|
|
1545
|
+
removePromptArtifact(paths, "issue_details.md"),
|
|
1546
|
+
removeSessionRootFile(paths, "issue_details.md"),
|
|
1547
|
+
removeGithubCommentPurpose(paths, "issue_details"),
|
|
1548
|
+
removeIssueDetailsMetadata(paths)
|
|
1549
|
+
]);
|
|
1550
|
+
},
|
|
1551
|
+
plan_made: cancelAllCycleState,
|
|
1552
|
+
plan_executed: cancelAllCycleState,
|
|
1553
|
+
deep_ui_check_run: cancelAllCycleState,
|
|
1554
|
+
review_prompt_rendered: cancelAllCycleState,
|
|
1555
|
+
review_changes_accepted: cancelAllCycleState,
|
|
1556
|
+
automated_checks_run: cancelAllCycleState,
|
|
1557
|
+
user_check_completed: cancelAllCycleState,
|
|
1558
|
+
changes_committed: async (paths) => {
|
|
1559
|
+
await removeSessionRootFile(paths, "changes_committed.json");
|
|
1560
|
+
},
|
|
1561
|
+
blueprint_updated: async (paths) => {
|
|
1562
|
+
await Promise.all([
|
|
1563
|
+
removePromptArtifact(paths, "update_blueprint.md"),
|
|
1564
|
+
removeGlobalCodexResult(paths, "blueprint_updated")
|
|
1565
|
+
]);
|
|
1566
|
+
},
|
|
1567
|
+
final_report_created: async (paths) => {
|
|
1568
|
+
await Promise.all([
|
|
1569
|
+
removeSessionRootFile(paths, "final_report.md"),
|
|
1570
|
+
removeGithubCommentPurpose(paths, "final_report")
|
|
1571
|
+
]);
|
|
1572
|
+
},
|
|
1573
|
+
pr_created: async (paths) => {
|
|
1574
|
+
await Promise.all([
|
|
1575
|
+
removePromptArtifact(paths, "pr_create_failure.md"),
|
|
1576
|
+
removeSessionRootFile(paths, "pr_body.md"),
|
|
1577
|
+
removeSessionRootFile(paths, "pr_url")
|
|
1578
|
+
]);
|
|
1579
|
+
},
|
|
1580
|
+
pr_merge_prepared: async (paths) => {
|
|
1581
|
+
await removePromptArtifact(paths, "prepare_pr_merge.md");
|
|
1582
|
+
},
|
|
1583
|
+
pr_finalized: async (paths) => {
|
|
1584
|
+
await Promise.all([
|
|
1585
|
+
removePromptArtifact(paths, "pr_merge_failure.md"),
|
|
1586
|
+
removeSessionRootFile(paths, "pr_base_branch"),
|
|
1587
|
+
removeSessionRootFile(paths, "pr_merge_completed"),
|
|
1588
|
+
removeSessionRootFile(paths, "pr_outcome.json")
|
|
1589
|
+
]);
|
|
1590
|
+
},
|
|
1591
|
+
main_checkout_synced: async (paths) => {
|
|
1592
|
+
await Promise.all([
|
|
1593
|
+
removeSessionRootFile(paths, "local_base_updated"),
|
|
1594
|
+
removeSessionRootFile(paths, "main_checkout_sync.json")
|
|
1595
|
+
]);
|
|
1596
|
+
},
|
|
1597
|
+
session_finished: async (paths) => {
|
|
1598
|
+
await Promise.all([
|
|
1599
|
+
removeSessionRootFile(paths, "final_comment.md")
|
|
1600
|
+
]);
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
function targetRequiresCycleReset(stepId) {
|
|
1605
|
+
const targetIndex = STEP_IDS.indexOf(stepId);
|
|
1606
|
+
const planIndex = STEP_IDS.indexOf(CYCLE_REWIND_TARGET_STEP_ID);
|
|
1607
|
+
return targetIndex >= 0 && targetIndex <= planIndex;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function targetIsAllowedRewindStep(stepId) {
|
|
1611
|
+
if (!STEP_IDS.includes(stepId)) {
|
|
1612
|
+
return false;
|
|
1613
|
+
}
|
|
1614
|
+
if (stepId === "session_created" || stepId === "worktree_created") {
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
if (CYCLE_STEP_IDS.includes(stepId)) {
|
|
1618
|
+
return stepId === CYCLE_REWIND_TARGET_STEP_ID;
|
|
1619
|
+
}
|
|
1620
|
+
return STEP_IDS.indexOf(stepId) >= STEP_IDS.indexOf(FIRST_REWINDABLE_STEP_ID);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
function deletedStepIdsForRewindTarget(stepId) {
|
|
1624
|
+
const targetIndex = STEP_IDS.indexOf(stepId);
|
|
1625
|
+
return targetIndex < 0 ? [] : STEP_IDS.slice(targetIndex);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
async function removeReceiptsForDeletedSteps(paths, deletedStepIds) {
|
|
1629
|
+
await Promise.all(deletedStepIds
|
|
1630
|
+
.filter((stepId) => !CYCLE_STEP_IDS.includes(stepId))
|
|
1631
|
+
.map((stepId) => removeSessionPath(paths, "steps", stepId)));
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
async function cancelDeletedStepArtifacts(paths, deletedStepIds, {
|
|
1635
|
+
cycleReset = false
|
|
1636
|
+
} = {}) {
|
|
1637
|
+
const cancelerIds = cycleReset
|
|
1638
|
+
? deletedStepIds.filter((stepId) => !CYCLE_STEP_IDS.includes(stepId)).concat(CYCLE_REWIND_TARGET_STEP_ID)
|
|
1639
|
+
: deletedStepIds;
|
|
1640
|
+
const calledCycleCancel = new Set();
|
|
1641
|
+
for (const stepId of cancelerIds) {
|
|
1642
|
+
const canceler = STEP_CANCELERS[stepId];
|
|
1643
|
+
if (typeof canceler !== "function") {
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
if (CYCLE_STEP_IDS.includes(stepId)) {
|
|
1647
|
+
if (calledCycleCancel.has(CYCLE_REWIND_TARGET_STEP_ID)) {
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
calledCycleCancel.add(CYCLE_REWIND_TARGET_STEP_ID);
|
|
1651
|
+
}
|
|
1652
|
+
await canceler(paths);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
async function rewindSession({
|
|
1657
|
+
targetRoot = process.cwd(),
|
|
1658
|
+
sessionId,
|
|
1659
|
+
stepId
|
|
1660
|
+
} = {}) {
|
|
1661
|
+
return withExistingSession({ targetRoot, sessionId }, async (paths) => {
|
|
1662
|
+
const artifacts = await readSessionArtifacts(paths);
|
|
1663
|
+
const normalizedStepId = normalizeText(stepId);
|
|
1664
|
+
const currentStatus = artifacts.status || SESSION_STATUS.PENDING;
|
|
1665
|
+
|
|
1666
|
+
if (paths.archive && paths.archive !== "active") {
|
|
1667
|
+
return buildSessionResponse(paths, {
|
|
1668
|
+
ok: false,
|
|
1669
|
+
errors: [
|
|
1670
|
+
createError({
|
|
1671
|
+
code: "session_archived_read_only",
|
|
1672
|
+
message: `Session ${paths.sessionId} is archived and cannot be rewound.`
|
|
1673
|
+
})
|
|
1674
|
+
],
|
|
1675
|
+
status: currentStatus
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
if (REWIND_CLOSED_STATUSES.includes(currentStatus)) {
|
|
1680
|
+
return buildSessionResponse(paths, {
|
|
1681
|
+
ok: false,
|
|
1682
|
+
errors: [
|
|
1683
|
+
createError({
|
|
1684
|
+
code: "session_closed_read_only",
|
|
1685
|
+
message: `Session ${paths.sessionId} is ${currentStatus} and cannot be rewound.`
|
|
1686
|
+
})
|
|
1687
|
+
],
|
|
1688
|
+
status: currentStatus
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
if (artifacts.workflowVersion !== SESSION_WORKFLOW_VERSION) {
|
|
1693
|
+
return buildSessionResponse(paths, {
|
|
1694
|
+
ok: false,
|
|
1695
|
+
errors: [
|
|
1696
|
+
createError({
|
|
1697
|
+
code: "unsupported_workflow_version",
|
|
1698
|
+
message: `Session ${paths.sessionId} uses workflow version ${artifacts.workflowVersion || "unknown"}, but this JSKIT runtime expects ${SESSION_WORKFLOW_VERSION}.`
|
|
1699
|
+
})
|
|
1700
|
+
],
|
|
1701
|
+
status: SESSION_STATUS.BLOCKED
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
if (!targetIsAllowedRewindStep(normalizedStepId)) {
|
|
1706
|
+
const cycleHint = CYCLE_STEP_IDS.includes(normalizedStepId)
|
|
1707
|
+
? " Only Plan made can be used as a cycle rewind target; it resets all cycle/rework state."
|
|
1708
|
+
: "";
|
|
1709
|
+
return buildSessionResponse(paths, {
|
|
1710
|
+
ok: false,
|
|
1711
|
+
errors: [
|
|
1712
|
+
createError({
|
|
1713
|
+
code: "rewind_step_not_allowed",
|
|
1714
|
+
message: `Cannot rewind session ${paths.sessionId} to ${normalizedStepId || "(missing)"}.${cycleHint}`
|
|
1715
|
+
})
|
|
1716
|
+
],
|
|
1717
|
+
status: currentStatus
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
if (!artifacts.completedSteps.includes(normalizedStepId)) {
|
|
1722
|
+
return buildSessionResponse(paths, {
|
|
1723
|
+
ok: false,
|
|
1724
|
+
errors: [
|
|
1725
|
+
createError({
|
|
1726
|
+
code: "rewind_step_not_completed",
|
|
1727
|
+
message: `Cannot rewind session ${paths.sessionId} to ${normalizedStepId} because that step is not completed.`
|
|
1728
|
+
})
|
|
1729
|
+
],
|
|
1730
|
+
status: currentStatus
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const deletedStepIds = deletedStepIdsForRewindTarget(normalizedStepId);
|
|
1735
|
+
const cycleReset = targetRequiresCycleReset(normalizedStepId);
|
|
1736
|
+
await removeReceiptsForDeletedSteps(paths, deletedStepIds);
|
|
1737
|
+
await cancelDeletedStepArtifacts(paths, deletedStepIds, { cycleReset });
|
|
1738
|
+
if (cycleReset) {
|
|
1739
|
+
await writeActiveCycle(paths, "001");
|
|
1740
|
+
}
|
|
1741
|
+
await markCurrentStep(paths, normalizedStepId);
|
|
1742
|
+
await markStatus(paths, SESSION_STATUS.PENDING);
|
|
1743
|
+
return buildSessionResponse(paths, {
|
|
1744
|
+
status: SESSION_STATUS.PENDING
|
|
1745
|
+
});
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1285
1749
|
async function commitWorktree(paths, {
|
|
1286
1750
|
message,
|
|
1287
1751
|
allowNoChanges = false
|
|
@@ -2118,6 +2582,17 @@ async function writePrOutcome(paths, outcome) {
|
|
|
2118
2582
|
}, null, 2)}\n`);
|
|
2119
2583
|
}
|
|
2120
2584
|
|
|
2585
|
+
function mainCheckoutSyncPath(paths) {
|
|
2586
|
+
return path.join(paths.sessionRoot, "main_checkout_sync.json");
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
async function writeMainCheckoutSync(paths, payload = {}) {
|
|
2590
|
+
await writeTextFile(mainCheckoutSyncPath(paths), `${JSON.stringify({
|
|
2591
|
+
recordedAt: timestampForReceipt(),
|
|
2592
|
+
...payload
|
|
2593
|
+
}, null, 2)}\n`);
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2121
2596
|
async function assertTargetRootCanUpdateBase(paths, branch) {
|
|
2122
2597
|
const cleanFailure = await assertTargetRootCleanForBaseUpdate(paths);
|
|
2123
2598
|
if (cleanFailure) {
|
|
@@ -2136,50 +2611,6 @@ async function assertTargetRootCanUpdateBase(paths, branch) {
|
|
|
2136
2611
|
return null;
|
|
2137
2612
|
}
|
|
2138
2613
|
|
|
2139
|
-
async function assertTargetBaseCanFastForward(paths, branch) {
|
|
2140
|
-
const branchFailure = await assertTargetRootCanUpdateBase(paths, branch);
|
|
2141
|
-
if (branchFailure) {
|
|
2142
|
-
return branchFailure;
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
const fetchResult = await runLoggedCommand(paths, "git_fetch_origin", "git", ["fetch", "origin"], {
|
|
2146
|
-
cwd: paths.targetRoot,
|
|
2147
|
-
timeout: 1000 * 60 * 5
|
|
2148
|
-
});
|
|
2149
|
-
if (!fetchResult.ok) {
|
|
2150
|
-
return failSession(paths, {
|
|
2151
|
-
code: "target_fetch_failed",
|
|
2152
|
-
message: fetchResult.output || `Failed to fetch origin before checking local ${branch}.`,
|
|
2153
|
-
repairCommand: `git -C ${paths.targetRoot} fetch origin`
|
|
2154
|
-
});
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
const remoteBranch = `origin/${branch}`;
|
|
2158
|
-
const remoteExists = await runGit(paths.targetRoot, ["rev-parse", "--verify", remoteBranch], {
|
|
2159
|
-
timeout: 15000
|
|
2160
|
-
});
|
|
2161
|
-
if (!remoteExists.ok) {
|
|
2162
|
-
return failSession(paths, {
|
|
2163
|
-
code: "target_remote_branch_missing",
|
|
2164
|
-
message: `Remote base branch ${remoteBranch} does not exist; JSKIT cannot safely update local ${branch} after merge.`,
|
|
2165
|
-
repairCommand: `git -C ${paths.targetRoot} fetch origin`
|
|
2166
|
-
});
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
const ancestor = await runGit(paths.targetRoot, ["merge-base", "--is-ancestor", branch, remoteBranch], {
|
|
2170
|
-
timeout: 15000
|
|
2171
|
-
});
|
|
2172
|
-
if (!ancestor.ok) {
|
|
2173
|
-
return failSession(paths, {
|
|
2174
|
-
code: "target_branch_not_fast_forwardable",
|
|
2175
|
-
message: `Local ${branch} is not an ancestor of ${remoteBranch}; JSKIT will not merge the PR while the local base branch has diverged.`,
|
|
2176
|
-
repairCommand: `git -C ${paths.targetRoot} pull --ff-only origin ${branch}`
|
|
2177
|
-
});
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
return null;
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
2614
|
async function updateLocalBaseBranch(paths, baseBranch = "") {
|
|
2184
2615
|
const branch = normalizeText(baseBranch) || await currentTargetBranch(paths.targetRoot);
|
|
2185
2616
|
if (!branch) {
|
|
@@ -2222,6 +2653,54 @@ async function updateLocalBaseBranch(paths, baseBranch = "") {
|
|
|
2222
2653
|
return null;
|
|
2223
2654
|
}
|
|
2224
2655
|
|
|
2656
|
+
async function syncMainCheckout(paths, options = {}, context = {}) {
|
|
2657
|
+
const prOutcome = parseJsonObject(await readTextIfExists(path.join(paths.sessionRoot, "pr_outcome.json")));
|
|
2658
|
+
const preconditions = context.preconditions || [];
|
|
2659
|
+
if (!prOutcome?.outcome) {
|
|
2660
|
+
return failSession(paths, {
|
|
2661
|
+
code: "pr_outcome_missing",
|
|
2662
|
+
message: "Cannot sync the main checkout before PR finalization records an outcome.",
|
|
2663
|
+
preconditions,
|
|
2664
|
+
repairCommand: `jskit session ${paths.sessionId} step`
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
const skipRequested = options.skipMainSync === true ||
|
|
2669
|
+
normalizeText(options["skip-main-sync"]).toLowerCase() === "true";
|
|
2670
|
+
const skipReason = normalizeText(options.skipReason || options["skip-reason"]) ||
|
|
2671
|
+
"User skipped main checkout sync.";
|
|
2672
|
+
if (skipRequested || prOutcome.outcome !== "merged") {
|
|
2673
|
+
const reason = prOutcome.outcome === "merged"
|
|
2674
|
+
? skipReason
|
|
2675
|
+
: `PR outcome is ${prOutcome.outcome}; no main checkout sync is required.`;
|
|
2676
|
+
await writeMainCheckoutSync(paths, {
|
|
2677
|
+
branch: prOutcome.baseBranch || "",
|
|
2678
|
+
outcome: prOutcome.outcome,
|
|
2679
|
+
reason,
|
|
2680
|
+
status: "skipped"
|
|
2681
|
+
});
|
|
2682
|
+
await writeReceipt(paths, "main_checkout_synced", `Main checkout sync skipped: ${reason}`);
|
|
2683
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2684
|
+
return buildSessionResponse(paths);
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
const baseBranch = prOutcome.baseBranch || await readTrimmedFile(path.join(paths.sessionRoot, "pr_base_branch"));
|
|
2688
|
+
const syncFailure = await updateLocalBaseBranch(paths, baseBranch);
|
|
2689
|
+
if (syncFailure) {
|
|
2690
|
+
return syncFailure;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
const branch = normalizeText(baseBranch) || await currentTargetBranch(paths.targetRoot);
|
|
2694
|
+
await writeMainCheckoutSync(paths, {
|
|
2695
|
+
branch,
|
|
2696
|
+
outcome: prOutcome.outcome,
|
|
2697
|
+
status: "synced"
|
|
2698
|
+
});
|
|
2699
|
+
await writeReceipt(paths, "main_checkout_synced", `Fast-forwarded target checkout branch ${branch}.`);
|
|
2700
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2701
|
+
return buildSessionResponse(paths);
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2225
2704
|
async function updateHelperMapBeforePr(paths) {
|
|
2226
2705
|
let helperMapPayload;
|
|
2227
2706
|
try {
|
|
@@ -2380,14 +2859,7 @@ async function createPr(paths) {
|
|
|
2380
2859
|
}
|
|
2381
2860
|
|
|
2382
2861
|
async function closePrWithoutMerge(paths, prUrl, options = {}) {
|
|
2383
|
-
const reason = normalizeText(options.closeReason || options["close-reason"]);
|
|
2384
|
-
if (!reason) {
|
|
2385
|
-
return failSession(paths, {
|
|
2386
|
-
code: "close_without_merge_reason_required",
|
|
2387
|
-
message: "Finishing without merging requires --close-reason.",
|
|
2388
|
-
repairCommand: `jskit session ${paths.sessionId} step --close-without-merge --close-reason "<reason>"`
|
|
2389
|
-
});
|
|
2390
|
-
}
|
|
2862
|
+
const reason = normalizeText(options.closeReason || options["close-reason"]) || "User skipped merge in JSKIT Studio.";
|
|
2391
2863
|
const prState = await readPrState(paths, prUrl);
|
|
2392
2864
|
if (!prState.ok) {
|
|
2393
2865
|
return failSession(paths, {
|
|
@@ -2423,10 +2895,6 @@ async function closePrWithoutMerge(paths, prUrl, options = {}) {
|
|
|
2423
2895
|
timeout: 1000 * 60
|
|
2424
2896
|
});
|
|
2425
2897
|
}
|
|
2426
|
-
const removeFailure = await removeSessionWorktree(paths);
|
|
2427
|
-
if (removeFailure) {
|
|
2428
|
-
return removeFailure;
|
|
2429
|
-
}
|
|
2430
2898
|
await writePrOutcome(paths, {
|
|
2431
2899
|
issueUrl,
|
|
2432
2900
|
outcome: "closed_without_merge",
|
|
@@ -2434,16 +2902,69 @@ async function closePrWithoutMerge(paths, prUrl, options = {}) {
|
|
|
2434
2902
|
prState: prState.state,
|
|
2435
2903
|
reason
|
|
2436
2904
|
});
|
|
2437
|
-
await writeReceipt(paths, "pr_finalized", `Finished without merging PR ${prUrl}; PR left open
|
|
2905
|
+
await writeReceipt(paths, "pr_finalized", `Finished without merging PR ${prUrl}; PR left open. Reason: ${reason}`);
|
|
2438
2906
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2439
2907
|
return buildSessionResponse(paths);
|
|
2440
2908
|
}
|
|
2441
2909
|
|
|
2442
|
-
async function
|
|
2910
|
+
async function preparePrMerge(paths, options = {}, context = {}) {
|
|
2911
|
+
const preconditions = context.preconditions || [];
|
|
2912
|
+
const prepareMerge = options.prepareMerge === true ||
|
|
2913
|
+
normalizeText(options["prepare-merge"]).toLowerCase() === "true";
|
|
2914
|
+
const continueToMerge = options.continueToMerge === true ||
|
|
2915
|
+
normalizeText(options["continue-to-merge"]).toLowerCase() === "true";
|
|
2916
|
+
|
|
2917
|
+
if (prepareMerge) {
|
|
2918
|
+
const prUrl = await readTrimmedFile(path.join(paths.sessionRoot, "pr_url"));
|
|
2919
|
+
const baseBranch = await readTrimmedFile(path.join(paths.sessionRoot, "pr_base_branch")) ||
|
|
2920
|
+
await readTrimmedFile(path.join(paths.sessionRoot, "base_branch")) ||
|
|
2921
|
+
await currentTargetBranch(paths.targetRoot);
|
|
2922
|
+
const prompt = await renderPrompt(paths, "prepare_pr_merge.md", {
|
|
2923
|
+
base_branch: baseBranch,
|
|
2924
|
+
final_report_file: path.join(paths.sessionRoot, "final_report.md"),
|
|
2925
|
+
issue_url: await readTrimmedFile(path.join(paths.sessionRoot, "issue_url")),
|
|
2926
|
+
pr_url: prUrl,
|
|
2927
|
+
target_root: paths.targetRoot
|
|
2928
|
+
});
|
|
2929
|
+
await writePromptArtifact(paths, "prepare_pr_merge.md", prompt);
|
|
2930
|
+
await markStatus(paths, SESSION_STATUS.WAITING_FOR_USER);
|
|
2931
|
+
return buildSessionResponse(paths, {
|
|
2932
|
+
codex: PR_MERGE_PREP_CODEX_HANDOFF,
|
|
2933
|
+
ok: true,
|
|
2934
|
+
preconditions,
|
|
2935
|
+
prompt,
|
|
2936
|
+
status: SESSION_STATUS.WAITING_FOR_USER
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
if (!continueToMerge) {
|
|
2941
|
+
return failSession(paths, {
|
|
2942
|
+
code: "pr_merge_prepare_decision_required",
|
|
2943
|
+
message: "Choose whether to ask Codex to prepare the PR for merge or continue to the merge decision.",
|
|
2944
|
+
repairCommand: `jskit session ${paths.sessionId} step --continue-to-merge true`,
|
|
2945
|
+
preconditions
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
await writeReceipt(paths, "pr_merge_prepared", "User continued from PR merge preparation to the merge decision.");
|
|
2950
|
+
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2951
|
+
return buildSessionResponse(paths, {
|
|
2952
|
+
preconditions
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
async function finalizePr(paths, options = {}, context = {}) {
|
|
2957
|
+
const preconditions = context.preconditions || [];
|
|
2443
2958
|
const prUrl = await readTrimmedFile(path.join(paths.sessionRoot, "pr_url"));
|
|
2444
2959
|
const closeWithoutMerge = options.closeWithoutMerge === true ||
|
|
2445
|
-
normalizeText(options["close-without-merge"]).toLowerCase() === "true"
|
|
2960
|
+
normalizeText(options["close-without-merge"]).toLowerCase() === "true" ||
|
|
2961
|
+
options.skipMerge === true ||
|
|
2962
|
+
normalizeText(options["skip-merge"]).toLowerCase() === "true";
|
|
2446
2963
|
if (closeWithoutMerge) {
|
|
2964
|
+
const guardResult = await runSessionFinalizationGuard(paths, preconditions);
|
|
2965
|
+
if (!guardResult.ok) {
|
|
2966
|
+
return guardResult.response;
|
|
2967
|
+
}
|
|
2447
2968
|
return closePrWithoutMerge(paths, prUrl, options);
|
|
2448
2969
|
}
|
|
2449
2970
|
const mergePr = options.mergePr === true ||
|
|
@@ -2451,10 +2972,15 @@ async function finalizePr(paths, options = {}) {
|
|
|
2451
2972
|
if (!mergePr) {
|
|
2452
2973
|
return failSession(paths, {
|
|
2453
2974
|
code: "pr_finalize_decision_required",
|
|
2454
|
-
message: "Choose whether to merge the PR or
|
|
2455
|
-
repairCommand: `jskit session ${paths.sessionId} step --merge-pr true
|
|
2975
|
+
message: "Choose whether to merge the PR or skip merge.",
|
|
2976
|
+
repairCommand: `jskit session ${paths.sessionId} step --merge-pr true`,
|
|
2977
|
+
preconditions
|
|
2456
2978
|
});
|
|
2457
2979
|
}
|
|
2980
|
+
const guardResult = await runSessionFinalizationGuard(paths, preconditions);
|
|
2981
|
+
if (!guardResult.ok) {
|
|
2982
|
+
return guardResult.response;
|
|
2983
|
+
}
|
|
2458
2984
|
const mergeMarkerPath = path.join(paths.sessionRoot, "pr_merge_completed");
|
|
2459
2985
|
const baseBranchPath = path.join(paths.sessionRoot, "pr_base_branch");
|
|
2460
2986
|
const mergeAlreadyCompleted = await readTrimmedFile(mergeMarkerPath);
|
|
@@ -2465,10 +2991,6 @@ async function finalizePr(paths, options = {}) {
|
|
|
2465
2991
|
if (baseBranch) {
|
|
2466
2992
|
await writeTextFile(baseBranchPath, `${baseBranch}\n`);
|
|
2467
2993
|
}
|
|
2468
|
-
const baseFailure = await assertTargetBaseCanFastForward(paths, baseBranch);
|
|
2469
|
-
if (baseFailure) {
|
|
2470
|
-
return baseFailure;
|
|
2471
|
-
}
|
|
2472
2994
|
let prMerged = prStateIsMerged(existingPrState);
|
|
2473
2995
|
let mergeResult = null;
|
|
2474
2996
|
if (!prMerged) {
|
|
@@ -2491,6 +3013,7 @@ async function finalizePr(paths, options = {}) {
|
|
|
2491
3013
|
code: "pr_merge_failed",
|
|
2492
3014
|
message: mergeResult?.output || existingPrState.output || "Failed to merge PR.",
|
|
2493
3015
|
repairCommand: `gh pr merge ${prUrl} --merge --delete-branch`,
|
|
3016
|
+
preconditions,
|
|
2494
3017
|
prompt
|
|
2495
3018
|
});
|
|
2496
3019
|
}
|
|
@@ -2509,15 +3032,7 @@ async function finalizePr(paths, options = {}) {
|
|
|
2509
3032
|
});
|
|
2510
3033
|
await writeTextFile(mergeMarkerPath, `${prUrl}\n`);
|
|
2511
3034
|
}
|
|
2512
|
-
|
|
2513
|
-
if (updateFailure) {
|
|
2514
|
-
return updateFailure;
|
|
2515
|
-
}
|
|
2516
|
-
const removeFailure = await removeSessionWorktree(paths);
|
|
2517
|
-
if (removeFailure) {
|
|
2518
|
-
return removeFailure;
|
|
2519
|
-
}
|
|
2520
|
-
await writeReceipt(paths, "pr_finalized", `Merged PR ${prUrl}, updated local ${baseBranch || "base branch"}, and removed worktree ${paths.worktree}.`);
|
|
3035
|
+
await writeReceipt(paths, "pr_finalized", `Merged PR ${prUrl}.`);
|
|
2521
3036
|
await markStatus(paths, SESSION_STATUS.RUNNING);
|
|
2522
3037
|
return buildSessionResponse(paths);
|
|
2523
3038
|
}
|
|
@@ -2527,6 +3042,10 @@ async function finishSession(paths) {
|
|
|
2527
3042
|
const issueUrl = await readTrimmedFile(path.join(paths.sessionRoot, "issue_url"));
|
|
2528
3043
|
const codexThreadId = await readTrimmedFile(path.join(paths.sessionRoot, "codex_thread_id"));
|
|
2529
3044
|
const prOutcome = parseJsonObject(await readTextIfExists(path.join(paths.sessionRoot, "pr_outcome.json")));
|
|
3045
|
+
const removeFailure = await removeSessionWorktree(paths);
|
|
3046
|
+
if (removeFailure) {
|
|
3047
|
+
return removeFailure;
|
|
3048
|
+
}
|
|
2530
3049
|
const prompt = await renderPrompt(paths, "final_comment.md", {
|
|
2531
3050
|
codex_thread_id: codexThreadId,
|
|
2532
3051
|
issue_url: issueUrl,
|
|
@@ -2543,7 +3062,7 @@ async function finishSession(paths) {
|
|
|
2543
3062
|
timeout: 1000 * 60
|
|
2544
3063
|
});
|
|
2545
3064
|
}
|
|
2546
|
-
await writeReceipt(paths, "session_finished", `
|
|
3065
|
+
await writeReceipt(paths, "session_finished", `Removed worktree ${paths.worktree} and finished session ${paths.sessionId} with PR outcome ${prOutcome?.outcome || "unknown"}.`);
|
|
2547
3066
|
await markStatus(paths, SESSION_STATUS.FINISHED);
|
|
2548
3067
|
await markCurrentStep(paths, "");
|
|
2549
3068
|
const archivedPaths = await archiveSession(paths, "completed");
|
|
@@ -2577,7 +3096,9 @@ const STEP_RUNNERS = Object.freeze({
|
|
|
2577
3096
|
blueprint_updated: updateBlueprint,
|
|
2578
3097
|
final_report_created: createFinalReport,
|
|
2579
3098
|
pr_created: createPr,
|
|
3099
|
+
pr_merge_prepared: preparePrMerge,
|
|
2580
3100
|
pr_finalized: finalizePr,
|
|
3101
|
+
main_checkout_synced: syncMainCheckout,
|
|
2581
3102
|
session_finished: finishSession
|
|
2582
3103
|
});
|
|
2583
3104
|
|
|
@@ -2598,6 +3119,7 @@ const PRECONDITION_RUNNERS = Object.freeze({
|
|
|
2598
3119
|
issue_url_exists: assertIssueUrlExists,
|
|
2599
3120
|
automated_checks_passed: assertAutomatedChecksPassed,
|
|
2600
3121
|
issue_details_exists: assertIssueDetailsExists,
|
|
3122
|
+
main_checkout_sync_satisfied: assertMainCheckoutSyncSatisfied,
|
|
2601
3123
|
plan_text_exists: assertPlanTextExists,
|
|
2602
3124
|
pr_url_exists: assertPrUrlExists,
|
|
2603
3125
|
ready_jskit_app: assertReadyJskitApp,
|
|
@@ -2772,6 +3294,7 @@ export {
|
|
|
2772
3294
|
listSessions,
|
|
2773
3295
|
renderTemplate,
|
|
2774
3296
|
recordDependenciesInstalled,
|
|
3297
|
+
rewindSession,
|
|
2775
3298
|
resolveSessionPaths,
|
|
2776
3299
|
runSessionStep
|
|
2777
3300
|
};
|