@gajae-code/coding-agent 0.5.0 → 0.5.2

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 (194) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +26 -0
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/list-models.d.ts +6 -0
  6. package/dist/types/cli/setup-cli.d.ts +8 -1
  7. package/dist/types/commands/gc.d.ts +26 -0
  8. package/dist/types/commands/setup.d.ts +7 -0
  9. package/dist/types/config/file-lock-gc.d.ts +5 -0
  10. package/dist/types/config/file-lock.d.ts +29 -0
  11. package/dist/types/config/model-registry.d.ts +4 -0
  12. package/dist/types/config/models-config-schema.d.ts +5 -0
  13. package/dist/types/config/settings-schema.d.ts +62 -0
  14. package/dist/types/coordinator/contract.d.ts +1 -1
  15. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  16. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  24. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  25. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  26. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  27. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  28. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  29. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  31. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  32. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  33. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  34. package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
  35. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
  38. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  39. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  40. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
  42. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  43. package/dist/types/harness-control-plane/owner.d.ts +7 -0
  44. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  45. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  46. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -1
  49. package/dist/types/modes/rpc/rpc-mode.d.ts +72 -2
  50. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  51. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  52. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  53. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  54. package/dist/types/modes/theme/defaults/index.d.ts +302 -0
  55. package/dist/types/modes/theme/theme.d.ts +1 -0
  56. package/dist/types/modes/types.d.ts +1 -1
  57. package/dist/types/session/agent-session.d.ts +1 -1
  58. package/dist/types/session/blob-store.d.ts +39 -3
  59. package/dist/types/session/history-storage.d.ts +2 -2
  60. package/dist/types/session/session-manager.d.ts +10 -1
  61. package/dist/types/setup/credential-import.d.ts +79 -0
  62. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  63. package/dist/types/task/executor.d.ts +1 -0
  64. package/dist/types/task/render.d.ts +1 -1
  65. package/dist/types/tools/ask.d.ts +15 -1
  66. package/dist/types/tools/subagent-render.d.ts +7 -1
  67. package/dist/types/tools/subagent.d.ts +27 -0
  68. package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
  69. package/dist/types/web/search/index.d.ts +4 -4
  70. package/dist/types/web/search/provider.d.ts +16 -20
  71. package/dist/types/web/search/providers/base.d.ts +2 -1
  72. package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
  73. package/dist/types/web/search/types.d.ts +14 -2
  74. package/package.json +7 -7
  75. package/scripts/build-binary.ts +7 -0
  76. package/src/async/job-manager.ts +52 -0
  77. package/src/cli/args.ts +5 -0
  78. package/src/cli/auth-broker-cli.ts +1 -0
  79. package/src/cli/fast-help.ts +2 -0
  80. package/src/cli/list-models.ts +13 -1
  81. package/src/cli/setup-cli.ts +138 -3
  82. package/src/cli.ts +1 -0
  83. package/src/commands/gc.ts +22 -0
  84. package/src/commands/harness.ts +7 -3
  85. package/src/commands/setup.ts +5 -1
  86. package/src/commands/ultragoal.ts +3 -1
  87. package/src/config/file-lock-gc.ts +193 -0
  88. package/src/config/file-lock.ts +66 -10
  89. package/src/config/model-profile-activation.ts +15 -3
  90. package/src/config/model-profiles.ts +39 -30
  91. package/src/config/model-registry.ts +21 -1
  92. package/src/config/models-config-schema.ts +1 -0
  93. package/src/config/settings-schema.ts +62 -0
  94. package/src/coordinator/contract.ts +1 -0
  95. package/src/coordinator-mcp/server.ts +459 -3
  96. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  97. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  98. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  99. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  100. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  101. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  102. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  103. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  104. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  105. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  106. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  107. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  108. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  109. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  110. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  111. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  112. package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
  113. package/src/defaults/gjc-defaults.ts +7 -0
  114. package/src/defaults/gjc-grok-cli.ts +22 -0
  115. package/src/extensibility/extensions/index.ts +1 -0
  116. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  117. package/src/gjc-runtime/deep-interview-recorder.ts +457 -0
  118. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  119. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  120. package/src/gjc-runtime/gc-render.ts +70 -0
  121. package/src/gjc-runtime/gc-runtime.ts +403 -0
  122. package/src/gjc-runtime/launch-tmux.ts +3 -4
  123. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  124. package/src/gjc-runtime/ralplan-runtime.ts +232 -19
  125. package/src/gjc-runtime/state-renderer.ts +12 -3
  126. package/src/gjc-runtime/state-runtime.ts +48 -30
  127. package/src/gjc-runtime/state-writer.ts +254 -7
  128. package/src/gjc-runtime/team-gc.ts +49 -0
  129. package/src/gjc-runtime/team-runtime.ts +179 -2
  130. package/src/gjc-runtime/tmux-common.ts +14 -0
  131. package/src/gjc-runtime/tmux-gc.ts +177 -0
  132. package/src/gjc-runtime/tmux-sessions.ts +49 -1
  133. package/src/gjc-runtime/ultragoal-guard.ts +155 -0
  134. package/src/gjc-runtime/ultragoal-runtime.ts +1239 -31
  135. package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
  136. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  137. package/src/harness-control-plane/gc-adapter.ts +184 -0
  138. package/src/harness-control-plane/owner.ts +14 -2
  139. package/src/harness-control-plane/rpc-adapter.ts +1 -1
  140. package/src/harness-control-plane/storage.ts +70 -0
  141. package/src/hooks/skill-state.ts +121 -2
  142. package/src/internal-urls/docs-index.generated.ts +22 -12
  143. package/src/lsp/defaults.json +1 -0
  144. package/src/main.ts +18 -3
  145. package/src/modes/acp/acp-agent.ts +4 -2
  146. package/src/modes/bridge/bridge-mode.ts +2 -1
  147. package/src/modes/components/history-search.ts +5 -2
  148. package/src/modes/components/hook-selector.ts +19 -0
  149. package/src/modes/components/model-selector.ts +51 -8
  150. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  151. package/src/modes/components/status-line/segments.ts +1 -1
  152. package/src/modes/controllers/command-controller.ts +25 -6
  153. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  154. package/src/modes/controllers/selector-controller.ts +81 -1
  155. package/src/modes/interactive-mode.ts +11 -1
  156. package/src/modes/rpc/rpc-mode.ts +266 -34
  157. package/src/modes/shared/agent-wire/command-dispatch.ts +281 -261
  158. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  159. package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
  160. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  161. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  162. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  163. package/src/modes/shared/agent-wire/unattended-session.ts +32 -2
  164. package/src/modes/theme/defaults/claude-code.json +100 -0
  165. package/src/modes/theme/defaults/codex.json +100 -0
  166. package/src/modes/theme/defaults/index.ts +6 -0
  167. package/src/modes/theme/defaults/opencode.json +102 -0
  168. package/src/modes/theme/theme.ts +2 -2
  169. package/src/modes/types.ts +1 -1
  170. package/src/prompts/agents/executor.md +5 -2
  171. package/src/sdk.ts +29 -4
  172. package/src/session/agent-session.ts +99 -19
  173. package/src/session/blob-store.ts +59 -3
  174. package/src/session/history-storage.ts +32 -11
  175. package/src/session/session-manager.ts +72 -20
  176. package/src/setup/credential-import.ts +429 -0
  177. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  178. package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
  179. package/src/skill-state/workflow-hud.ts +106 -10
  180. package/src/slash-commands/builtin-registry.ts +3 -2
  181. package/src/task/executor.ts +16 -1
  182. package/src/task/render.ts +18 -7
  183. package/src/tools/ask.ts +59 -2
  184. package/src/tools/cron.ts +1 -1
  185. package/src/tools/job.ts +3 -2
  186. package/src/tools/monitor.ts +36 -1
  187. package/src/tools/subagent-render.ts +128 -29
  188. package/src/tools/subagent.ts +173 -9
  189. package/src/tools/ultragoal-ask-guard.ts +39 -0
  190. package/src/web/search/index.ts +25 -25
  191. package/src/web/search/provider.ts +178 -87
  192. package/src/web/search/providers/base.ts +2 -1
  193. package/src/web/search/providers/openai-compatible.ts +151 -0
  194. package/src/web/search/types.ts +47 -22
@@ -5,9 +5,20 @@ import { syncSkillActiveState } from "../skill-state/active-state";
5
5
  import { buildRalplanHudSummary } from "../skill-state/workflow-hud";
6
6
  import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
7
7
  import { renderCliWriteReceipt } from "./cli-write-receipt";
8
+ import {
9
+ formatRalplanStagePresence,
10
+ parseRalplanIndexLine,
11
+ type RalplanIndexRow,
12
+ summarizeRalplanIndex,
13
+ } from "./ledger-event-renderer";
8
14
  import { isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
9
15
  import { migrateWorkflowState } from "./state-migrations";
10
- import { appendJsonl, readExistingStateForMutation, writeArtifact, writeWorkflowEnvelopeAtomic } from "./state-writer";
16
+ import {
17
+ appendJsonlIdempotent,
18
+ readExistingStateForMutation,
19
+ writeArtifact,
20
+ writeWorkflowEnvelopeAtomic,
21
+ } from "./state-writer";
11
22
 
12
23
  /**
13
24
  * Native implementation of `gjc ralplan`.
@@ -180,7 +191,37 @@ async function readActiveRunId(cwd: string, sessionId: string | undefined): Prom
180
191
  return candidate;
181
192
  }
182
193
 
183
- async function persistActiveRunId(cwd: string, sessionId: string | undefined, runId: string): Promise<void> {
194
+ /**
195
+ * Run-state phases that an artifact write must never reopen. Once ralplan has
196
+ * reached a terminal/handed-off phase, a stray `--write` must not regress
197
+ * `current_phase` back to a stage — that would silently re-arm a chain guard or
198
+ * undo Stop semantics. Every other phase advances to track the stage just
199
+ * persisted so run-state stays coherent with the active ralplan stage.
200
+ */
201
+ const PHASE_LOCK = new Set([
202
+ "final",
203
+ "handoff",
204
+ "complete",
205
+ "completed",
206
+ "failed",
207
+ "cancelled",
208
+ "canceled",
209
+ "inactive",
210
+ ]);
211
+
212
+ /** Phase that keeps run-state coherent with the stage just written, preserving locked phases. */
213
+ function advanceCurrentPhase(existingPhase: unknown, stage: RalplanStage): string {
214
+ const current = typeof existingPhase === "string" ? existingPhase.trim() : "";
215
+ if (current && PHASE_LOCK.has(current)) return current;
216
+ return stage;
217
+ }
218
+
219
+ async function persistActiveRunId(
220
+ cwd: string,
221
+ sessionId: string | undefined,
222
+ runId: string,
223
+ stage: RalplanStage,
224
+ ): Promise<void> {
184
225
  const statePath = ralplanStatePath(cwd, sessionId);
185
226
  const existingRead = await readExistingStateForMutation(statePath);
186
227
  if (existingRead.kind === "corrupt") {
@@ -191,11 +232,25 @@ async function persistActiveRunId(cwd: string, sessionId: string | undefined, ru
191
232
  }
192
233
  let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
193
234
 
194
- if (existing.run_id === runId && existing.version === WORKFLOW_STATE_VERSION) return;
235
+ // A new run_id is a fresh run, not a stray write on the prior run: never inherit a
236
+ // previous run's terminal/locked phase (which would start the new run already
237
+ // "complete"/"handoff" and disarm the Stop hook). PHASE_LOCK only guards same-run writes.
238
+ const isNewRun = existing.run_id !== runId;
239
+ const nextPhase = isNewRun ? stage : advanceCurrentPhase(existing.current_phase, stage);
240
+ if (
241
+ existing.run_id === runId &&
242
+ existing.version === WORKFLOW_STATE_VERSION &&
243
+ existing.current_phase === nextPhase &&
244
+ (existing.active === true || PHASE_LOCK.has(nextPhase))
245
+ ) {
246
+ return;
247
+ }
195
248
  existing.run_id = runId;
196
249
  if (typeof existing.skill !== "string") existing.skill = "ralplan";
197
- if (typeof existing.active !== "boolean") existing.active = true;
198
- if (typeof existing.current_phase !== "string") existing.current_phase = "planner";
250
+ // A successful persist means ralplan is actively writing this run's artifacts, so always
251
+ // re-assert active. Fallback-only init left active:false after a clear (#644, sibling of #638).
252
+ existing.active = true;
253
+ existing.current_phase = nextPhase;
199
254
  existing = migrateWorkflowState(existing, "ralplan").state;
200
255
  existing.updated_at = new Date().toISOString();
201
256
  await writeWorkflowEnvelopeAtomic(statePath, existing, {
@@ -375,8 +430,6 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
375
430
  const explicitRunId = flagValue(args, "--run-id")?.trim();
376
431
  const runId = explicitRunId || (await readActiveRunId(cwd, sessionId)) || sessionIdRaw || defaultRunId();
377
432
  assertSafePathComponent(runId, "run-id");
378
- // Persist the active run id so later writes in the same loop land in the same directory.
379
- await persistActiveRunId(cwd, sessionId, runId);
380
433
 
381
434
  const artifact = await resolveArtifactContent(rawArtifact, cwd);
382
435
  return { stage: stage as RalplanStage, stageN, runId, artifact, sessionId, json: hasFlag(args, "--json") };
@@ -392,18 +445,34 @@ interface PersistedArtifact {
392
445
  pendingApprovalPath?: string;
393
446
  }
394
447
 
395
- async function persistArtifact(resolved: ResolvedArtifactArgs, cwd: string): Promise<PersistedArtifact> {
448
+ /**
449
+ * Content-addressed identity for an `index.jsonl` row: a repeated `--write` of the
450
+ * same `(stage, stage_n)` at identical content (same sha256) is the #638 duplicate
451
+ * the append must collapse. Rows missing these fields opt out of dedup.
452
+ */
453
+ function ralplanIndexKey(entry: unknown): string | undefined {
454
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return undefined;
455
+ const record = entry as Record<string, unknown>;
456
+ const { stage, stage_n, sha256 } = record;
457
+ if (typeof stage !== "string" || typeof stage_n !== "number" || typeof sha256 !== "string") return undefined;
458
+ return `${stage}\u0000${stage_n}\u0000${sha256}`;
459
+ }
460
+
461
+ async function persistArtifact(
462
+ resolved: ResolvedArtifactArgs,
463
+ cwd: string,
464
+ content: string,
465
+ sha256: string,
466
+ ): Promise<PersistedArtifact> {
396
467
  const runDir = path.join(cwd, ".gjc", "plans", "ralplan", resolved.runId);
397
468
 
398
469
  const fileName = `stage-${pad2(resolved.stageN)}-${resolved.stage}.md`;
399
470
  const filePath = path.join(runDir, fileName);
400
- const content = resolved.artifact.endsWith("\n") ? resolved.artifact : `${resolved.artifact}\n`;
401
471
  await writeArtifact(filePath, content, {
402
472
  cwd,
403
473
  audit: { category: "artifact", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
404
474
  });
405
475
 
406
- const sha256 = createHash("sha256").update(content).digest("hex");
407
476
  const createdAt = new Date().toISOString();
408
477
  const indexEntry = {
409
478
  stage: resolved.stage,
@@ -412,9 +481,10 @@ async function persistArtifact(resolved: ResolvedArtifactArgs, cwd: string): Pro
412
481
  created_at: createdAt,
413
482
  sha256,
414
483
  };
415
- await appendJsonl(path.join(runDir, "index.jsonl"), indexEntry, {
484
+ await appendJsonlIdempotent(path.join(runDir, "index.jsonl"), indexEntry, {
416
485
  cwd,
417
486
  audit: { category: "ledger", verb: "append", owner: "gjc-runtime", skill: "ralplan" },
487
+ key: ralplanIndexKey,
418
488
  });
419
489
 
420
490
  let pendingApprovalPath: string | undefined;
@@ -437,12 +507,82 @@ async function persistArtifact(resolved: ResolvedArtifactArgs, cwd: string): Pro
437
507
  };
438
508
  }
439
509
 
510
+ /** The persisted `(stage, stage_n)` artifact recorded in a run's `index.jsonl`. */
511
+ interface ExistingStageArtifact {
512
+ path: string;
513
+ sha256: string;
514
+ createdAt: string;
515
+ }
516
+
517
+ /**
518
+ * Find the most recent `index.jsonl` row for a `(stage, stage_n)` pair so a
519
+ * repeated `--write` can dedupe instead of silently clobbering the artifact and
520
+ * appending a duplicate ledger row. Best-effort: a missing or unreadable index
521
+ * yields `undefined`, treated as "no prior artifact". The ledger is the source of
522
+ * truth for dedup because it is exactly what a duplicate write would corrupt.
523
+ */
524
+ async function findExistingStageArtifact(
525
+ cwd: string,
526
+ runId: string,
527
+ stage: RalplanStage,
528
+ stageN: number,
529
+ ): Promise<ExistingStageArtifact | undefined> {
530
+ const indexPath = path.join(cwd, ".gjc", "plans", "ralplan", runId, "index.jsonl");
531
+ let text: string;
532
+ try {
533
+ text = await fs.readFile(indexPath, "utf8");
534
+ } catch {
535
+ return undefined;
536
+ }
537
+ let match: ExistingStageArtifact | undefined;
538
+ for (const line of text.split(/\r?\n/)) {
539
+ const trimmed = line.trim();
540
+ if (!trimmed) continue;
541
+ let row: unknown;
542
+ try {
543
+ row = JSON.parse(trimmed);
544
+ } catch {
545
+ continue;
546
+ }
547
+ if (!row || typeof row !== "object" || Array.isArray(row)) continue;
548
+ const record = row as Record<string, unknown>;
549
+ if (record.stage !== stage || record.stage_n !== stageN) continue;
550
+ if (typeof record.path !== "string" || typeof record.sha256 !== "string") continue;
551
+ match = {
552
+ path: record.path,
553
+ sha256: record.sha256,
554
+ createdAt: typeof record.created_at === "string" ? record.created_at : "",
555
+ };
556
+ }
557
+ return match;
558
+ }
559
+
560
+ /**
561
+ * Read and parse the run's `index.jsonl` rows. Best-effort: returns [] when the
562
+ * file is absent or unreadable so HUD sync never fails on a missing index.
563
+ */
564
+ async function readRalplanIndexRows(cwd: string, runId: string): Promise<RalplanIndexRow[]> {
565
+ try {
566
+ const indexPath = path.join(cwd, ".gjc", "plans", "ralplan", runId, "index.jsonl");
567
+ const text = await fs.readFile(indexPath, "utf8");
568
+ const rows: RalplanIndexRow[] = [];
569
+ for (const line of text.split(/\r?\n/)) {
570
+ const row = parseRalplanIndexLine(line);
571
+ if (row) rows.push(row);
572
+ }
573
+ return rows;
574
+ } catch {
575
+ return [];
576
+ }
577
+ }
578
+
440
579
  async function syncRalplanHud(options: {
441
580
  cwd: string;
442
581
  sessionId?: string;
443
582
  stage: string;
444
583
  pendingApproval: boolean;
445
584
  iteration?: number;
585
+ runId?: string;
446
586
  latestSummary?: string;
447
587
  }): Promise<void> {
448
588
  try {
@@ -453,23 +593,65 @@ async function syncRalplanHud(options: {
453
593
  phase: options.stage,
454
594
  sessionId: options.sessionId,
455
595
  source: "gjc-ralplan-native",
456
- hud: buildRalplanHudSummary({
457
- stage: options.stage,
458
- iteration: options.iteration,
459
- pendingApproval: options.pendingApproval,
460
- latestSummary: options.latestSummary,
461
- updatedAt: new Date().toISOString(),
462
- }),
596
+ hud: await buildRalplanHud(options),
463
597
  });
464
598
  } catch {
465
599
  // HUD sync is best-effort and must not change command semantics.
466
600
  }
467
601
  }
468
602
 
603
+ async function buildRalplanHud(options: {
604
+ cwd: string;
605
+ stage: string;
606
+ pendingApproval: boolean;
607
+ iteration?: number;
608
+ latestSummary?: string;
609
+ runId?: string;
610
+ }) {
611
+ let iterationFromIndex: number | undefined;
612
+ let stages: string | undefined;
613
+ if (options.runId) {
614
+ const rows = await readRalplanIndexRows(options.cwd, options.runId);
615
+ if (rows.length > 0) {
616
+ const summary = summarizeRalplanIndex(rows);
617
+ iterationFromIndex = summary.iteration;
618
+ stages = formatRalplanStagePresence(summary.currentStages);
619
+ }
620
+ }
621
+ return buildRalplanHudSummary({
622
+ stage: options.stage,
623
+ iteration: options.iteration,
624
+ iterationFromIndex,
625
+ stages,
626
+ pendingApproval: options.pendingApproval,
627
+ latestSummary: options.latestSummary,
628
+ updatedAt: new Date().toISOString(),
629
+ });
630
+ }
631
+
469
632
  async function handleArtifactWrite(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
470
633
  const plannerState = parsePlannerStateArgs(args);
471
634
  const resolved = await resolveArtifactArgs(args, cwd);
472
- const persisted = await persistArtifact(resolved, cwd);
635
+ const content = resolved.artifact.endsWith("\n") ? resolved.artifact : `${resolved.artifact}\n`;
636
+ const sha256 = createHash("sha256").update(content).digest("hex");
637
+
638
+ // Duplicate-write guard: a second `--write` for the same (stage, stage_n) must not
639
+ // silently clobber the artifact or append a duplicate ledger row. Classify before any
640
+ // state mutation so a conflict never regresses run-state phase.
641
+ const existingArtifact = await findExistingStageArtifact(cwd, resolved.runId, resolved.stage, resolved.stageN);
642
+ if (existingArtifact) {
643
+ if (existingArtifact.sha256 !== sha256) {
644
+ throw new RalplanCommandError(
645
+ 2,
646
+ `refusing to overwrite ralplan ${resolved.stage} stage ${resolved.stageN} at ${existingArtifact.path}: an artifact with different content already exists (existing sha256=${existingArtifact.sha256}, new sha256=${sha256}). Use a new --stage_n to record another pass.`,
647
+ );
648
+ }
649
+ return buildDeduplicatedResult(resolved, existingArtifact, sha256, cwd);
650
+ }
651
+
652
+ // Keep run-state `current_phase` coherent with the stage being persisted.
653
+ await persistActiveRunId(cwd, resolved.sessionId, resolved.runId, resolved.stage);
654
+ const persisted = await persistArtifact(resolved, cwd, content, sha256);
473
655
  if (plannerState) {
474
656
  await applyPlannerStateUpdate(cwd, resolved.sessionId, plannerState);
475
657
  }
@@ -477,6 +659,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
477
659
  cwd,
478
660
  sessionId: resolved.sessionId,
479
661
  stage: persisted.stage,
662
+ runId: persisted.runId,
480
663
  pendingApproval: persisted.stage === "final",
481
664
  iteration: persisted.stageN,
482
665
  latestSummary: `persisted ${persisted.stage} stage ${persisted.stageN}`,
@@ -497,6 +680,35 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
497
680
  return { status: 0, stdout };
498
681
  }
499
682
 
683
+ /**
684
+ * Deterministic no-op receipt for an identical repeated `--write`: report the
685
+ * already-persisted artifact without rewriting the file, appending a ledger row, or
686
+ * churning run-state. `deduplicated: true` lets callers distinguish it from a fresh write.
687
+ */
688
+ function buildDeduplicatedResult(
689
+ resolved: ResolvedArtifactArgs,
690
+ existing: ExistingStageArtifact,
691
+ sha256: string,
692
+ cwd: string,
693
+ ): RalplanCommandResult {
694
+ const payload: Record<string, unknown> = {
695
+ run_id: resolved.runId,
696
+ path: existing.path,
697
+ stage: resolved.stage,
698
+ stage_n: resolved.stageN,
699
+ sha256,
700
+ created_at: existing.createdAt,
701
+ deduplicated: true,
702
+ };
703
+ if (resolved.stage === "final") {
704
+ payload.pending_approval_path = path.join(cwd, ".gjc", "plans", "ralplan", resolved.runId, "pending-approval.md");
705
+ }
706
+ const stdout = resolved.json
707
+ ? `${JSON.stringify(payload, null, 2)}\n`
708
+ : `ralplan ${resolved.stage} stage ${resolved.stageN} already persisted at ${existing.path} (identical content; no changes written).\n`;
709
+ return { status: 0, stdout };
710
+ }
711
+
500
712
  /* -------------------------------- handoff -------------------------------- */
501
713
 
502
714
  interface ConsensusHandoffArgs {
@@ -617,6 +829,7 @@ async function handleConsensusHandoff(args: readonly string[], cwd: string): Pro
617
829
  cwd,
618
830
  sessionId: resolved.sessionId,
619
831
  stage: "planner",
832
+ runId,
620
833
  pendingApproval: false,
621
834
  iteration: 1,
622
835
  latestSummary: `${mode} run · ${resolved.interactive ? "interactive" : "automated"}`,
@@ -118,8 +118,13 @@ export interface StateStatusSummary {
118
118
 
119
119
  function compactStateFields(state: Record<string, unknown>): Array<[string, string]> {
120
120
  const fields: Array<[string, string]> = [];
121
+ const nested = isRecord(state.state) ? state.state : undefined;
121
122
  for (const key of COMPACT_ELIDE_KEYS) {
122
- const value = state[key];
123
+ const value = Array.isArray(state[key])
124
+ ? state[key]
125
+ : nested && Array.isArray(nested[key])
126
+ ? nested[key]
127
+ : undefined;
123
128
  if (Array.isArray(value)) fields.push([key, `${value.length} entries (elided)`]);
124
129
  }
125
130
  return fields;
@@ -133,9 +138,13 @@ export function compactProjectStateJson(
133
138
  const state = stateObject(stateJson);
134
139
  const compact = projectStateFields(skill, stateJson, manifest, STATE_FIELD_ALLOWLIST);
135
140
  const elisions: Record<string, unknown> = {};
141
+ const nested = isRecord(state.state) ? state.state : undefined;
136
142
  for (const key of COMPACT_ELIDE_KEYS) {
137
- const value = state[key];
138
- if (Array.isArray(value)) elisions[key] = { type: "array", count: value.length, pointer: `/${key}` };
143
+ if (Array.isArray(state[key])) {
144
+ elisions[key] = { type: "array", count: (state[key] as unknown[]).length, pointer: `/${key}` };
145
+ } else if (nested && Array.isArray(nested[key])) {
146
+ elisions[key] = { type: "array", count: (nested[key] as unknown[]).length, pointer: `/state/${key}` };
147
+ }
139
148
  }
140
149
  if (Object.keys(elisions).length) compact.elided = elisions;
141
150
  return compact;
@@ -11,10 +11,10 @@ import {
11
11
  } from "../skill-state/active-state";
12
12
  import { initialPhaseForSkill } from "../skill-state/initial-phase";
13
13
  import {
14
- buildDeepInterviewHudSummary,
15
14
  buildRalplanHudSummary,
16
15
  buildTeamHudSummary,
17
16
  buildUltragoalHudSummary,
17
+ deriveDeepInterviewHud,
18
18
  } from "../skill-state/workflow-hud";
19
19
  import {
20
20
  type AuditEntry,
@@ -26,6 +26,7 @@ import {
26
26
  type WorkflowStateReceipt,
27
27
  } from "../skill-state/workflow-state-contract";
28
28
  import { renderCliWriteReceipt } from "./cli-write-receipt";
29
+ import { mergeDeepInterviewEnvelope, normalizeDeepInterviewEnvelope } from "./deep-interview-state";
29
30
  import { renderStateGraph, type StateGraphFormat } from "./state-graph";
30
31
  import { migrateAndPersistLegacyState, migrateWorkflowState } from "./state-migrations";
31
32
  import {
@@ -51,6 +52,7 @@ import {
51
52
  type StateWriterAuditContext,
52
53
  softDelete,
53
54
  updateWorkflowTransactionJournal,
55
+ type WorkflowEnvelopeIntegrityMismatch,
54
56
  writeWorkflowEnvelopeAtomic,
55
57
  } from "./state-writer";
56
58
  import { getSkillManifest, isKnownWorkflowState, isValidTransition, typedArgsFor } from "./workflow-manifest";
@@ -658,7 +660,7 @@ async function warnAndAuditOutOfBandIfNeeded(
658
660
  skill: CanonicalGjcWorkflowSkill,
659
661
  options?: { mutationId?: string; forced?: boolean },
660
662
  ): Promise<string | undefined> {
661
- let mismatch: Awaited<ReturnType<typeof detectWorkflowEnvelopeIntegrityMismatch>>;
663
+ let mismatch: WorkflowEnvelopeIntegrityMismatch | undefined;
662
664
  try {
663
665
  mismatch = await detectWorkflowEnvelopeIntegrityMismatch(filePath);
664
666
  } catch {
@@ -833,31 +835,9 @@ function buildHudForMode(
833
835
  ): WorkflowHudSummary | undefined {
834
836
  const updatedAt = new Date().toISOString();
835
837
  const phase = typeof payload.current_phase === "string" ? payload.current_phase : undefined;
836
- const stateField = isPlainObject(payload.state) ? (payload.state as Record<string, unknown>) : {};
837
838
  switch (mode) {
838
- case "deep-interview": {
839
- const pick = <T>(key: string, guard: (value: unknown) => value is T): T | undefined => {
840
- const v = (stateField as Record<string, unknown>)[key] ?? (payload as Record<string, unknown>)[key];
841
- return guard(v) ? v : undefined;
842
- };
843
- const isNumber = (v: unknown): v is number => typeof v === "number";
844
- const isString = (v: unknown): v is string => typeof v === "string";
845
- const isArray = (v: unknown): v is unknown[] => Array.isArray(v);
846
- const ambiguity = pick("current_ambiguity", isNumber);
847
- const threshold = pick("threshold", isNumber);
848
- const rounds = pick("rounds", isArray);
849
- const targetComponent = pick("last_targeted_component_id", isString);
850
- const weakestDimension = pick("weakest_dimension", isString);
851
- return buildDeepInterviewHudSummary({
852
- phase,
853
- ambiguity,
854
- threshold,
855
- roundCount: rounds?.length,
856
- targetComponent,
857
- weakestDimension,
858
- updatedAt,
859
- });
860
- }
839
+ case "deep-interview":
840
+ return deriveDeepInterviewHud(payload, { updatedAt });
861
841
  case "ralplan": {
862
842
  const stage =
863
843
  typeof payload.current_phase === "string"
@@ -888,6 +868,24 @@ function buildHudForMode(
888
868
  counts[status] = (counts[status] ?? 0) + 1;
889
869
  }
890
870
  const currentGoalRaw = goals.find(g => g.status === "active") ?? goals.find(g => g.status === "pending");
871
+ const rawLedger = payload.latestLedgerEvent;
872
+ const latestLedgerEvent =
873
+ rawLedger && typeof rawLedger === "object" && !Array.isArray(rawLedger)
874
+ ? {
875
+ event:
876
+ typeof (rawLedger as Record<string, unknown>).event === "string"
877
+ ? ((rawLedger as Record<string, unknown>).event as string)
878
+ : undefined,
879
+ goalId:
880
+ typeof (rawLedger as Record<string, unknown>).goalId === "string"
881
+ ? ((rawLedger as Record<string, unknown>).goalId as string)
882
+ : undefined,
883
+ timestamp:
884
+ typeof (rawLedger as Record<string, unknown>).timestamp === "string"
885
+ ? ((rawLedger as Record<string, unknown>).timestamp as string)
886
+ : undefined,
887
+ }
888
+ : undefined;
891
889
  const status = typeof payload.status === "string" ? (payload.status as string) : (phase ?? "pending");
892
890
  return buildUltragoalHudSummary({
893
891
  status,
@@ -900,6 +898,7 @@ function buildHudForMode(
900
898
  : undefined,
901
899
  counts,
902
900
  goals: goals.map(g => ({ id: g.id as string, title: g.title as string, status: g.status as string })),
901
+ latestLedgerEvent,
903
902
  updatedAt,
904
903
  });
905
904
  }
@@ -1009,7 +1008,10 @@ export async function reconcileWorkflowSkillState(options: {
1009
1008
  receipt.from_phase = fromPhase;
1010
1009
  receipt.to_phase = trimmedPhase;
1011
1010
 
1012
- const merged = mergeWithNullDelete(existingPayload, payload);
1011
+ const merged =
1012
+ mode === "deep-interview"
1013
+ ? (mergeDeepInterviewEnvelope(existingPayload, payload) as Record<string, unknown>)
1014
+ : mergeWithNullDelete(existingPayload, payload);
1013
1015
  merged.skill = mode;
1014
1016
  merged.current_phase = trimmedPhase;
1015
1017
  merged.active = active;
@@ -1173,7 +1175,11 @@ async function handleWrite(
1173
1175
  ? (innerState.current_phase as string).trim()
1174
1176
  : undefined;
1175
1177
  let merged: Record<string, unknown>;
1176
- if (hasFlag(args, "--replace")) {
1178
+ if (mode === "deep-interview") {
1179
+ // Deep-interview keeps interview data nested under `state` and merges rounds
1180
+ // losslessly by durable key; never flatten or delete `state` (that drops recorder history).
1181
+ merged = mergeDeepInterviewEnvelope(existingPayload, payload, { replace: hasFlag(args, "--replace") });
1182
+ } else if (hasFlag(args, "--replace")) {
1177
1183
  merged = { ...payload };
1178
1184
  } else {
1179
1185
  merged = mergeWithNullDelete(existingPayload, payload);
@@ -1423,8 +1429,20 @@ async function handleHandoff(
1423
1429
  });
1424
1430
 
1425
1431
  const calleeInitial = initialPhaseForSkill(callee);
1426
- const normalizedCaller = migrateWorkflowState(existingCaller, caller).state;
1427
- const normalizedCallee = migrateWorkflowState(existingCallee, callee).state;
1432
+ const normalizedCaller =
1433
+ caller === "deep-interview"
1434
+ ? (normalizeDeepInterviewEnvelope(migrateWorkflowState(existingCaller, caller).state) as Record<
1435
+ string,
1436
+ unknown
1437
+ >)
1438
+ : migrateWorkflowState(existingCaller, caller).state;
1439
+ const normalizedCallee =
1440
+ callee === "deep-interview"
1441
+ ? (normalizeDeepInterviewEnvelope(migrateWorkflowState(existingCallee, callee).state) as Record<
1442
+ string,
1443
+ unknown
1444
+ >)
1445
+ : migrateWorkflowState(existingCallee, callee).state;
1428
1446
  const mergedCalleeState: Record<string, unknown> = {
1429
1447
  ...normalizedCallee,
1430
1448
  skill: callee,