@jskit-ai/jskit-cli 0.2.80 → 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.
@@ -18,6 +18,9 @@ import {
18
18
  import {
19
19
  pathsForExistingSession
20
20
  } from "./paths.js";
21
+ import {
22
+ hasWorktree
23
+ } from "./worktrees.js";
21
24
 
22
25
  function createError({
23
26
  code,
@@ -43,15 +46,83 @@ function createPrecondition({
43
46
  });
44
47
  }
45
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
+
46
119
  async function readCompletedSteps(sessionRoot) {
47
120
  const stepsRoot = path.join(sessionRoot, "steps");
48
121
  try {
49
122
  const entries = await readdir(stepsRoot, { withFileTypes: true });
50
- return entries
123
+ return normalizeKnownStepIds(entries
51
124
  .filter((entry) => entry.isFile())
52
- .map((entry) => entry.name)
53
- .filter((entry) => STEP_IDS.includes(entry))
54
- .sort((left, right) => STEP_IDS.indexOf(left) - STEP_IDS.indexOf(right));
125
+ .map((entry) => entry.name));
55
126
  } catch {
56
127
  return [];
57
128
  }
@@ -61,12 +132,32 @@ async function readReceiptSteps(paths) {
61
132
  const stepsRoot = path.join(paths.sessionRoot, "steps");
62
133
  try {
63
134
  const entries = await readdir(stepsRoot, { withFileTypes: true });
64
- const stepNames = entries
135
+ const knownStepRows = new Map();
136
+ const unknownStepRows = [];
137
+ entries
65
138
  .filter((entry) => entry.isFile())
66
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]
67
158
  .sort((left, right) => {
68
- const leftIndex = STEP_IDS.indexOf(left);
69
- const rightIndex = STEP_IDS.indexOf(right);
159
+ const leftIndex = stepIndex(left.stepId);
160
+ const rightIndex = stepIndex(right.stepId);
70
161
  if (leftIndex >= 0 && rightIndex >= 0) {
71
162
  return leftIndex - rightIndex;
72
163
  }
@@ -76,12 +167,12 @@ async function readReceiptSteps(paths) {
76
167
  if (rightIndex >= 0) {
77
168
  return 1;
78
169
  }
79
- return left.localeCompare(right);
170
+ return left.stepId.localeCompare(right.stepId);
80
171
  });
81
172
 
82
- return Promise.all(stepNames.map(async (stepId) => ({
173
+ return Promise.all(stepRows.map(async ({ receiptName, stepId }) => ({
83
174
  label: STEP_LABEL_BY_ID[stepId] || stepId,
84
- receipt: (await readTextIfExists(path.join(stepsRoot, stepId))).trim(),
175
+ receipt: (await readTextIfExists(path.join(stepsRoot, receiptName))).trim(),
85
176
  stepId
86
177
  })));
87
178
  } catch {
@@ -113,7 +204,8 @@ function publicStepDefinition(step, index) {
113
204
  index,
114
205
  input: cloneContractValue(step.input),
115
206
  kind: step.kind,
116
- label: step.label
207
+ label: step.label,
208
+ utilityActions: cloneContractValue(step.utilityActions || [])
117
209
  };
118
210
  }
119
211
 
@@ -132,7 +224,8 @@ function buildCurrentStepAction(stepId) {
132
224
  index: STEP_IDS.indexOf(step.id),
133
225
  input: cloneContractValue(step.input),
134
226
  kind: step.kind,
135
- stepId: step.id
227
+ stepId: step.id,
228
+ utilityActions: cloneContractValue(step.utilityActions || [])
136
229
  };
137
230
  }
138
231
 
@@ -142,28 +235,51 @@ function buildCodexHandoff(stepId) {
142
235
  }
143
236
 
144
237
  async function readSessionArtifacts(paths) {
145
- const [status, currentStep, issueUrl, prUrl, prompt, issueText, codexThreadId] = await Promise.all([
238
+ const [status, rawCurrentStep, issueUrl, prUrl, issueText, issueTitle, planText, codexThreadId] = await Promise.all([
146
239
  readTrimmedFile(path.join(paths.sessionRoot, "status")),
147
240
  readTrimmedFile(path.join(paths.sessionRoot, "current_step")),
148
241
  readTrimmedFile(path.join(paths.sessionRoot, "issue_url")),
149
242
  readTrimmedFile(path.join(paths.sessionRoot, "pr_url")),
150
- readTextIfExists(path.join(paths.sessionRoot, "prompt.md")),
151
243
  readTextIfExists(path.join(paths.sessionRoot, "issue.md")),
244
+ readTrimmedFile(path.join(paths.sessionRoot, "issue_title")),
245
+ readTextIfExists(path.join(paths.sessionRoot, "plan.md")),
152
246
  readTrimmedFile(path.join(paths.sessionRoot, "codex_thread_id"))
153
247
  ]);
154
- const completedSteps = await readCompletedSteps(paths.sessionRoot);
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
+ }
155
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);
156
269
 
157
270
  return {
158
271
  codexThreadId,
159
272
  completedSteps,
160
- currentStep: currentStep || nextStep,
273
+ currentStep: effectiveCurrentStep,
274
+ issueTitle,
161
275
  issueText: issueText.trim(),
162
276
  issueUrl,
163
277
  nextStep,
164
278
  prUrl,
279
+ planText: planText.trim(),
165
280
  prompt: prompt.trim(),
166
- status: status || SESSION_STATUS.PENDING
281
+ status: status || SESSION_STATUS.PENDING,
282
+ worktreeReady
167
283
  };
168
284
  }
169
285
 
@@ -176,6 +292,7 @@ function buildNextCommand(sessionId, stepId) {
176
292
  }
177
293
 
178
294
  async function buildSessionResponse(paths, {
295
+ codex = undefined,
179
296
  ok = true,
180
297
  errors = [],
181
298
  preconditions = [],
@@ -186,7 +303,9 @@ async function buildSessionResponse(paths, {
186
303
  const artifacts = responsePaths.sessionRoot ? await readSessionArtifacts(responsePaths) : {};
187
304
  const resolvedStatus = status || artifacts.status || (ok ? SESSION_STATUS.PENDING : SESSION_STATUS.BLOCKED);
188
305
  const currentStep = artifacts.currentStep || artifacts.nextStep || "";
189
- const responsePrompt = typeof prompt === "string" ? prompt : artifacts.prompt || "";
306
+ const responsePrompt = typeof prompt === "string"
307
+ ? prompt
308
+ : stepCanExposeStoredPrompt(currentStep) ? artifacts.prompt || "" : "";
190
309
 
191
310
  return {
192
311
  ok: ok === true,
@@ -196,16 +315,20 @@ async function buildSessionResponse(paths, {
196
315
  completedSteps: artifacts.completedSteps || [],
197
316
  stepDefinitions: buildStepDefinitions(),
198
317
  currentStepAction: buildCurrentStepAction(currentStep),
199
- codex: buildCodexHandoff(currentStep),
318
+ codex: codex === undefined ? buildCodexHandoff(currentStep) : cloneContractValue(codex),
200
319
  prompt: responsePrompt,
201
320
  nextCommand: buildNextCommand(paths.sessionId || "", currentStep),
202
321
  issueUrl: artifacts.issueUrl || "",
322
+ issueTitle: artifacts.issueTitle || "",
323
+ issueText: artifacts.issueText || "",
324
+ planText: artifacts.planText || "",
203
325
  prUrl: artifacts.prUrl || "",
204
326
  preconditions,
205
327
  errors,
206
328
  archive: responsePaths.archive || (resolvedStatus === SESSION_STATUS.FINISHED ? "completed" : resolvedStatus === SESSION_STATUS.ABANDONED ? "abandoned" : "active"),
207
329
  sessionRoot: responsePaths.sessionRoot || "",
208
330
  worktree: paths.worktree || "",
331
+ worktreeReady: artifacts.worktreeReady === true,
209
332
  branch: paths.branch || "",
210
333
  codexThreadId: artifacts.codexThreadId || ""
211
334
  };
@@ -243,6 +366,9 @@ function buildSessionErrorResponse({
243
366
  codex: null,
244
367
  prompt: "",
245
368
  nextCommand: "",
369
+ issueTitle: "",
370
+ issueText: "",
371
+ planText: "",
246
372
  issueUrl: "",
247
373
  prUrl: "",
248
374
  preconditions,
@@ -250,6 +376,7 @@ function buildSessionErrorResponse({
250
376
  archive: "",
251
377
  sessionRoot: "",
252
378
  worktree: "",
379
+ worktreeReady: false,
253
380
  branch: "",
254
381
  codexThreadId: "",
255
382
  targetRoot: normalizedTargetRoot
@@ -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
+ };