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