@lawaty/lacode 1.3.1 → 2.1.0-beta.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 +40 -1
- package/README.md +3 -6
- package/dist/agents.js +3 -41
- package/dist/commands.js +2 -5
- package/dist/index.js +445 -249
- package/docs/CONTEXT.md +17 -33
- package/docs/USER-STORIES.md +27 -2
- package/package.json +1 -1
- package/scripts/refresh-pool.mjs +74 -63
- package/scripts/score-models.mjs +111 -37
package/docs/CONTEXT.md
CHANGED
|
@@ -30,8 +30,7 @@ the exploration it replaces is worse than no map:
|
|
|
30
30
|
`src/orders/`". The cartographer is forbidden by name from writing directory
|
|
31
31
|
listings, copying source, or restating what the README already says.
|
|
32
32
|
- **One file per question.** Five navigation files, each with a defined scope, so
|
|
33
|
-
an agent reads one of them rather than all of them.
|
|
34
|
-
`active.md`, holds the work in flight rather than a map of the repository.
|
|
33
|
+
an agent reads one of them rather than all of them.
|
|
35
34
|
- **Canonical entry points.** The highest-value thing in the map is a short
|
|
36
35
|
"Start here" list per subsystem: the canonical files an agent should open, each
|
|
37
36
|
with a one-phrase reason.
|
|
@@ -48,8 +47,7 @@ harness that produces a number for your own workload.
|
|
|
48
47
|
|
|
49
48
|
## The files in the map
|
|
50
49
|
|
|
51
|
-
`.opencode/context/` holds the five navigation files below
|
|
52
|
-
a work list rather than a map:
|
|
50
|
+
`.opencode/context/` holds the five navigation files below:
|
|
53
51
|
|
|
54
52
|
| File | What belongs in it |
|
|
55
53
|
| --- | --- |
|
|
@@ -58,7 +56,6 @@ a work list rather than a map:
|
|
|
58
56
|
| `conventions.md` | Repository-specific rules: naming, module layout, error handling, testing, persistence. Read this before writing code, not after. |
|
|
59
57
|
| `workflows.md` | The few runtime flows worth tracing end to end — how a request becomes a mutation, how a plugin hook reaches the thing it hooks. Read this when you need to know what actually happens at runtime. |
|
|
60
58
|
| `decisions.md` | Architectural constraints that bind future work and that the repository's own decision log does not already record. Each entry states the constraint and points at the page or path that owns it. Capped at 120 non-blank lines; decision records and deliberation are not copied here. |
|
|
61
|
-
| `active.md` | The active backlog: work in flight, grouped by the ISO week each task was opened in. A task that is not marked `[x]` is open and is never removed; a task that is finished and dated is kept for a week after it was finished, then dropped. The plugin enforces that window — see [How it stays current](#how-it-stays-current). Not a navigation file: there is no question it is the answer to. |
|
|
62
59
|
|
|
63
60
|
The project's root `AGENTS.md` is the standing-instructions home: the client's
|
|
64
61
|
own statement of intent, what they want worked on and how they want it
|
|
@@ -98,20 +95,19 @@ Every navigation entry has one shape, in every file:
|
|
|
98
95
|
the drift check.
|
|
99
96
|
|
|
100
97
|
Every file has a hard cap, counted in **non-blank lines**: architecture 150,
|
|
101
|
-
contexts 200, conventions 150, workflows 150, decisions 120
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
cartographer writes the shape, code decides what has aged out.
|
|
98
|
+
contexts 200, conventions 150, workflows 150, decisions 120. Lines are the unit
|
|
99
|
+
because they are what a reader pays for, they bound an entry that wraps to three
|
|
100
|
+
lines, and the validator can count them exactly; an entry count does not bound
|
|
101
|
+
cost, and a token count needs a tokenizer and is model-specific. The cap is
|
|
102
|
+
enforced by a post-generation validator, not only by the prompt.
|
|
103
|
+
|
|
104
|
+
A `[provisional]` entry in any navigation file is dropped 30 days after its
|
|
105
|
+
`verified` date unless it is re-verified. `[binding]` and `[disputed]` never
|
|
106
|
+
expire by age. An entry whose cited source no longer exists is **never**
|
|
107
|
+
auto-dropped: it may be the only copy, so it is reported as unarchived and kept.
|
|
108
|
+
Only provisional entries whose source still resolves are eligible for the sweep.
|
|
109
|
+
The arithmetic lives in `src/lib/map-schema.ts`; the cartographer writes the
|
|
110
|
+
shape, code decides what has aged out.
|
|
115
111
|
|
|
116
112
|
The map is **local and untracked**. It describes *your* checkout on *this*
|
|
117
113
|
machine, so committing it would churn on every session that edits a file, and
|
|
@@ -193,25 +189,13 @@ your source.
|
|
|
193
189
|
If you have defined a command of the same name, yours wins and is left exactly
|
|
194
190
|
as you wrote it — the same merge-without-clobber rule the agents follow.
|
|
195
191
|
|
|
196
|
-
`active.md` is the file the plugin maintains as a backlog rather than as a
|
|
197
|
-
navigation map. The cartographer records what happened — work in flight, and a
|
|
198
|
-
task marked finished with the date it finished — and the plugin decides what is
|
|
199
|
-
still worth keeping: the file is regrouped by week and a task that was finished
|
|
200
|
-
more than a week ago is dropped. The arithmetic lives in
|
|
201
|
-
`src/lib/active-backlog.ts`, not in the cartographer's prompt, because a rule a
|
|
202
|
-
model applies is a rule the model applies when it feels like it, and the failure
|
|
203
|
-
is silent — the backlog just grows. The plugin normalizes the file on every edit
|
|
204
|
-
of it and once when the session starts, so a hand edit is grouped and pruned
|
|
205
|
-
whether the cartographer ran or not, and the file is rewritten only when its
|
|
206
|
-
canonical form differs from what is on disk, so the write cannot loop.
|
|
207
|
-
|
|
208
192
|
The same code owns the navigation files' lifecycle, but it may only **drop** an
|
|
209
193
|
entry that is `[provisional]`, past its 30-day window, and whose cited source
|
|
210
194
|
still exists. It never drops a binding constraint or disputed entry, and it
|
|
211
195
|
never drops an entry with no source — that is the one case where the map may be
|
|
212
196
|
the only copy. The plugin is not a second writer of the map in the permission
|
|
213
197
|
sense: `context` is still the only **agent** that writes it. The lifecycle is
|
|
214
|
-
housekeeping by code,
|
|
198
|
+
housekeeping by code, applied after a run and then audited.
|
|
215
199
|
|
|
216
200
|
## One writer
|
|
217
201
|
|
|
@@ -301,7 +285,7 @@ to check is that permission rule, not the map's content.
|
|
|
301
285
|
- The prompt the model actually runs, in one place: `src/lib/cartographer-prompt.ts`; the tracked copy is `.opencode/prompts/context.txt`, and a test pins equality.
|
|
302
286
|
- The idle/edit trigger and the post-run audit wiring: `src/plugins/context-autoupdate.ts`.
|
|
303
287
|
- The schema, budget, lifecycle and deterministic drift check: `src/lib/map-schema.ts`.
|
|
304
|
-
|
|
288
|
+
|
|
305
289
|
- The one-writer rule check: `src/lib/writer-rule.ts`.
|
|
306
290
|
- Fixtures and their report: `tests/fixtures/context-map/`, `scripts/context-map-fixture.mjs`.
|
|
307
291
|
- The behavioural contracts, with acceptance criteria a test asserts:
|
package/docs/USER-STORIES.md
CHANGED
|
@@ -471,6 +471,29 @@ Enforcement is split deliberately, and the split matters: **what the permission
|
|
|
471
471
|
- [ ] Given the user's config already defines one of the three commands, When the config hook runs, Then that command is left byte-identical while the other two are still added.
|
|
472
472
|
- [ ] Given the commands are injected, When each one's target agent is resolved against the same config object, Then all three resolve to an agent that actually exists there — a command must never point at a missing agent.
|
|
473
473
|
|
|
474
|
+
### US-35: A timeline survives a restart and resumes unfinished work
|
|
475
|
+
**As a** user whose opencode process restarts in the middle of a session, **I want** the timeline to remember what it has already summarized and to finish the slice it had not written yet, **so that** a crash or restart never silently loses part of the session's work record.
|
|
476
|
+
|
|
477
|
+
The cursor and retry state used to live only in memory; a process that stopped between messages and the next idle flush forgot that a slice was pending, and a successful summary that produced no bullet text advanced the cursor only in memory, so a restart could summarize the same slice again. The plugin now writes a small per-session state file next to the JSONL timeline (cursor, pending flag, failure count, next retry time); startup resumes pending work, and a deleted session removes both files.
|
|
478
|
+
|
|
479
|
+
**Story ID(s) for tests:** US-35
|
|
480
|
+
**Acceptance criteria:**
|
|
481
|
+
- [ ] Given a session has a pending timeline slice recorded on disk, When the process restarts, Then the timeline resumes and completes that slice without waiting for a new message event.
|
|
482
|
+
- [ ] Given a summarization succeeds but produces no bullet text, When the process restarts, Then the advanced cursor is still known and the same transcript slice is not summarized again.
|
|
483
|
+
- [ ] Given a session is deleted, When its timeline is removed, Then its durable state file is removed with it.
|
|
484
|
+
|
|
485
|
+
### US-36: A failed timeline summary retries with backoff
|
|
486
|
+
**As a** user whose summarizer call fails transiently, **I want** the timeline to retry on its own with an increasing delay, **so that** a temporary model or network failure does not leave the timeline permanently behind the session.
|
|
487
|
+
|
|
488
|
+
A failed summarization used to clear the pending flag and never reschedule, so only new session activity could try again. A failure now marks the slice pending, records the next attempt time, and schedules a retry; repeated failures back off exponentially up to a cap, and an idle event during the wait re-arms the timer instead of bypassing it. A successful retry clears the failure count.
|
|
489
|
+
|
|
490
|
+
**Story ID(s) for tests:** US-36
|
|
491
|
+
**Acceptance criteria:**
|
|
492
|
+
- [ ] Given a summarization fails transiently, When the retry interval elapses, Then the timeline retries without waiting for new session activity.
|
|
493
|
+
- [ ] Given repeated failures, When retries are scheduled, Then the wait grows with each failure up to a configured cap.
|
|
494
|
+
- [ ] Given a retry is waiting, When the session goes idle before the backoff window elapses, Then the idle event does not bypass the wait.
|
|
495
|
+
- [ ] Given a retry succeeds, When entries are appended, Then the failure count resets to zero and the timeline is no longer pending.
|
|
496
|
+
|
|
474
497
|
---
|
|
475
498
|
|
|
476
499
|
## Epic H — Retention (sweeps)
|
|
@@ -554,6 +577,8 @@ table was filled in.
|
|
|
554
577
|
| US-20 | G | `tests/stories/g-context-management.test.mjs` |
|
|
555
578
|
| US-21 | G | `tests/stories/g-context-management.test.mjs`, `tests/permission-test.mjs` |
|
|
556
579
|
| US-22 | G | `tests/stories/g-context-management.test.mjs` |
|
|
580
|
+
| US-35 | G | `tests/stories/j-timeline-reliability.test.mjs` |
|
|
581
|
+
| US-36 | G | `tests/stories/j-timeline-reliability.test.mjs` |
|
|
557
582
|
| US-32 | H | `tests/retire-old-sessions-test.mjs` |
|
|
558
583
|
| US-33 | H | `tests/prune-old-events-test.mjs` |
|
|
559
584
|
|
|
@@ -561,8 +586,8 @@ The nine suites under `tests/` are unit-style; the `tests/stories/` suites are
|
|
|
561
586
|
the behavioural layer, written one epic per file with every test name carrying
|
|
562
587
|
its story id. Run everything with `npm test`.
|
|
563
588
|
|
|
564
|
-
**No story is uncovered.** The guard reports
|
|
565
|
-
|
|
589
|
+
**No story is uncovered.** The guard reports 36 stories, 348 test cases, and
|
|
590
|
+
36/36 stories with at least one behavioural test; US-1, US-2, US-3, US-6, and
|
|
566
591
|
all of epic F have tests, and the coverage guard fails the build if any of them
|
|
567
592
|
loses one. The scan covers the `tests/stories/` suites and the root-level
|
|
568
593
|
`tests/*-test.mjs` suites.
|
package/package.json
CHANGED
package/scripts/refresh-pool.mjs
CHANGED
|
@@ -6,20 +6,19 @@
|
|
|
6
6
|
// writes on a fail-closed condition.
|
|
7
7
|
//
|
|
8
8
|
// Layout (exact, per spec):
|
|
9
|
-
// 1. every
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// weight 2, with b:opencode
|
|
9
|
+
// 1. every opencode-go model with ok:true, a numeric score at or above the
|
|
10
|
+
// score floor and a context at or above the context floor, sorted by score
|
|
11
|
+
// descending, weight 4
|
|
12
|
+
// 2. the best (10 - n) models from every other provider, sorted by score
|
|
13
|
+
// descending, weight 2; opencode models get b:opencode
|
|
15
14
|
//
|
|
16
|
-
// Total =
|
|
15
|
+
// Total = n + min(10 - n, #others), where n = #1.
|
|
17
16
|
//
|
|
18
17
|
// This file ships as source and must run with plain `node` (ESM, no TS).
|
|
19
18
|
|
|
20
|
-
import {
|
|
19
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"
|
|
21
20
|
import { execFileSync } from "node:child_process"
|
|
22
|
-
import { homedir } from "node:os"
|
|
21
|
+
import { homedir, userInfo } from "node:os"
|
|
23
22
|
import { dirname, join, resolve } from "node:path"
|
|
24
23
|
import { fileURLToPath } from "node:url"
|
|
25
24
|
|
|
@@ -27,6 +26,8 @@ const HOME = homedir()
|
|
|
27
26
|
export const DEFAULT_SCORES_PATH = join(HOME, ".config", "lacode", "model-scores.json")
|
|
28
27
|
export const DEFAULT_POOL_PATH = join(HOME, ".config", "lacode", "pool.txt")
|
|
29
28
|
export const SYSTEMD_USER_DIR = (home = HOME) => join(home, ".config", "systemd", "user")
|
|
29
|
+
export const STABLE_LIB_DIR = (home = HOME) => join(home, ".local", "lib", "lacode")
|
|
30
|
+
export const STABLE_SCRIPT = (home = HOME) => join(STABLE_LIB_DIR(home), "refresh-pool.mjs")
|
|
30
31
|
export const REFRESH_SERVICE_NAME = "lacode-refresh-pool.service"
|
|
31
32
|
export const REFRESH_TIMER_NAME = "lacode-refresh-pool.timer"
|
|
32
33
|
|
|
@@ -42,17 +43,12 @@ const providerOf = (modelOrLine) => {
|
|
|
42
43
|
return slash < 0 ? token : token.slice(0, slash)
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
const isFree = (modelId) => {
|
|
46
|
-
const id = String(modelId || "")
|
|
47
|
-
return id.endsWith(":free") || id.endsWith("-free")
|
|
48
|
-
}
|
|
49
|
-
|
|
50
46
|
const numericScore = (row) => typeof row?.score === "number" && Number.isFinite(row.score)
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
// Fallbacks for score files written before the floors were recorded there.
|
|
49
|
+
const MIN_CONTEXT = 200_000
|
|
50
|
+
const MIN_SCORE = 60
|
|
51
|
+
const POOL_SIZE = 10
|
|
56
52
|
|
|
57
53
|
function splitHeaderAndSlots(text) {
|
|
58
54
|
const lines = String(text ?? "").split(/\r?\n/)
|
|
@@ -70,7 +66,7 @@ function splitHeaderAndSlots(text) {
|
|
|
70
66
|
function refreshComment(now, scoresGeneratedAt) {
|
|
71
67
|
const when = now instanceof Date ? now.toISOString() : String(now)
|
|
72
68
|
const gen = scoresGeneratedAt ? ` (scores generated ${scoresGeneratedAt})` : ""
|
|
73
|
-
return `# Refreshed ${when} from model-scores.json${gen}: opencode-go
|
|
69
|
+
return `# Refreshed ${when} from model-scores.json${gen}: opencode-go first (weight 4), then the best others (weight 2).`
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
function withRefreshComment(headerLines, comment) {
|
|
@@ -88,7 +84,7 @@ function formatGeneratedLine(model, weight, bucket) {
|
|
|
88
84
|
* Pure builder. Throws on every fail-closed condition; never touches the filesystem.
|
|
89
85
|
*
|
|
90
86
|
* @param {{ scores: any, currentPoolText: string, now?: Date|string }} input
|
|
91
|
-
* @returns {{ text: string, slots: string[], warnings: string[], chosen: { opencodeGo: string[],
|
|
87
|
+
* @returns {{ text: string, slots: string[], warnings: string[], chosen: { opencodeGo: string[], others: string[] } }}
|
|
92
88
|
*/
|
|
93
89
|
export function buildPool(input) {
|
|
94
90
|
const warnings = []
|
|
@@ -97,33 +93,34 @@ export function buildPool(input) {
|
|
|
97
93
|
throw new Error("score file is missing or invalid (expected { providers: [...] })")
|
|
98
94
|
}
|
|
99
95
|
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
96
|
+
const minScore = Number(scores.minScore) || MIN_SCORE
|
|
97
|
+
const minContext = Number(scores.minContext) || MIN_CONTEXT
|
|
98
|
+
const rankable = (row) =>
|
|
99
|
+
row?.ok === true &&
|
|
100
|
+
numericScore(row) &&
|
|
101
|
+
row.score >= minScore &&
|
|
102
|
+
(row.context == null || row.context >= minContext)
|
|
103
|
+
const byScore = (a, b) => b.score - a.score || String(a.model).localeCompare(String(b.model))
|
|
104
|
+
|
|
105
|
+
const rows = scores.providers.flatMap((entry) => (Array.isArray(entry?.rows) ? entry.rows : []))
|
|
106
|
+
const goRows = rows.filter((row) => providerOf(row.model) === "opencode-go" && rankable(row)).sort(byScore)
|
|
107
|
+
const others = rows.filter((row) => providerOf(row.model) !== "opencode-go" && rankable(row)).sort(byScore)
|
|
108
|
+
const otherRows = others.slice(0, Math.max(0, POOL_SIZE - goRows.length))
|
|
109
|
+
|
|
110
|
+
if (goRows.length + otherRows.length === 0) {
|
|
111
|
+
throw new Error("no scorable model at or above the minimum score is available; refusing to write pool.txt")
|
|
109
112
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const opRows = okFreeRows(opEntry).sort((a, b) => b.score - a.score)
|
|
113
|
-
if (opRows.length === 0) {
|
|
114
|
-
throw new Error("no free opencode model with ok:true and a numeric score is available; refusing to write pool.txt")
|
|
113
|
+
if (goRows.length + otherRows.length < POOL_SIZE) {
|
|
114
|
+
warnings.push(`only ${goRows.length + otherRows.length} scorable model(s) available, wanted ${POOL_SIZE}`)
|
|
115
115
|
}
|
|
116
|
-
const bestOp = opRows[0].model
|
|
117
116
|
|
|
118
|
-
const { header
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const block3 = [formatGeneratedLine(bestOp, 2, "opencode")]
|
|
126
|
-
const outSlots = [...block1, ...preserved, ...block3]
|
|
117
|
+
const { header } = splitHeaderAndSlots(input?.currentPoolText ?? "")
|
|
118
|
+
const outSlots = [
|
|
119
|
+
...goRows.map((row) => formatGeneratedLine(row.model, 4)),
|
|
120
|
+
...otherRows.map((row) =>
|
|
121
|
+
formatGeneratedLine(row.model, 2, providerOf(row.model) === "opencode" ? "opencode" : undefined),
|
|
122
|
+
),
|
|
123
|
+
]
|
|
127
124
|
|
|
128
125
|
const comment = refreshComment(input?.now ?? new Date(), scores.generatedAt)
|
|
129
126
|
const outHeader = withRefreshComment(header, comment)
|
|
@@ -135,8 +132,7 @@ export function buildPool(input) {
|
|
|
135
132
|
warnings,
|
|
136
133
|
chosen: {
|
|
137
134
|
opencodeGo: goRows.map((r) => r.model),
|
|
138
|
-
|
|
139
|
-
preserved: preserved.map((l) => String(l).split("#")[0].trim().split(/\s+/)[0]),
|
|
135
|
+
others: otherRows.map((r) => r.model),
|
|
140
136
|
},
|
|
141
137
|
}
|
|
142
138
|
}
|
|
@@ -163,6 +159,12 @@ function atomicWriteSimple(path, text) {
|
|
|
163
159
|
renameSync(tmp, path)
|
|
164
160
|
}
|
|
165
161
|
|
|
162
|
+
function writeFileAtomic(path, text) {
|
|
163
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`
|
|
164
|
+
writeFileSync(tmp, text, "utf8")
|
|
165
|
+
renameSync(tmp, path)
|
|
166
|
+
}
|
|
167
|
+
|
|
166
168
|
function runScorer(max) {
|
|
167
169
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
168
170
|
const scorer = resolve(here, "score-models.mjs")
|
|
@@ -192,8 +194,7 @@ Nice=10
|
|
|
192
194
|
Description=Daily LaCode model pool refresh
|
|
193
195
|
|
|
194
196
|
[Timer]
|
|
195
|
-
OnCalendar
|
|
196
|
-
RandomizedDelaySec=2h
|
|
197
|
+
OnCalendar=*-*-* 01:00:00
|
|
197
198
|
Persistent=true
|
|
198
199
|
Unit=${REFRESH_SERVICE_NAME}
|
|
199
200
|
|
|
@@ -203,18 +204,23 @@ WantedBy=timers.target
|
|
|
203
204
|
return { service, timer, servicePath, timerPath }
|
|
204
205
|
}
|
|
205
206
|
|
|
206
|
-
function installTimerUnits() {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
home: HOME,
|
|
211
|
-
})
|
|
212
|
-
mkdirSync(dirname(units.servicePath), { recursive: true })
|
|
213
|
-
writeFileSync(units.servicePath, units.service, "utf8")
|
|
214
|
-
writeFileSync(units.timerPath, units.timer, "utf8")
|
|
207
|
+
function installTimerUnits(stable = false) {
|
|
208
|
+
const live = fileURLToPath(import.meta.url)
|
|
209
|
+
const scriptPath = stable ? STABLE_SCRIPT(HOME) : live
|
|
210
|
+
const units = renderTimerUnits({ nodePath: process.execPath, scriptPath, home: HOME })
|
|
215
211
|
try {
|
|
212
|
+
if (stable) {
|
|
213
|
+
mkdirSync(STABLE_LIB_DIR(HOME), { recursive: true })
|
|
214
|
+
copyFileSync(live, scriptPath)
|
|
215
|
+
}
|
|
216
|
+
mkdirSync(dirname(units.servicePath), { recursive: true })
|
|
217
|
+
writeFileAtomic(units.servicePath, units.service)
|
|
218
|
+
writeFileAtomic(units.timerPath, units.timer)
|
|
216
219
|
execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" })
|
|
217
220
|
execFileSync("systemctl", ["--user", "enable", "--now", REFRESH_TIMER_NAME], { stdio: "inherit" })
|
|
221
|
+
try {
|
|
222
|
+
execFileSync("loginctl", ["enable-linger", userInfo().username], { stdio: "ignore" })
|
|
223
|
+
} catch {}
|
|
218
224
|
} catch (error) {
|
|
219
225
|
console.error(`failed to install systemd user timer: ${String(error?.message ?? error)}`)
|
|
220
226
|
console.error(`unit files written:`)
|
|
@@ -226,6 +232,7 @@ function installTimerUnits() {
|
|
|
226
232
|
process.exitCode = 1
|
|
227
233
|
return false
|
|
228
234
|
}
|
|
235
|
+
if (stable) console.log(`copied ${live} to ${scriptPath}`)
|
|
229
236
|
console.log(`installed ${units.servicePath}`)
|
|
230
237
|
console.log(`installed ${units.timerPath}`)
|
|
231
238
|
console.log(`enabled ${REFRESH_TIMER_NAME}`)
|
|
@@ -237,6 +244,8 @@ function usage() {
|
|
|
237
244
|
|
|
238
245
|
--from-scores skip scoring, use the existing ~/.config/lacode/model-scores.json
|
|
239
246
|
--install-timer install and enable a user systemd oneshot service + daily timer
|
|
247
|
+
(add --stable to copy the script to ~/.local/lib/lacode and
|
|
248
|
+
point ExecStart there, so plugin-cache updates do not break it)
|
|
240
249
|
--print print the proposed pool.txt and make no changes (alias: --dry-run)
|
|
241
250
|
--dry-run same as --print
|
|
242
251
|
--max N models per provider passed to the scorer (default: 20)
|
|
@@ -248,10 +257,10 @@ function usage() {
|
|
|
248
257
|
async function main() {
|
|
249
258
|
const argv = process.argv.slice(2)
|
|
250
259
|
if (argv.includes("--help") || argv.includes("-h")) { usage(); return }
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
260
|
+
if (argv.includes("--install-timer")) {
|
|
261
|
+
installTimerUnits(argv.includes("--stable"))
|
|
262
|
+
return
|
|
263
|
+
}
|
|
255
264
|
const fromScores = argv.includes("--from-scores")
|
|
256
265
|
const printOnly = argv.includes("--print") || argv.includes("--dry-run")
|
|
257
266
|
const value = (flag, fallback) => {
|
|
@@ -294,10 +303,12 @@ async function main() {
|
|
|
294
303
|
|
|
295
304
|
const oldSlotCount = splitHeaderAndSlots(currentPoolText).slots.filter(isSlotLine).length
|
|
296
305
|
const newSlotCount = built.slots.filter(isSlotLine).length
|
|
306
|
+
const dropped = scores.providers.flatMap((entry) => (Array.isArray(entry?.excluded) ? entry.excluded : []))
|
|
297
307
|
const summary = [
|
|
298
308
|
`opencode-go [${built.chosen.opencodeGo.join(", ")}]`,
|
|
299
|
-
`
|
|
300
|
-
`
|
|
309
|
+
`others [${built.chosen.others.join(", ")}]`,
|
|
310
|
+
...(dropped.length ? [`dropped [${dropped.map((x) => x.model).join(", ")}]`] : []),
|
|
311
|
+
...(built.warnings.length ? [`warnings [${built.warnings.join("; ")}]`] : []),
|
|
301
312
|
].join(" | ")
|
|
302
313
|
|
|
303
314
|
if (printOnly) {
|