@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.
- package/CHANGELOG.md +48 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/commands/sdk.d.ts +23 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-discovery-manager.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/resolve-config-value.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +17 -1
- package/dist/types/discovery/agents-md.d.ts +11 -1
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +20 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/broker/broker.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
- package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
- package/dist/types/sdk/bus/notification-service.d.ts +54 -4
- package/dist/types/sdk/bus/session-control.d.ts +8 -3
- package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
- package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/providers.d.ts +22 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +19 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +3 -2
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/session/session-storage.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +12 -1
- package/dist/types/tools/composer-bash-policy.d.ts +9 -1
- package/package.json +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
- package/src/cli/notify-cli.ts +215 -99
- package/src/commands/notify.ts +19 -8
- package/src/commands/sdk.ts +54 -0
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-profiles.ts +8 -0
- package/src/config/model-registry.ts +866 -76
- package/src/config/resolve-config-value.ts +10 -8
- package/src/config/settings-schema.ts +25 -15
- package/src/config/settings.ts +15 -15
- package/src/discovery/agents-md.ts +76 -19
- package/src/gjc-runtime/psmux-detect.ts +12 -16
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/components/extensions/inspector-panel.ts +9 -5
- package/src/modes/components/hook-selector.ts +155 -44
- package/src/modes/components/model-selector.ts +242 -49
- package/src/modes/components/notifications-settings-editor.ts +631 -85
- package/src/modes/controllers/extension-ui-controller.ts +114 -15
- package/src/modes/controllers/selector-controller.ts +525 -93
- package/src/modes/print-mode.ts +3 -0
- package/src/modes/types.ts +6 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/sdk/broker/broker.ts +54 -10
- package/src/sdk/broker/lifecycle-ledger.ts +34 -1
- package/src/sdk/broker/lifecycle.ts +47 -50
- package/src/sdk/bus/chat-daemon-cli.ts +12 -5
- package/src/sdk/bus/chat-daemon-control.ts +35 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/discord-live-provider.ts +57 -2
- package/src/sdk/bus/discord-provider.ts +21 -0
- package/src/sdk/bus/index.ts +97 -35
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +571 -137
- package/src/sdk/bus/session-control.ts +119 -44
- package/src/sdk/bus/slack-live-provider.ts +78 -2
- package/src/sdk/bus/slack-provider.ts +24 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/bus/telegram-daemon-control.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +23 -15
- package/src/sdk/client/discovery.ts +10 -4
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +5 -2
- package/src/sdk/providers.ts +51 -0
- package/src/sdk/session-directory.ts +1 -0
- package/src/sdk/session.ts +34 -14
- package/src/session/agent-session.ts +109 -37
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +418 -47
- package/src/session/internal/native-publish-outcome.ts +15 -3
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/session-manager.ts +27 -3
- package/src/session/session-storage.ts +72 -5
- package/src/slash-commands/builtin-registry.ts +117 -15
- package/src/system-prompt.ts +28 -15
- package/src/task/index.ts +9 -3
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +4 -2
- package/src/tools/composer-bash-policy.ts +21 -5
- package/src/tools/telegram-send.ts +4 -2
- package/src/utils/command-args.ts +19 -30
- package/src/utils/lang-from-path.ts +2 -4
|
@@ -16,22 +16,24 @@ const commandInFlight = new Map<string, Promise<string | undefined>>();
|
|
|
16
16
|
* Resolve a config value (API key, header value, etc.) to an actual value.
|
|
17
17
|
* - If starts with "!", executes the rest as a shell command and uses stdout (cached)
|
|
18
18
|
* - Otherwise checks environment variable first, then treats as literal (not cached)
|
|
19
|
+
* - `cacheScope` isolates command-cache entries when a caller rotates its config
|
|
19
20
|
*/
|
|
20
|
-
export async function resolveConfigValue(config: string): Promise<string | undefined> {
|
|
21
|
+
export async function resolveConfigValue(config: string, cacheScope?: string): Promise<string | undefined> {
|
|
21
22
|
if (config.startsWith("!")) {
|
|
22
|
-
return await executeCommand(config);
|
|
23
|
+
return await executeCommand(config, cacheScope);
|
|
23
24
|
}
|
|
24
25
|
const envValue = process.env[config];
|
|
25
26
|
return envValue || config;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
async function executeCommand(commandConfig: string): Promise<string | undefined> {
|
|
29
|
-
const
|
|
29
|
+
async function executeCommand(commandConfig: string, cacheScope?: string): Promise<string | undefined> {
|
|
30
|
+
const cacheKey = cacheScope === undefined ? commandConfig : `${cacheScope}\u0000${commandConfig}`;
|
|
31
|
+
const cached = commandResultCache.get(cacheKey);
|
|
30
32
|
if (cached !== undefined) {
|
|
31
33
|
return cached;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
const existing = commandInFlight.get(
|
|
36
|
+
const existing = commandInFlight.get(cacheKey);
|
|
35
37
|
if (existing) {
|
|
36
38
|
return await existing;
|
|
37
39
|
}
|
|
@@ -40,15 +42,15 @@ async function executeCommand(commandConfig: string): Promise<string | undefined
|
|
|
40
42
|
const promise = runShellCommand(command, 10_000)
|
|
41
43
|
.then(result => {
|
|
42
44
|
if (result !== undefined) {
|
|
43
|
-
commandResultCache.set(
|
|
45
|
+
commandResultCache.set(cacheKey, result);
|
|
44
46
|
}
|
|
45
47
|
return result;
|
|
46
48
|
})
|
|
47
49
|
.finally(() => {
|
|
48
|
-
commandInFlight.delete(
|
|
50
|
+
commandInFlight.delete(cacheKey);
|
|
49
51
|
});
|
|
50
52
|
|
|
51
|
-
commandInFlight.set(
|
|
53
|
+
commandInFlight.set(cacheKey, promise);
|
|
52
54
|
return await promise;
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -149,7 +149,7 @@ export type AnyUiMetadata = UiBase & {
|
|
|
149
149
|
|
|
150
150
|
interface BooleanDef {
|
|
151
151
|
type: "boolean";
|
|
152
|
-
default
|
|
152
|
+
default?: boolean;
|
|
153
153
|
ui?: UiBoolean;
|
|
154
154
|
}
|
|
155
155
|
|
|
@@ -285,6 +285,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
285
285
|
|
|
286
286
|
// Notifications (shared daemon with Telegram/Discord/Slack presentation adapters)
|
|
287
287
|
"notifications.enabled": { type: "boolean", default: false },
|
|
288
|
+
"notifications.telegram.enabled": { type: "boolean" },
|
|
288
289
|
"notifications.telegram.botToken": {
|
|
289
290
|
type: "string",
|
|
290
291
|
default: undefined,
|
|
@@ -336,10 +337,12 @@ export const SETTINGS_SCHEMA = {
|
|
|
336
337
|
},
|
|
337
338
|
},
|
|
338
339
|
"notifications.telegram.topics.nameTemplate": { type: "string", default: undefined },
|
|
340
|
+
"notifications.discord.enabled": { type: "boolean" },
|
|
339
341
|
"notifications.discord.botToken": { type: "string", default: undefined },
|
|
340
342
|
"notifications.discord.applicationId": { type: "string", default: undefined },
|
|
341
343
|
"notifications.discord.guildId": { type: "string", default: undefined },
|
|
342
344
|
"notifications.discord.parentChannelId": { type: "string", default: undefined },
|
|
345
|
+
"notifications.slack.enabled": { type: "boolean" },
|
|
343
346
|
"notifications.slack.botToken": { type: "string", default: undefined },
|
|
344
347
|
"notifications.slack.appToken": { type: "string", default: undefined },
|
|
345
348
|
"notifications.slack.workspaceId": { type: "string", default: undefined },
|
|
@@ -3646,25 +3649,28 @@ type Schema = typeof SETTINGS_SCHEMA;
|
|
|
3646
3649
|
export type SettingPath = keyof Schema;
|
|
3647
3650
|
|
|
3648
3651
|
/** Infer the value type for a setting path */
|
|
3649
|
-
export type SettingValue<P extends SettingPath> = Schema[P] extends { type: "boolean" }
|
|
3652
|
+
export type SettingValue<P extends SettingPath> = Schema[P] extends { type: "boolean"; default: boolean }
|
|
3650
3653
|
? boolean
|
|
3651
|
-
: Schema[P] extends { type: "
|
|
3652
|
-
?
|
|
3653
|
-
: Schema[P] extends { type: "
|
|
3654
|
-
?
|
|
3655
|
-
: Schema[P] extends { type: "
|
|
3656
|
-
?
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
: Schema[P] extends { type: "
|
|
3654
|
+
: Schema[P] extends { type: "boolean" }
|
|
3655
|
+
? boolean | undefined
|
|
3656
|
+
: Schema[P] extends { type: "string" }
|
|
3657
|
+
? string | undefined
|
|
3658
|
+
: Schema[P] extends { type: "number" }
|
|
3659
|
+
? number
|
|
3660
|
+
: Schema[P] extends { type: "enum"; values: infer V }
|
|
3661
|
+
? V extends readonly string[]
|
|
3662
|
+
? V[number]
|
|
3663
|
+
: never
|
|
3664
|
+
: Schema[P] extends { type: "array"; default: infer D }
|
|
3662
3665
|
? D
|
|
3663
|
-
:
|
|
3666
|
+
: Schema[P] extends { type: "record"; default: infer D }
|
|
3667
|
+
? D
|
|
3668
|
+
: never;
|
|
3664
3669
|
|
|
3665
3670
|
/** Get the default value for a setting path */
|
|
3666
3671
|
export function getDefault<P extends SettingPath>(path: P): SettingValue<P> {
|
|
3667
|
-
|
|
3672
|
+
const definition = SETTINGS_SCHEMA[path];
|
|
3673
|
+
return ("default" in definition ? definition.default : undefined) as SettingValue<P>;
|
|
3668
3674
|
}
|
|
3669
3675
|
|
|
3670
3676
|
/** Check if a path has UI metadata (should appear in settings panel) */
|
|
@@ -3991,6 +3997,7 @@ export interface MemoryGuardSettings {
|
|
|
3991
3997
|
export interface NotificationsSettings {
|
|
3992
3998
|
enabled: boolean;
|
|
3993
3999
|
telegram: {
|
|
4000
|
+
enabled?: boolean;
|
|
3994
4001
|
botToken: string | undefined;
|
|
3995
4002
|
chatId: string | undefined;
|
|
3996
4003
|
sound: "all" | "important" | "none";
|
|
@@ -4014,16 +4021,19 @@ export interface NotificationsSettings {
|
|
|
4014
4021
|
};
|
|
4015
4022
|
};
|
|
4016
4023
|
discord: {
|
|
4024
|
+
enabled?: boolean;
|
|
4017
4025
|
botToken: string | undefined;
|
|
4018
4026
|
applicationId: string | undefined;
|
|
4019
4027
|
guildId: string | undefined;
|
|
4020
4028
|
parentChannelId: string | undefined;
|
|
4021
4029
|
};
|
|
4022
4030
|
slack: {
|
|
4031
|
+
enabled?: boolean;
|
|
4023
4032
|
botToken: string | undefined;
|
|
4024
4033
|
appToken: string | undefined;
|
|
4025
4034
|
workspaceId: string | undefined;
|
|
4026
4035
|
channelId: string | undefined;
|
|
4036
|
+
authorizedUserId: string | undefined;
|
|
4027
4037
|
};
|
|
4028
4038
|
redact: boolean;
|
|
4029
4039
|
verbosity: "lean" | "verbose";
|
package/src/config/settings.ts
CHANGED
|
@@ -98,7 +98,7 @@ type DurableBatchRevision = {
|
|
|
98
98
|
};
|
|
99
99
|
type NotificationValidationState = {
|
|
100
100
|
malformedConfigRoot: boolean;
|
|
101
|
-
|
|
101
|
+
invalidNotificationGlobal: boolean;
|
|
102
102
|
generation: number;
|
|
103
103
|
};
|
|
104
104
|
type NotificationValidationRestoreGuard = {
|
|
@@ -378,7 +378,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
378
378
|
#hasMalformedConfigRoot = false;
|
|
379
379
|
/** YAML syntax was unrecoverable, so the loaded defaults are read-only until config.yml is repaired. */
|
|
380
380
|
#hasRecoveredConfigSyntax = false;
|
|
381
|
-
#
|
|
381
|
+
#hasInvalidNotificationGlobal = false;
|
|
382
382
|
#notificationValidationGeneration = 0;
|
|
383
383
|
/** Notification subtree fingerprint from the last raw durable config read. */
|
|
384
384
|
#durableNotificationFingerprint: string | undefined;
|
|
@@ -513,7 +513,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
513
513
|
*/
|
|
514
514
|
getNotificationSettingsSnapshot(): NotificationSettingsSnapshot {
|
|
515
515
|
return parseNotificationSettingsSnapshot(
|
|
516
|
-
this.#hasMalformedConfigRoot || this.#
|
|
516
|
+
this.#hasMalformedConfigRoot || this.#hasInvalidNotificationGlobal ? null : this.#rawNotificationConfig,
|
|
517
517
|
);
|
|
518
518
|
}
|
|
519
519
|
|
|
@@ -895,7 +895,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
895
895
|
cloned.#futureSchemaVersion = this.#futureSchemaVersion;
|
|
896
896
|
cloned.#hasMalformedConfigRoot = this.#hasMalformedConfigRoot;
|
|
897
897
|
cloned.#hasRecoveredConfigSyntax = this.#hasRecoveredConfigSyntax;
|
|
898
|
-
cloned.#
|
|
898
|
+
cloned.#hasInvalidNotificationGlobal = this.#hasInvalidNotificationGlobal;
|
|
899
899
|
cloned.#notificationValidationGeneration = this.#notificationValidationGeneration;
|
|
900
900
|
cloned.#global = structuredClone(this.#global);
|
|
901
901
|
cloned.#rawNotificationConfig = structuredClone(this.#rawNotificationConfig);
|
|
@@ -1149,7 +1149,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1149
1149
|
#resetYamlLoadState(): void {
|
|
1150
1150
|
this.#hasMalformedConfigRoot = false;
|
|
1151
1151
|
this.#hasRecoveredConfigSyntax = false;
|
|
1152
|
-
this.#
|
|
1152
|
+
this.#hasInvalidNotificationGlobal = false;
|
|
1153
1153
|
this.#schemaReport = { issues: [], valid: true };
|
|
1154
1154
|
this.#schemaMigrationPending = false;
|
|
1155
1155
|
this.#futureSchemaVersion = false;
|
|
@@ -1211,7 +1211,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1211
1211
|
parseNotificationSettingsSnapshot(parsedRaw);
|
|
1212
1212
|
} catch (error) {
|
|
1213
1213
|
if (!(error instanceof Error) || error.message !== "gjc_notify_daemon_invalid_configuration") throw error;
|
|
1214
|
-
this.#
|
|
1214
|
+
this.#hasInvalidNotificationGlobal = true;
|
|
1215
1215
|
}
|
|
1216
1216
|
}
|
|
1217
1217
|
this.#futureSchemaVersion =
|
|
@@ -1875,7 +1875,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1875
1875
|
#notificationValidationState(): NotificationValidationState {
|
|
1876
1876
|
return {
|
|
1877
1877
|
malformedConfigRoot: this.#hasMalformedConfigRoot,
|
|
1878
|
-
|
|
1878
|
+
invalidNotificationGlobal: this.#hasInvalidNotificationGlobal,
|
|
1879
1879
|
generation: this.#notificationValidationGeneration,
|
|
1880
1880
|
};
|
|
1881
1881
|
}
|
|
@@ -1900,7 +1900,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1900
1900
|
}
|
|
1901
1901
|
#restoreNotificationValidationState(state: NotificationValidationState): void {
|
|
1902
1902
|
this.#hasMalformedConfigRoot = state.malformedConfigRoot;
|
|
1903
|
-
this.#
|
|
1903
|
+
this.#hasInvalidNotificationGlobal = state.invalidNotificationGlobal;
|
|
1904
1904
|
}
|
|
1905
1905
|
#rejectAtomicNotificationRepairForMalformedRoot(patches: readonly AtomicYamlPatch[], root: unknown): void {
|
|
1906
1906
|
if (
|
|
@@ -1951,17 +1951,17 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1951
1951
|
#recomputeNotificationValidationFromRaw(): void {
|
|
1952
1952
|
if (this.#rawNotificationConfig === undefined) {
|
|
1953
1953
|
this.#hasMalformedConfigRoot = true;
|
|
1954
|
-
this.#
|
|
1954
|
+
this.#hasInvalidNotificationGlobal = false;
|
|
1955
1955
|
return;
|
|
1956
1956
|
}
|
|
1957
1957
|
try {
|
|
1958
1958
|
parseNotificationSettingsSnapshot(this.#rawNotificationConfig);
|
|
1959
1959
|
this.#hasMalformedConfigRoot = false;
|
|
1960
|
-
this.#
|
|
1960
|
+
this.#hasInvalidNotificationGlobal = false;
|
|
1961
1961
|
} catch (error) {
|
|
1962
1962
|
if (error instanceof Error && error.message === "gjc_notify_daemon_invalid_configuration") {
|
|
1963
1963
|
this.#hasMalformedConfigRoot = false;
|
|
1964
|
-
this.#
|
|
1964
|
+
this.#hasInvalidNotificationGlobal = true;
|
|
1965
1965
|
return;
|
|
1966
1966
|
}
|
|
1967
1967
|
throw error;
|
|
@@ -1973,10 +1973,10 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1973
1973
|
try {
|
|
1974
1974
|
parseNotificationSettingsSnapshot(this.#rawNotificationConfig);
|
|
1975
1975
|
this.#hasMalformedConfigRoot = false;
|
|
1976
|
-
this.#
|
|
1976
|
+
this.#hasInvalidNotificationGlobal = false;
|
|
1977
1977
|
} catch (error) {
|
|
1978
1978
|
if (error instanceof Error && error.message === "gjc_notify_daemon_invalid_configuration") {
|
|
1979
|
-
this.#
|
|
1979
|
+
this.#hasInvalidNotificationGlobal = true;
|
|
1980
1980
|
return;
|
|
1981
1981
|
}
|
|
1982
1982
|
throw error;
|
|
@@ -2054,9 +2054,9 @@ export class Settings implements NotificationSettingsReader {
|
|
|
2054
2054
|
// Setting Hooks
|
|
2055
2055
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2056
2056
|
|
|
2057
|
-
type SettingHook
|
|
2057
|
+
type SettingHook = (value: unknown, prev: unknown) => void;
|
|
2058
2058
|
|
|
2059
|
-
const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook
|
|
2059
|
+
const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook>> = {
|
|
2060
2060
|
"theme.dark": value => {
|
|
2061
2061
|
if (typeof value === "string") {
|
|
2062
2062
|
setAutoThemeMapping("dark", value);
|
|
@@ -5,56 +5,113 @@
|
|
|
5
5
|
* This handles AGENTS.md files that live in project root (not in config directories
|
|
6
6
|
* like .OpenAI code backend/ or .gemini/, which are handled by their respective providers).
|
|
7
7
|
*/
|
|
8
|
+
import * as fs from "node:fs/promises";
|
|
8
9
|
import * as path from "node:path";
|
|
9
10
|
import { registerProvider } from "../capability";
|
|
10
11
|
import { type ContextFile, contextFileCapability } from "../capability/context-file";
|
|
11
|
-
import { readFile } from "../capability/fs";
|
|
12
12
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
13
13
|
import { calculateDepth, createSourceMeta } from "./helpers";
|
|
14
14
|
|
|
15
15
|
const PROVIDER_ID = "agents-md";
|
|
16
16
|
const DISPLAY_NAME = "AGENTS.md";
|
|
17
|
+
// Bound hostile ancestor walks and instruction payloads before they reach prompt assembly.
|
|
18
|
+
const MAX_ANCESTOR_DIRECTORIES = 32;
|
|
19
|
+
const MAX_FILE_BYTES = 64 * 1024;
|
|
20
|
+
const MAX_AGGREGATE_BYTES = 256 * 1024;
|
|
21
|
+
const DIRECTORY_LIMIT_WARNING = "AGENTS.md discovery stopped after scanning 32 ancestor directories.";
|
|
22
|
+
const FILE_LIMIT_WARNING = "Skipped one or more AGENTS.md files that exceed the 64 KiB limit.";
|
|
23
|
+
const AGGREGATE_LIMIT_WARNING = "Skipped one or more AGENTS.md files that exceed the 256 KiB aggregate limit.";
|
|
24
|
+
|
|
25
|
+
export type AgentsMdReader = (
|
|
26
|
+
filePath: string,
|
|
27
|
+
maxBytes: number,
|
|
28
|
+
) => Promise<{ content: string | null; byteLength: number; tooLarge: boolean }>;
|
|
29
|
+
|
|
30
|
+
async function readBoundedAgentsMdFile(
|
|
31
|
+
filePath: string,
|
|
32
|
+
maxBytes: number,
|
|
33
|
+
): Promise<{ content: string | null; byteLength: number; tooLarge: boolean }> {
|
|
34
|
+
try {
|
|
35
|
+
const file = await fs.open(filePath, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
|
|
36
|
+
try {
|
|
37
|
+
const stats = await file.stat();
|
|
38
|
+
if (!stats.isFile()) return { content: null, byteLength: 0, tooLarge: false };
|
|
39
|
+
const bytes = Buffer.alloc(maxBytes + 1);
|
|
40
|
+
let bytesRead = 0;
|
|
41
|
+
while (bytesRead < bytes.length) {
|
|
42
|
+
const result = await file.read(bytes, bytesRead, bytes.length - bytesRead, bytesRead);
|
|
43
|
+
if (result.bytesRead === 0) break;
|
|
44
|
+
bytesRead += result.bytesRead;
|
|
45
|
+
}
|
|
46
|
+
if (bytesRead > maxBytes) return { content: null, byteLength: bytesRead, tooLarge: true };
|
|
47
|
+
return {
|
|
48
|
+
content: new TextDecoder().decode(bytes.subarray(0, bytesRead)),
|
|
49
|
+
byteLength: bytesRead,
|
|
50
|
+
tooLarge: false,
|
|
51
|
+
};
|
|
52
|
+
} finally {
|
|
53
|
+
await file.close();
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
return { content: null, byteLength: 0, tooLarge: false };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
17
59
|
|
|
18
60
|
/**
|
|
19
61
|
* Load standalone AGENTS.md files.
|
|
20
62
|
*/
|
|
21
|
-
async function loadAgentsMd(
|
|
63
|
+
export async function loadAgentsMd(
|
|
64
|
+
ctx: LoadContext,
|
|
65
|
+
readCandidate: AgentsMdReader = readBoundedAgentsMdFile,
|
|
66
|
+
): Promise<LoadResult<ContextFile>> {
|
|
22
67
|
const items: ContextFile[] = [];
|
|
23
68
|
const warnings: string[] = [];
|
|
24
|
-
|
|
25
|
-
// Walk up from cwd looking for AGENTS.md files
|
|
26
69
|
let current = ctx.cwd;
|
|
70
|
+
let scannedDirectories = 0;
|
|
71
|
+
let aggregateBytes = 0;
|
|
72
|
+
let omittedOversizedFile = false;
|
|
73
|
+
let omittedAggregateFile = false;
|
|
27
74
|
|
|
28
75
|
while (true) {
|
|
76
|
+
scannedDirectories += 1;
|
|
29
77
|
const candidate = path.join(current, "AGENTS.md");
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
if (content !== null) {
|
|
33
|
-
const parent = path.dirname(candidate);
|
|
34
|
-
const baseName = parent.split(path.sep).pop() ?? "";
|
|
78
|
+
const parent = path.dirname(candidate);
|
|
79
|
+
const baseName = parent.split(path.sep).pop() ?? "";
|
|
35
80
|
|
|
36
|
-
|
|
81
|
+
if (!baseName.startsWith(".")) {
|
|
82
|
+
const remainingAggregateBytes = MAX_AGGREGATE_BYTES - aggregateBytes;
|
|
83
|
+
const allowedBytes = Math.min(MAX_FILE_BYTES, remainingAggregateBytes);
|
|
84
|
+
const result = await readCandidate(candidate, allowedBytes);
|
|
85
|
+
if (result.tooLarge) {
|
|
86
|
+
if (allowedBytes < MAX_FILE_BYTES) omittedAggregateFile = true;
|
|
87
|
+
else omittedOversizedFile = true;
|
|
88
|
+
} else if (result.content !== null) {
|
|
37
89
|
const fileDir = path.dirname(candidate);
|
|
38
|
-
const calculatedDepth = calculateDepth(ctx.cwd, fileDir, path.sep);
|
|
39
|
-
|
|
40
90
|
items.push({
|
|
41
91
|
path: candidate,
|
|
42
|
-
content,
|
|
92
|
+
content: result.content,
|
|
43
93
|
level: "project",
|
|
44
|
-
depth:
|
|
94
|
+
depth: calculateDepth(ctx.cwd, fileDir, path.sep),
|
|
45
95
|
_source: createSourceMeta(PROVIDER_ID, candidate, "project"),
|
|
46
96
|
});
|
|
97
|
+
aggregateBytes += result.byteLength;
|
|
47
98
|
}
|
|
48
99
|
}
|
|
49
100
|
|
|
50
|
-
|
|
101
|
+
const stopDirectory = ctx.repoRoot ?? ctx.home;
|
|
102
|
+
if (current === stopDirectory) break;
|
|
103
|
+
if (scannedDirectories === MAX_ANCESTOR_DIRECTORIES) {
|
|
104
|
+
warnings.push(DIRECTORY_LIMIT_WARNING);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
51
107
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
current = parent;
|
|
108
|
+
const parentDirectory = path.dirname(current);
|
|
109
|
+
if (parentDirectory === current) break;
|
|
110
|
+
current = parentDirectory;
|
|
56
111
|
}
|
|
57
112
|
|
|
113
|
+
if (omittedOversizedFile) warnings.push(FILE_LIMIT_WARNING);
|
|
114
|
+
if (omittedAggregateFile) warnings.push(AGGREGATE_LIMIT_WARNING);
|
|
58
115
|
return { items, warnings };
|
|
59
116
|
}
|
|
60
117
|
|
|
@@ -324,25 +324,21 @@ export function resolveGjcTmuxBinary(options: ResolveGjcTmuxBinaryOptions = {}):
|
|
|
324
324
|
return { command: explicit, isPsmux, viaExplicitOverride: true };
|
|
325
325
|
}
|
|
326
326
|
if (platform === "win32") {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
const executablePath = resolveBinaryPath(candidate);
|
|
327
|
+
for (const command of ["psmux", "pmux"] as const) {
|
|
328
|
+
const executablePath = resolveBinaryPath(command);
|
|
330
329
|
if (!executablePath) continue;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (!identity)
|
|
335
|
-
throw new Error(`gjc_tmux_provider_ambiguous: Windows ${candidate} executable identity is unavailable`);
|
|
336
|
-
candidates.push({ command: candidate, identity });
|
|
330
|
+
if (!activeExecutableIdentityResolver(executablePath))
|
|
331
|
+
throw new Error(`gjc_tmux_provider_ambiguous: Windows ${command} executable identity is unavailable`);
|
|
332
|
+
return { command, isPsmux: true, viaExplicitOverride: false };
|
|
337
333
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return { command: candidates[0]!.command, isPsmux: true, viaExplicitOverride: false };
|
|
334
|
+
const tmuxPath = resolveBinaryPath("tmux");
|
|
335
|
+
if (tmuxPath) {
|
|
336
|
+
const isPsmux = outputMentionsPsmux(probeVersionOutput(tmuxPath, runner));
|
|
337
|
+
if (isPsmux && !activeExecutableIdentityResolver(tmuxPath))
|
|
338
|
+
throw new Error("gjc_tmux_provider_ambiguous: Windows tmux executable identity is unavailable");
|
|
339
|
+
return { command: "tmux", isPsmux, viaExplicitOverride: false };
|
|
345
340
|
}
|
|
341
|
+
return { command: "tmux", isPsmux: false, viaExplicitOverride: false };
|
|
346
342
|
}
|
|
347
343
|
const tmuxPath = resolveBinaryPath("tmux");
|
|
348
344
|
if (tmuxPath) {
|
|
@@ -5,7 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import type { AssistantMessage } from "@gajae-code/ai";
|
|
6
6
|
import { normalizePathForComparison, postmortem } from "@gajae-code/utils";
|
|
7
7
|
import { withFileLock } from "../config/file-lock";
|
|
8
|
-
import { sessionRuntimeDir } from "./session-layout";
|
|
8
|
+
import { sessionRoot, sessionRuntimeDir } from "./session-layout";
|
|
9
9
|
import {
|
|
10
10
|
isValidOwnerIntent,
|
|
11
11
|
lifecyclePaths,
|
|
@@ -1022,6 +1022,7 @@ export async function persistCoordinatorRuntimeStateFromPostmortem(
|
|
|
1022
1022
|
const stateFile = runtimeStateFileForContext(context);
|
|
1023
1023
|
if (!stateFile) return;
|
|
1024
1024
|
const identity = normalizedIdentity(context);
|
|
1025
|
+
const ownerSessionRoot = sessionRoot(context.cwd, identity.sessionId);
|
|
1025
1026
|
const ownerTerminalVerdict = context.ownerTerminal
|
|
1026
1027
|
? await observeOwnerTerminalPostmortem(reason, context.ownerTerminal, identity.sessionId)
|
|
1027
1028
|
: null;
|
|
@@ -1091,7 +1092,16 @@ export async function persistCoordinatorRuntimeStateFromPostmortem(
|
|
|
1091
1092
|
await writeStateFileSync(stateFile, payload);
|
|
1092
1093
|
}),
|
|
1093
1094
|
),
|
|
1094
|
-
)
|
|
1095
|
+
).catch(error => {
|
|
1096
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
1097
|
+
try {
|
|
1098
|
+
fsSync.lstatSync(ownerSessionRoot);
|
|
1099
|
+
} catch (rootError) {
|
|
1100
|
+
if ((rootError as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
throw error;
|
|
1104
|
+
});
|
|
1095
1105
|
}
|
|
1096
1106
|
|
|
1097
1107
|
export function registerCoordinatorRuntimeStateFinalizer(context: RuntimeStateContext): () => void {
|
|
@@ -228,7 +228,7 @@ function runTmux(
|
|
|
228
228
|
);
|
|
229
229
|
}
|
|
230
230
|
if (result.exitCode === 0) return result.stdout?.toString() ?? "";
|
|
231
|
-
throw new Error(result.stderr?.toString().trim() ||
|
|
231
|
+
throw new Error(result.stderr?.toString().trim() || `${tmuxCommand} ${args.join(" ")} failed`);
|
|
232
232
|
}
|
|
233
233
|
function normalizeExactTmuxTarget(sessionTarget: string, env: NodeJS.ProcessEnv, kind: "session" | "option"): string {
|
|
234
234
|
if (sessionTarget.startsWith("$")) return sessionTarget;
|
|
@@ -407,6 +407,13 @@ function psmuxAuthorityEnvironments(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv[]
|
|
|
407
407
|
if (authorities.length > 0)
|
|
408
408
|
return authorities.map(authority => environmentForProviderAuthority(env, authority));
|
|
409
409
|
}
|
|
410
|
+
if (!ambient.viaExplicitOverride && !ambientAvailable) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
"gjc_tmux_provider_unavailable — GJC searched for psmux, pmux, and tmux on PATH. " +
|
|
413
|
+
"Install psmux from https://github.com/psmux/psmux for native Windows support, use WSL with real tmux, " +
|
|
414
|
+
"or set GJC_TMUX_COMMAND (and GJC_PSMUX_COMMAND when selecting a psmux compatibility alias).",
|
|
415
|
+
);
|
|
416
|
+
}
|
|
410
417
|
}
|
|
411
418
|
if (ambient.isPsmux) throw new Error("gjc_tmux_provider_authority_unavailable");
|
|
412
419
|
return [env];
|