@gajae-code/coding-agent 0.6.4 → 0.7.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 (231) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/migrate-cli.d.ts +20 -0
  5. package/dist/types/cli/notify-cli.d.ts +23 -0
  6. package/dist/types/cli/setup-cli.d.ts +20 -1
  7. package/dist/types/commands/daemon.d.ts +41 -0
  8. package/dist/types/commands/migrate.d.ts +33 -0
  9. package/dist/types/commands/notify.d.ts +41 -0
  10. package/dist/types/config/keybindings.d.ts +4 -0
  11. package/dist/types/config/model-profile-activation.d.ts +12 -0
  12. package/dist/types/config/model-profiles.d.ts +2 -1
  13. package/dist/types/config/model-registry.d.ts +3 -3
  14. package/dist/types/config/models-config-schema.d.ts +5 -0
  15. package/dist/types/config/settings-schema.d.ts +38 -0
  16. package/dist/types/coordinator/contract.d.ts +1 -1
  17. package/dist/types/daemon/builtin.d.ts +20 -0
  18. package/dist/types/daemon/control-types.d.ts +57 -0
  19. package/dist/types/daemon/runtime.d.ts +25 -0
  20. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  21. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
  22. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
  23. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  24. package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
  25. package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
  26. package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
  27. package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
  28. package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
  29. package/dist/types/gjc-runtime/state-writer.d.ts +38 -7
  30. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
  31. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
  32. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
  33. package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
  34. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  35. package/dist/types/hooks/skill-state.d.ts +12 -4
  36. package/dist/types/migrate/action-planner.d.ts +11 -0
  37. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  38. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  39. package/dist/types/migrate/adapters/index.d.ts +45 -0
  40. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  41. package/dist/types/migrate/executor.d.ts +2 -0
  42. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  43. package/dist/types/migrate/report.d.ts +18 -0
  44. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  45. package/dist/types/migrate/types.d.ts +126 -0
  46. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  47. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  48. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  49. package/dist/types/modes/interactive-mode.d.ts +1 -1
  50. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  51. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  52. package/dist/types/modes/types.d.ts +7 -1
  53. package/dist/types/notifications/config-commands.d.ts +26 -0
  54. package/dist/types/notifications/config.d.ts +61 -0
  55. package/dist/types/notifications/helpers.d.ts +55 -0
  56. package/dist/types/notifications/html-format.d.ts +62 -0
  57. package/dist/types/notifications/index.d.ts +28 -0
  58. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  59. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  60. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  61. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  62. package/dist/types/notifications/telegram-daemon.d.ts +276 -0
  63. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  64. package/dist/types/notifications/threaded-inbound.d.ts +58 -0
  65. package/dist/types/notifications/threaded-render.d.ts +66 -0
  66. package/dist/types/notifications/topic-registry.d.ts +67 -0
  67. package/dist/types/research-plan/index.d.ts +1 -0
  68. package/dist/types/research-plan/ledger.d.ts +33 -0
  69. package/dist/types/rlm/artifacts.d.ts +1 -1
  70. package/dist/types/rlm/index.d.ts +12 -0
  71. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  72. package/dist/types/session/agent-session.d.ts +39 -2
  73. package/dist/types/session/auth-storage.d.ts +1 -1
  74. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  75. package/dist/types/setup/credential-import.d.ts +3 -0
  76. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  77. package/dist/types/skill-state/active-state.d.ts +6 -11
  78. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  79. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  80. package/dist/types/task/spawn-gate.d.ts +1 -10
  81. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  82. package/dist/types/tools/index.d.ts +18 -0
  83. package/dist/types/tools/subagent.d.ts +3 -0
  84. package/package.json +7 -7
  85. package/scripts/build-binary.ts +3 -0
  86. package/src/async/job-manager.ts +5 -1
  87. package/src/cli/daemon-cli.ts +122 -0
  88. package/src/cli/migrate-cli.ts +106 -0
  89. package/src/cli/notify-cli.ts +274 -0
  90. package/src/cli/setup-cli.ts +173 -84
  91. package/src/cli.ts +3 -0
  92. package/src/commands/daemon.ts +47 -0
  93. package/src/commands/deep-interview.ts +2 -2
  94. package/src/commands/migrate.ts +46 -0
  95. package/src/commands/notify.ts +61 -0
  96. package/src/commands/setup.ts +11 -1
  97. package/src/commands/state.ts +2 -1
  98. package/src/commands/team.ts +7 -3
  99. package/src/config/model-profile-activation.ts +74 -5
  100. package/src/config/model-profiles.ts +7 -4
  101. package/src/config/model-registry.ts +6 -3
  102. package/src/config/models-config-schema.ts +1 -1
  103. package/src/config/settings-schema.ts +29 -0
  104. package/src/coordinator/contract.ts +3 -0
  105. package/src/coordinator-mcp/policy.ts +10 -2
  106. package/src/coordinator-mcp/server.ts +270 -1
  107. package/src/daemon/builtin.ts +46 -0
  108. package/src/daemon/control-types.ts +65 -0
  109. package/src/daemon/runtime.ts +51 -0
  110. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
  111. package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
  112. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  113. package/src/defaults/gjc/skills/team/SKILL.md +51 -47
  114. package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -13
  115. package/src/extensibility/custom-commands/loader.ts +0 -7
  116. package/src/extensibility/extensions/runner.ts +4 -0
  117. package/src/extensibility/extensions/types.ts +8 -0
  118. package/src/extensibility/gjc-plugins/injection.ts +23 -4
  119. package/src/extensibility/gjc-plugins/state.ts +16 -1
  120. package/src/gjc-runtime/deep-interview-recorder.ts +51 -18
  121. package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
  122. package/src/gjc-runtime/goal-mode-request.ts +26 -11
  123. package/src/gjc-runtime/launch-tmux.ts +6 -1
  124. package/src/gjc-runtime/ralplan-runtime.ts +79 -50
  125. package/src/gjc-runtime/session-layout.ts +180 -0
  126. package/src/gjc-runtime/session-resolution.ts +217 -0
  127. package/src/gjc-runtime/state-graph.ts +1 -2
  128. package/src/gjc-runtime/state-migrations.ts +1 -0
  129. package/src/gjc-runtime/state-runtime.ts +247 -124
  130. package/src/gjc-runtime/state-schema.ts +2 -0
  131. package/src/gjc-runtime/state-writer.ts +289 -41
  132. package/src/gjc-runtime/team-runtime.ts +43 -19
  133. package/src/gjc-runtime/tmux-sessions.ts +7 -1
  134. package/src/gjc-runtime/ultragoal-guard.ts +102 -4
  135. package/src/gjc-runtime/ultragoal-runtime.ts +226 -60
  136. package/src/gjc-runtime/workflow-command-ref.ts +1 -2
  137. package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
  138. package/src/gjc-runtime/workflow-manifest.ts +12 -3
  139. package/src/goals/tools/goal-tool.ts +11 -2
  140. package/src/harness-control-plane/storage.ts +14 -4
  141. package/src/hooks/native-skill-hook.ts +38 -12
  142. package/src/hooks/skill-state.ts +178 -83
  143. package/src/internal-urls/docs-index.generated.ts +9 -6
  144. package/src/main.ts +30 -0
  145. package/src/migrate/action-planner.ts +318 -0
  146. package/src/migrate/adapters/claude-code.ts +39 -0
  147. package/src/migrate/adapters/codex.ts +70 -0
  148. package/src/migrate/adapters/index.ts +277 -0
  149. package/src/migrate/adapters/opencode.ts +52 -0
  150. package/src/migrate/executor.ts +81 -0
  151. package/src/migrate/mcp-mapper.ts +152 -0
  152. package/src/migrate/report.ts +104 -0
  153. package/src/migrate/skill-normalizer.ts +80 -0
  154. package/src/migrate/types.ts +163 -0
  155. package/src/modes/acp/acp-event-mapper.ts +1 -0
  156. package/src/modes/bridge/bridge-mode.ts +2 -2
  157. package/src/modes/components/custom-editor.ts +30 -20
  158. package/src/modes/components/hook-editor.ts +7 -2
  159. package/src/modes/components/oauth-selector.ts +19 -0
  160. package/src/modes/controllers/event-controller.ts +20 -0
  161. package/src/modes/controllers/selector-controller.ts +80 -17
  162. package/src/modes/interactive-mode.ts +6 -2
  163. package/src/modes/rpc/rpc-mode.ts +2 -2
  164. package/src/modes/runtime-init.ts +1 -0
  165. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  166. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  167. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  168. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  169. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  170. package/src/modes/types.ts +7 -1
  171. package/src/modes/utils/ui-helpers.ts +23 -0
  172. package/src/notifications/config-commands.ts +50 -0
  173. package/src/notifications/config.ts +107 -0
  174. package/src/notifications/helpers.ts +135 -0
  175. package/src/notifications/html-format.ts +389 -0
  176. package/src/notifications/index.ts +663 -0
  177. package/src/notifications/rate-limit-pool.ts +179 -0
  178. package/src/notifications/telegram-cli.ts +194 -0
  179. package/src/notifications/telegram-daemon-cli.ts +74 -0
  180. package/src/notifications/telegram-daemon-control.ts +370 -0
  181. package/src/notifications/telegram-daemon.ts +1370 -0
  182. package/src/notifications/telegram-reference.ts +335 -0
  183. package/src/notifications/threaded-inbound.ts +80 -0
  184. package/src/notifications/threaded-render.ts +155 -0
  185. package/src/notifications/topic-registry.ts +133 -0
  186. package/src/prompts/agents/init.md +1 -1
  187. package/src/prompts/system/plan-mode-active.md +1 -1
  188. package/src/prompts/tools/ast-grep.md +1 -1
  189. package/src/prompts/tools/search.md +1 -1
  190. package/src/prompts/tools/task.md +1 -2
  191. package/src/research-plan/index.ts +1 -0
  192. package/src/research-plan/ledger.ts +177 -0
  193. package/src/rlm/artifacts.ts +12 -3
  194. package/src/rlm/index.ts +26 -0
  195. package/src/runtime-mcp/config-writer.ts +46 -0
  196. package/src/sdk.ts +16 -0
  197. package/src/session/agent-session.ts +128 -24
  198. package/src/session/auth-storage.ts +3 -0
  199. package/src/session/session-dump-format.ts +43 -2
  200. package/src/session/session-manager.ts +39 -5
  201. package/src/setup/credential-auto-import.ts +258 -0
  202. package/src/setup/credential-import.ts +17 -0
  203. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  204. package/src/setup/hermes-setup.ts +1 -1
  205. package/src/setup/host-plugin-setup.ts +142 -0
  206. package/src/skill-state/active-state.ts +72 -108
  207. package/src/skill-state/canonical-skills.ts +4 -0
  208. package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
  209. package/src/skill-state/workflow-hud.ts +4 -2
  210. package/src/skill-state/workflow-state-contract.ts +3 -3
  211. package/src/slash-commands/builtin-registry.ts +4 -1
  212. package/src/task/agents.ts +1 -22
  213. package/src/task/executor.ts +5 -1
  214. package/src/task/index.ts +1 -41
  215. package/src/task/spawn-gate.ts +1 -38
  216. package/src/task/types.ts +1 -1
  217. package/src/tools/ask-answer-registry.ts +25 -0
  218. package/src/tools/ask.ts +108 -16
  219. package/src/tools/computer.ts +58 -4
  220. package/src/tools/image-gen.ts +5 -8
  221. package/src/tools/index.ts +19 -0
  222. package/src/tools/inspect-image.ts +16 -11
  223. package/src/tools/subagent-render.ts +7 -0
  224. package/src/tools/subagent.ts +38 -7
  225. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  226. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  227. package/src/prompts/agents/explore.md +0 -58
  228. package/src/prompts/agents/plan.md +0 -49
  229. package/src/prompts/agents/reviewer.md +0 -141
  230. package/src/prompts/agents/task.md +0 -16
  231. package/src/prompts/review-request.md +0 -70
@@ -1,4 +1,6 @@
1
- import * as path from "node:path";
1
+ import * as logger from "@gajae-code/utils/logger";
2
+ import { activeSnapshotPath, assertNonEmptyGjcSessionId, modeStatePath } from "../gjc-runtime/session-layout";
3
+ import { resolveGjcSessionForRead, SessionResolutionError } from "../gjc-runtime/session-resolution";
2
4
  import {
3
5
  type ActiveSessionScope,
4
6
  readActiveEntries,
@@ -6,13 +8,12 @@ import {
6
8
  removeActiveEntry,
7
9
  writeActiveEntry,
8
10
  } from "../gjc-runtime/state-writer";
11
+ import { CANONICAL_GJC_WORKFLOW_SKILLS, type CanonicalGjcWorkflowSkill } from "./canonical-skills";
9
12
  import type { WorkflowStateReceipt } from "./workflow-state-contract";
10
13
 
11
14
  export const SKILL_ACTIVE_STATE_FILE = "skill-active-state.json";
12
15
 
13
- export const CANONICAL_GJC_WORKFLOW_SKILLS = ["deep-interview", "ralplan", "ultragoal", "team"] as const;
14
-
15
- export type CanonicalGjcWorkflowSkill = (typeof CANONICAL_GJC_WORKFLOW_SKILLS)[number];
16
+ export { CANONICAL_GJC_WORKFLOW_SKILLS, type CanonicalGjcWorkflowSkill };
16
17
  export type WorkflowHudSeverity = "info" | "warning" | "blocked" | "error" | "success";
17
18
 
18
19
  export interface WorkflowHudChip {
@@ -60,6 +61,7 @@ export interface SkillActiveEntry {
60
61
  handoff_to?: string;
61
62
  handoff_at?: string;
62
63
  active_subskills?: ActiveSubskillEntry[];
64
+ source_state_revision?: number;
63
65
  }
64
66
 
65
67
  export interface SkillActiveState {
@@ -83,7 +85,7 @@ export interface SkillActiveState {
83
85
 
84
86
  export interface SkillActiveStatePaths {
85
87
  rootPath: string;
86
- sessionPath?: string;
88
+ sessionPath: string;
87
89
  }
88
90
 
89
91
  export interface SyncSkillActiveStateOptions {
@@ -102,6 +104,7 @@ export interface SyncSkillActiveStateOptions {
102
104
  handoff_to?: string;
103
105
  handoff_at?: string;
104
106
  active_subskills?: ActiveSubskillEntry[];
107
+ sourceRevision?: number;
105
108
  }
106
109
 
107
110
  const HUD_TEXT_LIMIT = 80;
@@ -246,8 +249,12 @@ function unionActiveSubskillEntries(...entrySets: Array<ActiveSubskillEntry[] |
246
249
  return merged;
247
250
  }
248
251
 
249
- function encodePathSegment(value: string): string {
250
- return encodeURIComponent(value).replaceAll(".", "%2E");
252
+ function resolveBoundarySessionId(cwd: string, sessionId?: string): Promise<string> {
253
+ const normalizedSessionId = safeString(sessionId).trim();
254
+ if (normalizedSessionId) return Promise.resolve(normalizedSessionId);
255
+ return resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID }).then(
256
+ context => context.gjcSessionId,
257
+ );
251
258
  }
252
259
 
253
260
  function entryKey(entry: Pick<SkillActiveEntry, "skill" | "session_id">): string {
@@ -343,14 +350,10 @@ export function normalizeSkillActiveState(raw: unknown): SkillActiveState | null
343
350
  }
344
351
 
345
352
  export function getSkillActiveStatePaths(cwd: string, sessionId?: string): SkillActiveStatePaths {
346
- const stateDir = path.join(cwd, ".gjc", "state");
347
- const rootPath = path.join(stateDir, SKILL_ACTIVE_STATE_FILE);
348
353
  const normalizedSessionId = safeString(sessionId).trim();
349
- if (!normalizedSessionId) return { rootPath };
350
- return {
351
- rootPath,
352
- sessionPath: path.join(stateDir, "sessions", encodePathSegment(normalizedSessionId), SKILL_ACTIVE_STATE_FILE),
353
- };
354
+ assertNonEmptyGjcSessionId(normalizedSessionId, "getSkillActiveStatePaths");
355
+ const sessionPath = activeSnapshotPath(cwd, normalizedSessionId);
356
+ return { rootPath: sessionPath, sessionPath };
354
357
  }
355
358
 
356
359
  /**
@@ -380,7 +383,12 @@ async function readRawActiveStateForHandoff(filePath: string, strict: boolean):
380
383
  if (!parsed || typeof parsed !== "object") return null;
381
384
  return parsed as SkillActiveState;
382
385
  } catch (err) {
383
- if (!strict) return null;
386
+ if (!strict) {
387
+ logger.warn(
388
+ `gjc skill-state: invalid skill-active-state at ${filePath}: invalid JSON: ${(err as Error).message}`,
389
+ );
390
+ return null;
391
+ }
384
392
  throw err;
385
393
  }
386
394
  }
@@ -419,14 +427,10 @@ function rawActiveEntries(state: SkillActiveState | null): SkillActiveEntry[] {
419
427
 
420
428
  async function readModeStatePhase(
421
429
  cwd: string,
422
- sessionId: string | undefined,
430
+ sessionId: string,
423
431
  skill: CanonicalGjcWorkflowSkill,
424
432
  ): Promise<string | undefined> {
425
- const stateDir = path.join(cwd, ".gjc", "state");
426
- const normalizedSessionId = safeString(sessionId).trim();
427
- const filePath = normalizedSessionId
428
- ? path.join(stateDir, "sessions", encodePathSegment(normalizedSessionId), `${skill}-state.json`)
429
- : path.join(stateDir, `${skill}-state.json`);
433
+ const filePath = modeStatePath(cwd, sessionId, skill);
430
434
  try {
431
435
  const parsed = JSON.parse(await Bun.file(filePath).text());
432
436
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
@@ -473,15 +477,6 @@ function withCanonicalRalplanPhase(entry: SkillActiveEntry, canonicalPhase: stri
473
477
  };
474
478
  }
475
479
 
476
- function filterRootEntriesForSession(entries: SkillActiveEntry[], sessionId?: string): SkillActiveEntry[] {
477
- const normalizedSessionId = safeString(sessionId).trim();
478
- if (!normalizedSessionId) return entries;
479
- return entries.filter(entry => {
480
- const entrySessionId = safeString(entry.session_id).trim();
481
- return entrySessionId.length === 0 || entrySessionId === normalizedSessionId;
482
- });
483
- }
484
-
485
480
  function entryRecency(entry: SkillActiveEntry): number {
486
481
  const stamp = entry.handoff_at || entry.updated_at || entry.activated_at;
487
482
  const ms = stamp ? Date.parse(stamp) : Number.NaN;
@@ -603,57 +598,50 @@ export function collapsePlanningPipeline(entries: readonly SkillActiveEntry[]):
603
598
  async function mergeVisibleEntries(
604
599
  cwd: string,
605
600
  sessionState: SkillActiveState | null,
606
- rootState: SkillActiveState | null,
607
- sessionId?: string,
601
+ sessionId: string,
608
602
  ): Promise<SkillActiveEntry[]> {
609
603
  // Use the raw (active + inactive) rows so a handoff demotion stays visible
610
604
  // long enough to supersede a stale same-skill row before the active filter.
611
605
  // Per-skill files in active/<skill>.json are authoritative and are merged
612
606
  // after the derived snapshot cache, so a stale skill-active-state.json row
613
607
  // cannot override the latest entry file.
614
- const rootEntries = filterRootEntriesForSession(
615
- [...rawActiveEntries(rootState), ...(await readActiveEntries(cwd))],
616
- sessionId,
617
- );
618
- const merged = new Map(rootEntries.map(entry => [entryKey(entry), entry]));
619
- const sessionEntries = sessionId
620
- ? [...rawActiveEntries(sessionState), ...(await readActiveEntries(cwd, { sessionId }))]
621
- : rawActiveEntries(sessionState);
622
- for (const entry of sessionEntries) {
623
- merged.set(entryKey(entry), entry);
624
- }
608
+ const entries = [...rawActiveEntries(sessionState), ...(await readActiveEntries(cwd, { sessionId }))];
609
+ const merged = new Map(entries.map(entry => [entryKey(entry), entry]));
625
610
  const canonicalRalplanPhase = await readModeStatePhase(cwd, sessionId, "ralplan");
626
- return collapsePlanningPipeline(
627
- dedupeVisibleBySkill([...merged.values()], sessionId)
628
- .filter(entry => entry.active !== false)
629
- .map(entry => withCanonicalRalplanPhase(entry, canonicalRalplanPhase)),
630
- );
611
+ const visibleEntries = dedupeVisibleBySkill([...merged.values()], sessionId)
612
+ .filter(entry => entry.active !== false)
613
+ .map(entry => withCanonicalRalplanPhase(entry, canonicalRalplanPhase));
614
+ return collapsePlanningPipeline(visibleEntries).toSorted(comparePipelineEntry);
631
615
  }
632
616
 
633
617
  export async function readVisibleSkillActiveState(cwd: string, sessionId?: string): Promise<SkillActiveState | null> {
634
- const { rootPath, sessionPath } = getSkillActiveStatePaths(cwd, sessionId);
635
- const [rootState, sessionState] = await Promise.all([
636
- readRawActiveStateForHandoff(rootPath, false),
637
- sessionPath ? readRawActiveStateForHandoff(sessionPath, false) : Promise.resolve(null),
638
- ]);
639
- const activeSkills = await mergeVisibleEntries(cwd, sessionState, rootState, sessionId);
618
+ let resolvedSessionId: string;
619
+ try {
620
+ resolvedSessionId = await resolveBoundarySessionId(cwd, sessionId);
621
+ } catch (error) {
622
+ if (error instanceof SessionResolutionError && error.code === "no_session") return null;
623
+ throw error;
624
+ }
625
+ const { sessionPath } = getSkillActiveStatePaths(cwd, resolvedSessionId);
626
+ const sessionState = await readRawActiveStateForHandoff(sessionPath, false);
627
+ const activeSkills = await mergeVisibleEntries(cwd, sessionState, resolvedSessionId);
640
628
  if (activeSkills.length === 0) return null;
641
629
  const primary = activeSkills[0];
642
630
  return {
643
- ...(rootState ?? {}),
644
631
  ...(sessionState ?? {}),
645
632
  version: 1,
646
633
  active: true,
647
- skill: primary?.skill ?? "",
648
- phase: primary?.phase ?? "",
649
- session_id: safeString(sessionId).trim() || primary?.session_id,
634
+ skill: sessionState?.skill ?? primary?.skill ?? "",
635
+ phase: sessionState?.phase ?? primary?.phase ?? "",
636
+ session_id: resolvedSessionId,
650
637
  active_skills: activeSkills,
651
638
  active_subskills: activeSkills.flatMap(entry => entry.active_subskills ?? []),
652
639
  };
653
640
  }
654
641
 
655
- function activeStateWriterAudit(verb: string) {
656
- return { category: "state" as const, verb, owner: "gjc-runtime" as const };
642
+ function activeStateWriterAudit(verb: string, sessionScope?: ActiveSessionScope | string) {
643
+ const sessionId = typeof sessionScope === "string" ? sessionScope : sessionScope?.sessionId;
644
+ return { category: "state" as const, verb, owner: "gjc-runtime" as const, ...(sessionId ? { sessionId } : {}) };
657
645
  }
658
646
 
659
647
  async function persistActiveEntry(
@@ -664,12 +652,13 @@ async function persistActiveEntry(
664
652
  if (entry.active === false) {
665
653
  await removeActiveEntry(cwd, sessionScope, entry.skill, {
666
654
  cwd,
667
- audit: activeStateWriterAudit("remove-active-entry"),
655
+ audit: activeStateWriterAudit("remove-active-entry", sessionScope),
656
+ sourceRevision: entry.source_state_revision,
668
657
  });
669
658
  } else {
670
659
  await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
671
660
  cwd,
672
- audit: activeStateWriterAudit("write-active-entry"),
661
+ audit: activeStateWriterAudit("write-active-entry", sessionScope),
673
662
  });
674
663
  }
675
664
  }
@@ -681,12 +670,15 @@ async function writeHandoffEntry(
681
670
  ): Promise<void> {
682
671
  await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
683
672
  cwd,
684
- audit: activeStateWriterAudit("write-active-entry"),
673
+ audit: activeStateWriterAudit("write-active-entry", sessionScope),
685
674
  });
686
675
  }
687
676
 
688
677
  async function rebuildActiveState(cwd: string, sessionScope?: ActiveSessionScope): Promise<void> {
689
- await rebuildActiveSnapshot(cwd, sessionScope, { cwd, audit: activeStateWriterAudit("rebuild-active-snapshot") });
678
+ await rebuildActiveSnapshot(cwd, sessionScope, {
679
+ cwd,
680
+ audit: activeStateWriterAudit("rebuild-active-snapshot", sessionScope),
681
+ });
690
682
  }
691
683
 
692
684
  async function removeSupersededPlanningPipelineEntries(
@@ -698,7 +690,7 @@ async function removeSupersededPlanningPipelineEntries(
698
690
  for (const skill of upstreamPlanningPipelineSkills(entry.skill)) {
699
691
  await removeActiveEntry(cwd, sessionScope, skill, {
700
692
  cwd,
701
- audit: activeStateWriterAudit("remove-superseded-pipeline-entry"),
693
+ audit: activeStateWriterAudit("remove-superseded-pipeline-entry", sessionScope),
702
694
  });
703
695
  }
704
696
  }
@@ -708,18 +700,17 @@ async function activeSubskillsForExistingEntry(
708
700
  sessionId: string | undefined,
709
701
  skill: string,
710
702
  ): Promise<ActiveSubskillEntry[] | undefined> {
711
- const { rootPath, sessionPath } = getSkillActiveStatePaths(cwd, sessionId);
712
- const [rootState, sessionState] = await Promise.all([
713
- readRawActiveStateForHandoff(rootPath, false),
714
- sessionPath ? readRawActiveStateForHandoff(sessionPath, false) : Promise.resolve(null),
715
- ]);
716
- const existing = (await mergeVisibleEntries(cwd, sessionState, rootState, sessionId)).find(
703
+ const resolvedSessionId = await resolveBoundarySessionId(cwd, sessionId);
704
+ const { sessionPath } = getSkillActiveStatePaths(cwd, resolvedSessionId);
705
+ const sessionState = await readRawActiveStateForHandoff(sessionPath, false);
706
+ const existing = (await mergeVisibleEntries(cwd, sessionState, resolvedSessionId)).find(
717
707
  entry => entry.skill === skill,
718
708
  );
719
709
  return existing?.active_subskills;
720
710
  }
721
711
 
722
712
  export async function syncSkillActiveState(options: SyncSkillActiveStateOptions): Promise<void> {
713
+ if (!options.sessionId) return;
723
714
  const preservedActiveSubskills =
724
715
  options.active_subskills === undefined
725
716
  ? await activeSubskillsForExistingEntry(options.cwd, options.sessionId, options.skill)
@@ -745,12 +736,8 @@ export async function syncSkillActiveState(options: SyncSkillActiveStateOptions)
745
736
  : preservedActiveSubskills
746
737
  ? { active_subskills: preservedActiveSubskills }
747
738
  : {}),
739
+ ...(typeof options.sourceRevision === "number" ? { source_state_revision: options.sourceRevision } : {}),
748
740
  };
749
- await removeSupersededPlanningPipelineEntries(options.cwd, undefined, entry);
750
- await persistActiveEntry(options.cwd, undefined, entry);
751
- await rebuildActiveState(options.cwd);
752
-
753
- if (!options.sessionId) return;
754
741
  const sessionScope = { sessionId: options.sessionId };
755
742
  await removeSupersededPlanningPipelineEntries(options.cwd, sessionScope, entry);
756
743
  await persistActiveEntry(options.cwd, sessionScope, entry);
@@ -768,36 +755,23 @@ export interface ApplyHandoffOptions {
768
755
  }
769
756
 
770
757
  /**
771
- * Atomically apply a workflow-skill handoff to both the session-scoped and
772
- * root `skill-active-state.json` files in a single write per file.
773
- *
774
- * Write order: **session first, root last**. The session file is the
775
- * source of truth for HUD; the root aggregate must never lead the session
776
- * during a handoff window. Each file is rewritten once with caller demoted
777
- * to `active:false` (preserving `handoff_to`/`handoff_at` lineage) and
778
- * callee promoted to `active:true` (with `handoff_from`/`handoff_at`).
758
+ * Atomically apply a workflow-skill handoff to the session-scoped active state.
779
759
  */
780
760
  export async function applyHandoffToActiveState(options: ApplyHandoffOptions): Promise<void> {
781
761
  const nowIso = options.nowIso ?? new Date().toISOString();
782
762
  const callerEntry = buildSyncEntry(options.caller, nowIso);
783
763
  const calleeEntry = buildSyncEntry(options.callee, nowIso);
784
764
  const sessionId = options.callee.sessionId ?? options.caller.sessionId;
785
- const { rootPath, sessionPath } = getSkillActiveStatePaths(options.cwd, sessionId);
765
+ assertNonEmptyGjcSessionId(sessionId, "applyHandoffToActiveState");
766
+ const { sessionPath } = getSkillActiveStatePaths(options.cwd, sessionId);
786
767
  const readState = (filePath: string) => readRawActiveStateForHandoff(filePath, options.strict === true);
787
- await Promise.all([readState(rootPath), ...(sessionPath ? [readState(sessionPath)] : [])]);
788
-
789
- // A skill can hold more than one visible row in this session's scope — e.g.
790
- // it was seeded without a session id (rendered globally) and is now handed
791
- // off under a concrete session id. Supersede every same-session-scope row of
792
- // the caller and callee skills, not just the exact `skill::session_id` key,
793
- // so a stale `active:true` row cannot survive the demotion and keep showing
794
- // in the HUD. Rows owned by other sessions are left untouched.
768
+ await readState(sessionPath);
769
+
795
770
  const handoffSession = safeString(sessionId).trim();
796
771
  const reassignedSkills = new Set([callerEntry.skill, calleeEntry.skill]);
797
772
  const supersedesVisible = (entry: SkillActiveEntry): boolean => {
798
773
  if (!reassignedSkills.has(entry.skill)) return false;
799
- const entrySession = safeString(entry.session_id).trim();
800
- return entrySession.length === 0 || entrySession === handoffSession;
774
+ return safeString(entry.session_id).trim() === handoffSession;
801
775
  };
802
776
  const applyEntries = (entries: SkillActiveEntry[]): SkillActiveEntry[] => {
803
777
  const callerKey = entryKey(callerEntry);
@@ -805,9 +779,6 @@ export async function applyHandoffToActiveState(options: ApplyHandoffOptions): P
805
779
  entries.find(e => entryKey(e) === callerKey) ??
806
780
  entries.find(e => e.skill === callerEntry.skill && supersedesVisible(e) && Boolean(e.handoff_from));
807
781
  const kept = entries.filter(e => !supersedesVisible(e));
808
- // Merge prior lineage into the demoted caller so multi-step handoff
809
- // chains preserve `handoff_from` from the previous transition while
810
- // the new `handoff_to`/`handoff_at` describe this one.
811
782
  const mergedCaller: SkillActiveEntry = priorCaller
812
783
  ? {
813
784
  ...callerEntry,
@@ -822,10 +793,7 @@ export async function applyHandoffToActiveState(options: ApplyHandoffOptions): P
822
793
  activeSubskills.length > 0 ? { ...calleeEntry, active_subskills: activeSubskills } : calleeEntry;
823
794
  return [...kept, mergedCaller, mergedCallee];
824
795
  };
825
- const writeEntries = async (
826
- sessionScope: ActiveSessionScope | undefined,
827
- prior: SkillActiveState | null,
828
- ): Promise<void> => {
796
+ const writeEntries = async (sessionScope: ActiveSessionScope, prior: SkillActiveState | null): Promise<void> => {
829
797
  const nextEntries = applyEntries(rawActiveEntries(prior));
830
798
  for (const entry of nextEntries) {
831
799
  await writeHandoffEntry(options.cwd, sessionScope, entry);
@@ -833,12 +801,8 @@ export async function applyHandoffToActiveState(options: ApplyHandoffOptions): P
833
801
  await rebuildActiveState(options.cwd, sessionScope);
834
802
  };
835
803
 
836
- if (sessionPath) {
837
- const prior = await readState(sessionPath);
838
- await writeEntries({ sessionId }, prior);
839
- }
840
- const priorRoot = await readState(rootPath);
841
- await writeEntries(undefined, priorRoot);
804
+ const prior = await readState(sessionPath);
805
+ await writeEntries({ sessionId }, prior);
842
806
  }
843
807
 
844
808
  function buildSyncEntry(options: SyncSkillActiveStateOptions, nowIso: string): SkillActiveEntry {
@@ -0,0 +1,4 @@
1
+ /** Native-free canonical GJC workflow skill identifiers. */
2
+ export const CANONICAL_GJC_WORKFLOW_SKILLS = ["deep-interview", "ralplan", "ultragoal", "team"] as const;
3
+
4
+ export type CanonicalGjcWorkflowSkill = (typeof CANONICAL_GJC_WORKFLOW_SKILLS)[number];
@@ -4,6 +4,8 @@ import * as path from "node:path";
4
4
  import type { AgentTool } from "@gajae-code/agent-core";
5
5
  import { logger } from "@gajae-code/utils";
6
6
  import { expandApplyPatchToEntries } from "../edit/modes/apply-patch";
7
+ import { GJC_SESSION_PREFIX, modeStatePath as sessionModeStatePath } from "../gjc-runtime/session-layout";
8
+ import { resolveGjcSessionForRead } from "../gjc-runtime/session-resolution";
7
9
  import { ModeStateSchema } from "../gjc-runtime/state-schema";
8
10
  import { LocalProtocolHandler, resolveLocalUrlToPath } from "../internal-urls/local-protocol";
9
11
  import { resolveToCwd } from "../tools/path-utils";
@@ -31,27 +33,10 @@ function planningPhaseBlockMessage(skill: CanonicalGjcWorkflowSkill): string {
31
33
  return DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE;
32
34
  }
33
35
 
34
- const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit", "bash"]);
36
+ const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit"]);
35
37
  const ARCHIVE_OR_SQLITE_BASE_RE = /^(.+?\.(?:tar\.gz|sqlite3|sqlite|db3|zip|tgz|tar|db))(?:$|:)/i;
36
38
  const INTERNAL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
37
39
  const VIM_FILE_SWITCH_RE = /^\s*:(?:e|e!|edit|edit!)(?:\s+([^<\r\n]+))?(?:<CR>|\r|\n|$)/i;
38
- const BASH_TOKEN_RE = /'[^']*'|"(?:\\.|[^"\\])*"|\S+/g;
39
- const BASH_REDIRECT_RE = /^(?:\d*)>>?$/;
40
- const BASH_HEREDOC_RE = /^(?:\d*)<<-?$/;
41
- // Shell command-list / redirection / substitution operators. Includes `\r` and
42
- // `\n` because the shell treats a newline as a command separator and tool command
43
- // strings can be multiline (e.g. heredocs).
44
- const BASH_CONTROL_OPERATOR_RE = /[;&|<>`\r\n]|\$\(/;
45
- // Best-effort, defense-in-depth bash mutation detection. The authoritative
46
- // planning-phase guard is the dedicated `write`/`edit`/`ast_edit` tools (fully
47
- // pathed); this catches the common shell mutators plus all redirect targets so a
48
- // cooperative agent cannot trivially side-step those tools. It is deliberately
49
- // NOT exhaustive: arbitrary interpreters (`python -c`, `node -e`) and the
50
- // `key=value` operand forms of utilities like `dd of=` are not parsed, and path
51
- // classification is lexical (no realpath), matching the rest of this guard and
52
- // the broader `.gjc` path handling. Hardening any of these would require a real
53
- // shell parser / symlink resolution and is out of scope for the planning rails.
54
- const BASH_MUTATION_COMMANDS = new Set(["rm", "mv", "cp", "touch", "mkdir", "ln", "tee"]);
55
40
 
56
41
  type ToolWithEditMode = AgentTool & {
57
42
  mode?: unknown;
@@ -93,15 +78,18 @@ function safeString(value: unknown): string {
93
78
  return typeof value === "string" ? value : "";
94
79
  }
95
80
 
96
- function encodePathSegment(value: string): string {
97
- return encodeURIComponent(value).replaceAll(".", "%2E");
81
+ async function resolveBoundarySessionId(cwd: string, sessionId?: string): Promise<string | null> {
82
+ const normalizedSessionId = sessionId?.trim();
83
+ if (normalizedSessionId) return normalizedSessionId;
84
+ try {
85
+ return (await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID })).gjcSessionId;
86
+ } catch {
87
+ return null;
88
+ }
98
89
  }
99
90
 
100
- function modeStatePath(cwd: string, skill: string, sessionId?: string): string {
101
- const stateDir = path.join(cwd, ".gjc", "state");
102
- const fileName = `${skill}-state.json`;
103
- if (sessionId) return path.join(stateDir, "sessions", encodePathSegment(sessionId), fileName);
104
- return path.join(stateDir, fileName);
91
+ function modeStatePath(cwd: string, skill: string, sessionId: string): string {
92
+ return sessionModeStatePath(cwd, sessionId, skill);
105
93
  }
106
94
 
107
95
  function warnInvalidModeState(filePath: string, error: string): void {
@@ -129,12 +117,8 @@ async function readValidatedModeState(filePath: string): Promise<ModeState | nul
129
117
  }
130
118
  return state;
131
119
  }
132
- async function readVisibleModeState(cwd: string, skill: string, sessionId?: string): Promise<ModeState | null> {
133
- if (sessionId) {
134
- const sessionState = await readValidatedModeState(modeStatePath(cwd, skill, sessionId));
135
- if (sessionState) return sessionState;
136
- }
137
- return await readValidatedModeState(modeStatePath(cwd, skill));
120
+ async function readVisibleModeState(cwd: string, skill: string, sessionId: string): Promise<ModeState | null> {
121
+ return await readValidatedModeState(modeStatePath(cwd, skill, sessionId));
138
122
  }
139
123
 
140
124
  /**
@@ -228,16 +212,20 @@ async function getActivePlanningSkill(
228
212
  sessionId?: string,
229
213
  threadId?: string,
230
214
  ): Promise<ActivePlanningSkill | null> {
231
- const skillState = await readVisibleSkillActiveState(cwd, sessionId);
215
+ const resolvedSessionId = await resolveBoundarySessionId(cwd, sessionId);
216
+ if (!resolvedSessionId) return null;
217
+ const skillState = await readVisibleSkillActiveState(cwd, resolvedSessionId);
232
218
  if (!skillState) return null;
233
- const activeEntries = listActiveSkills(skillState).filter(entry => entryMatchesContext(entry, sessionId, threadId));
219
+ const activeEntries = listActiveSkills(skillState).filter(entry =>
220
+ entryMatchesContext(entry, resolvedSessionId, threadId),
221
+ );
234
222
  if (activeEntries.length === 0) return null;
235
223
  const current = resolveCurrentWorkflowEntry(activeEntries, safeString(skillState.skill).trim());
236
224
  if (!isPlanningSkill(current.skill)) return null;
237
- const modeState = await readVisibleModeState(cwd, current.skill, sessionId);
225
+ const modeState = await readVisibleModeState(cwd, current.skill, resolvedSessionId);
238
226
  if (!modeState) return null;
239
227
  if (modeState.active !== true) return null;
240
- if (!modeStateMatchesContext(modeState, sessionId, threadId)) return null;
228
+ if (!modeStateMatchesContext(modeState, resolvedSessionId, threadId)) return null;
241
229
  const phase = String(modeState.current_phase ?? current.phase ?? "").trim();
242
230
  if (!isBlockingPlanningPhase(current.skill, phase)) return null;
243
231
  return { skill: current.skill, phase };
@@ -342,81 +330,10 @@ function extractEditTargets(args: unknown, tool: ToolWithEditMode): ExtractedTar
342
330
  return targets;
343
331
  }
344
332
 
345
- function extractBashTargets(args: unknown): ExtractedTargets {
346
- const record = getRecord(args);
347
- const command = safeString(record?.command).trim();
348
- const targets: ExtractedTargets = { paths: [], unknown: false };
349
- if (!command) {
350
- targets.unknown = true;
351
- return targets;
352
- }
353
- // Fast path for a sanctioned `gjc …` invocation, but ONLY when it is a single
354
- // command with no shell control operators or redirects. Otherwise a compound
355
- // like `gjc … ; tee src/x` or `gjc … > .gjc/state/foo` would skip scanning and
356
- // bypass both the planning block and the always-on `.gjc/**` block, so fall
357
- // through to full token scanning (which leaves the `gjc` segment's own args
358
- // unextracted but still catches the trailing mutation/redirect).
359
- if (/^gjc(?:\s|$)/.test(command) && !BASH_CONTROL_OPERATOR_RE.test(command)) return targets;
360
-
361
- const tokens = command.match(BASH_TOKEN_RE)?.map(unquoteBashToken) ?? [];
362
- for (let index = 0; index < tokens.length; index++) {
363
- const token = tokens[index] ?? "";
364
- if (BASH_REDIRECT_RE.test(token)) {
365
- addPath(targets, tokens[index + 1]);
366
- index++;
367
- continue;
368
- }
369
- const redirectMatch = token.match(/^(?:\d*)>>?(.+)$/);
370
- if (redirectMatch?.[1]) {
371
- addPath(targets, redirectMatch[1]);
372
- continue;
373
- }
374
- // A heredoc delimiter (`<<EOF`) is a here-document word, NOT a filesystem
375
- // target. Consume it without recording a target so a legitimate
376
- // `cat <<EOF > /tmp/scratch.md` is judged solely by its redirect target.
377
- if (BASH_HEREDOC_RE.test(token)) {
378
- index++;
379
- continue;
380
- }
381
- if (/^(?:\d*)<<-?.+$/.test(token)) {
382
- continue;
383
- }
384
- if (isMutationBashCommand(tokens, index)) {
385
- for (let targetIndex = index + 1; targetIndex < tokens.length; targetIndex++) {
386
- const target = tokens[targetIndex] ?? "";
387
- if (isBashCommandBoundary(target)) break;
388
- if (target.startsWith("-")) continue;
389
- addPath(targets, target);
390
- }
391
- }
392
- }
393
- return targets;
394
- }
395
-
396
- function unquoteBashToken(token: string): string {
397
- if (token.length < 2) return token;
398
- const quote = token[0];
399
- if ((quote === "'" || quote === '"') && token.at(-1) === quote) return token.slice(1, -1);
400
- return token;
401
- }
402
-
403
- function isBashCommandBoundary(token: string): boolean {
404
- return [";", "&&", "||", "|"].includes(token);
405
- }
406
-
407
- function isMutationBashCommand(tokens: string[], index: number): boolean {
408
- const token = path.basename(tokens[index] ?? "");
409
- if (BASH_MUTATION_COMMANDS.has(token)) return true;
410
- if (token !== "sed") return false;
411
- const next = tokens[index + 1] ?? "";
412
- return next === "-i" || next.startsWith("-i") || next.includes("i");
413
- }
414
-
415
333
  function extractTargets(tool: ToolWithEditMode, args: unknown): ExtractedTargets {
416
334
  if (tool.name === "write") return extractWriteTargets(args);
417
335
  if (tool.name === "ast_edit") return extractAstEditTargets(args);
418
336
  if (tool.name === "edit") return extractEditTargets(args, tool);
419
- if (tool.name === "bash") return extractBashTargets(args);
420
337
  return { paths: [], unknown: true };
421
338
  }
422
339
 
@@ -460,8 +377,9 @@ function relativeGjcSegments(cwd: string, rawPath: string): string[] | null {
460
377
  function blockedWorkflowStateSkill(cwd: string, rawPath: string): CanonicalGjcWorkflowSkill | null {
461
378
  const segments = relativeGjcSegments(cwd, rawPath);
462
379
  if (segments?.[0] !== ".gjc") return null;
463
- if (segments[1] === "specs" || segments[1] === "plans") return null;
464
- if (segments[1] !== "state") return null;
380
+ const generatedRoot = segments[1]?.startsWith(GJC_SESSION_PREFIX) ? segments[2] : segments[1];
381
+ if (generatedRoot === "specs" || generatedRoot === "plans") return null;
382
+ if (generatedRoot !== "state") return null;
465
383
  const fileName = segments.at(-1) ?? "";
466
384
  for (const skillName of ["deep-interview", "ralplan", "ultragoal", "team"] as const) {
467
385
  if (fileName === workflowModeStateFileName(skillName)) return skillName;
@@ -481,7 +399,8 @@ function firstBlockedWorkflowStateSkill(cwd: string, targets: ExtractedTargets):
481
399
  function isAllowlistedPath(cwd: string, rawPath: string): boolean {
482
400
  const segments = relativeGjcSegments(cwd, rawPath);
483
401
  if (segments?.[0] !== ".gjc") return false;
484
- return segments[1] === "specs" || segments[1] === "plans";
402
+ const generatedRoot = segments[1]?.startsWith(GJC_SESSION_PREFIX) ? segments[2] : segments[1];
403
+ return generatedRoot === "specs" || generatedRoot === "plans";
485
404
  }
486
405
  function isBlockedGjcPath(cwd: string, rawPath: string): boolean {
487
406
  const segments = relativeGjcSegments(cwd, rawPath);
@@ -40,7 +40,7 @@ interface UltragoalHudState extends WorkflowGateHudState {
40
40
  currentGoal?: UltragoalLikeGoal;
41
41
  counts: Record<string, number>;
42
42
  goals: UltragoalLikeGoal[];
43
- latestLedgerEvent?: { event?: string; goalId?: string; timestamp?: string };
43
+ latestLedgerEvent?: { event?: string; goalId?: string; timestamp?: string; kind?: string; evidence?: string };
44
44
  updatedAt?: string;
45
45
  }
46
46
 
@@ -237,7 +237,9 @@ export function buildUltragoalHudSummary(state: UltragoalHudState): WorkflowHudS
237
237
  chip(
238
238
  "ledger",
239
239
  state.latestLedgerEvent?.event
240
- ? [state.latestLedgerEvent.event, state.latestLedgerEvent.goalId].filter(Boolean).join(":")
240
+ ? [state.latestLedgerEvent.event, state.latestLedgerEvent.kind, state.latestLedgerEvent.goalId]
241
+ .filter(Boolean)
242
+ .join(":")
241
243
  : undefined,
242
244
  35,
243
245
  ),
@@ -141,10 +141,10 @@ export function sanctionedWorkflowStateCommand(skill: CanonicalGjcWorkflowSkill)
141
141
  export function describeWorkflowStateContract(skill: CanonicalGjcWorkflowSkill): string[] {
142
142
  return [
143
143
  `Sanctioned mutation path: gjc state ${skill} read|write --input '<json>'`,
144
- `Canonical active HUD state: .gjc/state/${SKILL_ACTIVE_STATE_FILE} and .gjc/state/sessions/<session>/${SKILL_ACTIVE_STATE_FILE}`,
145
- `Skill mode state: .gjc/state/${workflowModeStateFileName(skill)} or .gjc/state/sessions/<session>/${workflowModeStateFileName(skill)}`,
144
+ `Canonical active HUD state: .gjc/_session-{sessionid}/state/${SKILL_ACTIVE_STATE_FILE}`,
145
+ `Skill mode state: .gjc/_session-{sessionid}/state/${workflowModeStateFileName(skill)}`,
146
146
  "Receipts include version, skill, owner, command, state_path, storage_path, mutated_at, fresh_until, status, and mutation_id.",
147
147
  "Receipts are fresh for 30 minutes; older receipts are stale and render as HUD warnings.",
148
- "Planning artifacts under .gjc/specs/** and .gjc/plans/** remain writable outside the state command.",
148
+ "Planning artifacts under .gjc/_session-{sessionid}/specs/** and .gjc/_session-{sessionid}/plans/** remain writable outside the state command.",
149
149
  ];
150
150
  }
@@ -797,7 +797,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
797
797
  return;
798
798
  }
799
799
 
800
- runtime.ctx.showProviderOnboarding();
800
+ runtime.ctx.showOAuthSelector("login", undefined, {
801
+ allowExternalCredentialDiscovery: true,
802
+ trigger: "bare-login",
803
+ });
801
804
  runtime.ctx.editor.setText("");
802
805
  },
803
806
  },