@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.
@@ -0,0 +1,31 @@
1
+ Create a GitHub issue from this user request:
2
+
3
+ {{user_input}}
4
+
5
+ First inspect the local app enough to understand the request in context. Use package.json, config, .jskit metadata, routes, packages, and any saved app blueprint when available. Classify the current app state as empty, non_jskit_repo, partial_jskit_app, or jskit_app before assuming ordinary feature work is possible.
6
+
7
+ Draft an implementation-ready issue, not a broad product essay.
8
+
9
+ Preserve these JSKIT boundaries:
10
+
11
+ - If the app is empty or a partial JSKIT app, the issue should be about bootstrap or recovery before feature implementation.
12
+ - If platform choices are still provisional, make the issue resolve those choices before installing tenancy-sensitive packages.
13
+ - Do not ask the developer to redesign standard JSKIT package-owned workflows from scratch. Treat selected package workflows as defaults unless the request asks for overrides, restrictions, or custom additions.
14
+ - For persisted app-owned data, prefer generated/package ownership over direct hand-built persistence. A new ordinary table should usually become a server CRUD-owned entity before CRUD UI or route work.
15
+ - For non-CRUD app pages, prefer the JSKIT UI generator when it fits.
16
+ - Keep direct knex or low-level runtime work exceptional and explicitly justified.
17
+ - Do not include workflow bookkeeping such as old workboards in the issue body. JSKIT session state and receipts are the workflow tracker.
18
+
19
+ Ask concise clarifying questions if the request is not specific enough to produce a useful implementation issue. Ask only for details that materially change the ticket.
20
+
21
+ When the issue is ready, output only the final issue title and body surrounded by these exact markers:
22
+
23
+ [issue_title]
24
+ <short issue title>
25
+ [/issue_title]
26
+
27
+ [issue_text]
28
+ <issue body in Markdown, without repeating the title as a heading>
29
+ [/issue_text]
30
+
31
+ The issue should be concrete, scoped, and implementation-ready. Include acceptance criteria when they are useful.
@@ -0,0 +1,50 @@
1
+ Create an implementation plan for JSKIT session {{session_id}}.
2
+
3
+ GitHub issue: {{issue_url}}
4
+ Issue number: {{issue_number}}
5
+ Issue title: {{issue_title}}
6
+ Issue body file: {{issue_file}}
7
+ Issue title file: {{issue_title_file}}
8
+ Plan file to create: {{plan_file}}
9
+ Worktree: {{worktree}}
10
+
11
+ Read the issue and inspect the local app before planning. Use the issue files, package.json, .jskit metadata, config, packages, routes, generated references, package docs, and any saved app blueprint when available.
12
+
13
+ Start by identifying the app state and the implementation lane:
14
+
15
+ - empty, non_jskit_repo, partial_jskit_app, or jskit_app
16
+ - package install
17
+ - generator scaffolding
18
+ - custom local code
19
+ - a combination of those
20
+
21
+ Planning rules:
22
+
23
+ - Keep the plan scoped to the issue. Avoid "while I am here" work.
24
+ - Prefer vertical slices that produce visible or end-to-end progress.
25
+ - If the work is too broad to review confidently, split it into clear chunks.
26
+ - Make generator decisions concrete. Name the exact `jskit` commands to run when a generator or package install applies.
27
+ - For non-CRUD route page work, plan to check `jskit show ui-generator --details` and `jskit list-placements` before hand-writing pages or placement entries.
28
+ - For CRUD work, plan server ownership first. Name the `jskit generate crud-server-generator scaffold ...` command before any CRUD UI plan.
29
+ - For CRUD-owned tables, plan around the real database table shape. Do not plan a separate hand-written migration for a table that `crud-server-generator` will own.
30
+ - Every persisted app-owned table should have generated/package CRUD ownership unless there is a narrow explicit exception.
31
+ - Do not hand-build CRUD routes, CRUD endpoints, or CRUD page trees before the server CRUD package and shared resource file exist.
32
+ - `feature-server-generator` is for non-CRUD workflows and orchestration, not ordinary persisted entity tables.
33
+ - Keep runtime, UI, and data concerns separated.
34
+ - Keep direct knex exceptional and minimal. Prefer internal json-rest-api seams outside generated CRUD packages and explicit weird-custom feature lanes.
35
+ - For package-owned baseline workflows, plan to install and verify the baseline before inventing custom code around it.
36
+ - For user-facing UI, include Material/Vuetify quality expectations and a Playwright verification path.
37
+ - If login is required for UI verification, plan for a development-only auth bootstrap path instead of live external auth.
38
+ - Do not create or update the old `.jskit/WORKBOARD.md` workflow. JSKIT session state, receipts, issue text, plan file, transcript, and commits are the tracker.
39
+
40
+ If setup values are needed, ask plainly using exact env var or option names. For example: DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, AUTH_SUPABASE_URL, AUTH_SUPABASE_PUBLISHABLE_KEY, APP_PUBLIC_URL.
41
+
42
+ If the issue is not clear enough to plan safely, ask the user concise follow-up questions first.
43
+
44
+ When the plan is ready, output only the final plan surrounded by these exact markers:
45
+
46
+ [plan]
47
+ <implementation plan in Markdown>
48
+ [/plan]
49
+
50
+ Keep the plan concrete and implementation-oriented. Include the likely files or areas to touch, ordered steps, generator commands to consider, review expectations, and checks that should be run.
@@ -0,0 +1,28 @@
1
+ The JSKIT session PR operation failed.
2
+
3
+ Session:
4
+
5
+ {{session_id}}
6
+
7
+ Worktree:
8
+
9
+ {{worktree}}
10
+
11
+ Failure output:
12
+
13
+ {{doctor_output}}
14
+
15
+ Diagnose the failure and fix only what is required in this worktree.
16
+
17
+ Check for:
18
+
19
+ - missing or wrong GitHub remote
20
+ - missing GitHub auth
21
+ - branch push failure
22
+ - PR body/title issue
23
+ - stale local git state
24
+ - rejected merge or failing required checks
25
+
26
+ Use repository-portable repairs only. Do not hard-code local paths or credentials. Do not create a parallel manual workflow around the JSKIT session.
27
+
28
+ Do not push, open a PR, merge, or remove the worktree unless JSKIT Studio or the JSKIT session step explicitly instructs it. Report root cause, changed files, commands run, and remaining risk.
@@ -0,0 +1,43 @@
1
+ Review changes for session {{session_id}}.
2
+
3
+ Changed files from the latest commit:
4
+
5
+ {{changed_files}}
6
+
7
+ Review the committed changes and fix important issues in this worktree when the fix is clear and scoped.
8
+
9
+ Use four passes:
10
+
11
+ 1. Deslop review
12
+ - repeated functions or duplicated local helpers
13
+ - helpers reimplemented locally when a kernel/runtime seam already exists
14
+ - placeholder, fake-complete, or vague UI/copy/code structure
15
+ - dead code, unused props/imports, TODO-shaped gaps, or accidental abstractions
16
+ - missing loading, empty, error, permission, or ownership states
17
+ - broken flows, missing route wiring, missing migrations, or stale generated metadata
18
+ - surface or entity ownership mistakes: public, user, workspace, workspace_user
19
+ 2. JSKIT review
20
+ - existing helper/runtime seam available?
21
+ - should this have been a package install, generator step, or scaffold extension instead of hand code?
22
+ - if a generator existed, was the exact `jskit` command used or was the gap documented?
23
+ - for CRUD work, was `crud-server-generator scaffold` used before CRUD UI or CRUD route hand-coding?
24
+ - for CRUD-owned tables, did the change avoid a separate hand-written CRUD migration?
25
+ - does every live app-owned table have generated/package CRUD ownership or a narrow explicit exception?
26
+ - is direct app-owned knex usage limited to generated CRUD packages or explicit weird-custom feature lanes?
27
+ - are surface, route, ownership, package metadata, and migration choices aligned with JSKIT conventions?
28
+ 3. UI standards review
29
+ - user-facing screens follow Material Design and Vuetify best practices
30
+ - list screens have clear hierarchy, actions, density, empty states, and table/list patterns
31
+ - view and edit/new screens have clear grouping, labels, helper text, validation, spacing, and action placement
32
+ - responsive layout, loading, disabled, success, and error states are coherent
33
+ - improve weak screens before sign-off when the fix is scoped
34
+ 4. Verification review
35
+ - run the smallest relevant verification commands for the changed scope
36
+ - any changed user-facing UI should be exercised with Playwright when possible
37
+ - UI verification should normally be recorded through `jskit app verify-ui`
38
+ - if login is required, use the chosen local test-auth path instead of live external auth
39
+ - if there is no usable local auth bootstrap path, record it as a blocking testability gap
40
+
41
+ Do not create commits, branches, pull requests, merges, or worktree cleanup yourself. JSKIT session owns those steps.
42
+
43
+ When finished, report findings ordered by severity, fixes made, changed files, checks run, and anything still unverified. If there are no important findings, say so explicitly and list residual risk.
@@ -0,0 +1,13 @@
1
+ User check for session {{session_id}}.
2
+
3
+ The code should already be built or runnable according to the implementation instructions.
4
+
5
+ Ask the user to test the changed behavior in the app and report whether it works as intended. Be specific about the user-visible behavior, route, command, or workflow to inspect.
6
+
7
+ If the user finds a problem, diagnose the root cause and fix it in this worktree. Keep the fix scoped. Reuse JSKIT helpers, generated seams, and package workflows where they apply.
8
+
9
+ If the behavior works, tell the user to run:
10
+
11
+ jskit session {{session_id}} step --user-check passed
12
+
13
+ Do not commit, push, create a PR, merge, or clean up the worktree yourself. JSKIT session owns those steps.
@@ -0,0 +1,442 @@
1
+ import { mkdir, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {
4
+ SESSION_STATUS,
5
+ STEP_DEFINITION_BY_ID,
6
+ STEP_DEFINITIONS,
7
+ STEP_IDS,
8
+ STEP_LABEL_BY_ID
9
+ } from "./constants.js";
10
+ import {
11
+ fileExists,
12
+ normalizeText,
13
+ readTextIfExists,
14
+ readTrimmedFile,
15
+ timestampForReceipt,
16
+ writeTextFile
17
+ } from "./io.js";
18
+ import {
19
+ pathsForExistingSession
20
+ } from "./paths.js";
21
+ import {
22
+ hasWorktree
23
+ } from "./worktrees.js";
24
+
25
+ function createError({
26
+ code,
27
+ message,
28
+ repairCommand = ""
29
+ }) {
30
+ return Object.freeze({
31
+ code: normalizeText(code),
32
+ message: normalizeText(message),
33
+ repairCommand: normalizeText(repairCommand)
34
+ });
35
+ }
36
+
37
+ function createPrecondition({
38
+ id,
39
+ ok,
40
+ message
41
+ }) {
42
+ return Object.freeze({
43
+ id: normalizeText(id),
44
+ ok: ok === true,
45
+ message: normalizeText(message)
46
+ });
47
+ }
48
+
49
+ const LEGACY_CURRENT_STEP_ID_ALIASES = Object.freeze({
50
+ implementation_changes_detected: "implementation_changes_accepted",
51
+ review_changes_detected: "review_changes_accepted"
52
+ });
53
+
54
+ const LEGACY_COMPLETED_STEP_ID_ALIASES = Object.freeze({
55
+ implementation_changes_detected: Object.freeze(["implementation_changes_accepted"]),
56
+ review_changes_detected: Object.freeze(["review_changes_accepted", "review_changes_committed"])
57
+ });
58
+
59
+ const LEGACY_RECEIPT_STEP_ID_ALIASES = Object.freeze({
60
+ implementation_changes_detected: "implementation_changes_accepted",
61
+ review_changes_detected: "review_changes_committed"
62
+ });
63
+
64
+ function normalizeStepId(stepId) {
65
+ const normalized = normalizeText(stepId);
66
+ return LEGACY_CURRENT_STEP_ID_ALIASES[normalized] || normalized;
67
+ }
68
+
69
+ function completedStepIdsForReceipt(stepId) {
70
+ const normalized = normalizeText(stepId);
71
+ return LEGACY_COMPLETED_STEP_ID_ALIASES[normalized] || [normalized];
72
+ }
73
+
74
+ function receiptStepId(stepId) {
75
+ const normalized = normalizeText(stepId);
76
+ return LEGACY_RECEIPT_STEP_ID_ALIASES[normalized] || normalizeStepId(normalized);
77
+ }
78
+
79
+ function stepIndex(stepId) {
80
+ return STEP_IDS.indexOf(normalizeStepId(stepId));
81
+ }
82
+
83
+ function normalizeKnownStepIds(stepIds = []) {
84
+ return Array.from(
85
+ new Set(
86
+ stepIds
87
+ .flatMap((stepId) => completedStepIdsForReceipt(stepId))
88
+ .map((stepId) => normalizeText(stepId))
89
+ .filter((stepId) => STEP_IDS.includes(stepId))
90
+ )
91
+ ).sort((left, right) => STEP_IDS.indexOf(left) - STEP_IDS.indexOf(right));
92
+ }
93
+
94
+ function stepCanExposeStoredPrompt(stepId) {
95
+ const step = STEP_DEFINITION_BY_ID[normalizeStepId(stepId)];
96
+ return Boolean(step?.codex || step?.kind === "human_input");
97
+ }
98
+
99
+ const PROMPT_ARTIFACT_BY_STEP_ID = Object.freeze({
100
+ issue_drafted: "issue_draft.md",
101
+ plan_made: "plan_request.md",
102
+ user_check_completed: "user_check.md"
103
+ });
104
+
105
+ async function readPromptForStep(paths, stepId) {
106
+ if (!stepCanExposeStoredPrompt(stepId)) {
107
+ return "";
108
+ }
109
+ const promptArtifact = PROMPT_ARTIFACT_BY_STEP_ID[normalizeStepId(stepId)];
110
+ if (promptArtifact) {
111
+ const prompt = await readTextIfExists(path.join(paths.sessionRoot, "prompts", promptArtifact));
112
+ if (prompt) {
113
+ return prompt;
114
+ }
115
+ }
116
+ return readTextIfExists(path.join(paths.sessionRoot, "prompt.md"));
117
+ }
118
+
119
+ async function readCompletedSteps(sessionRoot) {
120
+ const stepsRoot = path.join(sessionRoot, "steps");
121
+ try {
122
+ const entries = await readdir(stepsRoot, { withFileTypes: true });
123
+ return normalizeKnownStepIds(entries
124
+ .filter((entry) => entry.isFile())
125
+ .map((entry) => entry.name));
126
+ } catch {
127
+ return [];
128
+ }
129
+ }
130
+
131
+ async function readReceiptSteps(paths) {
132
+ const stepsRoot = path.join(paths.sessionRoot, "steps");
133
+ try {
134
+ const entries = await readdir(stepsRoot, { withFileTypes: true });
135
+ const knownStepRows = new Map();
136
+ const unknownStepRows = [];
137
+ entries
138
+ .filter((entry) => entry.isFile())
139
+ .map((entry) => entry.name)
140
+ .forEach((receiptName) => {
141
+ const stepId = receiptStepId(receiptName);
142
+ if (STEP_IDS.includes(stepId)) {
143
+ if (!knownStepRows.has(stepId) || receiptName === stepId) {
144
+ knownStepRows.set(stepId, {
145
+ receiptName,
146
+ stepId
147
+ });
148
+ }
149
+ return;
150
+ }
151
+ unknownStepRows.push({
152
+ receiptName,
153
+ stepId
154
+ });
155
+ });
156
+
157
+ const stepRows = [...knownStepRows.values(), ...unknownStepRows]
158
+ .sort((left, right) => {
159
+ const leftIndex = stepIndex(left.stepId);
160
+ const rightIndex = stepIndex(right.stepId);
161
+ if (leftIndex >= 0 && rightIndex >= 0) {
162
+ return leftIndex - rightIndex;
163
+ }
164
+ if (leftIndex >= 0) {
165
+ return -1;
166
+ }
167
+ if (rightIndex >= 0) {
168
+ return 1;
169
+ }
170
+ return left.stepId.localeCompare(right.stepId);
171
+ });
172
+
173
+ return Promise.all(stepRows.map(async ({ receiptName, stepId }) => ({
174
+ label: STEP_LABEL_BY_ID[stepId] || stepId,
175
+ receipt: (await readTextIfExists(path.join(stepsRoot, receiptName))).trim(),
176
+ stepId
177
+ })));
178
+ } catch {
179
+ return [];
180
+ }
181
+ }
182
+
183
+ function resolveNextStep(completedSteps = []) {
184
+ const completed = new Set(completedSteps);
185
+ return STEP_IDS.find((stepId) => !completed.has(stepId)) || "";
186
+ }
187
+
188
+ function cloneContractValue(value) {
189
+ if (!value || typeof value !== "object") {
190
+ return value;
191
+ }
192
+ if (Array.isArray(value)) {
193
+ return value.map((entry) => cloneContractValue(entry));
194
+ }
195
+ return Object.fromEntries(
196
+ Object.entries(value).map(([key, entry]) => [key, cloneContractValue(entry)])
197
+ );
198
+ }
199
+
200
+ function publicStepDefinition(step, index) {
201
+ return {
202
+ description: step.description,
203
+ id: step.id,
204
+ index,
205
+ input: cloneContractValue(step.input),
206
+ kind: step.kind,
207
+ label: step.label,
208
+ utilityActions: cloneContractValue(step.utilityActions || [])
209
+ };
210
+ }
211
+
212
+ function buildStepDefinitions() {
213
+ return STEP_DEFINITIONS.map((step, index) => publicStepDefinition(step, index));
214
+ }
215
+
216
+ function buildCurrentStepAction(stepId) {
217
+ const step = STEP_DEFINITION_BY_ID[stepId];
218
+ if (!step) {
219
+ return null;
220
+ }
221
+ return {
222
+ buttonLabel: step.buttonLabel,
223
+ description: step.description,
224
+ index: STEP_IDS.indexOf(step.id),
225
+ input: cloneContractValue(step.input),
226
+ kind: step.kind,
227
+ stepId: step.id,
228
+ utilityActions: cloneContractValue(step.utilityActions || [])
229
+ };
230
+ }
231
+
232
+ function buildCodexHandoff(stepId) {
233
+ const step = STEP_DEFINITION_BY_ID[stepId];
234
+ return step?.codex ? cloneContractValue(step.codex) : null;
235
+ }
236
+
237
+ async function readSessionArtifacts(paths) {
238
+ const [status, rawCurrentStep, issueUrl, prUrl, issueText, issueTitle, planText, codexThreadId] = await Promise.all([
239
+ readTrimmedFile(path.join(paths.sessionRoot, "status")),
240
+ readTrimmedFile(path.join(paths.sessionRoot, "current_step")),
241
+ readTrimmedFile(path.join(paths.sessionRoot, "issue_url")),
242
+ readTrimmedFile(path.join(paths.sessionRoot, "pr_url")),
243
+ readTextIfExists(path.join(paths.sessionRoot, "issue.md")),
244
+ readTrimmedFile(path.join(paths.sessionRoot, "issue_title")),
245
+ readTextIfExists(path.join(paths.sessionRoot, "plan.md")),
246
+ readTrimmedFile(path.join(paths.sessionRoot, "codex_thread_id"))
247
+ ]);
248
+ const currentStep = normalizeStepId(rawCurrentStep);
249
+ const worktreeReady = await hasWorktree(paths);
250
+ let completedSteps = await readCompletedSteps(paths.sessionRoot);
251
+ const worktreeRemovalCompleted = completedSteps.includes("pr_merged") ||
252
+ completedSteps.includes("worktree_removed");
253
+ const worktreeReceiptInvalid = !worktreeReady &&
254
+ completedSteps.includes("worktree_created") &&
255
+ !worktreeRemovalCompleted &&
256
+ status !== SESSION_STATUS.FINISHED &&
257
+ status !== SESSION_STATUS.ABANDONED;
258
+ if (worktreeReceiptInvalid) {
259
+ completedSteps = completedSteps.filter((stepId) => !["worktree_created", "dependencies_installed"].includes(stepId));
260
+ }
261
+ const nextStep = resolveNextStep(completedSteps);
262
+ const currentStepIndex = stepIndex(currentStep);
263
+ const nextStepIndex = stepIndex(nextStep);
264
+ const effectiveCurrentStep = nextStep &&
265
+ (completedSteps.includes(currentStep) || currentStepIndex < 0 || currentStepIndex > nextStepIndex)
266
+ ? nextStep
267
+ : currentStep || nextStep;
268
+ const prompt = await readPromptForStep(paths, effectiveCurrentStep);
269
+
270
+ return {
271
+ codexThreadId,
272
+ completedSteps,
273
+ currentStep: effectiveCurrentStep,
274
+ issueTitle,
275
+ issueText: issueText.trim(),
276
+ issueUrl,
277
+ nextStep,
278
+ prUrl,
279
+ planText: planText.trim(),
280
+ prompt: prompt.trim(),
281
+ status: status || SESSION_STATUS.PENDING,
282
+ worktreeReady
283
+ };
284
+ }
285
+
286
+ function buildNextCommand(sessionId, stepId) {
287
+ if (!stepId) {
288
+ return "";
289
+ }
290
+ const template = STEP_DEFINITION_BY_ID[stepId]?.nextCommandTemplate || "jskit session {{session_id}} step";
291
+ return template.replaceAll("{{session_id}}", sessionId);
292
+ }
293
+
294
+ async function buildSessionResponse(paths, {
295
+ codex = undefined,
296
+ ok = true,
297
+ errors = [],
298
+ preconditions = [],
299
+ prompt = undefined,
300
+ status = undefined
301
+ } = {}) {
302
+ const responsePaths = paths.sessionId ? await pathsForExistingSession(paths) : paths;
303
+ const artifacts = responsePaths.sessionRoot ? await readSessionArtifacts(responsePaths) : {};
304
+ const resolvedStatus = status || artifacts.status || (ok ? SESSION_STATUS.PENDING : SESSION_STATUS.BLOCKED);
305
+ const currentStep = artifacts.currentStep || artifacts.nextStep || "";
306
+ const responsePrompt = typeof prompt === "string"
307
+ ? prompt
308
+ : stepCanExposeStoredPrompt(currentStep) ? artifacts.prompt || "" : "";
309
+
310
+ return {
311
+ ok: ok === true,
312
+ sessionId: paths.sessionId || "",
313
+ status: resolvedStatus,
314
+ currentStep,
315
+ completedSteps: artifacts.completedSteps || [],
316
+ stepDefinitions: buildStepDefinitions(),
317
+ currentStepAction: buildCurrentStepAction(currentStep),
318
+ codex: codex === undefined ? buildCodexHandoff(currentStep) : cloneContractValue(codex),
319
+ prompt: responsePrompt,
320
+ nextCommand: buildNextCommand(paths.sessionId || "", currentStep),
321
+ issueUrl: artifacts.issueUrl || "",
322
+ issueTitle: artifacts.issueTitle || "",
323
+ issueText: artifacts.issueText || "",
324
+ planText: artifacts.planText || "",
325
+ prUrl: artifacts.prUrl || "",
326
+ preconditions,
327
+ errors,
328
+ archive: responsePaths.archive || (resolvedStatus === SESSION_STATUS.FINISHED ? "completed" : resolvedStatus === SESSION_STATUS.ABANDONED ? "abandoned" : "active"),
329
+ sessionRoot: responsePaths.sessionRoot || "",
330
+ worktree: paths.worktree || "",
331
+ worktreeReady: artifacts.worktreeReady === true,
332
+ branch: paths.branch || "",
333
+ codexThreadId: artifacts.codexThreadId || ""
334
+ };
335
+ }
336
+
337
+ function buildSessionErrorResponse({
338
+ targetRoot = process.cwd(),
339
+ sessionId = "",
340
+ code,
341
+ message,
342
+ repairCommand = "",
343
+ status = SESSION_STATUS.BLOCKED,
344
+ preconditions = [],
345
+ errors = undefined
346
+ } = {}) {
347
+ const normalizedTargetRoot = path.resolve(normalizeText(targetRoot) || process.cwd());
348
+ const errorList = Array.isArray(errors)
349
+ ? errors
350
+ : [
351
+ createError({
352
+ code,
353
+ message,
354
+ repairCommand
355
+ })
356
+ ];
357
+
358
+ return {
359
+ ok: false,
360
+ sessionId: normalizeText(sessionId),
361
+ status,
362
+ currentStep: "",
363
+ completedSteps: [],
364
+ stepDefinitions: buildStepDefinitions(),
365
+ currentStepAction: null,
366
+ codex: null,
367
+ prompt: "",
368
+ nextCommand: "",
369
+ issueTitle: "",
370
+ issueText: "",
371
+ planText: "",
372
+ issueUrl: "",
373
+ prUrl: "",
374
+ preconditions,
375
+ errors: errorList,
376
+ archive: "",
377
+ sessionRoot: "",
378
+ worktree: "",
379
+ worktreeReady: false,
380
+ branch: "",
381
+ codexThreadId: "",
382
+ targetRoot: normalizedTargetRoot
383
+ };
384
+ }
385
+
386
+ async function markStatus(paths, status) {
387
+ await writeTextFile(path.join(paths.sessionRoot, "status"), status);
388
+ }
389
+
390
+ async function markCurrentStep(paths, stepId) {
391
+ await writeTextFile(path.join(paths.sessionRoot, "current_step"), stepId);
392
+ }
393
+
394
+ async function writeReceipt(paths, stepId, message) {
395
+ await mkdir(path.join(paths.sessionRoot, "steps"), { recursive: true });
396
+ await writeTextFile(
397
+ path.join(paths.sessionRoot, "steps", stepId),
398
+ `${timestampForReceipt()}\n${normalizeText(message) || STEP_LABEL_BY_ID[stepId] || stepId}`
399
+ );
400
+ const completedSteps = await readCompletedSteps(paths.sessionRoot);
401
+ await markCurrentStep(paths, resolveNextStep(completedSteps));
402
+ }
403
+
404
+ async function failSession(paths, {
405
+ code,
406
+ message,
407
+ repairCommand = "",
408
+ preconditions = [],
409
+ status = SESSION_STATUS.BLOCKED,
410
+ prompt = ""
411
+ }) {
412
+ if (paths.sessionRoot && await fileExists(paths.sessionRoot)) {
413
+ await markStatus(paths, status);
414
+ }
415
+ return buildSessionResponse(paths, {
416
+ ok: false,
417
+ status,
418
+ prompt,
419
+ preconditions,
420
+ errors: [
421
+ createError({
422
+ code,
423
+ message,
424
+ repairCommand
425
+ })
426
+ ]
427
+ });
428
+ }
429
+
430
+ export {
431
+ buildSessionErrorResponse,
432
+ buildSessionResponse,
433
+ buildStepDefinitions,
434
+ createError,
435
+ createPrecondition,
436
+ failSession,
437
+ markCurrentStep,
438
+ markStatus,
439
+ readReceiptSteps,
440
+ readSessionArtifacts,
441
+ writeReceipt
442
+ };
@@ -0,0 +1,31 @@
1
+ import path from "node:path";
2
+ import {
3
+ runGit
4
+ } from "./io.js";
5
+
6
+ function parseGitWorktreeList(output = "") {
7
+ return String(output || "")
8
+ .split(/\r?\n/u)
9
+ .filter((line) => line.startsWith("worktree "))
10
+ .map((line) => path.resolve(line.slice("worktree ".length).trim()))
11
+ .filter(Boolean);
12
+ }
13
+
14
+ async function hasWorktree(paths = {}) {
15
+ if (!paths.targetRoot || !paths.worktree) {
16
+ return false;
17
+ }
18
+ const result = await runGit(paths.targetRoot, ["worktree", "list", "--porcelain"], {
19
+ timeout: 10000
20
+ });
21
+ if (!result.ok) {
22
+ return false;
23
+ }
24
+ const expectedWorktree = path.resolve(paths.worktree);
25
+ return parseGitWorktreeList(result.stdout).includes(expectedWorktree);
26
+ }
27
+
28
+ export {
29
+ hasWorktree,
30
+ parseGitWorktreeList
31
+ };