@gajae-code/coding-agent 0.18.7 → 0.18.8

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.
Files changed (279) hide show
  1. package/CHANGELOG.md +279 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +2 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  15. package/dist/types/cursor-delete-path.d.ts +1 -0
  16. package/dist/types/cursor-mcp-name.d.ts +8 -0
  17. package/dist/types/customization/inventory.d.ts +2 -0
  18. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  21. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  22. package/dist/types/discovery/helpers.d.ts +6 -1
  23. package/dist/types/export/html/css-color.d.ts +2 -0
  24. package/dist/types/export/html/template.generated.d.ts +1 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  26. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  27. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  28. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  29. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  30. package/dist/types/extensibility/skill-management.d.ts +3 -1
  31. package/dist/types/extensibility/skills.d.ts +2 -0
  32. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  33. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  34. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  36. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  37. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  38. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  39. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  40. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  41. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  42. package/dist/types/lsp/workspace-path.d.ts +1 -0
  43. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  44. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  45. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  46. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  47. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -0
  49. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  50. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  51. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  52. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  53. package/dist/types/progress/progress-contract.d.ts +154 -0
  54. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  55. package/dist/types/progress/project-progress.d.ts +141 -0
  56. package/dist/types/progress/render-progress.d.ts +28 -0
  57. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  58. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  59. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  60. package/dist/types/runtime-mcp/types.d.ts +6 -0
  61. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  62. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  63. package/dist/types/sdk/broker/broker.d.ts +18 -1
  64. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  65. package/dist/types/sdk/bus/index.d.ts +12 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  67. package/dist/types/sdk/client/client.d.ts +2 -0
  68. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  69. package/dist/types/sdk/index.d.ts +1 -0
  70. package/dist/types/sdk/router/session-router.d.ts +2 -0
  71. package/dist/types/sdk/session-list.d.ts +1 -1
  72. package/dist/types/sdk/session.d.ts +8 -0
  73. package/dist/types/session/agent-session.d.ts +38 -9
  74. package/dist/types/session/artifacts.d.ts +2 -2
  75. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  76. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  77. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  78. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  79. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  80. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  81. package/dist/types/session/session-manager.d.ts +31 -5
  82. package/dist/types/session/session-retirement.d.ts +31 -7
  83. package/dist/types/session/session-storage.d.ts +62 -2
  84. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  85. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  86. package/dist/types/setup/credential-import.d.ts +6 -4
  87. package/dist/types/skill-state/active-state.d.ts +38 -0
  88. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  89. package/dist/types/task/executor.d.ts +3 -0
  90. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  91. package/dist/types/tools/index.d.ts +3 -0
  92. package/dist/types/tools/python.d.ts +9 -1
  93. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  94. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  95. package/dist/types/tools/todo-write.d.ts +6 -0
  96. package/dist/types/utils/tools-checksum.d.ts +7 -0
  97. package/package.json +7 -7
  98. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  99. package/scripts/verify-managed-empty-stop.ts +28 -0
  100. package/src/async/job-manager.ts +228 -122
  101. package/src/capability/index.ts +17 -0
  102. package/src/capability/mcp.ts +2 -0
  103. package/src/capability/skill.ts +2 -0
  104. package/src/capability/types.ts +2 -0
  105. package/src/cli/auth-broker-cli.ts +1 -1
  106. package/src/cli/customize-doctor.ts +15 -2
  107. package/src/cli/public-command-errors.ts +21 -1
  108. package/src/cli/setup-cli.ts +4 -4
  109. package/src/cli/skills-cli.ts +5 -0
  110. package/src/commands/deep-interview.ts +47 -8
  111. package/src/commands/sdk.ts +28 -8
  112. package/src/config/autorouting-tier-map.ts +117 -7
  113. package/src/config/file-lock.ts +307 -68
  114. package/src/config/mcp-schema.json +4 -0
  115. package/src/config/model-bindings-applier.ts +20 -0
  116. package/src/config/model-profile-activation.ts +240 -46
  117. package/src/config/model-registry.ts +47 -153
  118. package/src/config/skill-settings-defaults.ts +7 -0
  119. package/src/coordinator-mcp/policy.ts +3 -2
  120. package/src/cursor-delete-path.ts +10 -0
  121. package/src/cursor-mcp-name.ts +15 -0
  122. package/src/cursor.ts +13 -2
  123. package/src/customization/inventory.ts +3 -0
  124. package/src/debug/report-bundle-parent.ts +22 -0
  125. package/src/debug/report-bundle.ts +3 -0
  126. package/src/debug/system-info.ts +13 -1
  127. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  128. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  129. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  130. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  131. package/src/defaults/gjc-defaults.test.ts +18 -1
  132. package/src/defaults/gjc-defaults.ts +3 -0
  133. package/src/defaults/gjc-skills.generated.ts +58 -36
  134. package/src/discovery/agents.ts +6 -1
  135. package/src/discovery/builtin.ts +10 -0
  136. package/src/discovery/helpers.ts +159 -20
  137. package/src/discovery/mcp-json.ts +6 -0
  138. package/src/discovery/ssh.ts +1 -1
  139. package/src/discovery/substitute-plugin-root.ts +3 -1
  140. package/src/edit/file-read-cache.ts +1 -1
  141. package/src/eval/py/executor.ts +380 -120
  142. package/src/export/html/css-color.ts +9 -0
  143. package/src/export/html/index.ts +5 -4
  144. package/src/export/html/template.generated.ts +1 -1
  145. package/src/extensibility/extensions/runner.ts +4 -0
  146. package/src/extensibility/extensions/types.ts +7 -0
  147. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  148. package/src/extensibility/plugins/manager.ts +3 -2
  149. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  150. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  151. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  152. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  153. package/src/extensibility/skill-management.ts +6 -2
  154. package/src/extensibility/skills.ts +6 -1
  155. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  156. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  157. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  158. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  159. package/src/gjc-runtime/gc-runtime.ts +206 -9
  160. package/src/gjc-runtime/launch-worktree.ts +14 -4
  161. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  162. package/src/gjc-runtime/state-argv.ts +2 -0
  163. package/src/gjc-runtime/state-migrations.ts +91 -29
  164. package/src/gjc-runtime/state-runtime.ts +3716 -135
  165. package/src/gjc-runtime/state-writer.ts +83 -8
  166. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  167. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  168. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  169. package/src/harness-control-plane/operate.ts +12 -4
  170. package/src/harness-control-plane/owner.ts +5 -1
  171. package/src/hashline/parser.ts +48 -9
  172. package/src/hashline/recovery.ts +13 -4
  173. package/src/internal-urls/docs-index.generated.ts +18 -17
  174. package/src/internal-urls/skill-path.ts +19 -0
  175. package/src/internal-urls/skill-protocol.ts +2 -0
  176. package/src/lsp/edits.ts +6 -0
  177. package/src/lsp/workspace-path.ts +51 -0
  178. package/src/main.ts +195 -31
  179. package/src/modes/acp/acp-agent.ts +88 -4
  180. package/src/modes/components/custom-editor.ts +14 -0
  181. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  182. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  183. package/src/modes/components/extensions/state-manager.ts +15 -5
  184. package/src/modes/components/model-selector.ts +1 -4
  185. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  186. package/src/modes/components/status-line/segments.ts +4 -0
  187. package/src/modes/controllers/event-controller.ts +11 -4
  188. package/src/modes/controllers/input-controller.ts +74 -29
  189. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  190. package/src/modes/controllers/selector-controller.ts +28 -15
  191. package/src/modes/interactive-mode.ts +4 -0
  192. package/src/modes/runtime-init.ts +4 -0
  193. package/src/modes/utils/abort-message.ts +3 -1
  194. package/src/modes/utils/interrupt-ownership.ts +12 -0
  195. package/src/modes/utils/keybinding-matchers.ts +4 -0
  196. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  197. package/src/progress/collect-project-progress.ts +153 -0
  198. package/src/progress/progress-contract.ts +172 -0
  199. package/src/progress/progress-snapshot.ts +140 -0
  200. package/src/progress/project-progress.ts +446 -0
  201. package/src/progress/render-progress.ts +209 -0
  202. package/src/prompts/system/master-mode.md +1 -0
  203. package/src/prompts/tools/skill-discovery.md +1 -1
  204. package/src/runtime-mcp/config-writer.ts +10 -6
  205. package/src/runtime-mcp/config.ts +1 -0
  206. package/src/runtime-mcp/oauth-flow.ts +5 -0
  207. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  208. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  209. package/src/runtime-mcp/smithery-registry.ts +22 -3
  210. package/src/runtime-mcp/transports/http.ts +2 -2
  211. package/src/runtime-mcp/types.ts +6 -0
  212. package/src/sdk/acp/adapter.ts +93 -8
  213. package/src/sdk/broker/broker-exit.ts +52 -23
  214. package/src/sdk/broker/broker.ts +75 -6
  215. package/src/sdk/broker/ensure.ts +2 -0
  216. package/src/sdk/broker/lifecycle.ts +711 -66
  217. package/src/sdk/broker/session-index.ts +191 -81
  218. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  219. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  220. package/src/sdk/bus/index.ts +238 -43
  221. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  222. package/src/sdk/cli/session-cli.ts +17 -3
  223. package/src/sdk/client/client.ts +28 -8
  224. package/src/sdk/host/query/handlers.ts +9 -1
  225. package/src/sdk/host/session-runtime.test.ts +2647 -294
  226. package/src/sdk/host/session-runtime.ts +426 -109
  227. package/src/sdk/host/surface-policy.ts +1 -0
  228. package/src/sdk/index.ts +8 -0
  229. package/src/sdk/lifecycle-session.ts +3 -1
  230. package/src/sdk/prompt-deadline-manager.ts +15 -0
  231. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  232. package/src/sdk/protocol/operation-registry.ts +9 -2
  233. package/src/sdk/router/session-router.ts +75 -12
  234. package/src/sdk/session-list.ts +1 -0
  235. package/src/sdk/session.ts +67 -38
  236. package/src/session/agent-session.ts +623 -224
  237. package/src/session/artifacts.ts +174 -70
  238. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  239. package/src/session/internal/managed-session-scope.ts +3457 -462
  240. package/src/session/internal/managed-session-storage.ts +376 -61
  241. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  242. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  243. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  244. package/src/session/session-manager.ts +939 -133
  245. package/src/session/session-retirement.ts +573 -14
  246. package/src/session/session-storage.ts +417 -14
  247. package/src/session/task-artifact-owner-codec.ts +71 -8
  248. package/src/session/task-artifact-owner-retirement.ts +21 -19
  249. package/src/setup/credential-auto-import.ts +12 -3
  250. package/src/setup/credential-import.ts +176 -5
  251. package/src/setup/paseo/announce.ts +6 -1
  252. package/src/skill-state/active-state.ts +132 -2
  253. package/src/slash-commands/builtin-registry.ts +39 -9
  254. package/src/slash-commands/helpers/progress-report.ts +23 -0
  255. package/src/stt/setup.ts +3 -5
  256. package/src/task/commands.ts +3 -1
  257. package/src/task/executor.ts +4 -0
  258. package/src/task/index.ts +6 -0
  259. package/src/tools/ask.ts +205 -6
  260. package/src/tools/ast-edit.ts +4 -0
  261. package/src/tools/bash-allowed-prefixes.ts +15 -0
  262. package/src/tools/bash-skill-urls.ts +56 -3
  263. package/src/tools/bash.ts +48 -79
  264. package/src/tools/descriptors.ts +5 -1
  265. package/src/tools/eval.ts +56 -36
  266. package/src/tools/image-gen.ts +1 -1
  267. package/src/tools/index.ts +3 -0
  268. package/src/tools/python.ts +187 -66
  269. package/src/tools/recipe/index.ts +9 -0
  270. package/src/tools/recipe/recipe-guard.ts +19 -0
  271. package/src/tools/restricted-cwd.ts +10 -0
  272. package/src/tools/skill-discovery.ts +5 -0
  273. package/src/tools/skill.ts +28 -1
  274. package/src/tools/todo-write.ts +10 -1
  275. package/src/tools/tool-catalog.generated.ts +1 -1
  276. package/src/tools/write.ts +1 -0
  277. package/src/utils/tools-checksum.ts +34 -0
  278. package/src/utils/tools-manager.ts +24 -6
  279. package/src/web/search/providers/codex.ts +3 -2
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import type * as nodeFs from "node:fs";
2
3
  import { constants as fsConstants, type Stats } from "node:fs";
3
4
  import type { FileHandle } from "node:fs/promises";
4
5
  import * as fs from "node:fs/promises";
@@ -89,6 +90,7 @@ export interface WorkflowTransactionJournal {
89
90
  callee?: CanonicalGjcWorkflowSkill;
90
91
  paths: string[];
91
92
  steps: string[];
93
+ approval_audit_offset?: number;
92
94
  }
93
95
 
94
96
  export type StateWritePolicy = "source" | "cache";
@@ -1368,9 +1370,16 @@ export async function removeActiveEntry(
1368
1370
  );
1369
1371
  }
1370
1372
 
1371
- export async function readActiveEntries(
1373
+ /**
1374
+ * Read every per-skill active entry. Unreadable JSON propagates; a parsed record
1375
+ * that is not a skill entry (not an object, or without a `skill`) is skipped, or
1376
+ * rejected when `strict` is set so status-bearing readers can tell an existing
1377
+ * but unusable authoritative entry apart from a deleted one.
1378
+ */
1379
+ async function readActiveEntriesFromDir(
1372
1380
  cwd: string,
1373
- sessionScope?: string | ActiveSessionScope,
1381
+ sessionScope: string | ActiveSessionScope | undefined,
1382
+ strict: boolean,
1374
1383
  ): Promise<SkillActiveEntry[]> {
1375
1384
  const dir = activeStateDir(path.resolve(cwd), sessionScope);
1376
1385
  let names: string[];
@@ -1383,15 +1392,38 @@ export async function readActiveEntries(
1383
1392
  const entries: SkillActiveEntry[] = [];
1384
1393
  for (const name of names.sort()) {
1385
1394
  if (!name.endsWith(".json")) continue;
1386
- const raw = await readJsonIfPresent(path.join(dir, name));
1387
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
1388
- const skill = safeString((raw as SkillActiveEntry).skill).trim();
1389
- if (!skill) continue;
1395
+ const filePath = path.join(dir, name);
1396
+ const raw = await readJsonIfPresent(filePath);
1397
+ if (raw === undefined) continue;
1398
+ const skill = isPlainObject(raw) ? safeString(raw.skill).trim() : "";
1399
+ if (!skill) {
1400
+ if (strict) throw new Error(`active entry at ${filePath} is not a skill entry record`);
1401
+ continue;
1402
+ }
1390
1403
  entries.push(raw as SkillActiveEntry);
1391
1404
  }
1392
1405
  return entries;
1393
1406
  }
1394
1407
 
1408
+ export async function readActiveEntries(
1409
+ cwd: string,
1410
+ sessionScope?: string | ActiveSessionScope,
1411
+ ): Promise<SkillActiveEntry[]> {
1412
+ return readActiveEntriesFromDir(cwd, sessionScope, false);
1413
+ }
1414
+
1415
+ /**
1416
+ * Failure-observable counterpart of `readActiveEntries`: an entry file that exists
1417
+ * but holds no usable skill entry throws instead of being skipped, so its absence
1418
+ * from the result always means the entry does not exist.
1419
+ */
1420
+ export async function readActiveEntriesStrict(
1421
+ cwd: string,
1422
+ sessionScope?: string | ActiveSessionScope,
1423
+ ): Promise<SkillActiveEntry[]> {
1424
+ return readActiveEntriesFromDir(cwd, sessionScope, true);
1425
+ }
1426
+
1395
1427
  export async function rebuildActiveSnapshot(
1396
1428
  cwd: string,
1397
1429
  sessionScope?: string | ActiveSessionScope,
@@ -1559,6 +1591,7 @@ export async function appendAuditEntry(
1559
1591
  cwd: string,
1560
1592
  sessionIdOrEntry: string | AuditEntry,
1561
1593
  maybeEntry?: AuditEntry,
1594
+ options: { lockHeld?: boolean; beforeAppend?: (offset: number) => Promise<unknown> } = {},
1562
1595
  ): Promise<string> {
1563
1596
  const sessionId =
1564
1597
  typeof sessionIdOrEntry === "string"
@@ -1568,8 +1601,50 @@ export async function appendAuditEntry(
1568
1601
  const entry = typeof sessionIdOrEntry === "string" ? maybeEntry : sessionIdOrEntry;
1569
1602
  if (!entry) throw new Error("audit entry is required");
1570
1603
  const filePath = resolveGjcTarget(layoutAuditPath(cwd, sessionId), cwd);
1571
- await fs.mkdir(path.dirname(filePath), { recursive: true });
1572
- await fs.appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf-8");
1604
+ const append = async () => {
1605
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
1606
+ let initialStat: nodeFs.BigIntStats | undefined;
1607
+ try {
1608
+ initialStat = await fs.lstat(filePath, { bigint: true });
1609
+ if (initialStat.isSymbolicLink() || !initialStat.isFile()) throw new Error("audit path is not a regular file");
1610
+ } catch (error) {
1611
+ if (!isErrno(error, "ENOENT")) throw error;
1612
+ }
1613
+ const flags = initialStat
1614
+ ? fsConstants.O_WRONLY |
1615
+ fsConstants.O_APPEND |
1616
+ (process.platform === "win32" ? 0 : (fsConstants.O_NOFOLLOW ?? 0))
1617
+ : fsConstants.O_WRONLY | fsConstants.O_APPEND | fsConstants.O_CREAT | fsConstants.O_EXCL;
1618
+ let handle: fs.FileHandle | undefined;
1619
+ try {
1620
+ handle = await fs.open(filePath, flags, 0o600);
1621
+ const openedStat = await handle.stat({ bigint: true });
1622
+ const pathStat = await fs.lstat(filePath, { bigint: true });
1623
+ const sameObject = (left: nodeFs.BigIntStats, right: nodeFs.BigIntStats) =>
1624
+ left.dev === right.dev && left.ino === right.ino && left.nlink === right.nlink;
1625
+ if (
1626
+ openedStat.isSymbolicLink() ||
1627
+ !openedStat.isFile() ||
1628
+ pathStat.isSymbolicLink() ||
1629
+ !pathStat.isFile() ||
1630
+ (initialStat !== undefined && !sameObject(initialStat, openedStat)) ||
1631
+ !sameObject(openedStat, pathStat)
1632
+ )
1633
+ throw new Error("audit path identity changed before append");
1634
+ if (openedStat.size > BigInt(Number.MAX_SAFE_INTEGER))
1635
+ throw new Error("audit path is too large to append safely");
1636
+ await options.beforeAppend?.(Number(openedStat.size));
1637
+ await handle.writeFile(`${JSON.stringify(entry)}\n`, "utf-8");
1638
+ await handle.sync();
1639
+ const afterPathStat = await fs.lstat(filePath, { bigint: true });
1640
+ if (afterPathStat.isSymbolicLink() || !sameObject(openedStat, afterPathStat))
1641
+ throw new Error("audit path identity changed during append");
1642
+ } finally {
1643
+ await handle?.close();
1644
+ }
1645
+ };
1646
+ if (options.lockHeld) await append();
1647
+ else await withWorkflowStateLock(filePath, append, { cwd });
1573
1648
  return filePath;
1574
1649
  }
1575
1650
 
@@ -1275,13 +1275,25 @@ export async function createUltragoalPlan(input: {
1275
1275
  return plan;
1276
1276
  }
1277
1277
 
1278
- function chooseNextGoal(plan: UltragoalPlan, retryFailed: boolean): UltragoalGoal | undefined {
1278
+ /**
1279
+ * Scheduler order shared with read-only projections: active, then pending, then failed
1280
+ * only when the caller explicitly opts into retrying failed goals. Blocked,
1281
+ * review_blocked, complete, and superseded goals are never schedulable.
1282
+ */
1283
+ export function selectNextSchedulableGoal<T extends { status: UltragoalGoalStatus }>(
1284
+ goals: readonly T[],
1285
+ retryFailed: boolean,
1286
+ ): T | undefined {
1279
1287
  return (
1280
- plan.goals.find(goal => goal.status === "active") ??
1281
- plan.goals.find(goal => goal.status === "pending") ??
1282
- (retryFailed ? plan.goals.find(goal => goal.status === "failed") : undefined)
1288
+ goals.find(goal => goal.status === "active") ??
1289
+ goals.find(goal => goal.status === "pending") ??
1290
+ (retryFailed ? goals.find(goal => goal.status === "failed") : undefined)
1283
1291
  );
1284
1292
  }
1293
+
1294
+ function chooseNextGoal(plan: UltragoalPlan, retryFailed: boolean): UltragoalGoal | undefined {
1295
+ return selectNextSchedulableGoal(plan.goals, retryFailed);
1296
+ }
1285
1297
  export interface UltragoalRunCompletionState {
1286
1298
  requiredGoals: UltragoalGoal[];
1287
1299
  incompleteGoals: UltragoalGoal[];
@@ -174,16 +174,7 @@
174
174
  "clear",
175
175
  "contract",
176
176
  "handoff",
177
- "doctor",
178
- "kickoff",
179
- "write-spec",
180
- "write-artifact",
181
- "stage",
182
- "check",
183
- "apply",
184
- "discard",
185
- "read",
186
- "write"
177
+ "doctor"
187
178
  ],
188
179
  "name": "session-id",
189
180
  "type": "string"
@@ -414,6 +405,11 @@
414
405
  "to": "handoff",
415
406
  "verb": "write-spec"
416
407
  },
408
+ {
409
+ "from": "interviewing",
410
+ "to": "handoff",
411
+ "verb": "crystallize"
412
+ },
417
413
  {
418
414
  "from": "handoff",
419
415
  "to": "complete",
@@ -426,14 +422,6 @@
426
422
  }
427
423
  ],
428
424
  "typedArgs": [
429
- {
430
- "appliesToVerbs": [
431
- "write",
432
- "api"
433
- ],
434
- "name": "input",
435
- "type": "string"
436
- },
437
425
  {
438
426
  "appliesToVerbs": [
439
427
  "read",
@@ -462,13 +450,10 @@
462
450
  "doctor",
463
451
  "kickoff",
464
452
  "write-spec",
465
- "write-artifact",
466
453
  "stage",
467
454
  "check",
468
455
  "apply",
469
- "discard",
470
- "read",
471
- "write"
456
+ "discard"
472
457
  ],
473
458
  "name": "session-id",
474
459
  "type": "string"
@@ -556,6 +541,13 @@
556
541
  "name": "deep",
557
542
  "type": "boolean"
558
543
  },
544
+ {
545
+ "appliesToVerbs": [
546
+ "kickoff"
547
+ ],
548
+ "name": "trace",
549
+ "type": "boolean"
550
+ },
559
551
  {
560
552
  "appliesToVerbs": [
561
553
  "kickoff"
@@ -587,6 +579,14 @@
587
579
  "name": "slug",
588
580
  "type": "string"
589
581
  },
582
+ {
583
+ "appliesToVerbs": [
584
+ "crystallize"
585
+ ],
586
+ "name": "slug",
587
+ "required": true,
588
+ "type": "string"
589
+ },
590
590
  {
591
591
  "appliesToVerbs": [
592
592
  "write-spec"
@@ -614,7 +614,16 @@
614
614
  },
615
615
  {
616
616
  "appliesToVerbs": [
617
+ "write-spec"
618
+ ],
619
+ "name": "force",
620
+ "type": "boolean"
621
+ },
622
+ {
623
+ "appliesToVerbs": [
624
+ "kickoff",
617
625
  "write-spec",
626
+ "crystallize",
618
627
  "stage",
619
628
  "check",
620
629
  "apply",
@@ -622,20 +631,47 @@
622
631
  "read",
623
632
  "write",
624
633
  "clear",
625
- "handoff"
634
+ "handoff",
635
+ "approve-execution"
626
636
  ],
627
637
  "name": "json",
628
638
  "type": "boolean"
629
639
  },
630
640
  {
631
641
  "appliesToVerbs": [
642
+ "api"
643
+ ],
644
+ "name": "input",
645
+ "type": "string"
646
+ },
647
+ {
648
+ "appliesToVerbs": [
649
+ "write",
632
650
  "stage",
633
- "write"
651
+ "crystallize"
634
652
  ],
635
653
  "name": "input",
636
654
  "required": true,
637
655
  "type": "string"
638
656
  },
657
+ {
658
+ "appliesToVerbs": [
659
+ "approve-execution"
660
+ ],
661
+ "enumValues": [
662
+ "deep-interview"
663
+ ],
664
+ "name": "mode",
665
+ "type": "enum"
666
+ },
667
+ {
668
+ "appliesToVerbs": [
669
+ "crystallize",
670
+ "approve-execution"
671
+ ],
672
+ "name": "session-id",
673
+ "type": "string"
674
+ },
639
675
  {
640
676
  "appliesToVerbs": [
641
677
  "write"
@@ -701,6 +737,10 @@
701
737
  "name": "write-spec",
702
738
  "surface": "command-flag"
703
739
  },
740
+ {
741
+ "name": "crystallize",
742
+ "surface": "command-flag"
743
+ },
704
744
  {
705
745
  "name": "stage",
706
746
  "surface": "command-positional"
@@ -733,6 +773,10 @@
733
773
  "name": "handoff",
734
774
  "surface": "command-positional"
735
775
  },
776
+ {
777
+ "name": "approve-execution",
778
+ "surface": "command-positional"
779
+ },
736
780
  {
737
781
  "name": "graph",
738
782
  "planned": true,
@@ -1017,14 +1061,7 @@
1017
1061
  "handoff",
1018
1062
  "doctor",
1019
1063
  "kickoff",
1020
- "write-spec",
1021
- "write-artifact",
1022
- "stage",
1023
- "check",
1024
- "apply",
1025
- "discard",
1026
- "read",
1027
- "write"
1064
+ "write-artifact"
1028
1065
  ],
1029
1066
  "name": "session-id",
1030
1067
  "type": "string"
@@ -1403,16 +1440,7 @@
1403
1440
  "clear",
1404
1441
  "contract",
1405
1442
  "handoff",
1406
- "doctor",
1407
- "kickoff",
1408
- "write-spec",
1409
- "write-artifact",
1410
- "stage",
1411
- "check",
1412
- "apply",
1413
- "discard",
1414
- "read",
1415
- "write"
1443
+ "doctor"
1416
1444
  ],
1417
1445
  "name": "session-id",
1418
1446
  "type": "string"
@@ -70,22 +70,16 @@ const PLANNED_ADMIN_VERBS = ["graph", "prune", "migrate", "force-overwrite"] as
70
70
 
71
71
  const COMMON_TYPED_ARGS: TypedArgSpec[] = [
72
72
  { name: "input", type: "string", appliesToVerbs: ["write", "api"] },
73
- { name: "mode", type: "enum", enumValues: [...CANONICAL_GJC_WORKFLOW_SKILLS], appliesToVerbs: [...STATE_VERBS] },
73
+ {
74
+ name: "mode",
75
+ type: "enum",
76
+ enumValues: [...CANONICAL_GJC_WORKFLOW_SKILLS],
77
+ appliesToVerbs: [...STATE_VERBS],
78
+ },
74
79
  {
75
80
  name: "session-id",
76
81
  type: "string",
77
- appliesToVerbs: [
78
- ...STATE_VERBS,
79
- "kickoff",
80
- "write-spec",
81
- "write-artifact",
82
- "stage",
83
- "check",
84
- "apply",
85
- "discard",
86
- "read",
87
- "write",
88
- ],
82
+ appliesToVerbs: [...STATE_VERBS, "kickoff", "write-spec", "write-artifact", "stage", "check", "apply", "discard"],
89
83
  },
90
84
  { name: "thread-id", type: "string", appliesToVerbs: ["write", "clear", "handoff"] },
91
85
  { name: "turn-id", type: "string", appliesToVerbs: ["write", "clear", "handoff"] },
@@ -137,9 +131,18 @@ function manifest(input: {
137
131
  phaseLock?: readonly string[];
138
132
  canonicalOverrides?: readonly string[];
139
133
  initialState?: string;
134
+ commonTypedArgs?: TypedArgSpec[];
140
135
  }): SkillManifest {
141
136
  const staleInitialState = initialPhaseForSkill(input.skill);
142
137
  const initialState = input.initialState ?? staleInitialState;
138
+ const supportedVerbs = new Set([...input.verbs.map(item => item.name), "api"]);
139
+ const typedArgs = [...(input.commonTypedArgs ?? COMMON_TYPED_ARGS), ...(input.typedArgs ?? [])]
140
+ .map(argument =>
141
+ argument.appliesToVerbs === undefined
142
+ ? argument
143
+ : { ...argument, appliesToVerbs: argument.appliesToVerbs.filter(verb => supportedVerbs.has(verb)) },
144
+ )
145
+ .filter(argument => argument.appliesToVerbs === undefined || argument.appliesToVerbs.length > 0);
143
146
  return {
144
147
  skill: input.skill,
145
148
  states: input.states.map(item => state(item, initialState, input.terminalStates)),
@@ -147,7 +150,7 @@ function manifest(input: {
147
150
  terminalStates: input.terminalStates,
148
151
  transitions: input.transitions,
149
152
  verbs: input.verbs,
150
- typedArgs: [...COMMON_TYPED_ARGS, ...(input.typedArgs ?? [])],
153
+ typedArgs,
151
154
  retention: input.retention,
152
155
  hudFields: input.hudFields,
153
156
  graphLabel: input.graphLabel,
@@ -171,32 +174,63 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
171
174
  terminalStates: ["handoff", "complete"],
172
175
  transitions: [
173
176
  { from: "interviewing", to: "handoff", verb: "write-spec" },
177
+ { from: "interviewing", to: "handoff", verb: "crystallize" },
174
178
  { from: "handoff", to: "complete", verb: "clear" },
175
179
  { from: "interviewing", to: "complete", verb: "clear" },
176
180
  ],
177
181
  verbs: [
178
182
  ...stateVerbs(),
179
- ...flagVerbs(["kickoff", "write-spec"]),
180
- ...positionalVerbs(["stage", "check", "apply", "discard", "read", "write", "clear", "handoff"]),
183
+ ...flagVerbs(["kickoff", "write-spec", "crystallize"]),
184
+ ...positionalVerbs([
185
+ "stage",
186
+ "check",
187
+ "apply",
188
+ "discard",
189
+ "read",
190
+ "write",
191
+ "clear",
192
+ "handoff",
193
+ "approve-execution",
194
+ ]),
181
195
  ...plannedVerbs(PLANNED_ADMIN_VERBS),
182
196
  ],
197
+ commonTypedArgs: COMMON_TYPED_ARGS.filter(argument => argument.name !== "input"),
183
198
  typedArgs: [
184
199
  { name: "quick", type: "boolean", appliesToVerbs: ["kickoff"] },
185
200
  { name: "standard", type: "boolean", appliesToVerbs: ["kickoff"] },
186
201
  { name: "deep", type: "boolean", appliesToVerbs: ["kickoff"] },
202
+ { name: "trace", type: "boolean", appliesToVerbs: ["kickoff"] },
187
203
  { name: "threshold", type: "number", appliesToVerbs: ["kickoff"] },
188
204
  { name: "threshold-source", type: "string", appliesToVerbs: ["kickoff"] },
189
205
  { name: "stage", type: "enum", enumValues: ["final"], appliesToVerbs: ["write-spec"] },
190
206
  { name: "slug", type: "string", appliesToVerbs: ["write-spec"] },
207
+ { name: "slug", type: "string", required: true, appliesToVerbs: ["crystallize"] },
191
208
  { name: "spec", type: "string", required: true, appliesToVerbs: ["write-spec"] },
192
209
  { name: "handoff", type: "enum", enumValues: ["ralplan"], appliesToVerbs: ["write-spec"] },
193
210
  { name: "deliberate", type: "boolean", appliesToVerbs: ["write-spec"] },
211
+ { name: "force", type: "boolean", appliesToVerbs: ["write-spec"] },
194
212
  {
195
213
  name: "json",
196
214
  type: "boolean",
197
- appliesToVerbs: ["write-spec", "stage", "check", "apply", "discard", "read", "write", "clear", "handoff"],
215
+ appliesToVerbs: [
216
+ "kickoff",
217
+ "write-spec",
218
+ "crystallize",
219
+ "stage",
220
+ "check",
221
+ "apply",
222
+ "discard",
223
+ "read",
224
+ "write",
225
+ "clear",
226
+ "handoff",
227
+ "approve-execution",
228
+ ],
198
229
  },
199
- { name: "input", type: "string", required: true, appliesToVerbs: ["stage", "write"] },
230
+ { name: "input", type: "string", appliesToVerbs: ["api"] },
231
+ { name: "input", type: "string", required: true, appliesToVerbs: ["write", "stage", "crystallize"] },
232
+ { name: "mode", type: "enum", enumValues: ["deep-interview"], appliesToVerbs: ["approve-execution"] },
233
+ { name: "session-id", type: "string", appliesToVerbs: ["crystallize", "approve-execution"] },
200
234
  { name: "reset", type: "boolean", appliesToVerbs: ["write"] },
201
235
  {
202
236
  name: "for",
@@ -523,9 +557,8 @@ export function listVerbs(skill: CanonicalGjcWorkflowSkill): string[] {
523
557
  }
524
558
 
525
559
  export function typedArgsFor(skill: CanonicalGjcWorkflowSkill, verb: string): TypedArgSpec[] {
526
- return WORKFLOW_MANIFEST[skill].typedArgs.filter(
527
- arg => arg.appliesToVerbs === undefined || arg.appliesToVerbs.includes(verb),
528
- );
560
+ const manifest = WORKFLOW_MANIFEST[skill];
561
+ return manifest.typedArgs.filter(arg => arg.appliesToVerbs === undefined || arg.appliesToVerbs.includes(verb));
529
562
  }
530
563
 
531
564
  function stableSort(value: unknown): unknown {
@@ -142,6 +142,7 @@ export async function operate(goal: string, opts: OperateOptions): Promise<Opera
142
142
  lifecycle = "finalizing";
143
143
  break;
144
144
  }
145
+ lifecycle = accepted ? "observing" : "submitted";
145
146
  continue;
146
147
  }
147
148
 
@@ -182,11 +183,18 @@ export async function operate(goal: string, opts: OperateOptions): Promise<Opera
182
183
  return { completed: false, lifecycle, iterations, classifications, vanishReceiptIds, blockers };
183
184
  }
184
185
 
185
- // B3: never finalize on loop-exhaustion — require an explicit observed completion.
186
+ // An observation budget bounds this invocation, not the work itself. Keep active
187
+ // sessions nonterminal so a caller can resume observation without treating a slow
188
+ // but healthy owner as a failure. Completion still requires an explicit signal.
186
189
  if (lifecycle !== "finalizing") {
187
- blockers.push("no-observed-completion");
188
- await emit("critical", "operate_blocked", { blockers });
189
- return { completed: false, lifecycle: "blocked", iterations, classifications, vanishReceiptIds, blockers };
190
+ await emit("info", "operate_observation_window_ended", {
191
+ lifecycle,
192
+ iterations,
193
+ lastClassification: classifications.at(-1) ?? null,
194
+ completionObserved: false,
195
+ resumeWith: "observe",
196
+ });
197
+ return { completed: false, lifecycle, iterations, classifications, vanishReceiptIds, blockers };
190
198
  }
191
199
 
192
200
  const finalize = await runFinalize({
@@ -809,7 +809,11 @@ export class RuntimeOwner {
809
809
  evidence: Record<string, unknown>,
810
810
  ): Promise<void> => {
811
811
  if (signal.aborted) return;
812
- if (kind === "operate_blocked" || kind === "operate_finalized") {
812
+ if (kind === "operate_observation_window_ended") {
813
+ const lifecycle = evidence.lifecycle === "submitted" ? "submitted" : "observing";
814
+ const persisted = await this.#persistValidationLifecycle(signal, lifecycle, []);
815
+ if (!persisted.persisted) return;
816
+ } else if (kind === "operate_blocked" || kind === "operate_finalized") {
813
817
  const lifecycle = kind === "operate_finalized" && evidence.completed === true ? "completed" : "blocked";
814
818
  const blockers = Array.isArray(evidence.blockers)
815
819
  ? evidence.blockers.filter((blocker): blocker is string => typeof blocker === "string")
@@ -2,6 +2,7 @@ import { ABORT_MARKER, ABORT_WARNING, BEGIN_PATCH_MARKER, END_PATCH_MARKER, RANG
2
2
  import {
3
3
  computeLineHash,
4
4
  describeAnchorExamples,
5
+ HL_BODY_SEP,
5
6
  HL_BODY_SEP_RE_RAW,
6
7
  HL_FILE_PREFIX,
7
8
  HL_HASH_CAPTURE_RE_RAW,
@@ -206,11 +207,43 @@ export function cloneCursor(cursor: HashlineCursor): HashlineCursor {
206
207
  return cursor;
207
208
  }
208
209
 
210
+ /** Context for diagnosing an insert op that ended up with no payload lines. */
211
+ interface EmptyInsertContext {
212
+ /** The op token as written, without any `|TEXT` body (e.g. `»300rk`). */
213
+ op: string;
214
+ /** True when the op line carried `|TEXT` that only echoed the anchored line. */
215
+ echoedAnchorText: boolean;
216
+ }
217
+
218
+ function formatEmptyInsertError(opLineNum: number, context: EmptyInsertContext): string {
219
+ const base = `line ${opLineNum}: ${HL_OP_INSERT_BEFORE} and ${HL_OP_INSERT_AFTER} operations require at least one verbatim payload line.`;
220
+ if (context.echoedAnchorText) {
221
+ return (
222
+ `${base} The text after "${HL_BODY_SEP}" on "${context.op}${HL_BODY_SEP}…" matches the supplied anchor hash, ` +
223
+ `so it was read as an anchor echo, not as new content. Put the lines to insert on the lines after "${context.op}".`
224
+ );
225
+ }
226
+ return (
227
+ `${base} To insert a single blank line, put one empty line after "${context.op}" ` +
228
+ `(when it is the last op, the edit input ends with two newlines).`
229
+ );
230
+ }
231
+
232
+ /** Payload is required only when the op line carried no usable inline `|TEXT` body. */
233
+ function emptyInsertContext(
234
+ sigil: string,
235
+ match: RegExpExecArray,
236
+ inlineBody: string | undefined,
237
+ ): EmptyInsertContext | undefined {
238
+ if (inlineBody !== undefined) return undefined;
239
+ return { op: `${sigil}${match[1]}`, echoedAnchorText: match[2] !== undefined };
240
+ }
241
+
209
242
  function collectPayload(
210
243
  lines: string[],
211
244
  startIndex: number,
212
245
  opLineNum: number,
213
- requirePayload: boolean,
246
+ emptyInsert: EmptyInsertContext | undefined,
214
247
  ): { payload: string[]; nextIndex: number } {
215
248
  const payload: string[] = [];
216
249
  let index = startIndex;
@@ -220,11 +253,7 @@ function collectPayload(
220
253
  payload.push(line);
221
254
  index++;
222
255
  }
223
- if (payload.length === 0 && requirePayload) {
224
- throw new Error(
225
- `line ${opLineNum}: ${HL_OP_INSERT_BEFORE} and ${HL_OP_INSERT_AFTER} operations require at least one verbatim payload line.`,
226
- );
227
- }
256
+ if (payload.length === 0 && emptyInsert) throw new Error(formatEmptyInsertError(opLineNum, emptyInsert));
228
257
  return { payload, nextIndex: index };
229
258
  }
230
259
 
@@ -267,7 +296,12 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
267
296
  if (insertBeforeMatch) {
268
297
  const cursor = parseInsertTarget(insertBeforeMatch[1], lineNum, "before");
269
298
  const inlineBody = resolveInlineInsertBody(cursor, insertBeforeMatch[2]);
270
- const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, inlineBody === undefined);
299
+ const { payload, nextIndex } = collectPayload(
300
+ lines,
301
+ i + 1,
302
+ lineNum,
303
+ emptyInsertContext(HL_OP_INSERT_BEFORE, insertBeforeMatch, inlineBody),
304
+ );
271
305
  if (inlineBody !== undefined) pushInsert(cursor, inlineBody, lineNum);
272
306
  for (const text of payload) pushInsert(cursor, text, lineNum);
273
307
  i = nextIndex;
@@ -278,7 +312,12 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
278
312
  if (insertAfterMatch) {
279
313
  const cursor = parseInsertTarget(insertAfterMatch[1], lineNum, "after");
280
314
  const inlineBody = resolveInlineInsertBody(cursor, insertAfterMatch[2]);
281
- const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, inlineBody === undefined);
315
+ const { payload, nextIndex } = collectPayload(
316
+ lines,
317
+ i + 1,
318
+ lineNum,
319
+ emptyInsertContext(HL_OP_INSERT_AFTER, insertAfterMatch, inlineBody),
320
+ );
282
321
  if (inlineBody !== undefined) pushInsert(cursor, inlineBody, lineNum);
283
322
  for (const text of payload) pushInsert(cursor, text, lineNum);
284
323
  i = nextIndex;
@@ -288,7 +327,7 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
288
327
  const replaceMatch = REPLACE_OP_RE.exec(line);
289
328
  if (replaceMatch) {
290
329
  const range = parseRange(replaceMatch[1], lineNum, HL_OP_REPLACE);
291
- const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, false);
330
+ const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, undefined);
292
331
  if (payload.length > 0) {
293
332
  for (const text of payload) {
294
333
  edits.push({