@gajae-code/coding-agent 0.4.3 → 0.4.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 (92) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/types/async/job-manager.d.ts +19 -1
  3. package/dist/types/cli/fast-help.d.ts +1 -0
  4. package/dist/types/cli/setup-cli.d.ts +16 -1
  5. package/dist/types/commands/coordinator.d.ts +19 -0
  6. package/dist/types/commands/harness.d.ts +3 -0
  7. package/dist/types/commands/mcp-serve.d.ts +24 -0
  8. package/dist/types/commands/setup.d.ts +47 -0
  9. package/dist/types/config/model-registry.d.ts +3 -0
  10. package/dist/types/config/models-config-schema.d.ts +5 -0
  11. package/dist/types/coordinator/contract.d.ts +4 -0
  12. package/dist/types/coordinator-mcp/policy.d.ts +24 -0
  13. package/dist/types/coordinator-mcp/safety.d.ts +26 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +58 -0
  15. package/dist/types/extensibility/extensions/types.d.ts +13 -0
  16. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  17. package/dist/types/harness-control-plane/finalize.d.ts +5 -0
  18. package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
  19. package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
  20. package/dist/types/harness-control-plane/receipts.d.ts +46 -0
  21. package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
  22. package/dist/types/harness-control-plane/types.d.ts +9 -1
  23. package/dist/types/main.d.ts +2 -2
  24. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  25. package/dist/types/modes/utils/abort-message.d.ts +4 -0
  26. package/dist/types/session/session-manager.d.ts +8 -0
  27. package/dist/types/setup/hermes-setup.d.ts +78 -0
  28. package/dist/types/task/fork-context-advisory.d.ts +13 -0
  29. package/dist/types/task/receipt.d.ts +1 -0
  30. package/dist/types/task/render.d.ts +7 -1
  31. package/dist/types/task/roi-reconciliation.d.ts +27 -0
  32. package/dist/types/task/types.d.ts +10 -0
  33. package/dist/types/tools/subagent-render.d.ts +25 -0
  34. package/dist/types/tools/subagent.d.ts +5 -1
  35. package/package.json +8 -7
  36. package/scripts/build-binary.ts +4 -0
  37. package/src/async/job-manager.ts +43 -1
  38. package/src/cli/fast-help.ts +80 -0
  39. package/src/cli/setup-cli.ts +95 -2
  40. package/src/cli.ts +109 -16
  41. package/src/commands/coordinator.ts +113 -0
  42. package/src/commands/harness.ts +92 -9
  43. package/src/commands/mcp-serve.ts +63 -0
  44. package/src/commands/setup.ts +34 -1
  45. package/src/config/models-config-schema.ts +1 -0
  46. package/src/coordinator/contract.ts +21 -0
  47. package/src/coordinator-mcp/policy.ts +160 -0
  48. package/src/coordinator-mcp/safety.ts +80 -0
  49. package/src/coordinator-mcp/server.ts +1519 -0
  50. package/src/cursor.ts +30 -2
  51. package/src/extensibility/extensions/types.ts +13 -0
  52. package/src/gjc-runtime/launch-worktree.ts +12 -1
  53. package/src/gjc-runtime/session-state-sidecar.ts +117 -0
  54. package/src/harness-control-plane/finalize.ts +39 -5
  55. package/src/harness-control-plane/owner.ts +9 -1
  56. package/src/harness-control-plane/phase-rollup.ts +96 -0
  57. package/src/harness-control-plane/receipt-ingest.ts +127 -0
  58. package/src/harness-control-plane/receipts.ts +229 -1
  59. package/src/harness-control-plane/rpc-adapter.ts +8 -0
  60. package/src/harness-control-plane/types.ts +29 -1
  61. package/src/internal-urls/docs-index.generated.ts +6 -4
  62. package/src/main.ts +7 -3
  63. package/src/modes/components/hook-selector.ts +109 -5
  64. package/src/modes/components/status-line.ts +6 -6
  65. package/src/modes/controllers/event-controller.ts +5 -4
  66. package/src/modes/controllers/extension-ui-controller.ts +16 -1
  67. package/src/modes/interactive-mode.ts +4 -5
  68. package/src/modes/print-mode.ts +1 -1
  69. package/src/modes/theme/theme.ts +2 -2
  70. package/src/modes/utils/abort-message.ts +41 -0
  71. package/src/modes/utils/context-usage.ts +15 -8
  72. package/src/modes/utils/ui-helpers.ts +5 -6
  73. package/src/prompts/agents/architect.md +6 -0
  74. package/src/prompts/agents/critic.md +6 -0
  75. package/src/prompts/agents/planner.md +8 -1
  76. package/src/sdk.ts +9 -4
  77. package/src/session/agent-session.ts +22 -5
  78. package/src/session/session-manager.ts +20 -0
  79. package/src/setup/hermes/templates/operator-instructions.v1.md +30 -0
  80. package/src/setup/hermes-setup.ts +484 -0
  81. package/src/task/fork-context-advisory.ts +99 -0
  82. package/src/task/index.ts +33 -2
  83. package/src/task/receipt.ts +2 -0
  84. package/src/task/render.ts +14 -0
  85. package/src/task/roi-reconciliation.ts +90 -0
  86. package/src/task/types.ts +7 -0
  87. package/src/tools/ask.ts +30 -10
  88. package/src/tools/index.ts +2 -2
  89. package/src/tools/renderers.ts +2 -0
  90. package/src/tools/subagent-render.ts +169 -0
  91. package/src/tools/subagent.ts +49 -7
  92. package/src/utils/title-generator.ts +16 -2
@@ -0,0 +1,169 @@
1
+ /**
2
+ * TUI renderer for the `subagent` tool.
3
+ *
4
+ * The await panel surfaces each awaited subagent's live streaming status at
5
+ * parity with the inline `task` panel by reusing `renderSubagentLiveProgress`.
6
+ * Falls back to a `running, no activity yet` placeholder when a live producer
7
+ * exists but has not emitted yet, and to a static status line when no live
8
+ * producer is available (resumed-from-disk or backward-compat records).
9
+ */
10
+ import type { Component } from "@gajae-code/tui";
11
+ import { Text } from "@gajae-code/tui";
12
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
13
+ import type { Theme } from "../modes/theme/theme";
14
+ import { renderSubagentLiveProgress } from "../task/render";
15
+ import { Ellipsis, Hasher, type RenderCache, renderStatusLine } from "../tui";
16
+ import {
17
+ formatDuration,
18
+ formatStatusIcon,
19
+ getPreviewLines,
20
+ replaceTabs,
21
+ type ToolUIStatus,
22
+ truncateToWidth,
23
+ } from "./render-utils";
24
+ import type { SubagentSnapshot, SubagentToolDetails } from "./subagent";
25
+
26
+ const PREVIEW_LINES_COLLAPSED = 1;
27
+ const PREVIEW_LINES_EXPANDED = 4;
28
+ const PREVIEW_LINE_WIDTH = 80;
29
+
30
+ function statusIconKind(status: SubagentSnapshot["status"]): ToolUIStatus {
31
+ switch (status) {
32
+ case "completed":
33
+ case "already_completed":
34
+ return "success";
35
+ case "failed":
36
+ return "error";
37
+ case "cancelled":
38
+ case "not_found":
39
+ return "warning";
40
+ case "queued":
41
+ return "pending";
42
+ default:
43
+ return "info";
44
+ }
45
+ }
46
+
47
+ function renderSubagentSnapshot(
48
+ snapshot: SubagentSnapshot,
49
+ expanded: boolean,
50
+ theme: Theme,
51
+ spinnerFrame: number | undefined,
52
+ ): string[] {
53
+ const lines: string[] = [];
54
+ const icon = formatStatusIcon(
55
+ statusIconKind(snapshot.status),
56
+ theme,
57
+ snapshot.status === "running" ? spinnerFrame : undefined,
58
+ );
59
+ const id = theme.fg("muted", snapshot.id);
60
+ const status = theme.fg("dim", snapshot.status);
61
+ const duration = theme.fg("dim", formatDuration(snapshot.durationMs));
62
+ lines.push(`${icon} ${id} ${status} ${duration}`);
63
+
64
+ // Static receipt fields (parity with the markdown content for non-await actions).
65
+ if (snapshot.jobId !== snapshot.id) lines.push(` ${theme.fg("dim", `Job: ${snapshot.jobId}`)}`);
66
+ if (snapshot.agent && snapshot.agent !== "unknown") {
67
+ lines.push(` ${theme.fg("dim", `Agent: ${snapshot.agent} (${snapshot.agentSource})`)}`);
68
+ }
69
+ if (snapshot.description) lines.push(` ${theme.fg("dim", `Description: ${snapshot.description}`)}`);
70
+ if (snapshot.outputRef) lines.push(` ${theme.fg("dim", `Output: ${snapshot.outputRef}`)}`);
71
+ if (snapshot.assignment) {
72
+ lines.push(` ${theme.fg("dim", "Assignment:")}`);
73
+ for (const al of snapshot.assignment.split("\n")) lines.push(` ${theme.fg("toolOutput", replaceTabs(al))}`);
74
+ }
75
+
76
+ // Defense in depth: the producer only attaches `progress` when a live
77
+ // producer exists (subagent.ts #liveProgressFields), but the renderer
78
+ // also honors an explicit `liveProgressAvailable: false` so stale retained
79
+ // progress can never resurrect a live panel (AC5).
80
+ if (snapshot.progress && snapshot.liveProgressAvailable !== false) {
81
+ // Live streaming panel (full task-panel parity), indented under the header.
82
+ for (const pl of renderSubagentLiveProgress(snapshot.progress, expanded, theme, spinnerFrame)) {
83
+ lines.push(` ${pl}`);
84
+ }
85
+ } else if (snapshot.liveProgressAvailable && (snapshot.status === "running" || snapshot.status === "queued")) {
86
+ lines.push(` ${theme.fg("dim", "running, no activity yet")}`);
87
+ }
88
+
89
+ const preview = snapshot.errorText?.trim() || snapshot.resultText?.trim();
90
+ if (preview) {
91
+ const maxLines = expanded ? PREVIEW_LINES_EXPANDED : PREVIEW_LINES_COLLAPSED;
92
+ const tone = snapshot.errorText ? "error" : "dim";
93
+ for (const pl of getPreviewLines(preview, maxLines, PREVIEW_LINE_WIDTH, Ellipsis.Unicode)) {
94
+ lines.push(` ${theme.fg(tone, replaceTabs(pl))}`);
95
+ }
96
+ if (snapshot.truncated) {
97
+ lines.push(
98
+ ` ${theme.fg("dim", "Preview truncated; use the output ref or explicit ids with `verbosity=full` for more.")}`,
99
+ );
100
+ }
101
+ }
102
+
103
+ if (snapshot.guidance) lines.push(` ${theme.fg("dim", snapshot.guidance)}`);
104
+ return lines;
105
+ }
106
+
107
+ export const subagentToolRenderer = {
108
+ inline: true,
109
+
110
+ renderCall(_args: unknown, _options: RenderResultOptions, theme: Theme): Component {
111
+ return new Text(renderStatusLine({ icon: "pending", title: "Subagent" }, theme), 0, 0);
112
+ },
113
+
114
+ renderResult(
115
+ result: { content: Array<{ type: string; text?: string }>; details?: SubagentToolDetails },
116
+ options: RenderResultOptions,
117
+ theme: Theme,
118
+ ): Component {
119
+ const subagents = result.details?.subagents ?? [];
120
+ if (subagents.length === 0) {
121
+ const fallback = result.content.find(c => c.type === "text")?.text || "No subagents";
122
+ return new Text(theme.fg("dim", truncateToWidth(fallback, 100)), 0, 0);
123
+ }
124
+
125
+ const runningCount = subagents.filter(s => s.status === "running").length;
126
+
127
+ let cached: RenderCache | undefined;
128
+ return {
129
+ render(width: number): string[] {
130
+ const expanded = options.expanded;
131
+ const spinnerFrame = options.spinnerFrame ?? 0;
132
+ const key = new Hasher().bool(expanded).u32(width).u32(spinnerFrame).digest();
133
+ if (cached?.key === key) return cached.lines;
134
+
135
+ const header = renderStatusLine(
136
+ {
137
+ icon: runningCount > 0 ? "info" : "success",
138
+ spinnerFrame: runningCount > 0 ? options.spinnerFrame : undefined,
139
+ title: "Subagent",
140
+ description:
141
+ runningCount > 0
142
+ ? `awaiting ${runningCount} of ${subagents.length}`
143
+ : `${subagents.length} ${subagents.length === 1 ? "subagent" : "subagents"}`,
144
+ },
145
+ theme,
146
+ );
147
+
148
+ const lines: string[] = [header];
149
+ // Discoverability: the inline panel is a bounded preview; the session
150
+ // observer (ctrl+s) streams the full per-subagent message history.
151
+ if (runningCount > 0) {
152
+ lines.push(` ${theme.fg("dim", "(ctrl+s to observe sessions)")}`);
153
+ }
154
+ for (const snapshot of subagents) {
155
+ lines.push(...renderSubagentSnapshot(snapshot, expanded, theme, options.spinnerFrame));
156
+ }
157
+
158
+ const out = lines.map(l => (l.length > 0 ? truncateToWidth(l, width, Ellipsis.Omit) : ""));
159
+ cached = { key, lines: out };
160
+ return out;
161
+ },
162
+ invalidate() {
163
+ cached = undefined;
164
+ },
165
+ };
166
+ },
167
+
168
+ mergeCallAndResult: true,
169
+ };
@@ -4,7 +4,7 @@ import { prompt } from "@gajae-code/utils";
4
4
  import * as z from "zod/v4";
5
5
  import { type AsyncJob, AsyncJobManager, type SubagentRecord } from "../async";
6
6
  import subagentDescription from "../prompts/tools/subagent.md" with { type: "text" };
7
- import type { AgentSource } from "../task/types";
7
+ import type { AgentProgress, AgentSource } from "../task/types";
8
8
  import { Ellipsis, truncateToWidth } from "../tui";
9
9
  import type { ToolSession } from "./index";
10
10
  import { replaceTabs } from "./render-utils";
@@ -63,6 +63,10 @@ export interface SubagentSnapshot {
63
63
  outputRef?: string;
64
64
  truncated?: boolean;
65
65
  guidance?: string;
66
+ /** Live streaming progress for the awaited subagent (await panel only; UI detail). */
67
+ progress?: AgentProgress;
68
+ /** True when a live in-session progress producer exists for this subagent. */
69
+ liveProgressAvailable?: boolean;
66
70
  }
67
71
 
68
72
  export interface SubagentToolDetails {
@@ -322,10 +326,10 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
322
326
  manager.watchJobs(watchedJobIds);
323
327
  const progressTimer = onUpdate
324
328
  ? setInterval(() => {
325
- onUpdate(this.#progressResult(manager, records));
329
+ onUpdate(this.#progressResult(manager, records, true));
326
330
  }, 500)
327
331
  : undefined;
328
- onUpdate?.(this.#progressResult(manager, records));
332
+ onUpdate?.(this.#progressResult(manager, records, true));
329
333
 
330
334
  let timedOut = false;
331
335
  try {
@@ -355,6 +359,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
355
359
  notFoundIds,
356
360
  timedOut,
357
361
  verbosity: params.verbosity ?? "receipt",
362
+ attachLiveProgress: true,
358
363
  });
359
364
  }
360
365
 
@@ -450,17 +455,29 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
450
455
  return ids.filter(id => !this.#findVisibleRecord(manager, id, ownerFilter));
451
456
  }
452
457
 
453
- #progressResult(manager: AsyncJobManager, records: SubagentRecord[]): AgentToolResult<SubagentToolDetails> {
458
+ #progressResult(
459
+ manager: AsyncJobManager,
460
+ records: SubagentRecord[],
461
+ attachLiveProgress = false,
462
+ ): AgentToolResult<SubagentToolDetails> {
454
463
  return {
455
464
  content: [{ type: "text", text: "" }],
456
- details: { subagents: this.#recordSnapshots(manager, records, false, "receipt", new Set()) },
465
+ details: {
466
+ subagents: this.#recordSnapshots(manager, records, false, "receipt", new Set(), attachLiveProgress),
467
+ },
457
468
  };
458
469
  }
459
470
 
460
471
  async #buildRecordResult(
461
472
  manager: AsyncJobManager,
462
473
  records: SubagentRecord[],
463
- options: { title: string; notFoundIds?: string[]; timedOut?: boolean; verbosity?: SubagentParams["verbosity"] },
474
+ options: {
475
+ title: string;
476
+ notFoundIds?: string[];
477
+ timedOut?: boolean;
478
+ verbosity?: SubagentParams["verbosity"];
479
+ attachLiveProgress?: boolean;
480
+ },
464
481
  ): Promise<AgentToolResult<SubagentToolDetails>> {
465
482
  const verifiedOutputIds = await this.#verifiedOutputIds(records);
466
483
  const snapshots = this.#recordSnapshots(
@@ -469,6 +486,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
469
486
  options.timedOut,
470
487
  options.verbosity ?? "receipt",
471
488
  verifiedOutputIds,
489
+ options.attachLiveProgress ?? false,
472
490
  );
473
491
  for (const id of options.notFoundIds ?? []) {
474
492
  snapshots.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
@@ -513,8 +531,28 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
513
531
  timedOut = false,
514
532
  verbosity: SubagentParams["verbosity"] = "receipt",
515
533
  verifiedOutputIds: ReadonlySet<string>,
534
+ attachLiveProgress = false,
516
535
  ): SubagentSnapshot[] {
517
- return records.map(record => this.#recordSnapshot(manager, record, timedOut, verbosity, verifiedOutputIds));
536
+ return records.map(record =>
537
+ this.#recordSnapshot(manager, record, timedOut, verbosity, verifiedOutputIds, attachLiveProgress),
538
+ );
539
+ }
540
+
541
+ #liveProgressFields(
542
+ manager: AsyncJobManager,
543
+ record: SubagentRecord,
544
+ attachLiveProgress: boolean,
545
+ ): Pick<SubagentSnapshot, "progress" | "liveProgressAvailable"> {
546
+ if (!attachLiveProgress) return {};
547
+ const liveProgressAvailable = manager.hasLiveSubagent(record.subagentId);
548
+ // Only surface progress when a live producer exists; stale/retained progress
549
+ // for a record with no live producer must degrade to a static snapshot (AC5).
550
+ if (!liveProgressAvailable) return { liveProgressAvailable: false };
551
+ const progress = manager.getSubagentProgress(record.subagentId);
552
+ return {
553
+ liveProgressAvailable: true,
554
+ ...(progress ? { progress } : {}),
555
+ };
518
556
  }
519
557
 
520
558
  #recordSnapshot(
@@ -523,7 +561,9 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
523
561
  timedOut = false,
524
562
  verbosity: SubagentParams["verbosity"] = "receipt",
525
563
  verifiedOutputIds: ReadonlySet<string>,
564
+ attachLiveProgress = false,
526
565
  ): SubagentSnapshot {
566
+ const liveFields = this.#liveProgressFields(manager, record, attachLiveProgress);
527
567
  const job = record.currentJobId ? manager.getJob(record.currentJobId) : undefined;
528
568
  if (job) {
529
569
  return {
@@ -531,6 +571,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
531
571
  id: record.subagentId,
532
572
  jobId: record.currentJobId ?? job.id,
533
573
  status: record.status,
574
+ ...liveFields,
534
575
  };
535
576
  }
536
577
  return {
@@ -542,6 +583,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
542
583
  agentSource: "bundled",
543
584
  durationMs: 0,
544
585
  ...(verifiedOutputIds.has(record.subagentId) ? { outputRef: `agent://${record.subagentId}` } : {}),
586
+ ...liveFields,
545
587
  };
546
588
  }
547
589
 
@@ -12,7 +12,7 @@ import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "
12
12
 
13
13
  const TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
14
14
 
15
- const DEFAULT_TERMINAL_TITLE = "π";
15
+ const DEFAULT_TERMINAL_TITLE = "GJC";
16
16
  const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
17
17
 
18
18
  const MAX_INPUT_CHARS = 2000;
@@ -20,6 +20,13 @@ const TITLE_MAX_TOKENS = 30;
20
20
  const REASONING_SAFE_MAX_TOKENS = 1024;
21
21
  const SET_TITLE_TOOL_NAME = "set_title";
22
22
 
23
+ // Some models (notably cursor/composer-*) ignore the forced set_title tool call
24
+ // and instead emit a long free-text narrative. Without the tool call we fall back
25
+ // to the plain text, so cap its length: a real 3-6 word title never exceeds these.
26
+ // Beyond the cap we treat the response as a non-title hallucination and reject it.
27
+ const MAX_TITLE_CHARS = 80;
28
+ const MAX_TITLE_WORDS = 12;
29
+
23
30
  const setTitleTool: Tool = {
24
31
  name: SET_TITLE_TOOL_NAME,
25
32
  description: "Set the generated session title.",
@@ -169,7 +176,14 @@ function extractGeneratedTitle(contentBlocks: AssistantMessage["content"]): stri
169
176
  textTitle += content.text;
170
177
  }
171
178
  }
172
- return textTitle.trim();
179
+ // Plain-text fallback (no set_title tool call): only accept it if it actually
180
+ // looks like a title. A model that ignored the tool and rambled produces a long
181
+ // blob — reject it so the caller falls back rather than persisting the narrative.
182
+ const trimmed = textTitle.trim();
183
+ if (trimmed.length > MAX_TITLE_CHARS || trimmed.split(/\s+/).length > MAX_TITLE_WORDS) {
184
+ return "";
185
+ }
186
+ return trimmed;
173
187
  }
174
188
 
175
189
  /**