@gajae-code/coding-agent 0.2.4 → 0.3.0

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 (266) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +145 -2
  4. package/dist/types/commands/harness.d.ts +37 -0
  5. package/dist/types/config/settings-schema.d.ts +13 -3
  6. package/dist/types/config/settings.d.ts +3 -1
  7. package/dist/types/deep-interview/render-middleware.d.ts +5 -0
  8. package/dist/types/discovery/helpers.d.ts +1 -0
  9. package/dist/types/exec/bash-executor.d.ts +8 -1
  10. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  12. package/dist/types/extensibility/shared-events.d.ts +1 -0
  13. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +2 -0
  14. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
  16. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  17. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  18. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  19. package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
  20. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  21. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  22. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  23. package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
  24. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  25. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  26. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  27. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  28. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  29. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  30. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  31. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  32. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  33. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  34. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  35. package/dist/types/harness-control-plane/types.d.ts +162 -0
  36. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  37. package/dist/types/hooks/skill-state.d.ts +2 -29
  38. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  39. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  40. package/dist/types/modes/components/skill-hud/render.d.ts +1 -1
  41. package/dist/types/modes/interactive-mode.d.ts +2 -0
  42. package/dist/types/modes/theme/defaults/index.d.ts +45 -9477
  43. package/dist/types/modes/theme/theme.d.ts +1 -5
  44. package/dist/types/modes/types.d.ts +2 -0
  45. package/dist/types/sdk.d.ts +4 -0
  46. package/dist/types/session/agent-session.d.ts +8 -0
  47. package/dist/types/session/streaming-output.d.ts +11 -0
  48. package/dist/types/skill-state/active-state.d.ts +3 -0
  49. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  50. package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
  51. package/dist/types/task/executor.d.ts +3 -0
  52. package/dist/types/task/types.d.ts +56 -3
  53. package/dist/types/tools/bash-allowed-prefixes.d.ts +5 -0
  54. package/dist/types/tools/bash.d.ts +24 -0
  55. package/dist/types/tools/cron.d.ts +110 -0
  56. package/dist/types/tools/index.d.ts +4 -0
  57. package/dist/types/tools/monitor.d.ts +54 -0
  58. package/dist/types/tools/subagent.d.ts +11 -1
  59. package/dist/types/web/search/index.d.ts +1 -0
  60. package/dist/types/web/search/provider.d.ts +11 -4
  61. package/dist/types/web/search/providers/duckduckgo.d.ts +57 -0
  62. package/dist/types/web/search/types.d.ts +1 -1
  63. package/package.json +7 -7
  64. package/src/async/job-manager.ts +522 -6
  65. package/src/cli/agents-cli.ts +3 -0
  66. package/src/cli/auth-broker-cli.ts +1 -0
  67. package/src/cli/config-cli.ts +10 -2
  68. package/src/cli.ts +2 -0
  69. package/src/commands/harness.ts +592 -0
  70. package/src/commands/team.ts +36 -39
  71. package/src/config/settings-schema.ts +15 -2
  72. package/src/config/settings.ts +49 -7
  73. package/src/deep-interview/render-middleware.ts +366 -0
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +9 -2
  75. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  76. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  77. package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
  78. package/src/discovery/helpers.ts +5 -0
  79. package/src/eval/js/shared/rewrite-imports.ts +1 -2
  80. package/src/exec/bash-executor.ts +20 -9
  81. package/src/extensibility/custom-tools/types.ts +1 -0
  82. package/src/extensibility/extensions/types.ts +6 -0
  83. package/src/extensibility/shared-events.ts +1 -0
  84. package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
  85. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  86. package/src/gjc-runtime/ralplan-runtime.ts +27 -10
  87. package/src/gjc-runtime/restricted-role-agent-bash.ts +5 -0
  88. package/src/gjc-runtime/state-graph.ts +86 -0
  89. package/src/gjc-runtime/state-migrations.ts +132 -0
  90. package/src/gjc-runtime/state-renderer.ts +345 -0
  91. package/src/gjc-runtime/state-runtime.ts +733 -21
  92. package/src/gjc-runtime/state-validation.ts +49 -0
  93. package/src/gjc-runtime/state-writer.ts +718 -0
  94. package/src/gjc-runtime/team-runtime.ts +1083 -89
  95. package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
  96. package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
  97. package/src/gjc-runtime/workflow-manifest.ts +425 -0
  98. package/src/harness-control-plane/classifier.ts +128 -0
  99. package/src/harness-control-plane/control-endpoint.ts +137 -0
  100. package/src/harness-control-plane/finalize.ts +222 -0
  101. package/src/harness-control-plane/frame-mapper.ts +286 -0
  102. package/src/harness-control-plane/operate.ts +225 -0
  103. package/src/harness-control-plane/owner.ts +553 -0
  104. package/src/harness-control-plane/preserve.ts +102 -0
  105. package/src/harness-control-plane/receipts.ts +216 -0
  106. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  107. package/src/harness-control-plane/seams.ts +39 -0
  108. package/src/harness-control-plane/session-lease.ts +388 -0
  109. package/src/harness-control-plane/state-machine.ts +97 -0
  110. package/src/harness-control-plane/storage.ts +257 -0
  111. package/src/harness-control-plane/types.ts +214 -0
  112. package/src/hooks/skill-keywords.ts +4 -2
  113. package/src/hooks/skill-state.ts +25 -42
  114. package/src/internal-urls/docs-index.generated.ts +6 -4
  115. package/src/lsp/render.ts +1 -1
  116. package/src/modes/acp/acp-agent.ts +1 -1
  117. package/src/modes/acp/acp-client-bridge.ts +1 -1
  118. package/src/modes/components/agent-dashboard.ts +1 -1
  119. package/src/modes/components/assistant-message.ts +5 -1
  120. package/src/modes/components/diff.ts +2 -2
  121. package/src/modes/components/hook-selector.ts +72 -2
  122. package/src/modes/components/skill-hud/render.ts +7 -2
  123. package/src/modes/controllers/event-controller.ts +71 -6
  124. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  125. package/src/modes/controllers/input-controller.ts +19 -3
  126. package/src/modes/controllers/selector-controller.ts +3 -2
  127. package/src/modes/interactive-mode.ts +21 -2
  128. package/src/modes/theme/defaults/index.ts +0 -196
  129. package/src/modes/theme/theme.ts +35 -35
  130. package/src/modes/types.ts +2 -0
  131. package/src/prompts/agents/architect.md +5 -1
  132. package/src/prompts/agents/critic.md +5 -1
  133. package/src/prompts/agents/executor.md +13 -0
  134. package/src/prompts/agents/frontmatter.md +1 -0
  135. package/src/prompts/agents/planner.md +5 -1
  136. package/src/prompts/tools/bash.md +9 -0
  137. package/src/prompts/tools/cron.md +25 -0
  138. package/src/prompts/tools/monitor.md +30 -0
  139. package/src/prompts/tools/subagent.md +33 -3
  140. package/src/runtime-mcp/oauth-flow.ts +4 -2
  141. package/src/sdk.ts +7 -0
  142. package/src/session/agent-session.ts +247 -38
  143. package/src/session/session-manager.ts +13 -1
  144. package/src/session/streaming-output.ts +21 -0
  145. package/src/skill-state/active-state.ts +222 -78
  146. package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
  147. package/src/skill-state/initial-phase.ts +2 -0
  148. package/src/skill-state/workflow-state-contract.ts +26 -0
  149. package/src/task/agents.ts +1 -0
  150. package/src/task/executor.ts +51 -8
  151. package/src/task/index.ts +120 -8
  152. package/src/task/render.ts +6 -3
  153. package/src/task/types.ts +57 -3
  154. package/src/tools/ask.ts +28 -7
  155. package/src/tools/bash-allowed-prefixes.ts +169 -0
  156. package/src/tools/bash.ts +190 -29
  157. package/src/tools/browser/tab-worker.ts +1 -1
  158. package/src/tools/cron.ts +665 -0
  159. package/src/tools/index.ts +20 -2
  160. package/src/tools/monitor.ts +136 -0
  161. package/src/tools/subagent.ts +255 -64
  162. package/src/vim/engine.ts +3 -3
  163. package/src/web/search/index.ts +31 -18
  164. package/src/web/search/provider.ts +57 -12
  165. package/src/web/search/providers/duckduckgo.ts +279 -0
  166. package/src/web/search/types.ts +2 -0
  167. package/src/modes/theme/dark.json +0 -95
  168. package/src/modes/theme/defaults/alabaster.json +0 -93
  169. package/src/modes/theme/defaults/amethyst.json +0 -96
  170. package/src/modes/theme/defaults/anthracite.json +0 -93
  171. package/src/modes/theme/defaults/basalt.json +0 -91
  172. package/src/modes/theme/defaults/birch.json +0 -95
  173. package/src/modes/theme/defaults/dark-abyss.json +0 -91
  174. package/src/modes/theme/defaults/dark-arctic.json +0 -104
  175. package/src/modes/theme/defaults/dark-aurora.json +0 -95
  176. package/src/modes/theme/defaults/dark-catppuccin.json +0 -107
  177. package/src/modes/theme/defaults/dark-cavern.json +0 -91
  178. package/src/modes/theme/defaults/dark-copper.json +0 -95
  179. package/src/modes/theme/defaults/dark-cosmos.json +0 -90
  180. package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
  181. package/src/modes/theme/defaults/dark-dracula.json +0 -98
  182. package/src/modes/theme/defaults/dark-eclipse.json +0 -91
  183. package/src/modes/theme/defaults/dark-ember.json +0 -95
  184. package/src/modes/theme/defaults/dark-equinox.json +0 -90
  185. package/src/modes/theme/defaults/dark-forest.json +0 -96
  186. package/src/modes/theme/defaults/dark-github.json +0 -105
  187. package/src/modes/theme/defaults/dark-gruvbox.json +0 -112
  188. package/src/modes/theme/defaults/dark-lavender.json +0 -95
  189. package/src/modes/theme/defaults/dark-lunar.json +0 -89
  190. package/src/modes/theme/defaults/dark-midnight.json +0 -95
  191. package/src/modes/theme/defaults/dark-monochrome.json +0 -94
  192. package/src/modes/theme/defaults/dark-monokai.json +0 -98
  193. package/src/modes/theme/defaults/dark-nebula.json +0 -90
  194. package/src/modes/theme/defaults/dark-nord.json +0 -97
  195. package/src/modes/theme/defaults/dark-ocean.json +0 -101
  196. package/src/modes/theme/defaults/dark-one.json +0 -100
  197. package/src/modes/theme/defaults/dark-poimandres.json +0 -141
  198. package/src/modes/theme/defaults/dark-rainforest.json +0 -91
  199. package/src/modes/theme/defaults/dark-reef.json +0 -91
  200. package/src/modes/theme/defaults/dark-retro.json +0 -92
  201. package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
  202. package/src/modes/theme/defaults/dark-sakura.json +0 -95
  203. package/src/modes/theme/defaults/dark-slate.json +0 -95
  204. package/src/modes/theme/defaults/dark-solarized.json +0 -97
  205. package/src/modes/theme/defaults/dark-solstice.json +0 -90
  206. package/src/modes/theme/defaults/dark-starfall.json +0 -91
  207. package/src/modes/theme/defaults/dark-sunset.json +0 -99
  208. package/src/modes/theme/defaults/dark-swamp.json +0 -90
  209. package/src/modes/theme/defaults/dark-synthwave.json +0 -103
  210. package/src/modes/theme/defaults/dark-taiga.json +0 -91
  211. package/src/modes/theme/defaults/dark-terminal.json +0 -95
  212. package/src/modes/theme/defaults/dark-tokyo-night.json +0 -101
  213. package/src/modes/theme/defaults/dark-tundra.json +0 -91
  214. package/src/modes/theme/defaults/dark-twilight.json +0 -91
  215. package/src/modes/theme/defaults/dark-volcanic.json +0 -91
  216. package/src/modes/theme/defaults/graphite.json +0 -92
  217. package/src/modes/theme/defaults/light-arctic.json +0 -107
  218. package/src/modes/theme/defaults/light-aurora-day.json +0 -91
  219. package/src/modes/theme/defaults/light-canyon.json +0 -91
  220. package/src/modes/theme/defaults/light-catppuccin.json +0 -106
  221. package/src/modes/theme/defaults/light-cirrus.json +0 -90
  222. package/src/modes/theme/defaults/light-coral.json +0 -95
  223. package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
  224. package/src/modes/theme/defaults/light-dawn.json +0 -90
  225. package/src/modes/theme/defaults/light-dunes.json +0 -91
  226. package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
  227. package/src/modes/theme/defaults/light-forest.json +0 -100
  228. package/src/modes/theme/defaults/light-frost.json +0 -95
  229. package/src/modes/theme/defaults/light-github.json +0 -115
  230. package/src/modes/theme/defaults/light-glacier.json +0 -91
  231. package/src/modes/theme/defaults/light-gruvbox.json +0 -108
  232. package/src/modes/theme/defaults/light-haze.json +0 -90
  233. package/src/modes/theme/defaults/light-honeycomb.json +0 -95
  234. package/src/modes/theme/defaults/light-lagoon.json +0 -91
  235. package/src/modes/theme/defaults/light-lavender.json +0 -95
  236. package/src/modes/theme/defaults/light-meadow.json +0 -91
  237. package/src/modes/theme/defaults/light-mint.json +0 -95
  238. package/src/modes/theme/defaults/light-monochrome.json +0 -101
  239. package/src/modes/theme/defaults/light-ocean.json +0 -99
  240. package/src/modes/theme/defaults/light-one.json +0 -99
  241. package/src/modes/theme/defaults/light-opal.json +0 -91
  242. package/src/modes/theme/defaults/light-orchard.json +0 -91
  243. package/src/modes/theme/defaults/light-paper.json +0 -95
  244. package/src/modes/theme/defaults/light-poimandres.json +0 -141
  245. package/src/modes/theme/defaults/light-prism.json +0 -90
  246. package/src/modes/theme/defaults/light-retro.json +0 -98
  247. package/src/modes/theme/defaults/light-sand.json +0 -95
  248. package/src/modes/theme/defaults/light-savanna.json +0 -91
  249. package/src/modes/theme/defaults/light-solarized.json +0 -102
  250. package/src/modes/theme/defaults/light-soleil.json +0 -90
  251. package/src/modes/theme/defaults/light-sunset.json +0 -99
  252. package/src/modes/theme/defaults/light-synthwave.json +0 -98
  253. package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
  254. package/src/modes/theme/defaults/light-wetland.json +0 -91
  255. package/src/modes/theme/defaults/light-zenith.json +0 -89
  256. package/src/modes/theme/defaults/limestone.json +0 -94
  257. package/src/modes/theme/defaults/mahogany.json +0 -97
  258. package/src/modes/theme/defaults/marble.json +0 -93
  259. package/src/modes/theme/defaults/obsidian.json +0 -91
  260. package/src/modes/theme/defaults/onyx.json +0 -91
  261. package/src/modes/theme/defaults/pearl.json +0 -93
  262. package/src/modes/theme/defaults/porcelain.json +0 -91
  263. package/src/modes/theme/defaults/quartz.json +0 -96
  264. package/src/modes/theme/defaults/sandstone.json +0 -95
  265. package/src/modes/theme/defaults/titanium.json +0 -90
  266. package/src/modes/theme/light.json +0 -93
@@ -2,10 +2,12 @@ import { createHash, randomBytes } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
+ import { Settings } from "../config/settings";
5
6
  import { syncSkillActiveState } from "../skill-state/active-state";
6
7
  import { buildDeepInterviewHudSummary } from "../skill-state/workflow-hud";
7
8
  import { runNativeRalplanCommand } from "./ralplan-runtime";
8
9
  import { runNativeStateCommand } from "./state-runtime";
10
+ import { appendJsonl, writeArtifact, writeJsonAtomic } from "./state-writer";
9
11
 
10
12
  /**
11
13
  * Native implementation of `gjc deep-interview`.
@@ -104,13 +106,6 @@ async function readJsonObject(filePath: string): Promise<Record<string, unknown>
104
106
  return {};
105
107
  }
106
108
 
107
- async function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {
108
- await fs.mkdir(path.dirname(filePath), { recursive: true });
109
- const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
110
- await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`);
111
- await fs.rename(tmp, filePath);
112
- }
113
-
114
109
  async function resolveSpecContent(rawSpec: string, cwd: string): Promise<string> {
115
110
  const candidate = path.isAbsolute(rawSpec) ? rawSpec : path.resolve(cwd, rawSpec);
116
111
  try {
@@ -202,9 +197,29 @@ async function readSettingsAmbiguityThreshold(
202
197
  return { threshold: candidate, source: settingsPath };
203
198
  }
204
199
 
200
+ async function readModernSettingsAmbiguityThreshold(
201
+ cwd: string,
202
+ ): Promise<{ threshold: number; source: string } | undefined> {
203
+ const settings = await Settings.init({ cwd });
204
+ const modernConfigPath = path.join(settings.getAgentDir(), "config.yml");
205
+ let parsed: unknown;
206
+ try {
207
+ parsed = (await import("bun")).YAML.parse(await fs.readFile(modernConfigPath, "utf-8"));
208
+ } catch {
209
+ return undefined;
210
+ }
211
+ const candidate = (parsed as { gjc?: { deepInterview?: { ambiguityThreshold?: unknown } } })?.gjc?.deepInterview
212
+ ?.ambiguityThreshold;
213
+ if (typeof candidate !== "number" || !Number.isFinite(candidate) || candidate <= 0 || candidate > 1)
214
+ return undefined;
215
+ return { threshold: candidate, source: modernConfigPath };
216
+ }
217
+
205
218
  async function resolveConfiguredAmbiguityThreshold(
206
219
  cwd: string,
207
220
  ): Promise<{ threshold: number; source: string } | undefined> {
221
+ const modernValue = await readModernSettingsAmbiguityThreshold(cwd);
222
+ if (modernValue) return modernValue;
208
223
  const projectSettings = path.join(cwd, ".gjc", "settings.json");
209
224
  const projectValue = await readSettingsAmbiguityThreshold(projectSettings);
210
225
  if (projectValue) return projectValue;
@@ -373,17 +388,19 @@ export async function persistDeepInterviewSpec(
373
388
  cwd: string,
374
389
  resolved: ResolvedDeepInterviewSpecWriteArgs,
375
390
  ): Promise<PersistedDeepInterviewSpec> {
376
- const specsDir = path.join(cwd, ".gjc", "specs");
377
- await fs.mkdir(specsDir, { recursive: true });
378
- const specPath = path.join(specsDir, `deep-interview-${resolved.slug}.md`);
391
+ const specPath = path.join(cwd, ".gjc", "specs", `deep-interview-${resolved.slug}.md`);
379
392
  const content = resolved.spec.endsWith("\n") ? resolved.spec : `${resolved.spec}\n`;
380
- await fs.writeFile(specPath, content);
393
+ await writeArtifact(specPath, content, {
394
+ cwd,
395
+ audit: { category: "artifact", verb: "write", owner: "gjc-runtime", skill: "deep-interview" },
396
+ });
381
397
 
382
398
  const sha256 = createHash("sha256").update(content).digest("hex");
383
399
  const createdAt = new Date().toISOString();
384
- await fs.appendFile(
385
- path.join(specsDir, "deep-interview-index.jsonl"),
386
- `${JSON.stringify({ slug: resolved.slug, stage: resolved.stage, path: specPath, created_at: createdAt, sha256 })}\n`,
400
+ await appendJsonl(
401
+ path.join(cwd, ".gjc", "specs", "deep-interview-index.jsonl"),
402
+ { slug: resolved.slug, stage: resolved.stage, path: specPath, created_at: createdAt, sha256 },
403
+ { cwd, audit: { category: "ledger", verb: "append", owner: "gjc-runtime", skill: "deep-interview" } },
387
404
  );
388
405
 
389
406
  const statePath = deepInterviewStatePath(cwd, resolved.sessionId);
@@ -402,7 +419,10 @@ export async function persistDeepInterviewSpec(
402
419
  updated_at: createdAt,
403
420
  };
404
421
  if (resolved.sessionId) payload.session_id = resolved.sessionId;
405
- await writeJsonAtomic(statePath, payload);
422
+ await writeJsonAtomic(statePath, payload, {
423
+ cwd,
424
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "deep-interview" },
425
+ });
406
426
  await syncDeepInterviewHud({
407
427
  cwd,
408
428
  sessionId: resolved.sessionId,
@@ -421,11 +441,7 @@ export async function persistDeepInterviewSpec(
421
441
  }
422
442
 
423
443
  async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepInterviewArgs): Promise<string> {
424
- const stateDir = resolved.sessionId
425
- ? path.join(cwd, ".gjc", "state", "sessions", encodeSessionSegment(resolved.sessionId))
426
- : path.join(cwd, ".gjc", "state");
427
- await fs.mkdir(stateDir, { recursive: true });
428
- const statePath = path.join(stateDir, "deep-interview-state.json");
444
+ const statePath = deepInterviewStatePath(cwd, resolved.sessionId);
429
445
  const now = new Date().toISOString();
430
446
  const payload: Record<string, unknown> = {
431
447
  active: true,
@@ -448,7 +464,10 @@ async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepIntervi
448
464
  (payload.state as Record<string, unknown>).language = resolved.language;
449
465
  }
450
466
  if (resolved.sessionId) payload.session_id = resolved.sessionId;
451
- await fs.writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`);
467
+ await writeJsonAtomic(statePath, payload, {
468
+ cwd,
469
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "deep-interview" },
470
+ });
452
471
  return statePath;
453
472
  }
454
473
 
@@ -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,6 +3,8 @@ 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 { isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
7
+ import { appendJsonl, writeArtifact, writeJsonAtomic } from "./state-writer";
6
8
 
7
9
  /**
8
10
  * Native implementation of `gjc ralplan`.
@@ -110,6 +112,7 @@ function defaultRunId(now: Date = new Date()): string {
110
112
  }
111
113
 
112
114
  async function resolveArtifactContent(rawArtifact: string, cwd: string): Promise<string> {
115
+ if (isRestrictedRoleAgentBash()) return rawArtifact;
113
116
  const candidate = path.isAbsolute(rawArtifact) ? rawArtifact : path.resolve(cwd, rawArtifact);
114
117
  try {
115
118
  const stat = await fs.stat(candidate);
@@ -171,8 +174,10 @@ async function persistActiveRunId(cwd: string, sessionId: string | undefined, ru
171
174
  if (typeof existing.skill !== "string") existing.skill = "ralplan";
172
175
  if (typeof existing.active !== "boolean") existing.active = true;
173
176
  existing.updated_at = new Date().toISOString();
174
- await fs.mkdir(path.dirname(statePath), { recursive: true });
175
- await fs.writeFile(statePath, `${JSON.stringify(existing, null, 2)}\n`);
177
+ await writeJsonAtomic(statePath, existing, {
178
+ cwd,
179
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
180
+ });
176
181
  }
177
182
 
178
183
  async function resolveArtifactArgs(args: readonly string[], cwd: string): Promise<ResolvedArtifactArgs> {
@@ -218,27 +223,36 @@ interface PersistedArtifact {
218
223
 
219
224
  async function persistArtifact(resolved: ResolvedArtifactArgs, cwd: string): Promise<PersistedArtifact> {
220
225
  const runDir = path.join(cwd, ".gjc", "plans", "ralplan", resolved.runId);
221
- await fs.mkdir(runDir, { recursive: true });
226
+
222
227
  const fileName = `stage-${pad2(resolved.stageN)}-${resolved.stage}.md`;
223
228
  const filePath = path.join(runDir, fileName);
224
229
  const content = resolved.artifact.endsWith("\n") ? resolved.artifact : `${resolved.artifact}\n`;
225
- await fs.writeFile(filePath, content);
230
+ await writeArtifact(filePath, content, {
231
+ cwd,
232
+ audit: { category: "artifact", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
233
+ });
226
234
 
227
235
  const sha256 = createHash("sha256").update(content).digest("hex");
228
236
  const createdAt = new Date().toISOString();
229
- const indexLine = `${JSON.stringify({
237
+ const indexEntry = {
230
238
  stage: resolved.stage,
231
239
  stage_n: resolved.stageN,
232
240
  path: filePath,
233
241
  created_at: createdAt,
234
242
  sha256,
235
- })}\n`;
236
- await fs.appendFile(path.join(runDir, "index.jsonl"), indexLine);
243
+ };
244
+ await appendJsonl(path.join(runDir, "index.jsonl"), indexEntry, {
245
+ cwd,
246
+ audit: { category: "ledger", verb: "append", owner: "gjc-runtime", skill: "ralplan" },
247
+ });
237
248
 
238
249
  let pendingApprovalPath: string | undefined;
239
250
  if (resolved.stage === "final") {
240
251
  pendingApprovalPath = path.join(runDir, "pending-approval.md");
241
- await fs.writeFile(pendingApprovalPath, content);
252
+ await writeArtifact(pendingApprovalPath, content, {
253
+ cwd,
254
+ audit: { category: "artifact", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
255
+ });
242
256
  }
243
257
 
244
258
  return {
@@ -380,7 +394,7 @@ async function seedRalplanState(
380
394
  const stateDir = resolved.sessionId
381
395
  ? path.join(cwd, ".gjc", "state", "sessions", encodeSessionSegment(resolved.sessionId))
382
396
  : path.join(cwd, ".gjc", "state");
383
- await fs.mkdir(stateDir, { recursive: true });
397
+
384
398
  const statePath = path.join(stateDir, "ralplan-state.json");
385
399
  // Reuse an existing run id when present so a re-invocation of `gjc ralplan "task"` doesn't
386
400
  // orphan in-progress artifacts under a fresh run id.
@@ -401,7 +415,10 @@ async function seedRalplanState(
401
415
  if (resolved.architectKind) payload.architect_kind = resolved.architectKind;
402
416
  if (resolved.criticKind) payload.critic_kind = resolved.criticKind;
403
417
  if (resolved.sessionId) payload.session_id = resolved.sessionId;
404
- await fs.writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`);
418
+ await writeJsonAtomic(statePath, payload, {
419
+ cwd,
420
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
421
+ });
405
422
  return { statePath, runId };
406
423
  }
407
424
 
@@ -0,0 +1,5 @@
1
+ export const GJC_RESTRICTED_ROLE_AGENT_BASH_ENV = "GJC_RESTRICTED_ROLE_AGENT_BASH";
2
+
3
+ export function isRestrictedRoleAgentBash(): boolean {
4
+ return process.env[GJC_RESTRICTED_ROLE_AGENT_BASH_ENV] === "1";
5
+ }
@@ -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
+ }
@@ -0,0 +1,132 @@
1
+ import * as fs from "node:fs/promises";
2
+ import type { CanonicalGjcWorkflowSkill } from "../skill-state/active-state";
3
+ import { initialPhaseForSkill } from "../skill-state/initial-phase";
4
+ import { canonicalWorkflowSkill, WORKFLOW_STATE_RECEIPT_VERSION } from "../skill-state/workflow-state-contract";
5
+ import { writeWorkflowEnvelopeAtomic } from "./state-writer";
6
+ import { getSkillManifest } from "./workflow-manifest";
7
+
8
+ export interface NormalizeLegacyStateResult {
9
+ state: Record<string, unknown>;
10
+ changed: boolean;
11
+ }
12
+
13
+ export interface MigrateAndPersistLegacyStateArgs {
14
+ cwd: string;
15
+ skill: string;
16
+ statePath: string;
17
+ sessionId?: string;
18
+ }
19
+
20
+ export interface MigrateAndPersistLegacyStateResult {
21
+ migrated: boolean;
22
+ path: string;
23
+ }
24
+
25
+ const RECEIPT_STRING_FIELDS = [
26
+ "command",
27
+ "state_path",
28
+ "storage_path",
29
+ "mutated_at",
30
+ "fresh_until",
31
+ "mutation_id",
32
+ ] as const;
33
+
34
+ function cloneRecord(record: Record<string, unknown>): Record<string, unknown> {
35
+ return { ...record };
36
+ }
37
+
38
+ function canonicalSkillOrThrow(skill: string): CanonicalGjcWorkflowSkill {
39
+ const canonical = canonicalWorkflowSkill(skill);
40
+ if (!canonical) throw new Error(`Unsupported GJC workflow skill: ${skill}`);
41
+ return canonical;
42
+ }
43
+
44
+ function safeString(value: unknown): string {
45
+ return typeof value === "string" ? value : "";
46
+ }
47
+
48
+ function legacyPhaseForSkill(skill: CanonicalGjcWorkflowSkill, phase: string): string {
49
+ if (phase === "planning") return initialPhaseForSkill(skill);
50
+ return phase;
51
+ }
52
+
53
+ function normalizePhase(skill: CanonicalGjcWorkflowSkill, value: unknown): string {
54
+ const manifest = getSkillManifest(skill);
55
+ const manifestStates = new Set(manifest.states.map(state => state.id));
56
+ const phase = legacyPhaseForSkill(skill, safeString(value).trim());
57
+ return manifestStates.has(phase) ? phase : manifest.initialState;
58
+ }
59
+
60
+ function receiptWithRequiredFields(raw: unknown, skill: CanonicalGjcWorkflowSkill): Record<string, unknown> {
61
+ const receipt =
62
+ raw && typeof raw === "object" && !Array.isArray(raw) ? cloneRecord(raw as Record<string, unknown>) : {};
63
+ receipt.version = WORKFLOW_STATE_RECEIPT_VERSION;
64
+ receipt.skill = skill;
65
+ if (receipt.owner !== "gjc-state-cli" && receipt.owner !== "gjc-runtime" && receipt.owner !== "gjc-hook") {
66
+ receipt.owner = "gjc-state-cli";
67
+ }
68
+ if (receipt.status !== "fresh" && receipt.status !== "stale") receipt.status = "stale";
69
+ for (const field of RECEIPT_STRING_FIELDS) {
70
+ if (typeof receipt[field] !== "string") receipt[field] = "";
71
+ }
72
+ return receipt;
73
+ }
74
+
75
+ function recordsEqual(left: Record<string, unknown>, right: Record<string, unknown>): boolean {
76
+ return JSON.stringify(left) === JSON.stringify(right);
77
+ }
78
+
79
+ /**
80
+ * Pure legacy state normalizer for background/internal readers.
81
+ *
82
+ * Readers that need compatibility with old on-disk workflow state shapes must call
83
+ * this in-memory helper and must never call `migrateAndPersistLegacyState`. The
84
+ * persist variant is reserved for explicit state migration commands because it is
85
+ * the only path allowed to write normalized upgrades back to `.gjc/state/**`.
86
+ */
87
+ export function normalizeLegacyState(raw: Record<string, unknown>, skill: string): NormalizeLegacyStateResult {
88
+ const canonicalSkill = canonicalSkillOrThrow(skill);
89
+ const state = cloneRecord(raw);
90
+ state.skill = canonicalSkill;
91
+ if (typeof state.version !== "number") state.version = 1;
92
+ if (typeof state.active !== "boolean") state.active = true;
93
+
94
+ const sourcePhase = typeof state.current_phase === "string" ? state.current_phase : state.phase;
95
+ const normalizedPhase = normalizePhase(canonicalSkill, sourcePhase);
96
+ state.current_phase = normalizedPhase;
97
+ if ("phase" in state && typeof state.phase === "string") state.phase = normalizedPhase;
98
+ state.receipt = receiptWithRequiredFields(state.receipt, canonicalSkill);
99
+
100
+ return { state, changed: !recordsEqual(raw, state) };
101
+ }
102
+
103
+ export async function migrateAndPersistLegacyState(
104
+ args: MigrateAndPersistLegacyStateArgs,
105
+ ): Promise<MigrateAndPersistLegacyStateResult> {
106
+ const canonicalSkill = canonicalSkillOrThrow(args.skill);
107
+ const raw = JSON.parse(await fs.readFile(args.statePath, "utf-8")) as unknown;
108
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
109
+ throw new Error(`Workflow state file must contain a JSON object: ${args.statePath}`);
110
+ }
111
+ const { state, changed } = normalizeLegacyState(raw as Record<string, unknown>, canonicalSkill);
112
+ if (!changed) return { migrated: false, path: args.statePath };
113
+
114
+ const persistedPath = await writeWorkflowEnvelopeAtomic(args.statePath, state, {
115
+ cwd: args.cwd,
116
+ receipt: {
117
+ cwd: args.cwd,
118
+ skill: canonicalSkill,
119
+ owner: "gjc-state-cli",
120
+ command: `gjc state ${canonicalSkill} migrate`,
121
+ sessionId: args.sessionId,
122
+ },
123
+ audit: {
124
+ cwd: args.cwd,
125
+ skill: canonicalSkill,
126
+ verb: "migrate",
127
+ owner: "gjc-state-cli",
128
+ category: "state",
129
+ },
130
+ });
131
+ return { migrated: true, path: persistedPath };
132
+ }