Skip to content
Merged
4 changes: 2 additions & 2 deletions docs/discord-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ secret mechanism rather than placing them in shell history, files committed to
the repository, chat transcripts, or screenshots. The setup command writes:

- `notifications.enabled = true`
- `notifications.discord.enabled = true` (durable desired intent)
- `notifications.discord.botToken`
- `notifications.discord.applicationId`
- `notifications.discord.guildId`
- `notifications.discord.parentChannelId`
- `notifications.redact = true` when requested

`gjc notify status` shows configured Discord identifiers and masks token values.
It must not be used as a way to recover a token.
`gjc notify status` reports Discord completeness, repair/quarantine state, desired intent, effective enablement, destination identifiers, and a masked token. It must not be used as a way to recover a token. A successful durable save is not rolled back when later daemon activation fails; the command reports the saved-but-runtime-degraded outcome and exits nonzero so the configuration can be repaired or reactivated explicitly. In `/settings`, secret edits are explicit `keep`, `replace`, or `remove`; removing the required bot token turns Discord desired intent off without changing Telegram, Slack, or the global master.

## Threads, resume, and replies

Expand Down
9 changes: 5 additions & 4 deletions docs/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,10 @@ revalidates the complete bot-token/chat identity immediately before polling and
again before activation. A foreign or unknown owner is never killed, reloaded, or taken over;
setup fails closed without saving or exposing the raw token.

Configuration completeness, provider-local quarantine, durable desired intent, effective enablement, runtime readiness, and delivery outcomes are separate contracts. The global `notifications.enabled` master never erases provider credentials or desired flags. `/settings` edits secrets through explicit `keep`, `replace`, or `remove` actions, commits only the selected provider in one CAS batch, and reports post-commit observer or activation failures without pretending the durable save rolled back. Malformed provider-local values are quarantined for explicit repair while safe sibling providers remain usable; malformed global notification structure remains fail-closed.

`GJC_NOTIFICATIONS=0` suppresses only automatic generic current-session admission. Explicit `/notify on` can opt the current session back in without mutating durable provider state, and direct provider APIs remain governed by provider effectiveness and their own runtime readiness. If Telegram ownership is proven foreign while Discord or Slack is effective, GJC publishes the chat daemon endpoint under the isolated `.gjc/state/chat/sdk/` discovery path; the blocked Telegram scanner never receives the shared endpoint token.

- [Telegram notification onboarding](./telegram-onboarding.md) documents
`gjc notify setup` and private-chat pairing.
- [Discord notification onboarding](./discord-onboarding.md) documents
Expand All @@ -555,10 +559,7 @@ setup fails closed without saving or exposing the raw token.
`gjc notify setup slack`, Socket Mode configuration, immediate envelope ack,
and thread lifecycle.

`gjc notify status` reports configured providers while masking every token. The
Discord and Slack setup commands are non-interactive and require their documented
identifier and token flags; supply secrets through an approved local mechanism,
not examples, committed files, shell history, logs, or chat.
`gjc notify status` reports provider completeness, repair/quarantine state, desired intent, effective enablement, and masked tokens. Destination identifiers remain visible and may be sensitive. The Discord and Slack setup commands are non-interactive and require their documented identifier and token flags; supply secrets through an approved local mechanism, not examples, committed files, shell history, logs, or chat. `gjc notify health --provider <provider> --probe` performs a provider-owned REST diagnostic even when complete credentials are intentionally inactive, while `gjc notify test --provider <provider>` additionally requires effective enablement and runtime readiness.

The daemon/session engine is shared. Session discovery, WebSocket protocol,
redaction decisions, rate-limit pooling, reply routing, singleton ownership, and
Expand Down
4 changes: 2 additions & 2 deletions docs/slack-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ API calls.
Without `--slack-authorized-user-id`, the adapter remains outbound-only: every inbound envelope is acknowledged but denied before it can create a durable claim or reach an SDK endpoint. The user ID is an identifier, not a secret. It also accepts `--redact`. Provide secret values from an approved local secret mechanism, not shell history, committed configuration, tickets, screenshots, or chat. Setup writes:

- `notifications.enabled = true`
- `notifications.slack.enabled = true` (durable desired intent)
- `notifications.slack.botToken`
- `notifications.slack.appToken`
- `notifications.slack.workspaceId`
- `notifications.slack.channelId`
- `notifications.slack.authorizedUserId` when configured
- `notifications.redact = true` when requested

`gjc notify status` masks all token values. It is status output, not a credential
recovery mechanism.
`gjc notify status` reports Slack completeness, repair/quarantine state, desired intent, effective enablement, destination identifiers, and masked token values. It is status output, not a credential recovery mechanism. A successful durable save is not rolled back when later daemon activation fails; the command reports the saved-but-runtime-degraded outcome and exits nonzero. In `/settings`, bot/app secret edits are explicit `keep`, `replace`, or `remove`; removing either required token turns Slack desired intent off without changing Telegram, Discord, or the global master.

## Socket Mode, threads, and resume

Expand Down
47 changes: 18 additions & 29 deletions docs/telegram-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,14 @@ options remain readable because they must be answerable remotely.
gjc notify status
```

The status command reads the typed notification settings and prints:

- `enabled`
- masked `botToken`
- paired `chatId`
- `redact`

It uses the same masking helper as setup (`first 4 chars + … + length`), so it is
safe to paste into a support thread if the chat id itself is not sensitive in
your environment.
The status command reports the global master plus each provider's independent
configuration completeness, repair/quarantine state, durable desired-intent
source, and effective enablement. Stored tokens are masked with the shared
`first 4 chars + … + length` helper. Destination identifiers such as Telegram
chat IDs remain visible and may be sensitive, so redact them before pasting a
status report into a public support thread. Runtime readiness and actual
delivery outcomes remain separate; use `gjc notify health --provider telegram`
and `gjc notify test --provider telegram` for those checks.

## 5. Global configuration, adapters, and precedence

Expand All @@ -183,17 +181,13 @@ notification identity.
layer:

- `notifications.enabled = true`
- `notifications.telegram.enabled = true` (durable desired intent)
- `notifications.telegram.botToken = <token>`
- `notifications.telegram.chatId = <paired chat id>`
- `notifications.redact = true` only when `--redact` was passed
- `notifications.telegram.streaming.enabled = true` by default; set it to `false` to disable durable live Telegram assistant-output updates globally. `GJC_NOTIFICATIONS_STREAM=1` forces process-local streaming, while `0`, `off`, or `false` forces it off.

A complete global configuration is `notifications.enabled` plus at least one
complete adapter. Telegram needs its bot token and private-chat id; Discord and
Slack each need their own credential and destination. Removing Telegram in
`/settings` is adapter-local: it preserves a complete Discord or Slack adapter
and global enablement, and disables global notifications only when Telegram was
the last complete adapter.
Provider completeness, malformed-state quarantine, desired intent, effective enablement, runtime readiness, and delivery outcome are separate status dimensions. Telegram is complete when its bot token and private-chat id are valid; it is effective only when it is complete, not quarantined, desired on, and the global master is on. Provider-local malformed values are quarantined without erasing safe sibling values or secrets. Removing Telegram is adapter-local: it removes only Telegram credentials and sets Telegram desired intent off without changing `notifications.enabled` or any Discord/Slack state.


Three lifecycle gates keep SDK hosting, setup, and managed delivery separate:
Expand Down Expand Up @@ -221,7 +215,7 @@ hosted SDK endpoints:

1. `GJC_NOTIFY=off`, `0`, or `false` prevents the notification control surface
for that process.
2. `GJC_NOTIFICATIONS=0` is a hard managed-delivery opt-out.
2. `GJC_NOTIFICATIONS=0` suppresses automatic generic current-session admission; explicit `/notify on` may override that suppression only for the current session.
3. Local `/notify off` disables managed delivery only for the current session.
4. `GJC_NOTIFICATIONS=1` or `GJC_NOTIFICATIONS_TOKEN` enables the legacy
explicit managed-delivery path.
Expand Down Expand Up @@ -393,15 +387,10 @@ Inside a running GJC session, `/notify` controls the current session only; it
does not edit global config or credentials:

- `/notify status` reports current session notification status without secrets;
- `/notify off` disables the current session endpoint and removes its discovery
record without changing global setup;
- `/notify on` re-enables the current session when a complete global
configuration or explicit environment path is available, unless
`GJC_NOTIFICATIONS=0` is forcing opt-out.
- `/notify off` disables the current session endpoint and removes its discovery record without changing global setup;
- `/notify on` explicitly re-enables the current generic session when a complete effective provider or another explicit environment path is available.

Neither command changes `GJC_NOTIFY` or `GJC_NOTIFICATIONS` precedence. A
process with `GJC_NOTIFY=off`, `0`, or `false` has no notification control
surface to override.
`GJC_NOTIFICATIONS=0` suppresses automatic generic current-session admission only. An explicit `/notify on` may override that one automatic-admission suppression for the current session; it does not alter durable provider intent or enable a direct provider API. `GJC_NOTIFY=off`, `0`, or `false` remains the hard process-level opt-out and exposes no notification control surface to override.

## 9. Debug-only manual bridge

Expand Down Expand Up @@ -463,10 +452,10 @@ recovery removes only dead-owner artifacts and never touches a live owner.

Check, in order:

1. `gjc notify status`
2. `GJC_NOTIFICATIONS` is not set to `0`
3. the session has not run `/notify off`
4. the repo has `.gjc/state/sdk/<sessionId>.json`
1. `gjc notify status` and confirm the selected provider is complete, not quarantined, desired on, and effective
2. the session has not run `/notify off`; when `GJC_NOTIFICATIONS=0` suppresses automatic admission, run `/notify on` explicitly
3. the repo has `.gjc/state/sdk/<sessionId>.json`, or `.gjc/state/chat/sdk/<sessionId>.json` when a proven foreign Telegram owner is isolated while Discord/Slack remains effective
4. the selected provider runtime is ready or attached
5. the managed daemon state is fresh under the GJC agent notifications directory

Do not paste endpoint discovery files into public issues; they contain the
Expand Down
4 changes: 4 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- Notification settings now expose first-class Telegram, Discord, and Slack configuration, desired-intent toggles, provider-local quarantine and repair guidance, explicit `keep | replace | remove` secret actions, provider-specific health/test diagnostics, and truthful saved-but-runtime-degraded outcomes. The global master preserves provider credentials and intent, `GJC_NOTIFICATIONS=0` suppresses only automatic generic-session admission, and blocked Telegram ownership uses an isolated chat-only endpoint so verified Discord or Slack siblings can continue without exposing the shared endpoint.

### Fixed

- Ordinary `ask` selectors now bound long question premises and page through every premise row without skipping rows hidden by overflow indicators (#3675).
Expand Down
51 changes: 51 additions & 0 deletions packages/coding-agent/scripts/build-sdk-package-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const packageName = "@gajae-code/coding-agent";
const aiPackageDir = path.resolve(packageDir, "../ai");
const bridgeClientPackageDir = path.resolve(packageDir, "../bridge-client");
const tuiPackageDir = path.resolve(packageDir, "../tui");
const agentPackageDir = path.resolve(packageDir, "../agent");
const nativesPackageDir = path.resolve(packageDir, "../natives");
const linuxX64PackageDir = path.resolve(packageDir, "../natives-linux-x64");
const manifestsDir = path.join(packageDir, "test/manifests");
Expand Down Expand Up @@ -41,6 +42,7 @@ async function runSmoke(): Promise<Surface> {
await fs.copyFile(path.join(nativesPackageDir, "native", entry), path.join(stagedNativeDir, entry));
}
}
const agentTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], agentPackageDir);
const aiTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], aiPackageDir);
const bridgeClientTarball = run(
["bun", "pm", "pack", "--destination", tempDir, "--quiet"],
Expand All @@ -49,6 +51,7 @@ async function runSmoke(): Promise<Surface> {
const tuiTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], tuiPackageDir);
const nativesTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], nativesPackageDir);
const linuxX64Tarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], stagedLinuxX64Dir);
const agentTarballPath = path.isAbsolute(agentTarball) ? agentTarball : path.join(agentPackageDir, agentTarball);
const codingAgentTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], packageDir);
const aiTarballPath = path.isAbsolute(aiTarball) ? aiTarball : path.join(aiPackageDir, aiTarball);
const bridgeClientTarballPath = path.isAbsolute(bridgeClientTarball)
Expand All @@ -71,6 +74,7 @@ async function runSmoke(): Promise<Surface> {
name: "sdk-smoke",
private: true,
dependencies: {
"@gajae-code/agent-core": `file:${agentTarballPath}`,
"@gajae-code/ai": `file:${aiTarballPath}`,
"@gajae-code/bridge-client": `file:${bridgeClientTarballPath}`,
[packageName]: `file:${codingAgentTarballPath}`,
Expand All @@ -79,6 +83,7 @@ async function runSmoke(): Promise<Surface> {
"@gajae-code/natives-linux-x64": `file:${linuxX64TarballPath}`,
},
overrides: {
"@gajae-code/agent-core": `file:${agentTarballPath}`,
"@gajae-code/ai": `file:${aiTarballPath}`,
"@gajae-code/bridge-client": `file:${bridgeClientTarballPath}`,
"@gajae-code/tui": `file:${tuiTarballPath}`,
Expand All @@ -96,6 +101,52 @@ async function runSmoke(): Promise<Surface> {
const installedPackage = JSON.parse(
await fs.readFile(path.join(tempDir, "node_modules", packageName, "package.json"), "utf8"),
) as { exports?: Record<string, unknown> };
const installedAgentPackagePath = path.join(tempDir, "node_modules", "@gajae-code", "agent-core");
const installedAgentPackageJsonPath = path.join(installedAgentPackagePath, "package.json");
const installedAgentPackage = JSON.parse(await fs.readFile(installedAgentPackageJsonPath, "utf8")) as {
name?: string;
version?: string;
};
const expectedAgentPackage = JSON.parse(
await fs.readFile(path.join(agentPackageDir, "package.json"), "utf8"),
) as {
name?: string;
version?: string;
};
const installedAgentRealpath = await fs.realpath(installedAgentPackagePath);
const tempRealpath = await fs.realpath(tempDir);
const sourceAgentRealpath = await fs.realpath(agentPackageDir);
if (
!installedAgentRealpath.startsWith(`${tempRealpath}${path.sep}`) ||
installedAgentRealpath.startsWith(`${sourceAgentRealpath}${path.sep}`)
) {
throw new Error("packed smoke resolved agent-core outside the temporary packed installation");
}
const packedAgentInspectDir = path.join(tempDir, "packed-agent-inspect");
await fs.mkdir(packedAgentInspectDir);
const extract = Bun.spawnSync(["tar", "xzf", agentTarballPath, "-C", packedAgentInspectDir], {
stdout: "pipe",
stderr: "pipe",
});
if (extract.exitCode !== 0)
throw new Error(`could not inspect packed agent-core tarball: ${extract.stderr.toString()}`);
const packedAgentPackageJson = await fs.readFile(
path.join(packedAgentInspectDir, "package", "package.json"),
"utf8",
);
const installedAgentPackageJson = await fs.readFile(installedAgentPackageJsonPath, "utf8");
if (installedAgentPackageJson !== packedAgentPackageJson) {
throw new Error("packed smoke installed agent-core content different from the packed tarball");
}
if (
installedAgentPackage.name !== expectedAgentPackage.name ||
installedAgentPackage.version !== expectedAgentPackage.version
) {
throw new Error("packed smoke installed a mismatched agent-core package");
}
if (installedAgentPackage.name !== "@gajae-code/agent-core") {
throw new Error("packed smoke agent-core package identity is invalid");
}
if (installedPackage.exports?.["./session/internal/*"] !== null) {
throw new Error("packed package must explicitly block ./session/internal/*");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "../test/fixtures/tui/notifications-settings-showcase";

const CANONICAL_COMMAND =
"bun packages/coding-agent/scripts/capture-notifications-settings-showcase.ts --output .gjc/qa/issue-2050-notifications";
"bun packages/coding-agent/scripts/capture-notifications-settings-showcase.ts --output .gjc/qa/issue-3570-notifications";
const DETERMINISTIC_CAPTURE_TIMESTAMP = "1970-01-01T00:00:00.000Z";
const CAPTURE_TOOL_VERSION = "notifications-settings-showcase-live-settings-selector-v3";

Expand Down
Loading
Loading