@gajae-code/coding-agent 0.6.3 → 0.6.5

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 (140) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +73 -1
  3. package/dist/types/cli/migrate-cli.d.ts +20 -0
  4. package/dist/types/commands/migrate.d.ts +33 -0
  5. package/dist/types/config/keybindings.d.ts +4 -0
  6. package/dist/types/config/settings-schema.d.ts +27 -0
  7. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
  8. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
  9. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  10. package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
  11. package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
  12. package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
  14. package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +36 -7
  16. package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
  17. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +7 -4
  18. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
  19. package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
  20. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  21. package/dist/types/hooks/skill-state.d.ts +12 -4
  22. package/dist/types/migrate/action-planner.d.ts +11 -0
  23. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  24. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  25. package/dist/types/migrate/adapters/index.d.ts +45 -0
  26. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  27. package/dist/types/migrate/executor.d.ts +2 -0
  28. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  29. package/dist/types/migrate/report.d.ts +18 -0
  30. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  31. package/dist/types/migrate/types.d.ts +126 -0
  32. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  33. package/dist/types/modes/components/welcome.d.ts +3 -1
  34. package/dist/types/modes/interactive-mode.d.ts +3 -0
  35. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  36. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  37. package/dist/types/research-plan/index.d.ts +1 -0
  38. package/dist/types/research-plan/ledger.d.ts +33 -0
  39. package/dist/types/rlm/artifacts.d.ts +1 -1
  40. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  41. package/dist/types/skill-state/active-state.d.ts +6 -11
  42. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  43. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  44. package/dist/types/task/spawn-gate.d.ts +1 -10
  45. package/package.json +7 -7
  46. package/src/cli/migrate-cli.ts +106 -0
  47. package/src/cli/setup-cli.ts +14 -1
  48. package/src/cli.ts +1 -0
  49. package/src/commands/deep-interview.ts +2 -2
  50. package/src/commands/launch.ts +1 -1
  51. package/src/commands/migrate.ts +46 -0
  52. package/src/commands/state.ts +2 -1
  53. package/src/commands/team.ts +7 -3
  54. package/src/config/model-registry.ts +9 -2
  55. package/src/config/model-resolver.ts +13 -2
  56. package/src/config/settings-schema.ts +17 -0
  57. package/src/coordinator-mcp/policy.ts +10 -2
  58. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
  59. package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
  60. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  61. package/src/defaults/gjc/skills/team/SKILL.md +51 -47
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +17 -13
  63. package/src/exec/bash-executor.ts +3 -1
  64. package/src/extensibility/custom-commands/loader.ts +0 -7
  65. package/src/extensibility/gjc-plugins/injection.ts +23 -4
  66. package/src/extensibility/gjc-plugins/state.ts +16 -1
  67. package/src/gjc-runtime/deep-interview-recorder.ts +43 -18
  68. package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
  69. package/src/gjc-runtime/goal-mode-request.ts +26 -11
  70. package/src/gjc-runtime/launch-tmux.ts +68 -15
  71. package/src/gjc-runtime/ralplan-runtime.ts +79 -50
  72. package/src/gjc-runtime/session-layout.ts +180 -0
  73. package/src/gjc-runtime/session-resolution.ts +217 -0
  74. package/src/gjc-runtime/state-graph.ts +1 -2
  75. package/src/gjc-runtime/state-migrations.ts +1 -0
  76. package/src/gjc-runtime/state-runtime.ts +230 -121
  77. package/src/gjc-runtime/state-schema.ts +2 -0
  78. package/src/gjc-runtime/state-writer.ts +289 -41
  79. package/src/gjc-runtime/team-runtime.ts +43 -19
  80. package/src/gjc-runtime/tmux-sessions.ts +43 -2
  81. package/src/gjc-runtime/ultragoal-guard.ts +45 -2
  82. package/src/gjc-runtime/ultragoal-runtime.ts +121 -41
  83. package/src/gjc-runtime/workflow-command-ref.ts +1 -2
  84. package/src/gjc-runtime/workflow-manifest.ts +1 -2
  85. package/src/harness-control-plane/storage.ts +14 -4
  86. package/src/hooks/native-skill-hook.ts +38 -12
  87. package/src/hooks/skill-state.ts +178 -83
  88. package/src/internal-urls/docs-index.generated.ts +9 -6
  89. package/src/migrate/action-planner.ts +318 -0
  90. package/src/migrate/adapters/claude-code.ts +39 -0
  91. package/src/migrate/adapters/codex.ts +70 -0
  92. package/src/migrate/adapters/index.ts +277 -0
  93. package/src/migrate/adapters/opencode.ts +52 -0
  94. package/src/migrate/executor.ts +81 -0
  95. package/src/migrate/mcp-mapper.ts +152 -0
  96. package/src/migrate/report.ts +104 -0
  97. package/src/migrate/skill-normalizer.ts +80 -0
  98. package/src/migrate/types.ts +163 -0
  99. package/src/modes/bridge/bridge-mode.ts +2 -2
  100. package/src/modes/components/custom-editor.ts +30 -20
  101. package/src/modes/components/welcome.ts +42 -9
  102. package/src/modes/controllers/input-controller.ts +21 -3
  103. package/src/modes/interactive-mode.ts +22 -1
  104. package/src/modes/prompt-action-autocomplete.ts +11 -1
  105. package/src/modes/rpc/rpc-mode.ts +2 -2
  106. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  107. package/src/prompts/agents/init.md +1 -1
  108. package/src/prompts/system/plan-mode-active.md +1 -1
  109. package/src/prompts/tools/ast-grep.md +1 -1
  110. package/src/prompts/tools/search.md +1 -1
  111. package/src/prompts/tools/task.md +1 -2
  112. package/src/research-plan/index.ts +1 -0
  113. package/src/research-plan/ledger.ts +177 -0
  114. package/src/rlm/artifacts.ts +12 -3
  115. package/src/rlm/index.ts +7 -0
  116. package/src/runtime-mcp/config-writer.ts +46 -0
  117. package/src/session/agent-session.ts +15 -21
  118. package/src/session/session-manager.ts +19 -2
  119. package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
  120. package/src/setup/hermes-setup.ts +1 -1
  121. package/src/skill-state/active-state.ts +72 -108
  122. package/src/skill-state/canonical-skills.ts +4 -0
  123. package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
  124. package/src/skill-state/workflow-hud.ts +4 -2
  125. package/src/skill-state/workflow-state-contract.ts +3 -3
  126. package/src/slash-commands/builtin-registry.ts +8 -4
  127. package/src/system-prompt.ts +11 -9
  128. package/src/task/agents.ts +1 -22
  129. package/src/task/index.ts +1 -41
  130. package/src/task/spawn-gate.ts +1 -38
  131. package/src/task/types.ts +1 -1
  132. package/src/tools/ask.ts +34 -12
  133. package/src/tools/computer.ts +58 -4
  134. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  135. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  136. package/src/prompts/agents/explore.md +0 -58
  137. package/src/prompts/agents/plan.md +0 -49
  138. package/src/prompts/agents/reviewer.md +0 -141
  139. package/src/prompts/agents/task.md +0 -16
  140. package/src/prompts/review-request.md +0 -70
@@ -114,7 +114,13 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
114
114
  output = runTmux(["list-sessions", "-F", format], env);
115
115
  } catch (error) {
116
116
  const message = error instanceof Error ? error.message : String(error);
117
- if (message.includes("no server running") || message.includes("failed to connect to server")) return [];
117
+ if (
118
+ message.includes("no server running") ||
119
+ message.includes("failed to connect to server") ||
120
+ message.includes("error connecting to")
121
+ ) {
122
+ return [];
123
+ }
118
124
  throw error;
119
125
  }
120
126
  return output
@@ -137,6 +143,8 @@ function listRawTmuxSessionNames(env: NodeJS.ProcessEnv = process.env): string[]
137
143
  export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus[] {
138
144
  return listSessionLines(env)
139
145
  .map(parseSessionLine)
146
+ .filter((session): session is GjcTmuxSessionStatus => session != null)
147
+ .map(session => hydrateSessionFromExactOptions(session, env))
140
148
  .filter((session): session is GjcTmuxSessionStatus => session?.profile === GJC_TMUX_PROFILE_VALUE)
141
149
  .sort((a, b) => a.name.localeCompare(b.name));
142
150
  }
@@ -145,7 +153,8 @@ export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTm
145
153
  export function listTmuxSessionsForGc(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionsForGc {
146
154
  const sessions = listSessionLines(env)
147
155
  .map(parseSessionLine)
148
- .filter((session): session is GjcTmuxSessionStatus => session != null);
156
+ .filter((session): session is GjcTmuxSessionStatus => session != null)
157
+ .map(session => hydrateSessionFromExactOptions(session, env));
149
158
  const tagged = sessions
150
159
  .filter(session => session.profile === GJC_TMUX_PROFILE_VALUE)
151
160
  .sort((a, b) => a.name.localeCompare(b.name));
@@ -179,6 +188,22 @@ export function findGjcTmuxSessionByBranch(
179
188
  );
180
189
  }
181
190
 
191
+ export function findGjcTmuxSessionByName(
192
+ sessionName: string,
193
+ env: NodeJS.ProcessEnv = process.env,
194
+ ): GjcTmuxSessionStatus | undefined {
195
+ return listGjcTmuxSessions(env).find(session => session.name === sessionName);
196
+ }
197
+
198
+ export function findGjcTmuxSessionByScope(
199
+ project: string,
200
+ branch: string | null | undefined,
201
+ env: NodeJS.ProcessEnv = process.env,
202
+ ): GjcTmuxSessionStatus | undefined {
203
+ return listGjcTmuxSessions(env).find(
204
+ session => session.project === project && (branch ? session.branch === branch : session.branch === undefined),
205
+ );
206
+ }
182
207
  export function statusGjcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus {
183
208
  const session = listGjcTmuxSessions(env).find(candidate => candidate.name === sessionName);
184
209
  if (session) return session;
@@ -227,6 +252,22 @@ function readExactOptionForGc(sessionName: string, option: string, env: NodeJS.P
227
252
  }
228
253
  }
229
254
 
255
+ function hydrateSessionFromExactOptions(session: GjcTmuxSessionStatus, env: NodeJS.ProcessEnv): GjcTmuxSessionStatus {
256
+ if (session.profile === GJC_TMUX_PROFILE_VALUE) return session;
257
+ const profile = readExactOptionForGc(session.name, GJC_TMUX_PROFILE_OPTION, env);
258
+ if (profile !== GJC_TMUX_PROFILE_VALUE) return session;
259
+ return {
260
+ ...session,
261
+ profile,
262
+ branch: session.branch ?? readExactOptionForGc(session.name, GJC_TMUX_BRANCH_OPTION, env),
263
+ branchSlug: session.branchSlug ?? readExactOptionForGc(session.name, GJC_TMUX_BRANCH_SLUG_OPTION, env),
264
+ project: session.project ?? readExactOptionForGc(session.name, GJC_TMUX_PROJECT_OPTION, env),
265
+ sessionId: session.sessionId ?? readExactOptionForGc(session.name, GJC_TMUX_SESSION_ID_OPTION, env),
266
+ sessionStateFile:
267
+ session.sessionStateFile ?? readExactOptionForGc(session.name, GJC_TMUX_SESSION_STATE_FILE_OPTION, env),
268
+ };
269
+ }
270
+
230
271
  /** @internal */
231
272
  export function readTmuxSessionTagsForGc(
232
273
  sessionName: string,
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
3
+ import { resolveGjcSessionForRead, SessionResolutionError } from "./session-resolution";
3
4
  import {
4
5
  computeUltragoalPlanGeneration,
5
6
  getUltragoalPaths,
@@ -10,6 +11,7 @@ import {
10
11
  type UltragoalCompletionVerification,
11
12
  type UltragoalGoal,
12
13
  type UltragoalLedgerEvent,
14
+ type UltragoalPaths,
13
15
  type UltragoalPlan,
14
16
  type UltragoalReceiptKind,
15
17
  } from "./ultragoal-runtime";
@@ -63,9 +65,30 @@ function isKnownUltragoalObjective(currentObjective: string): boolean {
63
65
  );
64
66
  }
65
67
 
68
+ async function ultragoalReadPaths(cwd: string): Promise<UltragoalPaths> {
69
+ const envSessionId = process.env.GJC_SESSION_ID?.trim();
70
+ if (envSessionId) return getUltragoalPaths(cwd, envSessionId);
71
+ try {
72
+ const session = await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID });
73
+ return getUltragoalPaths(cwd, session.gjcSessionId);
74
+ } catch (error) {
75
+ if (error instanceof SessionResolutionError && error.code === "no_session") {
76
+ return getUltragoalPaths(cwd, null);
77
+ }
78
+ throw error;
79
+ }
80
+ }
81
+
66
82
  async function hasDurableUltragoalState(cwd: string): Promise<boolean> {
83
+ let paths: UltragoalPaths;
84
+ try {
85
+ paths = await ultragoalReadPaths(cwd);
86
+ } catch (error) {
87
+ if (error instanceof SessionResolutionError) return true;
88
+ throw error;
89
+ }
67
90
  try {
68
- await fs.stat(getUltragoalPaths(cwd).dir);
91
+ await fs.stat(paths.dir);
69
92
  return true;
70
93
  } catch (error) {
71
94
  if (
@@ -308,6 +331,18 @@ export async function readUltragoalVerificationState(input: {
308
331
  }
309
332
  const receiptTarget = findReceiptGoal(plan, currentObjective);
310
333
  if (!receiptTarget) {
334
+ // When earlier required goals are already complete but later ones remain, name the
335
+ // specific blocking goals (a final-aggregate receipt cannot exist yet anyway). Only
336
+ // fall back to the generic missing-receipt message when no progress has been verified.
337
+ const completedRequired = requiredGoals(plan).filter(goal => goal.status === "complete");
338
+ if (completedRequired.length > 0 && runState.incompleteGoals.length > 0) {
339
+ return {
340
+ state: "active_missing_final_receipt",
341
+ message: `Ultragoal still has incomplete required goals: ${runState.incompleteGoals
342
+ .map(goal => goal.id)
343
+ .join(", ")}. Run \`gjc ultragoal complete-goals\` to continue.`,
344
+ };
345
+ }
311
346
  return {
312
347
  state: "active_missing_final_receipt",
313
348
  message: "Ultragoal aggregate completion requires a fresh final aggregate receipt.",
@@ -331,7 +366,15 @@ export async function readUltragoalVerificationState(input: {
331
366
  }
332
367
 
333
368
  export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBlockDiagnostic> {
334
- const paths = getUltragoalPaths(cwd);
369
+ let paths: UltragoalPaths;
370
+ try {
371
+ paths = await ultragoalReadPaths(cwd);
372
+ } catch (error) {
373
+ return activeAskDiagnostic({
374
+ reason: `Unable to resolve durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
375
+ source: "durable_state_unreadable",
376
+ });
377
+ }
335
378
  try {
336
379
  await fs.stat(paths.dir);
337
380
  } catch (error) {
@@ -6,10 +6,11 @@ import type { WorkflowHudSummary } from "../skill-state/active-state";
6
6
  import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
7
7
  import { renderCliWriteReceipt } from "./cli-write-receipt";
8
8
  import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
9
- import { latestUltragoalLedgerEventFromText } from "./ledger-event-renderer";
9
+ import { gjcRoot, sessionUltragoalDir } from "./session-layout";
10
+ import { resolveGjcSessionForRead, resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
10
11
  import { renderUltragoalStatusMarkdown } from "./state-renderer";
11
12
  import { reconcileWorkflowSkillState } from "./state-runtime";
12
- import { appendJsonl, writeArtifact, writeJsonAtomic } from "./state-writer";
13
+ import { appendJsonl, persistedStateRevision, writeArtifact, writeGuardedJsonAtomic } from "./state-writer";
13
14
 
14
15
  export type UltragoalGjcGoalMode = "aggregate" | "per-story";
15
16
  export type UltragoalGoalStatus =
@@ -44,6 +45,7 @@ export interface UltragoalPlan {
44
45
  goals: UltragoalGoal[];
45
46
  createdAt: string;
46
47
  updatedAt: string;
48
+ [key: string]: unknown;
47
49
  }
48
50
 
49
51
  export type UltragoalReceiptKind = "per-goal" | "final-aggregate";
@@ -107,6 +109,10 @@ interface JsonObject {
107
109
  [key: string]: unknown;
108
110
  }
109
111
 
112
+ function currentUltragoalSessionId(cwd: string): string {
113
+ return resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
114
+ }
115
+
110
116
  const TERMINAL_OR_SKIPPED_STATUSES = new Set<UltragoalGoalStatus>(["complete", "superseded"]);
111
117
  const CLEAN_ARCHITECT_STATUS = "CLEAR";
112
118
  const APPROVE_RECOMMENDATION = "APPROVE";
@@ -162,8 +168,9 @@ export function hashStructuredValue(value: unknown): string {
162
168
  .digest("hex");
163
169
  }
164
170
 
165
- export function getUltragoalPaths(cwd: string): UltragoalPaths {
166
- const dir = path.join(cwd, ".gjc", "ultragoal");
171
+ export function getUltragoalPaths(cwd: string, sessionId?: string | null): UltragoalPaths {
172
+ const explicitSessionId = sessionId?.trim() || process.env.GJC_SESSION_ID?.trim();
173
+ const dir = explicitSessionId ? sessionUltragoalDir(cwd, explicitSessionId) : path.join(gjcRoot(cwd), "ultragoal");
167
174
  return {
168
175
  dir,
169
176
  briefPath: path.join(dir, "brief.md"),
@@ -178,8 +185,10 @@ function isEnoent(error: unknown): boolean {
178
185
  );
179
186
  }
180
187
 
181
- async function appendLedger(cwd: string, event: JsonObject): Promise<UltragoalLedgerEvent> {
182
- const paths = getUltragoalPaths(cwd);
188
+ async function appendLedger(cwd: string, event: JsonObject, sessionId?: string | null): Promise<UltragoalLedgerEvent> {
189
+ const resolvedSessionId =
190
+ sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
191
+ const paths = getUltragoalPaths(cwd, resolvedSessionId);
183
192
  const entry: UltragoalLedgerEvent = {
184
193
  eventId: typeof event.eventId === "string" ? event.eventId : crypto.randomUUID(),
185
194
  ...event,
@@ -187,14 +196,18 @@ async function appendLedger(cwd: string, event: JsonObject): Promise<UltragoalLe
187
196
  };
188
197
  await appendJsonl(paths.ledgerPath, entry, {
189
198
  cwd,
190
- audit: { category: "ledger", verb: "append", owner: "gjc-runtime" },
199
+ audit: { category: "ledger", verb: "append", owner: "gjc-runtime", sessionId: resolvedSessionId },
191
200
  });
201
+ await writeSessionActivityMarker(cwd, resolvedSessionId, { writer: "ultragoal-runtime", path: paths.ledgerPath });
192
202
  return entry;
193
203
  }
194
204
 
195
- export async function readUltragoalLedger(cwd: string): Promise<UltragoalLedgerEvent[]> {
205
+ export async function readUltragoalLedger(cwd: string, sessionId?: string | null): Promise<UltragoalLedgerEvent[]> {
206
+ const resolvedSessionId =
207
+ sessionId?.trim() ||
208
+ (await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID })).gjcSessionId;
196
209
  try {
197
- const raw = await Bun.file(getUltragoalPaths(cwd).ledgerPath).text();
210
+ const raw = await Bun.file(getUltragoalPaths(cwd, resolvedSessionId).ledgerPath).text();
198
211
  return raw
199
212
  .split(/\r?\n/)
200
213
  .map(line => line.trim())
@@ -206,16 +219,21 @@ export async function readUltragoalLedger(cwd: string): Promise<UltragoalLedgerE
206
219
  }
207
220
  }
208
221
 
209
- async function writePlan(cwd: string, plan: UltragoalPlan): Promise<void> {
210
- const paths = getUltragoalPaths(cwd);
222
+ async function writePlan(cwd: string, plan: UltragoalPlan, sessionId?: string | null): Promise<void> {
223
+ const resolvedSessionId =
224
+ sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
225
+ const paths = getUltragoalPaths(cwd, resolvedSessionId);
211
226
  await writeArtifact(paths.briefPath, `${plan.brief.trim()}\n`, {
212
227
  cwd,
213
- audit: { category: "artifact", verb: "write", owner: "gjc-runtime" },
228
+ audit: { category: "artifact", verb: "write", owner: "gjc-runtime", sessionId: resolvedSessionId },
214
229
  });
215
- await writeJsonAtomic(paths.goalsPath, plan, {
230
+ await writeGuardedJsonAtomic(paths.goalsPath, plan, {
216
231
  cwd,
217
- audit: { category: "state", verb: "write", owner: "gjc-runtime" },
232
+ policy: "source",
233
+ expectedRevision: typeof plan.state_revision === "number" ? persistedStateRevision(plan) : undefined,
234
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", sessionId: resolvedSessionId },
218
235
  });
236
+ await writeSessionActivityMarker(cwd, resolvedSessionId, { writer: "ultragoal-runtime", path: paths.goalsPath });
219
237
  }
220
238
 
221
239
  function requiredUltragoalGoals(plan: UltragoalPlan): UltragoalGoal[] {
@@ -426,6 +444,7 @@ function normalizePlan(raw: unknown): UltragoalPlan {
426
444
  const objective = nonEmptyString(goalRecord.objective) ?? title;
427
445
  const goalCreatedAt = nonEmptyString(goalRecord.createdAt) ?? createdAt;
428
446
  return {
447
+ ...goalRecord,
429
448
  id,
430
449
  title,
431
450
  objective,
@@ -459,12 +478,18 @@ function normalizePlan(raw: unknown): UltragoalPlan {
459
478
  goals,
460
479
  createdAt,
461
480
  updatedAt,
481
+ ...(typeof record.state_revision === "number" && Number.isFinite(record.state_revision)
482
+ ? { state_revision: record.state_revision }
483
+ : {}),
462
484
  };
463
485
  }
464
486
 
465
- export async function readUltragoalPlan(cwd: string): Promise<UltragoalPlan | null> {
487
+ export async function readUltragoalPlan(cwd: string, sessionId?: string | null): Promise<UltragoalPlan | null> {
488
+ const resolvedSessionId =
489
+ sessionId?.trim() ||
490
+ (await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID })).gjcSessionId;
466
491
  try {
467
- return normalizePlan(await Bun.file(getUltragoalPaths(cwd).goalsPath).json());
492
+ return normalizePlan(await Bun.file(getUltragoalPaths(cwd, resolvedSessionId).goalsPath).json());
468
493
  } catch (error) {
469
494
  if (isEnoent(error)) return null;
470
495
  throw error;
@@ -483,9 +508,12 @@ function emptyCounts(): Record<UltragoalGoalStatus, number> {
483
508
  };
484
509
  }
485
510
 
486
- export async function getUltragoalStatus(cwd: string): Promise<UltragoalStatusSummary> {
487
- const paths = getUltragoalPaths(cwd);
488
- const plan = await readUltragoalPlan(cwd);
511
+ export async function getUltragoalStatus(cwd: string, sessionId?: string | null): Promise<UltragoalStatusSummary> {
512
+ const resolvedSessionId =
513
+ sessionId?.trim() ||
514
+ (await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID })).gjcSessionId;
515
+ const paths = getUltragoalPaths(cwd, resolvedSessionId);
516
+ const plan = await readUltragoalPlan(cwd, resolvedSessionId);
489
517
  const counts = emptyCounts();
490
518
  if (!plan) return { exists: false, status: "missing", paths, counts, goals: [] };
491
519
  for (const goal of plan.goals) counts[goal.status] += 1;
@@ -577,6 +605,7 @@ export async function createUltragoalPlan(input: {
577
605
  cwd: string;
578
606
  brief: string;
579
607
  gjcGoalMode?: UltragoalGjcGoalMode;
608
+ sessionId?: string | null;
580
609
  }): Promise<UltragoalPlan> {
581
610
  const brief = input.brief.trim();
582
611
  if (!brief) throw new Error("ultragoal brief is required");
@@ -601,8 +630,8 @@ export async function createUltragoalPlan(input: {
601
630
  createdAt: now,
602
631
  updatedAt: now,
603
632
  };
604
- await writePlan(input.cwd, plan);
605
- await appendLedger(input.cwd, { event: "plan_created", goalIds: plan.goals.map(goal => goal.id) });
633
+ await writePlan(input.cwd, plan, input.sessionId);
634
+ await appendLedger(input.cwd, { event: "plan_created", goalIds: plan.goals.map(goal => goal.id) }, input.sessionId);
606
635
  return plan;
607
636
  }
608
637
 
@@ -639,12 +668,16 @@ export function getUltragoalRunCompletionState(
639
668
  };
640
669
  }
641
670
 
642
- export async function startNextUltragoalGoal(input: { cwd: string; retryFailed?: boolean }): Promise<{
671
+ export async function startNextUltragoalGoal(input: {
672
+ cwd: string;
673
+ retryFailed?: boolean;
674
+ sessionId?: string | null;
675
+ }): Promise<{
643
676
  plan: UltragoalPlan;
644
677
  goal?: UltragoalGoal;
645
678
  allComplete: boolean;
646
679
  }> {
647
- const plan = await readUltragoalPlan(input.cwd);
680
+ const plan = await readUltragoalPlan(input.cwd, input.sessionId);
648
681
  if (!plan) throw new Error("No ultragoal plan found. Run `gjc ultragoal create-goals --brief ...` first.");
649
682
  const goal = chooseNextGoal(plan, input.retryFailed === true);
650
683
  if (!goal) return { plan, allComplete: getUltragoalRunCompletionState(plan).allComplete };
@@ -654,8 +687,8 @@ export async function startNextUltragoalGoal(input: { cwd: string; retryFailed?:
654
687
  goal.startedAt = goal.startedAt ?? now;
655
688
  goal.updatedAt = now;
656
689
  plan.updatedAt = now;
657
- await writePlan(input.cwd, plan);
658
- await appendLedger(input.cwd, { event: "goal_started", goalId: goal.id });
690
+ await writePlan(input.cwd, plan, input.sessionId);
691
+ await appendLedger(input.cwd, { event: "goal_started", goalId: goal.id }, input.sessionId);
659
692
  }
660
693
  return { plan, goal, allComplete: false };
661
694
  }
@@ -2350,6 +2383,8 @@ export async function checkpointUltragoalGoal(input: {
2350
2383
  if (input.status === "complete") goal.completedAt = now;
2351
2384
  plan.updatedAt = now;
2352
2385
  await writePlan(input.cwd, plan);
2386
+ const persistedPlan = await readUltragoalPlan(input.cwd);
2387
+ if (persistedPlan?.state_revision !== undefined) plan.state_revision = persistedPlan.state_revision;
2353
2388
  await appendLedger(input.cwd, {
2354
2389
  eventId: pendingCheckpointEventId,
2355
2390
  event: "goal_checkpointed",
@@ -2799,6 +2834,8 @@ export async function recordUltragoalReviewBlockers(input: {
2799
2834
  evidence: input.evidence,
2800
2835
  gjcGoalJson: input.gjcGoalJson,
2801
2836
  });
2837
+ const persistedPlan = await readUltragoalPlan(input.cwd);
2838
+ if (persistedPlan?.state_revision !== undefined) plan.state_revision = persistedPlan.state_revision;
2802
2839
  const now = new Date().toISOString();
2803
2840
  const nextId = `G${String(plan.goals.length + 1).padStart(3, "0")}`;
2804
2841
  plan.goals.push({
@@ -3329,7 +3366,7 @@ function renderCompleteHandoff(
3329
3366
  goal_id: result.goal?.id,
3330
3367
  goal_status: result.goal?.status,
3331
3368
  gjc_objective: result.plan.gjcObjective,
3332
- goals_path: getUltragoalPaths(cwd).goalsPath,
3369
+ goals_path: getUltragoalPaths(cwd, currentUltragoalSessionId(cwd)).goalsPath,
3333
3370
  });
3334
3371
  }
3335
3372
  if (result.allComplete) return "ultragoal complete all=true\n";
@@ -3353,7 +3390,7 @@ function renderCheckpointContinuation(
3353
3390
  ok: true,
3354
3391
  goal_id: result.checkpointedGoal.id,
3355
3392
  status,
3356
- goals_path: getUltragoalPaths(cwd).goalsPath,
3393
+ goals_path: getUltragoalPaths(cwd, currentUltragoalSessionId(cwd)).goalsPath,
3357
3394
  completion_receipt_kind: result.checkpointedGoal.completionVerification?.receiptKind,
3358
3395
  quality_gate_hash: result.checkpointedGoal.completionVerification?.qualityGateHash,
3359
3396
  all_complete: result.allComplete,
@@ -3407,7 +3444,12 @@ async function executeUltragoalSteeringCommand(args: readonly string[], cwd: str
3407
3444
  return {
3408
3445
  kind,
3409
3446
  message: "Accepted add_subgoal steering.\n",
3410
- receipt: { ok: true, kind, goal_id: result.goalId, goals_path: getUltragoalPaths(cwd).goalsPath },
3447
+ receipt: {
3448
+ ok: true,
3449
+ kind,
3450
+ goal_id: result.goalId,
3451
+ goals_path: getUltragoalPaths(cwd, currentUltragoalSessionId(cwd)).goalsPath,
3452
+ },
3411
3453
  };
3412
3454
  }
3413
3455
  case "split_subgoal": {
@@ -3427,7 +3469,7 @@ async function executeUltragoalSteeringCommand(args: readonly string[], cwd: str
3427
3469
  kind,
3428
3470
  goal_id: result.goalId,
3429
3471
  replacement_goal_ids: result.replacementGoalIds,
3430
- goals_path: getUltragoalPaths(cwd).goalsPath,
3472
+ goals_path: getUltragoalPaths(cwd, currentUltragoalSessionId(cwd)).goalsPath,
3431
3473
  },
3432
3474
  };
3433
3475
  }
@@ -3446,7 +3488,7 @@ async function executeUltragoalSteeringCommand(args: readonly string[], cwd: str
3446
3488
  ok: true,
3447
3489
  kind,
3448
3490
  pending_goal_ids: result.pendingGoalIds,
3449
- goals_path: getUltragoalPaths(cwd).goalsPath,
3491
+ goals_path: getUltragoalPaths(cwd, currentUltragoalSessionId(cwd)).goalsPath,
3450
3492
  },
3451
3493
  };
3452
3494
  }
@@ -3468,7 +3510,7 @@ async function executeUltragoalSteeringCommand(args: readonly string[], cwd: str
3468
3510
  kind,
3469
3511
  goal_id: result.goalId,
3470
3512
  changed_fields: result.changedFields,
3471
- goals_path: getUltragoalPaths(cwd).goalsPath,
3513
+ goals_path: getUltragoalPaths(cwd, currentUltragoalSessionId(cwd)).goalsPath,
3472
3514
  },
3473
3515
  };
3474
3516
  }
@@ -3477,7 +3519,11 @@ async function executeUltragoalSteeringCommand(args: readonly string[], cwd: str
3477
3519
  return {
3478
3520
  kind,
3479
3521
  message: "Accepted annotate_ledger steering.\n",
3480
- receipt: { ok: true, kind, ledger_path: getUltragoalPaths(cwd).ledgerPath },
3522
+ receipt: {
3523
+ ok: true,
3524
+ kind,
3525
+ ledger_path: getUltragoalPaths(cwd, currentUltragoalSessionId(cwd)).ledgerPath,
3526
+ },
3481
3527
  };
3482
3528
  }
3483
3529
  case "mark_blocked_superseded": {
@@ -3496,7 +3542,7 @@ async function executeUltragoalSteeringCommand(args: readonly string[], cwd: str
3496
3542
  kind,
3497
3543
  goal_id: result.goalId,
3498
3544
  no_replacement_required: true,
3499
- goals_path: getUltragoalPaths(cwd).goalsPath,
3545
+ goals_path: getUltragoalPaths(cwd, currentUltragoalSessionId(cwd)).goalsPath,
3500
3546
  },
3501
3547
  };
3502
3548
  }
@@ -3517,6 +3563,7 @@ async function executeUltragoalSteeringCommand(args: readonly string[], cwd: str
3517
3563
  }
3518
3564
 
3519
3565
  async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<UltragoalCommandResult> {
3566
+ const sessionId = currentUltragoalSessionId(cwd);
3520
3567
  const help = renderUltragoalHelp(args);
3521
3568
  if (help) return { status: 0, stdout: help };
3522
3569
  try {
@@ -3524,7 +3571,7 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
3524
3571
  const json = hasFlag(args, "--json");
3525
3572
  switch (command) {
3526
3573
  case "status":
3527
- return { status: 0, stdout: renderStatus(await getUltragoalStatus(cwd), json) };
3574
+ return { status: 0, stdout: renderStatus(await getUltragoalStatus(cwd, sessionId), json) };
3528
3575
  case "create":
3529
3576
  case "create-goals": {
3530
3577
  const mode = flagValue(args, "--gjc-goal-mode") === "per-story" ? "per-story" : "aggregate";
@@ -3537,9 +3584,9 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
3537
3584
  ok: true,
3538
3585
  goals_count: plan.goals.length,
3539
3586
  goal_ids: plan.goals.map(goal => goal.id),
3540
- goals_path: getUltragoalPaths(cwd).goalsPath,
3587
+ goals_path: getUltragoalPaths(cwd, currentUltragoalSessionId(cwd)).goalsPath,
3541
3588
  })
3542
- : `Created ultragoal plan with ${plan.goals.length} goal${plan.goals.length === 1 ? "" : "s"} at ${getUltragoalPaths(cwd).goalsPath}.\n`,
3589
+ : `Created ultragoal plan with ${plan.goals.length} goal${plan.goals.length === 1 ? "" : "s"} at ${getUltragoalPaths(cwd, currentUltragoalSessionId(cwd)).goalsPath}.\n`,
3543
3590
  };
3544
3591
  }
3545
3592
  case "complete-goals":
@@ -3598,7 +3645,11 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
3598
3645
  return {
3599
3646
  status: 0,
3600
3647
  stdout: json
3601
- ? renderCliWriteReceipt({ ok: true, goal_id: goal?.id, goals_path: getUltragoalPaths(cwd).goalsPath })
3648
+ ? renderCliWriteReceipt({
3649
+ ok: true,
3650
+ goal_id: goal?.id,
3651
+ goals_path: getUltragoalPaths(cwd, currentUltragoalSessionId(cwd)).goalsPath,
3652
+ })
3602
3653
  : "Recorded review blockers.\n",
3603
3654
  };
3604
3655
  }
@@ -3632,9 +3683,9 @@ const RECONCILE_COMMANDS = new Set([
3632
3683
  * beyond that reconcile-failure audit event.
3633
3684
  */
3634
3685
  async function reconcileUltragoalState(cwd: string): Promise<void> {
3635
- const sessionId = process.env.GJC_SESSION_ID?.trim() || undefined;
3686
+ const sessionId = currentUltragoalSessionId(cwd);
3636
3687
  try {
3637
- const summary = await getUltragoalStatus(cwd);
3688
+ const summary = await getUltragoalStatus(cwd, sessionId);
3638
3689
  const status = summary.status;
3639
3690
  const active = summary.exists && status !== "complete";
3640
3691
  const payload: Record<string, unknown> = {
@@ -3653,15 +3704,44 @@ async function reconcileUltragoalState(cwd: string): Promise<void> {
3653
3704
  const ledgerText = await Bun.file(summary.paths.ledgerPath)
3654
3705
  .text()
3655
3706
  .catch(() => "");
3656
- const latestLedger = latestUltragoalLedgerEventFromText(ledgerText);
3707
+ const latestLedger = ledgerText
3708
+ .split(/\r?\n/)
3709
+ .map(line => line.trim())
3710
+ .filter(Boolean)
3711
+ .toReversed()
3712
+ .map(line => {
3713
+ try {
3714
+ const row = JSON.parse(line) as Record<string, unknown>;
3715
+ const event =
3716
+ typeof row.event === "string" ? row.event : typeof row.type === "string" ? row.type : undefined;
3717
+ return event ? { ...row, event } : undefined;
3718
+ } catch {
3719
+ return undefined;
3720
+ }
3721
+ })
3722
+ .find((row): row is Record<string, unknown> & { event: string } => Boolean(row));
3657
3723
  if (latestLedger) {
3658
3724
  payload.latestLedgerEvent = {
3659
3725
  event: latestLedger.event,
3660
3726
  ...(latestLedger.goalId ? { goalId: latestLedger.goalId } : {}),
3661
3727
  ...(latestLedger.timestamp ? { timestamp: latestLedger.timestamp } : {}),
3728
+ ...(typeof latestLedger.kind === "string" ? { kind: latestLedger.kind } : {}),
3729
+ ...(typeof latestLedger.evidence === "string" ? { evidence: latestLedger.evidence } : {}),
3662
3730
  };
3663
3731
  }
3664
- await reconcileWorkflowSkillState({ cwd, mode: "ultragoal", sessionId, active, phase: status, payload });
3732
+ const sourceRevision = Math.max(
3733
+ persistedStateRevision(await readUltragoalPlan(cwd, sessionId)),
3734
+ ledgerText.split(/\r?\n/).filter(line => line.trim().length > 0).length,
3735
+ );
3736
+ await reconcileWorkflowSkillState({
3737
+ cwd,
3738
+ mode: "ultragoal",
3739
+ sessionId,
3740
+ active,
3741
+ phase: status,
3742
+ payload,
3743
+ ...(sourceRevision > 0 ? { sourceRevision } : {}),
3744
+ });
3665
3745
  } catch (error) {
3666
3746
  const message = error instanceof Error ? error.message : String(error);
3667
3747
  process.stderr.write(`ultragoal state reconciliation failed: ${message}\n`);
@@ -1,5 +1,4 @@
1
- import type { CanonicalGjcWorkflowSkill } from "../skill-state/active-state";
2
- import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../skill-state/active-state";
1
+ import { CANONICAL_GJC_WORKFLOW_SKILLS, type CanonicalGjcWorkflowSkill } from "../skill-state/canonical-skills";
3
2
 
4
3
  export type CommandRefVisibility = "public" | "hidden" | "planned";
5
4
  export type CommandRefIncludeWhen = "implemented-only" | "planned";
@@ -4,8 +4,7 @@
4
4
  * hand-edited.
5
5
  */
6
6
 
7
- import type { CanonicalGjcWorkflowSkill } from "../skill-state/active-state";
8
- import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../skill-state/active-state";
7
+ import { CANONICAL_GJC_WORKFLOW_SKILLS, type CanonicalGjcWorkflowSkill } from "../skill-state/canonical-skills";
9
8
  import { initialPhaseForSkill } from "../skill-state/initial-phase";
10
9
 
11
10
  export interface WorkflowState {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Session-scoped storage for the harness control plane.
3
3
  *
4
- * Layout (under the harness state root, default `<cwd>/.gjc/state/harness`):
4
+ * Layout (under the harness state root, default `<cwd>/.gjc/_session-{sessionid}/state/harness`):
5
5
  * sessions/<encoded-id>/state.json lifecycle + handle (atomic)
6
6
  * sessions/<encoded-id>/lease.json owner lease (M3)
7
7
  * sessions/<encoded-id>/events.jsonl owner-only severity envelopes
@@ -18,6 +18,7 @@ import * as fsSync from "node:fs";
18
18
  import * as fs from "node:fs/promises";
19
19
  import * as os from "node:os";
20
20
  import * as path from "node:path";
21
+ import { harnessStateRoot } from "../gjc-runtime/session-layout";
21
22
  import { appendReceiptToConfiguredSpool } from "./receipt-spool";
22
23
  import type { ReceiptEnvelope } from "./receipts";
23
24
  import type { EventEnvelope, ReceiptFamily, SessionState } from "./types";
@@ -232,13 +233,22 @@ export class StorageError extends Error {
232
233
  }
233
234
  }
234
235
 
235
- /** Resolve the harness state root from explicit value, env, or cwd default. */
236
- export function resolveHarnessRoot(opts?: { root?: string; cwd?: string; env?: NodeJS.ProcessEnv }): string {
236
+ /** Resolve the harness state root from explicit value, env, or cwd/session default. */
237
+ export function resolveHarnessRoot(opts?: {
238
+ root?: string;
239
+ cwd?: string;
240
+ env?: NodeJS.ProcessEnv;
241
+ gjcSessionId?: string;
242
+ }): string {
237
243
  const env = opts?.env ?? process.env;
238
244
  if (opts?.root) return path.resolve(opts.root);
239
245
  const fromEnv = env.GJC_HARNESS_STATE_ROOT;
240
246
  if (fromEnv?.trim()) return path.resolve(fromEnv.trim());
241
- return path.join(opts?.cwd ?? process.cwd(), ".gjc", "state", "harness");
247
+ const gjcSessionId = opts?.gjcSessionId ?? env.GJC_SESSION_ID?.trim();
248
+ if (!gjcSessionId) {
249
+ throw new StorageError("GJC session id is required for default harness state root", "missing_gjc_session_id");
250
+ }
251
+ return harnessStateRoot(opts?.cwd ?? process.cwd(), gjcSessionId);
242
252
  }
243
253
 
244
254
  export function assertSafeSessionId(id: string): void {