@triflux/remote 10.37.0 → 10.39.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/cto/events.mjs +267 -0
- package/package.json +1 -1
- package/scripts/lib/gemini-profiles.mjs +73 -9
package/cto/events.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
unlinkSync,
|
|
7
7
|
} from "node:fs";
|
|
8
8
|
import { basename, join } from "node:path";
|
|
9
|
+
import { resolveLakeRootDir } from "./lake-root.mjs";
|
|
9
10
|
|
|
10
11
|
export const CTO_EVENT_SCHEMA_VERSION = "cto-event.v1";
|
|
11
12
|
export const DEFAULT_CTO_EVENT_SOURCE = "tfx_cto_event";
|
|
@@ -41,6 +42,98 @@ export const CTO_HYGIENE_STATUSES = Object.freeze([
|
|
|
41
42
|
const EVENT_TYPE_SET = new Set(CTO_EVENT_TYPES);
|
|
42
43
|
const STATUS_SET = new Set(CTO_HYGIENE_STATUSES);
|
|
43
44
|
|
|
45
|
+
const EVENT_PRESETS = Object.freeze({
|
|
46
|
+
"context-save": {
|
|
47
|
+
event: "checkpoint_saved",
|
|
48
|
+
source: "gstack_context_save",
|
|
49
|
+
actor: { cli: "gstack context-save" },
|
|
50
|
+
ownerSurface: "gstack context-save -> tfx cto event context-save",
|
|
51
|
+
},
|
|
52
|
+
"checkpoint-saved": {
|
|
53
|
+
event: "checkpoint_saved",
|
|
54
|
+
ownerSurface: "checkpoint wrapper -> tfx cto event checkpoint-saved",
|
|
55
|
+
},
|
|
56
|
+
"context-restore": {
|
|
57
|
+
event: "checkpoint_restored",
|
|
58
|
+
source: "gstack_context_restore",
|
|
59
|
+
actor: { cli: "gstack context-restore" },
|
|
60
|
+
ownerSurface: "gstack context-restore -> tfx cto event context-restore",
|
|
61
|
+
},
|
|
62
|
+
"checkpoint-restored": {
|
|
63
|
+
event: "checkpoint_restored",
|
|
64
|
+
ownerSurface: "checkpoint wrapper -> tfx cto event checkpoint-restored",
|
|
65
|
+
},
|
|
66
|
+
"pr-created": {
|
|
67
|
+
event: "pr_created",
|
|
68
|
+
source: "tfx_pr_lifecycle",
|
|
69
|
+
actor: { cli: "gh pr create" },
|
|
70
|
+
ownerSurface: "gh pr create/merge/close -> tfx cto event pr-created",
|
|
71
|
+
},
|
|
72
|
+
"pr-merged": {
|
|
73
|
+
event: "pr_merged_or_closed",
|
|
74
|
+
source: "tfx_pr_lifecycle",
|
|
75
|
+
status: "completed",
|
|
76
|
+
actor: { cli: "gh pr merge" },
|
|
77
|
+
ownerSurface:
|
|
78
|
+
"gh pr create/merge/close -> tfx cto event pr-merged-or-closed",
|
|
79
|
+
},
|
|
80
|
+
"pr-closed": {
|
|
81
|
+
event: "pr_merged_or_closed",
|
|
82
|
+
source: "tfx_pr_lifecycle",
|
|
83
|
+
status: "completed",
|
|
84
|
+
actor: { cli: "gh pr close" },
|
|
85
|
+
ownerSurface:
|
|
86
|
+
"gh pr create/merge/close -> tfx cto event pr-merged-or-closed",
|
|
87
|
+
},
|
|
88
|
+
"pr-merged-or-closed": {
|
|
89
|
+
event: "pr_merged_or_closed",
|
|
90
|
+
source: "tfx_pr_lifecycle",
|
|
91
|
+
actor: { cli: "gh pr merge/close" },
|
|
92
|
+
ownerSurface:
|
|
93
|
+
"gh pr create/merge/close -> tfx cto event pr-merged-or-closed",
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const OPTION_KEYS = Object.freeze({
|
|
98
|
+
event: "event",
|
|
99
|
+
"event-type": "event",
|
|
100
|
+
source: "source",
|
|
101
|
+
summary: "summary",
|
|
102
|
+
now: "now",
|
|
103
|
+
ts: "ts",
|
|
104
|
+
status: "status",
|
|
105
|
+
"session-id": "session_id",
|
|
106
|
+
"parent-session-id": "parent_session_id",
|
|
107
|
+
"restored-from-session-id": "restored_from_session_id",
|
|
108
|
+
"checkpoint-id": "checkpoint_id",
|
|
109
|
+
"artifact-path": "artifact_path",
|
|
110
|
+
artifact: "artifact_path",
|
|
111
|
+
"task-id": "task_id",
|
|
112
|
+
branch: "branch",
|
|
113
|
+
"last-seen-at": "last_seen_at",
|
|
114
|
+
"stale-reason": "stale_reason",
|
|
115
|
+
"hygiene-key": "hygiene_key",
|
|
116
|
+
"hygiene-kind": "hygiene_kind",
|
|
117
|
+
"hygiene-id": "hygiene_id",
|
|
118
|
+
"hygiene-action": "hygiene_action",
|
|
119
|
+
"project-root": "project_root",
|
|
120
|
+
"worktree-path": "worktree_path",
|
|
121
|
+
worktree: "worktree_path",
|
|
122
|
+
issue: "issue_refs",
|
|
123
|
+
"issue-ref": "issue_refs",
|
|
124
|
+
"issue-refs": "issue_refs",
|
|
125
|
+
pr: "pr_refs",
|
|
126
|
+
"pr-ref": "pr_refs",
|
|
127
|
+
"pr-refs": "pr_refs",
|
|
128
|
+
"actor-cli": "actor.cli",
|
|
129
|
+
"actor-session-id": "actor.session_id",
|
|
130
|
+
"actor-agent-id": "actor.agent_id",
|
|
131
|
+
"actor-host": "actor.host",
|
|
132
|
+
"owner-surface": "owner_surface",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const COLLECTION_KEYS = new Set(["issue_refs", "pr_refs"]);
|
|
136
|
+
|
|
44
137
|
function sleep(ms) {
|
|
45
138
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
46
139
|
}
|
|
@@ -107,6 +200,124 @@ function normalizeActor(actor) {
|
|
|
107
200
|
return Object.keys(normalized).length > 0 ? normalized : null;
|
|
108
201
|
}
|
|
109
202
|
|
|
203
|
+
function optionName(raw) {
|
|
204
|
+
return String(raw || "")
|
|
205
|
+
.replace(/^--?/u, "")
|
|
206
|
+
.trim();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function appendCollection(target, key, value) {
|
|
210
|
+
const values = String(value ?? "")
|
|
211
|
+
.split(",")
|
|
212
|
+
.map((item) => item.trim())
|
|
213
|
+
.filter(Boolean);
|
|
214
|
+
if (values.length === 0) return;
|
|
215
|
+
if (!Array.isArray(target[key])) target[key] = [];
|
|
216
|
+
target[key].push(...values);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function putPathValue(target, keyPath, value) {
|
|
220
|
+
if (keyPath.startsWith("actor.")) {
|
|
221
|
+
const key = keyPath.slice("actor.".length);
|
|
222
|
+
target.actor ||= {};
|
|
223
|
+
target.actor[key] = value;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (COLLECTION_KEYS.has(keyPath)) {
|
|
227
|
+
appendCollection(target, keyPath, value);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
target[keyPath] = value;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parseEventArgs(args = []) {
|
|
234
|
+
const fields = {};
|
|
235
|
+
const positional = [];
|
|
236
|
+
let json = false;
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
239
|
+
const arg = args[i];
|
|
240
|
+
if (arg === "--json") {
|
|
241
|
+
json = true;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (!arg?.startsWith?.("--")) {
|
|
245
|
+
positional.push(arg);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const eqIndex = arg.indexOf("=");
|
|
250
|
+
const rawName = eqIndex >= 0 ? arg.slice(0, eqIndex) : arg;
|
|
251
|
+
const name = optionName(rawName);
|
|
252
|
+
const mapped = OPTION_KEYS[name];
|
|
253
|
+
if (!mapped) {
|
|
254
|
+
throw new Error(`unknown tfx cto event option: --${name}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let value = eqIndex >= 0 ? arg.slice(eqIndex + 1) : null;
|
|
258
|
+
if (value === null) {
|
|
259
|
+
const next = args[i + 1];
|
|
260
|
+
if (typeof next === "string" && !next.startsWith("--")) {
|
|
261
|
+
value = next;
|
|
262
|
+
i += 1;
|
|
263
|
+
} else {
|
|
264
|
+
value = "true";
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
putPathValue(fields, mapped, value);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
presetName: positional[0] || null,
|
|
272
|
+
extraPositionals: positional.slice(1),
|
|
273
|
+
fields,
|
|
274
|
+
json,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function mergeActor(defaultActor, overrideActor) {
|
|
279
|
+
const actor = { ...(defaultActor || {}), ...(overrideActor || {}) };
|
|
280
|
+
return Object.keys(actor).length > 0 ? actor : undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function defaultEventSummary(input) {
|
|
284
|
+
if (input.event === "checkpoint_saved") {
|
|
285
|
+
return `context-save checkpoint ${input.checkpoint_id}`;
|
|
286
|
+
}
|
|
287
|
+
if (input.event === "checkpoint_restored") {
|
|
288
|
+
return `context-restore checkpoint ${input.checkpoint_id}`;
|
|
289
|
+
}
|
|
290
|
+
if (input.event === "pr_created") {
|
|
291
|
+
const [pr] = normalizeNumericRefs(input.pr_refs);
|
|
292
|
+
return pr ? `PR #${pr} created` : "PR created";
|
|
293
|
+
}
|
|
294
|
+
if (input.event === "pr_merged_or_closed") {
|
|
295
|
+
const [pr] = normalizeNumericRefs(input.pr_refs);
|
|
296
|
+
return pr ? `PR #${pr} merged or closed` : "PR merged or closed";
|
|
297
|
+
}
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function validateRunEventInput(input) {
|
|
302
|
+
if (!input.event) {
|
|
303
|
+
throw new Error("tfx cto event requires a preset or --event <event_type>");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (
|
|
307
|
+
["checkpoint_saved", "checkpoint_restored"].includes(input.event) &&
|
|
308
|
+
!maybeString(input.checkpoint_id)
|
|
309
|
+
) {
|
|
310
|
+
throw new Error(`tfx cto event ${input.event} requires --checkpoint-id`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (
|
|
314
|
+
["pr_created", "pr_merged_or_closed"].includes(input.event) &&
|
|
315
|
+
normalizeNumericRefs(input.pr_refs).length === 0
|
|
316
|
+
) {
|
|
317
|
+
throw new Error(`tfx cto event ${input.event} requires --pr`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
110
321
|
function putString(target, key, value) {
|
|
111
322
|
const normalized = maybeString(value);
|
|
112
323
|
if (normalized) target[key] = normalized;
|
|
@@ -220,3 +431,59 @@ export async function appendCtoEvent(lakeRoot, input, opts = {}) {
|
|
|
220
431
|
} catch {}
|
|
221
432
|
}
|
|
222
433
|
}
|
|
434
|
+
|
|
435
|
+
export async function runEvent(args = [], opts = {}) {
|
|
436
|
+
const parsed = parseEventArgs(args);
|
|
437
|
+
if (parsed.extraPositionals.length > 0) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
`unexpected tfx cto event argument: ${parsed.extraPositionals[0]}`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
const preset = parsed.presetName ? EVENT_PRESETS[parsed.presetName] : null;
|
|
443
|
+
if (parsed.presetName && !preset && !parsed.fields.event) {
|
|
444
|
+
throw new Error(`unknown tfx cto event preset: ${parsed.presetName}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const rootDir = opts.rootDir || resolveLakeRootDir(process.cwd());
|
|
448
|
+
const lakeRoot = opts.lakeRoot || join(rootDir, ".triflux", "lake");
|
|
449
|
+
const stdout = opts.stdout || process.stdout;
|
|
450
|
+
const jsonOut = opts.json === true || parsed.json;
|
|
451
|
+
const ownerSurface =
|
|
452
|
+
parsed.fields.owner_surface ||
|
|
453
|
+
preset?.ownerSurface ||
|
|
454
|
+
"external wrapper -> tfx cto event --event";
|
|
455
|
+
|
|
456
|
+
const input = {
|
|
457
|
+
...(preset || {}),
|
|
458
|
+
...parsed.fields,
|
|
459
|
+
actor: mergeActor(preset?.actor, parsed.fields.actor),
|
|
460
|
+
};
|
|
461
|
+
delete input.ownerSurface;
|
|
462
|
+
delete input.owner_surface;
|
|
463
|
+
if (!input.project_root) input.project_root = rootDir;
|
|
464
|
+
if (!input.summary) input.summary = defaultEventSummary(input);
|
|
465
|
+
|
|
466
|
+
validateRunEventInput(input);
|
|
467
|
+
|
|
468
|
+
const result = await appendCtoEvent(lakeRoot, input, {
|
|
469
|
+
stderr: opts.stderr,
|
|
470
|
+
lockRetries: opts.ledgerLockRetries,
|
|
471
|
+
lockRetryDelayMs: opts.ledgerLockRetryDelayMs,
|
|
472
|
+
});
|
|
473
|
+
const payload = {
|
|
474
|
+
appended: result.appended,
|
|
475
|
+
owner_surface: ownerSurface,
|
|
476
|
+
event: result.event,
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
if (jsonOut) {
|
|
480
|
+
stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
481
|
+
} else {
|
|
482
|
+
const status = result.appended ? "appended" : "skipped";
|
|
483
|
+
stdout.write(
|
|
484
|
+
`[tfx cto event] ${status} ${result.event.event} via ${ownerSurface}\n`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return payload;
|
|
489
|
+
}
|
package/package.json
CHANGED
|
@@ -8,23 +8,49 @@ import {
|
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
|
|
11
|
+
// 모델명 SSOT. 값은 agy 1.0.x `agy models` 가 출력하는 display name 형식이다
|
|
12
|
+
// (`--model` 인자가 이 문자열을 받는다). 옛 `gemini-*-preview` ID 형식이 아님.
|
|
11
13
|
const DEFAULT_GEMINI_PROFILES = {
|
|
12
|
-
model: "
|
|
14
|
+
model: "Gemini 3.5 Flash (Medium)",
|
|
13
15
|
profiles: {
|
|
16
|
+
flash35_low: {
|
|
17
|
+
model: "Gemini 3.5 Flash (Low)",
|
|
18
|
+
hint: "3.5 Flash (Low) — 경량·저비용",
|
|
19
|
+
},
|
|
20
|
+
flash35: {
|
|
21
|
+
model: "Gemini 3.5 Flash (Medium)",
|
|
22
|
+
hint: "3.5 Flash (Medium) — 기본, 비용·속도 균형",
|
|
23
|
+
},
|
|
24
|
+
flash35_high: {
|
|
25
|
+
model: "Gemini 3.5 Flash (High)",
|
|
26
|
+
hint: "3.5 Flash (High) — 코드/추론 강화",
|
|
27
|
+
},
|
|
28
|
+
pro31_low: {
|
|
29
|
+
model: "Gemini 3.1 Pro (Low)",
|
|
30
|
+
hint: "3.1 Pro (Low) — 플래그십 경량",
|
|
31
|
+
},
|
|
14
32
|
pro31: {
|
|
15
|
-
model: "
|
|
16
|
-
hint: "3.1 Pro — 플래그십 (
|
|
33
|
+
model: "Gemini 3.1 Pro (High)",
|
|
34
|
+
hint: "3.1 Pro (High) — 플래그십 최고 (긴 컨텍스트/복잡 추론)",
|
|
17
35
|
},
|
|
18
36
|
flash3: {
|
|
19
|
-
model: "
|
|
20
|
-
hint: "3.0 Flash —
|
|
37
|
+
model: "Gemini 3 Flash",
|
|
38
|
+
hint: "3.0 Flash — 레거시 호환",
|
|
21
39
|
},
|
|
22
|
-
pro25: { model: "gemini-2.5-pro", hint: "2.5 Pro — 안정 (추론 강화)" },
|
|
23
|
-
flash25: { model: "gemini-2.5-flash", hint: "2.5 Flash — 경량 범용" },
|
|
24
|
-
lite25: { model: "gemini-2.5-flash-lite", hint: "2.5 Flash Lite — 최경량" },
|
|
25
40
|
},
|
|
26
41
|
};
|
|
27
42
|
|
|
43
|
+
// 1회 마이그레이션 테이블: 옛 ID 형식 모델값 → agy display name.
|
|
44
|
+
// null = deprecated(2.5 계열) → 해당 프로필 제거 대상.
|
|
45
|
+
const LEGACY_MODEL_MIGRATION = {
|
|
46
|
+
"gemini-3.1-pro-preview": "Gemini 3.1 Pro (High)",
|
|
47
|
+
"gemini-3-flash-preview": "Gemini 3 Flash",
|
|
48
|
+
"gemini-2.5-pro": null,
|
|
49
|
+
"gemini-2.5-flash": null,
|
|
50
|
+
"gemini-2.5-flash-lite": null,
|
|
51
|
+
};
|
|
52
|
+
const LEGACY_PROFILE_NAMES = ["pro25", "flash25", "lite25"];
|
|
53
|
+
|
|
28
54
|
const DEFAULT_PROFILE_COUNT = Object.keys(
|
|
29
55
|
DEFAULT_GEMINI_PROFILES.profiles,
|
|
30
56
|
).length;
|
|
@@ -80,6 +106,39 @@ function ensureGeminiProfiles({
|
|
|
80
106
|
)
|
|
81
107
|
cfg.profiles = {};
|
|
82
108
|
|
|
109
|
+
// ── 1회 마이그레이션: deprecated 2.5 프로필 prune + 옛 ID 형식 → display name ──
|
|
110
|
+
// merge-only 로직만으로는 기존 사용자 파일에 남은 stale 프로필/옛 모델 ID 가
|
|
111
|
+
// 정리되지 않으므로, 신규 default 를 채우기 전에 마이그레이션을 먼저 적용한다.
|
|
112
|
+
let migrated = false;
|
|
113
|
+
for (const legacy of LEGACY_PROFILE_NAMES) {
|
|
114
|
+
if (cfg.profiles[legacy]) {
|
|
115
|
+
delete cfg.profiles[legacy];
|
|
116
|
+
migrated = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const [pname, pval] of Object.entries(cfg.profiles)) {
|
|
120
|
+
const mid = typeof pval === "string" ? pval : pval?.model;
|
|
121
|
+
if (mid && Object.hasOwn(LEGACY_MODEL_MIGRATION, mid)) {
|
|
122
|
+
const repl = LEGACY_MODEL_MIGRATION[mid];
|
|
123
|
+
if (repl === null) {
|
|
124
|
+
delete cfg.profiles[pname];
|
|
125
|
+
} else if (typeof pval === "string") {
|
|
126
|
+
cfg.profiles[pname] = repl;
|
|
127
|
+
} else {
|
|
128
|
+
cfg.profiles[pname].model = repl;
|
|
129
|
+
}
|
|
130
|
+
migrated = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (
|
|
134
|
+
typeof cfg.model === "string" &&
|
|
135
|
+
Object.hasOwn(LEGACY_MODEL_MIGRATION, cfg.model)
|
|
136
|
+
) {
|
|
137
|
+
// 옛 ID 형식 기본값은 구버전 자동 생성값이므로 새 기본(DEFAULT)으로 정규화한다.
|
|
138
|
+
cfg.model = DEFAULT_GEMINI_PROFILES.model;
|
|
139
|
+
migrated = true;
|
|
140
|
+
}
|
|
141
|
+
|
|
83
142
|
let added = 0;
|
|
84
143
|
for (const [name, value] of Object.entries(
|
|
85
144
|
DEFAULT_GEMINI_PROFILES.profiles,
|
|
@@ -91,7 +150,12 @@ function ensureGeminiProfiles({
|
|
|
91
150
|
}
|
|
92
151
|
if (!cfg.model) cfg.model = DEFAULT_GEMINI_PROFILES.model;
|
|
93
152
|
|
|
94
|
-
if (added > 0) {
|
|
153
|
+
if (added > 0 || migrated) {
|
|
154
|
+
if (migrated) {
|
|
155
|
+
try {
|
|
156
|
+
copyFileSync(profilesPath, profilesPath + `.bak.${Date.now()}`);
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
95
159
|
writeFileSync(profilesPath, JSON.stringify(cfg, null, 2) + "\n", {
|
|
96
160
|
encoding: "utf8",
|
|
97
161
|
mode: 0o600,
|