@gajae-code/coding-agent 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/notify-cli.d.ts +23 -0
  5. package/dist/types/cli/setup-cli.d.ts +20 -1
  6. package/dist/types/commands/daemon.d.ts +41 -0
  7. package/dist/types/commands/notify.d.ts +41 -0
  8. package/dist/types/config/model-profile-activation.d.ts +12 -0
  9. package/dist/types/config/model-profiles.d.ts +2 -1
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +38 -0
  13. package/dist/types/coordinator/contract.d.ts +1 -1
  14. package/dist/types/daemon/builtin.d.ts +20 -0
  15. package/dist/types/daemon/control-types.d.ts +57 -0
  16. package/dist/types/daemon/runtime.d.ts +25 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  18. package/dist/types/gjc-runtime/state-writer.d.ts +2 -0
  19. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +14 -0
  21. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  22. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  23. package/dist/types/modes/interactive-mode.d.ts +1 -1
  24. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  25. package/dist/types/modes/types.d.ts +7 -1
  26. package/dist/types/notifications/config-commands.d.ts +26 -0
  27. package/dist/types/notifications/config.d.ts +61 -0
  28. package/dist/types/notifications/helpers.d.ts +55 -0
  29. package/dist/types/notifications/html-format.d.ts +62 -0
  30. package/dist/types/notifications/index.d.ts +28 -0
  31. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  32. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  33. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  34. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  35. package/dist/types/notifications/telegram-daemon.d.ts +276 -0
  36. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  37. package/dist/types/notifications/threaded-inbound.d.ts +58 -0
  38. package/dist/types/notifications/threaded-render.d.ts +66 -0
  39. package/dist/types/notifications/topic-registry.d.ts +67 -0
  40. package/dist/types/rlm/index.d.ts +12 -0
  41. package/dist/types/session/agent-session.d.ts +39 -2
  42. package/dist/types/session/auth-storage.d.ts +1 -1
  43. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  44. package/dist/types/setup/credential-import.d.ts +3 -0
  45. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  46. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  47. package/dist/types/tools/index.d.ts +18 -0
  48. package/dist/types/tools/subagent.d.ts +3 -0
  49. package/package.json +7 -7
  50. package/scripts/build-binary.ts +3 -0
  51. package/src/async/job-manager.ts +5 -1
  52. package/src/cli/daemon-cli.ts +122 -0
  53. package/src/cli/notify-cli.ts +274 -0
  54. package/src/cli/setup-cli.ts +173 -84
  55. package/src/cli.ts +2 -0
  56. package/src/commands/daemon.ts +47 -0
  57. package/src/commands/notify.ts +61 -0
  58. package/src/commands/setup.ts +11 -1
  59. package/src/config/model-profile-activation.ts +74 -5
  60. package/src/config/model-profiles.ts +7 -4
  61. package/src/config/model-registry.ts +6 -3
  62. package/src/config/models-config-schema.ts +1 -1
  63. package/src/config/settings-schema.ts +29 -0
  64. package/src/coordinator/contract.ts +3 -0
  65. package/src/coordinator-mcp/server.ts +270 -1
  66. package/src/daemon/builtin.ts +46 -0
  67. package/src/daemon/control-types.ts +65 -0
  68. package/src/daemon/runtime.ts +51 -0
  69. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  70. package/src/extensibility/extensions/runner.ts +4 -0
  71. package/src/extensibility/extensions/types.ts +8 -0
  72. package/src/gjc-runtime/deep-interview-recorder.ts +12 -4
  73. package/src/gjc-runtime/state-runtime.ts +18 -4
  74. package/src/gjc-runtime/state-writer.ts +8 -8
  75. package/src/gjc-runtime/ultragoal-guard.ts +57 -2
  76. package/src/gjc-runtime/ultragoal-runtime.ts +105 -19
  77. package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
  78. package/src/gjc-runtime/workflow-manifest.ts +11 -1
  79. package/src/goals/tools/goal-tool.ts +11 -2
  80. package/src/internal-urls/docs-index.generated.ts +6 -5
  81. package/src/main.ts +30 -0
  82. package/src/modes/acp/acp-event-mapper.ts +1 -0
  83. package/src/modes/components/hook-editor.ts +7 -2
  84. package/src/modes/components/oauth-selector.ts +19 -0
  85. package/src/modes/controllers/event-controller.ts +20 -0
  86. package/src/modes/controllers/selector-controller.ts +80 -17
  87. package/src/modes/interactive-mode.ts +6 -2
  88. package/src/modes/runtime-init.ts +1 -0
  89. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  90. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  91. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  92. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  93. package/src/modes/types.ts +7 -1
  94. package/src/modes/utils/ui-helpers.ts +23 -0
  95. package/src/notifications/config-commands.ts +50 -0
  96. package/src/notifications/config.ts +107 -0
  97. package/src/notifications/helpers.ts +135 -0
  98. package/src/notifications/html-format.ts +389 -0
  99. package/src/notifications/index.ts +663 -0
  100. package/src/notifications/rate-limit-pool.ts +179 -0
  101. package/src/notifications/telegram-cli.ts +194 -0
  102. package/src/notifications/telegram-daemon-cli.ts +74 -0
  103. package/src/notifications/telegram-daemon-control.ts +370 -0
  104. package/src/notifications/telegram-daemon.ts +1370 -0
  105. package/src/notifications/telegram-reference.ts +335 -0
  106. package/src/notifications/threaded-inbound.ts +80 -0
  107. package/src/notifications/threaded-render.ts +155 -0
  108. package/src/notifications/topic-registry.ts +133 -0
  109. package/src/rlm/index.ts +19 -0
  110. package/src/sdk.ts +16 -0
  111. package/src/session/agent-session.ts +113 -3
  112. package/src/session/auth-storage.ts +3 -0
  113. package/src/session/session-dump-format.ts +43 -2
  114. package/src/session/session-manager.ts +39 -5
  115. package/src/setup/credential-auto-import.ts +258 -0
  116. package/src/setup/credential-import.ts +17 -0
  117. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  118. package/src/setup/host-plugin-setup.ts +142 -0
  119. package/src/slash-commands/builtin-registry.ts +4 -1
  120. package/src/task/executor.ts +5 -1
  121. package/src/tools/ask-answer-registry.ts +25 -0
  122. package/src/tools/ask.ts +74 -4
  123. package/src/tools/image-gen.ts +5 -8
  124. package/src/tools/index.ts +19 -0
  125. package/src/tools/inspect-image.ts +16 -11
  126. package/src/tools/subagent-render.ts +7 -0
  127. package/src/tools/subagent.ts +38 -7
@@ -70,7 +70,7 @@ export function isAuthenticated(apiKey: string | undefined | null): apiKey is st
70
70
  return Boolean(apiKey) && apiKey !== kNoAuth;
71
71
  }
72
72
 
73
- export type ModelRole = "default";
73
+ export type ModelRole = "default" | "vision";
74
74
 
75
75
  export interface ModelRoleInfo {
76
76
  tag?: string;
@@ -80,11 +80,12 @@ export interface ModelRoleInfo {
80
80
 
81
81
  export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
82
82
  default: { tag: "DEFAULT", name: "Default", color: "success" },
83
+ vision: { tag: "VISION", name: "Vision", color: "accent" },
83
84
  };
84
85
 
85
- export const MODEL_ROLE_IDS: ModelRole[] = ["default"];
86
+ export const MODEL_ROLE_IDS: ModelRole[] = ["default", "vision"];
86
87
 
87
- export type GjcModelAssignmentTargetId = "default" | "executor" | "architect" | "planner" | "critic";
88
+ export type GjcModelAssignmentTargetId = "default" | "vision" | "executor" | "architect" | "planner" | "critic";
88
89
 
89
90
  export interface GjcModelAssignmentTargetInfo extends ModelRoleInfo {
90
91
  id: GjcModelAssignmentTargetId;
@@ -93,6 +94,7 @@ export interface GjcModelAssignmentTargetInfo extends ModelRoleInfo {
93
94
 
94
95
  export const GJC_MODEL_ASSIGNMENT_TARGET_IDS: GjcModelAssignmentTargetId[] = [
95
96
  "default",
97
+ "vision",
96
98
  "executor",
97
99
  "architect",
98
100
  "planner",
@@ -101,6 +103,7 @@ export const GJC_MODEL_ASSIGNMENT_TARGET_IDS: GjcModelAssignmentTargetId[] = [
101
103
 
102
104
  export const GJC_MODEL_ASSIGNMENT_TARGETS: Record<GjcModelAssignmentTargetId, GjcModelAssignmentTargetInfo> = {
103
105
  default: { id: "default", tag: "DEFAULT", name: "Default", color: "success", settingsPath: "modelRoles" },
106
+ vision: { id: "vision", tag: "VISION", name: "Vision", color: "accent", settingsPath: "modelRoles" },
104
107
  executor: {
105
108
  id: "executor",
106
109
  tag: "EXECUTOR",
@@ -81,7 +81,7 @@ const ModelBindingsSchema = z.object({
81
81
  modelRoles: z.record(z.string(), z.string().min(1)).optional(),
82
82
  agentModelOverrides: z.record(z.string(), z.string().min(1)).optional(),
83
83
  });
84
- export const ProfileRoleSchema = z.enum(["default", "executor", "architect", "planner", "critic"]);
84
+ export const ProfileRoleSchema = z.enum(["default", "vision", "executor", "architect", "planner", "critic"]);
85
85
 
86
86
  function isValidProfileModelSelector(value: string): boolean {
87
87
  if (value.includes(",")) return false;
@@ -254,6 +254,22 @@ export const SETTINGS_SCHEMA = {
254
254
  "auth.broker.url": { type: "string", default: undefined },
255
255
  "auth.broker.token": { type: "string", default: undefined },
256
256
 
257
+ // Notifications (Telegram bundled reference client)
258
+ "notifications.enabled": { type: "boolean", default: false },
259
+ "notifications.telegram.botToken": { type: "string", default: undefined },
260
+ "notifications.telegram.chatId": { type: "string", default: undefined },
261
+ "notifications.redact": { type: "boolean", default: false },
262
+ "notifications.verbosity": {
263
+ type: "string",
264
+ default: "lean",
265
+ validate: (value: string) => value === "lean" || value === "verbose",
266
+ },
267
+ "notifications.daemon.idleTimeoutMs": {
268
+ type: "number",
269
+ default: 60000,
270
+ validate: (value: number) => Number.isFinite(value) && value > 0,
271
+ },
272
+
257
273
  autoResume: {
258
274
  type: "boolean",
259
275
  default: false,
@@ -3155,6 +3171,18 @@ export interface ShellMinimizerSettings {
3155
3171
  maxCaptureBytes: number;
3156
3172
  }
3157
3173
 
3174
+ export interface NotificationsSettings {
3175
+ enabled: boolean;
3176
+ telegram: {
3177
+ botToken: string | undefined;
3178
+ chatId: string | undefined;
3179
+ };
3180
+ redact: boolean;
3181
+ daemon: {
3182
+ idleTimeoutMs: number;
3183
+ };
3184
+ }
3185
+
3158
3186
  /** Map group prefix -> typed settings interface */
3159
3187
  export interface GroupTypeMap {
3160
3188
  compaction: CompactionSettings;
@@ -3173,6 +3201,7 @@ export interface GroupTypeMap {
3173
3201
  modelTags: ModelTagsSettings;
3174
3202
  cycleOrder: string[];
3175
3203
  shellMinimizer: ShellMinimizerSettings;
3204
+ notifications: NotificationsSettings;
3176
3205
  }
3177
3206
 
3178
3207
  export type GroupPrefix = keyof GroupTypeMap;
@@ -17,6 +17,9 @@ export const COORDINATOR_MCP_TOOL_NAMES = [
17
17
  "gjc_coordinator_read_turn",
18
18
  "gjc_coordinator_await_turn",
19
19
  "gjc_coordinator_report_status",
20
+ "gjc_delegate_plan",
21
+ "gjc_delegate_execute",
22
+ "gjc_delegate_team",
20
23
  ] as const;
21
24
 
22
25
  export type CoordinatorToolName = (typeof COORDINATOR_MCP_TOOL_NAMES)[number];
@@ -184,7 +184,8 @@ type CoordinatorEventKind =
184
184
  | "question.answered"
185
185
  | "report.written"
186
186
  | "tmux.delivery_succeeded"
187
- | "tmux.delivery_failed";
187
+ | "tmux.delivery_failed"
188
+ | "delegation.started";
188
189
 
189
190
  interface CoordinatorEvent {
190
191
  schema_version: 1;
@@ -411,9 +412,116 @@ function toolSchema(name: CoordinatorToolName): {
411
412
  },
412
413
  };
413
414
  }
415
+ const delegateWorkflow = workflowForDelegateTool(name);
416
+ if (delegateWorkflow) {
417
+ return {
418
+ name,
419
+ description: delegateToolDescription(delegateWorkflow),
420
+ inputSchema: {
421
+ type: "object",
422
+ properties: {
423
+ cwd,
424
+ task: {
425
+ type: "string",
426
+ description: "Delegated task or objective to run through the selected GJC workflow.",
427
+ },
428
+ prompt: { type: "string", description: "Alias for task; accepted when task is absent." },
429
+ allow_mutation: allowMutation,
430
+ session_id: {
431
+ type: "string",
432
+ description:
433
+ "Optional existing GJC coordinator bridge session id to reuse; omitted starts a fresh session.",
434
+ },
435
+ queue: {
436
+ type: "boolean",
437
+ description: "When reusing a session with an active turn, queue instead of failing.",
438
+ },
439
+ force: {
440
+ type: "boolean",
441
+ description: "When reusing a session with an active turn, supersede it before sending.",
442
+ },
443
+ model: {
444
+ type: "string",
445
+ description: "Optional model hint passed in prompt metadata; no provider default is implied.",
446
+ },
447
+ await_completion: { type: "boolean", description: "If true, poll the turn until terminal or timeout." },
448
+ timeout_ms: {
449
+ type: "number",
450
+ description: "Bounded await timeout; same cap semantics as gjc_coordinator_await_turn.",
451
+ },
452
+ poll_interval_ms: { type: "number", description: "Bounded await polling interval." },
453
+ lines: { type: "number", description: "Bounded advisory tail lines returned with await/read payloads." },
454
+ },
455
+ required: ["cwd", "allow_mutation"],
456
+ },
457
+ };
458
+ }
414
459
  return { name, description: "List known scoped GJC coordinator bridge sessions.", inputSchema: common };
415
460
  }
416
461
 
462
+ type DelegateWorkflow = "plan" | "execute" | "team";
463
+
464
+ function workflowForDelegateTool(name: string): DelegateWorkflow | null {
465
+ switch (name) {
466
+ case "gjc_delegate_plan":
467
+ return "plan";
468
+ case "gjc_delegate_execute":
469
+ return "execute";
470
+ case "gjc_delegate_team":
471
+ return "team";
472
+ default:
473
+ return null;
474
+ }
475
+ }
476
+
477
+ function workflowSkill(workflow: DelegateWorkflow): "ralplan" | "ultragoal" | "team" {
478
+ switch (workflow) {
479
+ case "plan":
480
+ return "ralplan";
481
+ case "execute":
482
+ return "ultragoal";
483
+ case "team":
484
+ return "team";
485
+ }
486
+ }
487
+
488
+ function delegateToolDescription(workflow: DelegateWorkflow): string {
489
+ switch (workflow) {
490
+ case "plan":
491
+ return "Delegate consensus planning to GJC: start a session and run /skill:ralplan to completion, returning durable turn status and artifact references.";
492
+ case "execute":
493
+ return "Delegate execution to GJC: start a session and run /skill:ultragoal to completion, returning durable turn status and artifact references.";
494
+ case "team":
495
+ return "Delegate parallel team execution to GJC: start a session and run /skill:team to completion, returning durable turn status and artifact references.";
496
+ }
497
+ }
498
+
499
+ function workflowPrompt(
500
+ workflow: DelegateWorkflow,
501
+ toolName: string,
502
+ canonicalCwd: string,
503
+ task: string,
504
+ options: { mutationRequested: boolean; model?: string | null },
505
+ ): string {
506
+ const skill = workflowSkill(workflow);
507
+ const model = options.model && options.model.trim().length > 0 ? options.model.trim() : "none";
508
+ const mutationIntent = options.mutationRequested ? "mutation requested" : "read-only";
509
+ return [
510
+ `/skill:${skill}`,
511
+ "",
512
+ `Delegated by coordinator MCP tool: ${toolName}`,
513
+ `Workflow: ${workflow}`,
514
+ `CWD: ${canonicalCwd}`,
515
+ `Mutation intent: ${mutationIntent}; coordinator startup policy remains authoritative.`,
516
+ `Optional model hint: ${model}`,
517
+ "",
518
+ "Task:",
519
+ task,
520
+ "",
521
+ "Return durable status and artifact references through GJC runtime/coordinator state. Do not expose host-facing tmux controls.",
522
+ ].join("\n");
523
+ }
524
+
417
525
  function normalizeSession(session: Record<string, unknown>): Record<string, unknown> {
418
526
  return {
419
527
  session_id: session.sessionId ?? session.session_id ?? session.name ?? "unknown",
@@ -424,6 +532,14 @@ function normalizeSession(session: Record<string, unknown>): Record<string, unkn
424
532
  };
425
533
  }
426
534
 
535
+ async function canonicalizePath(value: string): Promise<string> {
536
+ try {
537
+ return await fs.realpath(value);
538
+ } catch {
539
+ return path.resolve(value);
540
+ }
541
+ }
542
+
427
543
  async function ensureDir(dir: string): Promise<void> {
428
544
  await fs.mkdir(dir, { recursive: true });
429
545
  }
@@ -1587,6 +1703,159 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1587
1703
  transport: { mcp: "long_poll", push_subscriptions: false },
1588
1704
  };
1589
1705
  }
1706
+ const delegateWorkflow = workflowForDelegateTool(name);
1707
+ if (delegateWorkflow) {
1708
+ requireCoordinatorMutation(config, "sessions", args);
1709
+ const canonicalCwd = await assertCoordinatorWorkdir(config, args.cwd);
1710
+ const hasTask = typeof args.task === "string" && args.task.trim().length > 0;
1711
+ const hasPrompt = typeof args.prompt === "string" && args.prompt.trim().length > 0;
1712
+ const task = hasTask ? String(args.task) : hasPrompt ? String(args.prompt) : null;
1713
+ if (!task) return { ok: false, reason: "task_required" };
1714
+ const promptAliasIgnored = hasTask && hasPrompt;
1715
+ const mutationRequested = args.allow_mutation === true;
1716
+ const taggedPrompt = workflowPrompt(delegateWorkflow, name, canonicalCwd, task, {
1717
+ mutationRequested,
1718
+ model: typeof args.model === "string" ? args.model : null,
1719
+ });
1720
+
1721
+ let session: Record<string, unknown>;
1722
+ let reusedSession = false;
1723
+ if (args.session_id != null) {
1724
+ const sessionId = safeExternalId("session", args.session_id);
1725
+ const existing = asRecord(await readJsonFile(sessionFile(sessionId)));
1726
+ if (!existing) return { ok: false, reason: "unknown_session", session_id: sessionId };
1727
+ const storedCwd = typeof existing.cwd === "string" ? existing.cwd : null;
1728
+ const canonicalStored = storedCwd ? await canonicalizePath(storedCwd) : null;
1729
+ const canonicalRequested = await canonicalizePath(canonicalCwd);
1730
+ if (!canonicalStored || canonicalStored !== canonicalRequested) {
1731
+ return { ok: false, reason: "session_cwd_mismatch", session_id: sessionId };
1732
+ }
1733
+ session = existing;
1734
+ reusedSession = true;
1735
+ } else {
1736
+ const input = {
1737
+ cwd: canonicalCwd,
1738
+ prompt: undefined,
1739
+ namespace: config.namespace,
1740
+ worktree: true as const,
1741
+ };
1742
+ const started = services.startSession
1743
+ ? await services.startSession(input)
1744
+ : await startTmuxSession(config, input, namespaceDir, commandRunner);
1745
+ const startedRecord = asRecord(started);
1746
+ if (!startedRecord) throw new Error("coordinator_session_command_required");
1747
+ session = normalizeSession(startedRecord);
1748
+ await writeJsonFile(sessionFile(session.session_id), session);
1749
+ await appendCoordinatorEvent(namespaceDir, {
1750
+ kind: "session.started",
1751
+ sessionId: String(session.session_id),
1752
+ summary: `Session ${String(session.session_id)} started by coordinator delegate`,
1753
+ payloadRef: path.relative(namespaceDir, sessionFile(session.session_id)),
1754
+ metadata: { delegate: true, workflow: delegateWorkflow },
1755
+ });
1756
+ const live = hasTmuxIdentity(session) ? await hasTmuxSession(session, commandRunner) : null;
1757
+ await writeSessionState(namespaceDir, String(session.session_id), "ready_for_input", {
1758
+ live,
1759
+ reason: null,
1760
+ });
1761
+ }
1762
+
1763
+ const sessionId = String(session.session_id);
1764
+ const activeTurn = reusedSession ? await readActiveTurn(namespaceDir, sessionId) : null;
1765
+ if (activeTurn && args.force !== true && args.queue !== true) {
1766
+ return {
1767
+ ok: false,
1768
+ reason: "active_turn_exists",
1769
+ session_id: sessionId,
1770
+ active_turn_id: activeTurn.turn_id,
1771
+ };
1772
+ }
1773
+ if (activeTurn && args.force === true) {
1774
+ const timestamp = new Date().toISOString();
1775
+ const superseded = {
1776
+ ...activeTurn,
1777
+ status: "superseded" as const,
1778
+ updated_at: timestamp,
1779
+ completed_at: timestamp,
1780
+ };
1781
+ await writeTurnRecord(namespaceDir, superseded);
1782
+ await clearActiveTurn(namespaceDir, superseded);
1783
+ }
1784
+ const shouldQueue = args.queue === true && args.force !== true && !!activeTurn;
1785
+ const turn = shouldQueue
1786
+ ? makeTurnRecord(config, sessionId, taggedPrompt, "queued")
1787
+ : await activateTurn(session, makeTurnRecord(config, sessionId, taggedPrompt, "active"));
1788
+ if (shouldQueue) await writeTurnRecord(namespaceDir, turn);
1789
+ await appendCoordinatorEvent(namespaceDir, {
1790
+ kind: "delegation.started",
1791
+ sessionId,
1792
+ turnId: turn.turn_id,
1793
+ summary: `Delegated ${delegateWorkflow} via ${name} on session ${sessionId}`,
1794
+ metadata: {
1795
+ workflow: delegateWorkflow,
1796
+ tool_name: name,
1797
+ reused_session: reusedSession,
1798
+ queued: shouldQueue,
1799
+ allow_mutation: args.allow_mutation === true,
1800
+ },
1801
+ });
1802
+ const sessionState = await readSessionState(namespaceDir, sessionId);
1803
+ const base: Record<string, unknown> = {
1804
+ ok: true,
1805
+ workflow: delegateWorkflow,
1806
+ tool_name: name,
1807
+ session_id: sessionId,
1808
+ turn_id: turn.turn_id,
1809
+ active_turn_id: shouldQueue ? activeTurn?.turn_id : turn.turn_id,
1810
+ status: turn.status,
1811
+ queued: turn.delivery.queued,
1812
+ delivered: turn.delivery.delivered,
1813
+ delivery: turn.delivery,
1814
+ session,
1815
+ session_state: sessionState,
1816
+ turn,
1817
+ awaited: false,
1818
+ artifacts: [],
1819
+ };
1820
+ if (promptAliasIgnored) base.prompt_alias_ignored = true;
1821
+ if (args.await_completion === true && !shouldQueue) {
1822
+ const timeoutMs = boundedTimeoutMs(args.timeout_ms);
1823
+ const pollIntervalMs = boundedPollIntervalMs(args.poll_interval_ms);
1824
+ const deadline = Date.now() + timeoutMs;
1825
+ let payload = await readTurnPayload(turn.turn_id, sessionId, args.lines);
1826
+ while (
1827
+ payload.ok === true &&
1828
+ !TERMINAL_TURN_STATUSES.has((payload.turn as TurnRecord).status) &&
1829
+ Date.now() < deadline
1830
+ ) {
1831
+ const remainingMs = deadline - Date.now();
1832
+ await waitForTurnStateChange(
1833
+ namespaceDir,
1834
+ payload.turn as TurnRecord,
1835
+ Math.min(pollIntervalMs, remainingMs),
1836
+ );
1837
+ payload = await readTurnPayload(turn.turn_id, sessionId, args.lines);
1838
+ }
1839
+ const awaitedTurn = (payload.ok === true ? payload.turn : turn) as TurnRecord;
1840
+ base.awaited = true;
1841
+ base.status = awaitedTurn.status;
1842
+ base.turn = awaitedTurn;
1843
+ base.final_response = (awaitedTurn as unknown as Record<string, unknown>).final_response ?? null;
1844
+ base.evidence = (awaitedTurn as unknown as Record<string, unknown>).evidence ?? [];
1845
+ if (payload.ok === true) {
1846
+ base.session_state = payload.session_state;
1847
+ base.advisory_status = payload.advisory_status;
1848
+ }
1849
+ // Mirror gjc_coordinator_await_turn timeout semantics: a still-active
1850
+ // turn at the deadline is a bounded timeout, not a completion.
1851
+ if (!TERMINAL_TURN_STATUSES.has(awaitedTurn.status)) {
1852
+ base.timed_out = true;
1853
+ base.reason = "timeout";
1854
+ base.ok = false;
1855
+ }
1856
+ }
1857
+ return base;
1858
+ }
1590
1859
  if (name === "gjc_coordinator_start_session") {
1591
1860
  requireCoordinatorMutation(config, "sessions", args);
1592
1861
  const cwd = await assertCoordinatorWorkdir(config, args.cwd);
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Static built-in daemon controller map.
3
+ *
4
+ * Intentionally a static map keyed by daemon kind rather than a mutable plugin
5
+ * registry: there is exactly one kind today (`telegram`). Promote to a richer
6
+ * registry only when a second daemon kind exists.
7
+ */
8
+
9
+ import type { Settings } from "../config/settings";
10
+ import { type TelegramDaemonControlDeps, TelegramDaemonController } from "../notifications/telegram-daemon-control";
11
+ import type { BuiltInDaemonController, DaemonKind } from "./control-types";
12
+
13
+ export const BUILT_IN_DAEMON_KINDS = ["telegram"] as const satisfies readonly DaemonKind[];
14
+
15
+ export interface BuiltInDaemonControllerDeps {
16
+ telegram?: TelegramDaemonControlDeps;
17
+ }
18
+
19
+ export function createBuiltInDaemonControllers(
20
+ settings: Settings,
21
+ deps: BuiltInDaemonControllerDeps = {},
22
+ ): Record<DaemonKind, BuiltInDaemonController> {
23
+ return {
24
+ telegram: new TelegramDaemonController(settings, deps.telegram),
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Resolve the controllers a command should act on. `--all` selects every
30
+ * built-in kind; otherwise the explicit `kinds` (defaulting to `telegram`).
31
+ */
32
+ export function selectDaemonControllers(
33
+ settings: Settings,
34
+ kinds: DaemonKind[] | undefined,
35
+ all: boolean,
36
+ deps: BuiltInDaemonControllerDeps = {},
37
+ ): BuiltInDaemonController[] {
38
+ const map = createBuiltInDaemonControllers(settings, deps);
39
+ if (all) return Object.values(map);
40
+ const selected = kinds && kinds.length > 0 ? kinds : (["telegram"] as DaemonKind[]);
41
+ return selected.map(kind => {
42
+ const controller = map[kind];
43
+ if (!controller) throw new Error(`unknown daemon kind: ${kind}`);
44
+ return controller;
45
+ });
46
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Public types for the `gjc daemon` control plane.
3
+ *
4
+ * Deliberately compact: a small result/status surface plus a built-in
5
+ * controller contract. There is exactly one daemon kind today (`telegram`);
6
+ * a richer registry is intentionally deferred until a second kind exists.
7
+ */
8
+
9
+ export type DaemonKind = "telegram";
10
+
11
+ export type DaemonAction = "list" | "status" | "stop" | "reload";
12
+
13
+ export type DaemonHealth = "not_configured" | "stopped" | "running" | "stale" | "stopping" | "error";
14
+
15
+ export interface DaemonRuntimeInfo {
16
+ /** `source` when respawn goes through bun/node + the entry script; `compiled` for a single-file binary. */
17
+ mode: "source" | "compiled";
18
+ execPath: string;
19
+ /** True only in source/dev mode, where a respawn loads amended TypeScript directly. */
20
+ reloadPicksUpSourceEdits: boolean;
21
+ /** Present when the runtime mode constrains what reload can achieve (e.g. compiled binary). */
22
+ warning?: string;
23
+ }
24
+
25
+ export interface DaemonStatus {
26
+ kind: DaemonKind;
27
+ configured: boolean;
28
+ health: DaemonHealth;
29
+ pid?: number;
30
+ ownerId?: string;
31
+ startedAt?: number;
32
+ heartbeatAt?: number;
33
+ roots?: string[];
34
+ rootCount?: number;
35
+ runtime: DaemonRuntimeInfo;
36
+ detail?: string;
37
+ }
38
+
39
+ export interface DaemonOperationOptions {
40
+ /** How long to wait for cooperative release before escalating. */
41
+ gracefulTimeoutMs?: number;
42
+ /** How long to wait for the old pid to die after SIGKILL. */
43
+ killTimeoutMs?: number;
44
+ /** Allow hard-kill escalation / acting on a still-live owner. */
45
+ force?: boolean;
46
+ /** For reload: spawn a fresh owner even when none is currently running. */
47
+ spawnIfStopped?: boolean;
48
+ }
49
+
50
+ export interface DaemonOperationResult {
51
+ kind: DaemonKind;
52
+ action: Exclude<DaemonAction, "list">;
53
+ ok: boolean;
54
+ before?: DaemonStatus;
55
+ after?: DaemonStatus;
56
+ warnings: string[];
57
+ message: string;
58
+ }
59
+
60
+ export interface BuiltInDaemonController {
61
+ readonly kind: DaemonKind;
62
+ status(): Promise<DaemonStatus>;
63
+ stop(opts?: DaemonOperationOptions): Promise<DaemonOperationResult>;
64
+ reload(opts?: DaemonOperationOptions): Promise<DaemonOperationResult>;
65
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Shared source-vs-compiled runtime detection for daemon spawning.
3
+ *
4
+ * Centralizes the logic previously embedded in `ensureTelegramDaemonRunning`
5
+ * so session autostart, reload, and status reporting agree on how a daemon
6
+ * process is launched and whether a reload can pick up amended source.
7
+ */
8
+
9
+ import * as path from "node:path";
10
+
11
+ export interface GjcRuntimeSpawnInfo {
12
+ execPath: string;
13
+ mode: "source" | "compiled";
14
+ /** Prefix prepended before the gjc subcommand args; `[Bun.main]` in source mode, otherwise `[]`. */
15
+ argsPrefix: string[];
16
+ /** True only when respawn loads edited TypeScript directly (source/dev mode). */
17
+ reloadPicksUpSourceEdits: boolean;
18
+ /** Set in compiled mode to explain that a rebuild is required before reload picks up source edits. */
19
+ warning?: string;
20
+ }
21
+
22
+ const COMPILED_RELOAD_WARNING =
23
+ "Compiled binary: reload respawns the same binary. Rebuild the binary first for amended source to take effect.";
24
+
25
+ /**
26
+ * Resolve how to spawn a detached gjc subcommand for the current runtime.
27
+ *
28
+ * Source/dev mode (bun/node) prepends the entry script (`Bun.main`) so the
29
+ * respawn loads edited source. A compiled single-file binary self-spawns its
30
+ * own subcommand directly and cannot pick up workspace source edits.
31
+ */
32
+ export function resolveGjcRuntimeSpawnInfo(execPath: string = process.execPath): GjcRuntimeSpawnInfo {
33
+ const base = path.basename(execPath).toLowerCase();
34
+ const fromSource = base === "bun" || base === "node" || base.startsWith("bun") || base.startsWith("node");
35
+ const mainScript = fromSource && typeof Bun !== "undefined" ? (Bun as unknown as { main?: string }).main : undefined;
36
+ if (fromSource) {
37
+ return {
38
+ execPath,
39
+ mode: "source",
40
+ argsPrefix: mainScript ? [mainScript] : [],
41
+ reloadPicksUpSourceEdits: true,
42
+ };
43
+ }
44
+ return {
45
+ execPath,
46
+ mode: "compiled",
47
+ argsPrefix: [],
48
+ reloadPicksUpSourceEdits: false,
49
+ warning: COMPILED_RELOAD_WARNING,
50
+ };
51
+ }
@@ -107,6 +107,21 @@ Loop until `gjc ultragoal status` reports all goals complete:
107
107
  `gjc ultragoal checkpoint --goal-id <id> --status blocked --evidence "<completed legacy GJC goal blocks goal create in this thread>" --gjc-goal-json <goal-get-json-or-path>`
108
108
  11. Resume failed goals with `gjc ultragoal complete-goals --retry-failed`.
109
109
 
110
+ ## Blocker triage and pause discipline
111
+
112
+ An active Ultragoal run must not give up on a blocker by pausing the goal and asking the user. Classify every blocker before deciding what to do, and default to `resolvable` when unsure:
113
+
114
+ - **`resolvable`** — anything the agent can act on: failing tests, missing implementation, a dependency to install, an ambiguous-but-inferable detail, investigation. **Never pause.** Exhaust autonomous resolution first: investigate, `gjc ultragoal steer --kind add_subgoal --title "Investigate blocker" --objective "..." --evidence "..." --rationale "..."`, delegate an `executor`, or preserve the blocker durably with `gjc ultragoal checkpoint --status blocked` / `gjc ultragoal record-review-blockers` and keep scheduling the next goal.
115
+ - **`human_blocked`** — only the user can act: credentials/secrets, a manual or physical step, an external approval/decision, access the agent lacks. Pause is the last resort and is gated.
116
+
117
+ `goal({"op":"pause"})` is **blocked at runtime** while an Ultragoal run is active unless the latest durable ledger event classifies the current blocker as `human_blocked`. To pause, record the classification immediately before pausing and cite the human-only dependency as evidence:
118
+
119
+ ```sh
120
+ gjc ultragoal classify-blocker --classification human_blocked --evidence "<the specific human-only dependency>" [--goal-id <id>]
121
+ ```
122
+
123
+ Recording `--classification resolvable` is an audit note only; it never authorizes a pause. The `ask` tool stays blocked during active runs regardless of classification — record unresolved decisions as durable blockers instead of prompting.
124
+
110
125
  ## Dynamic steering
111
126
 
112
127
  Use `gjc ultragoal steer` when real findings or blockers prove the current story decomposition should change while the aggregate objective and constraints stay fixed. Steering is explicit-only and evidence-backed; broad natural-language requests are rejected instead of guessed.
@@ -199,6 +214,7 @@ An ultragoal story cannot be checkpointed `complete` until the active agent has
199
214
  - CLI surfaces require runtime argv replay: `replaySafe: true`, an allowlisted argv `command`, and replayed normalized stdout matching `recordedStdout`; unsafe commands require audited `replayExempt` metadata with exact fields `reasonCode`, `reason`, `approvedBy`, and `fallbackArtifactRefs` plus a structurally valid fallback artifact. Allowed `reasonCode` values are exactly `unsafe_side_effect`, `requires_credentials`, `requires_network`, `non_deterministic_external`, `destructive`, `interactive_only`, and `platform_unavailable`.
200
215
  - Native/desktop/tui surfaces require a structurally valid screenshot, PTY capture with terminal control codes, or app-automation transcript.
201
216
  - API/package/algorithm/math surfaces require a real artifact file or typed receipt. Bare `inlineEvidence` text alone is not sufficient for any surface.
217
+ - The mandatory **computer-use** red-team suite (`kill-switch-bypass`, `suspended-enforcement`, `permission-revoked`, …) is conditional, not universal: require it only when computer/desktop control is genuinely part of the product surface being dogfooded. For every other product type, prove the change through the matching live surface instead — browser-use automation for web/GUI, bash/CLI live invocation or argv replay for CLI, and real artifacts or typed receipts for API/package/algorithm/math. Editing docs, prompts, or skills that merely mention computer-use does not by itself make the computer-use suite applicable; pick the red-team surface that matches what the change actually ships.
202
218
  7. The executor QA/red-team lane must report a matrix using `executorQa.contractCoverage`, `executorQa.surfaceEvidence`, `executorQa.adversarialCases`, and `executorQa.artifactRefs`. Not-applicable rows are allowed only in `contractCoverage` and `surfaceEvidence`; each `status: "not_applicable"` row requires `contractRef` plus `reason`. `adversarialCases` rows cannot be not-applicable.
203
219
  8. Run a final code review pass and fold it into the strict quality gate. Clean means `architectReview.architectureStatus`, `architectReview.productStatus`, and `architectReview.codeStatus` are all `"CLEAR"`, `architectReview.recommendation` is `"APPROVE"`, executor QA statuses are `"passed"`, iteration is `"passed"` with `fullRerun: true`, every evidence field is non-empty, every required matrix row is present, and every blockers array is empty. `COMMENT`, `WATCH`, `REQUEST CHANGES`, `BLOCK`, missing evidence, missing or shallow matrix rows, plan/code mismatches, or non-empty blockers are non-clean.
204
220
  9. If any lane finds an issue, do **not** checkpoint `complete` and do **not** call `goal({"op":"complete"})`. Record durable blocker work instead:
@@ -6,6 +6,7 @@ import type { CredentialDisabledEvent, ImageContent, Model, ProviderResponseMeta
6
6
  import type { KeyId } from "@gajae-code/tui";
7
7
  import { logger } from "@gajae-code/utils";
8
8
  import type { ModelRegistry } from "../../config/model-registry";
9
+ import type { WorkflowGateEmitter } from "../../modes/shared/agent-wire/unattended-session";
9
10
  import { type Theme, theme } from "../../modes/theme/theme";
10
11
  import type { SessionManager } from "../../session/session-manager";
11
12
  import type {
@@ -180,6 +181,7 @@ export class ExtensionRunner {
180
181
  #getContextUsageFn: () => ContextUsage | undefined = () => undefined;
181
182
  #compactFn: (instructionsOrOptions?: string | CompactOptions) => Promise<void> = async () => {};
182
183
  #getSystemPromptFn: () => string[] = () => [];
184
+ #getWorkflowGateFn: () => WorkflowGateEmitter | undefined = () => undefined;
183
185
  #newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
184
186
  #branchHandler: BranchHandler = async () => ({ cancelled: false });
185
187
  #navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
@@ -234,6 +236,7 @@ export class ExtensionRunner {
234
236
  this.#hasPendingMessagesFn = contextActions.hasPendingMessages;
235
237
  this.#shutdownHandler = contextActions.shutdown;
236
238
  this.#getSystemPromptFn = contextActions.getSystemPrompt;
239
+ this.#getWorkflowGateFn = contextActions.getWorkflowGate ?? (() => undefined);
237
240
 
238
241
  // Command context actions (optional, only for interactive mode)
239
242
  if (commandContextActions) {
@@ -463,6 +466,7 @@ export class ExtensionRunner {
463
466
  shutdown: () => this.#shutdownHandler(),
464
467
  getSystemPrompt: () => this.#getSystemPromptFn(),
465
468
  hasQueuedMessages: () => this.#hasPendingMessagesFn(), // deprecated alias
469
+ workflowGate: this.#getWorkflowGateFn(),
466
470
  };
467
471
  }
468
472
 
@@ -32,6 +32,7 @@ import type { PythonResult } from "../../eval/py/executor";
32
32
  import type { BashResult } from "../../exec/bash-executor";
33
33
  import type { ExecOptions, ExecResult } from "../../exec/exec";
34
34
  import type { CustomEditor } from "../../modes/components/custom-editor";
35
+ import type { WorkflowGateEmitter } from "../../modes/shared/agent-wire/unattended-session";
35
36
  import type { Theme } from "../../modes/theme/theme";
36
37
  import type { CustomMessage } from "../../session/messages";
37
38
  import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
@@ -310,6 +311,11 @@ export interface ExtensionContext {
310
311
  getSystemPrompt(): string[];
311
312
  /** @deprecated Use hasPendingMessages() instead */
312
313
  hasQueuedMessages(): boolean;
314
+ /**
315
+ * Unattended workflow-gate bridge. Present only when the session runs in
316
+ * unattended/RPC mode; `undefined` in interactive/TUI mode (notify-only).
317
+ */
318
+ workflowGate?: WorkflowGateEmitter;
313
319
  }
314
320
 
315
321
  /**
@@ -1234,6 +1240,8 @@ export interface ExtensionContextActions {
1234
1240
  getContextUsage: () => ContextUsage | undefined;
1235
1241
  compact: (instructionsOrOptions?: string | CompactOptions) => Promise<void>;
1236
1242
  getSystemPrompt: () => string[];
1243
+ /** Unattended workflow-gate bridge (present only in unattended/RPC mode). */
1244
+ getWorkflowGate?: () => WorkflowGateEmitter | undefined;
1237
1245
  }
1238
1246
 
1239
1247
  /** Actions for ExtensionCommandContext (ctx.* in command handlers). */