@gajae-code/coding-agent 0.9.6 → 0.10.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.
- package/CHANGELOG.md +45 -0
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/daemon-cli.d.ts +2 -0
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/malloc-env-guard.d.ts +65 -0
- package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +3 -1
- package/dist/types/cli/session-picker.d.ts +4 -3
- package/dist/types/cli/update-cli.d.ts +22 -3
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/commands/daemon.d.ts +6 -2
- package/dist/types/commands/launch.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +1 -1
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +47 -7
- package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
- package/dist/types/coordinator-mcp/server.d.ts +67 -6
- package/dist/types/daemon/control-types.d.ts +10 -0
- package/dist/types/daemon/operator-contract.d.ts +47 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/gjc-runtime/launch-tmux.d.ts +26 -7
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +34 -3
- package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +309 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +49 -17
- package/dist/types/internal-urls/local-protocol.d.ts +13 -9
- package/dist/types/main.d.ts +66 -2
- package/dist/types/modes/acp/acp-agent.d.ts +19 -2
- package/dist/types/modes/acp/acp-mode.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/gajae-pet-widget.d.ts +59 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
- package/dist/types/modes/components/pet-selector.d.ts +11 -0
- package/dist/types/modes/components/session-selector.d.ts +22 -9
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/controllers/event-controller.d.ts +4 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +24 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
- package/dist/types/modes/theme/defaults/index.d.ts +568 -568
- package/dist/types/modes/types.d.ts +28 -2
- package/dist/types/modes/utils/abort-message.d.ts +1 -1
- package/dist/types/modes/utils/irc-message.d.ts +38 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
- package/dist/types/notifications/config.d.ts +34 -0
- package/dist/types/notifications/daemon-paths.d.ts +7 -0
- package/dist/types/notifications/index.d.ts +27 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
- package/dist/types/notifications/notification-service.d.ts +144 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
- package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
- package/dist/types/notifications/telegram-daemon.d.ts +130 -3
- package/dist/types/notifications/telegram-reference.d.ts +19 -2
- package/dist/types/notifications/topic-registry.d.ts +24 -4
- package/dist/types/registry/agent-registry.d.ts +2 -0
- package/dist/types/rlm/index.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +26 -2
- package/dist/types/session/cache-economics.d.ts +79 -0
- package/dist/types/session/provider-safety-stop.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +124 -4
- package/dist/types/session/session-storage.d.ts +155 -13
- package/dist/types/setup/credential-auto-import.d.ts +2 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +8 -0
- package/dist/types/tools/ask-answer-registry.d.ts +5 -0
- package/dist/types/tools/ask.d.ts +25 -1
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
- package/dist/types/tools/index.d.ts +58 -6
- package/dist/types/tools/telegram-send.d.ts +7 -0
- package/dist/types/utils/external-editor.d.ts +2 -0
- package/package.json +9 -9
- package/scripts/g004-tmux-smoke.ts +196 -36
- package/src/cli/daemon-cli.ts +19 -29
- package/src/cli/malloc-env-guard.test.ts +104 -0
- package/src/cli/malloc-env-guard.ts +149 -0
- package/src/cli/notify-cli.ts +78 -14
- package/src/cli/session-picker.ts +23 -40
- package/src/cli/update-cli.ts +56 -37
- package/src/cli.ts +50 -10
- package/src/commands/daemon.ts +19 -5
- package/src/commands/harness.ts +515 -47
- package/src/commands/launch.ts +11 -3
- package/src/commands/notify.ts +5 -1
- package/src/commands/session.ts +6 -1
- package/src/config/keybindings.ts +5 -0
- package/src/config/model-profile-activation.ts +1 -1
- package/src/config/model-profiles.ts +14 -14
- package/src/config/settings-schema.ts +41 -1
- package/src/coordinator-mcp/model-preset.ts +100 -0
- package/src/coordinator-mcp/server.ts +2653 -425
- package/src/daemon/control-types.ts +11 -0
- package/src/daemon/operator-contract.ts +121 -0
- package/src/deep-interview/render-middleware.ts +67 -2
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
- package/src/gjc-runtime/launch-tmux.ts +702 -135
- package/src/gjc-runtime/session-state-sidecar.ts +756 -135
- package/src/gjc-runtime/team-runtime.ts +6 -0
- package/src/gjc-runtime/tmux-common.ts +26 -7
- package/src/gjc-runtime/tmux-gc.ts +52 -9
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +185 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +2060 -0
- package/src/gjc-runtime/tmux-sessions.ts +871 -68
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/internal-urls/local-protocol.ts +29 -10
- package/src/main.ts +298 -75
- package/src/modes/acp/acp-agent.ts +728 -98
- package/src/modes/acp/acp-mode.ts +29 -2
- package/src/modes/components/assistant-message.ts +32 -3
- package/src/modes/components/bash-execution.ts +43 -13
- package/src/modes/components/custom-editor.ts +5 -0
- package/src/modes/components/gajae-pet-widget.ts +360 -0
- package/src/modes/components/hook-selector.ts +12 -0
- package/src/modes/components/irc-sidebar.ts +206 -0
- package/src/modes/components/pet-selector.ts +55 -0
- package/src/modes/components/session-selector.ts +222 -221
- package/src/modes/components/settings-selector.ts +10 -0
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/components/user-message.ts +26 -9
- package/src/modes/controllers/command-controller.ts +28 -2
- package/src/modes/controllers/event-controller.ts +159 -36
- package/src/modes/controllers/extension-ui-controller.ts +36 -17
- package/src/modes/controllers/input-controller.ts +8 -2
- package/src/modes/controllers/selector-controller.ts +62 -15
- package/src/modes/interactive-mode.ts +183 -7
- package/src/modes/irc-observation-ledger.ts +127 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
- package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
- package/src/modes/types.ts +34 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/irc-message.ts +138 -0
- package/src/modes/utils/ui-helpers.ts +216 -50
- package/src/notifications/config.ts +44 -0
- package/src/notifications/daemon-paths.ts +8 -0
- package/src/notifications/index.ts +535 -75
- package/src/notifications/lifecycle-control-runtime.ts +930 -77
- package/src/notifications/notification-service.ts +710 -0
- package/src/notifications/rate-limit-pool.ts +46 -12
- package/src/notifications/telegram-daemon-cli.ts +2 -0
- package/src/notifications/telegram-daemon-control.ts +27 -2
- package/src/notifications/telegram-daemon.ts +635 -94
- package/src/notifications/telegram-reference.ts +73 -13
- package/src/notifications/topic-registry.ts +86 -9
- package/src/prompts/system/irc-peer-roster.md +1 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/registry/agent-registry.ts +3 -0
- package/src/sdk.ts +52 -1
- package/src/secrets/obfuscator.ts +4 -2
- package/src/session/agent-session.ts +396 -145
- package/src/session/cache-economics.ts +275 -0
- package/src/session/provider-safety-stop.ts +9 -0
- package/src/session/session-dump-format.ts +6 -0
- package/src/session/session-manager.ts +809 -39
- package/src/session/session-storage.ts +582 -38
- package/src/slash-commands/builtin-registry.ts +143 -9
- package/src/task/executor.ts +12 -3
- package/src/task/index.ts +8 -0
- package/src/task/receipt.ts +7 -2
- package/src/task/types.ts +26 -0
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +355 -33
- package/src/tools/browser/tab-supervisor.ts +63 -6
- package/src/tools/browser.ts +8 -3
- package/src/tools/index.ts +81 -6
- package/src/tools/sqlite-reader.ts +8 -4
- package/src/tui/output-block.ts +18 -6
- package/src/utils/external-editor.ts +6 -1
|
@@ -26,6 +26,13 @@ export declare class TelegramSendTool implements AgentTool<typeof telegramSendSc
|
|
|
26
26
|
readonly strict = true;
|
|
27
27
|
constructor(session: ToolSession);
|
|
28
28
|
static createIf(session: ToolSession): TelegramSendTool | null;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve `requested` against the workspace root and confine it via realpath:
|
|
31
|
+
* blocks absolute paths outside the project, `..` traversal, and symlinks that
|
|
32
|
+
* escape the root. Returns the resolved real path of a regular file, or an
|
|
33
|
+
* error message. This is the egress safety boundary — the model can only send
|
|
34
|
+
* files that genuinely live inside the session workspace.
|
|
35
|
+
*/
|
|
29
36
|
private resolveContainedFile;
|
|
30
37
|
execute(_toolCallId: string, params: TelegramSendParams, _signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<TelegramSendDetails>, _context?: AgentToolContext): Promise<AgentToolResult<TelegramSendDetails>>;
|
|
31
38
|
}
|
|
@@ -8,6 +8,8 @@ export interface OpenInEditorOptions {
|
|
|
8
8
|
/** Keep the file's trailing newline instead of trimming it from the returned text. */
|
|
9
9
|
trimTrailingNewline?: boolean;
|
|
10
10
|
}
|
|
11
|
+
/** Remove one platform-standard trailing line terminator from editor content. */
|
|
12
|
+
export declare function trimEditorTrailingNewline(text: string): string;
|
|
11
13
|
/**
|
|
12
14
|
* Opens `content` in the user's external editor and returns the edited text.
|
|
13
15
|
* Returns `null` if the editor exits with a non-zero code.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/coding-agent",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"description": "Gajae Code CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "bun scripts/build-binary.ts",
|
|
32
32
|
"check": "biome check . && bun run check:types",
|
|
33
|
-
"check:types": "
|
|
33
|
+
"check:types": "tsc -p tsconfig.json --noEmit",
|
|
34
34
|
"lint": "biome lint .",
|
|
35
35
|
"test": "bun test",
|
|
36
36
|
"bench:context": "bun run bench/context-optimization.bench.ts",
|
|
@@ -46,15 +46,15 @@
|
|
|
46
46
|
"verify:insane-vendor": "bun scripts/verify-insane-vendor.ts"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@agentclientprotocol/sdk": "
|
|
49
|
+
"@agentclientprotocol/sdk": "1.2.1",
|
|
50
50
|
"@babel/parser": "^7.29.3",
|
|
51
51
|
"@mozilla/readability": "^0.6.0",
|
|
52
|
-
"@gajae-code/stats": "0.
|
|
53
|
-
"@gajae-code/agent-core": "0.
|
|
54
|
-
"@gajae-code/ai": "0.
|
|
55
|
-
"@gajae-code/natives": "0.
|
|
56
|
-
"@gajae-code/tui": "0.
|
|
57
|
-
"@gajae-code/utils": "0.
|
|
52
|
+
"@gajae-code/stats": "0.10.0",
|
|
53
|
+
"@gajae-code/agent-core": "0.10.0",
|
|
54
|
+
"@gajae-code/ai": "0.10.0",
|
|
55
|
+
"@gajae-code/natives": "0.10.0",
|
|
56
|
+
"@gajae-code/tui": "0.10.0",
|
|
57
|
+
"@gajae-code/utils": "0.10.0",
|
|
58
58
|
"@puppeteer/browsers": "^2.13.0",
|
|
59
59
|
"@types/turndown": "5.0.6",
|
|
60
60
|
"@xterm/headless": "^6.0.0",
|
|
@@ -1,100 +1,260 @@
|
|
|
1
|
-
// G004 real-tmux smoke: exercises forceCloseGjcTmuxSession
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// G004 real-tmux smoke: exercises forceCloseGjcTmuxSession refusal boundaries
|
|
2
|
+
// against live tmux sessions. Generation-bound successful TERM/verdict/cleanup is
|
|
3
|
+
// covered by the issue evidence harness; this smoke proves incomplete and non-GJC
|
|
4
|
+
// live owners are never hard-killed.
|
|
5
5
|
import assert from "node:assert";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
6
10
|
|
|
7
11
|
import {
|
|
8
12
|
buildGjcTmuxExactOptionTarget,
|
|
13
|
+
buildGjcTmuxExactSessionTarget,
|
|
9
14
|
buildGjcTmuxProfileCommands,
|
|
10
15
|
resolveGjcTmuxCommand,
|
|
11
16
|
} from "../src/gjc-runtime/tmux-common";
|
|
12
17
|
import { forceCloseGjcTmuxSession, removeGjcTmuxSession, statusGjcTmuxSession } from "../src/gjc-runtime/tmux-sessions";
|
|
13
18
|
|
|
14
|
-
const
|
|
19
|
+
const runId = randomUUID().slice(0, 8);
|
|
20
|
+
const suffix = `${process.pid}-${runId}`;
|
|
21
|
+
const privateTmpdir = mkdtempSync(path.join(tmpdir(), "g4-"));
|
|
22
|
+
chmodSync(privateTmpdir, 0o700);
|
|
23
|
+
const socket = `g4-${runId}`;
|
|
24
|
+
const wrapper = path.join(privateTmpdir, "tmux-private.sh");
|
|
25
|
+
const scopeUnit = `gjc-g004-${suffix}.scope`;
|
|
26
|
+
const tmuxBootstrapEnv: NodeJS.ProcessEnv = {
|
|
27
|
+
...process.env,
|
|
28
|
+
TMUX: "",
|
|
29
|
+
TMUX_PANE: "",
|
|
30
|
+
TMUX_TMPDIR: privateTmpdir,
|
|
31
|
+
};
|
|
32
|
+
const tmuxBinary = resolveGjcTmuxCommand(tmuxBootstrapEnv);
|
|
33
|
+
const privateEnv: NodeJS.ProcessEnv = {
|
|
34
|
+
...tmuxBootstrapEnv,
|
|
35
|
+
GJC_TMUX_COMMAND: wrapper,
|
|
36
|
+
};
|
|
37
|
+
let scopeRunner: Bun.Subprocess<"ignore", "ignore", "pipe"> | null = null;
|
|
15
38
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
39
|
+
function shellQuote(value: string): string {
|
|
40
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function bounded(value: string): string {
|
|
44
|
+
return value.trim().slice(0, 512) || "no diagnostic";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sh(args: string[]): { code: number; out: string; err: string } {
|
|
48
|
+
try {
|
|
49
|
+
const r = Bun.spawnSync([wrapper, ...args], { env: privateEnv, stdout: "pipe", stderr: "pipe" });
|
|
50
|
+
return { code: r.exitCode, out: r.stdout.toString().trim(), err: r.stderr.toString().trim() };
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return { code: -1, out: "", err: bounded(String(error)) };
|
|
53
|
+
}
|
|
19
54
|
}
|
|
20
55
|
|
|
21
56
|
function makeRawSession(name: string): void {
|
|
22
57
|
const r = sh(["new-session", "-d", "-s", name, "sleep 600"]);
|
|
23
|
-
if (r.code !== 0) throw new Error(`failed to create tmux session ${name}: ${r.err}`);
|
|
58
|
+
if (r.code !== 0) throw new Error(`failed to create private tmux session ${name}: ${bounded(r.err)}`);
|
|
24
59
|
}
|
|
25
60
|
|
|
26
61
|
function tagAsGjc(name: string, sessionId?: string): void {
|
|
27
|
-
const target = buildGjcTmuxExactOptionTarget(name);
|
|
28
|
-
for (const cmd of buildGjcTmuxProfileCommands(target,
|
|
62
|
+
const target = buildGjcTmuxExactOptionTarget(name, { env: privateEnv });
|
|
63
|
+
for (const cmd of buildGjcTmuxProfileCommands(target, privateEnv, { sessionId })) {
|
|
29
64
|
const r = sh(cmd.args);
|
|
30
|
-
if (r.code !== 0) throw new Error(`failed to tag ${name} (${cmd.description}): ${r.err}`);
|
|
65
|
+
if (r.code !== 0) throw new Error(`failed to tag ${name} (${cmd.description}): ${bounded(r.err)}`);
|
|
31
66
|
}
|
|
32
67
|
}
|
|
33
68
|
|
|
34
69
|
function exists(name: string): boolean {
|
|
35
|
-
return sh(["has-session", "-t",
|
|
70
|
+
return sh(["has-session", "-t", buildGjcTmuxExactSessionTarget(name, { env: privateEnv })]).code === 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isPrivateSessionAbsent(name: string): boolean {
|
|
74
|
+
const result = sh(["has-session", "-t", buildGjcTmuxExactSessionTarget(name, { env: privateEnv })]);
|
|
75
|
+
return result.code !== 0 && /(?:no server running|can't find session|no sessions)/i.test(result.err);
|
|
36
76
|
}
|
|
37
77
|
|
|
38
|
-
function
|
|
39
|
-
sh([
|
|
78
|
+
function privateServerPid(session: string): number | null {
|
|
79
|
+
const result = sh([
|
|
80
|
+
"display-message",
|
|
81
|
+
"-p",
|
|
82
|
+
"-t",
|
|
83
|
+
buildGjcTmuxExactOptionTarget(session, { env: privateEnv }),
|
|
84
|
+
"#{pid}",
|
|
85
|
+
]);
|
|
86
|
+
const pid = Number.parseInt(result.out, 10);
|
|
87
|
+
return result.code === 0 && Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function scopeIsActive(): boolean {
|
|
91
|
+
return (
|
|
92
|
+
Bun.spawnSync(["systemctl", "--user", "is-active", "--quiet", scopeUnit], { stdout: "ignore", stderr: "ignore" })
|
|
93
|
+
.exitCode === 0
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function serverIsProvenInScope(session: string): boolean {
|
|
98
|
+
const pid = privateServerPid(session);
|
|
99
|
+
if (pid == null) return false;
|
|
100
|
+
const controlGroup = Bun.spawnSync(
|
|
101
|
+
["systemctl", "--user", "show", scopeUnit, "--property=ControlGroup", "--value"],
|
|
102
|
+
{ stdout: "pipe", stderr: "ignore" },
|
|
103
|
+
);
|
|
104
|
+
const group = controlGroup.stdout.toString().trim();
|
|
105
|
+
if (controlGroup.exitCode !== 0 || !group.startsWith("/")) return false;
|
|
106
|
+
try {
|
|
107
|
+
return readFileSync(`/proc/${pid}/cgroup`, "utf8")
|
|
108
|
+
.split("\n")
|
|
109
|
+
.some(line => line.endsWith(group));
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function makeFirstPrivateSession(name: string): Promise<void> {
|
|
116
|
+
if (process.platform !== "linux") {
|
|
117
|
+
makeRawSession(name);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
scopeRunner = Bun.spawn(
|
|
122
|
+
[
|
|
123
|
+
"systemd-run",
|
|
124
|
+
"--user",
|
|
125
|
+
"--quiet",
|
|
126
|
+
"--collect",
|
|
127
|
+
`--setenv=TMUX_TMPDIR=${privateTmpdir}`,
|
|
128
|
+
"--scope",
|
|
129
|
+
`--unit=${scopeUnit}`,
|
|
130
|
+
wrapper,
|
|
131
|
+
"new-session",
|
|
132
|
+
"-d",
|
|
133
|
+
"-s",
|
|
134
|
+
name,
|
|
135
|
+
"sleep 600",
|
|
136
|
+
],
|
|
137
|
+
{ env: privateEnv, stdout: "ignore", stderr: "pipe" },
|
|
138
|
+
);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
throw new Error(`failed to provision private user scope ${scopeUnit}: ${bounded(String(error))}`);
|
|
141
|
+
}
|
|
142
|
+
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
143
|
+
if (scopeIsActive() && exists(name) && serverIsProvenInScope(name)) return;
|
|
144
|
+
await Bun.sleep(50);
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`failed to provision a proven private user scope ${scopeUnit}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function cleanupOwnedResources(names: string[]): Promise<void> {
|
|
150
|
+
const failures: string[] = [];
|
|
151
|
+
for (const name of names) {
|
|
152
|
+
const result = sh(["kill-session", "-t", buildGjcTmuxExactSessionTarget(name, { env: privateEnv })]);
|
|
153
|
+
if (result.code !== 0 && !isPrivateSessionAbsent(name)) failures.push(`session ${name}: ${bounded(result.err)}`);
|
|
154
|
+
}
|
|
155
|
+
const server = sh(["kill-server"]);
|
|
156
|
+
if (server.code !== 0) {
|
|
157
|
+
const probe = sh(["list-sessions"]);
|
|
158
|
+
if (!/(?:no server running|failed to connect to server|error connecting to)/i.test(probe.err))
|
|
159
|
+
failures.push(`server: ${bounded(server.err)}`);
|
|
160
|
+
}
|
|
161
|
+
if (process.platform === "linux") {
|
|
162
|
+
const stop = Bun.spawnSync(["systemctl", "--user", "stop", scopeUnit], { stdout: "pipe", stderr: "pipe" });
|
|
163
|
+
const state = Bun.spawnSync(
|
|
164
|
+
["systemctl", "--user", "show", scopeUnit, "--property=ActiveState", "--property=LoadState", "--value"],
|
|
165
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
166
|
+
);
|
|
167
|
+
const states = state.stdout.toString().trim().split("\n");
|
|
168
|
+
const scopeAbsent = state.exitCode === 0 && (states.includes("inactive") || states.includes("not-found"));
|
|
169
|
+
if (stop.exitCode !== 0 && !scopeAbsent) failures.push(`scope stop: ${bounded(stop.stderr.toString())}`);
|
|
170
|
+
if (!scopeAbsent)
|
|
171
|
+
failures.push(`scope remains present: ${bounded(state.stderr.toString() || state.stdout.toString())}`);
|
|
172
|
+
if (scopeRunner != null) {
|
|
173
|
+
const exited = await Promise.race([scopeRunner.exited.then(() => true), Bun.sleep(3_000).then(() => false)]);
|
|
174
|
+
if (!exited) {
|
|
175
|
+
try {
|
|
176
|
+
scopeRunner.kill();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
failures.push(`scope runner: ${bounded(String(error))}`);
|
|
179
|
+
}
|
|
180
|
+
const killed = await Promise.race([
|
|
181
|
+
scopeRunner.exited.then(() => true),
|
|
182
|
+
Bun.sleep(1_000).then(() => false),
|
|
183
|
+
]);
|
|
184
|
+
if (!killed) failures.push("scope runner did not exit after scope cleanup");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
rmSync(privateTmpdir, { recursive: true });
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (existsSync(privateTmpdir)) failures.push(`temporary directory: ${bounded(String(error))}`);
|
|
192
|
+
}
|
|
193
|
+
if (existsSync(privateTmpdir)) failures.push("temporary directory remains present");
|
|
194
|
+
if (failures.length > 0) throw new Error(`g004 cleanup incomplete: ${failures.join("; ")}`);
|
|
40
195
|
}
|
|
41
196
|
|
|
42
|
-
const suffix = `${process.pid}-${Date.now()}`;
|
|
43
197
|
const live = `gjc_g004live_${suffix}`;
|
|
44
198
|
const raw = `g004raw_${suffix}`;
|
|
45
199
|
const mism = `gjc_g004mism_${suffix}`;
|
|
46
|
-
const cleanup
|
|
200
|
+
const cleanup = [live, raw, mism];
|
|
47
201
|
|
|
48
202
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
203
|
+
writeFileSync(wrapper, `#!/bin/sh\nexec ${shellQuote(tmuxBinary)} -L ${shellQuote(socket)} "$@"\n`, { mode: 0o700 });
|
|
204
|
+
chmodSync(wrapper, 0o700);
|
|
205
|
+
|
|
206
|
+
// 1. Incompletely tagged LIVE session: remove refuses and force-close fails closed.
|
|
207
|
+
await makeFirstPrivateSession(live);
|
|
51
208
|
tagAsGjc(live, "sess-g004");
|
|
52
|
-
const status = statusGjcTmuxSession(live);
|
|
209
|
+
const status = statusGjcTmuxSession(live, privateEnv);
|
|
53
210
|
assert.equal(status.profile, "1", "session must be recognized as GJC-managed");
|
|
54
211
|
assert.ok(status.panePids.length > 0, "session must have a live pane (sleep)");
|
|
55
|
-
|
|
212
|
+
process.stdout.write(`[g004] incomplete GJC session up: ${live} panePids=${status.panePids.length}\n`);
|
|
56
213
|
|
|
57
214
|
let removeRefused = false;
|
|
58
215
|
try {
|
|
59
|
-
removeGjcTmuxSession(live);
|
|
216
|
+
removeGjcTmuxSession(live, privateEnv);
|
|
60
217
|
} catch (e) {
|
|
61
218
|
removeRefused = /gjc_tmux_session_live/.test(String(e));
|
|
62
219
|
}
|
|
63
220
|
assert.ok(removeRefused, "removeGjcTmuxSession must REFUSE a live pane");
|
|
64
|
-
console.log("[g004] remove refused live session (expected)");
|
|
65
221
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
222
|
+
let ownerUnverifiable = false;
|
|
223
|
+
try {
|
|
224
|
+
await forceCloseGjcTmuxSession(live, privateEnv, "sess-g004");
|
|
225
|
+
} catch (e) {
|
|
226
|
+
ownerUnverifiable = /gjc_tmux_owner_unverifiable/.test(String(e));
|
|
227
|
+
}
|
|
228
|
+
assert.ok(ownerUnverifiable, "force-close must refuse incomplete owner provenance");
|
|
229
|
+
assert.ok(exists(live), "incompletely tagged session must be left untouched");
|
|
230
|
+
process.stdout.write("[g004] force-close refused incomplete owner provenance (expected)\n");
|
|
70
231
|
|
|
71
232
|
// 2. Non-GJC (untagged) session: force-close must refuse.
|
|
72
233
|
makeRawSession(raw);
|
|
73
234
|
let notManaged = false;
|
|
74
235
|
try {
|
|
75
|
-
forceCloseGjcTmuxSession(raw,
|
|
236
|
+
await forceCloseGjcTmuxSession(raw, privateEnv);
|
|
76
237
|
} catch (e) {
|
|
77
|
-
notManaged = /gjc_tmux_session_(not_managed|not_found)/.test(String(e));
|
|
238
|
+
notManaged = /gjc_tmux_session_(not_managed|not_found|untagged)/.test(String(e));
|
|
78
239
|
}
|
|
79
240
|
assert.ok(notManaged, "force-close must refuse a non-GJC tmux session");
|
|
80
241
|
assert.ok(exists(raw), "non-GJC session must be left untouched");
|
|
81
|
-
|
|
82
|
-
console.log("[g004] force-close refused + preserved non-GJC session (expected)");
|
|
242
|
+
process.stdout.write("[g004] force-close refused + preserved non-GJC session (expected)\n");
|
|
83
243
|
|
|
84
244
|
// 3. GJC session with a MISMATCHED expected session id: must refuse.
|
|
85
245
|
makeRawSession(mism);
|
|
86
246
|
tagAsGjc(mism, "sess-real");
|
|
87
247
|
let idMismatch = false;
|
|
88
248
|
try {
|
|
89
|
-
forceCloseGjcTmuxSession(mism,
|
|
249
|
+
await forceCloseGjcTmuxSession(mism, privateEnv, "sess-WRONG");
|
|
90
250
|
} catch (e) {
|
|
91
251
|
idMismatch = /gjc_tmux_session_id_mismatch/.test(String(e));
|
|
92
252
|
}
|
|
93
253
|
assert.ok(idMismatch, "force-close must refuse on session-id mismatch");
|
|
94
254
|
assert.ok(exists(mism), "mismatched session must be left untouched");
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
console.log("[g004] PASS: forceCloseGjcTmuxSession verified against live tmux");
|
|
255
|
+
process.stdout.write("[g004] force-close refused on session-id mismatch (expected)\n");
|
|
98
256
|
} finally {
|
|
99
|
-
|
|
257
|
+
await cleanupOwnedResources(cleanup);
|
|
100
258
|
}
|
|
259
|
+
|
|
260
|
+
process.stdout.write("[g004] PASS: forceCloseGjcTmuxSession refusal boundaries verified against live tmux\n");
|
package/src/cli/daemon-cli.ts
CHANGED
|
@@ -12,8 +12,14 @@ import type {
|
|
|
12
12
|
DaemonKind,
|
|
13
13
|
DaemonOperationOptions,
|
|
14
14
|
DaemonOperationResult,
|
|
15
|
-
DaemonStatus,
|
|
16
15
|
} from "../daemon/control-types";
|
|
16
|
+
import {
|
|
17
|
+
DAEMON_ACTION_TOKENS,
|
|
18
|
+
DAEMON_EXIT,
|
|
19
|
+
formatDaemonResult,
|
|
20
|
+
formatDaemonStatus,
|
|
21
|
+
resolveDaemonAction,
|
|
22
|
+
} from "../daemon/operator-contract";
|
|
17
23
|
|
|
18
24
|
export type DaemonCliAction = "list" | "status" | "stop" | "reload";
|
|
19
25
|
|
|
@@ -26,6 +32,8 @@ export interface DaemonCommandArgs {
|
|
|
26
32
|
gracefulTimeoutMs?: number;
|
|
27
33
|
killTimeoutMs?: number;
|
|
28
34
|
spawnIfStopped?: boolean;
|
|
35
|
+
/** Show runtime detail and the full roots list in human output. */
|
|
36
|
+
verbose?: boolean;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
export interface DaemonCommandDeps {
|
|
@@ -33,18 +41,20 @@ export interface DaemonCommandDeps {
|
|
|
33
41
|
controllers?: BuiltInDaemonController[];
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
const KNOWN_ACTIONS: DaemonCliAction[] = ["list", "status", "stop", "reload"];
|
|
37
44
|
const KNOWN_KINDS: DaemonKind[] = ["telegram"];
|
|
38
45
|
|
|
39
46
|
export function parseDaemonArgs(argv: string[]): DaemonCommandArgs | undefined {
|
|
40
47
|
if (argv.length === 0 || argv[0] !== "daemon") return undefined;
|
|
41
48
|
const rest = argv.slice(1);
|
|
42
|
-
const
|
|
43
|
-
const
|
|
49
|
+
const resolved = resolveDaemonAction(rest[0]);
|
|
50
|
+
const isActionToken = (DAEMON_ACTION_TOKENS as readonly string[]).includes(rest[0] ?? "");
|
|
51
|
+
const action = (resolved ?? "status") as DaemonCliAction;
|
|
52
|
+
const positional = isActionToken ? rest.slice(1) : rest;
|
|
44
53
|
const kinds: DaemonKind[] = [];
|
|
45
54
|
let all = false;
|
|
46
55
|
let json = false;
|
|
47
56
|
let force = false;
|
|
57
|
+
let verbose = false;
|
|
48
58
|
let gracefulTimeoutMs: number | undefined;
|
|
49
59
|
let killTimeoutMs: number | undefined;
|
|
50
60
|
let spawnIfStopped: boolean | undefined;
|
|
@@ -53,34 +63,14 @@ export function parseDaemonArgs(argv: string[]): DaemonCommandArgs | undefined {
|
|
|
53
63
|
if (arg === "--all") all = true;
|
|
54
64
|
else if (arg === "--json") json = true;
|
|
55
65
|
else if (arg === "--force") force = true;
|
|
66
|
+
else if (arg === "--verbose" || arg === "-v") verbose = true;
|
|
56
67
|
else if (arg === "--spawn-if-stopped") spawnIfStopped = true;
|
|
57
68
|
else if (arg === "--graceful-timeout-ms") gracefulTimeoutMs = Number.parseInt(positional[++i], 10);
|
|
58
69
|
else if (arg === "--kill-timeout-ms") killTimeoutMs = Number.parseInt(positional[++i], 10);
|
|
59
70
|
else if (!arg.startsWith("--") && (KNOWN_KINDS as string[]).includes(arg)) kinds.push(arg as DaemonKind);
|
|
60
71
|
}
|
|
61
|
-
return { action, kinds, all, json, force, gracefulTimeoutMs, killTimeoutMs, spawnIfStopped };
|
|
72
|
+
return { action, kinds, all, json, force, verbose, gracefulTimeoutMs, killTimeoutMs, spawnIfStopped };
|
|
62
73
|
}
|
|
63
|
-
|
|
64
|
-
function formatStatus(status: DaemonStatus): string {
|
|
65
|
-
const parts = [
|
|
66
|
-
`${status.kind}: ${status.health}`,
|
|
67
|
-
status.configured ? undefined : "(not configured)",
|
|
68
|
-
status.pid !== undefined ? `pid=${status.pid}` : undefined,
|
|
69
|
-
status.ownerId ? `owner=${status.ownerId}` : undefined,
|
|
70
|
-
status.rootCount !== undefined ? `roots=${status.rootCount}` : undefined,
|
|
71
|
-
`mode=${status.runtime.mode}`,
|
|
72
|
-
].filter(Boolean);
|
|
73
|
-
let line = parts.join(" ");
|
|
74
|
-
if (status.runtime.warning) line += `\n warning: ${status.runtime.warning}`;
|
|
75
|
-
return line;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function formatResult(result: DaemonOperationResult): string {
|
|
79
|
-
const head = `${result.kind} ${result.action}: ${result.ok ? "ok" : "failed"} — ${result.message}`;
|
|
80
|
-
const warnings = result.warnings.map(w => `\n warning: ${w}`).join("");
|
|
81
|
-
return head + warnings;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
74
|
export async function runDaemonCommand(cmd: DaemonCommandArgs, deps: DaemonCommandDeps = {}): Promise<void> {
|
|
85
75
|
const unknownKinds = cmd.kinds.filter(kind => !(KNOWN_KINDS as string[]).includes(kind));
|
|
86
76
|
if (unknownKinds.length > 0) {
|
|
@@ -98,7 +88,7 @@ export async function runDaemonCommand(cmd: DaemonCommandArgs, deps: DaemonComma
|
|
|
98
88
|
if (cmd.json) {
|
|
99
89
|
process.stdout.write(`${JSON.stringify(statuses, null, 2)}\n`);
|
|
100
90
|
} else {
|
|
101
|
-
process.stdout.write(`${statuses.map(
|
|
91
|
+
process.stdout.write(`${statuses.map(s => formatDaemonStatus(s, { verbose: cmd.verbose })).join("\n")}\n`);
|
|
102
92
|
}
|
|
103
93
|
return;
|
|
104
94
|
}
|
|
@@ -116,7 +106,7 @@ export async function runDaemonCommand(cmd: DaemonCommandArgs, deps: DaemonComma
|
|
|
116
106
|
if (cmd.json) {
|
|
117
107
|
process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
|
|
118
108
|
} else {
|
|
119
|
-
process.stdout.write(`${results.map(
|
|
109
|
+
process.stdout.write(`${results.map(formatDaemonResult).join("\n")}\n`);
|
|
120
110
|
}
|
|
121
|
-
if (results.some(r => !r.ok)) process.exitCode =
|
|
111
|
+
if (results.some(r => !r.ok)) process.exitCode = DAEMON_EXIT.failure;
|
|
122
112
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildMallocReexecArgv,
|
|
4
|
+
formatMallocEnvNotice,
|
|
5
|
+
hasMacOSMallocEnv,
|
|
6
|
+
MACOS_MALLOC_ENV_VARS,
|
|
7
|
+
MALLOC_ENV_REEXEC_GUARD,
|
|
8
|
+
shouldReexecForMacOSMallocEnv,
|
|
9
|
+
} from "./malloc-env-guard";
|
|
10
|
+
|
|
11
|
+
describe("hasMacOSMallocEnv", () => {
|
|
12
|
+
it("detects MallocStackLogging", () => {
|
|
13
|
+
expect(hasMacOSMallocEnv({ MallocStackLogging: "0" })).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("detects MallocStackLoggingNoCompact", () => {
|
|
17
|
+
expect(hasMacOSMallocEnv({ MallocStackLoggingNoCompact: "1" })).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("is false when neither var is present", () => {
|
|
21
|
+
expect(hasMacOSMallocEnv({ PATH: "/usr/bin", HOME: "/home/x" })).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("is false for an empty environment", () => {
|
|
25
|
+
expect(hasMacOSMallocEnv({})).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("shouldReexecForMacOSMallocEnv", () => {
|
|
30
|
+
it("is true on darwin with a malloc var and no loop guard", () => {
|
|
31
|
+
expect(shouldReexecForMacOSMallocEnv({ MallocStackLogging: "0" }, "darwin")).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("is false off darwin even when a malloc var is present", () => {
|
|
35
|
+
expect(shouldReexecForMacOSMallocEnv({ MallocStackLogging: "0" }, "linux")).toBe(false);
|
|
36
|
+
expect(shouldReexecForMacOSMallocEnv({ MallocStackLogging: "0" }, "win32")).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("is false when the loop guard is already set (already re-exec'd)", () => {
|
|
40
|
+
expect(shouldReexecForMacOSMallocEnv({ MallocStackLogging: "0", [MALLOC_ENV_REEXEC_GUARD]: "1" }, "darwin")).toBe(
|
|
41
|
+
false,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("is false on darwin when no malloc var is present", () => {
|
|
46
|
+
expect(shouldReexecForMacOSMallocEnv({ PATH: "/usr/bin" }, "darwin")).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("buildMallocReexecArgv", () => {
|
|
51
|
+
it("drops the embedded virtual entry for a POSIX compiled binary", () => {
|
|
52
|
+
expect(buildMallocReexecArgv(["/exe", "/$bunfs/root/cli", "run", "--flag"], "/exe")).toEqual([
|
|
53
|
+
"/exe",
|
|
54
|
+
"run",
|
|
55
|
+
"--flag",
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("drops the embedded virtual entry for a Windows compiled binary", () => {
|
|
60
|
+
expect(buildMallocReexecArgv(["C:/exe.exe", "B:\\~BUN\\root\\cli", "run"], "C:/exe.exe")).toEqual([
|
|
61
|
+
"C:/exe.exe",
|
|
62
|
+
"run",
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("drops the percent-encoded embedded virtual entry", () => {
|
|
67
|
+
expect(buildMallocReexecArgv(["/exe", "B:\\%7EBUN\\root\\cli", "run"], "/exe")).toEqual(["/exe", "run"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("keeps the on-disk script for a source/dev-link run", () => {
|
|
71
|
+
expect(
|
|
72
|
+
buildMallocReexecArgv(["/usr/bin/bun", "/repo/packages/coding-agent/src/cli.ts", "run", "-x"], "/usr/bin/bun"),
|
|
73
|
+
).toEqual(["/usr/bin/bun", "/repo/packages/coding-agent/src/cli.ts", "run", "-x"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("handles a source run with no user args", () => {
|
|
77
|
+
expect(buildMallocReexecArgv(["/usr/bin/bun", "/repo/cli.ts"], "/usr/bin/bun")).toEqual([
|
|
78
|
+
"/usr/bin/bun",
|
|
79
|
+
"/repo/cli.ts",
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("formatMallocEnvNotice", () => {
|
|
85
|
+
it("names both env vars and the permanent launchctl fix", () => {
|
|
86
|
+
const notice = formatMallocEnvNotice();
|
|
87
|
+
for (const name of MACOS_MALLOC_ENV_VARS) {
|
|
88
|
+
expect(notice).toContain(name);
|
|
89
|
+
}
|
|
90
|
+
expect(notice).toContain("launchctl unsetenv MallocStackLogging");
|
|
91
|
+
expect(notice).toContain("launchctl unsetenv MallocStackLoggingNoCompact");
|
|
92
|
+
expect(notice.endsWith("\n")).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("module constants", () => {
|
|
97
|
+
it("exposes exactly the two macOS malloc env vars", () => {
|
|
98
|
+
expect([...MACOS_MALLOC_ENV_VARS]).toEqual(["MallocStackLogging", "MallocStackLoggingNoCompact"]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("uses a namespaced loop-guard variable", () => {
|
|
102
|
+
expect(MALLOC_ENV_REEXEC_GUARD).toBe("GJC_MALLOC_ENV_REEXEC");
|
|
103
|
+
});
|
|
104
|
+
});
|