@gajae-code/coding-agent 0.12.7 → 0.12.10

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 (117) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/types/cli/notify-cli.d.ts +1 -0
  3. package/dist/types/commands/notify.d.ts +4 -0
  4. package/dist/types/commands/sdk.d.ts +23 -0
  5. package/dist/types/config/file-lock.d.ts +3 -0
  6. package/dist/types/config/model-discovery-manager.d.ts +6 -1
  7. package/dist/types/config/model-registry.d.ts +2 -0
  8. package/dist/types/config/resolve-config-value.d.ts +2 -1
  9. package/dist/types/config/settings-schema.d.ts +17 -1
  10. package/dist/types/discovery/agents-md.d.ts +11 -1
  11. package/dist/types/main.d.ts +1 -0
  12. package/dist/types/modes/components/hook-selector.d.ts +20 -0
  13. package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
  14. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  15. package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
  16. package/dist/types/modes/types.d.ts +6 -0
  17. package/dist/types/sdk/broker/broker.d.ts +5 -1
  18. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  19. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  20. package/dist/types/sdk/bus/config.d.ts +75 -113
  21. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  22. package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
  23. package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
  24. package/dist/types/sdk/bus/index.d.ts +1 -1
  25. package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
  26. package/dist/types/sdk/bus/notification-service.d.ts +54 -4
  27. package/dist/types/sdk/bus/session-control.d.ts +8 -3
  28. package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
  29. package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
  30. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  31. package/dist/types/sdk/client/discovery.d.ts +2 -1
  32. package/dist/types/sdk/host/query/handlers.d.ts +2 -0
  33. package/dist/types/sdk/index.d.ts +1 -0
  34. package/dist/types/sdk/providers.d.ts +22 -0
  35. package/dist/types/sdk/session-directory.d.ts +1 -1
  36. package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
  37. package/dist/types/session/internal/managed-session-storage.d.ts +19 -1
  38. package/dist/types/session/internal/native-publish-outcome.d.ts +3 -2
  39. package/dist/types/session/internal/session-open-errors.d.ts +5 -0
  40. package/dist/types/session/session-manager.d.ts +3 -1
  41. package/dist/types/session/session-storage.d.ts +2 -0
  42. package/dist/types/system-prompt.d.ts +12 -1
  43. package/dist/types/tools/composer-bash-policy.d.ts +9 -1
  44. package/package.json +10 -8
  45. package/scripts/build-sdk-package-smoke.ts +51 -0
  46. package/scripts/capture-notifications-settings-showcase.ts +1 -1
  47. package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
  48. package/src/cli/notify-cli.ts +215 -99
  49. package/src/commands/notify.ts +19 -8
  50. package/src/commands/sdk.ts +54 -0
  51. package/src/config/file-lock.ts +6 -0
  52. package/src/config/model-discovery-manager.ts +50 -8
  53. package/src/config/model-profiles.ts +8 -0
  54. package/src/config/model-registry.ts +866 -76
  55. package/src/config/resolve-config-value.ts +10 -8
  56. package/src/config/settings-schema.ts +25 -15
  57. package/src/config/settings.ts +15 -15
  58. package/src/discovery/agents-md.ts +76 -19
  59. package/src/gjc-runtime/psmux-detect.ts +12 -16
  60. package/src/gjc-runtime/session-state-sidecar.ts +12 -2
  61. package/src/gjc-runtime/tmux-sessions.ts +8 -1
  62. package/src/internal-urls/docs-index.generated.ts +10 -9
  63. package/src/main.ts +43 -7
  64. package/src/modes/DESIGN.md +11 -11
  65. package/src/modes/components/extensions/inspector-panel.ts +9 -5
  66. package/src/modes/components/hook-selector.ts +155 -44
  67. package/src/modes/components/model-selector.ts +242 -49
  68. package/src/modes/components/notifications-settings-editor.ts +631 -85
  69. package/src/modes/controllers/extension-ui-controller.ts +114 -15
  70. package/src/modes/controllers/selector-controller.ts +525 -93
  71. package/src/modes/print-mode.ts +3 -0
  72. package/src/modes/types.ts +6 -0
  73. package/src/prompts/system/system-prompt.md +1 -0
  74. package/src/sdk/broker/broker.ts +54 -10
  75. package/src/sdk/broker/lifecycle-ledger.ts +34 -1
  76. package/src/sdk/broker/lifecycle.ts +47 -50
  77. package/src/sdk/bus/chat-daemon-cli.ts +12 -5
  78. package/src/sdk/bus/chat-daemon-control.ts +35 -6
  79. package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
  80. package/src/sdk/bus/config.ts +534 -286
  81. package/src/sdk/bus/daemon-paths.ts +2 -0
  82. package/src/sdk/bus/discord-live-provider.ts +57 -2
  83. package/src/sdk/bus/discord-provider.ts +21 -0
  84. package/src/sdk/bus/index.ts +97 -35
  85. package/src/sdk/bus/notification-orchestration.ts +371 -68
  86. package/src/sdk/bus/notification-service.ts +571 -137
  87. package/src/sdk/bus/session-control.ts +119 -44
  88. package/src/sdk/bus/slack-live-provider.ts +78 -2
  89. package/src/sdk/bus/slack-provider.ts +24 -0
  90. package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
  91. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  92. package/src/sdk/bus/telegram-daemon-control.ts +3 -3
  93. package/src/sdk/bus/telegram-daemon.ts +23 -15
  94. package/src/sdk/client/discovery.ts +10 -4
  95. package/src/sdk/host/query/handlers.ts +20 -1
  96. package/src/sdk/index.ts +1 -0
  97. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  98. package/src/sdk/protocol/operation-registry.ts +5 -2
  99. package/src/sdk/providers.ts +51 -0
  100. package/src/sdk/session-directory.ts +1 -0
  101. package/src/sdk/session.ts +34 -14
  102. package/src/session/agent-session.ts +109 -37
  103. package/src/session/internal/managed-session-scope.ts +981 -450
  104. package/src/session/internal/managed-session-storage.ts +418 -47
  105. package/src/session/internal/native-publish-outcome.ts +15 -3
  106. package/src/session/internal/session-open-errors.ts +11 -0
  107. package/src/session/session-manager.ts +27 -3
  108. package/src/session/session-storage.ts +72 -5
  109. package/src/slash-commands/builtin-registry.ts +117 -15
  110. package/src/system-prompt.ts +28 -15
  111. package/src/task/index.ts +9 -3
  112. package/src/tools/ask.ts +1 -1
  113. package/src/tools/bash.ts +4 -2
  114. package/src/tools/composer-bash-policy.ts +21 -5
  115. package/src/tools/telegram-send.ts +4 -2
  116. package/src/utils/command-args.ts +19 -30
  117. package/src/utils/lang-from-path.ts +2 -4
@@ -9,6 +9,7 @@ const packageName = "@gajae-code/coding-agent";
9
9
  const aiPackageDir = path.resolve(packageDir, "../ai");
10
10
  const bridgeClientPackageDir = path.resolve(packageDir, "../bridge-client");
11
11
  const tuiPackageDir = path.resolve(packageDir, "../tui");
12
+ const agentPackageDir = path.resolve(packageDir, "../agent");
12
13
  const nativesPackageDir = path.resolve(packageDir, "../natives");
13
14
  const linuxX64PackageDir = path.resolve(packageDir, "../natives-linux-x64");
14
15
  const manifestsDir = path.join(packageDir, "test/manifests");
@@ -41,6 +42,7 @@ async function runSmoke(): Promise<Surface> {
41
42
  await fs.copyFile(path.join(nativesPackageDir, "native", entry), path.join(stagedNativeDir, entry));
42
43
  }
43
44
  }
45
+ const agentTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], agentPackageDir);
44
46
  const aiTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], aiPackageDir);
45
47
  const bridgeClientTarball = run(
46
48
  ["bun", "pm", "pack", "--destination", tempDir, "--quiet"],
@@ -49,6 +51,7 @@ async function runSmoke(): Promise<Surface> {
49
51
  const tuiTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], tuiPackageDir);
50
52
  const nativesTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], nativesPackageDir);
51
53
  const linuxX64Tarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], stagedLinuxX64Dir);
54
+ const agentTarballPath = path.isAbsolute(agentTarball) ? agentTarball : path.join(agentPackageDir, agentTarball);
52
55
  const codingAgentTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], packageDir);
53
56
  const aiTarballPath = path.isAbsolute(aiTarball) ? aiTarball : path.join(aiPackageDir, aiTarball);
54
57
  const bridgeClientTarballPath = path.isAbsolute(bridgeClientTarball)
@@ -71,6 +74,7 @@ async function runSmoke(): Promise<Surface> {
71
74
  name: "sdk-smoke",
72
75
  private: true,
73
76
  dependencies: {
77
+ "@gajae-code/agent-core": `file:${agentTarballPath}`,
74
78
  "@gajae-code/ai": `file:${aiTarballPath}`,
75
79
  "@gajae-code/bridge-client": `file:${bridgeClientTarballPath}`,
76
80
  [packageName]: `file:${codingAgentTarballPath}`,
@@ -79,6 +83,7 @@ async function runSmoke(): Promise<Surface> {
79
83
  "@gajae-code/natives-linux-x64": `file:${linuxX64TarballPath}`,
80
84
  },
81
85
  overrides: {
86
+ "@gajae-code/agent-core": `file:${agentTarballPath}`,
82
87
  "@gajae-code/ai": `file:${aiTarballPath}`,
83
88
  "@gajae-code/bridge-client": `file:${bridgeClientTarballPath}`,
84
89
  "@gajae-code/tui": `file:${tuiTarballPath}`,
@@ -96,6 +101,52 @@ async function runSmoke(): Promise<Surface> {
96
101
  const installedPackage = JSON.parse(
97
102
  await fs.readFile(path.join(tempDir, "node_modules", packageName, "package.json"), "utf8"),
98
103
  ) as { exports?: Record<string, unknown> };
104
+ const installedAgentPackagePath = path.join(tempDir, "node_modules", "@gajae-code", "agent-core");
105
+ const installedAgentPackageJsonPath = path.join(installedAgentPackagePath, "package.json");
106
+ const installedAgentPackage = JSON.parse(await fs.readFile(installedAgentPackageJsonPath, "utf8")) as {
107
+ name?: string;
108
+ version?: string;
109
+ };
110
+ const expectedAgentPackage = JSON.parse(
111
+ await fs.readFile(path.join(agentPackageDir, "package.json"), "utf8"),
112
+ ) as {
113
+ name?: string;
114
+ version?: string;
115
+ };
116
+ const installedAgentRealpath = await fs.realpath(installedAgentPackagePath);
117
+ const tempRealpath = await fs.realpath(tempDir);
118
+ const sourceAgentRealpath = await fs.realpath(agentPackageDir);
119
+ if (
120
+ !installedAgentRealpath.startsWith(`${tempRealpath}${path.sep}`) ||
121
+ installedAgentRealpath.startsWith(`${sourceAgentRealpath}${path.sep}`)
122
+ ) {
123
+ throw new Error("packed smoke resolved agent-core outside the temporary packed installation");
124
+ }
125
+ const packedAgentInspectDir = path.join(tempDir, "packed-agent-inspect");
126
+ await fs.mkdir(packedAgentInspectDir);
127
+ const extract = Bun.spawnSync(["tar", "xzf", agentTarballPath, "-C", packedAgentInspectDir], {
128
+ stdout: "pipe",
129
+ stderr: "pipe",
130
+ });
131
+ if (extract.exitCode !== 0)
132
+ throw new Error(`could not inspect packed agent-core tarball: ${extract.stderr.toString()}`);
133
+ const packedAgentPackageJson = await fs.readFile(
134
+ path.join(packedAgentInspectDir, "package", "package.json"),
135
+ "utf8",
136
+ );
137
+ const installedAgentPackageJson = await fs.readFile(installedAgentPackageJsonPath, "utf8");
138
+ if (installedAgentPackageJson !== packedAgentPackageJson) {
139
+ throw new Error("packed smoke installed agent-core content different from the packed tarball");
140
+ }
141
+ if (
142
+ installedAgentPackage.name !== expectedAgentPackage.name ||
143
+ installedAgentPackage.version !== expectedAgentPackage.version
144
+ ) {
145
+ throw new Error("packed smoke installed a mismatched agent-core package");
146
+ }
147
+ if (installedAgentPackage.name !== "@gajae-code/agent-core") {
148
+ throw new Error("packed smoke agent-core package identity is invalid");
149
+ }
99
150
  if (installedPackage.exports?.["./session/internal/*"] !== null) {
100
151
  throw new Error("packed package must explicitly block ./session/internal/*");
101
152
  }
@@ -11,7 +11,7 @@ import {
11
11
  } from "../test/fixtures/tui/notifications-settings-showcase";
12
12
 
13
13
  const CANONICAL_COMMAND =
14
- "bun packages/coding-agent/scripts/capture-notifications-settings-showcase.ts --output .gjc/qa/issue-2050-notifications";
14
+ "bun packages/coding-agent/scripts/capture-notifications-settings-showcase.ts --output .gjc/qa/issue-3570-notifications";
15
15
  const DETERMINISTIC_CAPTURE_TIMESTAMP = "1970-01-01T00:00:00.000Z";
16
16
  const CAPTURE_TOOL_VERSION = "notifications-settings-showcase-live-settings-selector-v3";
17
17
 
@@ -1531,13 +1531,26 @@ function exactTeamRuntimeSendKeysRanges(contents: string): ShellRange[] {
1531
1531
  return [];
1532
1532
 
1533
1533
  const continuationBody = contents.slice(continuationRange.start, continuationRange.end);
1534
+ // The frozen argv may address the pane either through `worker.pane_id` directly or
1535
+ // through a local binding that was proven non-empty first (the optional field does
1536
+ // not narrow for the type checker). Either way both send operations must name the
1537
+ // identical pane token, and a local token must come from a checked `worker.pane_id`.
1534
1538
  const continuationArgs =
1535
- /const\s+args\s*=\s*Object\.freeze\(\s*\[\s*["']send-keys["']\s*,\s*["']-l["']\s*,\s*["']-t["']\s*,\s*worker\.pane_id\s*,\s*continuationPrompt\s*,\s*["'];["']\s*,\s*["']send-keys["']\s*,\s*["']-t["']\s*,\s*worker\.pane_id\s*,\s*["']Enter["']\s*,?\s*\]\s*\)\s*;/.exec(
1539
+ /const\s+args(?:\s*:\s*readonly\s+string\[\])?\s*=\s*Object\.freeze\(\s*\[\s*["']send-keys["']\s*,\s*["']-l["']\s*,\s*["']-t["']\s*,\s*(worker\.pane_id|[A-Za-z_$][\w$]*)\s*,\s*continuationPrompt\s*,\s*["'];["']\s*,\s*["']send-keys["']\s*,\s*["']-t["']\s*,\s*(worker\.pane_id|[A-Za-z_$][\w$]*)\s*,\s*["']Enter["']\s*,?\s*\]\s*\)\s*;/.exec(
1536
1540
  continuationBody,
1537
1541
  );
1542
+ const paneToken = continuationArgs?.[1];
1543
+ const paneTokenIsProvenLocal =
1544
+ paneToken !== undefined &&
1545
+ paneToken !== "worker.pane_id" &&
1546
+ new RegExp(`const\\s+${paneToken}\\s*=\\s*worker\\.pane_id\\s*;`).test(continuationBody) &&
1547
+ new RegExp(`if\\s*\\(\\s*!${paneToken}\\s*\\)\\s*return\\b`).test(continuationBody);
1538
1548
  if (
1539
1549
  !continuationArgs ||
1550
+ continuationArgs[1] !== continuationArgs[2] ||
1551
+ !(paneToken === "worker.pane_id" || paneTokenIsProvenLocal) ||
1540
1552
  !continuationBody.includes("const revalidationReason =") ||
1553
+ !/if\s*\(\s*revalidationReason\s*\)\s*(?:\{\s*return\b[^}]*;?\s*\}|return\b[^;]*;)/.test(continuationBody) ||
1541
1554
  !continuationBody.includes("await createJsonNoClobber(") ||
1542
1555
  !continuationBody.includes('type: "literal-send"') ||
1543
1556
  !continuationBody.includes('type: "key-send"') ||
@@ -7,11 +7,18 @@ import { createInterface } from "node:readline/promises";
7
7
  import { APP_NAME } from "@gajae-code/utils/dirs";
8
8
  import chalk from "chalk";
9
9
  import { Settings, type SettingsAtomicPatch } from "../config/settings";
10
- import { type EnsureChatDaemonResult, ensureDiscordDaemon, ensureSlackDaemon } from "../sdk/bus/chat-daemon-control";
10
+ import {
11
+ ChatDaemonController,
12
+ type EnsureChatDaemonResult,
13
+ ensureDiscordDaemon,
14
+ ensureSlackDaemon,
15
+ } from "../sdk/bus/chat-daemon-control";
11
16
  import { getNotificationConfig, maskToken, tokenFingerprint } from "../sdk/bus/config";
12
17
  import {
13
18
  clearTelegramActivationMarker,
14
19
  createTelegramActivationMarker,
20
+ mutateNotificationProvider,
21
+ type NotificationProviderRuntimeAuthority,
15
22
  observedTelegramActivationMarker,
16
23
  type ProposedTelegramIdentity,
17
24
  persistTelegramActivationMarker,
@@ -35,6 +42,7 @@ import {
35
42
  resolveTelegramSetupPreflight,
36
43
  } from "../sdk/bus/telegram-daemon";
37
44
  import { runDaemonInternal } from "../sdk/bus/telegram-daemon-cli";
45
+ import { TelegramDaemonController } from "../sdk/bus/telegram-daemon-control";
38
46
  import {
39
47
  runTelegramSetup as runTelegramPairingSetup,
40
48
  type TelegramSetupPreflight,
@@ -61,6 +69,7 @@ export interface NotifyCommandArgs {
61
69
  slackChannelId?: string;
62
70
  slackAuthorizedUserId?: string;
63
71
  redact?: boolean;
72
+ forceDaemonLock?: boolean;
64
73
  probe?: boolean;
65
74
  message?: string;
66
75
  }
@@ -93,75 +102,104 @@ export interface NotifyCommandDeps {
93
102
  }
94
103
 
95
104
  export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
96
- if (args.length === 0 || args[0] !== "notify") {
97
- return undefined;
98
- }
99
-
105
+ if (args.length === 0 || args[0] !== "notify") return undefined;
100
106
  const action = args[1];
101
- if (action === "setup" || action === "status") {
107
+ const providerValue = (value: string | undefined): NotifySetupProvider | undefined =>
108
+ value === "telegram" || value === "discord" || value === "slack" ? value : undefined;
109
+ const parseFlags = (
110
+ rest: string[],
111
+ valueFlags: ReadonlySet<string>,
112
+ booleanFlags: ReadonlySet<string>,
113
+ ): Map<string, string | true> | undefined => {
114
+ const parsed = new Map<string, string | true>();
115
+ for (let index = 0; index < rest.length; index++) {
116
+ const flag = rest[index];
117
+ if (!flag?.startsWith("--") || parsed.has(flag)) return undefined;
118
+ if (booleanFlags.has(flag)) {
119
+ parsed.set(flag, true);
120
+ continue;
121
+ }
122
+ if (!valueFlags.has(flag)) return undefined;
123
+ const value = rest[++index];
124
+ if (!value || value.startsWith("--")) return undefined;
125
+ parsed.set(flag, value);
126
+ }
127
+ return parsed;
128
+ };
129
+
130
+ if (action === "setup") {
102
131
  const rest = args.slice(2);
103
- const flag = (name: string): string | undefined => {
104
- const i = rest.indexOf(name);
105
- return i >= 0 ? rest[i + 1] : undefined;
132
+ const positional = rest[0]?.startsWith("--") ? undefined : rest.shift();
133
+ const provider = positional === undefined ? undefined : providerValue(positional);
134
+ if (positional !== undefined && !provider) return undefined;
135
+ const flags = parseFlags(
136
+ rest,
137
+ new Set([
138
+ "--token",
139
+ "--chat-id",
140
+ "--discord-bot-token",
141
+ "--discord-application-id",
142
+ "--discord-guild-id",
143
+ "--discord-parent-channel-id",
144
+ "--slack-bot-token",
145
+ "--slack-app-token",
146
+ "--slack-workspace-id",
147
+ "--slack-channel-id",
148
+ "--slack-authorized-user-id",
149
+ ]),
150
+ new Set(["--redact"]),
151
+ );
152
+ if (!flags) return undefined;
153
+ const value = (name: string): string | undefined => {
154
+ const found = flags.get(name);
155
+ return typeof found === "string" ? found : undefined;
106
156
  };
107
- const valueFlags = [
108
- "--token",
109
- "--chat-id",
110
- "--discord-bot-token",
111
- "--discord-application-id",
112
- "--discord-guild-id",
113
- "--discord-parent-channel-id",
114
- "--slack-bot-token",
115
- "--slack-app-token",
116
- "--slack-workspace-id",
117
- "--slack-channel-id",
118
- "--slack-authorized-user-id",
119
- ];
120
- if (
121
- valueFlags.some(name => {
122
- const index = rest.indexOf(name);
123
- const value = index >= 0 ? rest[index + 1] : undefined;
124
- return index >= 0 && (!value || value.startsWith("--"));
125
- })
126
- )
127
- return undefined;
128
- const provider = rest[0]?.startsWith("--") ? undefined : rest[0];
129
- if (provider !== undefined && provider !== "telegram" && provider !== "discord" && provider !== "slack") {
130
- return undefined;
131
- }
132
157
  return {
133
158
  action,
134
- rawArgs: rest,
159
+ rawArgs: args.slice(2),
135
160
  ...(provider ? { provider } : {}),
136
- token: flag("--token"),
137
- chatId: flag("--chat-id"),
138
- ...(flag("--discord-bot-token") ? { discordBotToken: flag("--discord-bot-token") } : {}),
139
- ...(flag("--discord-application-id") ? { discordApplicationId: flag("--discord-application-id") } : {}),
140
- ...(flag("--discord-guild-id") ? { discordGuildId: flag("--discord-guild-id") } : {}),
141
- ...(flag("--discord-parent-channel-id")
142
- ? { discordParentChannelId: flag("--discord-parent-channel-id") }
143
- : {}),
144
- ...(flag("--slack-bot-token") ? { slackBotToken: flag("--slack-bot-token") } : {}),
145
- ...(flag("--slack-app-token") ? { slackAppToken: flag("--slack-app-token") } : {}),
146
- ...(flag("--slack-workspace-id") ? { slackWorkspaceId: flag("--slack-workspace-id") } : {}),
147
- ...(flag("--slack-channel-id") ? { slackChannelId: flag("--slack-channel-id") } : {}),
148
- ...(flag("--slack-authorized-user-id") ? { slackAuthorizedUserId: flag("--slack-authorized-user-id") } : {}),
149
- redact: rest.includes("--redact"),
161
+ token: value("--token"),
162
+ chatId: value("--chat-id"),
163
+ discordBotToken: value("--discord-bot-token"),
164
+ discordApplicationId: value("--discord-application-id"),
165
+ discordGuildId: value("--discord-guild-id"),
166
+ discordParentChannelId: value("--discord-parent-channel-id"),
167
+ slackBotToken: value("--slack-bot-token"),
168
+ slackAppToken: value("--slack-app-token"),
169
+ slackWorkspaceId: value("--slack-workspace-id"),
170
+ slackChannelId: value("--slack-channel-id"),
171
+ slackAuthorizedUserId: value("--slack-authorized-user-id"),
172
+ redact: flags.get("--redact") === true,
150
173
  };
151
174
  }
152
- if (action === "health" || action === "test" || action === "recovery") {
175
+ if (action === "status") {
176
+ return args.length === 2 ? { action, rawArgs: [] } : undefined;
177
+ }
178
+ if (action === "health" || action === "test") {
153
179
  const rest = args.slice(2);
154
- const flag = (name: string): string | undefined => {
155
- const i = rest.indexOf(name);
156
- return i >= 0 ? rest[i + 1] : undefined;
157
- };
180
+ const flags = parseFlags(
181
+ rest,
182
+ new Set(action === "health" ? ["--provider"] : ["--provider", "--message"]),
183
+ new Set(action === "health" ? ["--probe"] : []),
184
+ );
185
+ if (!flags) return undefined;
186
+ const rawProvider = flags.get("--provider");
187
+ const provider = typeof rawProvider === "string" ? providerValue(rawProvider) : undefined;
188
+ if (rawProvider !== undefined && !provider) return undefined;
158
189
  return {
159
190
  action,
160
191
  rawArgs: rest,
161
- probe: rest.includes("--probe"),
162
- message: flag("--message"),
192
+ ...(provider ? { provider } : {}),
193
+ probe: flags.get("--probe") === true,
194
+ message: typeof flags.get("--message") === "string" ? (flags.get("--message") as string) : undefined,
163
195
  };
164
196
  }
197
+ if (action === "recovery") {
198
+ const flags = parseFlags(args.slice(2), new Set(), new Set(["--force-daemon-lock"]));
199
+ return flags
200
+ ? { action, rawArgs: args.slice(2), forceDaemonLock: flags.get("--force-daemon-lock") === true }
201
+ : undefined;
202
+ }
165
203
  if (action === "daemon-internal") {
166
204
  return {
167
205
  action,
@@ -169,7 +207,6 @@ export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
169
207
  rawArgs: args.slice(2),
170
208
  };
171
209
  }
172
-
173
210
  return undefined;
174
211
  }
175
212
 
@@ -193,7 +230,7 @@ export async function runNotifyCommand(cmd: NotifyCommandArgs, deps: NotifyComma
193
230
  await runTest(deps, cmd);
194
231
  return;
195
232
  case "recovery":
196
- await runRecovery(deps);
233
+ await runRecovery(deps, cmd.forceDaemonLock);
197
234
  return;
198
235
  case "daemon-internal":
199
236
  if (cmd.smoke) {
@@ -273,18 +310,46 @@ async function runDiscordSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps):
273
310
  deps,
274
311
  );
275
312
  const settings = await getSettings(deps);
276
- const patches: SettingsAtomicPatch[] = [
277
- { path: "notifications.discord.botToken", op: "set", value: botToken },
278
- { path: "notifications.discord.applicationId", op: "set", value: applicationId },
279
- { path: "notifications.discord.guildId", op: "set", value: guildId },
280
- { path: "notifications.discord.parentChannelId", op: "set", value: parentChannelId },
281
- { path: "notifications.enabled", op: "set", value: true },
282
- ];
283
- if (cmd.redact) patches.push({ path: "notifications.redact", op: "set", value: true });
284
- await settings.commitAtomicBatch(patches);
285
- const daemon = await ensureConfiguredProviderDaemon("discord", settings, deps);
313
+ let activationFailure: string | undefined;
314
+ let activationOutcome: EnsureChatDaemonResult | undefined;
315
+ const runtime: NotificationProviderRuntimeAuthority = {
316
+ activate: async provider => {
317
+ if (provider !== "discord") throw new Error("Unexpected provider activation request.");
318
+ try {
319
+ const result = await ensureConfiguredProviderDaemon("discord", settings, deps);
320
+ if (result === "disabled") throw new Error("Discord runtime did not activate.");
321
+ activationOutcome = result;
322
+ } catch (error) {
323
+ activationFailure = error instanceof Error ? error.message : "Discord runtime activation failed.";
324
+ throw error;
325
+ }
326
+ },
327
+ deactivate: async () => undefined,
328
+ };
329
+ const result = await mutateNotificationProvider({
330
+ settings,
331
+ mutation: {
332
+ provider: "discord",
333
+ botToken: { action: "replace", value: botToken },
334
+ applicationId,
335
+ guildId,
336
+ parentChannelId,
337
+ },
338
+ configureAndActivate: true,
339
+ ...(cmd.redact ? { redact: true } : {}),
340
+ runtime,
341
+ });
342
+ if (result.status === "commit_failed")
343
+ throw new Error("Discord configuration was not saved because the CAS commit failed.");
344
+ if (result.status !== "activated") {
345
+ const detail = `runtime activation failed: ${activationFailure ?? result.status}`;
346
+ process.stderr.write(`Discord configuration saved, but ${detail}.\n`);
347
+ if (deps.setExitCode) deps.setExitCode(1);
348
+ else process.exitCode = 1;
349
+ return;
350
+ }
286
351
  process.stdout.write(
287
- `Discord notifications enabled. botToken=${maskToken(botToken)} applicationId=${applicationId} guildId=${guildId} parentChannelId=${parentChannelId} daemon=${daemon}\n`,
352
+ `Discord configuration saved and activated. botToken=${maskToken(botToken)} applicationId=${applicationId} guildId=${guildId} parentChannelId=${parentChannelId} daemon=${activationOutcome ?? "attached"}\n`,
288
353
  );
289
354
  }
290
355
 
@@ -295,21 +360,47 @@ async function runSlackSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps): P
295
360
  const channelId = await promptSetupValue(cmd.slackChannelId, "--slack-channel-id", false, deps);
296
361
  const authorizedUserId = cmd.slackAuthorizedUserId?.trim() || undefined;
297
362
  const settings = await getSettings(deps);
298
- const patches: SettingsAtomicPatch[] = [
299
- { path: "notifications.slack.botToken", op: "set", value: botToken },
300
- { path: "notifications.slack.appToken", op: "set", value: appToken },
301
- { path: "notifications.slack.workspaceId", op: "set", value: workspaceId },
302
- { path: "notifications.slack.channelId", op: "set", value: channelId },
303
- authorizedUserId === undefined
304
- ? { path: "notifications.slack.authorizedUserId", op: "unset" }
305
- : { path: "notifications.slack.authorizedUserId", op: "set", value: authorizedUserId },
306
- { path: "notifications.enabled", op: "set", value: true },
307
- ];
308
- if (cmd.redact) patches.push({ path: "notifications.redact", op: "set", value: true });
309
- await settings.commitAtomicBatch(patches);
310
- const daemon = await ensureConfiguredProviderDaemon("slack", settings, deps);
363
+ let activationFailure: string | undefined;
364
+ let activationOutcome: EnsureChatDaemonResult | undefined;
365
+ const runtime: NotificationProviderRuntimeAuthority = {
366
+ activate: async provider => {
367
+ if (provider !== "slack") throw new Error("Unexpected provider activation request.");
368
+ try {
369
+ const result = await ensureConfiguredProviderDaemon("slack", settings, deps);
370
+ if (result === "disabled") throw new Error("Slack runtime did not activate.");
371
+ activationOutcome = result;
372
+ } catch (error) {
373
+ activationFailure = error instanceof Error ? error.message : "Slack runtime activation failed.";
374
+ throw error;
375
+ }
376
+ },
377
+ deactivate: async () => undefined,
378
+ };
379
+ const result = await mutateNotificationProvider({
380
+ settings,
381
+ mutation: {
382
+ provider: "slack",
383
+ botToken: { action: "replace", value: botToken },
384
+ appToken: { action: "replace", value: appToken },
385
+ workspaceId,
386
+ channelId,
387
+ authorizedUserId,
388
+ },
389
+ configureAndActivate: true,
390
+ ...(cmd.redact ? { redact: true } : {}),
391
+ runtime,
392
+ });
393
+ if (result.status === "commit_failed")
394
+ throw new Error("Slack configuration was not saved because the CAS commit failed.");
395
+ if (result.status !== "activated") {
396
+ const detail = `runtime activation failed: ${activationFailure ?? result.status}`;
397
+ process.stderr.write(`Slack configuration saved, but ${detail}.\n`);
398
+ if (deps.setExitCode) deps.setExitCode(1);
399
+ else process.exitCode = 1;
400
+ return;
401
+ }
311
402
  process.stdout.write(
312
- `Slack notifications enabled. botToken=${maskToken(botToken)} appToken=${maskToken(appToken)} workspaceId=${workspaceId} channelId=${channelId} authorizedUserId=${authorizedUserId ?? "(unset; inbound denied)"} daemon=${daemon}\n`,
403
+ `Slack configuration saved and activated. botToken=${maskToken(botToken)} appToken=${maskToken(appToken)} workspaceId=${workspaceId} channelId=${channelId} authorizedUserId=${authorizedUserId ?? "(unset; inbound denied)"} daemon=${activationOutcome ?? "attached"}\n`,
313
404
  );
314
405
  }
315
406
 
@@ -365,6 +456,7 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
365
456
  if (result.pairingSource === "provided") {
366
457
  process.stdout.write(`Using provided chat id ${result.chatId} (non-interactive).\n`);
367
458
  }
459
+ let settingsCommitted = false;
368
460
  try {
369
461
  const proposedIdentity = deps.setupPreflight
370
462
  ? proposedIdentityFromSetupPreflight(deps.setupPreflight, token.trim(), result.chatId)
@@ -385,9 +477,11 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
385
477
  { path: "notifications.telegram.botToken", op: "set", value: token.trim() },
386
478
  { path: "notifications.telegram.chatId", op: "set", value: result.chatId },
387
479
  { path: "notifications.enabled", op: "set", value: true },
480
+ { path: "notifications.telegram.enabled", op: "set", value: true },
388
481
  ];
389
482
  if (deps.setupRedact ?? cmd.redact) patches.push({ path: "notifications.redact", op: "set", value: true });
390
483
  const receipt = await settings.commitAtomicBatch(patches);
484
+ settingsCommitted = true;
391
485
  const activationMarker = createTelegramActivationMarker({
392
486
  botToken: token.trim(),
393
487
  chatId: result.chatId,
@@ -422,6 +516,7 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
422
516
  });
423
517
  if (activation.status === "blocked_identity") {
424
518
  const restored = await activation.restore();
519
+ if (restored.status === "restored" || restored.status === "still_blocked") settingsCommitted = false;
425
520
  const detail =
426
521
  restored.status === "restored"
427
522
  ? "Telegram activation was blocked by a foreign daemon; previous settings were restored."
@@ -432,9 +527,18 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
432
527
  : "Telegram activation was blocked; refusing to report setup success.";
433
528
  throw new Error(detail);
434
529
  }
530
+ if (activation.status === "activation_failed") {
531
+ receipt.discard();
532
+ throw new Error(activation.message);
533
+ }
534
+ receipt.discard();
435
535
  } catch (error) {
436
536
  const detail = sanitizeDiagnostic(error instanceof Error ? error.message : "unknown persistence failure", token);
437
- throw new Error(`Unable to persist and activate Telegram notification settings: ${detail}`);
537
+ throw new Error(
538
+ settingsCommitted
539
+ ? `Telegram notification settings were saved, but activation or recovery failed: ${detail}`
540
+ : `Unable to persist and activate Telegram notification settings: ${detail}`,
541
+ );
438
542
  }
439
543
  process.stdout.write(
440
544
  `Notifications enabled. botToken=${maskToken(token)} chatId=${result.chatId} threaded=${result.threadedLabel}\n`,
@@ -586,6 +690,7 @@ async function runHealth(deps: NotifyCommandDeps, cmd: NotifyCommandArgs): Promi
586
690
  const settings = await getSettings(deps);
587
691
  const report = await checkNotificationHealth({
588
692
  settings,
693
+ provider: cmd.provider,
589
694
  probe: cmd.probe,
590
695
  deps: { fetchImpl: deps.fetchImpl, apiBase: deps.apiBase },
591
696
  });
@@ -598,17 +703,28 @@ async function runTest(deps: NotifyCommandDeps, cmd: NotifyCommandArgs): Promise
598
703
  const settings = await getSettings(deps);
599
704
  const result = await sendNotificationTest({
600
705
  settings,
706
+ provider: cmd.provider,
601
707
  text: cmd.message,
602
- deps: { fetchImpl: deps.fetchImpl, apiBase: deps.apiBase },
708
+ deps: {
709
+ fetchImpl: deps.fetchImpl,
710
+ apiBase: deps.apiBase,
711
+ providerRuntimeStatus: async provider => {
712
+ const status =
713
+ provider === "telegram"
714
+ ? await new TelegramDaemonController(settings).status()
715
+ : await new ChatDaemonController(settings, provider).status();
716
+ return status.health === "running" ? "ready" : "inactive";
717
+ },
718
+ },
603
719
  });
604
720
  process.stdout.write(`${formatNotificationTestResult(result)}\n`);
605
721
  if (!result.ok && deps.setExitCode) deps.setExitCode(1);
606
722
  else if (!result.ok) process.exitCode = 1;
607
723
  }
608
724
 
609
- async function runRecovery(deps: NotifyCommandDeps): Promise<void> {
725
+ async function runRecovery(deps: NotifyCommandDeps, forceDaemonLock = false): Promise<void> {
610
726
  const settings = await getSettings(deps);
611
- const report = await recoverNotifications({ settings });
727
+ const report = await recoverNotifications({ settings, forceDaemonLock });
612
728
  process.stdout.write(`${formatNotificationRecoveryReport(report)}\n`);
613
729
  }
614
730
 
@@ -616,8 +732,8 @@ export function printNotifyHelp(): void {
616
732
  process.stdout.write(`${chalk.bold(`${APP_NAME} notify`)} - Configure Telegram, Discord, or Slack notifications
617
733
 
618
734
  ${chalk.bold("Interactive path:")}
619
- In a running GJC session, use /settings → Notifications for setup, health, test, recovery,
620
- reconnect, global enable/disable, adapter-local Telegram removal, and session on/off.
735
+ In a running GJC session, use /settings → Notifications for first-class Telegram, Discord,
736
+ and Slack configure/edit/repair, desired intent, health, test, removal, global master, and session controls.
621
737
  The CLI subcommands below remain the authoritative headless and automation fallback.
622
738
 
623
739
  ${chalk.bold("Usage:")}
@@ -625,16 +741,16 @@ ${chalk.bold("Usage:")}
625
741
  ${APP_NAME} notify setup discord --discord-bot-token <token> --discord-application-id <id> --discord-guild-id <id> --discord-parent-channel-id <id>
626
742
  ${APP_NAME} notify setup slack --slack-bot-token <token> --slack-app-token <token> --slack-workspace-id <id> --slack-channel-id <id> [--slack-authorized-user-id <id>]
627
743
  ${APP_NAME} notify status
628
- ${APP_NAME} notify health [--probe]
629
- ${APP_NAME} notify test [--message <text>]
630
- ${APP_NAME} notify recovery
744
+ ${APP_NAME} notify health [--provider telegram|discord|slack] [--probe]
745
+ ${APP_NAME} notify test [--provider telegram|discord|slack] [--message <text>]
746
+ ${APP_NAME} notify recovery [--force-daemon-lock]
631
747
 
632
748
  ${chalk.bold("Subcommands:")}
633
- setup Pair Telegram or save complete non-interactive Discord/Slack notification settings
634
- status Show notification configuration without secrets
635
- health Report config, daemon-ownership and endpoint health (--probe adds a Telegram reachability check)
636
- test Send a one-off test notification through the configured Telegram adapter
637
- recovery Clear dead-owner daemon locks and stale per-session endpoint files (never touches a live owner)
749
+ setup Pair Telegram or atomically save and activate complete Discord/Slack settings
750
+ status Show global master and provider configured/repair/desired/effective state without secrets
751
+ health Report selected provider state; --probe uses REST only and never opens Gateway/Socket Mode
752
+ test Send a one-off test through one selected or uniquely effective provider
753
+ recovery Clear dead-owner daemon locks and stale per-session endpoint files (never touches a live owner); --force-daemon-lock retries only with the same fail-closed dead-owner proof
638
754
 
639
755
  ${chalk.bold("Examples:")}
640
756
  ${APP_NAME} notify setup
@@ -642,8 +758,8 @@ ${chalk.bold("Examples:")}
642
758
  ${APP_NAME} notify setup discord --discord-bot-token <token> --discord-application-id <id> --discord-guild-id <id> --discord-parent-channel-id <id>
643
759
  ${APP_NAME} notify setup slack --slack-bot-token <token> --slack-app-token <token> --slack-workspace-id <id> --slack-channel-id <id> [--slack-authorized-user-id <id>]
644
760
  ${APP_NAME} notify status
645
- ${APP_NAME} notify health --probe
646
- ${APP_NAME} notify test --message "hello from gjc"
761
+ ${APP_NAME} notify health --provider discord --probe
762
+ ${APP_NAME} notify test --provider slack --message "hello from gjc"
647
763
  ${APP_NAME} notify recovery
648
764
 
649
765
  ${chalk.bold("Threaded Mode:")}
@@ -40,7 +40,11 @@ export default class Notify extends Command {
40
40
  description: "Slack user id authorized for inbound replies and commands",
41
41
  }),
42
42
  redact: Flags.boolean({ description: "Enable redaction of remote notification content" }),
43
- probe: Flags.boolean({ description: "notify health: probe Telegram reachability (getMe)" }),
43
+ provider: Flags.string({
44
+ description: "notify health/test: select telegram, discord, or slack",
45
+ options: ["telegram", "discord", "slack"],
46
+ }),
47
+ probe: Flags.boolean({ description: "notify health: run the selected provider's REST diagnostic" }),
44
48
  message: Flags.string({ description: "notify test: custom message body" }),
45
49
  "owner-id": Flags.string({ description: "Internal: daemon owner id" }),
46
50
  "agent-dir": Flags.string({ description: "Internal: agent dir for the daemon" }),
@@ -64,16 +68,23 @@ export default class Notify extends Command {
64
68
  ...(agentDir ? ["--agent-dir", agentDir] : []),
65
69
  ...extra,
66
70
  ];
67
- const provider = extra[0];
71
+ const positionalProvider = action === "setup" ? extra[0] : undefined;
68
72
  if (
69
- action === "setup" &&
70
- provider !== undefined &&
71
- provider !== "telegram" &&
72
- provider !== "discord" &&
73
- provider !== "slack"
73
+ positionalProvider !== undefined &&
74
+ positionalProvider !== "telegram" &&
75
+ positionalProvider !== "discord" &&
76
+ positionalProvider !== "slack"
74
77
  ) {
75
- throw new Error(`Unknown notification provider: ${provider}`);
78
+ throw new Error(`Unknown notification provider: ${positionalProvider}`);
79
+ }
80
+ const providerFlag = flagRec.provider as string | undefined;
81
+ if (providerFlag && action !== "health" && action !== "test") {
82
+ throw new Error("--provider is valid only for notify health and notify test.");
83
+ }
84
+ if (action !== "setup" && action !== "daemon-internal" && extra.length > 0) {
85
+ throw new Error(`Unexpected notify arguments: ${extra.join(" ")}`);
76
86
  }
87
+ const provider = providerFlag ?? positionalProvider;
77
88
 
78
89
  const cmd: NotifyCommandArgs = {
79
90
  action: action as NotifyAction,