@jskit-ai/jskit-cli 0.2.81 → 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 +110 -3
- package/src/server/commandHandlers/show.js +169 -34
- package/src/server/core/argParser.js +8 -0
- package/src/server/core/commandCatalog.js +58 -2
- 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 +217 -78
- package/src/server/sessionRuntime/preconditions.js +382 -5
- package/src/server/sessionRuntime/promptRenderer.js +15 -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 +11 -2
- package/src/server/sessionRuntime/prompts/execute_plan.md +32 -6
- 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 +15 -2
- package/src/server/sessionRuntime/prompts/plan_issue.md +40 -9
- package/src/server/sessionRuntime/prompts/review_changes.md +46 -5
- package/src/server/sessionRuntime/prompts/update_blueprint.md +36 -0
- package/src/server/sessionRuntime/prompts/user_check.md +15 -1
- package/src/server/sessionRuntime/responses.js +776 -56
- package/src/server/sessionRuntime.js +1658 -123
|
@@ -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,6 +16,7 @@ import {
|
|
|
12
16
|
normalizeText,
|
|
13
17
|
readTextIfExists,
|
|
14
18
|
readTrimmedFile,
|
|
19
|
+
runGitInWorktree,
|
|
15
20
|
timestampForReceipt,
|
|
16
21
|
writeTextFile
|
|
17
22
|
} from "./io.js";
|
|
@@ -21,6 +26,9 @@ import {
|
|
|
21
26
|
import {
|
|
22
27
|
hasWorktree
|
|
23
28
|
} from "./worktrees.js";
|
|
29
|
+
import {
|
|
30
|
+
inspectReadyJskitAppRoot
|
|
31
|
+
} from "./appReadiness.js";
|
|
24
32
|
|
|
25
33
|
function createError({
|
|
26
34
|
code,
|
|
@@ -46,34 +54,8 @@ function createPrecondition({
|
|
|
46
54
|
});
|
|
47
55
|
}
|
|
48
56
|
|
|
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
57
|
function normalizeStepId(stepId) {
|
|
65
|
-
|
|
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);
|
|
58
|
+
return normalizeText(stepId);
|
|
77
59
|
}
|
|
78
60
|
|
|
79
61
|
function stepIndex(stepId) {
|
|
@@ -84,7 +66,6 @@ function normalizeKnownStepIds(stepIds = []) {
|
|
|
84
66
|
return Array.from(
|
|
85
67
|
new Set(
|
|
86
68
|
stepIds
|
|
87
|
-
.flatMap((stepId) => completedStepIdsForReceipt(stepId))
|
|
88
69
|
.map((stepId) => normalizeText(stepId))
|
|
89
70
|
.filter((stepId) => STEP_IDS.includes(stepId))
|
|
90
71
|
)
|
|
@@ -93,20 +74,155 @@ function normalizeKnownStepIds(stepIds = []) {
|
|
|
93
74
|
|
|
94
75
|
function stepCanExposeStoredPrompt(stepId) {
|
|
95
76
|
const step = STEP_DEFINITION_BY_ID[normalizeStepId(stepId)];
|
|
96
|
-
return Boolean(step?.codex || step?.kind === "human_input");
|
|
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)));
|
|
97
199
|
}
|
|
98
200
|
|
|
99
201
|
const PROMPT_ARTIFACT_BY_STEP_ID = Object.freeze({
|
|
100
202
|
issue_drafted: "issue_draft.md",
|
|
101
|
-
|
|
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",
|
|
102
207
|
user_check_completed: "user_check.md"
|
|
103
208
|
});
|
|
104
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
|
+
|
|
105
221
|
async function readPromptForStep(paths, stepId) {
|
|
106
222
|
if (!stepCanExposeStoredPrompt(stepId)) {
|
|
107
223
|
return "";
|
|
108
224
|
}
|
|
109
|
-
const promptArtifact =
|
|
225
|
+
const promptArtifact = await promptArtifactForStep(paths, stepId);
|
|
110
226
|
if (promptArtifact) {
|
|
111
227
|
const prompt = await readTextIfExists(path.join(paths.sessionRoot, "prompts", promptArtifact));
|
|
112
228
|
if (prompt) {
|
|
@@ -116,18 +232,176 @@ async function readPromptForStep(paths, stepId) {
|
|
|
116
232
|
return readTextIfExists(path.join(paths.sessionRoot, "prompt.md"));
|
|
117
233
|
}
|
|
118
234
|
|
|
119
|
-
async function
|
|
120
|
-
const stepsRoot = path.join(sessionRoot, "steps");
|
|
235
|
+
async function readStepFileNames(stepsRoot) {
|
|
121
236
|
try {
|
|
122
237
|
const entries = await readdir(stepsRoot, { withFileTypes: true });
|
|
123
|
-
return
|
|
238
|
+
return entries
|
|
124
239
|
.filter((entry) => entry.isFile())
|
|
125
|
-
.map((entry) => entry.name)
|
|
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;
|
|
126
343
|
} catch {
|
|
127
344
|
return [];
|
|
128
345
|
}
|
|
129
346
|
}
|
|
130
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;
|
|
365
|
+
} catch {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
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
|
+
|
|
131
405
|
async function readReceiptSteps(paths) {
|
|
132
406
|
const stepsRoot = path.join(paths.sessionRoot, "steps");
|
|
133
407
|
try {
|
|
@@ -138,7 +412,7 @@ async function readReceiptSteps(paths) {
|
|
|
138
412
|
.filter((entry) => entry.isFile())
|
|
139
413
|
.map((entry) => entry.name)
|
|
140
414
|
.forEach((receiptName) => {
|
|
141
|
-
const stepId =
|
|
415
|
+
const stepId = normalizeStepId(receiptName);
|
|
142
416
|
if (STEP_IDS.includes(stepId)) {
|
|
143
417
|
if (!knownStepRows.has(stepId) || receiptName === stepId) {
|
|
144
418
|
knownStepRows.set(stepId, {
|
|
@@ -170,11 +444,34 @@ async function readReceiptSteps(paths) {
|
|
|
170
444
|
return left.stepId.localeCompare(right.stepId);
|
|
171
445
|
});
|
|
172
446
|
|
|
173
|
-
|
|
447
|
+
const globalReceipts = await Promise.all(stepRows.map(async ({ receiptName, stepId }) => ({
|
|
448
|
+
cycle: "",
|
|
174
449
|
label: STEP_LABEL_BY_ID[stepId] || stepId,
|
|
175
450
|
receipt: (await readTextIfExists(path.join(stepsRoot, receiptName))).trim(),
|
|
176
451
|
stepId
|
|
177
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];
|
|
178
475
|
} catch {
|
|
179
476
|
return [];
|
|
180
477
|
}
|
|
@@ -197,14 +494,35 @@ function cloneContractValue(value) {
|
|
|
197
494
|
);
|
|
198
495
|
}
|
|
199
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
|
+
|
|
200
514
|
function publicStepDefinition(step, index) {
|
|
201
515
|
return {
|
|
202
516
|
description: step.description,
|
|
517
|
+
displayGroupId: step.displayGroupId || "",
|
|
518
|
+
displayGroupLabel: step.displayGroupLabel || "",
|
|
203
519
|
id: step.id,
|
|
204
520
|
index,
|
|
205
521
|
input: cloneContractValue(step.input),
|
|
206
522
|
kind: step.kind,
|
|
207
523
|
label: step.label,
|
|
524
|
+
...stepRepeatabilityContract(step.id),
|
|
525
|
+
requiresExplicitRun: step.requiresExplicitRun === true,
|
|
208
526
|
utilityActions: cloneContractValue(step.utilityActions || [])
|
|
209
527
|
};
|
|
210
528
|
}
|
|
@@ -213,43 +531,258 @@ function buildStepDefinitions() {
|
|
|
213
531
|
return STEP_DEFINITIONS.map((step, index) => publicStepDefinition(step, index));
|
|
214
532
|
}
|
|
215
533
|
|
|
216
|
-
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 = {}) {
|
|
217
570
|
const step = STEP_DEFINITION_BY_ID[stepId];
|
|
218
571
|
if (!step) {
|
|
219
572
|
return null;
|
|
220
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
|
+
})();
|
|
221
678
|
return {
|
|
222
|
-
|
|
223
|
-
|
|
679
|
+
alternateActions,
|
|
680
|
+
buttonLabel: dynamicButtonLabel,
|
|
681
|
+
description: dynamicDescription,
|
|
224
682
|
index: STEP_IDS.indexOf(step.id),
|
|
225
683
|
input: cloneContractValue(step.input),
|
|
226
684
|
kind: step.kind,
|
|
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),
|
|
227
692
|
stepId: step.id,
|
|
228
|
-
utilityActions: cloneContractValue(
|
|
693
|
+
utilityActions: cloneContractValue(dynamicUtilityActions)
|
|
229
694
|
};
|
|
230
695
|
}
|
|
231
696
|
|
|
232
|
-
function buildCodexHandoff(stepId) {
|
|
697
|
+
function buildCodexHandoff(stepId, _artifacts = {}) {
|
|
233
698
|
const step = STEP_DEFINITION_BY_ID[stepId];
|
|
234
699
|
return step?.codex ? cloneContractValue(step.codex) : null;
|
|
235
700
|
}
|
|
236
701
|
|
|
237
702
|
async function readSessionArtifacts(paths) {
|
|
238
|
-
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([
|
|
239
724
|
readTrimmedFile(path.join(paths.sessionRoot, "status")),
|
|
240
725
|
readTrimmedFile(path.join(paths.sessionRoot, "current_step")),
|
|
241
726
|
readTrimmedFile(path.join(paths.sessionRoot, "issue_url")),
|
|
242
727
|
readTrimmedFile(path.join(paths.sessionRoot, "pr_url")),
|
|
243
728
|
readTextIfExists(path.join(paths.sessionRoot, "issue.md")),
|
|
244
729
|
readTrimmedFile(path.join(paths.sessionRoot, "issue_title")),
|
|
245
|
-
readTextIfExists(
|
|
246
|
-
|
|
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"))
|
|
247
742
|
]);
|
|
248
|
-
|
|
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);
|
|
249
773
|
const worktreeReady = await hasWorktree(paths);
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
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");
|
|
253
786
|
const worktreeReceiptInvalid = !worktreeReady &&
|
|
254
787
|
completedSteps.includes("worktree_created") &&
|
|
255
788
|
!worktreeRemovalCompleted &&
|
|
@@ -271,15 +804,59 @@ async function readSessionArtifacts(paths) {
|
|
|
271
804
|
codexThreadId,
|
|
272
805
|
completedSteps,
|
|
273
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"),
|
|
274
840
|
issueTitle,
|
|
275
841
|
issueText: issueText.trim(),
|
|
276
842
|
issueUrl,
|
|
277
843
|
nextStep,
|
|
278
844
|
prUrl,
|
|
845
|
+
prOutcome,
|
|
846
|
+
planExecution: {
|
|
847
|
+
prompted: planExecutionPromptExists,
|
|
848
|
+
promptPath: planExecutionPromptExists ? planExecutionPromptPath : "",
|
|
849
|
+
receipt: planExecutionReceipt.trim(),
|
|
850
|
+
submitted: Boolean(planExecutionReceipt.trim())
|
|
851
|
+
},
|
|
279
852
|
planText: planText.trim(),
|
|
853
|
+
issueDetails: issueDetails.trim(),
|
|
854
|
+
finalReportText: finalReportText.trim(),
|
|
280
855
|
prompt: prompt.trim(),
|
|
281
856
|
status: status || SESSION_STATUS.PENDING,
|
|
282
|
-
|
|
857
|
+
workflowVersion,
|
|
858
|
+
worktreeReady,
|
|
859
|
+
worktreeStatus
|
|
283
860
|
};
|
|
284
861
|
}
|
|
285
862
|
|
|
@@ -287,7 +864,7 @@ function buildNextCommand(sessionId, stepId) {
|
|
|
287
864
|
if (!stepId) {
|
|
288
865
|
return "";
|
|
289
866
|
}
|
|
290
|
-
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`;
|
|
291
868
|
return template.replaceAll("{{session_id}}", sessionId);
|
|
292
869
|
}
|
|
293
870
|
|
|
@@ -302,6 +879,68 @@ async function buildSessionResponse(paths, {
|
|
|
302
879
|
const responsePaths = paths.sessionId ? await pathsForExistingSession(paths) : paths;
|
|
303
880
|
const artifacts = responsePaths.sessionRoot ? await readSessionArtifacts(responsePaths) : {};
|
|
304
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
|
+
}
|
|
305
944
|
const currentStep = artifacts.currentStep || artifacts.nextStep || "";
|
|
306
945
|
const responsePrompt = typeof prompt === "string"
|
|
307
946
|
? prompt
|
|
@@ -313,22 +952,52 @@ async function buildSessionResponse(paths, {
|
|
|
313
952
|
status: resolvedStatus,
|
|
314
953
|
currentStep,
|
|
315
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 || "",
|
|
316
970
|
stepDefinitions: buildStepDefinitions(),
|
|
317
|
-
currentStepAction: buildCurrentStepAction(currentStep),
|
|
318
|
-
codex: codex === undefined ? buildCodexHandoff(currentStep) : cloneContractValue(codex),
|
|
971
|
+
currentStepAction: buildCurrentStepAction(currentStep, artifacts),
|
|
972
|
+
codex: codex === undefined ? buildCodexHandoff(currentStep, artifacts) : cloneContractValue(codex),
|
|
319
973
|
prompt: responsePrompt,
|
|
320
974
|
nextCommand: buildNextCommand(paths.sessionId || "", currentStep),
|
|
321
975
|
issueUrl: artifacts.issueUrl || "",
|
|
322
976
|
issueTitle: artifacts.issueTitle || "",
|
|
323
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),
|
|
324
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,
|
|
325
992
|
prUrl: artifacts.prUrl || "",
|
|
993
|
+
prOutcome: cloneContractValue(artifacts.prOutcome || null),
|
|
326
994
|
preconditions,
|
|
327
995
|
errors,
|
|
328
996
|
archive: responsePaths.archive || (resolvedStatus === SESSION_STATUS.FINISHED ? "completed" : resolvedStatus === SESSION_STATUS.ABANDONED ? "abandoned" : "active"),
|
|
329
997
|
sessionRoot: responsePaths.sessionRoot || "",
|
|
330
998
|
worktree: paths.worktree || "",
|
|
331
999
|
worktreeReady: artifacts.worktreeReady === true,
|
|
1000
|
+
worktreeStatus: cloneContractValue(artifacts.worktreeStatus || null),
|
|
332
1001
|
branch: paths.branch || "",
|
|
333
1002
|
codexThreadId: artifacts.codexThreadId || ""
|
|
334
1003
|
};
|
|
@@ -361,6 +1030,21 @@ function buildSessionErrorResponse({
|
|
|
361
1030
|
status,
|
|
362
1031
|
currentStep: "",
|
|
363
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: "",
|
|
364
1048
|
stepDefinitions: buildStepDefinitions(),
|
|
365
1049
|
currentStepAction: null,
|
|
366
1050
|
codex: null,
|
|
@@ -368,15 +1052,30 @@ function buildSessionErrorResponse({
|
|
|
368
1052
|
nextCommand: "",
|
|
369
1053
|
issueTitle: "",
|
|
370
1054
|
issueText: "",
|
|
1055
|
+
issueMetadata: null,
|
|
1056
|
+
githubComments: {},
|
|
1057
|
+
issueCategory: "",
|
|
1058
|
+
uiImpact: "",
|
|
1059
|
+
agentDecisionsPath: "",
|
|
1060
|
+
agentDecisionsLatest: "",
|
|
1061
|
+
planExecution: null,
|
|
371
1062
|
planText: "",
|
|
1063
|
+
issueDetails: "",
|
|
1064
|
+
issueDetailsPath: "",
|
|
1065
|
+
finalReportPath: "",
|
|
1066
|
+
finalReportText: "",
|
|
1067
|
+
helperMapPath: "",
|
|
1068
|
+
helperMapExists: false,
|
|
372
1069
|
issueUrl: "",
|
|
373
1070
|
prUrl: "",
|
|
1071
|
+
prOutcome: null,
|
|
374
1072
|
preconditions,
|
|
375
1073
|
errors: errorList,
|
|
376
1074
|
archive: "",
|
|
377
1075
|
sessionRoot: "",
|
|
378
1076
|
worktree: "",
|
|
379
1077
|
worktreeReady: false,
|
|
1078
|
+
worktreeStatus: null,
|
|
380
1079
|
branch: "",
|
|
381
1080
|
codexThreadId: "",
|
|
382
1081
|
targetRoot: normalizedTargetRoot
|
|
@@ -392,15 +1091,29 @@ async function markCurrentStep(paths, stepId) {
|
|
|
392
1091
|
}
|
|
393
1092
|
|
|
394
1093
|
async function writeReceipt(paths, stepId, message) {
|
|
395
|
-
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 });
|
|
396
1097
|
await writeTextFile(
|
|
397
|
-
path.join(
|
|
1098
|
+
path.join(root, stepId),
|
|
398
1099
|
`${timestampForReceipt()}\n${normalizeText(message) || STEP_LABEL_BY_ID[stepId] || stepId}`
|
|
399
1100
|
);
|
|
400
|
-
const completedSteps = await readCompletedSteps(paths
|
|
1101
|
+
const completedSteps = await readCompletedSteps(paths);
|
|
401
1102
|
await markCurrentStep(paths, resolveNextStep(completedSteps));
|
|
402
1103
|
}
|
|
403
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
|
+
|
|
404
1117
|
async function failSession(paths, {
|
|
405
1118
|
code,
|
|
406
1119
|
message,
|
|
@@ -436,7 +1149,14 @@ export {
|
|
|
436
1149
|
failSession,
|
|
437
1150
|
markCurrentStep,
|
|
438
1151
|
markStatus,
|
|
1152
|
+
normalizeReviewPassNumber,
|
|
1153
|
+
readActiveCycle,
|
|
439
1154
|
readReceiptSteps,
|
|
1155
|
+
readReviewPasses,
|
|
440
1156
|
readSessionArtifacts,
|
|
1157
|
+
reviewPassDirectoryName,
|
|
1158
|
+
reviewPassRoot,
|
|
1159
|
+
writeActiveCycle,
|
|
1160
|
+
writeCycleReceipt,
|
|
441
1161
|
writeReceipt
|
|
442
1162
|
};
|