@jskit-ai/jskit-cli 0.2.87 → 0.2.89

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/jskit-cli",
3
- "version": "0.2.87",
3
+ "version": "0.2.89",
4
4
  "description": "Bundle and package orchestration CLI for JSKIT apps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -20,9 +20,9 @@
20
20
  "test": "node --test"
21
21
  },
22
22
  "dependencies": {
23
- "@jskit-ai/jskit-catalog": "0.1.86",
24
- "@jskit-ai/kernel": "0.1.78",
25
- "@jskit-ai/shell-web": "0.1.77",
23
+ "@jskit-ai/jskit-catalog": "0.1.88",
24
+ "@jskit-ai/kernel": "0.1.80",
25
+ "@jskit-ai/shell-web": "0.1.79",
26
26
  "@vue/compiler-sfc": "^3.5.29",
27
27
  "ts-morph": "^28.0.0"
28
28
  },
@@ -147,6 +147,10 @@ const JSKIT_STEP_RESULT_CONTRACT = Object.freeze({
147
147
  required: true,
148
148
  stepField: "step"
149
149
  });
150
+ const MANUAL_JSKIT_STEP_RESULT_CONTRACT = Object.freeze({
151
+ ...JSKIT_STEP_RESULT_CONTRACT,
152
+ completionBehavior: "manual_advance"
153
+ });
150
154
  const DESLOP_RESULT_CONTRACT = Object.freeze({
151
155
  autoResolvePriorities: Object.freeze(["high", "medium"]),
152
156
  completionBehavior: "deslop_loop",
@@ -231,7 +235,7 @@ function stepAutomationFor({
231
235
  const PLAN_EXECUTION_CODEX_HANDOFF = codexHandoff([], {
232
236
  autoInject: true,
233
237
  promptActionLabel: "Get Codex to execute plan",
234
- responseContract: JSKIT_STEP_RESULT_CONTRACT
238
+ responseContract: MANUAL_JSKIT_STEP_RESULT_CONTRACT
235
239
  });
236
240
  const ISSUE_DETAILS_CODEX_HANDOFF = codexHandoff([
237
241
  ISSUE_CATEGORY_OUTPUT,
@@ -407,7 +411,7 @@ const STEP_DEFINITIONS = Object.freeze([
407
411
  defineStep({
408
412
  buttonLabel: "Get Codex to execute plan",
409
413
  codex: PLAN_EXECUTION_CODEX_HANDOFF,
410
- description: "JSKIT sends the active cycle plan to Codex; Codex implements it; Studio advances when Codex finishes.",
414
+ description: "JSKIT sends the active cycle plan to Codex; Codex implements it; the user advances after reviewing completion.",
411
415
  id: "plan_executed",
412
416
  kind: "codex_prompt",
413
417
  label: "Plan executed",
@@ -473,28 +477,28 @@ const STEP_DEFINITIONS = Object.freeze([
473
477
  })
474
478
  ])
475
479
  }),
476
- defineStep({
477
- buttonLabel: "Commit accepted changes",
478
- description: "JSKIT commits the user-accepted session changes in the session worktree.",
479
- id: "changes_committed",
480
- label: "Changes committed",
481
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "issue_metadata_exists", "issue_url_exists", "github_auth", "active_cycle_exists", "automated_checks_passed", "deep_ui_check_satisfied", "active_cycle_user_check_passed"]
482
- }),
483
480
  defineStep({
484
481
  buttonLabel: "Update blueprint",
485
482
  codex: BLUEPRINT_CODEX_HANDOFF,
486
- description: "JSKIT asks Codex to update durable app memory from the accepted work; Codex edits .jskit/APP_BLUEPRINT.md; JSKIT records and commits the update.",
483
+ description: "JSKIT asks Codex to update durable app memory from the accepted work; Codex edits .jskit/APP_BLUEPRINT.md; JSKIT records the update for the accepted-work commit.",
487
484
  id: "blueprint_updated",
488
485
  kind: "codex_prompt",
489
486
  label: "Blueprint updated",
490
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "issue_metadata_exists", "active_cycle_exists", "automated_checks_passed", "deep_ui_check_satisfied", "active_cycle_user_check_passed", "accepted_changes_committed"]
487
+ preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "issue_metadata_exists", "active_cycle_exists", "automated_checks_passed", "deep_ui_check_satisfied", "active_cycle_user_check_passed"]
488
+ }),
489
+ defineStep({
490
+ buttonLabel: "Commit accepted changes",
491
+ description: "JSKIT commits the accepted session changes, including durable app memory updates, in the session worktree.",
492
+ id: "changes_committed",
493
+ label: "Changes committed",
494
+ preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "issue_metadata_exists", "issue_url_exists", "github_auth", "active_cycle_exists", "automated_checks_passed", "deep_ui_check_satisfied", "active_cycle_user_check_passed", "blueprint_update_satisfied"]
491
495
  }),
492
496
  defineStep({
493
497
  buttonLabel: "Create final report",
494
498
  description: "JSKIT creates the deterministic final session report and comments it on the GitHub issue.",
495
499
  id: "final_report_created",
496
500
  label: "Final report created",
497
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "issue_metadata_exists", "active_cycle_exists", "automated_checks_passed", "deep_ui_check_satisfied", "active_cycle_user_check_passed", "accepted_changes_committed", "blueprint_update_satisfied"]
501
+ preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "issue_metadata_exists", "active_cycle_exists", "automated_checks_passed", "deep_ui_check_satisfied", "active_cycle_user_check_passed", "blueprint_update_satisfied", "accepted_changes_committed"]
498
502
  }),
499
503
  defineStep({
500
504
  buttonLabel: "Push branch and create PR",
@@ -379,7 +379,7 @@ async function assertAcceptedChangesCommitted(paths) {
379
379
  ok: false,
380
380
  error: createError({
381
381
  code: "accepted_changes_not_committed",
382
- message: "Accepted changes must be committed before app memory and finalization steps continue.",
382
+ message: "Accepted changes must be committed before finalization steps continue.",
383
383
  repairCommand: jskitCommand(`session ${paths.sessionId} step`)
384
384
  }),
385
385
  precondition: createPrecondition({
@@ -56,6 +56,19 @@ function createPrecondition({
56
56
  });
57
57
  }
58
58
 
59
+ function createWarning({
60
+ code,
61
+ message,
62
+ repairCommand = ""
63
+ }) {
64
+ return createError({ code, message, repairCommand });
65
+ }
66
+
67
+ const ACCEPTED_CHANGES_NOOP_WARNING = createWarning({
68
+ code: "accepted_changes_noop",
69
+ message: "No accepted worktree changes were found; continuing without a new commit."
70
+ });
71
+
59
72
  function normalizeStepId(stepId) {
60
73
  return normalizeText(stepId);
61
74
  }
@@ -530,6 +543,43 @@ function cloneContractValue(value) {
530
543
  );
531
544
  }
532
545
 
546
+ function normalizeWarning(warning) {
547
+ if (typeof warning === "string") {
548
+ return createWarning({
549
+ code: "session_warning",
550
+ message: warning
551
+ });
552
+ }
553
+ if (!warning || typeof warning !== "object" || Array.isArray(warning)) {
554
+ return null;
555
+ }
556
+ return createWarning({
557
+ code: warning.code || "session_warning",
558
+ message: warning.message || "",
559
+ repairCommand: warning.repairCommand || ""
560
+ });
561
+ }
562
+
563
+ function mergeWarnings(...warningLists) {
564
+ const merged = [];
565
+ const seen = new Set();
566
+ for (const warnings of warningLists) {
567
+ for (const warning of Array.isArray(warnings) ? warnings : []) {
568
+ const normalized = normalizeWarning(warning);
569
+ if (!normalized?.message) {
570
+ continue;
571
+ }
572
+ const key = `${normalized.code}\n${normalized.message}`;
573
+ if (seen.has(key)) {
574
+ continue;
575
+ }
576
+ seen.add(key);
577
+ merged.push(normalized);
578
+ }
579
+ }
580
+ return merged;
581
+ }
582
+
533
583
  async function publicCodexContract(codex = null) {
534
584
  if (!codex || typeof codex !== "object" || Array.isArray(codex)) {
535
585
  return null;
@@ -731,7 +781,7 @@ function buildCurrentStepAction(stepId, artifacts = {}) {
731
781
  })();
732
782
  const dynamicDescription = (() => {
733
783
  if (step.id === "plan_executed" && planExecutionPrompted && !planExecutionSubmitted) {
734
- return "Codex has the execution prompt. Studio advances when Codex finishes.";
784
+ return "Codex has the execution prompt. Review the result, then use Go to next step when ready.";
735
785
  }
736
786
  if (step.id === "deep_ui_check_run" && deepUiCheckPrompted) {
737
787
  return "Codex has the Deep UI check prompt. Studio advances when Codex finishes.";
@@ -814,7 +864,8 @@ async function readSessionArtifacts(paths) {
814
864
  issueMetadataText,
815
865
  planExecutionReceipt,
816
866
  prOutcomeText,
817
- mainCheckoutSyncText
867
+ mainCheckoutSyncText,
868
+ changesCommittedText
818
869
  ] = await Promise.all([
819
870
  readTrimmedFile(path.join(paths.sessionRoot, "status")),
820
871
  readTrimmedFile(path.join(paths.sessionRoot, "current_step")),
@@ -834,7 +885,8 @@ async function readSessionArtifacts(paths) {
834
885
  readTextIfExists(path.join(paths.sessionRoot, "issue_metadata.json")),
835
886
  readTextIfExists(path.join(cycleStepsRoot(paths, activeCycle), "plan_executed")),
836
887
  readTextIfExists(path.join(paths.sessionRoot, "pr_outcome.json")),
837
- readTextIfExists(path.join(paths.sessionRoot, "main_checkout_sync.json"))
888
+ readTextIfExists(path.join(paths.sessionRoot, "main_checkout_sync.json")),
889
+ readTextIfExists(path.join(paths.sessionRoot, "changes_committed.json"))
838
890
  ]);
839
891
  let issueMetadata = null;
840
892
  if (issueMetadataText) {
@@ -871,6 +923,18 @@ async function readSessionArtifacts(paths) {
871
923
  mainCheckoutSync = null;
872
924
  }
873
925
  }
926
+ let acceptedChangesCommit = null;
927
+ if (changesCommittedText) {
928
+ try {
929
+ const parsed = JSON.parse(changesCommittedText);
930
+ acceptedChangesCommit = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
931
+ } catch {
932
+ acceptedChangesCommit = null;
933
+ }
934
+ }
935
+ const warnings = acceptedChangesCommit?.noChanges === true
936
+ ? [ACCEPTED_CHANGES_NOOP_WARNING]
937
+ : [];
874
938
  const cycles = await readCycles(paths, activeCycle);
875
939
  const checks = await readStructuredChecks(paths);
876
940
  const uiChecks = await readStructuredUiChecks(paths);
@@ -960,6 +1024,7 @@ async function readSessionArtifacts(paths) {
960
1024
  finalReportText: finalReportText.trim(),
961
1025
  prompt: prompt.trim(),
962
1026
  status: status || SESSION_STATUS.PENDING,
1027
+ warnings,
963
1028
  workflowVersion,
964
1029
  worktreeReady,
965
1030
  worktreeStatus
@@ -980,7 +1045,8 @@ async function buildSessionResponse(paths, {
980
1045
  errors = [],
981
1046
  preconditions = [],
982
1047
  prompt = undefined,
983
- status = undefined
1048
+ status = undefined,
1049
+ warnings = []
984
1050
  } = {}) {
985
1051
  const responsePaths = paths.sessionId ? await pathsForExistingSession(paths) : paths;
986
1052
  const artifacts = responsePaths.sessionRoot ? await readSessionArtifacts(responsePaths) : {};
@@ -1039,6 +1105,7 @@ async function buildSessionResponse(paths, {
1039
1105
  message: `Session ${paths.sessionId || ""} uses workflow version ${artifacts.workflowVersion || "missing"}, but this JSKIT runtime expects ${SESSION_WORKFLOW_VERSION}.`
1040
1106
  })
1041
1107
  ],
1108
+ warnings: [],
1042
1109
  archive: responsePaths.archive || "active",
1043
1110
  sessionRoot: responsePaths.sessionRoot || "",
1044
1111
  worktree: paths.worktree || "",
@@ -1052,6 +1119,7 @@ async function buildSessionResponse(paths, {
1052
1119
  const responsePrompt = typeof prompt === "string"
1053
1120
  ? prompt
1054
1121
  : stepCanExposeStoredPrompt(currentStep) ? artifacts.prompt || "" : "";
1122
+ const responseWarnings = mergeWarnings(artifacts.warnings || [], warnings);
1055
1123
 
1056
1124
  return {
1057
1125
  ok: ok === true,
@@ -1101,6 +1169,7 @@ async function buildSessionResponse(paths, {
1101
1169
  mainCheckoutSync: cloneContractValue(artifacts.mainCheckoutSync || null),
1102
1170
  preconditions,
1103
1171
  errors,
1172
+ warnings: responseWarnings,
1104
1173
  archive: responsePaths.archive || (resolvedStatus === SESSION_STATUS.FINISHED ? "completed" : resolvedStatus === SESSION_STATUS.ABANDONED ? "abandoned" : "active"),
1105
1174
  sessionRoot: responsePaths.sessionRoot || "",
1106
1175
  worktree: paths.worktree || "",
@@ -1179,6 +1248,7 @@ function buildSessionErrorResponse({
1179
1248
  prOutcome: null,
1180
1249
  preconditions,
1181
1250
  errors: errorList,
1251
+ warnings: [],
1182
1252
  archive: "",
1183
1253
  sessionRoot: "",
1184
1254
  worktree: "",
@@ -1,3 +1,6 @@
1
+ import {
2
+ createHash
3
+ } from "node:crypto";
1
4
  import {
2
5
  appendFile,
3
6
  mkdir,
@@ -1581,6 +1584,7 @@ const STEP_CANCELERS = Object.freeze({
1581
1584
  blueprint_updated: async (paths) => {
1582
1585
  await Promise.all([
1583
1586
  removePromptArtifact(paths, "update_blueprint.md"),
1587
+ removeSessionRootFile(paths, BLUEPRINT_BASELINE_FILE),
1584
1588
  removeGlobalCodexResult(paths, "blueprint_updated")
1585
1589
  ]);
1586
1590
  },
@@ -1830,6 +1834,79 @@ async function changedFilesInWorktree(paths) {
1830
1834
  ]);
1831
1835
  }
1832
1836
 
1837
+ const BLUEPRINT_RELATIVE_PATH = ".jskit/APP_BLUEPRINT.md";
1838
+ const BLUEPRINT_BASELINE_FILE = "blueprint_update_baseline.json";
1839
+
1840
+ function blueprintBaselinePath(paths) {
1841
+ return path.join(paths.sessionRoot, BLUEPRINT_BASELINE_FILE);
1842
+ }
1843
+
1844
+ function isBlueprintRelativePath(filePath = "") {
1845
+ return normalizeText(filePath) === BLUEPRINT_RELATIVE_PATH;
1846
+ }
1847
+
1848
+ function nonBlueprintChangedFiles(files = []) {
1849
+ return files.filter((file) => !isBlueprintRelativePath(file));
1850
+ }
1851
+
1852
+ async function hashWorktreeFile(paths, filePath) {
1853
+ try {
1854
+ const buffer = await readFile(path.join(paths.worktree, filePath));
1855
+ return createHash("sha256").update(buffer).digest("hex");
1856
+ } catch {
1857
+ return "missing";
1858
+ }
1859
+ }
1860
+
1861
+ async function buildDirtyFileSnapshot(paths, files = []) {
1862
+ const entries = await Promise.all(nonBlueprintChangedFiles(files).map(async (file) => [
1863
+ file,
1864
+ await hashWorktreeFile(paths, file)
1865
+ ]));
1866
+ return Object.fromEntries(entries);
1867
+ }
1868
+
1869
+ async function writeBlueprintBaseline(paths) {
1870
+ const changedFiles = await changedFilesInWorktree(paths);
1871
+ const snapshot = await buildDirtyFileSnapshot(paths, changedFiles);
1872
+ const payload = {
1873
+ changedFiles: Object.keys(snapshot).sort((left, right) => left.localeCompare(right)),
1874
+ files: snapshot,
1875
+ recordedAt: timestampForReceipt()
1876
+ };
1877
+ await writeTextFile(blueprintBaselinePath(paths), `${JSON.stringify(payload, null, 2)}\n`);
1878
+ return payload;
1879
+ }
1880
+
1881
+ async function readBlueprintBaseline(paths) {
1882
+ return parseJsonObject(await readTextIfExists(blueprintBaselinePath(paths))) || null;
1883
+ }
1884
+
1885
+ async function unexpectedBlueprintStepChanges(paths, changedFiles = []) {
1886
+ const baseline = await readBlueprintBaseline(paths);
1887
+ if (!baseline?.files || typeof baseline.files !== "object" || Array.isArray(baseline.files)) {
1888
+ return nonBlueprintChangedFiles(changedFiles);
1889
+ }
1890
+ const baselineFiles = baseline.files;
1891
+ const currentFiles = new Set(nonBlueprintChangedFiles(changedFiles));
1892
+ const candidates = new Set([
1893
+ ...Object.keys(baselineFiles),
1894
+ ...currentFiles
1895
+ ]);
1896
+ const unexpected = [];
1897
+ for (const file of [...candidates].sort((left, right) => left.localeCompare(right))) {
1898
+ if (!Object.prototype.hasOwnProperty.call(baselineFiles, file)) {
1899
+ unexpected.push(file);
1900
+ continue;
1901
+ }
1902
+ const currentHash = await hashWorktreeFile(paths, file);
1903
+ if (currentHash !== baselineFiles[file]) {
1904
+ unexpected.push(file);
1905
+ }
1906
+ }
1907
+ return unexpected;
1908
+ }
1909
+
1833
1910
  async function changedFilesSinceBase(paths) {
1834
1911
  const baseCommit = await readTrimmedFile(path.join(paths.sessionRoot, "base_commit"));
1835
1912
  const args = baseCommit
@@ -2178,17 +2255,10 @@ async function commitAcceptedChanges(paths, _options = {}, context = {}) {
2178
2255
 
2179
2256
  if (!commitInfo?.commit) {
2180
2257
  const result = await commitWorktree(paths, {
2258
+ allowNoChanges: true,
2181
2259
  message: `Implement JSKIT session ${paths.sessionId}`
2182
2260
  });
2183
2261
  if (!result.ok) {
2184
- if (result.output === "No changes found.") {
2185
- return failSession(paths, {
2186
- code: "accepted_changes_missing",
2187
- message: "No accepted worktree changes found to commit.",
2188
- repairCommand: `git -C ${paths.worktree} status --short`,
2189
- preconditions
2190
- });
2191
- }
2192
2262
  return failSession(paths, {
2193
2263
  code: "accepted_changes_commit_failed",
2194
2264
  message: result.output || "Failed to commit accepted changes.",
@@ -2199,15 +2269,30 @@ async function commitAcceptedChanges(paths, _options = {}, context = {}) {
2199
2269
  commitInfo = {
2200
2270
  changedFiles: result.changedFiles || [],
2201
2271
  commit: await currentHead(paths),
2202
- committedAt: timestampForReceipt()
2272
+ committedAt: timestampForReceipt(),
2273
+ noChanges: (result.changedFiles || []).length < 1
2203
2274
  };
2204
2275
  await writeTextFile(path.join(paths.sessionRoot, "changes_committed.json"), `${JSON.stringify(commitInfo, null, 2)}\n`);
2205
2276
  }
2206
2277
 
2207
- await writeReceipt(paths, "changes_committed", `Committed accepted changes at ${commitInfo.commit || "unknown"}.`);
2278
+ const warnings = [];
2279
+ if (commitInfo.noChanges === true) {
2280
+ warnings.push({
2281
+ code: "accepted_changes_noop",
2282
+ message: "No accepted worktree changes were found; continuing without a new commit."
2283
+ });
2284
+ }
2285
+ await writeReceipt(
2286
+ paths,
2287
+ "changes_committed",
2288
+ commitInfo.noChanges === true
2289
+ ? "No accepted worktree changes were found; continued without a new commit."
2290
+ : `Committed accepted changes at ${commitInfo.commit || "unknown"}.`
2291
+ );
2208
2292
  await markStatus(paths, SESSION_STATUS.RUNNING);
2209
2293
  return buildSessionResponse(paths, {
2210
- preconditions
2294
+ preconditions,
2295
+ warnings
2211
2296
  });
2212
2297
  }
2213
2298
 
@@ -2220,7 +2305,7 @@ async function updateBlueprint(paths, options = {}, context = {}) {
2220
2305
  const { planPath } = await readCurrentPlan(paths);
2221
2306
  const issueDetailsPath = path.join(paths.sessionRoot, "issue_details.md");
2222
2307
  const agentDecisionsPath = path.join(paths.sessionRoot, "agent_decisions.md");
2223
- const blueprintPath = path.join(paths.worktree, ".jskit", "APP_BLUEPRINT.md");
2308
+ const blueprintPath = path.join(paths.worktree, BLUEPRINT_RELATIVE_PATH);
2224
2309
  const blueprintPromptPath = path.join(paths.sessionRoot, "prompts", "update_blueprint.md");
2225
2310
 
2226
2311
  if (await fileExists(blueprintPromptPath)) {
@@ -2229,11 +2314,11 @@ async function updateBlueprint(paths, options = {}, context = {}) {
2229
2314
  return codexResultFailure;
2230
2315
  }
2231
2316
  const changedFiles = await changedFilesInWorktree(paths);
2232
- const unexpectedChanges = changedFiles.filter((file) => file !== ".jskit/APP_BLUEPRINT.md");
2317
+ const unexpectedChanges = await unexpectedBlueprintStepChanges(paths, changedFiles);
2233
2318
  if (unexpectedChanges.length > 0) {
2234
2319
  return failSession(paths, {
2235
2320
  code: "blueprint_unexpected_changes",
2236
- message: `The blueprint step changed files outside .jskit/APP_BLUEPRINT.md: ${unexpectedChanges.join(", ")}`,
2321
+ message: `The blueprint step changed files outside ${BLUEPRINT_RELATIVE_PATH}: ${unexpectedChanges.join(", ")}`,
2237
2322
  repairCommand: `git -C ${paths.worktree} status --short`,
2238
2323
  preconditions
2239
2324
  });
@@ -2249,30 +2334,8 @@ async function updateBlueprint(paths, options = {}, context = {}) {
2249
2334
  });
2250
2335
  }
2251
2336
 
2252
- if (changedFiles.includes(".jskit/APP_BLUEPRINT.md")) {
2253
- const addResult = await runGitInWorktree(paths.worktree, ["add", ".jskit/APP_BLUEPRINT.md"], {
2254
- timeout: 15000
2255
- });
2256
- if (!addResult.ok) {
2257
- return failSession(paths, {
2258
- code: "blueprint_stage_failed",
2259
- message: addResult.output || "Failed to stage app blueprint update.",
2260
- repairCommand: `git -C ${paths.worktree} add .jskit/APP_BLUEPRINT.md`,
2261
- preconditions
2262
- });
2263
- }
2264
- const commitResult = await runGitInWorktree(paths.worktree, ["commit", "-m", `Update app blueprint for ${paths.sessionId}`], {
2265
- timeout: 1000 * 60
2266
- });
2267
- if (!commitResult.ok) {
2268
- return failSession(paths, {
2269
- code: "blueprint_commit_failed",
2270
- message: commitResult.output || "Failed to commit app blueprint update.",
2271
- repairCommand: `git -C ${paths.worktree} status --short`,
2272
- preconditions
2273
- });
2274
- }
2275
- await writeReceipt(paths, "blueprint_updated", "Codex updated and JSKIT committed the app blueprint.");
2337
+ if (changedFiles.includes(BLUEPRINT_RELATIVE_PATH)) {
2338
+ await writeReceipt(paths, "blueprint_updated", "Codex updated the app blueprint; JSKIT will include it in the accepted changes commit.");
2276
2339
  } else {
2277
2340
  await writeReceipt(paths, "blueprint_updated", "Codex reviewed the app blueprint; no blueprint changes were needed.");
2278
2341
  }
@@ -2283,6 +2346,7 @@ async function updateBlueprint(paths, options = {}, context = {}) {
2283
2346
  });
2284
2347
  }
2285
2348
 
2349
+ await writeBlueprintBaseline(paths);
2286
2350
  const prompt = await renderPrompt(paths, "update_blueprint.md", {
2287
2351
  agent_decisions_file: agentDecisionsPath,
2288
2352
  app_blueprint_file: blueprintPath,