@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
@@ -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
  }
@@ -250,11 +250,15 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
250
250
  inlineHint: "[objective]",
251
251
  allowArgs: true,
252
252
  handleTui: async (command, runtime) => {
253
- const hadArgs = !!command.args;
254
- // Capture state BEFORE the call (see /plan above for rationale).
255
- const wasGoalModeEnabled = runtime.ctx.goalModeEnabled;
253
+ // The goal command always consumes the typed input: it either submits
254
+ // the bare objective (never the literal `/goal …` text the user typed)
255
+ // or shows a warning, so the normal submission path never records it in
256
+ // input history. Preserve the typed command whenever args were supplied
257
+ // — including the first-time `/goal set <objective>` case where goal
258
+ // mode was not yet active. A previous `wasGoalModeEnabled` guard dropped
259
+ // that first-time case from history (up/down-arrow recall).
256
260
  await runtime.ctx.handleGoalModeCommand(command.args || undefined);
257
- if (hadArgs && wasGoalModeEnabled) {
261
+ if (command.args) {
258
262
  runtime.ctx.editor.addToHistory(command.text);
259
263
  }
260
264
  runtime.ctx.editor.setText("");
@@ -253,15 +253,17 @@ export async function loadProjectContextFiles(
253
253
 
254
254
  const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
255
255
 
256
- // Convert ContextFile items and preserve depth info
257
- const files = result.items.map(item => {
258
- const contextFile = item as ContextFile;
259
- return {
260
- path: contextFile.path,
261
- content: contextFile.content,
262
- depth: contextFile.depth,
263
- };
264
- });
256
+ // Convert project-level ContextFile items and preserve depth info
257
+ const files = result.items
258
+ .filter(item => (item as ContextFile).level === "project")
259
+ .map(item => {
260
+ const contextFile = item as ContextFile;
261
+ return {
262
+ path: contextFile.path,
263
+ content: contextFile.content,
264
+ depth: contextFile.depth,
265
+ };
266
+ });
265
267
 
266
268
  // Sort by depth (descending): higher depth (farther from cwd) comes first,
267
269
  // so files closer to cwd appear later and are more prominent
@@ -3,20 +3,14 @@
3
3
  *
4
4
  * Agents are embedded at build time via Bun's import with { type: "text" }.
5
5
  */
6
- import { Effort } from "@gajae-code/ai";
7
6
  import { parseFrontmatter, prompt } from "@gajae-code/utils";
8
7
  import { parseAgentFields } from "../discovery/helpers";
8
+ // Embed agent markdown files at build time
9
9
  import architectMd from "../prompts/agents/architect.md" with { type: "text" };
10
10
  import criticMd from "../prompts/agents/critic.md" with { type: "text" };
11
11
  import executorMd from "../prompts/agents/executor.md" with { type: "text" };
12
- import exploreMd from "../prompts/agents/explore.md" with { type: "text" };
13
- // Embed agent markdown files at build time
14
12
  import agentFrontmatterTemplate from "../prompts/agents/frontmatter.md" with { type: "text" };
15
-
16
- import planMd from "../prompts/agents/plan.md" with { type: "text" };
17
13
  import plannerMd from "../prompts/agents/planner.md" with { type: "text" };
18
- import reviewerMd from "../prompts/agents/reviewer.md" with { type: "text" };
19
- import taskMd from "../prompts/agents/task.md" with { type: "text" };
20
14
 
21
15
  import type { AgentDefinition, AgentSource } from "./types";
22
16
 
@@ -50,21 +44,6 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
50
44
  { fileName: "architect.md", template: architectMd },
51
45
  { fileName: "planner.md", template: plannerMd },
52
46
  { fileName: "critic.md", template: criticMd },
53
- { fileName: "explore.md", template: exploreMd },
54
- { fileName: "plan.md", template: planMd },
55
- { fileName: "reviewer.md", template: reviewerMd },
56
- {
57
- fileName: "task.md",
58
- frontmatter: {
59
- name: "task",
60
- description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
61
- spawns: "*",
62
- model: "pi/default",
63
- thinkingLevel: Effort.Medium,
64
- hide: true,
65
- },
66
- template: taskMd,
67
- },
68
47
  ];
69
48
 
70
49
  // Computed lazily on first loadBundledAgents() call to avoid eager prompt.render at module load.
package/src/task/index.ts CHANGED
@@ -55,7 +55,7 @@ import { assertNoRawTaskFields, buildTaskReceipt, buildTaskRoiSummary } from "./
55
55
  import { renderResult, renderCall as renderTaskCall } from "./render";
56
56
  import { reconcileSpawnRoi } from "./roi-reconciliation";
57
57
  import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
58
- import { DEFAULT_SPAWN_THRESHOLD, evaluateReviewerExploreGate, evaluateSpawnGate } from "./spawn-gate";
58
+ import { DEFAULT_SPAWN_THRESHOLD, evaluateSpawnGate } from "./spawn-gate";
59
59
  import {
60
60
  applyNestedPatches,
61
61
  captureBaseline,
@@ -359,7 +359,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
359
359
  readonly renderResult = renderResult;
360
360
  readonly #discoveredAgents: AgentDefinition[];
361
361
  readonly #blockedAgent: string | undefined;
362
- readonly #spawningAgentType: string | undefined;
363
362
 
364
363
  get parameters(): TaskToolSchemaInstance {
365
364
  const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
@@ -391,7 +390,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
391
390
  discoveredAgents: AgentDefinition[],
392
391
  ) {
393
392
  this.#blockedAgent = $env.PI_BLOCKED_AGENT;
394
- this.#spawningAgentType = session.currentAgentType;
395
393
  this.#discoveredAgents = discoveredAgents;
396
394
  }
397
395
 
@@ -478,23 +476,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
478
476
  };
479
477
  }
480
478
 
481
- const reviewerExploreDecision = evaluateReviewerExploreGate({
482
- spawningAgentType: this.#spawningAgentType,
483
- targetAgent: params.agent,
484
- plan: params.spawnPlan,
485
- });
486
- if (reviewerExploreDecision.outcome === "rejected") {
487
- return {
488
- content: [
489
- {
490
- type: "text",
491
- text: `Task spawn gate rejected reviewer->explore: ${reviewerExploreDecision.reason}. Provide spawnPlan fields: ${reviewerExploreDecision.missingFields.join(", ")}.`,
492
- },
493
- ],
494
- details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
495
- };
496
- }
497
-
498
479
  const manager = AsyncJobManager.instance();
499
480
  if (!manager) {
500
481
  return {
@@ -1083,27 +1064,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1083
1064
  };
1084
1065
  }
1085
1066
 
1086
- const reviewerExploreDecision = evaluateReviewerExploreGate({
1087
- spawningAgentType: this.#spawningAgentType,
1088
- targetAgent: agentName,
1089
- plan: params.spawnPlan,
1090
- });
1091
- if (reviewerExploreDecision.outcome === "rejected") {
1092
- return {
1093
- content: [
1094
- {
1095
- type: "text",
1096
- text: `Task spawn gate rejected reviewer->explore: ${reviewerExploreDecision.reason}. Provide spawnPlan fields: ${reviewerExploreDecision.missingFields.join(", ")}.`,
1097
- },
1098
- ],
1099
- details: {
1100
- projectAgentsDir,
1101
- results: [],
1102
- totalDurationMs: Date.now() - startTime,
1103
- },
1104
- };
1105
- }
1106
-
1107
1067
  let repoRoot: string | null = null;
1108
1068
  let baseline: WorktreeBaseline | null = null;
1109
1069
  if (isIsolated) {
@@ -1,7 +1,7 @@
1
1
  /** The hard, locked batch threshold enforced by the runtime gate. */
2
2
  export const DEFAULT_SPAWN_THRESHOLD = 4;
3
3
 
4
- /** The justification a large batch or reviewer-spawned explorer must supply to pass the hard gate. */
4
+ /** The justification a large batch must supply to pass the hard gate. */
5
5
  export interface SpawnPlanReceipt {
6
6
  whyParallel: string;
7
7
  whyNotLocal: string;
@@ -17,15 +17,6 @@ export interface SpawnGateRequest {
17
17
  plan?: SpawnPlanReceipt;
18
18
  }
19
19
 
20
- export interface ReviewerExploreGateRequest {
21
- /** Agent type/name doing the spawning, when known. */
22
- spawningAgentType?: string | null;
23
- /** Target agent type/name requested by the task call. */
24
- targetAgent: string;
25
- /** The spawn-plan receipt, when provided. */
26
- plan?: SpawnPlanReceipt;
27
- }
28
-
29
20
  export type SpawnGateOutcome = "allowed" | "rejected";
30
21
 
31
22
  export interface SpawnGateDecision {
@@ -102,31 +93,3 @@ export function decide(childCount: number, threshold: number, plan: SpawnPlanRec
102
93
  export function evaluateSpawnGate(request: SpawnGateRequest): SpawnGateDecision {
103
94
  return decide(request.childCount, DEFAULT_SPAWN_THRESHOLD, request.plan);
104
95
  }
105
-
106
- export function evaluateReviewerExploreGate(request: ReviewerExploreGateRequest): SpawnGateDecision {
107
- if (request.spawningAgentType !== "reviewer" || request.targetAgent !== "explore") {
108
- return {
109
- outcome: "allowed",
110
- reason: "reviewer->explore gate does not apply",
111
- planRequired: false,
112
- missingFields: [],
113
- };
114
- }
115
-
116
- const missingFields = findMissingPlanFields(request.plan);
117
- if (missingFields.length > 0) {
118
- return {
119
- outcome: "rejected",
120
- reason: `reviewer->explore spawn requires a complete spawn-plan receipt (${missingFields.join(", ")})`,
121
- planRequired: true,
122
- missingFields,
123
- };
124
- }
125
-
126
- return {
127
- outcome: "allowed",
128
- reason: "reviewer->explore spawn has a complete spawn-plan receipt",
129
- planRequired: true,
130
- missingFields: [],
131
- };
132
- }
package/src/task/types.ts CHANGED
@@ -72,7 +72,7 @@ const spawnPlanSchema = z
72
72
  expectedReceiptShape: z.string(),
73
73
  maxInlineTokens: z.number(),
74
74
  })
75
- .describe("justification required before spawning more than four tasks or reviewer-spawned explore tasks");
75
+ .describe("justification required before spawning more than four tasks");
76
76
 
77
77
  const createTaskItemSchema = (_contextEnabled: boolean) =>
78
78
  z.object({
package/src/tools/ask.ts CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  renderDeepInterviewAskQuestion,
35
35
  } from "../deep-interview/render-middleware";
36
36
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
37
- import { appendOrMergeDeepInterviewRound } from "../gjc-runtime/deep-interview-recorder";
37
+ import { appendOrMergeDeepInterviewRound, syncDeepInterviewRecorderHud } from "../gjc-runtime/deep-interview-recorder";
38
38
  import { deepInterviewStatePath } from "../gjc-runtime/deep-interview-runtime";
39
39
  import { gateAnswerToResult, questionToGate } from "../modes/shared/agent-wire/deep-interview-gate";
40
40
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
@@ -104,6 +104,30 @@ export interface AskToolDetails {
104
104
  const OTHER_OPTION = "Other (type your own)";
105
105
  const RECOMMENDED_SUFFIX = " (Recommended)";
106
106
  const DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS = Number.MAX_SAFE_INTEGER;
107
+ const DEEP_INTERVIEW_RECORDER_AWAIT_TIMEOUT_MS = 250;
108
+
109
+ function errorMessage(error: unknown): string {
110
+ return error instanceof Error ? error.message : String(error);
111
+ }
112
+
113
+ async function awaitDeepInterviewRecorderPersistence(persistence: Promise<void>): Promise<void> {
114
+ let timeout: ReturnType<typeof setTimeout> | undefined;
115
+ try {
116
+ await Promise.race([
117
+ persistence,
118
+ new Promise<never>((_resolve, reject) => {
119
+ timeout = setTimeout(
120
+ () => reject(new Error(`timed out after ${DEEP_INTERVIEW_RECORDER_AWAIT_TIMEOUT_MS}ms`)),
121
+ DEEP_INTERVIEW_RECORDER_AWAIT_TIMEOUT_MS,
122
+ );
123
+ }),
124
+ ]);
125
+ } catch (error) {
126
+ logger.warn(`ask: deep-interview round recording failed: ${errorMessage(error)}`);
127
+ } finally {
128
+ if (timeout) clearTimeout(timeout);
129
+ }
130
+ }
107
131
 
108
132
  function getDoneOptionLabel(): string {
109
133
  return `${theme.status.success} Done selecting`;
@@ -481,11 +505,11 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
481
505
  ): Promise<void> {
482
506
  const meta = q.deepInterview;
483
507
  if (!meta) return;
484
- try {
485
- const cwd = this.session.cwd;
486
- const sessionId = this.session.getSessionId?.() ?? undefined;
487
- const statePath = deepInterviewStatePath(cwd, sessionId);
488
- await appendOrMergeDeepInterviewRound(
508
+ const cwd = this.session.cwd;
509
+ const sessionId = this.session.getSessionId?.() ?? undefined;
510
+ const statePath = deepInterviewStatePath(cwd, sessionId);
511
+ await awaitDeepInterviewRecorderPersistence(
512
+ appendOrMergeDeepInterviewRound(
489
513
  cwd,
490
514
  statePath,
491
515
  {
@@ -500,12 +524,10 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
500
524
  customInput,
501
525
  },
502
526
  { sessionId },
503
- );
504
- } catch (error) {
505
- logger.warn(
506
- `ask: deep-interview round recording failed: ${error instanceof Error ? error.message : String(error)}`,
507
- );
508
- }
527
+ ).then(async () => {
528
+ await syncDeepInterviewRecorderHud(cwd, statePath, sessionId);
529
+ }),
530
+ );
509
531
  }
510
532
 
511
533
  async execute(