@jskit-ai/jskit-cli 0.2.79 → 0.2.81
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/appBlueprint.js +126 -0
- package/src/server/commandHandlers/blueprint.js +151 -0
- package/src/server/commandHandlers/session.js +305 -0
- package/src/server/core/argParser.js +14 -2
- package/src/server/core/commandCatalog.js +91 -1
- package/src/server/core/createCommandHandlers.js +7 -1
- package/src/server/index.js +2 -0
- package/src/server/sessionRuntime/constants.js +320 -0
- package/src/server/sessionRuntime/io.js +97 -0
- package/src/server/sessionRuntime/paths.js +163 -0
- package/src/server/sessionRuntime/preconditions.js +362 -0
- package/src/server/sessionRuntime/promptRenderer.js +41 -0
- package/src/server/sessionRuntime/prompts/app_blueprint.md +52 -0
- package/src/server/sessionRuntime/prompts/doctor_failure.md +26 -0
- package/src/server/sessionRuntime/prompts/execute_plan.md +35 -0
- package/src/server/sessionRuntime/prompts/final_comment.md +8 -0
- package/src/server/sessionRuntime/prompts/new_issue.md +31 -0
- package/src/server/sessionRuntime/prompts/plan_issue.md +50 -0
- package/src/server/sessionRuntime/prompts/pr_failure.md +28 -0
- package/src/server/sessionRuntime/prompts/review_changes.md +43 -0
- package/src/server/sessionRuntime/prompts/user_check.md +13 -0
- package/src/server/sessionRuntime/responses.js +442 -0
- package/src/server/sessionRuntime/worktrees.js +31 -0
- package/src/server/sessionRuntime.js +1218 -0
|
@@ -9,7 +9,9 @@ const OPTION_FLAG_LABELS = Object.freeze({
|
|
|
9
9
|
checkDiLabels: "--check-di-labels",
|
|
10
10
|
verbose: "--verbose",
|
|
11
11
|
json: "--json",
|
|
12
|
-
all: "--all"
|
|
12
|
+
all: "--all",
|
|
13
|
+
abandoned: "--abandoned",
|
|
14
|
+
completed: "--completed"
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
const PARSED_BOOLEAN_OPTION_KEYS = Object.freeze(Object.keys(OPTION_FLAG_LABELS));
|
|
@@ -198,6 +200,94 @@ const COMMAND_DESCRIPTORS = Object.freeze({
|
|
|
198
200
|
allowedValueOptionNames: Object.freeze([]),
|
|
199
201
|
canDelegateInlineOptions: (positional = []) => Array.isArray(positional) && positional.length > 0
|
|
200
202
|
}),
|
|
203
|
+
session: Object.freeze({
|
|
204
|
+
command: "session",
|
|
205
|
+
aliases: Object.freeze([]),
|
|
206
|
+
showInOverview: true,
|
|
207
|
+
summary: "Run file-backed JSKIT issue sessions.",
|
|
208
|
+
minimalUse: "jskit session create",
|
|
209
|
+
parameters: Object.freeze([
|
|
210
|
+
Object.freeze({
|
|
211
|
+
name: "create | <sessionId>",
|
|
212
|
+
description: "Create a session, inspect a session, or run a session subcommand."
|
|
213
|
+
}),
|
|
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."
|
|
217
|
+
})
|
|
218
|
+
]),
|
|
219
|
+
defaults: Object.freeze([
|
|
220
|
+
"Active session state lives in .jskit/sessions/active/<session_id> under the current target app.",
|
|
221
|
+
"Session worktrees live in .jskit/sessions/active/<session_id>/worktree.",
|
|
222
|
+
"The session id is timestamp-based and is the primary key.",
|
|
223
|
+
"Bare list output includes active sessions only; use --abandoned, --completed, or --all for archived sessions.",
|
|
224
|
+
"Use --json for the stable machine-readable contract consumed by JSKIT AI Studio.",
|
|
225
|
+
"Use --issue - to read approved issue body from stdin.",
|
|
226
|
+
"Use --issue-title when the approved issue title is separate from the body.",
|
|
227
|
+
"Use --plan - to read the approved implementation plan from stdin."
|
|
228
|
+
]),
|
|
229
|
+
examples: Object.freeze([
|
|
230
|
+
Object.freeze({
|
|
231
|
+
label: "Manual CLI flow",
|
|
232
|
+
lines: Object.freeze([
|
|
233
|
+
"jskit session create",
|
|
234
|
+
"jskit session 2026-05-11_21-42-08 step",
|
|
235
|
+
"jskit session 2026-05-11_21-42-08 step --prompt \"Fix the customer filters\"",
|
|
236
|
+
"jskit session 2026-05-11_21-42-08 step --issue-title \"Fix customer filters\" --issue -",
|
|
237
|
+
"jskit session 2026-05-11_21-42-08 step --plan -",
|
|
238
|
+
"jskit session 2026-05-11_21-42-08 diff --json"
|
|
239
|
+
])
|
|
240
|
+
})
|
|
241
|
+
]),
|
|
242
|
+
fullUse:
|
|
243
|
+
"jskit session [create|<sessionId>] [step|diff|abandon|adopt-codex-thread] [--prompt <text>] [--issue-title <text>|--issue-title-file <path>] [--issue <text>|--issue-file <path>] [--plan <text>|--plan-file <path>] [--user-check <passed|failed>] [--codex-thread-id <id>] [--abandoned|--completed|--all] [--json]",
|
|
244
|
+
showHelpOnBareInvocation: false,
|
|
245
|
+
handlerName: "commandSession",
|
|
246
|
+
allowedFlagKeys: Object.freeze(["json", "abandoned", "completed", "all"]),
|
|
247
|
+
inlineOptionMode: "delegate",
|
|
248
|
+
allowedValueOptionNames: Object.freeze([]),
|
|
249
|
+
canDelegateInlineOptions: (positional = []) => Array.isArray(positional) && positional.length > 0
|
|
250
|
+
}),
|
|
251
|
+
blueprint: Object.freeze({
|
|
252
|
+
command: "blueprint",
|
|
253
|
+
aliases: Object.freeze([]),
|
|
254
|
+
showInOverview: true,
|
|
255
|
+
summary: "Read, prompt, or set the app-level JSKIT blueprint.",
|
|
256
|
+
minimalUse: "jskit blueprint",
|
|
257
|
+
parameters: Object.freeze([
|
|
258
|
+
Object.freeze({
|
|
259
|
+
name: "[prompt|set]",
|
|
260
|
+
description: "Without a subcommand, prints the saved app blueprint. prompt renders the AI prompt; set writes the approved blueprint."
|
|
261
|
+
})
|
|
262
|
+
]),
|
|
263
|
+
defaults: Object.freeze([
|
|
264
|
+
"The app blueprint is product-level app state, not a feature-session step.",
|
|
265
|
+
"The saved blueprint lives at .jskit/APP_BLUEPRINT.md in the current target app.",
|
|
266
|
+
"Project prompt overrides live at .jskit/prompts/app_blueprint.md.",
|
|
267
|
+
"Use --json for a stable machine-readable response."
|
|
268
|
+
]),
|
|
269
|
+
examples: Object.freeze([
|
|
270
|
+
Object.freeze({
|
|
271
|
+
label: "Manual blueprint flow",
|
|
272
|
+
lines: Object.freeze([
|
|
273
|
+
"jskit blueprint prompt --brief \"A field app for customer visits\"",
|
|
274
|
+
"jskit blueprint set --blueprint -",
|
|
275
|
+
"jskit blueprint --json"
|
|
276
|
+
])
|
|
277
|
+
})
|
|
278
|
+
]),
|
|
279
|
+
fullUse:
|
|
280
|
+
"jskit blueprint [prompt|set] [--brief <text>|--brief-file <path>] [--blueprint <text>|--blueprint-file <path>] [--json]",
|
|
281
|
+
showHelpOnBareInvocation: false,
|
|
282
|
+
handlerName: "commandBlueprint",
|
|
283
|
+
allowedFlagKeys: Object.freeze(["json"]),
|
|
284
|
+
inlineOptionMode: "delegate",
|
|
285
|
+
allowedValueOptionNames: Object.freeze([]),
|
|
286
|
+
canDelegateInlineOptions: (positional = []) => {
|
|
287
|
+
const subcommand = String(Array.isArray(positional) ? positional[0] || "" : "").trim();
|
|
288
|
+
return subcommand === "prompt" || subcommand === "set";
|
|
289
|
+
}
|
|
290
|
+
}),
|
|
201
291
|
add: Object.freeze({
|
|
202
292
|
command: "add",
|
|
203
293
|
aliases: Object.freeze([]),
|
|
@@ -6,6 +6,8 @@ import { createAppCommands } from "../commandHandlers/app.js";
|
|
|
6
6
|
import { createMobileCommands } from "../commandHandlers/mobile.js";
|
|
7
7
|
import { createHealthCommands } from "../commandHandlers/health.js";
|
|
8
8
|
import { createCompletionCommands } from "../commandHandlers/completion.js";
|
|
9
|
+
import { createSessionCommands } from "../commandHandlers/session.js";
|
|
10
|
+
import { createBlueprintCommands } from "../commandHandlers/blueprint.js";
|
|
9
11
|
|
|
10
12
|
function createCommandHandlers(deps = {}) {
|
|
11
13
|
const shared = createCommandHandlerShared(deps);
|
|
@@ -29,6 +31,8 @@ function createCommandHandlers(deps = {}) {
|
|
|
29
31
|
const { commandMobile } = createMobileCommands(commandContext, { commandAdd });
|
|
30
32
|
const { commandDoctor, commandLintDescriptors } = createHealthCommands(commandContext);
|
|
31
33
|
const { commandCompletion } = createCompletionCommands(commandContext);
|
|
34
|
+
const { commandSession } = createSessionCommands(commandContext);
|
|
35
|
+
const { commandBlueprint } = createBlueprintCommands(commandContext);
|
|
32
36
|
|
|
33
37
|
return {
|
|
34
38
|
commandList,
|
|
@@ -46,7 +50,9 @@ function createCommandHandlers(deps = {}) {
|
|
|
46
50
|
commandUpdate,
|
|
47
51
|
commandRemove,
|
|
48
52
|
commandDoctor,
|
|
49
|
-
commandLintDescriptors
|
|
53
|
+
commandLintDescriptors,
|
|
54
|
+
commandSession,
|
|
55
|
+
commandBlueprint
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
58
|
|
package/src/server/index.js
CHANGED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
const SESSION_STATUS = Object.freeze({
|
|
5
|
+
ABANDONED: "abandoned",
|
|
6
|
+
BLOCKED: "blocked",
|
|
7
|
+
FAILED: "failed",
|
|
8
|
+
FINISHED: "finished",
|
|
9
|
+
PENDING: "pending",
|
|
10
|
+
RUNNING: "running",
|
|
11
|
+
WAITING_FOR_USER: "waiting_for_user"
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(?:-[a-z0-9]{4})?$/u;
|
|
15
|
+
const SESSION_STATE_RELATIVE_PATH = ".jskit/sessions";
|
|
16
|
+
const PROMPT_DIRECTORY = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "prompts");
|
|
17
|
+
|
|
18
|
+
const INPUT_NONE = Object.freeze({ type: "none" });
|
|
19
|
+
const ISSUE_TITLE_INPUT = Object.freeze({
|
|
20
|
+
extract: "issue_title",
|
|
21
|
+
formatHint: "text",
|
|
22
|
+
label: "Approved issue title",
|
|
23
|
+
name: "issueTitle",
|
|
24
|
+
required: true,
|
|
25
|
+
type: "text"
|
|
26
|
+
});
|
|
27
|
+
const ISSUE_TEXT_INPUT = Object.freeze({
|
|
28
|
+
extract: "issue_text",
|
|
29
|
+
formatHint: "markdown",
|
|
30
|
+
label: "Approved issue body",
|
|
31
|
+
multiline: true,
|
|
32
|
+
name: "issue",
|
|
33
|
+
required: true,
|
|
34
|
+
type: "text"
|
|
35
|
+
});
|
|
36
|
+
const ISSUE_DRAFT_INPUT = Object.freeze({
|
|
37
|
+
fields: Object.freeze([
|
|
38
|
+
ISSUE_TITLE_INPUT,
|
|
39
|
+
ISSUE_TEXT_INPUT
|
|
40
|
+
]),
|
|
41
|
+
type: "object"
|
|
42
|
+
});
|
|
43
|
+
const ISSUE_TITLE_OUTPUT = Object.freeze({
|
|
44
|
+
extract: "issue_title",
|
|
45
|
+
field: "issueTitle",
|
|
46
|
+
formatHint: "text",
|
|
47
|
+
label: "Issue title",
|
|
48
|
+
required: true
|
|
49
|
+
});
|
|
50
|
+
const ISSUE_TEXT_OUTPUT = Object.freeze({
|
|
51
|
+
extract: "issue_text",
|
|
52
|
+
field: "issue",
|
|
53
|
+
formatHint: "markdown",
|
|
54
|
+
label: "Issue body",
|
|
55
|
+
multiline: true,
|
|
56
|
+
required: true
|
|
57
|
+
});
|
|
58
|
+
const PLAN_INPUT = Object.freeze({
|
|
59
|
+
extract: "plan",
|
|
60
|
+
formatHint: "markdown",
|
|
61
|
+
label: "Approved plan",
|
|
62
|
+
multiline: true,
|
|
63
|
+
name: "plan",
|
|
64
|
+
required: true,
|
|
65
|
+
type: "text"
|
|
66
|
+
});
|
|
67
|
+
const PLAN_OUTPUT = Object.freeze({
|
|
68
|
+
extract: "plan",
|
|
69
|
+
field: "plan",
|
|
70
|
+
formatHint: "markdown",
|
|
71
|
+
label: "Plan",
|
|
72
|
+
multiline: true,
|
|
73
|
+
required: true
|
|
74
|
+
});
|
|
75
|
+
const USER_CHECK_INPUT = Object.freeze({
|
|
76
|
+
label: "User check result",
|
|
77
|
+
name: "userCheck",
|
|
78
|
+
options: Object.freeze([
|
|
79
|
+
Object.freeze({ label: "Passed", value: "passed" }),
|
|
80
|
+
Object.freeze({ label: "Failed", value: "failed" })
|
|
81
|
+
]),
|
|
82
|
+
required: true,
|
|
83
|
+
type: "choice"
|
|
84
|
+
});
|
|
85
|
+
function codexHandoff(expectedOutput, {
|
|
86
|
+
autoInject = false,
|
|
87
|
+
promptActionLabel = ""
|
|
88
|
+
} = {}) {
|
|
89
|
+
const expectedOutputs = Object.freeze(Array.isArray(expectedOutput) ? [...expectedOutput] : [expectedOutput]);
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
...(autoInject ? { autoInject: true } : {}),
|
|
92
|
+
expectedOutput: expectedOutputs[expectedOutputs.length - 1] || null,
|
|
93
|
+
expectedOutputs,
|
|
94
|
+
mode: "inject_prompt",
|
|
95
|
+
promptField: "prompt",
|
|
96
|
+
...(promptActionLabel ? { promptActionLabel } : {})
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const PLAN_EXECUTION_CODEX_HANDOFF = codexHandoff([], {
|
|
101
|
+
autoInject: true,
|
|
102
|
+
promptActionLabel: "Execute plan"
|
|
103
|
+
});
|
|
104
|
+
const REVIEW_EXECUTION_CODEX_HANDOFF = codexHandoff([], {
|
|
105
|
+
autoInject: true,
|
|
106
|
+
promptActionLabel: "Start review"
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
function defineStep({
|
|
110
|
+
buttonLabel,
|
|
111
|
+
codex = undefined,
|
|
112
|
+
description,
|
|
113
|
+
id,
|
|
114
|
+
input = INPUT_NONE,
|
|
115
|
+
kind = "automatic",
|
|
116
|
+
label,
|
|
117
|
+
nextCommandTemplate = "jskit session {{session_id}} step",
|
|
118
|
+
preconditions = [],
|
|
119
|
+
utilityActions = []
|
|
120
|
+
}) {
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
buttonLabel,
|
|
123
|
+
codex,
|
|
124
|
+
description,
|
|
125
|
+
id,
|
|
126
|
+
input,
|
|
127
|
+
kind,
|
|
128
|
+
label,
|
|
129
|
+
nextCommandTemplate,
|
|
130
|
+
preconditions: Object.freeze([...preconditions]),
|
|
131
|
+
utilityActions: Object.freeze([...utilityActions])
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const STEP_DEFINITIONS = Object.freeze([
|
|
136
|
+
defineStep({
|
|
137
|
+
buttonLabel: "Create session",
|
|
138
|
+
description: "Create the filesystem-backed session record.",
|
|
139
|
+
id: "session_created",
|
|
140
|
+
label: "Session created"
|
|
141
|
+
}),
|
|
142
|
+
defineStep({
|
|
143
|
+
buttonLabel: "Create worktree",
|
|
144
|
+
description: "Create the isolated branch and worktree for this session.",
|
|
145
|
+
id: "worktree_created",
|
|
146
|
+
label: "Worktree created",
|
|
147
|
+
preconditions: ["session_exists", "git_repository", "git_current_branch"]
|
|
148
|
+
}),
|
|
149
|
+
defineStep({
|
|
150
|
+
buttonLabel: "Install dependencies",
|
|
151
|
+
description: "Install Node dependencies inside the session worktree before Codex starts.",
|
|
152
|
+
id: "dependencies_installed",
|
|
153
|
+
label: "Dependencies installed",
|
|
154
|
+
preconditions: ["session_exists", "worktree_exists"]
|
|
155
|
+
}),
|
|
156
|
+
defineStep({
|
|
157
|
+
buttonLabel: "Submit prompt to Codex",
|
|
158
|
+
description: "Capture the initial change request and send it to Codex to draft a GitHub issue.",
|
|
159
|
+
id: "issue_prompt_rendered",
|
|
160
|
+
input: Object.freeze({
|
|
161
|
+
label: "What should change?",
|
|
162
|
+
multiline: true,
|
|
163
|
+
name: "prompt",
|
|
164
|
+
placeholder: "Describe the feature, bug, or change request.",
|
|
165
|
+
required: true,
|
|
166
|
+
type: "text"
|
|
167
|
+
}),
|
|
168
|
+
kind: "human_input",
|
|
169
|
+
label: "Initial issue prompt",
|
|
170
|
+
nextCommandTemplate: "jskit session {{session_id}} step --prompt \"<what should change>\"",
|
|
171
|
+
preconditions: ["session_exists"]
|
|
172
|
+
}),
|
|
173
|
+
defineStep({
|
|
174
|
+
buttonLabel: "Save issue draft",
|
|
175
|
+
codex: codexHandoff([
|
|
176
|
+
ISSUE_TITLE_OUTPUT,
|
|
177
|
+
ISSUE_TEXT_OUTPUT
|
|
178
|
+
]),
|
|
179
|
+
description: "Save the approved issue title and body drafted by Codex.",
|
|
180
|
+
id: "issue_drafted",
|
|
181
|
+
input: ISSUE_DRAFT_INPUT,
|
|
182
|
+
kind: "codex_output",
|
|
183
|
+
label: "Issue drafted",
|
|
184
|
+
nextCommandTemplate: "jskit session {{session_id}} step --issue -",
|
|
185
|
+
preconditions: ["session_exists"]
|
|
186
|
+
}),
|
|
187
|
+
defineStep({
|
|
188
|
+
buttonLabel: "Create GitHub issue",
|
|
189
|
+
description: "Create the GitHub issue from the approved draft.",
|
|
190
|
+
id: "issue_created",
|
|
191
|
+
label: "Issue created",
|
|
192
|
+
preconditions: ["session_exists", "issue_text_exists", "github_auth", "github_origin"]
|
|
193
|
+
}),
|
|
194
|
+
defineStep({
|
|
195
|
+
buttonLabel: "Execute plan",
|
|
196
|
+
codex: codexHandoff(PLAN_OUTPUT),
|
|
197
|
+
description: "Save the approved implementation plan, comment it on the GitHub issue, and submit it to Codex.",
|
|
198
|
+
id: "plan_made",
|
|
199
|
+
input: PLAN_INPUT,
|
|
200
|
+
kind: "codex_output",
|
|
201
|
+
label: "Plan execution",
|
|
202
|
+
nextCommandTemplate: "jskit session {{session_id}} step --plan -",
|
|
203
|
+
preconditions: ["session_exists", "issue_text_exists", "issue_url_exists", "worktree_exists"]
|
|
204
|
+
}),
|
|
205
|
+
defineStep({
|
|
206
|
+
buttonLabel: "Accept changes",
|
|
207
|
+
description: "Review the worktree diff and accept the changes as ready to commit.",
|
|
208
|
+
id: "implementation_changes_accepted",
|
|
209
|
+
kind: "user_check",
|
|
210
|
+
label: "Changes accepted",
|
|
211
|
+
preconditions: ["session_exists", "worktree_exists"],
|
|
212
|
+
utilityActions: Object.freeze([
|
|
213
|
+
Object.freeze({
|
|
214
|
+
id: "session_diff",
|
|
215
|
+
kind: "diff",
|
|
216
|
+
label: "Review changes",
|
|
217
|
+
nextCommandTemplate: "jskit session {{session_id}} diff"
|
|
218
|
+
})
|
|
219
|
+
])
|
|
220
|
+
}),
|
|
221
|
+
defineStep({
|
|
222
|
+
buttonLabel: "Commit implementation",
|
|
223
|
+
description: "Commit the accepted implementation changes in the session worktree.",
|
|
224
|
+
id: "implementation_changes_committed",
|
|
225
|
+
label: "Changes committed",
|
|
226
|
+
preconditions: ["session_exists", "worktree_exists"]
|
|
227
|
+
}),
|
|
228
|
+
defineStep({
|
|
229
|
+
buttonLabel: "Start review",
|
|
230
|
+
description: "Submit the code review prompt to Codex for the committed changes.",
|
|
231
|
+
id: "review_prompt_rendered",
|
|
232
|
+
kind: "codex_prompt",
|
|
233
|
+
label: "Review execution",
|
|
234
|
+
preconditions: ["session_exists", "worktree_exists"]
|
|
235
|
+
}),
|
|
236
|
+
defineStep({
|
|
237
|
+
buttonLabel: "Accept review changes",
|
|
238
|
+
description: "Review the post-review worktree diff and accept it as ready to commit.",
|
|
239
|
+
id: "review_changes_accepted",
|
|
240
|
+
kind: "user_check",
|
|
241
|
+
label: "Review changes accepted",
|
|
242
|
+
preconditions: ["session_exists", "worktree_exists"],
|
|
243
|
+
utilityActions: Object.freeze([
|
|
244
|
+
Object.freeze({
|
|
245
|
+
id: "session_diff",
|
|
246
|
+
kind: "diff",
|
|
247
|
+
label: "Review changes",
|
|
248
|
+
nextCommandTemplate: "jskit session {{session_id}} diff"
|
|
249
|
+
})
|
|
250
|
+
])
|
|
251
|
+
}),
|
|
252
|
+
defineStep({
|
|
253
|
+
buttonLabel: "Commit review changes",
|
|
254
|
+
description: "Commit accepted review changes, or record that no review changes were needed.",
|
|
255
|
+
id: "review_changes_committed",
|
|
256
|
+
label: "Review changes committed",
|
|
257
|
+
preconditions: ["session_exists", "worktree_exists"]
|
|
258
|
+
}),
|
|
259
|
+
defineStep({
|
|
260
|
+
buttonLabel: "Save user check",
|
|
261
|
+
description: "Record whether the user’s manual check passed.",
|
|
262
|
+
id: "user_check_completed",
|
|
263
|
+
input: USER_CHECK_INPUT,
|
|
264
|
+
kind: "user_check",
|
|
265
|
+
label: "User check",
|
|
266
|
+
nextCommandTemplate: "jskit session {{session_id}} step --user-check passed",
|
|
267
|
+
preconditions: ["session_exists"]
|
|
268
|
+
}),
|
|
269
|
+
defineStep({
|
|
270
|
+
buttonLabel: "Run verification",
|
|
271
|
+
description: "Run the project verification command in the session worktree.",
|
|
272
|
+
id: "doctor_run",
|
|
273
|
+
label: "Verification run",
|
|
274
|
+
preconditions: ["session_exists", "worktree_exists"]
|
|
275
|
+
}),
|
|
276
|
+
defineStep({
|
|
277
|
+
buttonLabel: "Push branch and create PR",
|
|
278
|
+
description: "Push the session branch to origin and create a GitHub pull request.",
|
|
279
|
+
id: "pr_created",
|
|
280
|
+
label: "Branch pushed, PR created",
|
|
281
|
+
preconditions: ["session_exists", "worktree_exists"]
|
|
282
|
+
}),
|
|
283
|
+
defineStep({
|
|
284
|
+
buttonLabel: "Merge PR and remove worktree",
|
|
285
|
+
description: "Merge the pull request, close the GitHub issue, and remove the session worktree.",
|
|
286
|
+
id: "pr_merged",
|
|
287
|
+
label: "PR merged, worktree removed",
|
|
288
|
+
preconditions: ["session_exists", "pr_url_exists", "worktree_exists"]
|
|
289
|
+
}),
|
|
290
|
+
defineStep({
|
|
291
|
+
buttonLabel: "Finish session",
|
|
292
|
+
description: "Write the final receipt and archive the completed session.",
|
|
293
|
+
id: "session_finished",
|
|
294
|
+
label: "Session finished",
|
|
295
|
+
preconditions: ["session_exists"]
|
|
296
|
+
})
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
const STEP_IDS = Object.freeze(STEP_DEFINITIONS.map((step) => step.id));
|
|
300
|
+
const STEP_LABEL_BY_ID = Object.freeze(Object.fromEntries(STEP_DEFINITIONS.map((step) => [step.id, step.label])));
|
|
301
|
+
const STEP_DEFINITION_BY_ID = Object.freeze(Object.fromEntries(STEP_DEFINITIONS.map((step) => [step.id, step])));
|
|
302
|
+
const STEP_PRECONDITION_NAMES = Object.freeze(Object.fromEntries(
|
|
303
|
+
STEP_DEFINITIONS
|
|
304
|
+
.filter((step) => step.id !== "session_created")
|
|
305
|
+
.map((step) => [step.id, step.preconditions])
|
|
306
|
+
));
|
|
307
|
+
|
|
308
|
+
export {
|
|
309
|
+
PROMPT_DIRECTORY,
|
|
310
|
+
SESSION_ID_PATTERN,
|
|
311
|
+
SESSION_STATUS,
|
|
312
|
+
STEP_DEFINITION_BY_ID,
|
|
313
|
+
STEP_DEFINITIONS,
|
|
314
|
+
STEP_IDS,
|
|
315
|
+
STEP_LABEL_BY_ID,
|
|
316
|
+
STEP_PRECONDITION_NAMES,
|
|
317
|
+
PLAN_EXECUTION_CODEX_HANDOFF,
|
|
318
|
+
REVIEW_EXECUTION_CODEX_HANDOFF,
|
|
319
|
+
SESSION_STATE_RELATIVE_PATH
|
|
320
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { constants as fsConstants } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
function normalizeText(value) {
|
|
10
|
+
return String(value || "").trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function timestampForReceipt(now = new Date()) {
|
|
14
|
+
return now.toISOString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function fileExists(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
await access(filePath, fsConstants.F_OK);
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function readTextIfExists(filePath) {
|
|
27
|
+
if (!filePath || !(await fileExists(filePath))) {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
return readFile(filePath, "utf8");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function readTrimmedFile(filePath) {
|
|
34
|
+
return normalizeText(await readTextIfExists(filePath));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function writeTextFile(filePath, value) {
|
|
38
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
39
|
+
await writeFile(filePath, `${String(value || "").replace(/\s*$/u, "")}\n`, "utf8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function runCommand(command, args = [], { cwd, env = {}, timeout = 30000 } = {}) {
|
|
43
|
+
try {
|
|
44
|
+
const result = await execFileAsync(command, args, {
|
|
45
|
+
cwd,
|
|
46
|
+
env: {
|
|
47
|
+
...process.env,
|
|
48
|
+
...env
|
|
49
|
+
},
|
|
50
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
51
|
+
timeout
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
exitCode: 0,
|
|
55
|
+
ok: true,
|
|
56
|
+
output: String([result.stdout, result.stderr].filter(Boolean).join("\n")).trim(),
|
|
57
|
+
stderr: String(result.stderr || "").trim(),
|
|
58
|
+
stdout: String(result.stdout || "").trim()
|
|
59
|
+
};
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const stdout = String(error?.stdout || "").trim();
|
|
62
|
+
const stderr = String(error?.stderr || "").trim();
|
|
63
|
+
return {
|
|
64
|
+
exitCode: typeof error?.code === "number" ? error.code : 1,
|
|
65
|
+
ok: false,
|
|
66
|
+
output: String(error?.message || [stdout, stderr].filter(Boolean).join("\n")).trim(),
|
|
67
|
+
stderr,
|
|
68
|
+
stdout
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function runGit(targetRoot, args = [], options = {}) {
|
|
74
|
+
return runCommand("git", args, {
|
|
75
|
+
cwd: targetRoot,
|
|
76
|
+
...options
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function runGitInWorktree(worktree, args = [], options = {}) {
|
|
81
|
+
return runCommand("git", args, {
|
|
82
|
+
cwd: worktree,
|
|
83
|
+
...options
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
fileExists,
|
|
89
|
+
normalizeText,
|
|
90
|
+
readTextIfExists,
|
|
91
|
+
readTrimmedFile,
|
|
92
|
+
runCommand,
|
|
93
|
+
runGit,
|
|
94
|
+
runGitInWorktree,
|
|
95
|
+
timestampForReceipt,
|
|
96
|
+
writeTextFile
|
|
97
|
+
};
|