@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
@@ -12,12 +12,12 @@ import {
12
12
  getEnumValues,
13
13
  getType,
14
14
  getUi,
15
+ SETTINGS_SCHEMA,
15
16
  type SettingPath,
16
17
  Settings,
17
18
  type SettingValue,
18
19
  settings,
19
20
  } from "../config/settings";
20
- import { SETTINGS_SCHEMA } from "../config/settings-schema";
21
21
  import { theme } from "../modes/theme/theme";
22
22
  import { initXdg } from "./commands/init-xdg";
23
23
 
@@ -183,10 +183,18 @@ function parseAndSetValue(path: SettingPath, rawValue: string): void {
183
183
  else throw new Error(`Invalid boolean value: ${rawValue}. Use true/false, yes/no, on/off, or 1/0`);
184
184
  break;
185
185
  }
186
- case "number":
186
+ case "number": {
187
187
  parsedValue = Number(trimmed);
188
188
  if (!Number.isFinite(parsedValue)) throw new Error(`Invalid number: ${rawValue}`);
189
+ const validate =
190
+ "validate" in SETTINGS_SCHEMA[path]
191
+ ? (SETTINGS_SCHEMA[path].validate as ((value: number) => boolean) | undefined)
192
+ : undefined;
193
+ if (validate?.(parsedValue as number) === false) {
194
+ throw new Error(`Invalid number for ${path}: ${rawValue}`);
195
+ }
189
196
  break;
197
+ }
190
198
  case "enum": {
191
199
  const valid = getEnumValues(path);
192
200
  if (valid && !valid.includes(trimmed)) {
package/src/cli.ts CHANGED
@@ -35,9 +35,11 @@ const commands: CommandEntry[] = [
35
35
  { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
36
36
  { name: "skills", load: () => import("./commands/skills").then(m => m.default) },
37
37
  { name: "session", load: () => import("./commands/session").then(m => m.default) },
38
+ { name: "harness", load: () => import("./commands/harness").then(m => m.default) },
38
39
  { name: "team", load: () => import("./commands/team").then(m => m.default) },
39
40
  { name: "ultragoal", load: () => import("./commands/ultragoal").then(m => m.default) },
40
41
  { name: "ralplan", load: () => import("./commands/ralplan").then(m => m.default) },
42
+ { name: "config", load: () => import("./commands/config").then(m => m.default) },
41
43
  {
42
44
  name: "contribute-pr",
43
45
  aliases: ["contribution-prep"],
@@ -0,0 +1,592 @@
1
+ /**
2
+ * `gjc harness <verb>` — AI-native stateless JSON CLI for the coding-harness
3
+ * operations control plane (v1, gajae-code adapter).
4
+ *
5
+ * Every verb emits the universal contract `{ ok, state, evidence, nextAllowedActions }`.
6
+ * Foundation milestone (M1/M2) implements: start, observe, classify, events, retire,
7
+ * and the spec-required `owner-not-live` blocking for submit. Owner-runtime verbs
8
+ * (recover/validate/finalize/operate) return an honest `pending-<milestone>` contract
9
+ * until the RuntimeOwner (M3+) lands.
10
+ */
11
+ import { execFileSync } from "node:child_process";
12
+ import { existsSync } from "node:fs";
13
+ import { Args, Command, Flags } from "@gajae-code/utils/cli";
14
+ import { resolveGjcTmuxCommand, sanitizeTmuxToken } from "../gjc-runtime/tmux-common";
15
+ import { classifyRecovery } from "../harness-control-plane/classifier";
16
+ import { callEndpoint, EndpointUnreachableError } from "../harness-control-plane/control-endpoint";
17
+ import { RuntimeOwner, resolveOwner } from "../harness-control-plane/owner";
18
+ import { GajaeCodeRpc } from "../harness-control-plane/rpc-adapter";
19
+ import { buildResponse, buildStateView } from "../harness-control-plane/state-machine";
20
+ import {
21
+ generateSessionId,
22
+ readEvents,
23
+ readSessionState,
24
+ resolveHarnessRoot,
25
+ sessionPaths,
26
+ writeSessionState,
27
+ } from "../harness-control-plane/storage";
28
+ import {
29
+ DEFAULT_RETRY_BUDGET,
30
+ type GitDelta,
31
+ type Harness as HarnessKind,
32
+ type Observation,
33
+ type RetryBudget,
34
+ SESSION_SCHEMA_VERSION,
35
+ type SessionHandle,
36
+ type SessionState,
37
+ } from "../harness-control-plane/types";
38
+
39
+ function writeJson(value: unknown): void {
40
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
41
+ }
42
+
43
+ function nowIso(): string {
44
+ return new Date().toISOString();
45
+ }
46
+
47
+ function parseInput(raw: string | undefined): Record<string, unknown> {
48
+ if (!raw?.trim()) return {};
49
+ const parsed = JSON.parse(raw) as unknown;
50
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
51
+ throw new Error("input_must_be_json_object");
52
+ }
53
+ return parsed as Record<string, unknown>;
54
+ }
55
+
56
+ function gitDeltaFor(workspace: string): { gitDelta: GitDelta; branch: string | null; deleted: boolean } {
57
+ if (!existsSync(workspace)) return { gitDelta: "unknown", branch: null, deleted: true };
58
+ let branch: string | null = null;
59
+ try {
60
+ branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
61
+ cwd: workspace,
62
+ encoding: "utf8",
63
+ stdio: ["ignore", "pipe", "ignore"],
64
+ }).trim();
65
+ } catch {
66
+ branch = null;
67
+ }
68
+ try {
69
+ const porcelain = execFileSync("git", ["status", "--porcelain"], {
70
+ cwd: workspace,
71
+ encoding: "utf8",
72
+ stdio: ["ignore", "pipe", "ignore"],
73
+ });
74
+ return { gitDelta: porcelain.trim().length > 0 ? "dirty" : "clean", branch, deleted: false };
75
+ } catch {
76
+ return { gitDelta: "unknown", branch, deleted: false };
77
+ }
78
+ }
79
+
80
+ /** Owner liveness — always false in the foundation build (RuntimeOwner is M3). */
81
+ function ownerLiveFor(_state: SessionState): boolean {
82
+ return false;
83
+ }
84
+
85
+ function buildObservation(state: SessionState, ownerLive: boolean): Observation {
86
+ const workspace = state.handle.workspace;
87
+ const { gitDelta, branch, deleted } = gitDeltaFor(workspace);
88
+ return {
89
+ lifecycle: state.lifecycle,
90
+ ownerLive,
91
+ cwd: workspace,
92
+ branch: branch ?? state.handle.branch,
93
+ gitDelta,
94
+ lastActivityAt: state.updatedAt,
95
+ observedSignals: ["SessionStart"],
96
+ risk: deleted ? "deleted-worktree" : "normal",
97
+ };
98
+ }
99
+
100
+ function resolveRetryBudget(input: Record<string, unknown>): RetryBudget {
101
+ const supplied = input.retryBudget;
102
+ if (supplied && typeof supplied === "object" && !Array.isArray(supplied)) {
103
+ return { ...DEFAULT_RETRY_BUDGET, ...(supplied as Partial<RetryBudget>) };
104
+ }
105
+ return { ...DEFAULT_RETRY_BUDGET };
106
+ }
107
+
108
+ interface OwnerSpawnResult {
109
+ live: boolean;
110
+ runtime: "tmux" | "detached" | "manual";
111
+ tmuxSessionName: string | null;
112
+ fallbackReason: string | null;
113
+ blockerReason: string | null;
114
+ }
115
+
116
+ function shellQuote(value: string): string {
117
+ return `'${value.replaceAll("'", "'\\''")}'`;
118
+ }
119
+
120
+ function deterministicHarnessTmuxSessionName(sessionId: string): string {
121
+ return `gajae_code_harness_${sanitizeTmuxToken(sessionId)}`;
122
+ }
123
+
124
+ async function loadState(root: string, sessionId: string): Promise<SessionState> {
125
+ const state = await readSessionState(root, sessionId);
126
+ if (!state) throw new Error(`session_not_found:${sessionId}`);
127
+ return state;
128
+ }
129
+
130
+ function requireSessionId(input: Record<string, unknown>, flagSession: string | undefined): string {
131
+ const id = flagSession ?? (typeof input.sessionId === "string" ? input.sessionId : undefined);
132
+ if (!id) throw new Error("missing_session_id");
133
+ return id;
134
+ }
135
+
136
+ export default class Harness extends Command {
137
+ static description = "Operate coding harnesses (v1: gajae-code) as a session/evidence/recovery/PR control plane";
138
+ static strict = false;
139
+
140
+ static args = {
141
+ verb: Args.string({
142
+ description: "start|submit|observe|classify|recover|validate|finalize|retire|events|monitor|operate",
143
+ required: true,
144
+ }),
145
+ };
146
+
147
+ static flags = {
148
+ input: Flags.string({ description: "JSON object input for the verb", default: "" }),
149
+ session: Flags.string({ char: "s", description: "Session id (re-grab a session)" }),
150
+ cursor: Flags.string({ description: "Event cursor for events --follow (exclusive)", default: "0" }),
151
+ follow: Flags.boolean({ description: "Tail the owner-written event log", default: false }),
152
+ json: Flags.boolean({ char: "j", description: "Emit machine-readable JSON", default: true }),
153
+ };
154
+
155
+ static examples = [
156
+ `gjc harness start --input '{"harness":"gajae-code","workspace":".","branch":"feat/x"}'`,
157
+ "gjc harness observe --session <id>",
158
+ `gjc harness classify --input '{"observation":{"ownerLive":false,"gitDelta":"dirty","risk":"vanished-dirty"}}'`,
159
+ "gjc harness events --session <id> --follow",
160
+ ];
161
+
162
+ async run(): Promise<void> {
163
+ const { args, flags } = await this.parse(Harness);
164
+ const verb = String(args.verb);
165
+ const root = resolveHarnessRoot();
166
+ try {
167
+ const input = parseInput(flags.input);
168
+ switch (verb) {
169
+ case "start":
170
+ return await this.#start(root, input);
171
+ case "observe":
172
+ return await this.#observe(root, input, flags.session);
173
+ case "classify":
174
+ return await this.#classify(root, input, flags.session);
175
+ case "submit":
176
+ return await this.#submit(root, input, flags.session);
177
+ case "events":
178
+ case "monitor":
179
+ return await this.#events(root, input, flags.session, Number(flags.cursor) || 0);
180
+ case "retire":
181
+ return await this.#retire(root, input, flags.session);
182
+ case "finalize":
183
+ return await this.#finalizeVerb(root, input, flags.session);
184
+ case "__owner":
185
+ return await this.#runOwner(root, input, flags.session);
186
+ case "recover":
187
+ case "validate":
188
+ case "operate":
189
+ return await this.#ownerVerbOrPending(root, verb, input, flags.session);
190
+ default:
191
+ throw new Error(`unknown_harness_verb:${verb}`);
192
+ }
193
+ } catch (error) {
194
+ writeJson({ ok: false, error: error instanceof Error ? error.message : String(error), verb });
195
+ process.exitCode = 1;
196
+ }
197
+ }
198
+
199
+ async #finalizeVerb(root: string, input: Record<string, unknown>, flagSession: string | undefined): Promise<void> {
200
+ const sessionId = requireSessionId(input, flagSession);
201
+ if (await this.#tryOwnerRoute(root, sessionId, "finalize", { ...input, sessionId })) return;
202
+ // finalize is owner-routed; without a live owner, report owner-not-live (start the owner first).
203
+ const state = await loadState(root, sessionId);
204
+ writeJson(buildResponse(state, false, { completed: false, reason: "owner-not-live" }, false));
205
+ process.exitCode = 1;
206
+ }
207
+
208
+ /** Route an owner-backed verb to the live owner; fall back to a pending response when none. */
209
+ async #ownerVerbOrPending(
210
+ root: string,
211
+ verb: string,
212
+ input: Record<string, unknown>,
213
+ flagSession: string | undefined,
214
+ ): Promise<void> {
215
+ const sessionId = flagSession ?? (typeof input.sessionId === "string" ? input.sessionId : undefined);
216
+ if (sessionId && (await this.#tryOwnerRoute(root, sessionId, verb, { ...input, sessionId }))) return;
217
+ return this.#pending(root, verb, input, flagSession);
218
+ }
219
+
220
+ /** Detached owner daemon (spawned by `start --detach`). Runs until retired or signalled. */
221
+ async #runOwner(root: string, input: Record<string, unknown>, flagSession: string | undefined): Promise<void> {
222
+ const sessionId = requireSessionId(input, flagSession);
223
+ const sessionDir = sessionPaths(root, sessionId).gjcSessionDir;
224
+ // Optional rpc command override (tests / non-default hosts); defaults to `gjc --mode rpc`.
225
+ const override = process.env.GJC_HARNESS_RPC_COMMAND;
226
+ const command = override ? (JSON.parse(override) as string[]) : undefined;
227
+ const rpc = new GajaeCodeRpc({ sessionDir, command });
228
+ const owner = new RuntimeOwner({ root, sessionId, rpc });
229
+ const info = await owner.start();
230
+ writeJson({ ok: true, owner: info });
231
+ await new Promise<void>(resolve => {
232
+ const stop = (): void => {
233
+ clearInterval(timer);
234
+ resolve();
235
+ };
236
+ const timer = setInterval(async () => {
237
+ const resolved = await resolveOwner(root, sessionId);
238
+ if (!resolved.live) stop();
239
+ }, 500);
240
+ timer.unref?.();
241
+ process.on("SIGTERM", stop);
242
+ process.on("SIGINT", stop);
243
+ });
244
+ await owner.stop();
245
+ process.exit(0);
246
+ }
247
+
248
+ #buildOwnerCommand(sessionId: string): string[] {
249
+ const argv1 = process.argv[1];
250
+ return argv1
251
+ ? [process.execPath, argv1, "harness", "__owner", "--session", sessionId]
252
+ : [process.execPath, "harness", "__owner", "--session", sessionId];
253
+ }
254
+
255
+ async #waitForOwner(root: string, sessionId: string): Promise<boolean> {
256
+ for (let i = 0; i < 100; i++) {
257
+ const owner = await resolveOwner(root, sessionId);
258
+ if (owner.live && owner.socketPath) {
259
+ try {
260
+ await callEndpoint(owner.socketPath, { verb: "observe", input: { sessionId } }, 250);
261
+ return true;
262
+ } catch (error) {
263
+ if (!(error instanceof EndpointUnreachableError)) throw error;
264
+ }
265
+ }
266
+ await new Promise(r => setTimeout(r, 50));
267
+ }
268
+ return false;
269
+ }
270
+
271
+ #startTmuxResidentOwner(
272
+ root: string,
273
+ sessionId: string,
274
+ cwd: string,
275
+ ): { started: boolean; sessionName: string; reason: string | null } {
276
+ const tmuxCommand = resolveGjcTmuxCommand();
277
+ if (Bun.which(tmuxCommand) === null) {
278
+ return {
279
+ started: false,
280
+ sessionName: deterministicHarnessTmuxSessionName(sessionId),
281
+ reason: "tmux-unavailable",
282
+ };
283
+ }
284
+ const sessionName = deterministicHarnessTmuxSessionName(sessionId);
285
+ const envAssignments = [`GJC_HARNESS_STATE_ROOT=${shellQuote(root)}`];
286
+ if (process.env.GJC_HARNESS_RPC_COMMAND) {
287
+ envAssignments.push(`GJC_HARNESS_RPC_COMMAND=${shellQuote(process.env.GJC_HARNESS_RPC_COMMAND)}`);
288
+ }
289
+ const ownerCommand = this.#buildOwnerCommand(sessionId).map(shellQuote).join(" ");
290
+ const shellCommand = `exec env ${envAssignments.join(" ")} ${ownerCommand}`;
291
+ const created = Bun.spawnSync([tmuxCommand, "new-session", "-d", "-s", sessionName, "-c", cwd, shellCommand], {
292
+ stdout: "pipe",
293
+ stderr: "pipe",
294
+ env: process.env,
295
+ });
296
+ if (created.exitCode === 0) return { started: true, sessionName, reason: null };
297
+ const stderr = created.stderr.toString().trim();
298
+ return { started: false, sessionName, reason: stderr || "tmux-start-failed" };
299
+ }
300
+
301
+ /** Spawn the owner daemon. Prefer a tmux-resident owner, then explicitly fall back to detached. */
302
+ async #spawnDetachedOwner(root: string, sessionId: string, cwd: string): Promise<OwnerSpawnResult> {
303
+ const tmux = this.#startTmuxResidentOwner(root, sessionId, cwd);
304
+ if (tmux.started && (await this.#waitForOwner(root, sessionId))) {
305
+ return {
306
+ live: true,
307
+ runtime: "tmux",
308
+ tmuxSessionName: tmux.sessionName,
309
+ fallbackReason: null,
310
+ blockerReason: null,
311
+ };
312
+ }
313
+ const fallbackReason = tmux.started
314
+ ? "tmux new-session exited 0 but owner endpoint did not become routable"
315
+ : tmux.reason;
316
+ const cmd = this.#buildOwnerCommand(sessionId);
317
+ const child = Bun.spawn(cmd, {
318
+ cwd,
319
+ env: { ...process.env, GJC_HARNESS_STATE_ROOT: root },
320
+ stdout: "ignore",
321
+ stderr: "ignore",
322
+ stdin: "ignore",
323
+ });
324
+ child.unref();
325
+ const live = await this.#waitForOwner(root, sessionId);
326
+ return {
327
+ live,
328
+ runtime: "detached",
329
+ tmuxSessionName: null,
330
+ fallbackReason,
331
+ blockerReason: live ? null : "detached-owner-not-live",
332
+ };
333
+ }
334
+
335
+ async #start(root: string, input: Record<string, unknown>): Promise<void> {
336
+ const harness = (typeof input.harness === "string" ? input.harness : "gajae-code") as HarnessKind;
337
+ if (harness !== "gajae-code") {
338
+ writeJson({
339
+ ok: false,
340
+ error: `harness_unsupported_in_v1:${harness}`,
341
+ evidence: { seam: true, supported: ["gajae-code"] },
342
+ });
343
+ process.exitCode = 1;
344
+ return;
345
+ }
346
+ const workspace = typeof input.workspace === "string" ? input.workspace : process.cwd();
347
+ const sessionId = typeof input.sessionId === "string" ? input.sessionId : generateSessionId();
348
+ const eventsPath = `${root}/sessions/${sessionId}/events.jsonl`;
349
+ const leasePath = `${root}/sessions/${sessionId}/lease.json`;
350
+ const startedAt = nowIso();
351
+ const handle: SessionHandle = {
352
+ sessionId,
353
+ harness,
354
+ repo: typeof input.repo === "string" ? input.repo : null,
355
+ workspace,
356
+ branch: typeof input.branch === "string" ? input.branch : null,
357
+ base: typeof input.base === "string" ? input.base : null,
358
+ issueOrPr: typeof input.issueOrPr === "string" ? input.issueOrPr : null,
359
+ processHandle: { kind: "runtime-owner", ownerId: null, pid: null },
360
+ rpcHandle: { kind: "rpc-subprocess", pid: null, sessionDir: `${root}/sessions/${sessionId}/gjc-session` },
361
+ ownerHandle: { leasePath, endpoint: null, heartbeatAt: null },
362
+ routerHandle: { kind: "default-in-owner", policy: "default-fallback", eventsPath },
363
+ viewportHandle: { kind: "event-monitor", tmuxSessionName: null, viewOnly: true },
364
+ startedAt,
365
+ updatedAt: startedAt,
366
+ };
367
+ const state: SessionState = {
368
+ schemaVersion: SESSION_SCHEMA_VERSION,
369
+ sessionId,
370
+ lifecycle: "started",
371
+ harness,
372
+ handle,
373
+ retries: {},
374
+ blockers: [],
375
+ createdAt: startedAt,
376
+ updatedAt: startedAt,
377
+ };
378
+ await writeSessionState(root, state);
379
+ let ownerLive = false;
380
+ let ownerRuntime: OwnerSpawnResult["runtime"] = "manual";
381
+ let ownerFallbackReason: string | null = null;
382
+ let ownerBlockerReason: string | null = null;
383
+ if (input.detach === true) {
384
+ const ownerSpawn = await this.#spawnDetachedOwner(root, sessionId, workspace);
385
+ ownerLive = ownerSpawn.live;
386
+ ownerRuntime = ownerSpawn.runtime;
387
+ ownerFallbackReason = ownerSpawn.fallbackReason;
388
+ ownerBlockerReason = ownerSpawn.blockerReason;
389
+ handle.viewportHandle = {
390
+ kind: "event-monitor",
391
+ tmuxSessionName: ownerSpawn.tmuxSessionName,
392
+ viewOnly: true,
393
+ };
394
+ if (ownerLive) {
395
+ const resolved = await resolveOwner(root, sessionId);
396
+ handle.processHandle = {
397
+ kind: "runtime-owner",
398
+ ownerId: resolved.lease?.ownerId ?? null,
399
+ pid: resolved.lease?.pid ?? null,
400
+ };
401
+ handle.ownerHandle = {
402
+ leasePath,
403
+ endpoint: resolved.socketPath,
404
+ heartbeatAt: resolved.lease?.heartbeatAt ?? null,
405
+ };
406
+ state.handle = handle;
407
+ await writeSessionState(root, state);
408
+ }
409
+ }
410
+ if (ownerBlockerReason) {
411
+ state.lifecycle = "blocked";
412
+ state.blockers = [...state.blockers, ownerBlockerReason];
413
+ state.handle = handle;
414
+ state.updatedAt = nowIso();
415
+ await writeSessionState(root, state);
416
+ }
417
+ writeJson(
418
+ buildResponse(
419
+ state,
420
+ ownerLive,
421
+ {
422
+ handle,
423
+ ownerRuntime,
424
+ ...(ownerFallbackReason ? { ownerFallbackReason } : {}),
425
+ ...(ownerBlockerReason ? { reason: ownerBlockerReason } : {}),
426
+ },
427
+ !ownerBlockerReason,
428
+ ),
429
+ );
430
+ if (ownerBlockerReason) process.exitCode = 1;
431
+ }
432
+
433
+ /** Returns true if a live owner handled the verb (response already printed). */
434
+ async #tryOwnerRoute(
435
+ root: string,
436
+ sessionId: string,
437
+ verb: string,
438
+ input: Record<string, unknown>,
439
+ ): Promise<boolean> {
440
+ const owner = await resolveOwner(root, sessionId);
441
+ if (!owner.live || !owner.socketPath) return false;
442
+ try {
443
+ const res = (await callEndpoint(owner.socketPath, { verb, input })) as { ok?: boolean };
444
+ writeJson(res);
445
+ if (res?.ok === false) process.exitCode = 1;
446
+ return true;
447
+ } catch (error) {
448
+ if (error instanceof EndpointUnreachableError) return false;
449
+ throw error;
450
+ }
451
+ }
452
+
453
+ async #observe(root: string, input: Record<string, unknown>, flagSession: string | undefined): Promise<void> {
454
+ const sessionId = requireSessionId(input, flagSession);
455
+ if (await this.#tryOwnerRoute(root, sessionId, "observe", { ...input, sessionId })) return;
456
+ const state = await loadState(root, sessionId);
457
+ const ownerLive = ownerLiveFor(state);
458
+ const observation = buildObservation(state, ownerLive);
459
+ writeJson(buildResponse(state, ownerLive, { observation, readOnly: !ownerLive }));
460
+ }
461
+
462
+ async #classify(root: string, input: Record<string, unknown>, flagSession: string | undefined): Promise<void> {
463
+ const budget = resolveRetryBudget(input);
464
+ let observation = input.observation as Partial<Observation> | undefined;
465
+ let stateView: SessionState | null = null;
466
+ const sessionId = flagSession ?? (typeof input.sessionId === "string" ? input.sessionId : undefined);
467
+ if (sessionId) {
468
+ stateView = await loadState(root, sessionId);
469
+ if (!observation) observation = buildObservation(stateView, ownerLiveFor(stateView));
470
+ }
471
+ if (!observation) throw new Error("classify_requires_observation_or_session");
472
+ const full: Observation = {
473
+ lifecycle: observation.lifecycle ?? "observing",
474
+ ownerLive: observation.ownerLive ?? false,
475
+ cwd: observation.cwd ?? ".",
476
+ branch: observation.branch ?? null,
477
+ gitDelta: observation.gitDelta ?? "unknown",
478
+ lastActivityAt: observation.lastActivityAt ?? null,
479
+ observedSignals: observation.observedSignals ?? [],
480
+ risk: observation.risk ?? "normal",
481
+ };
482
+ const decision = classifyRecovery({ observation: full, retryBudget: budget });
483
+ if (stateView) {
484
+ writeJson(buildResponse(stateView, ownerLiveFor(stateView), { decision, observation: full }));
485
+ return;
486
+ }
487
+ // Pure classify without a session: synthesize a minimal state view.
488
+ writeJson({
489
+ ok: true,
490
+ state: {
491
+ sessionId: "(none)",
492
+ lifecycle: full.lifecycle,
493
+ harness: "gajae-code",
494
+ ownerLive: full.ownerLive,
495
+ blockers: [],
496
+ },
497
+ evidence: { decision, observation: full },
498
+ nextAllowedActions: [],
499
+ });
500
+ }
501
+
502
+ async #submit(root: string, input: Record<string, unknown>, flagSession: string | undefined): Promise<void> {
503
+ const sessionId = requireSessionId(input, flagSession);
504
+ if (await this.#tryOwnerRoute(root, sessionId, "submit", { ...input, sessionId })) return;
505
+ const state = await loadState(root, sessionId);
506
+ // No live owner: submission is blocked (never echoed-as-accepted).
507
+ writeJson(buildResponse(state, false, { accepted: false, submitted: false, reason: "owner-not-live" }, false));
508
+ process.exitCode = 1;
509
+ }
510
+
511
+ async #events(
512
+ root: string,
513
+ input: Record<string, unknown>,
514
+ flagSession: string | undefined,
515
+ cursor: number,
516
+ ): Promise<void> {
517
+ const sessionId = requireSessionId(input, flagSession);
518
+ const state = await loadState(root, sessionId);
519
+ const events = await readEvents(root, sessionId, cursor);
520
+ const nextCursor = events.length > 0 ? events[events.length - 1].cursor : cursor;
521
+ writeJson(
522
+ buildResponse(state, ownerLiveFor(state), {
523
+ events,
524
+ cursor: nextCursor,
525
+ note: "tail-only; live producer (owner) lands in M3/M5",
526
+ }),
527
+ );
528
+ }
529
+
530
+ async #retire(root: string, input: Record<string, unknown>, flagSession: string | undefined): Promise<void> {
531
+ const sessionId = requireSessionId(input, flagSession);
532
+ if (await this.#tryOwnerRoute(root, sessionId, "retire", { ...input, sessionId })) return;
533
+ const state = await loadState(root, sessionId);
534
+ const observation = buildObservation(state, ownerLiveFor(state));
535
+ if (observation.gitDelta === "dirty" || observation.gitDelta === "unknown") {
536
+ writeJson(
537
+ buildResponse(
538
+ state,
539
+ false,
540
+ {
541
+ retired: false,
542
+ reason: `retire-blocked:${observation.gitDelta}-delta`,
543
+ gitDelta: observation.gitDelta,
544
+ },
545
+ false,
546
+ ),
547
+ );
548
+ process.exitCode = 1;
549
+ return;
550
+ }
551
+ state.lifecycle = "retired";
552
+ state.updatedAt = nowIso();
553
+ await writeSessionState(root, state);
554
+ writeJson(buildResponse(state, false, { retired: true }));
555
+ }
556
+
557
+ async #pending(
558
+ root: string,
559
+ verb: string,
560
+ input: Record<string, unknown>,
561
+ flagSession: string | undefined,
562
+ ): Promise<void> {
563
+ const sessionId = flagSession ?? (typeof input.sessionId === "string" ? input.sessionId : undefined);
564
+ const milestone = verb === "recover" ? "M7" : verb === "validate" || verb === "finalize" ? "M8" : "M9";
565
+ if (sessionId) {
566
+ const state = await loadState(root, sessionId);
567
+ writeJson(buildResponse(state, ownerLiveFor(state), { pending: true, milestone, verb }, false));
568
+ process.exitCode = 1;
569
+ return;
570
+ }
571
+ writeJson({
572
+ ok: false,
573
+ state: buildStateView(
574
+ {
575
+ schemaVersion: SESSION_SCHEMA_VERSION,
576
+ sessionId: "(none)",
577
+ lifecycle: "new",
578
+ harness: "gajae-code",
579
+ handle: {} as SessionHandle,
580
+ retries: {},
581
+ blockers: [],
582
+ createdAt: nowIso(),
583
+ updatedAt: nowIso(),
584
+ },
585
+ false,
586
+ ),
587
+ evidence: { pending: true, milestone, verb },
588
+ nextAllowedActions: [],
589
+ });
590
+ process.exitCode = 1;
591
+ }
592
+ }