@sayknow-cli/coding-agent 0.2.6 → 0.3.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 (145) hide show
  1. package/CHANGELOG.md +32 -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 +71 -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/defaults/skc-defaults.d.ts +13 -2
  18. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  19. package/dist/types/extensibility/plugins/index.d.ts +1 -0
  20. package/dist/types/extensibility/plugins/security-scanner.d.ts +31 -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/skc-runtime/launch-tmux.d.ts +1 -0
  47. package/dist/types/skc-runtime/state-writer.d.ts +2 -0
  48. package/dist/types/skc-runtime/tmux-common.d.ts +3 -0
  49. package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
  50. package/dist/types/skc-runtime/ultragoal-guard.d.ts +15 -0
  51. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +14 -0
  52. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  53. package/dist/types/tools/index.d.ts +18 -0
  54. package/dist/types/tools/subagent.d.ts +3 -0
  55. package/package.json +7 -7
  56. package/scripts/build-binary.ts +3 -0
  57. package/src/async/job-manager.ts +5 -1
  58. package/src/cli/daemon-cli.ts +122 -0
  59. package/src/cli/notify-cli.ts +274 -0
  60. package/src/cli/plugin-cli.ts +63 -6
  61. package/src/cli/setup-cli.ts +173 -84
  62. package/src/cli.ts +3 -3
  63. package/src/commands/daemon.ts +47 -0
  64. package/src/commands/notify.ts +61 -0
  65. package/src/commands/setup.ts +11 -1
  66. package/src/config/model-profile-activation.ts +74 -5
  67. package/src/config/model-profiles.ts +7 -4
  68. package/src/config/model-registry.ts +6 -3
  69. package/src/config/models-config-schema.ts +1 -1
  70. package/src/config/settings-schema.ts +61 -0
  71. package/src/coordinator/contract.ts +3 -0
  72. package/src/coordinator-mcp/server.ts +270 -1
  73. package/src/daemon/builtin.ts +46 -0
  74. package/src/daemon/control-types.ts +65 -0
  75. package/src/daemon/runtime.ts +51 -0
  76. package/src/defaults/skc/rules/ponytail.md +68 -0
  77. package/src/defaults/skc/skills/ultragoal/SKILL.md +16 -0
  78. package/src/defaults/skc-defaults.ts +27 -2
  79. package/src/edit/modes/replace.ts +1 -1
  80. package/src/extensibility/extensions/runner.ts +4 -0
  81. package/src/extensibility/extensions/types.ts +8 -0
  82. package/src/extensibility/plugins/index.ts +1 -0
  83. package/src/extensibility/plugins/manager.ts +50 -0
  84. package/src/extensibility/plugins/marketplace/manager.ts +42 -0
  85. package/src/extensibility/plugins/security-scanner.ts +477 -0
  86. package/src/goals/tools/goal-tool.ts +11 -2
  87. package/src/hashline/hash.ts +1 -1
  88. package/src/internal-urls/docs-index.generated.ts +10 -8
  89. package/src/main.ts +30 -0
  90. package/src/modes/acp/acp-event-mapper.ts +1 -0
  91. package/src/modes/components/hook-editor.ts +7 -2
  92. package/src/modes/components/oauth-selector.ts +19 -0
  93. package/src/modes/controllers/event-controller.ts +20 -0
  94. package/src/modes/controllers/selector-controller.ts +80 -17
  95. package/src/modes/interactive-mode.ts +6 -2
  96. package/src/modes/runtime-init.ts +1 -0
  97. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  98. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  99. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  100. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  101. package/src/modes/types.ts +7 -1
  102. package/src/modes/utils/ui-helpers.ts +23 -0
  103. package/src/notifications/config-commands.ts +50 -0
  104. package/src/notifications/config.ts +107 -0
  105. package/src/notifications/helpers.ts +135 -0
  106. package/src/notifications/html-format.ts +389 -0
  107. package/src/notifications/index.ts +700 -0
  108. package/src/notifications/rate-limit-pool.ts +179 -0
  109. package/src/notifications/telegram-cli.ts +194 -0
  110. package/src/notifications/telegram-daemon-cli.ts +74 -0
  111. package/src/notifications/telegram-daemon-control.ts +370 -0
  112. package/src/notifications/telegram-daemon.ts +1370 -0
  113. package/src/notifications/telegram-reference.ts +335 -0
  114. package/src/notifications/threaded-inbound.ts +80 -0
  115. package/src/notifications/threaded-render.ts +155 -0
  116. package/src/notifications/topic-registry.ts +133 -0
  117. package/src/rlm/index.ts +19 -0
  118. package/src/sdk.ts +16 -0
  119. package/src/session/agent-session.ts +113 -3
  120. package/src/session/auth-storage.ts +3 -0
  121. package/src/session/session-dump-format.ts +43 -2
  122. package/src/session/session-manager.ts +39 -5
  123. package/src/setup/credential-auto-import.ts +258 -0
  124. package/src/setup/credential-import.ts +17 -0
  125. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  126. package/src/setup/host-plugin-setup.ts +142 -0
  127. package/src/skc-runtime/deep-interview-recorder.ts +2 -2
  128. package/src/skc-runtime/launch-tmux.ts +10 -2
  129. package/src/skc-runtime/state-runtime.ts +18 -10
  130. package/src/skc-runtime/state-writer.ts +8 -8
  131. package/src/skc-runtime/tmux-common.ts +8 -0
  132. package/src/skc-runtime/tmux-sessions.ts +8 -1
  133. package/src/skc-runtime/ultragoal-guard.ts +57 -2
  134. package/src/skc-runtime/ultragoal-runtime.ts +105 -19
  135. package/src/skc-runtime/workflow-manifest.generated.json +27 -2
  136. package/src/skc-runtime/workflow-manifest.ts +11 -1
  137. package/src/slash-commands/builtin-registry.ts +4 -1
  138. package/src/task/executor.ts +5 -1
  139. package/src/tools/ask-answer-registry.ts +25 -0
  140. package/src/tools/ask.ts +77 -6
  141. package/src/tools/image-gen.ts +5 -8
  142. package/src/tools/index.ts +19 -0
  143. package/src/tools/inspect-image.ts +16 -11
  144. package/src/tools/subagent-render.ts +7 -0
  145. 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 SkcModelAssignmentTargetId = "default" | "executor" | "architect" | "planner" | "critic";
88
+ export type SkcModelAssignmentTargetId = "default" | "vision" | "executor" | "architect" | "planner" | "critic";
88
89
 
89
90
  export interface SkcModelAssignmentTargetInfo extends ModelRoleInfo {
90
91
  id: SkcModelAssignmentTargetId;
@@ -93,6 +94,7 @@ export interface SkcModelAssignmentTargetInfo extends ModelRoleInfo {
93
94
 
94
95
  export const SKC_MODEL_ASSIGNMENT_TARGET_IDS: SkcModelAssignmentTargetId[] = [
95
96
  "default",
97
+ "vision",
96
98
  "executor",
97
99
  "architect",
98
100
  "planner",
@@ -101,6 +103,7 @@ export const SKC_MODEL_ASSIGNMENT_TARGET_IDS: SkcModelAssignmentTargetId[] = [
101
103
 
102
104
  export const SKC_MODEL_ASSIGNMENT_TARGETS: Record<SkcModelAssignmentTargetId, SkcModelAssignmentTargetInfo> = {
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;
@@ -255,6 +255,22 @@ export const SETTINGS_SCHEMA = {
255
255
  "auth.broker.url": { type: "string", default: undefined },
256
256
  "auth.broker.token": { type: "string", default: undefined },
257
257
 
258
+ // Notifications (Telegram bundled reference client)
259
+ "notifications.enabled": { type: "boolean", default: false },
260
+ "notifications.telegram.botToken": { type: "string", default: undefined },
261
+ "notifications.telegram.chatId": { type: "string", default: undefined },
262
+ "notifications.redact": { type: "boolean", default: false },
263
+ "notifications.verbosity": {
264
+ type: "string",
265
+ default: "lean",
266
+ validate: (value: string) => value === "lean" || value === "verbose",
267
+ },
268
+ "notifications.daemon.idleTimeoutMs": {
269
+ type: "number",
270
+ default: 60000,
271
+ validate: (value: number) => Number.isFinite(value) && value > 0,
272
+ },
273
+
258
274
  autoResume: {
259
275
  type: "boolean",
260
276
  default: false,
@@ -2970,6 +2986,38 @@ export const SETTINGS_SCHEMA = {
2970
2986
  "thinkingBudgets.xhigh": { type: "number", default: 32768 },
2971
2987
 
2972
2988
  "thinkingBudgets.max": { type: "number", default: 65536 },
2989
+
2990
+ // ────────────────────────────────────────────────────────────────────────
2991
+ // Plugins — security scanner
2992
+ // ────────────────────────────────────────────────────────────────────────
2993
+
2994
+ "plugins.security.scanMode": {
2995
+ type: "enum",
2996
+ values: ["warn", "off", "block"] as const,
2997
+ default: "warn",
2998
+ ui: {
2999
+ tab: "interaction",
3000
+ label: "Plugin Security Scan Mode",
3001
+ description:
3002
+ "warn: scan on install and log advisory findings (default); off: skip scan; block: deny install when score exceeds threshold",
3003
+ options: [
3004
+ { value: "warn", label: "Warn", description: "Scan and log advisory findings (default)" },
3005
+ { value: "off", label: "Off", description: "Disable security scanning" },
3006
+ { value: "block", label: "Block", description: "Deny install when risk score exceeds threshold" },
3007
+ ],
3008
+ },
3009
+ },
3010
+
3011
+ "plugins.security.riskThreshold": {
3012
+ type: "number",
3013
+ default: 60,
3014
+ validate: (value: number) => Number.isFinite(value) && value >= 0 && value <= 100,
3015
+ ui: {
3016
+ tab: "interaction",
3017
+ label: "Plugin Security Risk Threshold",
3018
+ description: "Score threshold (0–100) above which block mode denies install. Default 60.",
3019
+ },
3020
+ },
2973
3021
  } as const;
2974
3022
 
2975
3023
  // ═══════════════════════════════════════════════════════════════════════════
@@ -3177,6 +3225,18 @@ export interface ShellMinimizerSettings {
3177
3225
  maxCaptureBytes: number;
3178
3226
  }
3179
3227
 
3228
+ export interface NotificationsSettings {
3229
+ enabled: boolean;
3230
+ telegram: {
3231
+ botToken: string | undefined;
3232
+ chatId: string | undefined;
3233
+ };
3234
+ redact: boolean;
3235
+ daemon: {
3236
+ idleTimeoutMs: number;
3237
+ };
3238
+ }
3239
+
3180
3240
  /** Map group prefix -> typed settings interface */
3181
3241
  export interface GroupTypeMap {
3182
3242
  compaction: CompactionSettings;
@@ -3195,6 +3255,7 @@ export interface GroupTypeMap {
3195
3255
  modelTags: ModelTagsSettings;
3196
3256
  cycleOrder: string[];
3197
3257
  shellMinimizer: ShellMinimizerSettings;
3258
+ notifications: NotificationsSettings;
3198
3259
  }
3199
3260
 
3200
3261
  export type GroupPrefix = keyof GroupTypeMap;
@@ -17,6 +17,9 @@ export const COORDINATOR_MCP_TOOL_NAMES = [
17
17
  "skc_coordinator_read_turn",
18
18
  "skc_coordinator_await_turn",
19
19
  "skc_coordinator_report_status",
20
+ "skc_delegate_plan",
21
+ "skc_delegate_execute",
22
+ "skc_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 SKC 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 SKC 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 skc_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 SKC 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 "skc_delegate_plan":
467
+ return "plan";
468
+ case "skc_delegate_execute":
469
+ return "execute";
470
+ case "skc_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 SKC: start a session and run /skill:ralplan to completion, returning durable turn status and artifact references.";
492
+ case "execute":
493
+ return "Delegate execution to SKC: 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 SKC: 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 SKC 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 skc_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 === "skc_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 `skc 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 SkcRuntimeSpawnInfo {
12
+ execPath: string;
13
+ mode: "source" | "compiled";
14
+ /** Prefix prepended before the skc 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 skc 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 resolveSkcRuntimeSpawnInfo(execPath: string = process.execPath): SkcRuntimeSpawnInfo {
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
+ }
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: ponytail
3
+ description: "Lazy senior dev mode — always pick the simplest, shortest solution that works (YAGNI, reuse, stdlib/native first), never cutting validation, error handling, security, or accessibility."
4
+ alwaysApply: true
5
+ license: MIT
6
+ source: https://github.com/DietrichGebert/ponytail
7
+ ---
8
+
9
+ # Ponytail — lazy senior dev mode
10
+
11
+ You write code like the laziest senior dev in the room. Lazy means efficient,
12
+ not careless. The best code is the code you never wrote. This rule governs
13
+ *what* you build, not how you talk.
14
+
15
+ ## The ladder
16
+
17
+ After you understand the problem, stop at the first rung that holds:
18
+
19
+ 1. **Does this need to exist at all?** Speculative need → skip it, say so in one line (YAGNI).
20
+ 2. **Already in this codebase?** A helper, util, type, or pattern that lives here → reuse it. Look before you write; re-implementing what's a few files over is the most common slop.
21
+ 3. **Stdlib does it?** Use it.
22
+ 4. **Native platform feature covers it?** `<input type="date">` over a picker lib, CSS over JS, DB constraint over app code.
23
+ 5. **Already-installed dependency solves it?** Use it. Never add a new dependency for what a few lines can do.
24
+ 6. **Can it be one line?** One line.
25
+ 7. **Only then:** the minimum code that works.
26
+
27
+ The ladder runs *after* comprehension, not instead of it. Read the task and the
28
+ code it touches, trace the real flow end to end, then climb. Two rungs work →
29
+ take the higher one and move on.
30
+
31
+ **Bug fix = root cause, not symptom.** Grep every caller of the function you are
32
+ about to touch. One guard in the shared function is a smaller diff than a guard
33
+ in every caller — and patching only the path the ticket names leaves every
34
+ sibling caller broken.
35
+
36
+ ## Rules
37
+
38
+ - No unrequested abstractions: no interface with one implementation, no factory for one product, no config for a value that never changes.
39
+ - No boilerplate or scaffolding "for later" — later can scaffold for itself.
40
+ - Deletion over addition. Boring over clever; clever is what someone decodes at 3am.
41
+ - Fewest files possible. Shortest working diff wins — but only once you understand the problem. The smallest change in the wrong place is a second bug, not laziness.
42
+ - Two stdlib options the same size? Take the one that is correct on edge cases.
43
+ - Mark deliberate simplifications with a `ponytail:` comment that names the ceiling and the upgrade path (e.g. `// ponytail: global lock, per-account locks if throughput matters`).
44
+
45
+ ## Output
46
+
47
+ Code first. Then at most three short lines: what was skipped, when to add it.
48
+ No essays defending a simplification — every paragraph of defense is complexity
49
+ smuggled back in as prose. Explanation the user explicitly asked for (a report,
50
+ a walkthrough) is not debt; give it in full.
51
+
52
+ Pattern: `[code] → skipped: [X], add when [Y].`
53
+
54
+ ## When NOT to be lazy
55
+
56
+ Never simplify away: input validation at trust boundaries, error handling that
57
+ prevents data loss, security measures, accessibility basics, or anything
58
+ explicitly requested. Never be lazy about *understanding* the problem — the
59
+ ladder shortens the solution, never the reading. A confident small diff in the
60
+ wrong place is the dangerous kind of lazy.
61
+
62
+ Non-trivial logic (a branch, loop, parser, money/security path) leaves ONE
63
+ runnable check behind — the smallest thing that fails if the logic breaks. No
64
+ frameworks or fixtures unless asked. Trivial one-liners need no test.
65
+
66
+ ---
67
+
68
+ Adapted from [ponytail](https://github.com/DietrichGebert/ponytail) (MIT) by DietrichGebert.