@gajae-code/coding-agent 0.8.2 → 0.9.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 (179) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/types/append-only-mode.d.ts +31 -0
  3. package/dist/types/capability/rule.d.ts +8 -0
  4. package/dist/types/cli/fixture-report.d.ts +20 -0
  5. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  6. package/dist/types/cli/nofile-limit.d.ts +11 -0
  7. package/dist/types/cli/update-cli.d.ts +22 -0
  8. package/dist/types/config/settings-schema.d.ts +105 -1
  9. package/dist/types/discovery/index.d.ts +1 -0
  10. package/dist/types/export/html/template.generated.d.ts +1 -1
  11. package/dist/types/export/ttsr.d.ts +14 -3
  12. package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
  13. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
  14. package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
  15. package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
  16. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  17. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
  18. package/dist/types/goals/runtime.d.ts +1 -0
  19. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  20. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  21. package/dist/types/modes/components/status-line.d.ts +9 -0
  22. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  23. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  24. package/dist/types/modes/interactive-mode.d.ts +2 -0
  25. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  26. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  27. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  28. package/dist/types/modes/types.d.ts +1 -0
  29. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  30. package/dist/types/notifications/config.d.ts +6 -0
  31. package/dist/types/notifications/telegram-daemon.d.ts +8 -0
  32. package/dist/types/notifications/threaded-render.d.ts +6 -0
  33. package/dist/types/sdk.d.ts +3 -0
  34. package/dist/types/session/agent-session.d.ts +43 -1
  35. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  36. package/dist/types/session/messages.d.ts +4 -0
  37. package/dist/types/session/session-manager.d.ts +17 -1
  38. package/dist/types/skill-state/active-state.d.ts +18 -1
  39. package/dist/types/system-prompt.d.ts +18 -2
  40. package/dist/types/task/executor.d.ts +1 -0
  41. package/dist/types/task/fork-context-budget.d.ts +7 -0
  42. package/dist/types/task/token-log.d.ts +30 -0
  43. package/dist/types/task/token-log.test.d.ts +1 -0
  44. package/dist/types/tools/bisect.d.ts +81 -0
  45. package/dist/types/tools/index.d.ts +1 -0
  46. package/dist/types/tools/search.d.ts +2 -2
  47. package/dist/types/utils/file-mentions.d.ts +14 -0
  48. package/dist/types/utils/git.d.ts +32 -0
  49. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  50. package/package.json +7 -7
  51. package/src/append-only-mode.ts +58 -0
  52. package/src/autoresearch/prompt.md +1 -1
  53. package/src/autoresearch/tools/log-experiment.ts +1 -1
  54. package/src/capability/rule.ts +9 -0
  55. package/src/cli/config-cli.ts +27 -3
  56. package/src/cli/fixture-report.test.ts +230 -0
  57. package/src/cli/fixture-report.ts +171 -0
  58. package/src/cli/nofile-limit.ts +48 -0
  59. package/src/cli/update-cli.ts +85 -9
  60. package/src/cli.ts +26 -0
  61. package/src/commands/contribution-prep.ts +1 -1
  62. package/src/config/settings-schema.ts +68 -1
  63. package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
  64. package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
  65. package/src/defaults/gjc/skills/team/SKILL.md +16 -13
  66. package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
  67. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  68. package/src/defaults/gjc-defaults.ts +9 -0
  69. package/src/discovery/helpers.ts +10 -0
  70. package/src/discovery/index.ts +1 -0
  71. package/src/export/html/template.generated.ts +1 -1
  72. package/src/export/html/template.js +60 -2
  73. package/src/export/ttsr.ts +96 -18
  74. package/src/extensibility/extensions/runner.ts +181 -219
  75. package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
  76. package/src/gjc-runtime/deep-interview-state.ts +31 -2
  77. package/src/gjc-runtime/session-state-sidecar.ts +208 -13
  78. package/src/gjc-runtime/state-writer.ts +15 -0
  79. package/src/gjc-runtime/team-runtime.ts +164 -15
  80. package/src/gjc-runtime/ultragoal-guard.ts +55 -66
  81. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
  82. package/src/gjc-runtime/ultragoal-runtime.ts +1593 -143
  83. package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
  84. package/src/gjc-runtime/workflow-manifest.ts +74 -1
  85. package/src/goals/runtime.ts +10 -2
  86. package/src/hindsight/backend.ts +1 -3
  87. package/src/hooks/native-skill-hook.ts +95 -4
  88. package/src/internal-urls/docs-index.generated.ts +14 -5
  89. package/src/main.ts +43 -2
  90. package/src/memories/index.ts +12 -1
  91. package/src/modes/components/assistant-message.ts +241 -82
  92. package/src/modes/components/custom-editor.ts +1 -0
  93. package/src/modes/components/status-line.ts +167 -54
  94. package/src/modes/components/tool-execution.ts +44 -19
  95. package/src/modes/controllers/command-controller.ts +39 -2
  96. package/src/modes/controllers/event-controller.ts +66 -47
  97. package/src/modes/controllers/input-controller.ts +1 -1
  98. package/src/modes/interactive-mode.ts +9 -1
  99. package/src/modes/rpc/rpc-mode.ts +29 -3
  100. package/src/modes/rpc/rpc-types.ts +9 -0
  101. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  102. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  103. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  104. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  105. package/src/modes/types.ts +1 -0
  106. package/src/modes/utils/ui-helpers.ts +17 -4
  107. package/src/notifications/config.ts +16 -4
  108. package/src/notifications/index.ts +78 -5
  109. package/src/notifications/telegram-daemon-cli.ts +2 -2
  110. package/src/notifications/telegram-daemon-control.ts +2 -2
  111. package/src/notifications/telegram-daemon.ts +85 -8
  112. package/src/notifications/threaded-render.ts +17 -1
  113. package/src/prompts/agents/architect.md +19 -10
  114. package/src/prompts/agents/critic.md +16 -18
  115. package/src/prompts/agents/executor.md +14 -11
  116. package/src/prompts/agents/planner.md +18 -13
  117. package/src/prompts/ci-green-request.md +3 -8
  118. package/src/prompts/goals/goal-continuation.md +0 -5
  119. package/src/prompts/goals/goal-mode-active.md +0 -5
  120. package/src/prompts/system/agent-creation-architect.md +2 -2
  121. package/src/prompts/system/plan-mode-active.md +1 -1
  122. package/src/prompts/system/plan-mode-subagent.md +3 -3
  123. package/src/prompts/system/project-prompt.md +0 -13
  124. package/src/prompts/system/subagent-system-prompt.md +3 -2
  125. package/src/prompts/system/system-prompt.md +26 -19
  126. package/src/prompts/system/volatile-project-context.md +10 -0
  127. package/src/prompts/system/web-search.md +4 -7
  128. package/src/prompts/tools/apply-patch.md +6 -0
  129. package/src/prompts/tools/ast-edit.md +1 -0
  130. package/src/prompts/tools/bash.md +1 -1
  131. package/src/prompts/tools/bisect.md +22 -0
  132. package/src/prompts/tools/browser.md +1 -1
  133. package/src/prompts/tools/cron.md +0 -2
  134. package/src/prompts/tools/eval.md +2 -2
  135. package/src/prompts/tools/find.md +1 -1
  136. package/src/prompts/tools/github.md +6 -6
  137. package/src/prompts/tools/goal.md +1 -3
  138. package/src/prompts/tools/image-gen.md +12 -5
  139. package/src/prompts/tools/irc.md +5 -4
  140. package/src/prompts/tools/job.md +2 -2
  141. package/src/prompts/tools/lsp.md +1 -1
  142. package/src/prompts/tools/monitor.md +2 -2
  143. package/src/prompts/tools/patch.md +2 -2
  144. package/src/prompts/tools/replace.md +2 -21
  145. package/src/prompts/tools/rewind.md +1 -1
  146. package/src/prompts/tools/search-tool-bm25.md +1 -11
  147. package/src/prompts/tools/search.md +1 -1
  148. package/src/prompts/tools/skill.md +3 -2
  149. package/src/prompts/tools/ssh.md +2 -1
  150. package/src/prompts/tools/subagent.md +2 -2
  151. package/src/prompts/tools/task.md +4 -5
  152. package/src/prompts/tools/todo-write.md +1 -1
  153. package/src/prompts/tools/vim.md +1 -0
  154. package/src/prompts/tools/web-search.md +2 -0
  155. package/src/prompts/tools/write.md +8 -2
  156. package/src/sdk.ts +32 -22
  157. package/src/session/agent-session.ts +732 -107
  158. package/src/session/file-mention-pruning.ts +64 -0
  159. package/src/session/messages.ts +4 -0
  160. package/src/session/session-manager.ts +59 -2
  161. package/src/skill-state/active-state.ts +141 -5
  162. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  163. package/src/slash-commands/builtin-registry.ts +65 -0
  164. package/src/system-prompt.ts +57 -16
  165. package/src/task/executor.ts +125 -20
  166. package/src/task/fork-context-advisory.ts +2 -7
  167. package/src/task/fork-context-budget.ts +9 -0
  168. package/src/task/index.ts +4 -3
  169. package/src/task/output-manager.ts +4 -2
  170. package/src/task/token-log.test.ts +188 -0
  171. package/src/task/token-log.ts +179 -0
  172. package/src/tools/bisect.ts +367 -0
  173. package/src/tools/index.ts +3 -0
  174. package/src/tools/output-meta.ts +116 -4
  175. package/src/tools/search.ts +8 -2
  176. package/src/tools/skill.ts +15 -0
  177. package/src/utils/file-mentions.ts +39 -7
  178. package/src/utils/git.ts +73 -0
  179. package/src/web/scrapers/youtube.ts +24 -12
@@ -14,6 +14,7 @@ import { loadSkills, type Skill } from "./extensibility/skills";
14
14
  import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
15
15
  import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
16
16
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
17
+ import volatileProjectContextTemplate from "./prompts/system/volatile-project-context.md" with { type: "text" };
17
18
  import { shortenPath } from "./tools/render-utils";
18
19
  import { AGENTS_MD_LIMIT, buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
19
20
 
@@ -242,9 +243,12 @@ function dedupeExactContextFiles(
242
243
  }
243
244
 
244
245
  /**
245
- * Load all project context files using the capability API.
246
+ * Load all context files using the capability API.
246
247
  * Returns {path, content, depth} entries for all discovered context files.
247
- * Files are sorted by depth (descending) so files closer to cwd appear last/more prominent.
248
+ * Native user-global files (`~/.gjc/agent/AGENTS.md`) come first, then project
249
+ * files sorted by depth (descending) so files closer to cwd appear last/more
250
+ * prominent. User-home files from foreign providers (`~/.claude/CLAUDE.md`,
251
+ * `~/.codex/AGENTS.md`, …) stay excluded — only gjc's own user config applies.
248
252
  */
249
253
  export async function loadProjectContextFiles(
250
254
  options: LoadContextFilesOptions = {},
@@ -252,28 +256,32 @@ export async function loadProjectContextFiles(
252
256
  const resolvedCwd = options.cwd ?? getProjectDir();
253
257
 
254
258
  const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
259
+ const items = result.items as ContextFile[];
260
+
261
+ // Native user-global context applies everywhere and is least specific, so it
262
+ // renders first — project files rendered later take precedence over it.
263
+ const userFiles = items
264
+ .filter(item => item.level === "user" && item._source.provider === "native")
265
+ .map(item => ({ path: item.path, content: item.content }));
255
266
 
256
267
  // 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
- });
268
+ const projectFiles = items
269
+ .filter(item => item.level === "project")
270
+ .map(item => ({
271
+ path: item.path,
272
+ content: item.content,
273
+ depth: item.depth,
274
+ }));
267
275
 
268
276
  // Sort by depth (descending): higher depth (farther from cwd) comes first,
269
277
  // so files closer to cwd appear later and are more prominent
270
- files.sort((a, b) => {
278
+ projectFiles.sort((a, b) => {
271
279
  const depthA = a.depth ?? -1;
272
280
  const depthB = b.depth ?? -1;
273
281
  return depthB - depthA;
274
282
  });
275
283
 
276
- return dedupeExactContextFiles(files);
284
+ return dedupeExactContextFiles([...userFiles, ...projectFiles]);
277
285
  }
278
286
 
279
287
  /**
@@ -368,6 +376,13 @@ export interface BuildSystemPromptOptions {
368
376
  secretsEnabled?: boolean;
369
377
  /** Pre-loaded workspace tree (skips discovery if provided). May be a Promise to allow early kick-off. */
370
378
  workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
379
+ /**
380
+ * Render a trimmed role-agent base prompt for subagent sessions: omits the
381
+ * workflow-surface/routing/self-awareness (`<gjc-runtime>`) and `<soul>` blocks
382
+ * that only apply to the top-level interactive/print agent. Tool safety, repo
383
+ * safety, and the completion contract are retained. Default: false.
384
+ */
385
+ subagent?: boolean;
371
386
  }
372
387
 
373
388
  /** Result of building provider-facing system prompt messages. */
@@ -375,6 +390,29 @@ export interface BuildSystemPromptResult {
375
390
  /** Ordered system prompt blocks. Providers should preserve entries as distinct messages/blocks. */
376
391
  systemPrompt: string[];
377
392
  }
393
+ export interface BuildVolatileProjectContextOptions {
394
+ cwd?: string;
395
+ date?: string;
396
+ workspaceTree?: WorkspaceTree;
397
+ }
398
+
399
+ export function buildVolatileProjectContext(options: BuildVolatileProjectContextOptions = {}): string {
400
+ const resolvedCwd = options.cwd ?? getProjectDir();
401
+ const date = options.date ?? new Date().toISOString().slice(0, 10);
402
+ return prompt
403
+ .render(volatileProjectContextTemplate, {
404
+ date,
405
+ cwd: shortenPath(resolvedCwd.replace(/\\/g, "/")),
406
+ workspaceTree: options.workspaceTree ?? {
407
+ rootPath: resolvedCwd,
408
+ rendered: "",
409
+ truncated: false,
410
+ totalLines: 0,
411
+ agentsMdFiles: [],
412
+ },
413
+ })
414
+ .trim();
415
+ }
378
416
 
379
417
  /** Build the system prompt with tools, guidelines, and context */
380
418
  export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<BuildSystemPromptResult> {
@@ -403,6 +441,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
403
441
  eagerTasks = false,
404
442
  secretsEnabled = false,
405
443
  workspaceTree: providedWorkspaceTree,
444
+ subagent = false,
406
445
  } = options;
407
446
  const resolvedCwd = cwd ?? getProjectDir();
408
447
 
@@ -534,6 +573,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
534
573
  // Build tool descriptions for system prompt rendering.
535
574
  const toolPromptNames = new Map<string, string>(toolNames.map(name => [name, tools?.get(name)?.wireName ?? name]));
536
575
  const toolRefs = Object.fromEntries(toolPromptNames.entries());
576
+ const hasHiddenToolDiscoveryTool = Object.hasOwn(toolRefs, "search_tool_bm25");
537
577
  const toolInfo = toolNames.map(name => ({
538
578
  name: toolPromptNames.get(name) ?? name,
539
579
  internalName: name,
@@ -575,13 +615,14 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
575
615
  cwd: promptCwd,
576
616
  intentTracing: !!intentField,
577
617
  intentField: intentField ?? "",
578
- mcpDiscoveryMode,
618
+ mcpDiscoveryMode: mcpDiscoveryMode && hasHiddenToolDiscoveryTool,
579
619
  hasMCPDiscoveryServers: mcpDiscoveryServerSummaries.length > 0,
580
620
  mcpDiscoveryServerSummaries,
581
- toolDiscoveryActive,
621
+ toolDiscoveryActive: toolDiscoveryActive && hasHiddenToolDiscoveryTool,
582
622
  discoverableTools,
583
623
  eagerTasks,
584
624
  secretsEnabled,
625
+ subagent,
585
626
  };
586
627
  const rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
587
628
  const systemPrompt = [rendered];
@@ -22,6 +22,7 @@ import { runExtensionCompact, runExtensionSetModel } from "../extensibility/exte
22
22
  import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
23
23
  import { buildAgentSubskillInjection, renderAgentPromptAdditions } from "../extensibility/gjc-plugins";
24
24
  import { buildSkillPromptMessage, type Skill } from "../extensibility/skills";
25
+ import { sessionRoot } from "../gjc-runtime/session-layout";
25
26
  import type { HindsightSessionState } from "../hindsight/state";
26
27
  import type { LocalProtocolOptions } from "../internal-urls";
27
28
  import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
@@ -42,6 +43,7 @@ import type { EventBus } from "../utils/event-bus";
42
43
  import { buildNamedToolChoiceResult } from "../utils/tool-choice";
43
44
  import type { WorkspaceTree } from "../workspace-tree";
44
45
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
46
+ import { persistTaskTokenLog, taskTokenLogFromUsage } from "./token-log";
45
47
  import {
46
48
  type AgentDefinition,
47
49
  type AgentProgress,
@@ -245,6 +247,51 @@ function previewOffendingData(value: unknown, maxLength = 500): string {
245
247
  }
246
248
  return serialized.length > maxLength ? `${serialized.slice(0, maxLength)}…` : serialized;
247
249
  }
250
+ const PLACEHOLDER_YIELD_PATTERNS = [
251
+ /^see (?:the )?message body(?:\b|[\s—:.,-])/i,
252
+ /^(?:complete\s+\w+\s+)?returned inline(?:\b|[\s—:.,-])/i,
253
+ /^leader persists(?:\b|[\s—:.,-])/i,
254
+ /^caller persists(?:\b|[\s—:.,-])/i,
255
+ ];
256
+
257
+ const PLACEHOLDER_YIELD_FIELD_NAMES = new Set([
258
+ "artifactmarkdown",
259
+ "finalmarkdown",
260
+ "fullplan",
261
+ "markdown",
262
+ "planmarkdown",
263
+ ]);
264
+
265
+ function looksLikePlaceholderYieldString(value: string): boolean {
266
+ const trimmed = value.trim();
267
+ if (trimmed.length === 0 || trimmed.length > 500) return false;
268
+ return PLACEHOLDER_YIELD_PATTERNS.some(pattern => pattern.test(trimmed));
269
+ }
270
+
271
+ function normalizePlaceholderYieldFieldName(key: string): string {
272
+ return key.replace(/[^a-z0-9]/gi, "").toLowerCase();
273
+ }
274
+
275
+ function findPlaceholderYieldPath(value: unknown, path = "$", depth = 0, inspectStrings = true): string | undefined {
276
+ if (typeof value === "string") {
277
+ return inspectStrings && looksLikePlaceholderYieldString(value) ? path : undefined;
278
+ }
279
+ if (!value || typeof value !== "object" || depth > 4) return undefined;
280
+ if (Array.isArray(value)) {
281
+ for (let i = 0; i < value.length; i++) {
282
+ const found = findPlaceholderYieldPath(value[i], `${path}[${i}]`, depth + 1, false);
283
+ if (found) return found;
284
+ }
285
+ return undefined;
286
+ }
287
+ const record = value as Record<string, unknown>;
288
+ for (const [key, item] of Object.entries(record)) {
289
+ const shouldInspectString = PLACEHOLDER_YIELD_FIELD_NAMES.has(normalizePlaceholderYieldFieldName(key));
290
+ const found = findPlaceholderYieldPath(item, `${path}.${key}`, depth + 1, shouldInspectString);
291
+ if (found) return found;
292
+ }
293
+ return undefined;
294
+ }
248
295
 
249
296
  function tryParseJsonOutput(text: string): unknown | undefined {
250
297
  const trimmed = text.trim();
@@ -321,6 +368,8 @@ interface FinalizeSubprocessOutputResult {
321
368
  export const SUBAGENT_WARNING_NULL_YIELD = "SYSTEM WARNING: Subagent called yield with null data.";
322
369
  export const SUBAGENT_WARNING_MISSING_YIELD =
323
370
  "SYSTEM WARNING: Subagent exited without calling yield tool after 3 reminders.";
371
+ export const SUBAGENT_WARNING_PLACEHOLDER_YIELD =
372
+ "SYSTEM WARNING: Subagent yield data contains a placeholder instead of the actual result.";
324
373
 
325
374
  /** Build a schema_violation outcome — surfaced as a non-zero exit so callers treat it as a failure. */
326
375
  function buildSchemaViolationOutcome(
@@ -347,6 +396,21 @@ function buildSchemaViolationOutcome(
347
396
  return { rawOutput, stderr: headline, exitCode: 1 };
348
397
  }
349
398
 
399
+ function buildPlaceholderYieldOutcome(
400
+ placeholderPath: string,
401
+ data: unknown,
402
+ ): { rawOutput: string; stderr: string; exitCode: number } {
403
+ return buildSchemaViolationOutcome(
404
+ {
405
+ message:
406
+ `${SUBAGENT_WARNING_PLACEHOLDER_YIELD} Offending path: ${placeholderPath}. ` +
407
+ "Return the real payload in yield.result.data or persist a durable artifact receipt.",
408
+ missingRequired: [],
409
+ },
410
+ data,
411
+ );
412
+ }
413
+
350
414
  export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult {
351
415
  let { rawOutput, exitCode, stderr } = args;
352
416
  const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
@@ -376,21 +440,29 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
376
440
  stderr = `schema_violation: invalid output schema: ${schemaError}`;
377
441
  exitCode = 1;
378
442
  } else {
379
- const verdict = validator ? validator.validate(completeData) : { ok: true as const };
380
- if (!verdict.ok) {
381
- const outcome = buildSchemaViolationOutcome(verdict, completeData);
443
+ const placeholderPath = findPlaceholderYieldPath(completeData);
444
+ if (placeholderPath) {
445
+ const outcome = buildPlaceholderYieldOutcome(placeholderPath, completeData);
382
446
  rawOutput = outcome.rawOutput;
383
447
  stderr = outcome.stderr;
384
448
  exitCode = outcome.exitCode;
385
449
  } else {
386
- try {
387
- rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
388
- } catch (err) {
389
- const errorMessage = err instanceof Error ? err.message : String(err);
390
- rawOutput = `{"error":"Failed to serialize yield data: ${errorMessage}"}`;
450
+ const verdict = validator ? validator.validate(completeData) : { ok: true as const };
451
+ if (!verdict.ok) {
452
+ const outcome = buildSchemaViolationOutcome(verdict, completeData);
453
+ rawOutput = outcome.rawOutput;
454
+ stderr = outcome.stderr;
455
+ exitCode = outcome.exitCode;
456
+ } else {
457
+ try {
458
+ rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
459
+ } catch (err) {
460
+ const errorMessage = err instanceof Error ? err.message : String(err);
461
+ rawOutput = `{"error":"Failed to serialize yield data: ${errorMessage}"}`;
462
+ }
463
+ exitCode = 0;
464
+ stderr = "";
391
465
  }
392
- exitCode = 0;
393
- stderr = "";
394
466
  }
395
467
  }
396
468
  }
@@ -403,21 +475,29 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
403
475
  if (fallback) {
404
476
  const completeData = normalizeCompleteData(fallback.data, reportFindings);
405
477
  const { validator } = buildOutputValidator(outputSchema);
406
- const verdict = validator ? validator.validate(completeData) : { ok: true as const };
407
- if (!verdict.ok) {
408
- const outcome = buildSchemaViolationOutcome(verdict, completeData);
478
+ const placeholderPath = findPlaceholderYieldPath(completeData);
479
+ if (placeholderPath) {
480
+ const outcome = buildPlaceholderYieldOutcome(placeholderPath, completeData);
409
481
  rawOutput = outcome.rawOutput;
410
482
  stderr = outcome.stderr;
411
483
  exitCode = outcome.exitCode;
412
484
  } else {
413
- try {
414
- rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
415
- } catch (err) {
416
- const errorMessage = err instanceof Error ? err.message : String(err);
417
- rawOutput = `{"error":"Failed to serialize fallback completion: ${errorMessage}"}`;
485
+ const verdict = validator ? validator.validate(completeData) : { ok: true as const };
486
+ if (!verdict.ok) {
487
+ const outcome = buildSchemaViolationOutcome(verdict, completeData);
488
+ rawOutput = outcome.rawOutput;
489
+ stderr = outcome.stderr;
490
+ exitCode = outcome.exitCode;
491
+ } else {
492
+ try {
493
+ rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
494
+ } catch (err) {
495
+ const errorMessage = err instanceof Error ? err.message : String(err);
496
+ rawOutput = `{"error":"Failed to serialize fallback completion: ${errorMessage}"}`;
497
+ }
498
+ exitCode = 0;
499
+ stderr = "";
418
500
  }
419
- exitCode = 0;
420
- stderr = "";
421
501
  }
422
502
  } else if (!hasOutputSchema && allowFallback && rawOutput.trim().length > 0) {
423
503
  exitCode = 0;
@@ -1209,6 +1289,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1209
1289
  const subagentAgentIdentity: AgentIdentity | undefined = options.parentTelemetry
1210
1290
  ? { id, name: agent.name, description: agent.description }
1211
1291
  : undefined;
1292
+ let subagentTokenTurn = 0;
1293
+ const tokenLogDir = options.parentSessionId
1294
+ ? path.join(sessionRoot(cwd, options.parentSessionId), "token-logs")
1295
+ : undefined;
1212
1296
  const subagentTelemetry: AgentTelemetryConfig | undefined =
1213
1297
  options.parentTelemetry && subagentAgentIdentity
1214
1298
  ? {
@@ -1217,6 +1301,27 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1217
1301
  // Clear parent's conversationId; the child loop falls back to
1218
1302
  // its own AgentLoopConfig.sessionId.
1219
1303
  conversationId: undefined,
1304
+ // Intentionally REPLACES (does not chain) the parent's onChatUsage:
1305
+ // the parent handler attributes turns to subagentId "root", so
1306
+ // chaining it here would double-log every subagent turn under root.
1307
+ // Subagent turns are attributed to this child's id instead.
1308
+ onChatUsage: async event => {
1309
+ if (!tokenLogDir) return;
1310
+ subagentTokenTurn += 1;
1311
+ await persistTaskTokenLog(
1312
+ taskTokenLogFromUsage(event.usage, {
1313
+ subagentId: id,
1314
+ agent: agent.name,
1315
+ // Monotonic 1-based sequence per subagent session
1316
+ // (event.stepNumber is 0-based and -1 for oneshots).
1317
+ turn: subagentTokenTurn,
1318
+ at: new Date().toISOString(),
1319
+ model: event.model,
1320
+ cost: event.cost,
1321
+ }),
1322
+ { dir: tokenLogDir },
1323
+ );
1324
+ },
1220
1325
  }
1221
1326
  : undefined;
1222
1327
 
@@ -1,3 +1,4 @@
1
+ import { FORK_CONTEXT_TOKEN_BUDGET_BY_MODE } from "./fork-context-budget";
1
2
  import type { ForkContextMode } from "./types";
2
3
 
3
4
  export interface ForkContextAdvisory {
@@ -12,13 +13,7 @@ export interface ForkContextAdvisory {
12
13
  * many parent-context tokens each mode may clone into the child; the actual
13
14
  * cloned amount can never exceed the parent context itself.
14
15
  */
15
- const CLONE_BUDGET_BY_MODE = {
16
- none: 0,
17
- receipt: 2000,
18
- "last-turn": 4000,
19
- bounded: 8000,
20
- full: 15000,
21
- } as const satisfies Record<ForkContextMode, number>;
16
+ const CLONE_BUDGET_BY_MODE = FORK_CONTEXT_TOKEN_BUDGET_BY_MODE;
22
17
 
23
18
  const RECEIPT_TRIGGERS = [
24
19
  { pattern: /as discussed/i, reason: "prior-session-reference:as-discussed" },
@@ -0,0 +1,9 @@
1
+ import type { ForkContextMode } from "./types";
2
+
3
+ export const FORK_CONTEXT_TOKEN_BUDGET_BY_MODE = {
4
+ none: 0,
5
+ receipt: 2000,
6
+ "last-turn": 4000,
7
+ bounded: 8000,
8
+ full: 15000,
9
+ } as const satisfies Record<ForkContextMode, number>;
package/src/task/index.ts CHANGED
@@ -48,6 +48,7 @@ import * as git from "../utils/git";
48
48
  import { discoverAgents, filterVisibleAgents, getAgent } from "./discovery";
49
49
  import { runSubprocess } from "./executor";
50
50
  import { adviseForkContextMode } from "./fork-context-advisory";
51
+ import { FORK_CONTEXT_TOKEN_BUDGET_BY_MODE } from "./fork-context-budget";
51
52
  import { getTaskIdValidationError, validateAllocatedTaskId } from "./id";
52
53
  import { AgentOutputManager } from "./output-manager";
53
54
  import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
@@ -303,11 +304,11 @@ function resolveForkSeedParamsForMode(
303
304
  case "none":
304
305
  return undefined;
305
306
  case "receipt":
306
- return { maxMessages: 1, maxTokens: 64 };
307
+ return { maxMessages: 1, maxTokens: FORK_CONTEXT_TOKEN_BUDGET_BY_MODE.receipt };
307
308
  case "last-turn":
308
- return { maxMessages: 2, maxTokens: 250 };
309
+ return { maxMessages: 2, maxTokens: FORK_CONTEXT_TOKEN_BUDGET_BY_MODE["last-turn"] };
309
310
  case "bounded":
310
- return { maxMessages: capMessages(50), maxTokens: 250 };
311
+ return { maxMessages: capMessages(50), maxTokens: FORK_CONTEXT_TOKEN_BUDGET_BY_MODE.bounded };
311
312
  case "full":
312
313
  return { maxMessages: capMessages(500), maxTokens: resolveForkContextMaxTokens(configuredMaxTokens, model) };
313
314
  default:
@@ -14,6 +14,8 @@ function escapeRegExp(value: string): string {
14
14
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15
15
  }
16
16
 
17
+ const RESERVED_OUTPUT_EXTENSIONS_PATTERN = "(?:md|jsonl|patch)";
18
+
17
19
  /**
18
20
  * Manages agent output ID allocation to ensure uniqueness.
19
21
  *
@@ -62,8 +64,8 @@ export class AgentOutputManager {
62
64
  }
63
65
 
64
66
  const pattern = this.#parentPrefix
65
- ? new RegExp(`^${escapeRegExp(this.#parentPrefix)}\\.(\\d+)-.*\\.md$`)
66
- : /^(\d+)-.*\.md$/;
67
+ ? new RegExp(`^${escapeRegExp(this.#parentPrefix)}\\.(\\d+)-.*\\.${RESERVED_OUTPUT_EXTENSIONS_PATTERN}$`)
68
+ : new RegExp(`^(\\d+)-.*\\.${RESERVED_OUTPUT_EXTENSIONS_PATTERN}$`);
67
69
 
68
70
  let maxId = -1;
69
71
  for (const file of files) {
@@ -0,0 +1,188 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { appendFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { GJC_SESSION_ACTIVITY_FILE, sessionRoot } from "../gjc-runtime/session-layout";
6
+ import {
7
+ computeCacheHitRate,
8
+ persistTaskTokenLog,
9
+ readTaskTokenLogs,
10
+ resolveTaskTokenLogDir,
11
+ taskTokenLogFromChat,
12
+ taskTokenLogFromUsage,
13
+ } from "./token-log";
14
+
15
+ async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
16
+ const dir = await mkdtemp(join(tmpdir(), "gjc-token-log-"));
17
+ try {
18
+ return await fn(dir);
19
+ } finally {
20
+ await rm(dir, { recursive: true, force: true });
21
+ }
22
+ }
23
+
24
+ describe("task token log", () => {
25
+ it("maps raw chat buckets without folding cache into input", () => {
26
+ const log = taskTokenLogFromChat(
27
+ {
28
+ inputTokens: 100,
29
+ outputTokens: 20,
30
+ cachedInputTokens: 30,
31
+ cacheWriteTokens: 5,
32
+ costUsd: 0.25,
33
+ },
34
+ {
35
+ subagentId: "root",
36
+ agent: "main",
37
+ turn: 1,
38
+ at: "2026-01-01T00:00:00.000Z",
39
+ model: "model-a",
40
+ },
41
+ );
42
+
43
+ expect(log.input).toBe(100);
44
+ expect(log.output).toBe(20);
45
+ expect(log.cacheRead).toBe(30);
46
+ expect(log.cacheWrite).toBe(5);
47
+ expect(log.totalTokens).toBe(155);
48
+ expect(log.cost).toBe(0.25);
49
+ expect(log.model).toBe("model-a");
50
+ });
51
+
52
+ it("maps zero-cache chat buckets without inventing cache tokens", () => {
53
+ const log = taskTokenLogFromChat(
54
+ { inputTokens: 7, outputTokens: 3, cachedInputTokens: 0, cacheWriteTokens: 0 },
55
+ { subagentId: "root", turn: 2, at: "2026-01-01T00:00:02.000Z" },
56
+ );
57
+
58
+ expect(log.input).toBe(7);
59
+ expect(log.output).toBe(3);
60
+ expect(log.cacheRead).toBe(0);
61
+ expect(log.cacheWrite).toBe(0);
62
+ expect(log.totalTokens).toBe(10);
63
+ });
64
+
65
+ it("keeps large cache-only reads out of input and hits the cache-rate upper boundary", () => {
66
+ const log = taskTokenLogFromChat(
67
+ { inputTokens: 0, outputTokens: 2, cachedInputTokens: 1_000_000_000, cacheWriteTokens: 0 },
68
+ { subagentId: "root", turn: 3, at: "2026-01-01T00:00:03.000Z" },
69
+ );
70
+
71
+ expect(log.input).toBe(0);
72
+ expect(log.output).toBe(2);
73
+ expect(log.cacheRead).toBe(1_000_000_000);
74
+ expect(log.cacheWrite).toBe(0);
75
+ expect(log.totalTokens).toBe(1_000_000_002);
76
+ expect(computeCacheHitRate(log.input, log.cacheRead)).toBe(1);
77
+ });
78
+
79
+ it("persists and reads a jsonl round trip", async () => {
80
+ await withTempDir(async dir => {
81
+ const first = taskTokenLogFromChat(
82
+ { inputTokens: 1, outputTokens: 2, cachedInputTokens: 3, cacheWriteTokens: 4 },
83
+ { subagentId: "root", turn: 1, at: "2026-01-01T00:00:00.000Z" },
84
+ );
85
+ const second = taskTokenLogFromChat(
86
+ { inputTokens: 5, outputTokens: 6, cachedInputTokens: 7, cacheWriteTokens: 8 },
87
+ { subagentId: "child", agent: "executor", turn: 1, at: "2026-01-01T00:00:01.000Z" },
88
+ );
89
+
90
+ await persistTaskTokenLog(first, { dir });
91
+ await persistTaskTokenLog(second, { dir });
92
+
93
+ expect(await readTaskTokenLogs(dir)).toEqual([first, second]);
94
+ });
95
+ });
96
+
97
+ it("prefers the current session manager over a stale latest-active session", async () => {
98
+ await withTempDir(async cwd => {
99
+ const currentSessionId = "current-session";
100
+ const staleSessionId = "stale-session";
101
+ for (const [sessionId, updatedAt] of [
102
+ [currentSessionId, "2026-01-01T00:00:00.000Z"],
103
+ [staleSessionId, "2026-01-01T00:00:10.000Z"],
104
+ ] as const) {
105
+ const markerPath = join(sessionRoot(cwd, sessionId), GJC_SESSION_ACTIVITY_FILE);
106
+ await mkdir(join(markerPath, ".."), { recursive: true });
107
+ await writeFile(
108
+ markerPath,
109
+ `${JSON.stringify({ session_id: sessionId, updated_at: updatedAt })}\n`,
110
+ "utf-8",
111
+ );
112
+ }
113
+
114
+ await expect(resolveTaskTokenLogDir(cwd, undefined, "")).resolves.toBe(
115
+ join(sessionRoot(cwd, staleSessionId), "token-logs"),
116
+ );
117
+ await expect(resolveTaskTokenLogDir(cwd, { getSessionId: () => currentSessionId }, "")).resolves.toBe(
118
+ join(sessionRoot(cwd, currentSessionId), "token-logs"),
119
+ );
120
+ });
121
+ });
122
+
123
+ it("missing token-log file reads as empty", async () => {
124
+ await withTempDir(async dir => {
125
+ expect(await readTaskTokenLogs(dir)).toEqual([]);
126
+ });
127
+ });
128
+
129
+ it("subtracts cache from the aggregate ChatUsageSnapshot input bucket", () => {
130
+ // buildUsageSnapshot sets inputTokens = rawInput + cacheRead + cacheWrite.
131
+ // taskTokenLogFromUsage must recover the raw (cache-excluded) input.
132
+ const log = taskTokenLogFromUsage(
133
+ {
134
+ inputTokens: 135,
135
+ outputTokens: 20,
136
+ totalTokens: 155,
137
+ cachedInputTokens: 30,
138
+ cacheWriteTokens: 5,
139
+ reasoningOutputTokens: 0,
140
+ },
141
+ { subagentId: "root", turn: 1, at: "2026-01-01T00:00:00.000Z", cost: { usd: 0.5 } },
142
+ );
143
+
144
+ expect(log.input).toBe(100);
145
+ expect(log.cacheRead).toBe(30);
146
+ expect(log.cacheWrite).toBe(5);
147
+ expect(log.output).toBe(20);
148
+ expect(log.totalTokens).toBe(155);
149
+ expect(log.cost).toBe(0.5);
150
+ });
151
+
152
+ it("clamps aggregate input underflow to zero", () => {
153
+ const log = taskTokenLogFromUsage(
154
+ {
155
+ inputTokens: 10,
156
+ outputTokens: 1,
157
+ totalTokens: 10,
158
+ cachedInputTokens: 8,
159
+ cacheWriteTokens: 5,
160
+ reasoningOutputTokens: undefined,
161
+ },
162
+ { subagentId: "root", turn: 1, at: "2026-01-01T00:00:00.000Z" },
163
+ );
164
+
165
+ expect(log.input).toBe(0);
166
+ });
167
+
168
+ it("skips a torn or corrupt jsonl line instead of throwing", async () => {
169
+ await withTempDir(async dir => {
170
+ const valid = taskTokenLogFromChat(
171
+ { inputTokens: 1, outputTokens: 2, cachedInputTokens: 3, cacheWriteTokens: 4 },
172
+ { subagentId: "root", turn: 1, at: "2026-01-01T00:00:00.000Z" },
173
+ );
174
+ await persistTaskTokenLog(valid, { dir });
175
+ // A partially-flushed / corrupt trailing line must not abort the read.
176
+ await appendFile(join(dir, "token-log.jsonl"), '{"subagentId":"root","turn":2,', "utf-8");
177
+
178
+ expect(await readTaskTokenLogs(dir)).toEqual([valid]);
179
+ });
180
+ });
181
+
182
+ it("throws when a non-empty log has no valid entries", async () => {
183
+ await withTempDir(async dir => {
184
+ await appendFile(join(dir, "token-log.jsonl"), "not json\n{}\n", "utf-8");
185
+ await expect(readTaskTokenLogs(dir)).rejects.toThrow(/corrupt token-log/);
186
+ });
187
+ });
188
+ });