@gajae-code/coding-agent 0.2.5 → 0.3.1

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 (234) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/types/async/job-manager.d.ts +91 -2
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/commands/harness.d.ts +37 -0
  6. package/dist/types/config/keybindings.d.ts +5 -0
  7. package/dist/types/config/settings-schema.d.ts +10 -4
  8. package/dist/types/config/settings.d.ts +2 -0
  9. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  10. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  11. package/dist/types/deep-interview/render-middleware.d.ts +6 -0
  12. package/dist/types/eval/py/executor.d.ts +2 -0
  13. package/dist/types/eval/py/kernel.d.ts +2 -0
  14. package/dist/types/exec/bash-executor.d.ts +10 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  17. package/dist/types/extensibility/shared-events.d.ts +1 -0
  18. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  19. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  20. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  21. package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
  22. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  23. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  25. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  26. package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  28. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  29. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  30. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  31. package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
  32. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  33. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  34. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  35. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  36. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  37. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  38. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  39. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  40. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  41. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  42. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  43. package/dist/types/harness-control-plane/types.d.ts +162 -0
  44. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  45. package/dist/types/hooks/skill-state.d.ts +23 -29
  46. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  47. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  48. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  49. package/dist/types/internal-urls/types.d.ts +4 -0
  50. package/dist/types/lsp/index.d.ts +10 -10
  51. package/dist/types/modes/bridge/auth.d.ts +12 -0
  52. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  53. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  54. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  55. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  56. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  57. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  58. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  59. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  60. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  61. package/dist/types/modes/components/status-line.d.ts +2 -0
  62. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  63. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  64. package/dist/types/modes/index.d.ts +1 -0
  65. package/dist/types/modes/interactive-mode.d.ts +2 -0
  66. package/dist/types/modes/jobs-observer.d.ts +57 -0
  67. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  68. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  69. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  70. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  71. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  72. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  73. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  74. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  75. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  76. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  77. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  78. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  79. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  80. package/dist/types/modes/types.d.ts +2 -0
  81. package/dist/types/sdk.d.ts +4 -0
  82. package/dist/types/session/agent-session.d.ts +19 -1
  83. package/dist/types/skill-state/active-state.d.ts +2 -0
  84. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  85. package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
  86. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  87. package/dist/types/task/executor.d.ts +3 -0
  88. package/dist/types/task/id.d.ts +7 -0
  89. package/dist/types/task/index.d.ts +5 -0
  90. package/dist/types/task/receipt.d.ts +85 -0
  91. package/dist/types/task/spawn-gate.d.ts +38 -0
  92. package/dist/types/task/types.d.ts +198 -14
  93. package/dist/types/tools/cron.d.ts +6 -0
  94. package/dist/types/tools/index.d.ts +2 -0
  95. package/dist/types/tools/path-utils.d.ts +1 -0
  96. package/dist/types/tools/subagent.d.ts +26 -1
  97. package/package.json +7 -7
  98. package/scripts/build-binary.ts +7 -0
  99. package/src/async/job-manager.ts +334 -6
  100. package/src/cli/args.ts +9 -2
  101. package/src/cli/auth-broker-cli.ts +1 -0
  102. package/src/cli/config-cli.ts +10 -2
  103. package/src/cli.ts +2 -0
  104. package/src/commands/deep-interview.ts +1 -0
  105. package/src/commands/harness.ts +862 -0
  106. package/src/commands/launch.ts +2 -2
  107. package/src/commands/state.ts +2 -1
  108. package/src/commands/team.ts +54 -39
  109. package/src/config/keybindings.ts +6 -0
  110. package/src/config/settings-schema.ts +13 -3
  111. package/src/config/settings.ts +5 -0
  112. package/src/dap/client.ts +17 -3
  113. package/src/debug/crash-diagnostics.ts +223 -0
  114. package/src/debug/runtime-gauges.ts +20 -0
  115. package/src/deep-interview/render-middleware.ts +372 -0
  116. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  117. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  118. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  119. package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
  120. package/src/eval/py/executor.ts +21 -1
  121. package/src/eval/py/kernel.ts +15 -0
  122. package/src/exec/bash-executor.ts +41 -0
  123. package/src/extensibility/custom-tools/types.ts +1 -0
  124. package/src/extensibility/extensions/types.ts +6 -0
  125. package/src/extensibility/shared-events.ts +1 -0
  126. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  127. package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
  128. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  129. package/src/gjc-runtime/ralplan-runtime.ts +235 -43
  130. package/src/gjc-runtime/state-graph.ts +86 -0
  131. package/src/gjc-runtime/state-migrations.ts +179 -0
  132. package/src/gjc-runtime/state-renderer.ts +345 -0
  133. package/src/gjc-runtime/state-runtime.ts +1155 -46
  134. package/src/gjc-runtime/state-schema.ts +192 -0
  135. package/src/gjc-runtime/state-validation.ts +49 -0
  136. package/src/gjc-runtime/state-writer.ts +749 -0
  137. package/src/gjc-runtime/team-runtime.ts +1255 -189
  138. package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
  139. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  140. package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
  141. package/src/gjc-runtime/workflow-manifest.ts +427 -0
  142. package/src/harness-control-plane/classifier.ts +128 -0
  143. package/src/harness-control-plane/control-endpoint.ts +148 -0
  144. package/src/harness-control-plane/finalize.ts +222 -0
  145. package/src/harness-control-plane/frame-mapper.ts +286 -0
  146. package/src/harness-control-plane/operate.ts +225 -0
  147. package/src/harness-control-plane/owner.ts +600 -0
  148. package/src/harness-control-plane/preserve.ts +102 -0
  149. package/src/harness-control-plane/receipts.ts +216 -0
  150. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  151. package/src/harness-control-plane/seams.ts +39 -0
  152. package/src/harness-control-plane/session-lease.ts +388 -0
  153. package/src/harness-control-plane/state-machine.ts +98 -0
  154. package/src/harness-control-plane/storage.ts +257 -0
  155. package/src/harness-control-plane/types.ts +214 -0
  156. package/src/hooks/skill-keywords.ts +4 -2
  157. package/src/hooks/skill-state.ts +197 -64
  158. package/src/internal-urls/agent-protocol.ts +68 -21
  159. package/src/internal-urls/artifact-protocol.ts +12 -17
  160. package/src/internal-urls/docs-index.generated.ts +3 -2
  161. package/src/internal-urls/registry-helpers.ts +19 -16
  162. package/src/internal-urls/types.ts +4 -0
  163. package/src/lsp/client.ts +18 -2
  164. package/src/main.ts +21 -5
  165. package/src/modes/bridge/auth.ts +41 -0
  166. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  167. package/src/modes/bridge/bridge-mode.ts +520 -0
  168. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  169. package/src/modes/bridge/event-stream.ts +70 -0
  170. package/src/modes/components/assistant-message.ts +5 -1
  171. package/src/modes/components/custom-editor.ts +101 -0
  172. package/src/modes/components/hook-selector.ts +133 -20
  173. package/src/modes/components/jobs-overlay-model.ts +109 -0
  174. package/src/modes/components/jobs-overlay.ts +172 -0
  175. package/src/modes/components/status-line/presets.ts +7 -5
  176. package/src/modes/components/status-line/segments.ts +25 -0
  177. package/src/modes/components/status-line/types.ts +2 -0
  178. package/src/modes/components/status-line.ts +9 -1
  179. package/src/modes/controllers/event-controller.ts +71 -6
  180. package/src/modes/controllers/extension-ui-controller.ts +43 -1
  181. package/src/modes/controllers/input-controller.ts +105 -9
  182. package/src/modes/controllers/selector-controller.ts +31 -1
  183. package/src/modes/index.ts +1 -0
  184. package/src/modes/interactive-mode.ts +28 -0
  185. package/src/modes/jobs-observer.ts +204 -0
  186. package/src/modes/rpc/host-tools.ts +1 -186
  187. package/src/modes/rpc/host-uris.ts +1 -235
  188. package/src/modes/rpc/rpc-client.ts +25 -10
  189. package/src/modes/rpc/rpc-mode.ts +12 -381
  190. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  191. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  192. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  193. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  194. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  195. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  196. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  197. package/src/modes/shared/agent-wire/responses.ts +17 -0
  198. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  199. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  200. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  201. package/src/modes/types.ts +2 -0
  202. package/src/prompts/agents/executor.md +13 -0
  203. package/src/prompts/tools/subagent.md +39 -4
  204. package/src/prompts/tools/task-summary.md +3 -9
  205. package/src/prompts/tools/task.md +5 -1
  206. package/src/sdk.ts +8 -0
  207. package/src/session/agent-session.ts +445 -71
  208. package/src/session/session-manager.ts +13 -1
  209. package/src/skill-state/active-state.ts +58 -65
  210. package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
  211. package/src/skill-state/initial-phase.ts +2 -0
  212. package/src/skill-state/workflow-state-contract.ts +33 -4
  213. package/src/skill-state/workflow-state-version.ts +3 -0
  214. package/src/slash-commands/builtin-registry.ts +8 -0
  215. package/src/task/executor.ts +79 -13
  216. package/src/task/id.ts +33 -0
  217. package/src/task/index.ts +376 -74
  218. package/src/task/output-manager.ts +5 -4
  219. package/src/task/receipt.ts +297 -0
  220. package/src/task/render.ts +54 -134
  221. package/src/task/spawn-gate.ts +132 -0
  222. package/src/task/types.ts +104 -10
  223. package/src/tools/ask.ts +88 -27
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash.ts +1 -1
  227. package/src/tools/cron.ts +48 -0
  228. package/src/tools/find.ts +4 -1
  229. package/src/tools/index.ts +2 -0
  230. package/src/tools/path-utils.ts +3 -2
  231. package/src/tools/read.ts +1 -0
  232. package/src/tools/search.ts +1 -0
  233. package/src/tools/skill.ts +6 -1
  234. package/src/tools/subagent.ts +423 -79
@@ -8,6 +8,7 @@ import {
8
8
  type ModeChangeEntry,
9
9
  type SessionEntry,
10
10
  } from "../session/session-manager";
11
+ import { removeFileAudited, writeJsonAtomic } from "./state-writer";
11
12
 
12
13
  export const GJC_SESSION_FILE_ENV = "GJC_SESSION_FILE";
13
14
  export const GJC_SESSION_ID_ENV = "GJC_SESSION_ID";
@@ -88,8 +89,10 @@ export async function writePendingGoalModeRequest(input: {
88
89
  goalsPath: input.goalsPath,
89
90
  };
90
91
  const filePath = requestPath(input.cwd);
91
- await fs.mkdir(path.dirname(filePath), { recursive: true });
92
- await Bun.write(filePath, `${JSON.stringify(request, null, 2)}\n`);
92
+ await writeJsonAtomic(filePath, request, {
93
+ cwd: input.cwd,
94
+ audit: { category: "state", verb: "write", owner: "gjc-runtime" },
95
+ });
93
96
  return request;
94
97
  }
95
98
 
@@ -153,6 +156,8 @@ export async function writeCurrentSessionGoalModeState(input: {
153
156
  mode: "goal",
154
157
  data: { goal: state.goal },
155
158
  };
159
+ // The session transcript file lives outside `.gjc/` (GJC_SESSION_FILE), so it is not a
160
+ // sanctioned-writer target; append directly.
156
161
  await fs.appendFile(sessionFile, `${JSON.stringify(entry)}\n`);
157
162
  return { status: "updated", goal: state.goal, sessionFile };
158
163
  }
@@ -176,7 +181,10 @@ export async function consumePendingGoalModeRequest(cwd: string): Promise<Pendin
176
181
  ) {
177
182
  return null;
178
183
  }
179
- await fs.unlink(filePath).catch(error => {
184
+ await removeFileAudited(filePath, {
185
+ cwd,
186
+ audit: { category: "prune", verb: "remove", owner: "gjc-runtime" },
187
+ }).catch(error => {
180
188
  if (!isEnoent(error)) throw error;
181
189
  });
182
190
  return { ...candidate, objective: candidate.objective.trim() } as PendingGoalModeRequest;
@@ -3,7 +3,11 @@ import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import { syncSkillActiveState } from "../skill-state/active-state";
5
5
  import { buildRalplanHudSummary } from "../skill-state/workflow-hud";
6
+ import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
7
+ import { renderCliWriteReceipt } from "./cli-write-receipt";
6
8
  import { isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
9
+ import { migrateWorkflowState } from "./state-migrations";
10
+ import { appendJsonl, readExistingStateForMutation, writeArtifact, writeWorkflowEnvelopeAtomic } from "./state-writer";
7
11
 
8
12
  /**
9
13
  * Native implementation of `gjc ralplan`.
@@ -38,6 +42,17 @@ const KNOWN_CRITIC_KINDS = new Set(["openai-code"]);
38
42
 
39
43
  const PATH_COMPONENT_RE = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
40
44
 
45
+ const SUBAGENT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
46
+
47
+ const KNOWN_FALLBACK_REASONS = new Set([
48
+ "context_unavailable",
49
+ "not_found",
50
+ "no_runner",
51
+ "resume_failed",
52
+ "process_restart",
53
+ "missing_record",
54
+ ]);
55
+
41
56
  class RalplanCommandError extends Error {
42
57
  constructor(
43
58
  public readonly exitStatus: number,
@@ -56,6 +71,12 @@ const VALUE_FLAGS = new Set([
56
71
  "--session-id",
57
72
  "--architect",
58
73
  "--critic",
74
+ "--planner-id",
75
+ "--planner-resumable",
76
+ "--fallback-reason",
77
+ "--fallback-attempted-id",
78
+ "--fallback-stage-n",
79
+ "--fallback-receipt-path",
59
80
  ]);
60
81
 
61
82
  function flagValue(args: readonly string[], flag: string): string | undefined {
@@ -144,37 +165,190 @@ function ralplanStatePath(cwd: string, sessionId: string | undefined): string {
144
165
  }
145
166
 
146
167
  async function readActiveRunId(cwd: string, sessionId: string | undefined): Promise<string | undefined> {
147
- try {
148
- const raw = await fs.readFile(ralplanStatePath(cwd, sessionId), "utf-8");
149
- const parsed = JSON.parse(raw) as { run_id?: unknown };
150
- const candidate = typeof parsed.run_id === "string" ? parsed.run_id.trim() : "";
151
- if (!candidate) return undefined;
152
- assertSafePathComponent(candidate, "run-id");
153
- return candidate;
154
- } catch {
155
- return undefined;
168
+ const statePath = ralplanStatePath(cwd, sessionId);
169
+ const existingRead = await readExistingStateForMutation(statePath);
170
+ if (existingRead.kind === "absent") return undefined;
171
+ if (existingRead.kind === "corrupt") {
172
+ throw new RalplanCommandError(
173
+ 2,
174
+ `existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to overwrite ${statePath}`,
175
+ );
156
176
  }
177
+ const candidate = typeof existingRead.value.run_id === "string" ? existingRead.value.run_id.trim() : "";
178
+ if (!candidate) return undefined;
179
+ assertSafePathComponent(candidate, "run-id");
180
+ return candidate;
157
181
  }
158
182
 
159
183
  async function persistActiveRunId(cwd: string, sessionId: string | undefined, runId: string): Promise<void> {
160
184
  const statePath = ralplanStatePath(cwd, sessionId);
161
- let existing: Record<string, unknown> = {};
162
- try {
163
- const raw = await fs.readFile(statePath, "utf-8");
164
- const parsed = JSON.parse(raw);
165
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
166
- existing = parsed as Record<string, unknown>;
167
- }
168
- } catch {
169
- // fresh receipt; fall through to create
185
+ const existingRead = await readExistingStateForMutation(statePath);
186
+ if (existingRead.kind === "corrupt") {
187
+ throw new RalplanCommandError(
188
+ 2,
189
+ `existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to overwrite ${statePath}`,
190
+ );
170
191
  }
171
- if (existing.run_id === runId) return;
192
+ let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
193
+
194
+ if (existing.run_id === runId && existing.version === WORKFLOW_STATE_VERSION) return;
172
195
  existing.run_id = runId;
173
196
  if (typeof existing.skill !== "string") existing.skill = "ralplan";
174
197
  if (typeof existing.active !== "boolean") existing.active = true;
198
+ if (typeof existing.current_phase !== "string") existing.current_phase = "planner";
199
+ existing = migrateWorkflowState(existing, "ralplan").state;
175
200
  existing.updated_at = new Date().toISOString();
176
- await fs.mkdir(path.dirname(statePath), { recursive: true });
177
- await fs.writeFile(statePath, `${JSON.stringify(existing, null, 2)}\n`);
201
+ await writeWorkflowEnvelopeAtomic(statePath, existing, {
202
+ cwd,
203
+ receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan persist-run-id", sessionId },
204
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
205
+ });
206
+ }
207
+
208
+ /* --------------------------- planner run-state --------------------------- */
209
+
210
+ interface PlannerStateUpdate {
211
+ subagentId?: string;
212
+ resumable?: boolean;
213
+ fallbackReason?: string;
214
+ fallbackAttemptedId?: string;
215
+ fallbackStageN?: number;
216
+ fallbackReceiptPath?: string;
217
+ }
218
+
219
+ function parseBooleanFlag(raw: string, flag: string): boolean {
220
+ if (raw === "true") return true;
221
+ if (raw === "false") return false;
222
+ throw new RalplanCommandError(2, `invalid ${flag}: ${raw}. Expected "true" or "false".`);
223
+ }
224
+
225
+ function assertSubagentId(value: string, label: string): void {
226
+ if (!SUBAGENT_ID_RE.test(value)) {
227
+ throw new RalplanCommandError(2, `invalid ${label}: ${value}`);
228
+ }
229
+ }
230
+
231
+ function plannerFlagValue(args: readonly string[], flag: string): string | undefined {
232
+ const value = flagValue(args, flag);
233
+ if (value === undefined && hasFlag(args, flag)) {
234
+ throw new RalplanCommandError(2, `missing value for ${flag}.`);
235
+ }
236
+ return value;
237
+ }
238
+
239
+ /**
240
+ * Parse the optional persisted-Planner metadata flags that may ride alongside a
241
+ * `--write`. Returns `undefined` when none are present so existing writes are
242
+ * unaffected. Throws `RalplanCommandError` on any malformed value. This records
243
+ * a same-session audit/routing hint, not a durable subagent registry.
244
+ */
245
+ function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | undefined {
246
+ const subagentId = plannerFlagValue(args, "--planner-id");
247
+ const resumableRaw = plannerFlagValue(args, "--planner-resumable");
248
+ const fallbackReason = plannerFlagValue(args, "--fallback-reason");
249
+ const fallbackAttemptedId = plannerFlagValue(args, "--fallback-attempted-id");
250
+ const fallbackStageNRaw = plannerFlagValue(args, "--fallback-stage-n");
251
+ const fallbackReceiptPath = plannerFlagValue(args, "--fallback-receipt-path");
252
+
253
+ const anyPresent = [
254
+ subagentId,
255
+ resumableRaw,
256
+ fallbackReason,
257
+ fallbackAttemptedId,
258
+ fallbackStageNRaw,
259
+ fallbackReceiptPath,
260
+ ].some(value => value !== undefined);
261
+ if (!anyPresent) return undefined;
262
+
263
+ const update: PlannerStateUpdate = {};
264
+
265
+ if (subagentId !== undefined) {
266
+ assertSubagentId(subagentId, "--planner-id");
267
+ update.subagentId = subagentId;
268
+ }
269
+ if (resumableRaw !== undefined) {
270
+ update.resumable = parseBooleanFlag(resumableRaw, "--planner-resumable");
271
+ }
272
+
273
+ const anyFallback = [fallbackReason, fallbackAttemptedId, fallbackStageNRaw, fallbackReceiptPath].some(
274
+ value => value !== undefined,
275
+ );
276
+ if (anyFallback) {
277
+ if (!fallbackReason) {
278
+ throw new RalplanCommandError(2, "--fallback-reason is required when recording planner fallback metadata.");
279
+ }
280
+ if (!KNOWN_FALLBACK_REASONS.has(fallbackReason)) {
281
+ throw new RalplanCommandError(
282
+ 2,
283
+ `invalid --fallback-reason: ${fallbackReason}. Expected one of: ${[...KNOWN_FALLBACK_REASONS].join(", ")}.`,
284
+ );
285
+ }
286
+ update.fallbackReason = fallbackReason;
287
+ if (fallbackAttemptedId === undefined) {
288
+ throw new RalplanCommandError(
289
+ 2,
290
+ "--fallback-attempted-id is required when recording planner fallback metadata.",
291
+ );
292
+ }
293
+ assertSubagentId(fallbackAttemptedId, "--fallback-attempted-id");
294
+ update.fallbackAttemptedId = fallbackAttemptedId;
295
+ if (fallbackStageNRaw === undefined) {
296
+ throw new RalplanCommandError(2, "--fallback-stage-n is required when recording planner fallback metadata.");
297
+ }
298
+ update.fallbackStageN = parseStageN(fallbackStageNRaw);
299
+ if (fallbackReceiptPath !== undefined) {
300
+ if (fallbackReceiptPath.trim() === "") {
301
+ throw new RalplanCommandError(2, "--fallback-receipt-path must not be empty.");
302
+ }
303
+ update.fallbackReceiptPath = fallbackReceiptPath;
304
+ }
305
+ }
306
+
307
+ return update;
308
+ }
309
+
310
+ /** Snake-case projection of a PlannerStateUpdate for state JSON + receipts. Omitted fields stay absent — an unknown `planner_resumable` is encoded by omission, never literal null. */
311
+ function plannerStatePayload(update: PlannerStateUpdate): Record<string, unknown> {
312
+ const payload: Record<string, unknown> = {};
313
+ if (update.subagentId !== undefined) payload.planner_subagent_id = update.subagentId;
314
+ if (update.resumable !== undefined) payload.planner_resumable = update.resumable;
315
+ if (update.fallbackReason !== undefined) payload.planner_fallback_reason = update.fallbackReason;
316
+ if (update.fallbackAttemptedId !== undefined) payload.planner_fallback_attempted_id = update.fallbackAttemptedId;
317
+ if (update.fallbackStageN !== undefined) payload.planner_fallback_stage_n = update.fallbackStageN;
318
+ if (update.fallbackReceiptPath !== undefined) payload.planner_fallback_receipt_path = update.fallbackReceiptPath;
319
+ return payload;
320
+ }
321
+
322
+ /**
323
+ * Merge persisted-Planner metadata into the ralplan run-state JSON. Same-session
324
+ * audit/routing hint only — it records what the caller has already proven and is
325
+ * NOT a durable cross-process subagent registry.
326
+ */
327
+ async function applyPlannerStateUpdate(
328
+ cwd: string,
329
+ sessionId: string | undefined,
330
+ update: PlannerStateUpdate,
331
+ ): Promise<void> {
332
+ const statePath = ralplanStatePath(cwd, sessionId);
333
+ const existingRead = await readExistingStateForMutation(statePath);
334
+ if (existingRead.kind === "corrupt") {
335
+ throw new RalplanCommandError(
336
+ 2,
337
+ `existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to overwrite ${statePath}`,
338
+ );
339
+ }
340
+ let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
341
+ Object.assign(existing, plannerStatePayload(update));
342
+ if (typeof existing.skill !== "string") existing.skill = "ralplan";
343
+ if (typeof existing.active !== "boolean") existing.active = true;
344
+ if (typeof existing.current_phase !== "string") existing.current_phase = "planner";
345
+ existing = migrateWorkflowState(existing, "ralplan").state;
346
+ existing.updated_at = new Date().toISOString();
347
+ await writeWorkflowEnvelopeAtomic(statePath, existing, {
348
+ cwd,
349
+ receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan planner-state", sessionId },
350
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
351
+ });
178
352
  }
179
353
 
180
354
  async function resolveArtifactArgs(args: readonly string[], cwd: string): Promise<ResolvedArtifactArgs> {
@@ -220,27 +394,36 @@ interface PersistedArtifact {
220
394
 
221
395
  async function persistArtifact(resolved: ResolvedArtifactArgs, cwd: string): Promise<PersistedArtifact> {
222
396
  const runDir = path.join(cwd, ".gjc", "plans", "ralplan", resolved.runId);
223
- await fs.mkdir(runDir, { recursive: true });
397
+
224
398
  const fileName = `stage-${pad2(resolved.stageN)}-${resolved.stage}.md`;
225
399
  const filePath = path.join(runDir, fileName);
226
400
  const content = resolved.artifact.endsWith("\n") ? resolved.artifact : `${resolved.artifact}\n`;
227
- await fs.writeFile(filePath, content);
401
+ await writeArtifact(filePath, content, {
402
+ cwd,
403
+ audit: { category: "artifact", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
404
+ });
228
405
 
229
406
  const sha256 = createHash("sha256").update(content).digest("hex");
230
407
  const createdAt = new Date().toISOString();
231
- const indexLine = `${JSON.stringify({
408
+ const indexEntry = {
232
409
  stage: resolved.stage,
233
410
  stage_n: resolved.stageN,
234
411
  path: filePath,
235
412
  created_at: createdAt,
236
413
  sha256,
237
- })}\n`;
238
- await fs.appendFile(path.join(runDir, "index.jsonl"), indexLine);
414
+ };
415
+ await appendJsonl(path.join(runDir, "index.jsonl"), indexEntry, {
416
+ cwd,
417
+ audit: { category: "ledger", verb: "append", owner: "gjc-runtime", skill: "ralplan" },
418
+ });
239
419
 
240
420
  let pendingApprovalPath: string | undefined;
241
421
  if (resolved.stage === "final") {
242
422
  pendingApprovalPath = path.join(runDir, "pending-approval.md");
243
- await fs.writeFile(pendingApprovalPath, content);
423
+ await writeArtifact(pendingApprovalPath, content, {
424
+ cwd,
425
+ audit: { category: "artifact", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
426
+ });
244
427
  }
245
428
 
246
429
  return {
@@ -284,8 +467,12 @@ async function syncRalplanHud(options: {
284
467
  }
285
468
 
286
469
  async function handleArtifactWrite(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
470
+ const plannerState = parsePlannerStateArgs(args);
287
471
  const resolved = await resolveArtifactArgs(args, cwd);
288
472
  const persisted = await persistArtifact(resolved, cwd);
473
+ if (plannerState) {
474
+ await applyPlannerStateUpdate(cwd, resolved.sessionId, plannerState);
475
+ }
289
476
  await syncRalplanHud({
290
477
  cwd,
291
478
  sessionId: resolved.sessionId,
@@ -303,6 +490,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
303
490
  created_at: persisted.createdAt,
304
491
  };
305
492
  if (persisted.pendingApprovalPath) payload.pending_approval_path = persisted.pendingApprovalPath;
493
+ if (plannerState) payload.planner_state = plannerStatePayload(plannerState);
306
494
  const stdout = resolved.json
307
495
  ? `${JSON.stringify(payload, null, 2)}\n`
308
496
  : `Persisted ralplan ${persisted.stage} stage ${persisted.stageN} at ${persisted.path}.\n`;
@@ -382,7 +570,7 @@ async function seedRalplanState(
382
570
  const stateDir = resolved.sessionId
383
571
  ? path.join(cwd, ".gjc", "state", "sessions", encodeSessionSegment(resolved.sessionId))
384
572
  : path.join(cwd, ".gjc", "state");
385
- await fs.mkdir(stateDir, { recursive: true });
573
+
386
574
  const statePath = path.join(stateDir, "ralplan-state.json");
387
575
  // Reuse an existing run id when present so a re-invocation of `gjc ralplan "task"` doesn't
388
576
  // orphan in-progress artifacts under a fresh run id.
@@ -394,6 +582,7 @@ async function seedRalplanState(
394
582
  active: true,
395
583
  current_phase: "planner",
396
584
  skill: "ralplan",
585
+ version: WORKFLOW_STATE_VERSION,
397
586
  mode: resolved.deliberate ? "deliberate" : "short",
398
587
  interactive: resolved.interactive,
399
588
  task: resolved.task,
@@ -403,7 +592,17 @@ async function seedRalplanState(
403
592
  if (resolved.architectKind) payload.architect_kind = resolved.architectKind;
404
593
  if (resolved.criticKind) payload.critic_kind = resolved.criticKind;
405
594
  if (resolved.sessionId) payload.session_id = resolved.sessionId;
406
- await fs.writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`);
595
+ await writeWorkflowEnvelopeAtomic(statePath, payload, {
596
+ cwd,
597
+ receipt: {
598
+ cwd,
599
+ skill: "ralplan",
600
+ owner: "gjc-runtime",
601
+ command: "gjc ralplan seed",
602
+ sessionId: resolved.sessionId,
603
+ },
604
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
605
+ });
407
606
  return { statePath, runId };
408
607
  }
409
608
 
@@ -426,26 +625,19 @@ async function handleConsensusHandoff(args: readonly string[], cwd: string): Pro
426
625
  const summary = {
427
626
  skill: "ralplan",
428
627
  mode,
429
- interactive: resolved.interactive,
430
- architect: resolved.architectKind ?? "default",
431
- critic: resolved.criticKind ?? "default",
432
- task: resolved.task,
433
628
  state_path: statePath,
434
629
  run_id: runId,
435
- handoff: "Run `/skill:ralplan` inside the GJC agent to drive the Planner / Architect / Critic consensus loop.",
630
+ handoff: "/skill:ralplan",
436
631
  };
437
632
  const stdout = resolved.json
438
- ? `${JSON.stringify(summary, null, 2)}\n`
633
+ ? renderCliWriteReceipt({ ok: true, ...summary })
439
634
  : [
440
- `Seeded ralplan ${summary.mode} run (${resolved.interactive ? "interactive" : "automated"}) at ${statePath}.`,
441
- `Active run_id: ${runId}`,
442
- resolved.architectKind ? `Architect: ${resolved.architectKind}` : undefined,
443
- resolved.criticKind ? `Critic: ${resolved.criticKind}` : undefined,
444
- "Run `/skill:ralplan` inside the GJC agent to execute the consensus loop.",
635
+ `ralplan seed run_id=${runId}`,
636
+ `state_path=${statePath}`,
637
+ `mode=${mode} interactive=${resolved.interactive} architect=${resolved.architectKind ?? "default"} critic=${resolved.criticKind ?? "default"}`,
638
+ "handoff=/skill:ralplan",
445
639
  "",
446
- ]
447
- .filter((line): line is string => Boolean(line))
448
- .join("\n");
640
+ ].join("\n");
449
641
  return { status: 0, stdout };
450
642
  }
451
643
 
@@ -0,0 +1,86 @@
1
+ import type { CanonicalGjcWorkflowSkill } from "../skill-state/active-state";
2
+ import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../skill-state/active-state";
3
+ import { getSkillManifest } from "./workflow-manifest";
4
+
5
+ export type StateGraphSkill = CanonicalGjcWorkflowSkill | "all";
6
+ export type StateGraphFormat = "ascii" | "mermaid" | "dot";
7
+
8
+ function assertGraphFormat(format: string): asserts format is StateGraphFormat {
9
+ if (format !== "ascii" && format !== "mermaid" && format !== "dot") {
10
+ throw new Error(`Invalid graph format: ${format}. Expected one of: ascii, mermaid, dot.`);
11
+ }
12
+ }
13
+
14
+ function skillsFor(skill: StateGraphSkill): CanonicalGjcWorkflowSkill[] {
15
+ return skill === "all" ? [...CANONICAL_GJC_WORKFLOW_SKILLS] : [skill];
16
+ }
17
+
18
+ function renderAscii(skill: StateGraphSkill): string {
19
+ const chunks = skillsFor(skill).map(item => {
20
+ const manifest = getSkillManifest(item);
21
+ const states = manifest.states
22
+ .map(state => {
23
+ const markers = [state.initial ? "initial" : undefined, state.terminal ? "terminal" : undefined]
24
+ .filter(Boolean)
25
+ .join(", ");
26
+ return ` - ${state.id}${markers ? ` (${markers})` : ""}`;
27
+ })
28
+ .join("\n");
29
+ const transitions = manifest.transitions
30
+ .map(transition => ` - ${transition.from} -> ${transition.to} [${transition.verb}]`)
31
+ .join("\n");
32
+ return `${manifest.skill} (${manifest.graphLabel})\nstates:\n${states}\ntransitions:\n${transitions}`;
33
+ });
34
+ return `${chunks.join("\n\n")}\n`;
35
+ }
36
+
37
+ function renderMermaid(skill: StateGraphSkill): string {
38
+ const lines = ["stateDiagram-v2"];
39
+ for (const item of skillsFor(skill)) {
40
+ const manifest = getSkillManifest(item);
41
+ lines.push(` state "${manifest.graphLabel}" as ${item} {`);
42
+ lines.push(` [*] --> ${manifest.initialState}`);
43
+ for (const transition of manifest.transitions) {
44
+ lines.push(` ${transition.from} --> ${transition.to}: ${transition.verb}`);
45
+ }
46
+ for (const terminal of manifest.terminalStates) {
47
+ lines.push(` ${terminal} --> [*]`);
48
+ }
49
+ lines.push(" }");
50
+ }
51
+ return `${lines.join("\n")}\n`;
52
+ }
53
+
54
+ function dotId(skill: CanonicalGjcWorkflowSkill, state: string): string {
55
+ return `"${skill}:${state}"`;
56
+ }
57
+
58
+ function renderDot(skill: StateGraphSkill): string {
59
+ const lines = ["digraph gjc_state {", " rankdir=LR;"];
60
+ for (const item of skillsFor(skill)) {
61
+ const manifest = getSkillManifest(item);
62
+ lines.push(` subgraph "cluster_${item}" {`);
63
+ lines.push(` label="${manifest.graphLabel}";`);
64
+ for (const state of manifest.states) {
65
+ const shape = state.terminal ? "doublecircle" : "circle";
66
+ lines.push(` ${dotId(item, state.id)} [label="${state.id}", shape=${shape}];`);
67
+ }
68
+ lines.push(` "${item}:__start" [label="", shape=point];`);
69
+ lines.push(` "${item}:__start" -> ${dotId(item, manifest.initialState)};`);
70
+ for (const transition of manifest.transitions) {
71
+ lines.push(
72
+ ` ${dotId(item, transition.from)} -> ${dotId(item, transition.to)} [label="${transition.verb}"];`,
73
+ );
74
+ }
75
+ lines.push(" }");
76
+ }
77
+ lines.push("}");
78
+ return `${lines.join("\n")}\n`;
79
+ }
80
+
81
+ export function renderStateGraph(skill: StateGraphSkill, format: string = "ascii"): string {
82
+ assertGraphFormat(format);
83
+ if (format === "mermaid") return renderMermaid(skill);
84
+ if (format === "dot") return renderDot(skill);
85
+ return renderAscii(skill);
86
+ }