@jskit-ai/jskit-cli 0.2.80 → 0.2.82
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 +6 -4
- package/src/server/appBlueprint.js +1 -1
- package/src/server/commandHandlers/helperMap.js +104 -0
- package/src/server/commandHandlers/session.js +179 -4
- package/src/server/commandHandlers/show.js +169 -34
- package/src/server/core/argParser.js +20 -0
- package/src/server/core/commandCatalog.js +70 -7
- package/src/server/core/createCommandHandlers.js +4 -1
- package/src/server/helperMap.js +463 -0
- package/src/server/helperMapPaths.js +7 -0
- package/src/server/sessionRuntime/appReadiness.js +55 -0
- package/src/server/sessionRuntime/constants.js +298 -135
- package/src/server/sessionRuntime/paths.js +2 -4
- package/src/server/sessionRuntime/preconditions.js +393 -26
- package/src/server/sessionRuntime/promptRenderer.js +15 -2
- package/src/server/sessionRuntime/prompts/app_blueprint.md +26 -2
- package/src/server/sessionRuntime/prompts/automated_checks.md +42 -0
- package/src/server/sessionRuntime/prompts/deep_ui_check.md +53 -0
- package/src/server/sessionRuntime/prompts/doctor_failure.md +21 -1
- package/src/server/sessionRuntime/prompts/execute_plan.md +61 -0
- package/src/server/sessionRuntime/prompts/final_comment.md +3 -1
- package/src/server/sessionRuntime/prompts/issue_details.md +52 -0
- package/src/server/sessionRuntime/prompts/new_issue.md +34 -3
- package/src/server/sessionRuntime/prompts/plan_issue.md +81 -0
- package/src/server/sessionRuntime/prompts/pr_failure.md +14 -1
- package/src/server/sessionRuntime/prompts/review_changes.md +77 -15
- package/src/server/sessionRuntime/prompts/update_blueprint.md +36 -0
- package/src/server/sessionRuntime/prompts/user_check.md +22 -4
- package/src/server/sessionRuntime/responses.js +877 -30
- package/src/server/sessionRuntime/worktrees.js +31 -0
- package/src/server/sessionRuntime.js +2070 -244
- package/src/server/sessionRuntime/prompts/implement_issue.md +0 -25
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { mkdir, readdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import {
|
|
4
|
+
CYCLE_STEP_IDS,
|
|
5
|
+
JSKIT_CLI_SHELL_COMMAND,
|
|
6
|
+
REVIEW_PASS_LIMIT,
|
|
7
|
+
SESSION_WORKFLOW_VERSION,
|
|
4
8
|
SESSION_STATUS,
|
|
5
9
|
STEP_DEFINITION_BY_ID,
|
|
6
10
|
STEP_DEFINITIONS,
|
|
@@ -12,12 +16,19 @@ import {
|
|
|
12
16
|
normalizeText,
|
|
13
17
|
readTextIfExists,
|
|
14
18
|
readTrimmedFile,
|
|
19
|
+
runGitInWorktree,
|
|
15
20
|
timestampForReceipt,
|
|
16
21
|
writeTextFile
|
|
17
22
|
} from "./io.js";
|
|
18
23
|
import {
|
|
19
24
|
pathsForExistingSession
|
|
20
25
|
} from "./paths.js";
|
|
26
|
+
import {
|
|
27
|
+
hasWorktree
|
|
28
|
+
} from "./worktrees.js";
|
|
29
|
+
import {
|
|
30
|
+
inspectReadyJskitAppRoot
|
|
31
|
+
} from "./appReadiness.js";
|
|
21
32
|
|
|
22
33
|
function createError({
|
|
23
34
|
code,
|
|
@@ -43,30 +54,384 @@ function createPrecondition({
|
|
|
43
54
|
});
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
function normalizeStepId(stepId) {
|
|
58
|
+
return normalizeText(stepId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stepIndex(stepId) {
|
|
62
|
+
return STEP_IDS.indexOf(normalizeStepId(stepId));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeKnownStepIds(stepIds = []) {
|
|
66
|
+
return Array.from(
|
|
67
|
+
new Set(
|
|
68
|
+
stepIds
|
|
69
|
+
.map((stepId) => normalizeText(stepId))
|
|
70
|
+
.filter((stepId) => STEP_IDS.includes(stepId))
|
|
71
|
+
)
|
|
72
|
+
).sort((left, right) => STEP_IDS.indexOf(left) - STEP_IDS.indexOf(right));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function stepCanExposeStoredPrompt(stepId) {
|
|
76
|
+
const step = STEP_DEFINITION_BY_ID[normalizeStepId(stepId)];
|
|
77
|
+
return Boolean(step?.codex || step?.kind === "codex_prompt" || step?.kind === "human_input");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const DEFAULT_ACTIVE_CYCLE = "001";
|
|
81
|
+
const DEFAULT_REVIEW_PASS = "001";
|
|
82
|
+
|
|
83
|
+
function normalizeCycleNumber(value = "") {
|
|
84
|
+
const normalized = normalizeText(value).replace(/^cycle_/u, "");
|
|
85
|
+
if (!/^\d+$/u.test(normalized)) {
|
|
86
|
+
return DEFAULT_ACTIVE_CYCLE;
|
|
87
|
+
}
|
|
88
|
+
return String(Number.parseInt(normalized, 10)).padStart(3, "0");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function cycleDirectoryName(cycle = DEFAULT_ACTIVE_CYCLE) {
|
|
92
|
+
return `cycle_${normalizeCycleNumber(cycle)}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isCycleStepId(stepId = "") {
|
|
96
|
+
return CYCLE_STEP_IDS.includes(normalizeStepId(stepId));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readWorkflowVersion(paths) {
|
|
100
|
+
return readTrimmedFile(path.join(paths.sessionRoot, "workflow_version"));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function readActiveCycle(paths) {
|
|
104
|
+
const cycle = await readTrimmedFile(path.join(paths.sessionRoot, "active_cycle"));
|
|
105
|
+
return normalizeCycleNumber(cycle || DEFAULT_ACTIVE_CYCLE);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function writeActiveCycle(paths, cycle) {
|
|
109
|
+
await writeTextFile(path.join(paths.sessionRoot, "active_cycle"), `${normalizeCycleNumber(cycle)}\n`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function cycleStepsRoot(paths, cycle) {
|
|
113
|
+
return path.join(paths.sessionRoot, "steps", cycleDirectoryName(cycle));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function cycleRoot(paths, cycle) {
|
|
117
|
+
return path.join(paths.sessionRoot, "cycles", cycleDirectoryName(cycle));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function cyclePlanPath(paths, cycle) {
|
|
121
|
+
return path.join(cycleRoot(paths, cycle), "plan.md");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function cyclePlanPromptFileName(cycle) {
|
|
125
|
+
return `cycle_${normalizeCycleNumber(cycle)}_plan_request.md`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function cyclePlanExecutionPromptFileName(cycle) {
|
|
129
|
+
return `cycle_${normalizeCycleNumber(cycle)}_plan_execution.md`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeReviewPassNumber(value = "") {
|
|
133
|
+
const normalized = normalizeText(value).replace(/^pass_/u, "");
|
|
134
|
+
if (!/^\d+$/u.test(normalized)) {
|
|
135
|
+
return DEFAULT_REVIEW_PASS;
|
|
136
|
+
}
|
|
137
|
+
return String(Number.parseInt(normalized, 10)).padStart(3, "0");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function reviewPassDirectoryName(pass = DEFAULT_REVIEW_PASS) {
|
|
141
|
+
return `pass_${normalizeReviewPassNumber(pass)}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function reviewPassRoot(paths, pass) {
|
|
145
|
+
return path.join(paths.sessionRoot, "review_passes", reviewPassDirectoryName(pass));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function parseJsonFileIfExists(filePath) {
|
|
149
|
+
const source = await readTextIfExists(filePath);
|
|
150
|
+
if (!source) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(source);
|
|
155
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function readReviewPassNumbers(paths) {
|
|
162
|
+
try {
|
|
163
|
+
const entries = await readdir(path.join(paths.sessionRoot, "review_passes"), { withFileTypes: true });
|
|
164
|
+
return entries
|
|
165
|
+
.filter((entry) => entry.isDirectory() && /^pass_\d+$/u.test(entry.name))
|
|
166
|
+
.map((entry) => normalizeReviewPassNumber(entry.name))
|
|
167
|
+
.sort((left, right) => Number.parseInt(left, 10) - Number.parseInt(right, 10));
|
|
168
|
+
} catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function readReviewPassInfo(paths, pass) {
|
|
174
|
+
const normalizedPass = normalizeReviewPassNumber(pass);
|
|
175
|
+
const root = reviewPassRoot(paths, normalizedPass);
|
|
176
|
+
const [prompt, accepted] = await Promise.all([
|
|
177
|
+
parseJsonFileIfExists(path.join(root, "prompt.json")),
|
|
178
|
+
parseJsonFileIfExists(path.join(root, "accepted.json"))
|
|
179
|
+
]);
|
|
180
|
+
const status = accepted?.status || prompt?.status || "unknown";
|
|
181
|
+
const changedFiles = Array.isArray(accepted?.changedFiles) ? accepted.changedFiles : [];
|
|
182
|
+
return {
|
|
183
|
+
pass: normalizedPass,
|
|
184
|
+
label: reviewPassDirectoryName(normalizedPass),
|
|
185
|
+
status,
|
|
186
|
+
promptPath: prompt?.promptPath || path.join(root, "prompt.md"),
|
|
187
|
+
acceptedAt: accepted?.acceptedAt || "",
|
|
188
|
+
changedFiles,
|
|
189
|
+
commit: "",
|
|
190
|
+
committedAt: "",
|
|
191
|
+
findingsRemaining: accepted?.findingsRemaining === true,
|
|
192
|
+
maxPasses: REVIEW_PASS_LIMIT
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function readReviewPasses(paths) {
|
|
197
|
+
const passes = await readReviewPassNumbers(paths);
|
|
198
|
+
return Promise.all(passes.map((pass) => readReviewPassInfo(paths, pass)));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const PROMPT_ARTIFACT_BY_STEP_ID = Object.freeze({
|
|
202
|
+
issue_drafted: "issue_draft.md",
|
|
203
|
+
issue_details_gathered: "issue_details.md",
|
|
204
|
+
deep_ui_check_run: "deep_ui_check_run.md",
|
|
205
|
+
automated_checks_run: "automated_checks_run.md",
|
|
206
|
+
blueprint_updated: "update_blueprint.md",
|
|
207
|
+
user_check_completed: "user_check.md"
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
async function promptArtifactForStep(paths, stepId) {
|
|
211
|
+
const normalizedStepId = normalizeStepId(stepId);
|
|
212
|
+
if (normalizedStepId === "plan_made") {
|
|
213
|
+
return cyclePlanPromptFileName(await readActiveCycle(paths));
|
|
214
|
+
}
|
|
215
|
+
if (normalizedStepId === "plan_executed") {
|
|
216
|
+
return cyclePlanExecutionPromptFileName(await readActiveCycle(paths));
|
|
217
|
+
}
|
|
218
|
+
return PROMPT_ARTIFACT_BY_STEP_ID[normalizedStepId] || "";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function readPromptForStep(paths, stepId) {
|
|
222
|
+
if (!stepCanExposeStoredPrompt(stepId)) {
|
|
223
|
+
return "";
|
|
224
|
+
}
|
|
225
|
+
const promptArtifact = await promptArtifactForStep(paths, stepId);
|
|
226
|
+
if (promptArtifact) {
|
|
227
|
+
const prompt = await readTextIfExists(path.join(paths.sessionRoot, "prompts", promptArtifact));
|
|
228
|
+
if (prompt) {
|
|
229
|
+
return prompt;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return readTextIfExists(path.join(paths.sessionRoot, "prompt.md"));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function readStepFileNames(stepsRoot) {
|
|
48
236
|
try {
|
|
49
237
|
const entries = await readdir(stepsRoot, { withFileTypes: true });
|
|
50
238
|
return entries
|
|
51
239
|
.filter((entry) => entry.isFile())
|
|
52
|
-
.map((entry) => entry.name)
|
|
53
|
-
|
|
54
|
-
|
|
240
|
+
.map((entry) => entry.name);
|
|
241
|
+
} catch {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function readCompletedSteps(paths) {
|
|
247
|
+
const stepsRoot = path.join(paths.sessionRoot, "steps");
|
|
248
|
+
const activeCycle = await readActiveCycle(paths);
|
|
249
|
+
const globalStepIds = normalizeKnownStepIds(
|
|
250
|
+
(await readStepFileNames(stepsRoot)).filter((stepId) => !isCycleStepId(stepId))
|
|
251
|
+
);
|
|
252
|
+
const cycleStepIds = normalizeKnownStepIds(await readStepFileNames(cycleStepsRoot(paths, activeCycle)));
|
|
253
|
+
return applyReviewPassCompletionOverlay(paths, normalizeKnownStepIds([...globalStepIds, ...cycleStepIds]));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const REVIEW_STEP_IDS = Object.freeze([
|
|
257
|
+
"review_prompt_rendered",
|
|
258
|
+
"review_changes_accepted"
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
async function applyReviewPassCompletionOverlay(paths, completedSteps = []) {
|
|
262
|
+
const completed = new Set(completedSteps);
|
|
263
|
+
if (!REVIEW_STEP_IDS.some((stepId) => completed.has(stepId))) {
|
|
264
|
+
return normalizeKnownStepIds([...completed]);
|
|
265
|
+
}
|
|
266
|
+
const reviewPasses = await readReviewPasses(paths);
|
|
267
|
+
const latestPass = reviewPasses.at(-1);
|
|
268
|
+
if (!latestPass) {
|
|
269
|
+
REVIEW_STEP_IDS.forEach((stepId) => completed.delete(stepId));
|
|
270
|
+
return normalizeKnownStepIds([...completed]);
|
|
271
|
+
}
|
|
272
|
+
const latestPassAccepted = latestPass.status === "accepted" || latestPass.status === "no_changes";
|
|
273
|
+
const anotherPassRequired = latestPassAccepted && latestPass.findingsRemaining === true;
|
|
274
|
+
if (anotherPassRequired) {
|
|
275
|
+
REVIEW_STEP_IDS.forEach((stepId) => completed.delete(stepId));
|
|
276
|
+
return normalizeKnownStepIds([...completed]);
|
|
277
|
+
}
|
|
278
|
+
if (latestPass.status === "prompted") {
|
|
279
|
+
completed.add("review_prompt_rendered");
|
|
280
|
+
completed.delete("review_changes_accepted");
|
|
281
|
+
return normalizeKnownStepIds([...completed]);
|
|
282
|
+
}
|
|
283
|
+
if (latestPass.status === "accepted" || latestPass.status === "no_changes") {
|
|
284
|
+
REVIEW_STEP_IDS.forEach((stepId) => completed.add(stepId));
|
|
285
|
+
}
|
|
286
|
+
return normalizeKnownStepIds([...completed]);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function readCycleInfo(paths, cycle) {
|
|
290
|
+
const normalizedCycle = normalizeCycleNumber(cycle);
|
|
291
|
+
const root = cycleRoot(paths, normalizedCycle);
|
|
292
|
+
const userCheckPassed = await readTextIfExists(path.join(cycleStepsRoot(paths, normalizedCycle), "user_check_completed"));
|
|
293
|
+
const userCheckFailed = await readTextIfExists(path.join(cycleStepsRoot(paths, normalizedCycle), "user_check_failed"));
|
|
294
|
+
const reworkRequestPath = path.join(root, "rework_request.md");
|
|
295
|
+
const reworkRequest = await readTextIfExists(reworkRequestPath);
|
|
296
|
+
return {
|
|
297
|
+
cycle: normalizedCycle,
|
|
298
|
+
label: cycleDirectoryName(normalizedCycle),
|
|
299
|
+
reworkRequest: reworkRequest.trim(),
|
|
300
|
+
reworkRequestPath: reworkRequest ? reworkRequestPath : "",
|
|
301
|
+
status: userCheckPassed ? "passed" : userCheckFailed ? "failed" : "active",
|
|
302
|
+
userCheckResult: userCheckPassed ? "passed" : userCheckFailed ? "failed" : "",
|
|
303
|
+
userCheckReceipt: (userCheckPassed || userCheckFailed).trim()
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function readCycles(paths, activeCycle) {
|
|
308
|
+
const cycles = new Set([normalizeCycleNumber(activeCycle || DEFAULT_ACTIVE_CYCLE)]);
|
|
309
|
+
for (const root of [path.join(paths.sessionRoot, "steps"), path.join(paths.sessionRoot, "cycles")]) {
|
|
310
|
+
try {
|
|
311
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
312
|
+
for (const entry of entries) {
|
|
313
|
+
if (entry.isDirectory() && /^cycle_\d+$/u.test(entry.name)) {
|
|
314
|
+
cycles.add(normalizeCycleNumber(entry.name));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
// Missing cycle directories are normal for older sessions.
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return Promise.all([...cycles]
|
|
322
|
+
.sort((left, right) => Number.parseInt(left, 10) - Number.parseInt(right, 10))
|
|
323
|
+
.map((cycle) => readCycleInfo(paths, cycle)));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function readStructuredChecks(paths) {
|
|
327
|
+
const checksRoot = path.join(paths.sessionRoot, "checks");
|
|
328
|
+
try {
|
|
329
|
+
const entries = await readdir(checksRoot, { withFileTypes: true });
|
|
330
|
+
const checks = [];
|
|
331
|
+
for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
332
|
+
const source = await readTextIfExists(path.join(checksRoot, entry.name));
|
|
333
|
+
try {
|
|
334
|
+
const parsed = JSON.parse(source);
|
|
335
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
336
|
+
checks.push(parsed);
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
// Ignore malformed check metadata; the raw log remains on disk.
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return checks;
|
|
343
|
+
} catch {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function readStructuredUiChecks(paths) {
|
|
349
|
+
const uiChecksRoot = path.join(paths.sessionRoot, "ui_checks");
|
|
350
|
+
try {
|
|
351
|
+
const entries = await readdir(uiChecksRoot, { withFileTypes: true });
|
|
352
|
+
const checks = [];
|
|
353
|
+
for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
354
|
+
const source = await readTextIfExists(path.join(uiChecksRoot, entry.name));
|
|
355
|
+
try {
|
|
356
|
+
const parsed = JSON.parse(source);
|
|
357
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
358
|
+
checks.push(parsed);
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
// Ignore malformed UI check metadata; the raw prompt/log remains on disk.
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return checks;
|
|
55
365
|
} catch {
|
|
56
366
|
return [];
|
|
57
367
|
}
|
|
58
368
|
}
|
|
59
369
|
|
|
370
|
+
async function readWorktreeStatus(paths, worktreeReady) {
|
|
371
|
+
if (!worktreeReady) {
|
|
372
|
+
return {
|
|
373
|
+
changedFiles: [],
|
|
374
|
+
dirty: false,
|
|
375
|
+
ok: true,
|
|
376
|
+
status: "missing",
|
|
377
|
+
statusText: ""
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const result = await runGitInWorktree(paths.worktree, ["status", "--porcelain=v1"], {
|
|
381
|
+
timeout: 15000
|
|
382
|
+
});
|
|
383
|
+
if (!result.ok) {
|
|
384
|
+
return {
|
|
385
|
+
changedFiles: [],
|
|
386
|
+
dirty: false,
|
|
387
|
+
ok: false,
|
|
388
|
+
status: "unknown",
|
|
389
|
+
statusText: result.output
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const changedFiles = result.stdout
|
|
393
|
+
.split(/\r?\n/u)
|
|
394
|
+
.map((line) => line.trim())
|
|
395
|
+
.filter(Boolean);
|
|
396
|
+
return {
|
|
397
|
+
changedFiles,
|
|
398
|
+
dirty: changedFiles.length > 0,
|
|
399
|
+
ok: true,
|
|
400
|
+
status: changedFiles.length > 0 ? "dirty" : "clean",
|
|
401
|
+
statusText: result.stdout
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
60
405
|
async function readReceiptSteps(paths) {
|
|
61
406
|
const stepsRoot = path.join(paths.sessionRoot, "steps");
|
|
62
407
|
try {
|
|
63
408
|
const entries = await readdir(stepsRoot, { withFileTypes: true });
|
|
64
|
-
const
|
|
409
|
+
const knownStepRows = new Map();
|
|
410
|
+
const unknownStepRows = [];
|
|
411
|
+
entries
|
|
65
412
|
.filter((entry) => entry.isFile())
|
|
66
413
|
.map((entry) => entry.name)
|
|
414
|
+
.forEach((receiptName) => {
|
|
415
|
+
const stepId = normalizeStepId(receiptName);
|
|
416
|
+
if (STEP_IDS.includes(stepId)) {
|
|
417
|
+
if (!knownStepRows.has(stepId) || receiptName === stepId) {
|
|
418
|
+
knownStepRows.set(stepId, {
|
|
419
|
+
receiptName,
|
|
420
|
+
stepId
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
unknownStepRows.push({
|
|
426
|
+
receiptName,
|
|
427
|
+
stepId
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const stepRows = [...knownStepRows.values(), ...unknownStepRows]
|
|
67
432
|
.sort((left, right) => {
|
|
68
|
-
const leftIndex =
|
|
69
|
-
const rightIndex =
|
|
433
|
+
const leftIndex = stepIndex(left.stepId);
|
|
434
|
+
const rightIndex = stepIndex(right.stepId);
|
|
70
435
|
if (leftIndex >= 0 && rightIndex >= 0) {
|
|
71
436
|
return leftIndex - rightIndex;
|
|
72
437
|
}
|
|
@@ -76,14 +441,37 @@ async function readReceiptSteps(paths) {
|
|
|
76
441
|
if (rightIndex >= 0) {
|
|
77
442
|
return 1;
|
|
78
443
|
}
|
|
79
|
-
return left.localeCompare(right);
|
|
444
|
+
return left.stepId.localeCompare(right.stepId);
|
|
80
445
|
});
|
|
81
446
|
|
|
82
|
-
|
|
447
|
+
const globalReceipts = await Promise.all(stepRows.map(async ({ receiptName, stepId }) => ({
|
|
448
|
+
cycle: "",
|
|
83
449
|
label: STEP_LABEL_BY_ID[stepId] || stepId,
|
|
84
|
-
receipt: (await readTextIfExists(path.join(stepsRoot,
|
|
450
|
+
receipt: (await readTextIfExists(path.join(stepsRoot, receiptName))).trim(),
|
|
85
451
|
stepId
|
|
86
452
|
})));
|
|
453
|
+
|
|
454
|
+
const cycleReceipts = [];
|
|
455
|
+
const cycleDirectories = entries
|
|
456
|
+
.filter((entry) => entry.isDirectory() && /^cycle_\d+$/u.test(entry.name))
|
|
457
|
+
.map((entry) => entry.name)
|
|
458
|
+
.sort();
|
|
459
|
+
for (const cycleDirectory of cycleDirectories) {
|
|
460
|
+
const cycle = normalizeCycleNumber(cycleDirectory);
|
|
461
|
+
const cycleRootPath = path.join(stepsRoot, cycleDirectory);
|
|
462
|
+
const cycleStepIds = await readStepFileNames(cycleRootPath);
|
|
463
|
+
for (const receiptName of cycleStepIds) {
|
|
464
|
+
const stepId = normalizeStepId(receiptName);
|
|
465
|
+
cycleReceipts.push({
|
|
466
|
+
cycle,
|
|
467
|
+
label: STEP_LABEL_BY_ID[stepId] || stepId,
|
|
468
|
+
receipt: (await readTextIfExists(path.join(cycleRootPath, receiptName))).trim(),
|
|
469
|
+
stepId
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return [...globalReceipts, ...cycleReceipts];
|
|
87
475
|
} catch {
|
|
88
476
|
return [];
|
|
89
477
|
}
|
|
@@ -106,14 +494,36 @@ function cloneContractValue(value) {
|
|
|
106
494
|
);
|
|
107
495
|
}
|
|
108
496
|
|
|
497
|
+
function stepRepeatabilityContract(stepId) {
|
|
498
|
+
if (!CYCLE_STEP_IDS.includes(normalizeStepId(stepId))) {
|
|
499
|
+
return {
|
|
500
|
+
repeatable: false,
|
|
501
|
+
repeatableGroupId: "",
|
|
502
|
+
repeatableGroupLabel: "",
|
|
503
|
+
repeatableLabel: ""
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
repeatable: true,
|
|
508
|
+
repeatableGroupId: "rework_cycle",
|
|
509
|
+
repeatableGroupLabel: "Rework cycle",
|
|
510
|
+
repeatableLabel: "Cycle step"
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
109
514
|
function publicStepDefinition(step, index) {
|
|
110
515
|
return {
|
|
111
516
|
description: step.description,
|
|
517
|
+
displayGroupId: step.displayGroupId || "",
|
|
518
|
+
displayGroupLabel: step.displayGroupLabel || "",
|
|
112
519
|
id: step.id,
|
|
113
520
|
index,
|
|
114
521
|
input: cloneContractValue(step.input),
|
|
115
522
|
kind: step.kind,
|
|
116
|
-
label: step.label
|
|
523
|
+
label: step.label,
|
|
524
|
+
...stepRepeatabilityContract(step.id),
|
|
525
|
+
requiresExplicitRun: step.requiresExplicitRun === true,
|
|
526
|
+
utilityActions: cloneContractValue(step.utilityActions || [])
|
|
117
527
|
};
|
|
118
528
|
}
|
|
119
529
|
|
|
@@ -121,49 +531,332 @@ function buildStepDefinitions() {
|
|
|
121
531
|
return STEP_DEFINITIONS.map((step, index) => publicStepDefinition(step, index));
|
|
122
532
|
}
|
|
123
533
|
|
|
124
|
-
function
|
|
534
|
+
function stepIsRetryableWhenBlocked(stepId) {
|
|
535
|
+
return [
|
|
536
|
+
"automated_checks_run",
|
|
537
|
+
"deep_ui_check_run",
|
|
538
|
+
"doctor_run"
|
|
539
|
+
].includes(normalizeStepId(stepId));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function stepIsConditional(stepId) {
|
|
543
|
+
return [
|
|
544
|
+
"deep_ui_check_run"
|
|
545
|
+
].includes(normalizeStepId(stepId));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function activeCycleInfoFromArtifacts(artifacts = {}) {
|
|
549
|
+
const activeCycle = normalizeCycleNumber(artifacts.activeCycle || "");
|
|
550
|
+
return (artifacts.cycles || []).find((cycle) => cycle?.cycle === activeCycle) || null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function uiCheckPromptedForStep(artifacts = {}, stepId = "") {
|
|
554
|
+
const normalizedStepId = normalizeStepId(stepId);
|
|
555
|
+
return (artifacts.uiChecks || []).some((entry) => {
|
|
556
|
+
return normalizeStepId(entry?.stepId || "") === normalizedStepId &&
|
|
557
|
+
normalizeText(entry?.status || "") === "prompted";
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function skipReasonForStep(stepId, artifacts = {}) {
|
|
562
|
+
const normalizedStepId = normalizeStepId(stepId);
|
|
563
|
+
if (normalizedStepId === "deep_ui_check_run" && artifacts.uiImpact === "none") {
|
|
564
|
+
return "uiImpact is none.";
|
|
565
|
+
}
|
|
566
|
+
return "";
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function buildCurrentStepAction(stepId, artifacts = {}) {
|
|
125
570
|
const step = STEP_DEFINITION_BY_ID[stepId];
|
|
126
571
|
if (!step) {
|
|
127
572
|
return null;
|
|
128
573
|
}
|
|
574
|
+
const activeCycleInfo = activeCycleInfoFromArtifacts(artifacts);
|
|
575
|
+
const planExecutionPrompted = artifacts.planExecution?.prompted === true;
|
|
576
|
+
const planExecutionSubmitted = artifacts.planExecution?.submitted === true;
|
|
577
|
+
const planReworkMode = step.id === "plan_made" && normalizeCycleNumber(artifacts.activeCycle || "") !== "001";
|
|
578
|
+
const deepUiCheckPrompted = step.id === "deep_ui_check_run" && uiCheckPromptedForStep(artifacts, "deep_ui_check_run");
|
|
579
|
+
const hasActiveReworkRequest = Boolean(activeCycleInfo?.reworkRequestPath);
|
|
580
|
+
const promptPhaseButtonLabel = step.kind === "codex_output" &&
|
|
581
|
+
step.codex?.mode === "inject_prompt" &&
|
|
582
|
+
!artifacts.prompt
|
|
583
|
+
? step.codex.promptActionLabel || ""
|
|
584
|
+
: "";
|
|
585
|
+
const buttonLabel = promptPhaseButtonLabel || step.buttonLabel;
|
|
586
|
+
const alternateActions = [];
|
|
587
|
+
if (step.id === "user_check_completed") {
|
|
588
|
+
alternateActions.push({
|
|
589
|
+
id: "return_to_plan_made",
|
|
590
|
+
input: {
|
|
591
|
+
formatHint: "markdown",
|
|
592
|
+
label: "What needs to be reworked?",
|
|
593
|
+
multiline: true,
|
|
594
|
+
name: "reworkNotes",
|
|
595
|
+
required: true,
|
|
596
|
+
type: "text"
|
|
597
|
+
},
|
|
598
|
+
label: "Return to Plan made",
|
|
599
|
+
presentation: "exclusive",
|
|
600
|
+
requiredErrorCode: "user_check_failed",
|
|
601
|
+
submitOptions: {
|
|
602
|
+
userCheck: "failed"
|
|
603
|
+
},
|
|
604
|
+
targetStep: "plan_made"
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
if (step.id === "review_changes_accepted") {
|
|
608
|
+
alternateActions.push({
|
|
609
|
+
id: "request_another_review_pass",
|
|
610
|
+
input: {
|
|
611
|
+
formatHint: "markdown",
|
|
612
|
+
label: "What important findings remain?",
|
|
613
|
+
multiline: true,
|
|
614
|
+
name: "reviewFindings",
|
|
615
|
+
required: true,
|
|
616
|
+
type: "text"
|
|
617
|
+
},
|
|
618
|
+
label: "Run another review pass",
|
|
619
|
+
presentation: "secondary",
|
|
620
|
+
submitOptions: {
|
|
621
|
+
reviewFindingsRemaining: true
|
|
622
|
+
},
|
|
623
|
+
targetStep: "review_prompt_rendered"
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
if (step.id === "pr_finalized") {
|
|
627
|
+
alternateActions.push({
|
|
628
|
+
id: "close_without_merge",
|
|
629
|
+
input: {
|
|
630
|
+
formatHint: "markdown",
|
|
631
|
+
label: "Why is this session finishing without merge?",
|
|
632
|
+
multiline: true,
|
|
633
|
+
name: "closeReason",
|
|
634
|
+
required: true,
|
|
635
|
+
type: "text"
|
|
636
|
+
},
|
|
637
|
+
label: "Finish without merge",
|
|
638
|
+
presentation: "secondary",
|
|
639
|
+
submitOptions: {
|
|
640
|
+
closeWithoutMerge: true
|
|
641
|
+
},
|
|
642
|
+
targetStep: "pr_finalized"
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
const dynamicButtonLabel = (() => {
|
|
646
|
+
if (step.id === "plan_executed" && planExecutionPrompted && !planExecutionSubmitted) {
|
|
647
|
+
return "Go to next step";
|
|
648
|
+
}
|
|
649
|
+
if (step.id === "deep_ui_check_run" && deepUiCheckPrompted) {
|
|
650
|
+
return "Go to next step";
|
|
651
|
+
}
|
|
652
|
+
if (step.id === "automated_checks_run" && artifacts.prompt) {
|
|
653
|
+
return "Go to next step";
|
|
654
|
+
}
|
|
655
|
+
if (step.id === "plan_made" && planReworkMode && !artifacts.prompt && hasActiveReworkRequest) {
|
|
656
|
+
return "Get Codex to create revised plan";
|
|
657
|
+
}
|
|
658
|
+
return buttonLabel;
|
|
659
|
+
})();
|
|
660
|
+
const dynamicDescription = (() => {
|
|
661
|
+
if (step.id === "plan_executed" && planExecutionPrompted && !planExecutionSubmitted) {
|
|
662
|
+
return "Codex has the execution prompt. Studio advances when Codex finishes.";
|
|
663
|
+
}
|
|
664
|
+
if (step.id === "deep_ui_check_run" && deepUiCheckPrompted) {
|
|
665
|
+
return "Codex has the Deep UI check prompt. Studio advances when Codex finishes.";
|
|
666
|
+
}
|
|
667
|
+
if (step.id === "automated_checks_run" && artifacts.prompt) {
|
|
668
|
+
return "Codex has the automated-checks prompt. Studio advances when Codex finishes.";
|
|
669
|
+
}
|
|
670
|
+
if (step.id === "plan_made" && planReworkMode && hasActiveReworkRequest) {
|
|
671
|
+
return "Codex writes a revised implementation plan from the user's rework notes for this cycle.";
|
|
672
|
+
}
|
|
673
|
+
return step.description;
|
|
674
|
+
})();
|
|
675
|
+
const dynamicUtilityActions = (() => {
|
|
676
|
+
return step.utilityActions || [];
|
|
677
|
+
})();
|
|
129
678
|
return {
|
|
130
|
-
|
|
131
|
-
|
|
679
|
+
alternateActions,
|
|
680
|
+
buttonLabel: dynamicButtonLabel,
|
|
681
|
+
description: dynamicDescription,
|
|
132
682
|
index: STEP_IDS.indexOf(step.id),
|
|
133
683
|
input: cloneContractValue(step.input),
|
|
134
684
|
kind: step.kind,
|
|
135
|
-
|
|
685
|
+
label: dynamicButtonLabel,
|
|
686
|
+
...stepRepeatabilityContract(step.id),
|
|
687
|
+
requiredInput: cloneContractValue(step.input),
|
|
688
|
+
requiresExplicitRun: step.requiresExplicitRun === true,
|
|
689
|
+
conditional: stepIsConditional(step.id),
|
|
690
|
+
retryable: artifacts.status === SESSION_STATUS.BLOCKED && stepIsRetryableWhenBlocked(step.id),
|
|
691
|
+
skipReason: skipReasonForStep(step.id, artifacts),
|
|
692
|
+
stepId: step.id,
|
|
693
|
+
utilityActions: cloneContractValue(dynamicUtilityActions)
|
|
136
694
|
};
|
|
137
695
|
}
|
|
138
696
|
|
|
139
|
-
function buildCodexHandoff(stepId) {
|
|
697
|
+
function buildCodexHandoff(stepId, _artifacts = {}) {
|
|
140
698
|
const step = STEP_DEFINITION_BY_ID[stepId];
|
|
141
699
|
return step?.codex ? cloneContractValue(step.codex) : null;
|
|
142
700
|
}
|
|
143
701
|
|
|
144
702
|
async function readSessionArtifacts(paths) {
|
|
145
|
-
const
|
|
703
|
+
const activeCycle = await readActiveCycle(paths);
|
|
704
|
+
const [
|
|
705
|
+
status,
|
|
706
|
+
rawCurrentStep,
|
|
707
|
+
issueUrl,
|
|
708
|
+
prUrl,
|
|
709
|
+
issueText,
|
|
710
|
+
issueTitle,
|
|
711
|
+
planText,
|
|
712
|
+
issueDetails,
|
|
713
|
+
agentDecisions,
|
|
714
|
+
finalReportText,
|
|
715
|
+
githubCommentsText,
|
|
716
|
+
codexThreadId,
|
|
717
|
+
workflowVersion,
|
|
718
|
+
baseBranch,
|
|
719
|
+
baseCommit,
|
|
720
|
+
issueMetadataText,
|
|
721
|
+
planExecutionReceipt,
|
|
722
|
+
prOutcomeText
|
|
723
|
+
] = await Promise.all([
|
|
146
724
|
readTrimmedFile(path.join(paths.sessionRoot, "status")),
|
|
147
725
|
readTrimmedFile(path.join(paths.sessionRoot, "current_step")),
|
|
148
726
|
readTrimmedFile(path.join(paths.sessionRoot, "issue_url")),
|
|
149
727
|
readTrimmedFile(path.join(paths.sessionRoot, "pr_url")),
|
|
150
|
-
readTextIfExists(path.join(paths.sessionRoot, "prompt.md")),
|
|
151
728
|
readTextIfExists(path.join(paths.sessionRoot, "issue.md")),
|
|
152
|
-
readTrimmedFile(path.join(paths.sessionRoot, "
|
|
729
|
+
readTrimmedFile(path.join(paths.sessionRoot, "issue_title")),
|
|
730
|
+
readTextIfExists(cyclePlanPath(paths, activeCycle)),
|
|
731
|
+
readTextIfExists(path.join(paths.sessionRoot, "issue_details.md")),
|
|
732
|
+
readTextIfExists(path.join(paths.sessionRoot, "agent_decisions.md")),
|
|
733
|
+
readTextIfExists(path.join(paths.sessionRoot, "final_report.md")),
|
|
734
|
+
readTextIfExists(path.join(paths.sessionRoot, "github_comments.json")),
|
|
735
|
+
readTrimmedFile(path.join(paths.sessionRoot, "codex_thread_id")),
|
|
736
|
+
readWorkflowVersion(paths),
|
|
737
|
+
readTrimmedFile(path.join(paths.sessionRoot, "base_branch")),
|
|
738
|
+
readTrimmedFile(path.join(paths.sessionRoot, "base_commit")),
|
|
739
|
+
readTextIfExists(path.join(paths.sessionRoot, "issue_metadata.json")),
|
|
740
|
+
readTextIfExists(path.join(cycleStepsRoot(paths, activeCycle), "plan_executed")),
|
|
741
|
+
readTextIfExists(path.join(paths.sessionRoot, "pr_outcome.json"))
|
|
153
742
|
]);
|
|
154
|
-
|
|
743
|
+
let issueMetadata = null;
|
|
744
|
+
if (issueMetadataText) {
|
|
745
|
+
try {
|
|
746
|
+
issueMetadata = JSON.parse(issueMetadataText);
|
|
747
|
+
} catch {
|
|
748
|
+
issueMetadata = null;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
let githubComments = {};
|
|
752
|
+
if (githubCommentsText) {
|
|
753
|
+
try {
|
|
754
|
+
const parsed = JSON.parse(githubCommentsText);
|
|
755
|
+
githubComments = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
756
|
+
} catch {
|
|
757
|
+
githubComments = {};
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
let prOutcome = null;
|
|
761
|
+
if (prOutcomeText) {
|
|
762
|
+
try {
|
|
763
|
+
const parsed = JSON.parse(prOutcomeText);
|
|
764
|
+
prOutcome = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
765
|
+
} catch {
|
|
766
|
+
prOutcome = null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const cycles = await readCycles(paths, activeCycle);
|
|
770
|
+
const checks = await readStructuredChecks(paths);
|
|
771
|
+
const uiChecks = await readStructuredUiChecks(paths);
|
|
772
|
+
const reviewPasses = await readReviewPasses(paths);
|
|
773
|
+
const worktreeReady = await hasWorktree(paths);
|
|
774
|
+
const worktreeStatus = await readWorktreeStatus(paths, worktreeReady);
|
|
775
|
+
const commandLogPath = path.join(paths.sessionRoot, "command_log.jsonl");
|
|
776
|
+
const dependencyInstallReceipt = await readTextIfExists(path.join(paths.sessionRoot, "steps", "dependencies_installed"));
|
|
777
|
+
const planExecutionPromptPath = path.join(paths.sessionRoot, "prompts", cyclePlanExecutionPromptFileName(activeCycle));
|
|
778
|
+
const planExecutionPromptExists = await fileExists(planExecutionPromptPath);
|
|
779
|
+
const appRootForArtifacts = worktreeReady ? paths.worktree : paths.targetRoot;
|
|
780
|
+
const appReady = await inspectReadyJskitAppRoot(appRootForArtifacts);
|
|
781
|
+
const blueprintPath = path.join(appRootForArtifacts, ".jskit", "APP_BLUEPRINT.md");
|
|
782
|
+
const helperMapPath = path.join(appRootForArtifacts, ".jskit", "helper-map.md");
|
|
783
|
+
const currentStep = normalizeStepId(rawCurrentStep);
|
|
784
|
+
let completedSteps = await readCompletedSteps(paths);
|
|
785
|
+
const worktreeRemovalCompleted = completedSteps.includes("pr_finalized");
|
|
786
|
+
const worktreeReceiptInvalid = !worktreeReady &&
|
|
787
|
+
completedSteps.includes("worktree_created") &&
|
|
788
|
+
!worktreeRemovalCompleted &&
|
|
789
|
+
status !== SESSION_STATUS.FINISHED &&
|
|
790
|
+
status !== SESSION_STATUS.ABANDONED;
|
|
791
|
+
if (worktreeReceiptInvalid) {
|
|
792
|
+
completedSteps = completedSteps.filter((stepId) => !["worktree_created", "dependencies_installed"].includes(stepId));
|
|
793
|
+
}
|
|
155
794
|
const nextStep = resolveNextStep(completedSteps);
|
|
795
|
+
const currentStepIndex = stepIndex(currentStep);
|
|
796
|
+
const nextStepIndex = stepIndex(nextStep);
|
|
797
|
+
const effectiveCurrentStep = nextStep &&
|
|
798
|
+
(completedSteps.includes(currentStep) || currentStepIndex < 0 || currentStepIndex > nextStepIndex)
|
|
799
|
+
? nextStep
|
|
800
|
+
: currentStep || nextStep;
|
|
801
|
+
const prompt = await readPromptForStep(paths, effectiveCurrentStep);
|
|
156
802
|
|
|
157
803
|
return {
|
|
158
804
|
codexThreadId,
|
|
159
805
|
completedSteps,
|
|
160
|
-
currentStep:
|
|
806
|
+
currentStep: effectiveCurrentStep,
|
|
807
|
+
activeCycle,
|
|
808
|
+
appReady,
|
|
809
|
+
baseBranch,
|
|
810
|
+
baseCommit,
|
|
811
|
+
blueprintExists: await fileExists(blueprintPath),
|
|
812
|
+
blueprintPath,
|
|
813
|
+
cycles,
|
|
814
|
+
checks,
|
|
815
|
+
uiChecks,
|
|
816
|
+
reviewPasses,
|
|
817
|
+
currentReviewPass: reviewPasses.at(-1)?.pass || "",
|
|
818
|
+
commandLogExists: await fileExists(commandLogPath),
|
|
819
|
+
commandLogPath,
|
|
820
|
+
dependencyInstall: {
|
|
821
|
+
installed: Boolean(dependencyInstallReceipt.trim()),
|
|
822
|
+
receipt: dependencyInstallReceipt.trim(),
|
|
823
|
+
status: dependencyInstallReceipt.trim()
|
|
824
|
+
? "installed"
|
|
825
|
+
: worktreeReady ? "pending" : "waiting_for_worktree"
|
|
826
|
+
},
|
|
827
|
+
helperMapExists: await fileExists(helperMapPath),
|
|
828
|
+
helperMapPath,
|
|
829
|
+
githubComments,
|
|
830
|
+
issueMetadata,
|
|
831
|
+
issueCategory: normalizeText(issueMetadata?.issueCategory || ""),
|
|
832
|
+
uiImpact: normalizeText(issueMetadata?.uiImpact || ""),
|
|
833
|
+
agentDecisions: agentDecisions.trim(),
|
|
834
|
+
agentDecisionsLatest: agentDecisions
|
|
835
|
+
.split(/\r?\n/u)
|
|
836
|
+
.map((line) => line.trim())
|
|
837
|
+
.filter((line) => line && !line.startsWith("#") && !line.startsWith("Session:"))
|
|
838
|
+
.slice(-5)
|
|
839
|
+
.join("\n"),
|
|
840
|
+
issueTitle,
|
|
161
841
|
issueText: issueText.trim(),
|
|
162
842
|
issueUrl,
|
|
163
843
|
nextStep,
|
|
164
844
|
prUrl,
|
|
845
|
+
prOutcome,
|
|
846
|
+
planExecution: {
|
|
847
|
+
prompted: planExecutionPromptExists,
|
|
848
|
+
promptPath: planExecutionPromptExists ? planExecutionPromptPath : "",
|
|
849
|
+
receipt: planExecutionReceipt.trim(),
|
|
850
|
+
submitted: Boolean(planExecutionReceipt.trim())
|
|
851
|
+
},
|
|
852
|
+
planText: planText.trim(),
|
|
853
|
+
issueDetails: issueDetails.trim(),
|
|
854
|
+
finalReportText: finalReportText.trim(),
|
|
165
855
|
prompt: prompt.trim(),
|
|
166
|
-
status: status || SESSION_STATUS.PENDING
|
|
856
|
+
status: status || SESSION_STATUS.PENDING,
|
|
857
|
+
workflowVersion,
|
|
858
|
+
worktreeReady,
|
|
859
|
+
worktreeStatus
|
|
167
860
|
};
|
|
168
861
|
}
|
|
169
862
|
|
|
@@ -171,11 +864,12 @@ function buildNextCommand(sessionId, stepId) {
|
|
|
171
864
|
if (!stepId) {
|
|
172
865
|
return "";
|
|
173
866
|
}
|
|
174
|
-
const template = STEP_DEFINITION_BY_ID[stepId]?.nextCommandTemplate ||
|
|
867
|
+
const template = STEP_DEFINITION_BY_ID[stepId]?.nextCommandTemplate || `${JSKIT_CLI_SHELL_COMMAND} session {{session_id}} step`;
|
|
175
868
|
return template.replaceAll("{{session_id}}", sessionId);
|
|
176
869
|
}
|
|
177
870
|
|
|
178
871
|
async function buildSessionResponse(paths, {
|
|
872
|
+
codex = undefined,
|
|
179
873
|
ok = true,
|
|
180
874
|
errors = [],
|
|
181
875
|
preconditions = [],
|
|
@@ -185,8 +879,72 @@ async function buildSessionResponse(paths, {
|
|
|
185
879
|
const responsePaths = paths.sessionId ? await pathsForExistingSession(paths) : paths;
|
|
186
880
|
const artifacts = responsePaths.sessionRoot ? await readSessionArtifacts(responsePaths) : {};
|
|
187
881
|
const resolvedStatus = status || artifacts.status || (ok ? SESSION_STATUS.PENDING : SESSION_STATUS.BLOCKED);
|
|
882
|
+
if (responsePaths.sessionRoot && await fileExists(responsePaths.sessionRoot) && artifacts.workflowVersion !== SESSION_WORKFLOW_VERSION) {
|
|
883
|
+
return {
|
|
884
|
+
ok: false,
|
|
885
|
+
sessionId: paths.sessionId || "",
|
|
886
|
+
status: SESSION_STATUS.BLOCKED,
|
|
887
|
+
currentStep: "",
|
|
888
|
+
completedSteps: artifacts.completedSteps || [],
|
|
889
|
+
workflowVersion: artifacts.workflowVersion || "",
|
|
890
|
+
baseBranch: artifacts.baseBranch || "",
|
|
891
|
+
baseCommit: artifacts.baseCommit || "",
|
|
892
|
+
blueprintPath: artifacts.blueprintPath || "",
|
|
893
|
+
blueprintExists: artifacts.blueprintExists === true,
|
|
894
|
+
activeCycle: artifacts.activeCycle || "",
|
|
895
|
+
appReady: cloneContractValue(artifacts.appReady || null),
|
|
896
|
+
cycles: cloneContractValue(artifacts.cycles || []),
|
|
897
|
+
checks: cloneContractValue(artifacts.checks || []),
|
|
898
|
+
dependencyInstall: cloneContractValue(artifacts.dependencyInstall || null),
|
|
899
|
+
uiChecks: cloneContractValue(artifacts.uiChecks || []),
|
|
900
|
+
reviewPasses: cloneContractValue(artifacts.reviewPasses || []),
|
|
901
|
+
currentReviewPass: artifacts.currentReviewPass || "",
|
|
902
|
+
commandLogExists: artifacts.commandLogExists === true,
|
|
903
|
+
commandLogPath: artifacts.commandLogPath || "",
|
|
904
|
+
stepDefinitions: buildStepDefinitions(),
|
|
905
|
+
currentStepAction: null,
|
|
906
|
+
codex: null,
|
|
907
|
+
prompt: "",
|
|
908
|
+
nextCommand: "",
|
|
909
|
+
issueUrl: artifacts.issueUrl || "",
|
|
910
|
+
issueTitle: artifacts.issueTitle || "",
|
|
911
|
+
issueText: artifacts.issueText || "",
|
|
912
|
+
issueMetadata: cloneContractValue(artifacts.issueMetadata || null),
|
|
913
|
+
githubComments: cloneContractValue(artifacts.githubComments || {}),
|
|
914
|
+
issueCategory: artifacts.issueCategory || "",
|
|
915
|
+
uiImpact: artifacts.uiImpact || "",
|
|
916
|
+
agentDecisionsPath: artifacts.agentDecisions ? path.join(responsePaths.sessionRoot, "agent_decisions.md") : "",
|
|
917
|
+
agentDecisionsLatest: artifacts.agentDecisionsLatest || "",
|
|
918
|
+
planExecution: cloneContractValue(artifacts.planExecution || null),
|
|
919
|
+
planText: artifacts.planText || "",
|
|
920
|
+
issueDetails: artifacts.issueDetails || "",
|
|
921
|
+
issueDetailsPath: artifacts.issueDetails ? path.join(responsePaths.sessionRoot, "issue_details.md") : "",
|
|
922
|
+
finalReportPath: artifacts.finalReportText ? path.join(responsePaths.sessionRoot, "final_report.md") : "",
|
|
923
|
+
finalReportText: artifacts.finalReportText || "",
|
|
924
|
+
helperMapPath: artifacts.helperMapPath || "",
|
|
925
|
+
helperMapExists: artifacts.helperMapExists === true,
|
|
926
|
+
prUrl: artifacts.prUrl || "",
|
|
927
|
+
prOutcome: cloneContractValue(artifacts.prOutcome || null),
|
|
928
|
+
preconditions,
|
|
929
|
+
errors: [
|
|
930
|
+
createError({
|
|
931
|
+
code: "unsupported_workflow_version",
|
|
932
|
+
message: `Session ${paths.sessionId || ""} uses workflow version ${artifacts.workflowVersion || "missing"}, but this JSKIT runtime expects ${SESSION_WORKFLOW_VERSION}.`
|
|
933
|
+
})
|
|
934
|
+
],
|
|
935
|
+
archive: responsePaths.archive || "active",
|
|
936
|
+
sessionRoot: responsePaths.sessionRoot || "",
|
|
937
|
+
worktree: paths.worktree || "",
|
|
938
|
+
worktreeReady: artifacts.worktreeReady === true,
|
|
939
|
+
worktreeStatus: cloneContractValue(artifacts.worktreeStatus || null),
|
|
940
|
+
branch: paths.branch || "",
|
|
941
|
+
codexThreadId: artifacts.codexThreadId || ""
|
|
942
|
+
};
|
|
943
|
+
}
|
|
188
944
|
const currentStep = artifacts.currentStep || artifacts.nextStep || "";
|
|
189
|
-
const responsePrompt = typeof prompt === "string"
|
|
945
|
+
const responsePrompt = typeof prompt === "string"
|
|
946
|
+
? prompt
|
|
947
|
+
: stepCanExposeStoredPrompt(currentStep) ? artifacts.prompt || "" : "";
|
|
190
948
|
|
|
191
949
|
return {
|
|
192
950
|
ok: ok === true,
|
|
@@ -194,18 +952,52 @@ async function buildSessionResponse(paths, {
|
|
|
194
952
|
status: resolvedStatus,
|
|
195
953
|
currentStep,
|
|
196
954
|
completedSteps: artifacts.completedSteps || [],
|
|
955
|
+
workflowVersion: artifacts.workflowVersion || "",
|
|
956
|
+
baseBranch: artifacts.baseBranch || "",
|
|
957
|
+
baseCommit: artifacts.baseCommit || "",
|
|
958
|
+
blueprintPath: artifacts.blueprintPath || "",
|
|
959
|
+
blueprintExists: artifacts.blueprintExists === true,
|
|
960
|
+
activeCycle: artifacts.activeCycle || "",
|
|
961
|
+
appReady: cloneContractValue(artifacts.appReady || null),
|
|
962
|
+
cycles: cloneContractValue(artifacts.cycles || []),
|
|
963
|
+
checks: cloneContractValue(artifacts.checks || []),
|
|
964
|
+
dependencyInstall: cloneContractValue(artifacts.dependencyInstall || null),
|
|
965
|
+
uiChecks: cloneContractValue(artifacts.uiChecks || []),
|
|
966
|
+
reviewPasses: cloneContractValue(artifacts.reviewPasses || []),
|
|
967
|
+
currentReviewPass: artifacts.currentReviewPass || "",
|
|
968
|
+
commandLogExists: artifacts.commandLogExists === true,
|
|
969
|
+
commandLogPath: artifacts.commandLogPath || "",
|
|
197
970
|
stepDefinitions: buildStepDefinitions(),
|
|
198
|
-
currentStepAction: buildCurrentStepAction(currentStep),
|
|
199
|
-
codex: buildCodexHandoff(currentStep),
|
|
971
|
+
currentStepAction: buildCurrentStepAction(currentStep, artifacts),
|
|
972
|
+
codex: codex === undefined ? buildCodexHandoff(currentStep, artifacts) : cloneContractValue(codex),
|
|
200
973
|
prompt: responsePrompt,
|
|
201
974
|
nextCommand: buildNextCommand(paths.sessionId || "", currentStep),
|
|
202
975
|
issueUrl: artifacts.issueUrl || "",
|
|
976
|
+
issueTitle: artifacts.issueTitle || "",
|
|
977
|
+
issueText: artifacts.issueText || "",
|
|
978
|
+
issueMetadata: cloneContractValue(artifacts.issueMetadata || null),
|
|
979
|
+
githubComments: cloneContractValue(artifacts.githubComments || {}),
|
|
980
|
+
issueCategory: artifacts.issueCategory || "",
|
|
981
|
+
uiImpact: artifacts.uiImpact || "",
|
|
982
|
+
agentDecisionsPath: artifacts.agentDecisions ? path.join(responsePaths.sessionRoot, "agent_decisions.md") : "",
|
|
983
|
+
agentDecisionsLatest: artifacts.agentDecisionsLatest || "",
|
|
984
|
+
planExecution: cloneContractValue(artifacts.planExecution || null),
|
|
985
|
+
planText: artifacts.planText || "",
|
|
986
|
+
issueDetails: artifacts.issueDetails || "",
|
|
987
|
+
issueDetailsPath: artifacts.issueDetails ? path.join(responsePaths.sessionRoot, "issue_details.md") : "",
|
|
988
|
+
finalReportPath: artifacts.finalReportText ? path.join(responsePaths.sessionRoot, "final_report.md") : "",
|
|
989
|
+
finalReportText: artifacts.finalReportText || "",
|
|
990
|
+
helperMapPath: artifacts.helperMapPath || "",
|
|
991
|
+
helperMapExists: artifacts.helperMapExists === true,
|
|
203
992
|
prUrl: artifacts.prUrl || "",
|
|
993
|
+
prOutcome: cloneContractValue(artifacts.prOutcome || null),
|
|
204
994
|
preconditions,
|
|
205
995
|
errors,
|
|
206
996
|
archive: responsePaths.archive || (resolvedStatus === SESSION_STATUS.FINISHED ? "completed" : resolvedStatus === SESSION_STATUS.ABANDONED ? "abandoned" : "active"),
|
|
207
997
|
sessionRoot: responsePaths.sessionRoot || "",
|
|
208
998
|
worktree: paths.worktree || "",
|
|
999
|
+
worktreeReady: artifacts.worktreeReady === true,
|
|
1000
|
+
worktreeStatus: cloneContractValue(artifacts.worktreeStatus || null),
|
|
209
1001
|
branch: paths.branch || "",
|
|
210
1002
|
codexThreadId: artifacts.codexThreadId || ""
|
|
211
1003
|
};
|
|
@@ -238,18 +1030,52 @@ function buildSessionErrorResponse({
|
|
|
238
1030
|
status,
|
|
239
1031
|
currentStep: "",
|
|
240
1032
|
completedSteps: [],
|
|
1033
|
+
workflowVersion: "",
|
|
1034
|
+
baseBranch: "",
|
|
1035
|
+
baseCommit: "",
|
|
1036
|
+
blueprintPath: "",
|
|
1037
|
+
blueprintExists: false,
|
|
1038
|
+
activeCycle: "",
|
|
1039
|
+
appReady: null,
|
|
1040
|
+
cycles: [],
|
|
1041
|
+
checks: [],
|
|
1042
|
+
dependencyInstall: null,
|
|
1043
|
+
uiChecks: [],
|
|
1044
|
+
reviewPasses: [],
|
|
1045
|
+
currentReviewPass: "",
|
|
1046
|
+
commandLogExists: false,
|
|
1047
|
+
commandLogPath: "",
|
|
241
1048
|
stepDefinitions: buildStepDefinitions(),
|
|
242
1049
|
currentStepAction: null,
|
|
243
1050
|
codex: null,
|
|
244
1051
|
prompt: "",
|
|
245
1052
|
nextCommand: "",
|
|
1053
|
+
issueTitle: "",
|
|
1054
|
+
issueText: "",
|
|
1055
|
+
issueMetadata: null,
|
|
1056
|
+
githubComments: {},
|
|
1057
|
+
issueCategory: "",
|
|
1058
|
+
uiImpact: "",
|
|
1059
|
+
agentDecisionsPath: "",
|
|
1060
|
+
agentDecisionsLatest: "",
|
|
1061
|
+
planExecution: null,
|
|
1062
|
+
planText: "",
|
|
1063
|
+
issueDetails: "",
|
|
1064
|
+
issueDetailsPath: "",
|
|
1065
|
+
finalReportPath: "",
|
|
1066
|
+
finalReportText: "",
|
|
1067
|
+
helperMapPath: "",
|
|
1068
|
+
helperMapExists: false,
|
|
246
1069
|
issueUrl: "",
|
|
247
1070
|
prUrl: "",
|
|
1071
|
+
prOutcome: null,
|
|
248
1072
|
preconditions,
|
|
249
1073
|
errors: errorList,
|
|
250
1074
|
archive: "",
|
|
251
1075
|
sessionRoot: "",
|
|
252
1076
|
worktree: "",
|
|
1077
|
+
worktreeReady: false,
|
|
1078
|
+
worktreeStatus: null,
|
|
253
1079
|
branch: "",
|
|
254
1080
|
codexThreadId: "",
|
|
255
1081
|
targetRoot: normalizedTargetRoot
|
|
@@ -265,15 +1091,29 @@ async function markCurrentStep(paths, stepId) {
|
|
|
265
1091
|
}
|
|
266
1092
|
|
|
267
1093
|
async function writeReceipt(paths, stepId, message) {
|
|
268
|
-
await
|
|
1094
|
+
const activeCycle = await readActiveCycle(paths);
|
|
1095
|
+
const root = isCycleStepId(stepId) ? cycleStepsRoot(paths, activeCycle) : path.join(paths.sessionRoot, "steps");
|
|
1096
|
+
await mkdir(root, { recursive: true });
|
|
269
1097
|
await writeTextFile(
|
|
270
|
-
path.join(
|
|
1098
|
+
path.join(root, stepId),
|
|
271
1099
|
`${timestampForReceipt()}\n${normalizeText(message) || STEP_LABEL_BY_ID[stepId] || stepId}`
|
|
272
1100
|
);
|
|
273
|
-
const completedSteps = await readCompletedSteps(paths
|
|
1101
|
+
const completedSteps = await readCompletedSteps(paths);
|
|
274
1102
|
await markCurrentStep(paths, resolveNextStep(completedSteps));
|
|
275
1103
|
}
|
|
276
1104
|
|
|
1105
|
+
async function writeCycleReceipt(paths, receiptName, message, {
|
|
1106
|
+
cycle = ""
|
|
1107
|
+
} = {}) {
|
|
1108
|
+
const activeCycle = normalizeCycleNumber(cycle || await readActiveCycle(paths));
|
|
1109
|
+
const root = cycleStepsRoot(paths, activeCycle);
|
|
1110
|
+
await mkdir(root, { recursive: true });
|
|
1111
|
+
await writeTextFile(
|
|
1112
|
+
path.join(root, normalizeText(receiptName)),
|
|
1113
|
+
`${timestampForReceipt()}\n${normalizeText(message) || normalizeText(receiptName)}`
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
277
1117
|
async function failSession(paths, {
|
|
278
1118
|
code,
|
|
279
1119
|
message,
|
|
@@ -309,7 +1149,14 @@ export {
|
|
|
309
1149
|
failSession,
|
|
310
1150
|
markCurrentStep,
|
|
311
1151
|
markStatus,
|
|
1152
|
+
normalizeReviewPassNumber,
|
|
1153
|
+
readActiveCycle,
|
|
312
1154
|
readReceiptSteps,
|
|
1155
|
+
readReviewPasses,
|
|
313
1156
|
readSessionArtifacts,
|
|
1157
|
+
reviewPassDirectoryName,
|
|
1158
|
+
reviewPassRoot,
|
|
1159
|
+
writeActiveCycle,
|
|
1160
|
+
writeCycleReceipt,
|
|
314
1161
|
writeReceipt
|
|
315
1162
|
};
|