@gajae-code/coding-agent 0.8.2 → 0.9.1
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 +53 -0
- package/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +119 -1
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/components/thinking-selector.d.ts +2 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config-commands.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +22 -0
- package/dist/types/notifications/index.d.ts +2 -0
- package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
- package/dist/types/notifications/telegram-daemon.d.ts +30 -0
- package/dist/types/notifications/telegram-reference.d.ts +7 -0
- package/dist/types/notifications/threaded-render.d.ts +14 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +43 -1
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/package.json +8 -11
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/config-cli.ts +27 -3
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/notify-cli.ts +11 -9
- package/src/cli/update-cli.ts +85 -9
- package/src/cli.ts +26 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/config/keybindings.ts +1 -1
- package/src/config/settings-schema.ts +76 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
- package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
- package/src/defaults/gjc/skills/team/SKILL.md +16 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/gjc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +181 -219
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
- package/src/gjc-runtime/deep-interview-state.ts +31 -2
- package/src/gjc-runtime/launch-tmux.ts +6 -0
- package/src/gjc-runtime/session-state-sidecar.ts +208 -13
- package/src/gjc-runtime/state-writer.ts +15 -0
- package/src/gjc-runtime/team-runtime.ts +175 -19
- package/src/gjc-runtime/tmux-common.ts +1 -0
- package/src/gjc-runtime/ultragoal-guard.ts +55 -66
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1602 -143
- package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
- package/src/gjc-runtime/workflow-manifest.ts +74 -1
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +95 -4
- package/src/internal-urls/docs-index.generated.ts +18 -9
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +43 -2
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/thinking-selector.ts +10 -8
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/controllers/command-controller.ts +39 -2
- package/src/modes/controllers/event-controller.ts +77 -49
- package/src/modes/controllers/extension-ui-controller.ts +8 -1
- package/src/modes/controllers/input-controller.ts +13 -18
- package/src/modes/controllers/selector-controller.ts +41 -0
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/rpc/rpc-mode.ts +32 -12
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +17 -4
- package/src/notifications/config-commands.ts +20 -0
- package/src/notifications/config.ts +43 -4
- package/src/notifications/index.ts +105 -9
- package/src/notifications/lifecycle-commands.ts +37 -16
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +142 -0
- package/src/notifications/telegram-daemon-cli.ts +54 -7
- package/src/notifications/telegram-daemon-control.ts +2 -2
- package/src/notifications/telegram-daemon.ts +390 -56
- package/src/notifications/telegram-reference.ts +17 -0
- package/src/notifications/threaded-render.ts +45 -2
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +26 -19
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +6 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/sdk.ts +68 -30
- package/src/session/agent-session.ts +734 -107
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +59 -2
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +143 -1
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +125 -20
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +4 -3
- package/src/task/output-manager.ts +4 -2
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/utils/pasted-image-path.ts +170 -0
- package/src/web/scrapers/youtube.ts +24 -12
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { sessionRoot } from "../gjc-runtime/session-layout";
|
|
6
|
+
import type { TaskTokenLog } from "../task/types";
|
|
7
|
+
import { buildFixtureReport, type LiveRunReportShape, runFixtureReport } from "./fixture-report";
|
|
8
|
+
|
|
9
|
+
function expectParseReportCompatible(report: LiveRunReportShape, fixtureId: string): void {
|
|
10
|
+
expect(report.schemaVersion).toBe(1);
|
|
11
|
+
expect(typeof report.binaryId).toBe("string");
|
|
12
|
+
expect(report.binaryId.length).toBeGreaterThan(0);
|
|
13
|
+
expect(report.fixtureId).toBe(fixtureId);
|
|
14
|
+
for (const value of Object.values(report.totals)) {
|
|
15
|
+
expect(typeof value).toBe("number");
|
|
16
|
+
expect(Number.isFinite(value)).toBe(true);
|
|
17
|
+
}
|
|
18
|
+
expect(report.receiptArtifactRatio).toBeNull();
|
|
19
|
+
expect(report.spawnDecisions).toBeNull();
|
|
20
|
+
expect(report.roi).toBeNull();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("fixture report", () => {
|
|
24
|
+
it("reports empty logs as a schema-v1 null-cache-hit report", () => {
|
|
25
|
+
const report = buildFixtureReport("empty-fixture", []);
|
|
26
|
+
|
|
27
|
+
expectParseReportCompatible(report, "empty-fixture");
|
|
28
|
+
expect(report.totals).toEqual({
|
|
29
|
+
turns: 0,
|
|
30
|
+
inputTokens: 0,
|
|
31
|
+
outputTokens: 0,
|
|
32
|
+
cacheReadTokens: 0,
|
|
33
|
+
cacheWriteTokens: 0,
|
|
34
|
+
totalTokens: 0,
|
|
35
|
+
});
|
|
36
|
+
expect(report.cacheHitRate).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("sums deterministic fixed-fixture raw token buckets without folding cache into input", () => {
|
|
40
|
+
const logs: TaskTokenLog[] = [
|
|
41
|
+
{
|
|
42
|
+
subagentId: "root",
|
|
43
|
+
agent: "main",
|
|
44
|
+
turn: 1,
|
|
45
|
+
at: "2026-01-01T00:00:00.000Z",
|
|
46
|
+
input: 100,
|
|
47
|
+
output: 20,
|
|
48
|
+
cacheRead: 40,
|
|
49
|
+
cacheWrite: 10,
|
|
50
|
+
totalTokens: 170,
|
|
51
|
+
model: "fixture-model",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
subagentId: "1-executor",
|
|
55
|
+
agent: "executor",
|
|
56
|
+
turn: 1,
|
|
57
|
+
at: "2026-01-01T00:00:01.000Z",
|
|
58
|
+
input: 60,
|
|
59
|
+
output: 15,
|
|
60
|
+
cacheRead: 20,
|
|
61
|
+
cacheWrite: 5,
|
|
62
|
+
totalTokens: 100,
|
|
63
|
+
model: "fixture-model",
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
const report = buildFixtureReport("fixed-fixture", logs);
|
|
67
|
+
const inputTokens = logs.reduce((sum, log) => sum + log.input, 0);
|
|
68
|
+
const outputTokens = logs.reduce((sum, log) => sum + log.output, 0);
|
|
69
|
+
const cacheReadTokens = logs.reduce((sum, log) => sum + log.cacheRead, 0);
|
|
70
|
+
const cacheWriteTokens = logs.reduce((sum, log) => sum + log.cacheWrite, 0);
|
|
71
|
+
const totalTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
72
|
+
|
|
73
|
+
expectParseReportCompatible(report, "fixed-fixture");
|
|
74
|
+
expect(report.totals).toEqual({
|
|
75
|
+
turns: logs.length,
|
|
76
|
+
inputTokens,
|
|
77
|
+
outputTokens,
|
|
78
|
+
cacheReadTokens,
|
|
79
|
+
cacheWriteTokens,
|
|
80
|
+
totalTokens,
|
|
81
|
+
});
|
|
82
|
+
expect(report.cacheHitRate).toBe(cacheReadTokens / (inputTokens + cacheReadTokens));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("emits only parseReport-compatible schema-v1 fields", () => {
|
|
86
|
+
const report = buildFixtureReport("shape-fixture", [
|
|
87
|
+
{
|
|
88
|
+
subagentId: "root",
|
|
89
|
+
turn: 1,
|
|
90
|
+
at: "2026-01-01T00:00:00.000Z",
|
|
91
|
+
input: 0,
|
|
92
|
+
output: 0,
|
|
93
|
+
cacheRead: 1,
|
|
94
|
+
cacheWrite: 0,
|
|
95
|
+
totalTokens: 1,
|
|
96
|
+
},
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
expect(Object.keys(report).sort()).toEqual([
|
|
100
|
+
"binaryId",
|
|
101
|
+
"cacheHitRate",
|
|
102
|
+
"fixtureId",
|
|
103
|
+
"receiptArtifactRatio",
|
|
104
|
+
"roi",
|
|
105
|
+
"schemaVersion",
|
|
106
|
+
"spawnDecisions",
|
|
107
|
+
"totals",
|
|
108
|
+
]);
|
|
109
|
+
expect(Object.keys(report.totals).sort()).toEqual([
|
|
110
|
+
"cacheReadTokens",
|
|
111
|
+
"cacheWriteTokens",
|
|
112
|
+
"inputTokens",
|
|
113
|
+
"outputTokens",
|
|
114
|
+
"totalTokens",
|
|
115
|
+
"turns",
|
|
116
|
+
]);
|
|
117
|
+
expectParseReportCompatible(report, "shape-fixture");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
async function captureRun(fixtureId: string): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
122
|
+
const originalCwd = process.cwd();
|
|
123
|
+
const originalStdout = process.stdout.write.bind(process.stdout);
|
|
124
|
+
const originalStderr = process.stderr.write.bind(process.stderr);
|
|
125
|
+
let exitCode = 0;
|
|
126
|
+
let stdout = "";
|
|
127
|
+
let stderr = "";
|
|
128
|
+
process.stdout.write = ((chunk: string | Uint8Array): boolean => {
|
|
129
|
+
stdout += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8");
|
|
130
|
+
return true;
|
|
131
|
+
}) as typeof process.stdout.write;
|
|
132
|
+
process.stderr.write = ((chunk: string | Uint8Array): boolean => {
|
|
133
|
+
stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8");
|
|
134
|
+
return true;
|
|
135
|
+
}) as typeof process.stderr.write;
|
|
136
|
+
try {
|
|
137
|
+
exitCode = await runFixtureReport(fixtureId);
|
|
138
|
+
return { stdout, stderr, exitCode };
|
|
139
|
+
} finally {
|
|
140
|
+
process.stdout.write = originalStdout;
|
|
141
|
+
process.stderr.write = originalStderr;
|
|
142
|
+
process.chdir(originalCwd);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe("runFixtureReport CLI degradation", () => {
|
|
147
|
+
it("exits non-zero without emitting JSON for an unknown fixture id", async () => {
|
|
148
|
+
await withTempCwd(async () => {
|
|
149
|
+
const { stdout, stderr, exitCode } = await captureRun("definitely-not-a-real-session-id");
|
|
150
|
+
expect(exitCode).toBe(1);
|
|
151
|
+
expect(stdout).toBe("");
|
|
152
|
+
expect(stderr).toContain("unknown fixture id");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("reads only the requested session token-log directory", async () => {
|
|
157
|
+
await withTempCwd(async () => {
|
|
158
|
+
const firstSession = "fixture-session-a";
|
|
159
|
+
const secondSession = "fixture-session-b";
|
|
160
|
+
await writeTokenLog(firstSession, { input: 100, output: 10, cacheRead: 0, cacheWrite: 0, totalTokens: 110 });
|
|
161
|
+
await writeTokenLog(secondSession, { input: 900, output: 90, cacheRead: 0, cacheWrite: 0, totalTokens: 990 });
|
|
162
|
+
|
|
163
|
+
const { stdout, exitCode } = await captureRun(firstSession);
|
|
164
|
+
const report = JSON.parse(stdout) as LiveRunReportShape;
|
|
165
|
+
|
|
166
|
+
expect(exitCode).toBe(0);
|
|
167
|
+
expect(report.fixtureId).toBe(firstSession);
|
|
168
|
+
expect(report.totals.totalTokens).toBe(110);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("emits deterministic PR9 default-candidate fixture reports", async () => {
|
|
173
|
+
await withTempCwd(async () => {
|
|
174
|
+
const { stdout, exitCode } = await captureRun("pr9.read-artifact-spill-threshold.after");
|
|
175
|
+
const report = JSON.parse(stdout) as LiveRunReportShape;
|
|
176
|
+
|
|
177
|
+
expect(exitCode).toBe(0);
|
|
178
|
+
expectParseReportCompatible(report, "pr9.read-artifact-spill-threshold.after");
|
|
179
|
+
expect(report.totals.totalTokens).toBe(20_100);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("exits non-zero without emitting JSON for a corrupt token-log", async () => {
|
|
184
|
+
await withTempCwd(async () => {
|
|
185
|
+
const id = "corrupt-session";
|
|
186
|
+
const dir = join(sessionRoot(process.cwd(), id), "token-logs");
|
|
187
|
+
await mkdir(dir, { recursive: true });
|
|
188
|
+
await writeFile(join(dir, "token-log.jsonl"), "not json\n{}\n", "utf-8");
|
|
189
|
+
const { stdout, stderr, exitCode } = await captureRun(id);
|
|
190
|
+
expect(exitCode).toBe(1);
|
|
191
|
+
expect(stdout).toBe("");
|
|
192
|
+
expect(stderr).toContain("failed to build fixture report");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
async function withTempCwd(fn: (cwd: string) => Promise<void>): Promise<void> {
|
|
198
|
+
const originalCwd = process.cwd();
|
|
199
|
+
const dir = await mkdtemp(join(tmpdir(), "gjc-fixture-report-"));
|
|
200
|
+
process.chdir(dir);
|
|
201
|
+
try {
|
|
202
|
+
await fn(dir);
|
|
203
|
+
} finally {
|
|
204
|
+
// Restore the real cwd BEFORE removing the temp dir, otherwise the process
|
|
205
|
+
// is left sitting in a deleted directory and later tests in the same
|
|
206
|
+
// process fail with ENOENT on relative-path/process.cwd() operations.
|
|
207
|
+
process.chdir(originalCwd);
|
|
208
|
+
await rm(dir, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function writeTokenLog(
|
|
213
|
+
sessionId: string,
|
|
214
|
+
metrics: { input: number; output: number; cacheRead: number; cacheWrite: number; totalTokens: number },
|
|
215
|
+
): Promise<void> {
|
|
216
|
+
const dir = join(sessionRoot(process.cwd(), sessionId), "token-logs");
|
|
217
|
+
await mkdir(dir, { recursive: true });
|
|
218
|
+
await writeFile(
|
|
219
|
+
join(dir, "token-log.jsonl"),
|
|
220
|
+
`${JSON.stringify({
|
|
221
|
+
subagentId: "root",
|
|
222
|
+
agent: "main",
|
|
223
|
+
turn: 1,
|
|
224
|
+
at: "2026-01-01T00:00:00.000Z",
|
|
225
|
+
model: "fixture-model",
|
|
226
|
+
...metrics,
|
|
227
|
+
})}\n`,
|
|
228
|
+
"utf-8",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { GjcSessionContext } from "../gjc-runtime/session-layout";
|
|
4
|
+
import { resolveGjcSessionForRead, SessionResolutionError } from "../gjc-runtime/session-resolution";
|
|
5
|
+
import { computeTaskTokenMetrics, readTaskTokenLogs } from "../task/token-log";
|
|
6
|
+
import type { TaskTokenLog } from "../task/types";
|
|
7
|
+
|
|
8
|
+
const LIVE_RUNNER_SCHEMA_VERSION = 1;
|
|
9
|
+
const BINARY_ID = "gjc";
|
|
10
|
+
|
|
11
|
+
function deterministicLog(
|
|
12
|
+
input: number,
|
|
13
|
+
output: number,
|
|
14
|
+
cacheRead: number,
|
|
15
|
+
cacheWrite: number,
|
|
16
|
+
totalTokens: number,
|
|
17
|
+
): TaskTokenLog {
|
|
18
|
+
return {
|
|
19
|
+
subagentId: "root",
|
|
20
|
+
agent: "main",
|
|
21
|
+
turn: 1,
|
|
22
|
+
at: "2026-01-01T00:00:00.000Z",
|
|
23
|
+
input,
|
|
24
|
+
output,
|
|
25
|
+
cacheRead,
|
|
26
|
+
cacheWrite,
|
|
27
|
+
totalTokens,
|
|
28
|
+
model: "fixture-model",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface LiveRunReportShape {
|
|
33
|
+
schemaVersion: 1;
|
|
34
|
+
binaryId: string;
|
|
35
|
+
fixtureId: string;
|
|
36
|
+
totals: {
|
|
37
|
+
turns: number;
|
|
38
|
+
inputTokens: number;
|
|
39
|
+
outputTokens: number;
|
|
40
|
+
cacheReadTokens: number;
|
|
41
|
+
cacheWriteTokens: number;
|
|
42
|
+
totalTokens: number;
|
|
43
|
+
};
|
|
44
|
+
cacheHitRate: number | null;
|
|
45
|
+
receiptArtifactRatio: number | null;
|
|
46
|
+
spawnDecisions: number | null;
|
|
47
|
+
roi: number | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const DETERMINISTIC_FIXTURES: Record<string, readonly TaskTokenLog[]> = {
|
|
51
|
+
"fixed-fixture": [
|
|
52
|
+
{
|
|
53
|
+
subagentId: "root",
|
|
54
|
+
agent: "main",
|
|
55
|
+
turn: 1,
|
|
56
|
+
at: "2026-01-01T00:00:00.000Z",
|
|
57
|
+
input: 100,
|
|
58
|
+
output: 20,
|
|
59
|
+
cacheRead: 40,
|
|
60
|
+
cacheWrite: 10,
|
|
61
|
+
totalTokens: 170,
|
|
62
|
+
model: "fixture-model",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
subagentId: "1-executor",
|
|
66
|
+
agent: "executor",
|
|
67
|
+
turn: 1,
|
|
68
|
+
at: "2026-01-01T00:00:01.000Z",
|
|
69
|
+
input: 60,
|
|
70
|
+
output: 15,
|
|
71
|
+
cacheRead: 20,
|
|
72
|
+
cacheWrite: 5,
|
|
73
|
+
totalTokens: 100,
|
|
74
|
+
model: "fixture-model",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
const DEFAULT_REDUCTION_FIXTURE_LOGS: Record<string, readonly TaskTokenLog[]> = {
|
|
79
|
+
"pr9.task-recursion.before": [deterministicLog(2400, 420, 600, 200, 3620)],
|
|
80
|
+
"pr9.task-recursion.after": [deterministicLog(1800, 360, 450, 150, 2760)],
|
|
81
|
+
"pr9.output-caps.before": [deterministicLog(22_000, 3_200, 0, 0, 25_200)],
|
|
82
|
+
"pr9.output-caps.after": [deterministicLog(18_000, 2_400, 0, 0, 20_400)],
|
|
83
|
+
"pr9.max-inline-result-bytes.before": [deterministicLog(48_000, 2_000, 0, 0, 50_000)],
|
|
84
|
+
"pr9.max-inline-result-bytes.after": [deterministicLog(12_000, 1_600, 0, 0, 13_600)],
|
|
85
|
+
"pr9.read-artifact-spill-threshold.before": [deterministicLog(96_000, 2_500, 0, 0, 98_500)],
|
|
86
|
+
"pr9.read-artifact-spill-threshold.after": [deterministicLog(18_000, 2_100, 0, 0, 20_100)],
|
|
87
|
+
"pr9.maintenance-pruning.before": [deterministicLog(40_000, 2_000, 18_000, 6_000, 66_000)],
|
|
88
|
+
"pr9.maintenance-pruning.after": [deterministicLog(30_000, 1_800, 10_000, 3_000, 44_800)],
|
|
89
|
+
"pr9.append-only-provider-expansion.before": [deterministicLog(28_000, 1_700, 8_000, 2_500, 40_200)],
|
|
90
|
+
"pr9.append-only-provider-expansion.after": [deterministicLog(20_000, 1_700, 14_000, 2_500, 38_200)],
|
|
91
|
+
"pr9.rpc-compact-deltas.before": [deterministicLog(14_000, 1_200, 6_000, 1_500, 22_700)],
|
|
92
|
+
"pr9.rpc-compact-deltas.after": [deterministicLog(10_000, 900, 6_200, 1_500, 18_600)],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export function buildFixtureReport(fixtureId: string, logs: readonly TaskTokenLog[]): LiveRunReportShape {
|
|
96
|
+
const metrics = computeTaskTokenMetrics(logs);
|
|
97
|
+
return {
|
|
98
|
+
schemaVersion: LIVE_RUNNER_SCHEMA_VERSION,
|
|
99
|
+
binaryId: BINARY_ID,
|
|
100
|
+
fixtureId,
|
|
101
|
+
totals: {
|
|
102
|
+
turns: metrics.turns,
|
|
103
|
+
inputTokens: metrics.inputTokens,
|
|
104
|
+
outputTokens: metrics.outputTokens,
|
|
105
|
+
cacheReadTokens: metrics.cacheReadTokens,
|
|
106
|
+
cacheWriteTokens: metrics.cacheWriteTokens,
|
|
107
|
+
totalTokens: metrics.totalTokens,
|
|
108
|
+
},
|
|
109
|
+
cacheHitRate: logs.length === 0 ? null : metrics.cacheHitRate,
|
|
110
|
+
receiptArtifactRatio: null,
|
|
111
|
+
spawnDecisions: null,
|
|
112
|
+
roi: null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function runFixtureReport(fixtureId: string): Promise<number> {
|
|
117
|
+
let resolved: ResolvedFixtureLogs;
|
|
118
|
+
try {
|
|
119
|
+
resolved = await resolveFixtureLogs(fixtureId);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// e.g. a corrupt token-log. Fail loudly rather than emitting an all-zero report.
|
|
122
|
+
process.stderr.write(
|
|
123
|
+
`failed to build fixture report for ${fixtureId}: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
124
|
+
);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
if (resolved.kind === "unknown") {
|
|
128
|
+
// Neither a known deterministic fixture nor a resolvable GJC session. Emit a
|
|
129
|
+
// bounded error on stderr with a non-zero exit rather than a schema-valid
|
|
130
|
+
// all-zero report, which a before/after benchmark would misread as "0 tokens".
|
|
131
|
+
process.stderr.write(`unknown fixture id and no matching GJC session: ${fixtureId}\n`);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
process.stdout.write(JSON.stringify(buildFixtureReport(fixtureId, resolved.logs)));
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
type ResolvedFixtureLogs =
|
|
139
|
+
| { readonly kind: "logs"; readonly logs: readonly TaskTokenLog[] }
|
|
140
|
+
| { readonly kind: "unknown" };
|
|
141
|
+
|
|
142
|
+
async function resolveFixtureLogs(fixtureId: string): Promise<ResolvedFixtureLogs> {
|
|
143
|
+
const deterministic = DETERMINISTIC_FIXTURES[fixtureId] ?? DEFAULT_REDUCTION_FIXTURE_LOGS[fixtureId];
|
|
144
|
+
if (deterministic) return { kind: "logs", logs: deterministic };
|
|
145
|
+
let session: GjcSessionContext;
|
|
146
|
+
try {
|
|
147
|
+
session = await resolveGjcSessionForRead(process.cwd(), {
|
|
148
|
+
flagValue: fixtureId,
|
|
149
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error instanceof SessionResolutionError) return { kind: "unknown" };
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
// resolveGjcSessionForRead accepts any explicit flagValue as a session id
|
|
156
|
+
// without checking the dir exists, so a typo would otherwise yield a
|
|
157
|
+
// schema-valid all-zero report. Require the session root to exist; a real
|
|
158
|
+
// session with no turns yet still reads as a legitimate empty log set.
|
|
159
|
+
if (!(await directoryExists(session.sessionRoot))) return { kind: "unknown" };
|
|
160
|
+
const logs = await readTaskTokenLogs(path.join(session.sessionRoot, "token-logs"));
|
|
161
|
+
return { kind: "logs", logs };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function directoryExists(dir: string): Promise<boolean> {
|
|
165
|
+
try {
|
|
166
|
+
const stat = await fs.stat(dir);
|
|
167
|
+
return stat.isDirectory();
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export const RECOMMENDED_MACOS_NOFILE_LIMIT = 4096;
|
|
4
|
+
|
|
5
|
+
export function parseNoFileLimit(text: string): number | undefined {
|
|
6
|
+
const trimmed = text.trim();
|
|
7
|
+
if (!trimmed || trimmed === "unlimited") return undefined;
|
|
8
|
+
const value = Number(trimmed);
|
|
9
|
+
return Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildMacOSNoFileLimitWarning(currentLimit: number): string {
|
|
13
|
+
return [
|
|
14
|
+
`Warning: macOS file descriptor limit is low (ulimit -n = ${currentLimit}).`,
|
|
15
|
+
'GJC and project dev servers can hit EMFILE / "too many open files" while scanning or watching repositories.',
|
|
16
|
+
"For this terminal session, run:",
|
|
17
|
+
` ulimit -n ${RECOMMENDED_MACOS_NOFILE_LIMIT}`,
|
|
18
|
+
"If your shell refuses that value, raise the per-user launchd limit and restart the terminal:",
|
|
19
|
+
` sudo launchctl limit maxfiles ${RECOMMENDED_MACOS_NOFILE_LIMIT} 65536`,
|
|
20
|
+
"Avoid using huge values such as 2147483646 on macOS; they are commonly rejected or clamped.",
|
|
21
|
+
"Set GJC_SKIP_NOFILE_CHECK=1 to silence this preflight warning.",
|
|
22
|
+
].join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WarnIfMacOSNoFileLimitTooLowDeps {
|
|
26
|
+
platform?: NodeJS.Platform;
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
execFileSync?: typeof execFileSync;
|
|
29
|
+
writeStderr?: (text: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function warnIfMacOSNoFileLimitTooLow(deps: WarnIfMacOSNoFileLimitTooLowDeps = {}): void {
|
|
33
|
+
const platform = deps.platform ?? process.platform;
|
|
34
|
+
if (platform !== "darwin") return;
|
|
35
|
+
const env = deps.env ?? process.env;
|
|
36
|
+
if (env.GJC_SKIP_NOFILE_CHECK === "1" || env.GJC_SKIP_NOFILE_CHECK === "true") return;
|
|
37
|
+
|
|
38
|
+
const run = deps.execFileSync ?? execFileSync;
|
|
39
|
+
let currentLimit: number | undefined;
|
|
40
|
+
try {
|
|
41
|
+
currentLimit = parseNoFileLimit(run("/bin/sh", ["-lc", "ulimit -n"], { encoding: "utf8" }));
|
|
42
|
+
} catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (currentLimit === undefined || currentLimit >= RECOMMENDED_MACOS_NOFILE_LIMIT) return;
|
|
46
|
+
const write = deps.writeStderr ?? (text => process.stderr.write(text));
|
|
47
|
+
write(`${buildMacOSNoFileLimitWarning(currentLimit)}\n`);
|
|
48
|
+
}
|
package/src/cli/notify-cli.ts
CHANGED
|
@@ -249,18 +249,18 @@ export async function promptForToken(
|
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
const THREADED_ENABLED_SUCCESS =
|
|
252
|
-
"Telegram Threaded Mode capability verified for this bot. GJC will request a private-chat topic per session; if Telegram ever refuses topic creation, notifications fall back to this flat chat with a one-time nudge.\n";
|
|
252
|
+
"Telegram Threaded Mode capability verified for this bot. GJC will request a private-chat topic per session; if Telegram ever refuses topic creation, notifications fall back to this flat chat with inline ask buttons only and a one-time Threaded Mode nudge.\n";
|
|
253
253
|
|
|
254
254
|
const THREADED_MISSING_WARNING =
|
|
255
|
-
"Warning: Telegram getMe did not include has_topics_enabled, so GJC cannot verify private-chat Threaded Mode capability for this bot. Setup will continue;
|
|
255
|
+
"Warning: Telegram getMe did not include has_topics_enabled, so GJC cannot verify private-chat Threaded Mode capability for this bot. Setup will continue; flat private-chat fallback supports outbound notifications and inline ask buttons only. Free-text replies and session commands require Threaded Mode/topic routing.\n";
|
|
256
256
|
|
|
257
257
|
const THREADED_NONINTERACTIVE_WARNING =
|
|
258
|
-
"Warning: Telegram Threaded Mode capability is OFF for this bot. Setup will be saved because this run is non-interactive
|
|
258
|
+
"Warning: Telegram Threaded Mode capability is OFF for this bot. Setup will be saved because this run is non-interactive. Flat private-chat fallback supports outbound notifications and inline ask buttons only; free-text replies and session commands require enabling Threaded Mode in @BotFather > Bot Settings > Threads Settings.\n";
|
|
259
259
|
|
|
260
260
|
const THREADED_DISABLED_GUIDANCE =
|
|
261
261
|
"Telegram Threaded Mode is OFF for this bot. GJC needs Telegram private-chat topics so each session can use its own thread.\n" +
|
|
262
|
-
"GJC cannot enable this through the Bot API. Open @BotFather
|
|
263
|
-
"
|
|
262
|
+
"GJC cannot enable this through the Bot API. Open @BotFather > Bot Settings > Threads Settings for this bot, enable Threaded Mode / forum topics for private chats, then return here.\n" +
|
|
263
|
+
"Without Threaded Mode, flat private-chat fallback supports outbound notifications and inline ask buttons only; free-text replies and session commands require topic routing.\n";
|
|
264
264
|
|
|
265
265
|
const THREADED_DISABLED_PROMPT =
|
|
266
266
|
"Press Enter after enabling Threaded Mode, or type skip to finish setup with a warning: ";
|
|
@@ -270,7 +270,7 @@ const THREADED_STILL_OFF = "Telegram still reports Threaded Mode OFF for this bo
|
|
|
270
270
|
const THREADED_RETRY_PROMPT = "Press Enter to check again, or type skip to finish setup with a warning: ";
|
|
271
271
|
|
|
272
272
|
const THREADED_SKIP_WARNING =
|
|
273
|
-
"Warning: continuing without verified Telegram Threaded Mode capability. Setup will be saved
|
|
273
|
+
"Warning: continuing without verified Telegram Threaded Mode capability. Setup will be saved. Flat private-chat fallback supports outbound notifications and inline ask buttons only; free-text replies and session commands require enabling Threaded Mode in BotFather.\n";
|
|
274
274
|
|
|
275
275
|
const THREADED_INVALID_INPUT = "Type Enter to retry or skip to continue with a warning.\n";
|
|
276
276
|
|
|
@@ -500,8 +500,10 @@ ${chalk.bold("Examples:")}
|
|
|
500
500
|
|
|
501
501
|
${chalk.bold("Threaded Mode:")}
|
|
502
502
|
GJC uses Telegram private-chat topics for per-session threads. Setup verifies the bot
|
|
503
|
-
capability via getMe.has_topics_enabled.
|
|
504
|
-
bots cannot toggle it through the Bot API. If Telegram refuses topic
|
|
505
|
-
GJC delivers flat to the paired private chat
|
|
503
|
+
capability via getMe.has_topics_enabled. Enable Threaded Mode in @BotFather > Bot Settings
|
|
504
|
+
> Threads Settings; bots cannot toggle it through the Bot API. If Telegram refuses topic
|
|
505
|
+
creation at runtime, GJC delivers flat to the paired private chat with outbound notifications
|
|
506
|
+
and inline ask buttons only, then nudges you to enable Threaded Mode for free-text replies
|
|
507
|
+
and session commands.
|
|
506
508
|
`);
|
|
507
509
|
}
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -552,31 +552,107 @@ async function updateViaNpm(packageName: string, expectedVersion: string): Promi
|
|
|
552
552
|
}
|
|
553
553
|
|
|
554
554
|
/**
|
|
555
|
-
*
|
|
555
|
+
* Flush a freshly written file's data to stable storage.
|
|
556
|
+
*
|
|
557
|
+
* Critical on network filesystems (e.g. NFS home directories): `pipeline`
|
|
558
|
+
* resolving does not guarantee the downloaded bytes are durable on the
|
|
559
|
+
* server, so the post-install `gjc --version` check can exec a binary whose
|
|
560
|
+
* pages are not yet consistent. The child then faults, the version check
|
|
561
|
+
* fails, and the update is rolled back with "could not verify updated
|
|
562
|
+
* version" even though the download itself succeeded. Explicitly fsyncing
|
|
563
|
+
* before the rename/exec avoids the race.
|
|
556
564
|
*/
|
|
557
|
-
async function
|
|
558
|
-
const
|
|
559
|
-
|
|
565
|
+
async function fsyncFile(filePath: string): Promise<void> {
|
|
566
|
+
const handle = await fs.promises.open(filePath, "r+");
|
|
567
|
+
try {
|
|
568
|
+
await handle.sync();
|
|
569
|
+
} finally {
|
|
570
|
+
await handle.close();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
560
573
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
574
|
+
export async function fsyncFileForTest(filePath: string): Promise<void> {
|
|
575
|
+
return fsyncFile(filePath);
|
|
576
|
+
}
|
|
564
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Download a release binary to a temp path, throwing a friendly error when the
|
|
580
|
+
* release asset cannot be fetched.
|
|
581
|
+
*/
|
|
582
|
+
async function downloadBinaryTo(url: string, tempPath: string, binaryName: string): Promise<void> {
|
|
565
583
|
const response = await fetch(url, { redirect: "follow" });
|
|
566
584
|
if (!response.ok || !response.body) {
|
|
567
585
|
throw new Error(formatBinaryDownloadFailureMessage(binaryName, url, response.statusText || response.status));
|
|
568
586
|
}
|
|
569
587
|
const fileStream = fs.createWriteStream(tempPath, { mode: 0o755 });
|
|
570
588
|
await pipeline(response.body, fileStream);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Injectable steps of the binary update flow (seams for testing ordering). */
|
|
592
|
+
export interface BinaryUpdateFlow {
|
|
593
|
+
download(url: string, tempPath: string): Promise<void>;
|
|
594
|
+
fsync(filePath: string): Promise<void>;
|
|
595
|
+
replace(options: BinaryReplacementOptions): Promise<InstalledVersionVerification>;
|
|
596
|
+
verifyInstalledVersion(expectedVersion: string): Promise<InstalledVersionVerification>;
|
|
597
|
+
/** Best-effort cleanup of the temp file when the flow aborts before replace. */
|
|
598
|
+
removeTemp?(filePath: string): Promise<void>;
|
|
599
|
+
/** Called once fsync has succeeded, right before replacement begins. */
|
|
600
|
+
beforeReplace?(): void;
|
|
601
|
+
}
|
|
571
602
|
|
|
572
|
-
|
|
573
|
-
|
|
603
|
+
/**
|
|
604
|
+
* Orchestrate download → fsync → replace → verify with a strict ordering
|
|
605
|
+
* contract: the downloaded temp binary MUST be flushed to stable storage
|
|
606
|
+
* before it is published (renamed into place) or exec'd for verification.
|
|
607
|
+
*
|
|
608
|
+
* If fsync fails the temp bytes are not durable, so we abort before
|
|
609
|
+
* replacement/verification and clean up the temp file rather than installing a
|
|
610
|
+
* possibly-truncated binary.
|
|
611
|
+
*/
|
|
612
|
+
export async function runBinaryUpdateFlow(
|
|
613
|
+
targetPath: string,
|
|
614
|
+
url: string,
|
|
615
|
+
expectedVersion: string,
|
|
616
|
+
flow: BinaryUpdateFlow,
|
|
617
|
+
): Promise<InstalledVersionVerification> {
|
|
618
|
+
const tempPath = `${targetPath}.new`;
|
|
619
|
+
const backupPath = `${targetPath}.bak`;
|
|
620
|
+
|
|
621
|
+
await flow.download(url, tempPath);
|
|
622
|
+
try {
|
|
623
|
+
await flow.fsync(tempPath);
|
|
624
|
+
} catch (err) {
|
|
625
|
+
if (flow.removeTemp) await flow.removeTemp(tempPath);
|
|
626
|
+
throw err;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
flow.beforeReplace?.();
|
|
630
|
+
return flow.replace({
|
|
574
631
|
targetPath,
|
|
575
632
|
tempPath,
|
|
576
633
|
backupPath,
|
|
577
634
|
expectedVersion,
|
|
635
|
+
verifyInstalledVersion: flow.verifyInstalledVersion,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Download a release binary to a target path, replacing an existing file.
|
|
641
|
+
*/
|
|
642
|
+
async function updateViaBinaryAt(targetPath: string, expectedVersion: string): Promise<void> {
|
|
643
|
+
const binaryName = getBinaryName();
|
|
644
|
+
const url = buildReleaseBinaryUrl(expectedVersion);
|
|
645
|
+
console.log(chalk.dim(`Downloading ${binaryName}…`));
|
|
646
|
+
|
|
647
|
+
const verification = await runBinaryUpdateFlow(targetPath, url, expectedVersion, {
|
|
648
|
+
download: (downloadUrl, tempPath) => downloadBinaryTo(downloadUrl, tempPath, binaryName),
|
|
649
|
+
fsync: fsyncFile,
|
|
650
|
+
replace: replaceBinaryForUpdate,
|
|
578
651
|
verifyInstalledVersion: verifyInstalledRuntime,
|
|
652
|
+
removeTemp: unlinkIfExists,
|
|
653
|
+
beforeReplace: () => console.log(chalk.dim("Installing update...")),
|
|
579
654
|
});
|
|
655
|
+
|
|
580
656
|
printVerifiedVersion(expectedVersion);
|
|
581
657
|
if (verification.cleanupWarning) console.warn(chalk.yellow(verification.cleanupWarning));
|
|
582
658
|
printRestartGuidance();
|
package/src/cli.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Args, type CliConfig, Command, type CommandEntry, Flags, run } from "@gajae-code/utils/cli";
|
|
8
8
|
import { APP_NAME, formatBunRuntimeError, MIN_BUN_VERSION, VERSION } from "@gajae-code/utils/dirs";
|
|
9
|
+
import { runFixtureReport } from "./cli/fixture-report";
|
|
9
10
|
|
|
10
11
|
if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
|
|
11
12
|
process.stderr.write(
|
|
@@ -76,6 +77,9 @@ async function installRuntimeGlobals(): Promise<void> {
|
|
|
76
77
|
// `HTTP2Unsupported`. See @gajae-code/ai/utils/h2-fetch for details.
|
|
77
78
|
installH2Fetch();
|
|
78
79
|
|
|
80
|
+
const { warnIfMacOSNoFileLimitTooLow } = await import("./cli/nofile-limit");
|
|
81
|
+
warnIfMacOSNoFileLimitTooLow();
|
|
82
|
+
|
|
79
83
|
// Strip macOS malloc-stack-logging env vars before any subprocess is spawned.
|
|
80
84
|
// Otherwise every child bun process (subagents, plugin installs, ptree spawns,
|
|
81
85
|
// etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
|
|
@@ -96,6 +100,17 @@ async function runNotifyDaemonInternalFastPath(argv: string[]): Promise<void> {
|
|
|
96
100
|
await runNotifyCommand(cmd);
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
function rootFixtureArg(argv: string[]): { present: boolean; id: string | undefined } {
|
|
104
|
+
for (let i = 0; i < argv.length; i++) {
|
|
105
|
+
const arg = argv[i];
|
|
106
|
+
// Stop at the first subcommand token so a `--fixture` flag belonging to a
|
|
107
|
+
// subcommand never hijacks the root fast-path into fixture-report mode.
|
|
108
|
+
if (isSubcommand(arg)) return { present: false, id: undefined };
|
|
109
|
+
if (arg === "--fixture") return { present: true, id: argv[i + 1] };
|
|
110
|
+
}
|
|
111
|
+
return { present: false, id: undefined };
|
|
112
|
+
}
|
|
113
|
+
|
|
99
114
|
function hasRootFastFlag(argv: string[], flags: readonly string[]): boolean {
|
|
100
115
|
for (const arg of argv) {
|
|
101
116
|
if (isSubcommand(arg)) return false;
|
|
@@ -233,6 +248,17 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
233
248
|
await runSmokeTest();
|
|
234
249
|
return;
|
|
235
250
|
}
|
|
251
|
+
const fixtureArg = rootFixtureArg(argv);
|
|
252
|
+
if (fixtureArg.present) {
|
|
253
|
+
const id = fixtureArg.id;
|
|
254
|
+
if (!id || id.startsWith("-")) {
|
|
255
|
+
process.stderr.write(`${APP_NAME} --fixture requires a fixture id\n`);
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
process.exitCode = await runFixtureReport(id);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
236
262
|
if (hasRootHelpFlag(argv)) {
|
|
237
263
|
const { renderRootHelp } = await import("@gajae-code/utils/cli");
|
|
238
264
|
const { getExtraHelpText } = await import("./cli/fast-help");
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Command, Flags } from "@gajae-code/utils/cli";
|
|
2
|
-
import { prepareContributionPrep } from "../session/contribution-prep";
|
|
3
2
|
|
|
4
3
|
function writeText(lines: string[]): void {
|
|
5
4
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
@@ -19,6 +18,7 @@ export default class ContributionPrep extends Command {
|
|
|
19
18
|
|
|
20
19
|
async run(): Promise<void> {
|
|
21
20
|
const { flags } = await this.parse(ContributionPrep);
|
|
21
|
+
const { prepareContributionPrep } = await import("../session/contribution-prep");
|
|
22
22
|
const cwd = process.cwd();
|
|
23
23
|
const result = await prepareContributionPrep(
|
|
24
24
|
{
|