@gajae-code/coding-agent 0.6.4 → 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 (120) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/types/cli/migrate-cli.d.ts +20 -0
  3. package/dist/types/commands/migrate.d.ts +33 -0
  4. package/dist/types/config/keybindings.d.ts +4 -0
  5. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
  6. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
  7. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  8. package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
  9. package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
  10. package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
  11. package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
  12. package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
  13. package/dist/types/gjc-runtime/state-writer.d.ts +36 -7
  14. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +7 -4
  15. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
  16. package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
  17. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  18. package/dist/types/hooks/skill-state.d.ts +12 -4
  19. package/dist/types/migrate/action-planner.d.ts +11 -0
  20. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  21. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  22. package/dist/types/migrate/adapters/index.d.ts +45 -0
  23. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  24. package/dist/types/migrate/executor.d.ts +2 -0
  25. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  26. package/dist/types/migrate/report.d.ts +18 -0
  27. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  28. package/dist/types/migrate/types.d.ts +126 -0
  29. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  30. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  31. package/dist/types/research-plan/index.d.ts +1 -0
  32. package/dist/types/research-plan/ledger.d.ts +33 -0
  33. package/dist/types/rlm/artifacts.d.ts +1 -1
  34. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  35. package/dist/types/skill-state/active-state.d.ts +6 -11
  36. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  37. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  38. package/dist/types/task/spawn-gate.d.ts +1 -10
  39. package/package.json +7 -7
  40. package/src/cli/migrate-cli.ts +106 -0
  41. package/src/cli.ts +1 -0
  42. package/src/commands/deep-interview.ts +2 -2
  43. package/src/commands/migrate.ts +46 -0
  44. package/src/commands/state.ts +2 -1
  45. package/src/commands/team.ts +7 -3
  46. package/src/coordinator-mcp/policy.ts +10 -2
  47. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
  48. package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
  49. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  50. package/src/defaults/gjc/skills/team/SKILL.md +51 -47
  51. package/src/defaults/gjc/skills/ultragoal/SKILL.md +17 -13
  52. package/src/extensibility/custom-commands/loader.ts +0 -7
  53. package/src/extensibility/gjc-plugins/injection.ts +23 -4
  54. package/src/extensibility/gjc-plugins/state.ts +16 -1
  55. package/src/gjc-runtime/deep-interview-recorder.ts +43 -18
  56. package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
  57. package/src/gjc-runtime/goal-mode-request.ts +26 -11
  58. package/src/gjc-runtime/launch-tmux.ts +6 -1
  59. package/src/gjc-runtime/ralplan-runtime.ts +79 -50
  60. package/src/gjc-runtime/session-layout.ts +180 -0
  61. package/src/gjc-runtime/session-resolution.ts +217 -0
  62. package/src/gjc-runtime/state-graph.ts +1 -2
  63. package/src/gjc-runtime/state-migrations.ts +1 -0
  64. package/src/gjc-runtime/state-runtime.ts +230 -121
  65. package/src/gjc-runtime/state-schema.ts +2 -0
  66. package/src/gjc-runtime/state-writer.ts +289 -41
  67. package/src/gjc-runtime/team-runtime.ts +43 -19
  68. package/src/gjc-runtime/tmux-sessions.ts +7 -1
  69. package/src/gjc-runtime/ultragoal-guard.ts +45 -2
  70. package/src/gjc-runtime/ultragoal-runtime.ts +121 -41
  71. package/src/gjc-runtime/workflow-command-ref.ts +1 -2
  72. package/src/gjc-runtime/workflow-manifest.ts +1 -2
  73. package/src/harness-control-plane/storage.ts +14 -4
  74. package/src/hooks/native-skill-hook.ts +38 -12
  75. package/src/hooks/skill-state.ts +178 -83
  76. package/src/internal-urls/docs-index.generated.ts +6 -4
  77. package/src/migrate/action-planner.ts +318 -0
  78. package/src/migrate/adapters/claude-code.ts +39 -0
  79. package/src/migrate/adapters/codex.ts +70 -0
  80. package/src/migrate/adapters/index.ts +277 -0
  81. package/src/migrate/adapters/opencode.ts +52 -0
  82. package/src/migrate/executor.ts +81 -0
  83. package/src/migrate/mcp-mapper.ts +152 -0
  84. package/src/migrate/report.ts +104 -0
  85. package/src/migrate/skill-normalizer.ts +80 -0
  86. package/src/migrate/types.ts +163 -0
  87. package/src/modes/bridge/bridge-mode.ts +2 -2
  88. package/src/modes/components/custom-editor.ts +30 -20
  89. package/src/modes/rpc/rpc-mode.ts +2 -2
  90. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  91. package/src/prompts/agents/init.md +1 -1
  92. package/src/prompts/system/plan-mode-active.md +1 -1
  93. package/src/prompts/tools/ast-grep.md +1 -1
  94. package/src/prompts/tools/search.md +1 -1
  95. package/src/prompts/tools/task.md +1 -2
  96. package/src/research-plan/index.ts +1 -0
  97. package/src/research-plan/ledger.ts +177 -0
  98. package/src/rlm/artifacts.ts +12 -3
  99. package/src/rlm/index.ts +7 -0
  100. package/src/runtime-mcp/config-writer.ts +46 -0
  101. package/src/session/agent-session.ts +15 -21
  102. package/src/setup/hermes-setup.ts +1 -1
  103. package/src/skill-state/active-state.ts +72 -108
  104. package/src/skill-state/canonical-skills.ts +4 -0
  105. package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
  106. package/src/skill-state/workflow-hud.ts +4 -2
  107. package/src/skill-state/workflow-state-contract.ts +3 -3
  108. package/src/task/agents.ts +1 -22
  109. package/src/task/index.ts +1 -41
  110. package/src/task/spawn-gate.ts +1 -38
  111. package/src/task/types.ts +1 -1
  112. package/src/tools/ask.ts +34 -12
  113. package/src/tools/computer.ts +58 -4
  114. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  115. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  116. package/src/prompts/agents/explore.md +0 -58
  117. package/src/prompts/agents/plan.md +0 -49
  118. package/src/prompts/agents/reviewer.md +0 -141
  119. package/src/prompts/agents/task.md +0 -16
  120. package/src/prompts/review-request.md +0 -70
@@ -1,8 +1,15 @@
1
+ import { existsSync } from "node:fs";
1
2
  import * as path from "node:path";
2
3
  import { logger } from "@gajae-code/utils";
3
4
  import type { SkillDiscoverySettings } from "../config/skill-settings-defaults";
5
+ import { activeSnapshotPath, modeStatePath as sessionModeStatePath } from "../gjc-runtime/session-layout";
6
+ import { resolveGjcSessionForRead } from "../gjc-runtime/session-resolution";
4
7
  import { ModeStateSchema, SkillActiveStateSchema } from "../gjc-runtime/state-schema";
5
- import { writeJsonAtomic, writeWorkflowEnvelopeAtomic } from "../gjc-runtime/state-writer";
8
+ import {
9
+ readExistingStateForMutation,
10
+ writeGuardedJsonAtomic,
11
+ writeGuardedWorkflowEnvelopeAtomic,
12
+ } from "../gjc-runtime/state-writer";
6
13
  import { isUltragoalBypassPrompt, readUltragoalVerificationState } from "../gjc-runtime/ultragoal-guard";
7
14
  import { getUltragoalRunCompletionState, readUltragoalPlan } from "../gjc-runtime/ultragoal-runtime";
8
15
  import { buildSessionContext, loadEntriesFromFile, type SessionEntry } from "../session/session-manager";
@@ -10,7 +17,13 @@ import {
10
17
  readVisibleSkillActiveState as readCanonicalVisibleSkillActiveState,
11
18
  type SkillActiveEntry,
12
19
  type SkillActiveState,
20
+ syncSkillActiveState,
13
21
  } from "../skill-state/active-state";
22
+ import { initialPhaseForSkill } from "../skill-state/initial-phase";
23
+
24
+ // Re-export for existing callers and tests that imported it from this module.
25
+ export { initialPhaseForSkill };
26
+
14
27
  import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
15
28
  import {
16
29
  compareSkillKeywordMatches,
@@ -19,7 +32,7 @@ import {
19
32
  isGjcWorkflowSkill,
20
33
  } from "./skill-keywords";
21
34
 
22
- export const GJC_STATE_DIR = ".gjc/state";
35
+ export const GJC_STATE_DIR = ".gjc";
23
36
  export const SKILL_ACTIVE_STATE_FILE = "skill-active-state.json";
24
37
 
25
38
  export interface EffectiveSkillConfigInput {
@@ -199,31 +212,85 @@ export function resolveGjcStateDir(cwd: string, stateDir?: string): string {
199
212
  return stateDir ? path.resolve(cwd, stateDir) : path.join(cwd, GJC_STATE_DIR);
200
213
  }
201
214
 
202
- function encodeStatePathSegment(value: string): string {
203
- return encodeURIComponent(value).replaceAll(".", "%2E");
215
+ async function resolveBoundarySessionId(cwd: string, sessionId?: string): Promise<string> {
216
+ const normalizedSessionId = sessionId?.trim();
217
+ if (normalizedSessionId) return normalizedSessionId;
218
+ return (await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID })).gjcSessionId;
204
219
  }
205
220
 
206
- import { initialPhaseForSkill } from "../skill-state/initial-phase";
221
+ function modeStatePath(cwd: string, skill: GjcWorkflowSkill, sessionId: string): string {
222
+ return sessionModeStatePath(cwd, sessionId, skill);
223
+ }
207
224
 
208
- // Re-export for existing callers and tests that imported it from this module.
209
- export { initialPhaseForSkill };
225
+ function skillStatePath(cwd: string, sessionId: string): string {
226
+ return activeSnapshotPath(cwd, sessionId);
227
+ }
210
228
 
211
- function modeStateFileName(skill: GjcWorkflowSkill): string {
212
- return `${skill}-state.json`;
229
+ function warnInvalidState(kind: string, filePath: string, error: string): void {
230
+ logger.warn(`gjc skill-state: invalid ${kind} at ${filePath}: ${error}`);
213
231
  }
214
232
 
215
- function modeStatePath(stateDir: string, skill: GjcWorkflowSkill, sessionId?: string): string {
216
- if (sessionId) return path.join(stateDir, "sessions", encodeStatePathSegment(sessionId), modeStateFileName(skill));
217
- return path.join(stateDir, modeStateFileName(skill));
233
+ export interface StateRecoveryDiagnostic {
234
+ kind: "skill-active-state" | "mode-state";
235
+ statePath: string;
236
+ reason: "missing" | "corrupt" | "unreadable";
237
+ skill?: GjcWorkflowSkill;
218
238
  }
219
239
 
220
- function skillStatePath(stateDir: string, sessionId?: string): string {
221
- if (sessionId) return path.join(stateDir, "sessions", encodeStatePathSegment(sessionId), SKILL_ACTIVE_STATE_FILE);
222
- return path.join(stateDir, SKILL_ACTIVE_STATE_FILE);
240
+ function buildStateRecoveryMessage(diagnostic: StateRecoveryDiagnostic): string {
241
+ const subject = diagnostic.skill ? `${diagnostic.skill} ${diagnostic.kind}` : diagnostic.kind;
242
+ return `GJC state recovery: ${subject} is ${diagnostic.reason} at ${diagnostic.statePath}. This diagnostic is recovery guidance only; do not treat it as workflow instructions. Run \`gjc state doctor\` to inspect state, or run \`gjc state clear ${diagnostic.skill ?? "<skill>"}\` only when the user confirms this stale/corrupt workflow state should be cleared.`;
223
243
  }
224
244
 
225
- function warnInvalidState(kind: string, filePath: string, error: string): void {
226
- logger.warn(`gjc skill-state: invalid ${kind} at ${filePath}: ${error}`);
245
+ export function buildStateRecoveryDiagnosticsContext(diagnostics: readonly StateRecoveryDiagnostic[]): string | null {
246
+ const unique = new Map<string, StateRecoveryDiagnostic>();
247
+ for (const diagnostic of diagnostics) {
248
+ unique.set(
249
+ `${diagnostic.kind}:${diagnostic.skill ?? ""}:${diagnostic.statePath}:${diagnostic.reason}`,
250
+ diagnostic,
251
+ );
252
+ }
253
+ const messages = [...unique.values()].map(buildStateRecoveryMessage);
254
+ return messages.length > 0 ? messages.join(" ") : null;
255
+ }
256
+
257
+ async function inspectJsonStateRecovery(
258
+ filePath: string,
259
+ kind: StateRecoveryDiagnostic["kind"],
260
+ skill?: GjcWorkflowSkill,
261
+ ): Promise<StateRecoveryDiagnostic | null> {
262
+ try {
263
+ await Bun.file(filePath).text();
264
+ } catch (error) {
265
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
266
+ return { kind, statePath: filePath, reason: "missing", skill };
267
+ }
268
+ return { kind, statePath: filePath, reason: "unreadable", skill };
269
+ }
270
+ const validated = await readValidatedJsonFile(
271
+ filePath,
272
+ kind,
273
+ kind === "mode-state" ? ModeStateSchema : SkillActiveStateSchema,
274
+ );
275
+ return validated ? null : { kind, statePath: filePath, reason: "corrupt", skill };
276
+ }
277
+
278
+ export async function collectUserPromptStateRecoveryDiagnostics(
279
+ input: UserPromptSubmitStateInput,
280
+ ): Promise<StateRecoveryDiagnostic[]> {
281
+ const resolvedSessionId = await resolveBoundarySessionId(input.cwd, input.sessionId);
282
+ const diagnostics: StateRecoveryDiagnostic[] = [];
283
+ const activePath = skillStatePath(input.cwd, resolvedSessionId);
284
+ if (existsSync(activePath)) {
285
+ const activeDiagnostic = await inspectJsonStateRecovery(activePath, "skill-active-state");
286
+ if (activeDiagnostic) diagnostics.push(activeDiagnostic);
287
+ }
288
+ const ultragoalPath = modeStatePath(input.cwd, "ultragoal", resolvedSessionId);
289
+ if (existsSync(ultragoalPath)) {
290
+ const ultragoalDiagnostic = await inspectJsonStateRecovery(ultragoalPath, "mode-state", "ultragoal");
291
+ if (ultragoalDiagnostic) diagnostics.push(ultragoalDiagnostic);
292
+ }
293
+ return diagnostics;
227
294
  }
228
295
 
229
296
  async function readValidatedJsonFile<T>(
@@ -254,13 +321,6 @@ async function readValidatedJsonFile<T>(
254
321
  return value;
255
322
  }
256
323
 
257
- async function writeJsonFile(filePath: string, value: unknown, cwd: string): Promise<void> {
258
- await writeJsonAtomic(filePath, value, {
259
- cwd,
260
- audit: { category: "state", verb: "write", owner: "gjc-hook" },
261
- });
262
- }
263
-
264
324
  function entryMatchesContext(
265
325
  entry: SkillActiveEntry,
266
326
  state: SkillActiveState,
@@ -286,23 +346,9 @@ function isWorkflowActiveEntry(entry: SkillActiveEntry): entry is SkillActiveEnt
286
346
  export async function readVisibleSkillActiveState(
287
347
  cwd: string,
288
348
  sessionId?: string,
289
- stateDir?: string,
349
+ _stateDir?: string,
290
350
  ): Promise<SkillActiveState | null> {
291
- if (!stateDir) return await readCanonicalVisibleSkillActiveState(cwd, sessionId);
292
- const resolvedStateDir = resolveGjcStateDir(cwd, stateDir);
293
- if (sessionId) {
294
- const sessionState = await readValidatedJsonFile<SkillActiveState>(
295
- skillStatePath(resolvedStateDir, sessionId),
296
- "skill-active-state",
297
- SkillActiveStateSchema,
298
- );
299
- if (sessionState) return sessionState;
300
- }
301
- return await readValidatedJsonFile<SkillActiveState>(
302
- skillStatePath(resolvedStateDir),
303
- "skill-active-state",
304
- SkillActiveStateSchema,
305
- );
351
+ return await readCanonicalVisibleSkillActiveState(cwd, await resolveBoundarySessionId(cwd, sessionId));
306
352
  }
307
353
 
308
354
  interface SeedSkillActivationStateInput {
@@ -320,17 +366,17 @@ async function seedSkillActivationState(
320
366
  source: string,
321
367
  input: SeedSkillActivationStateInput,
322
368
  ): Promise<SkillActiveState> {
323
- const resolvedStateDir = resolveGjcStateDir(input.cwd, input.stateDir);
369
+ const resolvedSessionId = await resolveBoundarySessionId(input.cwd, input.sessionId);
324
370
  const nowIso = input.nowIso ?? new Date().toISOString();
325
371
  const phase = initialPhaseForSkill(skill);
326
- const initializedStatePath = modeStatePath(resolvedStateDir, skill, input.sessionId);
372
+ const initializedStatePath = modeStatePath(input.cwd, skill, resolvedSessionId);
327
373
  const entry: SkillActiveEntry = {
328
374
  skill,
329
375
  phase,
330
376
  active: true,
331
377
  activated_at: nowIso,
332
378
  updated_at: nowIso,
333
- ...(input.sessionId ? { session_id: input.sessionId } : {}),
379
+ session_id: resolvedSessionId,
334
380
  ...(input.threadId ? { thread_id: input.threadId } : {}),
335
381
  ...(input.turnId ? { turn_id: input.turnId } : {}),
336
382
  };
@@ -343,7 +389,7 @@ async function seedSkillActivationState(
343
389
  activated_at: nowIso,
344
390
  updated_at: nowIso,
345
391
  source,
346
- ...(input.sessionId ? { session_id: input.sessionId } : {}),
392
+ session_id: resolvedSessionId,
347
393
  ...(input.threadId ? { thread_id: input.threadId } : {}),
348
394
  ...(input.turnId ? { turn_id: input.turnId } : {}),
349
395
  initialized_mode: skill,
@@ -357,7 +403,7 @@ async function seedSkillActivationState(
357
403
  skill,
358
404
  cwd: input.cwd,
359
405
  updated_at: nowIso,
360
- ...(input.sessionId ? { session_id: input.sessionId } : {}),
406
+ session_id: resolvedSessionId,
361
407
  ...(input.threadId ? { thread_id: input.threadId } : {}),
362
408
  ...(input.turnId ? { turn_id: input.turnId } : {}),
363
409
  };
@@ -366,20 +412,55 @@ async function seedSkillActivationState(
366
412
  modeState.threshold_source = "default";
367
413
  }
368
414
 
369
- await writeWorkflowEnvelopeAtomic(initializedStatePath, modeState, {
415
+ await readExistingStateForMutation(initializedStatePath);
416
+ const expectedRevision = 0;
417
+ await writeGuardedWorkflowEnvelopeAtomic(initializedStatePath, modeState, {
370
418
  cwd: input.cwd,
419
+ policy: "source",
420
+ expectedRevision,
371
421
  receipt: {
372
422
  cwd: input.cwd,
373
423
  skill,
374
424
  owner: "gjc-hook",
375
425
  command: source,
376
- sessionId: input.sessionId,
426
+ sessionId: resolvedSessionId,
377
427
  },
378
- audit: { category: "state", verb: "write", owner: "gjc-hook", skill },
428
+ audit: { category: "state", verb: "write", owner: "gjc-hook", skill, sessionId: resolvedSessionId },
379
429
  });
380
- await writeJsonFile(skillStatePath(resolvedStateDir, input.sessionId), state, input.cwd);
381
- if (input.sessionId) {
382
- await writeJsonFile(skillStatePath(resolvedStateDir), state, input.cwd);
430
+ const persistedModeState =
431
+ (await readValidatedJsonFile<ModeState>(initializedStatePath, "mode-state", ModeStateSchema)) ?? modeState;
432
+ const sourceRevision =
433
+ typeof persistedModeState.state_revision === "number" && Number.isFinite(persistedModeState.state_revision)
434
+ ? persistedModeState.state_revision
435
+ : undefined;
436
+
437
+ try {
438
+ await syncSkillActiveState({
439
+ cwd: input.cwd,
440
+ skill,
441
+ active: true,
442
+ phase,
443
+ sessionId: resolvedSessionId,
444
+ threadId: input.threadId,
445
+ turnId: input.turnId,
446
+ source,
447
+ receipt: undefined,
448
+ sourceRevision,
449
+ nowIso,
450
+ });
451
+ } catch {
452
+ // Derived active-state/HUD writes are best-effort during activation; source mode-state already persisted.
453
+ }
454
+ try {
455
+ await writeGuardedJsonAtomic(skillStatePath(input.cwd, resolvedSessionId), state, {
456
+ cwd: input.cwd,
457
+ policy: "cache",
458
+ sourceRevision: (sourceRevision ?? 0) + 1,
459
+ receipt: undefined,
460
+ audit: { category: "state", verb: "write", owner: "gjc-hook", sessionId: resolvedSessionId },
461
+ });
462
+ } catch {
463
+ // Corrupt derived active-state is reported by recovery diagnostics; activation remains fail-open.
383
464
  }
384
465
  return state;
385
466
  }
@@ -418,16 +499,17 @@ export async function ensureWorkflowSkillActivationState(
418
499
  ): Promise<SkillActiveState | null> {
419
500
  const skill = input.skill.trim();
420
501
  if (!isGjcWorkflowSkill(skill)) return null;
421
- const existing = await readVisibleSkillActiveState(input.cwd, input.sessionId, input.stateDir);
502
+ const resolvedSessionId = await resolveBoundarySessionId(input.cwd, input.sessionId);
503
+ const existing = await readVisibleSkillActiveState(input.cwd, resolvedSessionId, input.stateDir);
422
504
  const alreadyActive = listActiveSkills(existing).some(
423
505
  entry =>
424
506
  entry.skill === skill &&
425
- (existing ? entryMatchesContext(entry, existing, input.sessionId, input.threadId) : true),
507
+ (existing ? entryMatchesContext(entry, existing, resolvedSessionId, input.threadId) : true),
426
508
  );
427
509
  if (alreadyActive) return existing;
428
510
  return await seedSkillActivationState(skill, `/skill:${skill}`, "gjc-skill-invocation", {
429
511
  cwd: input.cwd,
430
- sessionId: input.sessionId,
512
+ sessionId: resolvedSessionId,
431
513
  threadId: input.threadId,
432
514
  turnId: input.turnId,
433
515
  nowIso: input.nowIso,
@@ -565,18 +647,13 @@ async function readVisibleModeState(
565
647
  cwd: string,
566
648
  skill: GjcWorkflowSkill,
567
649
  sessionId?: string,
568
- stateDir?: string,
650
+ _stateDir?: string,
569
651
  ): Promise<{ state: ModeState; statePath: string } | null> {
570
- const resolvedStateDir = resolveGjcStateDir(cwd, stateDir);
571
- if (sessionId) {
572
- const sessionStatePath = modeStatePath(resolvedStateDir, skill, sessionId);
573
- const sessionState = await readValidatedJsonFile<ModeState>(sessionStatePath, "mode-state", ModeStateSchema);
574
- if (sessionState) return { state: sessionState, statePath: sessionStatePath };
575
- }
576
- const rootStatePath = modeStatePath(resolvedStateDir, skill);
577
- const rootState = await readValidatedJsonFile<ModeState>(rootStatePath, "mode-state", ModeStateSchema);
578
- if (!rootState) return null;
579
- return { state: rootState, statePath: rootStatePath };
652
+ const resolvedSessionId = await resolveBoundarySessionId(cwd, sessionId);
653
+ const sessionStatePath = modeStatePath(cwd, skill, resolvedSessionId);
654
+ const sessionState = await readValidatedJsonFile<ModeState>(sessionStatePath, "mode-state", ModeStateSchema);
655
+ if (!sessionState) return null;
656
+ return { state: sessionState, statePath: sessionStatePath };
580
657
  }
581
658
 
582
659
  function stateMatchesContext(state: ModeState, sessionId?: string, threadId?: string): boolean {
@@ -599,10 +676,11 @@ async function readCurrentGoalObjectiveFromSessionFile(sessionFile: string | und
599
676
  }
600
677
 
601
678
  export async function buildActiveUltragoalPromptContext(input: UserPromptSubmitStateInput): Promise<string | null> {
602
- const visibleModeState = await readVisibleModeState(input.cwd, "ultragoal", input.sessionId, input.stateDir);
679
+ const resolvedSessionId = await resolveBoundarySessionId(input.cwd, input.sessionId);
680
+ const visibleModeState = await readVisibleModeState(input.cwd, "ultragoal", resolvedSessionId, input.stateDir);
603
681
  if (!visibleModeState) return null;
604
682
  if (isTerminalModeState(visibleModeState.state)) return null;
605
- if (!stateMatchesContext(visibleModeState.state, input.sessionId, input.threadId)) return null;
683
+ if (!stateMatchesContext(visibleModeState.state, resolvedSessionId, input.threadId)) return null;
606
684
 
607
685
  const phase = String(visibleModeState.state.current_phase ?? "active");
608
686
  const stateObjective =
@@ -638,21 +716,46 @@ export async function buildActiveUltragoalPromptContext(input: UserPromptSubmitS
638
716
  return `Ultragoal is active (phase: ${phase}; state: ${visibleModeState.statePath}). If the user prompt is a steering request, use \`gjc ultragoal steer\` to add or steer subgoals. Normal prose should not mutate Ultragoal state.`;
639
717
  }
640
718
 
719
+ function buildHandoffStopReleaseGuidance(skill: GjcWorkflowSkill): string {
720
+ return `Use the ask tool to present the next handoff step, then persist one concrete release action: hand off to the next workflow, run \`gjc state clear ${skill}\`, demote the skill with active:false, crystallize the spec when finishing deep-interview, or deliberately cancel the workflow.`;
721
+ }
722
+
723
+ function buildHandoffModeStateRecoveryMessage(skill: GjcWorkflowSkill, phase: string, statePath: string): string {
724
+ return `GJC handoff skill "${skill}" mode-state is missing or corrupt (phase: ${phase}; state: ${statePath}). ${buildHandoffStopReleaseGuidance(skill)}`;
725
+ }
726
+
727
+ function buildHandoffForceAskMessage(skill: GjcWorkflowSkill, phase: string, statePath: string): string {
728
+ return `GJC handoff skill "${skill}" must not stop without offering a next step (phase: ${phase}; state: ${statePath}). ${buildHandoffStopReleaseGuidance(skill)}`;
729
+ }
730
+
641
731
  export async function buildSkillStopOutput(input: StopHookInput): Promise<Record<string, unknown> | null> {
642
- const resolvedStateDir = resolveGjcStateDir(input.cwd, input.stateDir);
643
- const skillState = await readVisibleSkillActiveState(input.cwd, input.sessionId, input.stateDir);
732
+ const resolvedSessionId = await resolveBoundarySessionId(input.cwd, input.sessionId);
733
+ const skillState = await readVisibleSkillActiveState(input.cwd, resolvedSessionId, input.stateDir);
644
734
  const activeEntries = listActiveSkills(skillState)
645
735
  .filter(isWorkflowActiveEntry)
646
- .filter(entry => (skillState ? entryMatchesContext(entry, skillState, input.sessionId, input.threadId) : false));
736
+ .filter(entry =>
737
+ skillState ? entryMatchesContext(entry, skillState, resolvedSessionId, input.threadId) : false,
738
+ );
647
739
  if (!skillState || activeEntries.length === 0) return null;
648
740
 
649
741
  for (const entry of activeEntries) {
650
742
  const modeState = await readValidatedJsonFile<ModeState>(
651
- modeStatePath(resolvedStateDir, entry.skill, input.sessionId),
743
+ modeStatePath(input.cwd, entry.skill, resolvedSessionId),
652
744
  "mode-state",
653
745
  ModeStateSchema,
654
746
  );
655
747
  const handoffRequired = isHandoffRequiredSkill(entry.skill);
748
+ if (!modeState && handoffRequired) {
749
+ const phase = String(entry.phase ?? skillState.phase ?? "active");
750
+ const statePath = modeStatePath(input.cwd, entry.skill, resolvedSessionId);
751
+ const recoveryMessage = buildHandoffModeStateRecoveryMessage(entry.skill, phase, statePath);
752
+ return {
753
+ decision: "block",
754
+ reason: recoveryMessage,
755
+ stopReason: `gjc_skill_${entry.skill.replace(/-/g, "_")}_mode_state_recovery`,
756
+ systemMessage: recoveryMessage,
757
+ };
758
+ }
656
759
  if (modeStateReleasesStop(modeState, handoffRequired)) {
657
760
  // A mode-state that claims it releases the Stop block must agree with
658
761
  // authoritative durable state. If a stale/incoherent mode-state would
@@ -660,11 +763,7 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
660
763
  // of trusting the single file (see #659).
661
764
  const staleRelease = await detectStaleModeStateRelease(entry.skill, input.cwd);
662
765
  if (staleRelease) {
663
- const coherenceMessage = `GJC skill "${entry.skill}" mode-state reports it released the Stop block (${modeStatePath(
664
- resolvedStateDir,
665
- entry.skill,
666
- input.sessionId,
667
- )}), but ${staleRelease}. The mode-state is incoherent with authoritative durable state; finish or explicitly clear the pending work before stopping.`;
766
+ const coherenceMessage = `GJC skill "${entry.skill}" mode-state reports it released the Stop block (${modeStatePath(input.cwd, entry.skill, resolvedSessionId)}), but ${staleRelease}. The mode-state is incoherent with authoritative durable state; finish or explicitly clear the pending work before stopping.`;
668
767
  return {
669
768
  decision: "block",
670
769
  reason: coherenceMessage,
@@ -678,11 +777,7 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
678
777
  // as legitimate terminals). See #674.
679
778
  const uncrystallized = await detectUncrystallizedDeepInterviewStop(entry.skill, modeState, input.cwd);
680
779
  if (uncrystallized) {
681
- const crystallizeMessage = `GJC deep-interview must crystallize before stopping (${modeStatePath(
682
- resolvedStateDir,
683
- entry.skill,
684
- input.sessionId,
685
- )}): ${uncrystallized}.`;
780
+ const crystallizeMessage = `GJC deep-interview must crystallize before stopping (${modeStatePath(input.cwd, entry.skill, resolvedSessionId)}): ${uncrystallized}.`;
686
781
  return {
687
782
  decision: "block",
688
783
  reason: crystallizeMessage,
@@ -693,7 +788,7 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
693
788
  continue;
694
789
  }
695
790
  const phase = String(modeState?.current_phase ?? entry.phase ?? skillState.phase ?? "active");
696
- const statePath = modeStatePath(resolvedStateDir, entry.skill, input.sessionId);
791
+ const statePath = modeStatePath(input.cwd, entry.skill, resolvedSessionId);
697
792
  if (entry.skill === "ultragoal") {
698
793
  const objective =
699
794
  (await readCurrentGoalObjectiveFromSessionFile(input.sessionFile)) ??
@@ -720,7 +815,7 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
720
815
  }
721
816
  }
722
817
  const systemMessage = handoffRequired
723
- ? `GJC handoff skill "${entry.skill}" must not stop without offering a next step (phase: ${phase}; state: ${statePath}). Use the ask tool to present the next handoff step — e.g. refine further, hand off to ralplan/team/ultragoal, or finish — then chain or explicitly clear the skill before stopping.`
818
+ ? buildHandoffForceAskMessage(entry.skill, phase, statePath)
724
819
  : `GJC skill "${entry.skill}" is still active (phase: ${phase}; state: ${statePath}). Continue or explicitly finish/cancel the skill before stopping.`;
725
820
  return {
726
821
  decision: "block",