@litfamily/litopencode 1.0.25 → 1.0.27
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 +14 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/dist/activation-routing.js +11 -1
- package/dist/activation.d.ts +0 -10
- package/dist/activation.js +0 -54
- package/dist/agents/defaults.js +2 -1
- package/dist/cli/auto-update.d.ts +27 -1
- package/dist/cli/auto-update.js +112 -13
- package/dist/cli/doctor.d.ts +9 -0
- package/dist/cli/doctor.js +71 -10
- package/dist/cli/managed-skill-assets.js +2 -2
- package/dist/deliverable-hedge-guard.js +176 -10
- package/dist/server.js +1 -17
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/privacy.md +1 -12
- package/docs/reference-Ko-KR.md +6 -44
- package/docs/reference.md +6 -48
- package/package.json +1 -1
- package/skills/lit-docx/SKILL.md +1 -1
- package/skills/lit-humanizer/SKILL.md +1 -1
- package/skills/lit-humanizer/references/taxonomy.md +2 -2
- package/skills/lit-humanizer/rules.json +10 -7
- package/skills/lit-humanizer/scripts/core.mjs +13 -2
- package/skills/lit-pptx/SKILL.md +1 -1
- package/skills/managed-skill-manifest.json +3 -3
- package/tools/check-pack-payload.mjs +2 -2
- package/dist/jev-skill-hint.d.ts +0 -50
- package/dist/jev-skill-hint.js +0 -318
package/dist/cli/doctor.js
CHANGED
|
@@ -16,8 +16,73 @@ import { inspectLspCapability } from "./lsp-capability.js";
|
|
|
16
16
|
import { probeMotionRuntime } from "./motion-runtime.js";
|
|
17
17
|
import { isLitOpenCodeEntry } from "./plugin-mutation.js";
|
|
18
18
|
import { inspectTuiPlugin } from "./tui-config.js";
|
|
19
|
-
import {
|
|
20
|
-
|
|
19
|
+
import { autoUpdateCheckPath, autoUpdateInstallLockPath, autoUpdateJournalPath, autoUpdateReceiptPath, isAutoUpdateDisabled, readAutoUpdateCheck, readAutoUpdateReceipt } from "./auto-update.js";
|
|
20
|
+
const updateNow = "npx @litfamily/litopencode@latest install";
|
|
21
|
+
/** Each row says what a cause means and the one thing to do about it. */
|
|
22
|
+
const autoUpdateRemedies = [
|
|
23
|
+
{ cause: "next-start", meaning: "A newer version is installed, but the OpenCode that installed it still runs the old one.", remedy: "Quit and start OpenCode again." },
|
|
24
|
+
{ cause: "offline", meaning: "OpenCode could not reach the npm registry at its last start, so nothing was checked.", remedy: "Check the network or proxy. The next OpenCode start checks again." },
|
|
25
|
+
{ cause: "backoff", meaning: "The last update to this version failed and was undone, so OpenCode waits a day before trying it again.", remedy: `Run \`${updateNow}\` to update now, or wait a day or for a newer release.` },
|
|
26
|
+
{ cause: "timeout", meaning: "A step of the last update ran past its 30 second limit and was undone.", remedy: `Usually a slow network. Run \`${updateNow}\` once while online.` },
|
|
27
|
+
{ cause: "missing-command", meaning: "npm or node was not on the PATH OpenCode started with, so the update could not run.", remedy: `Install Node.js where OpenCode can find it, or update by hand with \`${updateNow}\`.` },
|
|
28
|
+
{ cause: "install-failed", meaning: "The last update failed and the old setup was put back.", remedy: `Run \`${updateNow}\` and read what it prints.` },
|
|
29
|
+
{ cause: "stopped", meaning: "The last update could not prove the old setup came back, so the plugin refuses to load.", remedy: `Run \`${updateNow}\`. The kept backup folder holds the old files.` },
|
|
30
|
+
{ cause: "lock-busy", meaning: "Another OpenCode start held the update lock.", remedy: "Nothing to do; the next start tries again. If it repeats with no OpenCode open, delete the lock file." },
|
|
31
|
+
{ cause: "opted-out", meaning: "Automatic update is switched off by --no-auto-update or by CI, NO_UPDATE_NOTIFIER, LITOPENCODE_NO_AUTO_UPDATE or LITOPENCODE_NO_UPDATE_CHECK.", remedy: "Remove the switch from the environment OpenCode starts in." },
|
|
32
|
+
{ cause: "local-checkout", meaning: "The plugin runs from a git checkout, which is never updated automatically.", remedy: "Pull the checkout with git, or install the published package." }
|
|
33
|
+
];
|
|
34
|
+
const skipCauses = {
|
|
35
|
+
"registry-unavailable": "offline",
|
|
36
|
+
backoff: "backoff",
|
|
37
|
+
"install-lock-busy": "lock-busy",
|
|
38
|
+
disabled: "opted-out",
|
|
39
|
+
"local-checkout": "local-checkout"
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Doctor reads the receipt and the record each OpenCode start leaves behind. It
|
|
43
|
+
* cannot see inside a running OpenCode, so "running" means the last start seen.
|
|
44
|
+
*/
|
|
45
|
+
export async function inspectAutoUpdate(input) {
|
|
46
|
+
const enabled = !isAutoUpdateDisabled(input.env);
|
|
47
|
+
const receipt = await readAutoUpdateReceipt(input.homeDir);
|
|
48
|
+
const lastCheck = await readAutoUpdateCheck(input.homeDir);
|
|
49
|
+
const summary = receipt === undefined || receipt.status === "unreadable" ? undefined : receipt;
|
|
50
|
+
let cause = null;
|
|
51
|
+
let nextStart = null;
|
|
52
|
+
if (summary !== undefined && (summary.status === "failed" || (summary.status === "rolled-back" && !(summary.rollbackOk && summary.priorStateVerified && summary.stagedStateKnown)))) {
|
|
53
|
+
cause = "stopped";
|
|
54
|
+
}
|
|
55
|
+
else if (lastCheck?.reason !== undefined && skipCauses[lastCheck.reason] !== undefined) {
|
|
56
|
+
cause = skipCauses[lastCheck.reason];
|
|
57
|
+
}
|
|
58
|
+
else if (lastCheck?.status === "rolled-back" && summary?.failure !== undefined) {
|
|
59
|
+
cause = summary.failure.missingCommand ? "missing-command" : summary.failure.timedOut ? "timeout" : "install-failed";
|
|
60
|
+
}
|
|
61
|
+
else if (summary?.status === "updated" && summary.targetVersion !== undefined) {
|
|
62
|
+
const running = lastCheck?.runningVersion ?? summary.currentVersion;
|
|
63
|
+
if (running !== undefined && running !== summary.targetVersion) {
|
|
64
|
+
cause = "next-start";
|
|
65
|
+
nextStart = `installed ${summary.targetVersion}, running ${running}: takes effect the next time OpenCode starts`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (cause === null && !enabled)
|
|
69
|
+
cause = "opted-out";
|
|
70
|
+
return {
|
|
71
|
+
enabled,
|
|
72
|
+
state: cause === null ? (lastCheck === undefined ? "unknown" : "current") : cause === "next-start" ? "next-start" : "attention",
|
|
73
|
+
cause,
|
|
74
|
+
nextStart,
|
|
75
|
+
pinnedVersion: input.pinnedVersion,
|
|
76
|
+
lastCheck: lastCheck ?? null,
|
|
77
|
+
proof: "Doctor reads files only. It cannot see which version a running OpenCode has loaded; restarting OpenCode is the sure way.",
|
|
78
|
+
remedies: autoUpdateRemedies,
|
|
79
|
+
installLock: autoUpdateInstallLockPath(input.homeDir),
|
|
80
|
+
journal: autoUpdateJournalPath(input.homeDir),
|
|
81
|
+
receipt: autoUpdateReceiptPath(input.homeDir),
|
|
82
|
+
check: autoUpdateCheckPath(input.homeDir),
|
|
83
|
+
lastReceipt: receipt
|
|
84
|
+
};
|
|
85
|
+
}
|
|
21
86
|
function pluginSpec(metadata) {
|
|
22
87
|
return metadata.name + "@" + metadata.version;
|
|
23
88
|
}
|
|
@@ -164,7 +229,6 @@ export async function doctor(root) {
|
|
|
164
229
|
warnings: autoHandoffWarnings
|
|
165
230
|
},
|
|
166
231
|
motion,
|
|
167
|
-
jevSkillHint: jevSkillHintDoctorLine(process.env),
|
|
168
232
|
config: {
|
|
169
233
|
source: loaded.source,
|
|
170
234
|
sources: loaded.sources,
|
|
@@ -204,13 +268,10 @@ export async function doctor(root) {
|
|
|
204
268
|
derivedTruthFiles: false
|
|
205
269
|
}
|
|
206
270
|
},
|
|
207
|
-
autoUpdate: {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
receipt: autoUpdateReceiptPath(),
|
|
212
|
-
lastReceipt: await readAutoUpdateReceipt()
|
|
213
|
-
},
|
|
271
|
+
autoUpdate: await inspectAutoUpdate({
|
|
272
|
+
env: process.env,
|
|
273
|
+
pinnedVersion: installPlugin.configured?.startsWith(`${metadata.name}@`) === true ? installPlugin.configured.slice(metadata.name.length + 1) : null
|
|
274
|
+
}),
|
|
214
275
|
install: {
|
|
215
276
|
ok: installPlugin.ok && commandAliases.ok && nativeSkills.ok,
|
|
216
277
|
plugin: installPlugin,
|
|
@@ -78,12 +78,12 @@ export function isRecursivelyManagedSkill(id) {
|
|
|
78
78
|
// surfaces the skill at the moment it is needed. The record is total by type: adding a runtime skill
|
|
79
79
|
// id without writing its trigger is a compile error, not a silent fallback.
|
|
80
80
|
const skillDiscoveryDescriptions = Object.freeze({
|
|
81
|
-
"workflow-loop": "Trigger when the user wants a multi-step LitOpenCode work loop started, resumed, or kept running — bare `lit`, `/lit`, `/litwork`, \"keep going\", \"continue the loop\" — or when a request needs goal binding, ordered execution, and ledger-backed progress across turns instead of one answer.",
|
|
81
|
+
"workflow-loop": "Trigger when the user wants a multi-step LitOpenCode work loop started, resumed, or kept running — bare `lit`, `/lit`, `/litwork`, \"keep going\", \"continue the loop\" — or when a request needs goal binding, ordered execution, and ledger-backed progress across turns instead of one answer. Also trigger when one request hands over a whole feature, fix, or ticket to carry through to done — build it, add tests, verify against each acceptance condition and on the real user-facing path, clean up temporary artifacts, then report what changed (\"start to finish\", \"end to end\", \"see it through\", \"처음부터 끝까지\", \"끝까지 해줘\"). Do not trigger for a question, an explanation, or a one-line snippet.",
|
|
82
82
|
"durable-litgoal": "Trigger when work needs an objective that outlives the current turn: binding a goal with checkable criteria, reading or appending the `.litopencode/litgoal` ledger, resuming a schema-3 bounded-authority work record, or answering \"what was I working on\" from durable state rather than chat memory.",
|
|
83
83
|
"agent-roster": "Trigger when deciding which OpenCode agent should do the work, and again the moment work is about to be split across several lanes at once — before delegating through the task tool, when a request fans out into planning, implementation, review, research, or verification lanes, or when the user asks which agents exist and what each is permitted to touch. Delegation here is depth-one, so lanes need non-overlapping slices, explicit per-lane state, and a completion claim that aggregates every lane's evidence rather than summarising it.",
|
|
84
84
|
"lit-plan": "Trigger when the user asks for a plan, checklist, approach, or design before implementation, or when a request is large or risky enough that execution must not start until scope, non-goals, ordered action/output/verification items, and a falsifiable DoneClaim are agreed. Do not trigger to perform the work itself.",
|
|
85
85
|
"start-work": "Trigger when an approved plan or an explicit go-ahead already exists and implementation should begin or resume — \"start work\", \"execute the plan\", \"pick up where we left off\" — so the loop runs in verifiable slices with delegation lanes, evidence, and an independent verifier before any completion claim.",
|
|
86
|
-
"review-work": "Trigger when something must be judged rather than produced: a draft plan needing a PASS / ITERATE / NEEDS-CONTEXT achievability verdict
|
|
86
|
+
"review-work": "Trigger when something must be judged rather than produced: a patch, diff, PR, branch, or set of open changes to inspect for real defects, risks, or merge readiness (\"review this\", \"is this safe to merge\", \"point out anything risky\", \"리뷰해줘\", \"검토해줘\", \"합쳐도 될지 봐줘\"), where findings need file-and-line evidence, a reproduction path, and severity; a draft plan needing a PASS / ITERATE / NEEDS-CONTEXT achievability verdict; or finished work needing the five-lane scope-diff, tests-evidence, package-payload, security-provenance, and real-surface-docs check before it can be called done. When fixes are also asked for, the review comes first. Do not trigger for a general question about git, merging, or how code review works.",
|
|
87
87
|
litresearch: "Trigger when a question needs sourced investigation rather than recall — comparing options, confirming whether a library, API, or behavior actually works as claimed, gathering public evidence across several sources — so verified facts, hypotheses, contradictions, and residual uncertainty stay separated before a verdict.",
|
|
88
88
|
"reference-benchmark-claims": "Trigger before writing any comparative or superiority claim about this package — \"faster than\", \"better than\", \"always wins\", \"무조건\" — so the wording is classified against the measured benchmark suite and universal all-task claims are blocked rather than shipped.",
|
|
89
89
|
"native-goal-verdict": "Trigger when someone assumes the OpenCode host has a built-in goal primitive — asking for `/goal`, native goal tracking, or host-side objective state — so the answer reflects the verified host capability and routes to LitOpenCode durable goal state instead of promising an integration that does not exist.",
|
|
@@ -57,6 +57,116 @@ function toolText(input) {
|
|
|
57
57
|
return { file: candidate, unavailable: "new text exceeds scan limit" };
|
|
58
58
|
return { file: candidate, value: values[0] };
|
|
59
59
|
}
|
|
60
|
+
const patchTools = new Set(["apply_patch", "patch"]);
|
|
61
|
+
function patchTextOf(input) {
|
|
62
|
+
if (!patchTools.has(input.tool) || !isRecord(input.args))
|
|
63
|
+
return undefined;
|
|
64
|
+
const value = input.args.patchText;
|
|
65
|
+
return typeof value === "string" ? value : undefined;
|
|
66
|
+
}
|
|
67
|
+
// Reads the apply_patch envelope: `*** Add File:` bodies are `+` lines, `*** Update File:` carries an optional
|
|
68
|
+
// `*** Move to:` and `@@` chunks of ` `, `-` and `+` lines. Delete hunks remove text and are not scanned.
|
|
69
|
+
function parsePatch(patchText) {
|
|
70
|
+
const lines = patchText.split(/\r?\n/u);
|
|
71
|
+
const begin = lines.findIndex((line) => line.trim() === "*** Begin Patch");
|
|
72
|
+
const end = lines.findIndex((line) => line.trim() === "*** End Patch");
|
|
73
|
+
if (begin < 0 || end < 0 || begin >= end)
|
|
74
|
+
throw new Error("patch text has no Begin/End markers");
|
|
75
|
+
const isHeader = (line) => /^\*\*\* (?:Add|Update|Delete) File:/u.test(line);
|
|
76
|
+
const files = [];
|
|
77
|
+
let index = begin + 1;
|
|
78
|
+
while (index < end) {
|
|
79
|
+
const line = lines[index];
|
|
80
|
+
if (line.startsWith("*** Add File:")) {
|
|
81
|
+
const body = [];
|
|
82
|
+
index += 1;
|
|
83
|
+
while (index < end && !isHeader(lines[index])) {
|
|
84
|
+
if (lines[index].startsWith("+"))
|
|
85
|
+
body.push(lines[index].slice(1));
|
|
86
|
+
index += 1;
|
|
87
|
+
}
|
|
88
|
+
files.push({ kind: "add", file: line.slice(13).trim(), contents: body.join("\n"), chunks: [], added: body });
|
|
89
|
+
}
|
|
90
|
+
else if (line.startsWith("*** Update File:")) {
|
|
91
|
+
const file = line.slice(16).trim();
|
|
92
|
+
let moveTo;
|
|
93
|
+
index += 1;
|
|
94
|
+
if (index < end && lines[index].startsWith("*** Move to:")) {
|
|
95
|
+
moveTo = lines[index].slice(12).trim();
|
|
96
|
+
index += 1;
|
|
97
|
+
}
|
|
98
|
+
const chunks = [];
|
|
99
|
+
const added = [];
|
|
100
|
+
let before = [];
|
|
101
|
+
let after = [];
|
|
102
|
+
let open = false;
|
|
103
|
+
const close = () => {
|
|
104
|
+
if (open)
|
|
105
|
+
chunks.push({ before, after });
|
|
106
|
+
before = [];
|
|
107
|
+
after = [];
|
|
108
|
+
open = false;
|
|
109
|
+
};
|
|
110
|
+
while (index < end && !isHeader(lines[index])) {
|
|
111
|
+
const row = lines[index];
|
|
112
|
+
if (row.startsWith("@@"))
|
|
113
|
+
close();
|
|
114
|
+
else if (row.startsWith("*** End of File")) { /* marker only */ }
|
|
115
|
+
else if (row.startsWith("+")) {
|
|
116
|
+
open = true;
|
|
117
|
+
after.push(row.slice(1));
|
|
118
|
+
added.push(row.slice(1));
|
|
119
|
+
}
|
|
120
|
+
else if (row.startsWith("-")) {
|
|
121
|
+
open = true;
|
|
122
|
+
before.push(row.slice(1));
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
open = true;
|
|
126
|
+
const text = row.startsWith(" ") ? row.slice(1) : row;
|
|
127
|
+
before.push(text);
|
|
128
|
+
after.push(text);
|
|
129
|
+
}
|
|
130
|
+
index += 1;
|
|
131
|
+
}
|
|
132
|
+
close();
|
|
133
|
+
files.push({ kind: "update", file, moveTo: moveTo === "" ? undefined : moveTo, contents: "", chunks, added });
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
index += 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return files;
|
|
140
|
+
}
|
|
141
|
+
function applyChunks(oldText, chunks) {
|
|
142
|
+
const lines = oldText.split("\n");
|
|
143
|
+
const trailing = lines.length > 1 && lines[lines.length - 1] === "";
|
|
144
|
+
if (trailing)
|
|
145
|
+
lines.pop();
|
|
146
|
+
let cursor = 0;
|
|
147
|
+
for (const chunk of chunks) {
|
|
148
|
+
if (chunk.before.length === 0) {
|
|
149
|
+
lines.push(...chunk.after);
|
|
150
|
+
cursor = lines.length;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const matches = (at, normalize) => chunk.before.every((text, offset) => at + offset < lines.length && normalize(lines[at + offset]) === normalize(text));
|
|
154
|
+
let found = -1;
|
|
155
|
+
for (const normalize of [(text) => text, (text) => text.trimEnd(), (text) => text.trim()]) {
|
|
156
|
+
for (let at = cursor; at + chunk.before.length <= lines.length && found < 0; at += 1) {
|
|
157
|
+
if (matches(at, normalize))
|
|
158
|
+
found = at;
|
|
159
|
+
}
|
|
160
|
+
if (found >= 0)
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
if (found < 0)
|
|
164
|
+
throw new Error("patch chunk was not found in the current file");
|
|
165
|
+
lines.splice(found, chunk.before.length, ...chunk.after);
|
|
166
|
+
cursor = found + chunk.after.length;
|
|
167
|
+
}
|
|
168
|
+
return lines.join("\n") + (trailing ? "\n" : "");
|
|
169
|
+
}
|
|
60
170
|
function runJsonProcess(command, args, input, timeoutMs) {
|
|
61
171
|
return new Promise((resolve, reject) => {
|
|
62
172
|
const child = spawn(command, [...args], { shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -186,8 +296,71 @@ export function createDeliverableHedgeGuard(options) {
|
|
|
186
296
|
const timeoutMs = options.timeoutMs ?? defaultTimeoutMs;
|
|
187
297
|
const scan = options.scan ?? ((request) => defaultScan(request, timeoutMs));
|
|
188
298
|
const pending = new Map();
|
|
299
|
+
async function scanTarget(file, oldText, newText) {
|
|
300
|
+
if (Buffer.byteLength(newText, "utf8") > maxTextBytes)
|
|
301
|
+
throw new Error("new text exceeds scan limit");
|
|
302
|
+
const findings = await scan({ file, oldText, newText });
|
|
303
|
+
const block = findings.filter((finding) => finding.severity === "block");
|
|
304
|
+
if (block.length > 0) {
|
|
305
|
+
const first = block[0];
|
|
306
|
+
throw new Error(`LIT_HUMANIZER_BLOCKED rule=${first.rule} line=${first.line} path=${displayPath(file)}. Revise the new text before saving.`);
|
|
307
|
+
}
|
|
308
|
+
return findings.length;
|
|
309
|
+
}
|
|
310
|
+
// An apply_patch call can add several files; each deliverable-path target gets the same per-file scan as write
|
|
311
|
+
// and edit. A chunk that cannot be located in the current file is scanned as appended text instead, so an added
|
|
312
|
+
// pledge is still refused (the host rejects a patch whose context does not match, so nothing is saved either way).
|
|
313
|
+
async function guardPatch(callID, patchText) {
|
|
314
|
+
try {
|
|
315
|
+
if (Buffer.byteLength(patchText, "utf8") > maxTextBytes)
|
|
316
|
+
throw new Error("patch text exceeds scan limit");
|
|
317
|
+
let warned = 0;
|
|
318
|
+
let warnedFile = "";
|
|
319
|
+
for (const entry of parsePatch(patchText)) {
|
|
320
|
+
const target = entry.moveTo ?? entry.file;
|
|
321
|
+
if (isInternalPath(target) || !readerFacingExtensions.has(path.extname(target).toLowerCase()))
|
|
322
|
+
continue;
|
|
323
|
+
const oldText = await existingText(projectRoot, entry.kind === "add" ? target : entry.file);
|
|
324
|
+
let newText;
|
|
325
|
+
if (entry.kind === "add")
|
|
326
|
+
newText = entry.contents;
|
|
327
|
+
else {
|
|
328
|
+
try {
|
|
329
|
+
newText = applyChunks(oldText, entry.chunks);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
newText = `${oldText}\n${entry.added.join("\n")}`;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const count = await scanTarget(target, oldText, newText);
|
|
336
|
+
if (count > 0 && warned === 0)
|
|
337
|
+
warnedFile = target;
|
|
338
|
+
warned += count;
|
|
339
|
+
}
|
|
340
|
+
if (warned > 0) {
|
|
341
|
+
remember(pending, callID, {
|
|
342
|
+
tier: "warn",
|
|
343
|
+
message: `LIT_HUMANIZER_WARN findings=${warned} path=${displayPath(warnedFile)}. Review the new wording; keep any accurate, useful warning.`
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
if (error instanceof Error && error.message.startsWith("LIT_HUMANIZER_BLOCKED"))
|
|
349
|
+
throw error;
|
|
350
|
+
remember(pending, callID, {
|
|
351
|
+
tier: "unavailable",
|
|
352
|
+
message: `LIT_HUMANIZER_GUARD_UNAVAILABLE: ${oneLine(error)}; write allowed without this check.`
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
189
356
|
return {
|
|
190
357
|
async before(input) {
|
|
358
|
+
const patchText = patchTextOf(input);
|
|
359
|
+
if (patchText !== undefined) {
|
|
360
|
+
pending.delete(input.callID);
|
|
361
|
+
await guardPatch(input.callID, patchText);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
191
364
|
const text = toolText(input);
|
|
192
365
|
if (text === undefined)
|
|
193
366
|
return;
|
|
@@ -204,18 +377,11 @@ export function createDeliverableHedgeGuard(options) {
|
|
|
204
377
|
const newText = input.tool === "write"
|
|
205
378
|
? text.value ?? ""
|
|
206
379
|
: applyEdit(oldText, input.args.oldString ?? input.args.old_string, text.value ?? "", input.args.replaceAll);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const findings = await scan({ file: text.file, oldText, newText });
|
|
210
|
-
const block = findings.filter((finding) => finding.severity === "block");
|
|
211
|
-
if (block.length > 0) {
|
|
212
|
-
const first = block[0];
|
|
213
|
-
throw new Error(`LIT_HUMANIZER_BLOCKED rule=${first.rule} line=${first.line} path=${displayPath(text.file)}. Revise the new text before saving.`);
|
|
214
|
-
}
|
|
215
|
-
if (findings.length > 0) {
|
|
380
|
+
const warned = await scanTarget(text.file, oldText, newText);
|
|
381
|
+
if (warned > 0) {
|
|
216
382
|
remember(pending, input.callID, {
|
|
217
383
|
tier: "warn",
|
|
218
|
-
message: `LIT_HUMANIZER_WARN findings=${
|
|
384
|
+
message: `LIT_HUMANIZER_WARN findings=${warned} path=${displayPath(text.file)}. Review the new wording; keep any accurate, useful warning.`
|
|
219
385
|
});
|
|
220
386
|
}
|
|
221
387
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { createChatMessageActivationHook, showActivationToast
|
|
2
|
+
import { createChatMessageActivationHook, showActivationToast } from "./activation.js";
|
|
4
3
|
import { registerLitOpenCodeAgents } from "./agents.js";
|
|
5
4
|
import { createAutoHandoff } from "./auto-handoff-hooks.js";
|
|
6
5
|
import { createBoundedAuthorityEventHook } from "./bounded-authority-hooks.js";
|
|
@@ -9,7 +8,6 @@ import { loadConfig } from "./config.js";
|
|
|
9
8
|
import { createDeliverableHedgeGuard } from "./deliverable-hedge-guard.js";
|
|
10
9
|
import { createToolExecuteAfterHook, createToolExecuteBeforeHook } from "./hooks.js";
|
|
11
10
|
import { createLogger } from "./logger.js";
|
|
12
|
-
import { createJevSkillHint } from "./jev-skill-hint.js";
|
|
13
11
|
import { createIgnitionState } from "./ignition.js";
|
|
14
12
|
import { createSessionLookup } from "./session-lineage.js";
|
|
15
13
|
import { applyCompactionRuleReset, applyStaticRuleInjection } from "./rules/hooks.js";
|
|
@@ -62,12 +60,7 @@ export function createLitOpenCodePlugin(autoUpdateRunner = runPluginAutoUpdate)
|
|
|
62
60
|
ignitionState.activate(sessionID, skillId, !appendedLit);
|
|
63
61
|
};
|
|
64
62
|
const boundedAuthorityEvent = createBoundedAuthorityEventHook(root, input?.client, loaded.config.boundedAuthority);
|
|
65
|
-
const skillHint = createJevSkillHint({
|
|
66
|
-
traceFile: path.join(loaded.paths.logsDir, "jev-skill-hint.jsonl"),
|
|
67
|
-
traceRoot: loaded.paths.projectRoot
|
|
68
|
-
});
|
|
69
63
|
// Keyed by session: the host resolves command parts into a new array before chat.message runs.
|
|
70
|
-
const pendingCommand = new Set();
|
|
71
64
|
const trustedCommandActivations = new Map();
|
|
72
65
|
const commandActivation = createCommandActivationHook(root, {
|
|
73
66
|
commandAliasRoot: defaultOpenCodeConfigRoot(),
|
|
@@ -83,8 +76,6 @@ export function createLitOpenCodePlugin(autoUpdateRunner = runPluginAutoUpdate)
|
|
|
83
76
|
event: async (eventInput) => {
|
|
84
77
|
if (eventInput.event.type === "session.deleted") {
|
|
85
78
|
ignitionState.clear(eventInput.event.properties.info.id);
|
|
86
|
-
skillHint.forget(eventInput.event.properties.info.id);
|
|
87
|
-
pendingCommand.delete(eventInput.event.properties.info.id);
|
|
88
79
|
trustedCommandActivations.delete(eventInput.event.properties.info.id);
|
|
89
80
|
}
|
|
90
81
|
await boundedAuthorityEvent(eventInput);
|
|
@@ -100,12 +91,6 @@ export function createLitOpenCodePlugin(autoUpdateRunner = runPluginAutoUpdate)
|
|
|
100
91
|
ignitionState.resetDiscipline(sessionID);
|
|
101
92
|
},
|
|
102
93
|
onSkillActivation: recordSkillActivation,
|
|
103
|
-
isCommandTurn: (sessionID) => pendingCommand.delete(sessionID),
|
|
104
|
-
skillHint: async (sessionID, promptText) => {
|
|
105
|
-
const hint = await skillHint.hintFor(sessionID, promptText);
|
|
106
|
-
showJevSkillHintToast(input?.client, hint, skillHint.claimAwareness(sessionID, promptText));
|
|
107
|
-
return hint;
|
|
108
|
-
},
|
|
109
94
|
trustedInjectedSkill: (sessionID) => {
|
|
110
95
|
const skillId = trustedCommandActivations.get(sessionID);
|
|
111
96
|
trustedCommandActivations.delete(sessionID);
|
|
@@ -116,7 +101,6 @@ export function createLitOpenCodePlugin(autoUpdateRunner = runPluginAutoUpdate)
|
|
|
116
101
|
}
|
|
117
102
|
}),
|
|
118
103
|
"command.execute.before": async (commandInput, commandOutput) => {
|
|
119
|
-
pendingCommand.add(commandInput.sessionID);
|
|
120
104
|
const priorLength = commandOutput.parts.length;
|
|
121
105
|
await commandActivation(commandInput, commandOutput);
|
|
122
106
|
const activationPart = commandOutput.parts.slice(priorLength).reverse().find((part) => trustedCommandSkillId(part) !== undefined);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.27"><title>release: 1.0.27</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.27</text></g></svg>
|
package/docs/privacy.md
CHANGED
|
@@ -14,7 +14,7 @@ providers, npm, and other tools have their own behavior and policies.
|
|
|
14
14
|
| Project `.litopencode/litgoal/lit-loop/` | Goals, criteria, checkpoints, session identifiers, evidence references, a brief, ledger events, and bounded lifecycle state used to resume work. Evidence files can contain commands, output, or project material. |
|
|
15
15
|
| Project `.litopencode/knowledge/` | Locally captured knowledge claims and review state. Legacy skill-learning files from earlier releases are left untouched and are no longer read. |
|
|
16
16
|
| Project `.litopencode/logs/litopencode.log` | The logger's JSON-lines destination for timestamps, levels, and messages when explicitly enabled. The current server creates the logger with logging disabled by default. |
|
|
17
|
-
| Project `.litopencode/logs/jev-skill-hint.jsonl` |
|
|
17
|
+
| Project `.litopencode/logs/jev-skill-hint.jsonl` | No longer written. Earlier releases could leave this debug trace of the removed Jev skill hint; LitOpenCode never reads it, and you can delete it. |
|
|
18
18
|
| OpenCode config root | Plugin registration, `litopencode.json` routes and choices, and installed command/skill files. The default is `~/.config/opencode`; `XDG_CONFIG_HOME` or a CLI `--root` changes the applicable root. |
|
|
19
19
|
| Home `~/.litopencode/` | Update cache, locks, transaction journal and receipt, and retained recovery backups after an unsuccessful automatic update. Backups can contain OpenCode configuration and installed command/skill content. |
|
|
20
20
|
|
|
@@ -51,17 +51,6 @@ path and query string and normal network metadata. Never put secrets in a URL:
|
|
|
51
51
|
redacting a returned result cannot retract a request already sent to its server.
|
|
52
52
|
Retrieved text and source addresses can become part of tool output and host context.
|
|
53
53
|
|
|
54
|
-
**Jev skill hint (off by default).** When `LITOPENCODE_JEV=1` and `TYPESAFE_API_KEY`
|
|
55
|
-
are both set, the `chat.message` hook sends each eligible user prompt to TypeSafe at
|
|
56
|
-
`api.typesafe.ai`, together with the LitOpenCode skill ids and summaries. The prompt is cut
|
|
57
|
-
to 2,000 characters and home paths, e-mail addresses and token-shaped strings are replaced
|
|
58
|
-
first. Anything in the prompt without a token shape is sent as written, for example hostnames,
|
|
59
|
-
customer names, or a password that is not written as `password=...`. Files, tool output and
|
|
60
|
-
earlier turns are not sent. Because `TYPESAFE_API_KEY` is exported in the shell that starts
|
|
61
|
-
OpenCode, the agent's own tools can read it; use a key made only for this feature, with a low
|
|
62
|
-
spending limit. TypeSafe's own retention and billing terms apply. Unset `LITOPENCODE_JEV` to
|
|
63
|
-
stop these requests.
|
|
64
|
-
|
|
65
54
|
**OpenCode and providers.** Agent prompts, user requests, selected files, and tool
|
|
66
55
|
results are processed through the OpenCode host and configured provider. LitOpenCode
|
|
67
56
|
does not establish their retention or training policy. Review the provider and
|
package/docs/reference-Ko-KR.md
CHANGED
|
@@ -439,53 +439,15 @@ OpenCode는 세션이 바쁜 동안 들어온 `summarize` 호출을 줄 세웠
|
|
|
439
439
|
update barrier가 동작합니다. `--no-auto-update` 또는 `LITOPENCODE_NO_AUTO_UPDATE=1`로
|
|
440
440
|
끌 수 있습니다. cache-only 알림은 `NO_UPDATE_NOTIFIER=1` 또는
|
|
441
441
|
`LITOPENCODE_NO_UPDATE_CHECK=1`로 끌 수 있습니다.
|
|
442
|
+
- 시작할 때 설치된 업데이트는 OpenCode를 다음에 시작할 때 적용됩니다. 업데이트를 실행한
|
|
443
|
+
프로세스는 이전 플러그인으로 계속 동작합니다. 실패한 업데이트가 깨끗하게 되돌려졌다면
|
|
444
|
+
시작 시에는 같은 버전을 하루 동안 다시 시도하지 않습니다(직접 실행한 `install`은 진행합니다).
|
|
445
|
+
시작할 때마다 `~/.litopencode/auto-update-check.json`에 시각, 실행 중인 버전, 결과를 남깁니다.
|
|
446
|
+
`doctor`는 이 기록과 receipt를 읽어 `autoUpdate.state`, `cause`, `nextStart`, 원인별 조치 표를
|
|
447
|
+
보여 줍니다. 실행 중인 OpenCode 내부는 볼 수 없으므로 새 버전을 확실히 쓰려면 다시 시작하세요.
|
|
442
448
|
- Public-source retrieval에는 SSRF 검사, redirect 검증, byte 제한, access verdict가
|
|
443
449
|
있습니다. 가져온 본문은 명령이 아니라 데이터로 처리합니다.
|
|
444
450
|
|
|
445
|
-
## Jev 스킬 힌트 (선택)
|
|
446
|
-
|
|
447
|
-
`chat.message` hook이 TypeSafe의 판단 모델 Jev에게 사용자 턴에 맞는 runtime 스킬을 물을 수
|
|
448
|
-
있습니다. `LITOPENCODE_JEV=1`과 `TYPESAFE_API_KEY`가 모두 프로세스 환경에 있어야 켜지고, 그렇지
|
|
449
|
-
않으면 네트워크 요청을 하지 않습니다. 키는 환경 변수에서만 읽고 `Authorization` header로만
|
|
450
|
-
보내며, 파일·로그·화면에 남기지 않습니다.
|
|
451
|
-
|
|
452
|
-
- **대상 턴.** 루트 세션만 해당합니다. 슬래시 명령(`command.execute.before`가 펼친 명령 포함),
|
|
453
|
-
lit 경로가 이미 처리한 턴, 첨부 파일 본문 같은 synthetic part, 공백을 뺀 4자 미만의
|
|
454
|
-
프롬프트는 건너뜁니다. OpenCode는 `command.execute.before`가 받은 parts 배열이 아니라 새 배열을
|
|
455
|
-
`chat.message`에 넘기므로, 명령은 세션에 표시를 남기고 그 세션의 다음 `chat.message`가 이
|
|
456
|
-
표시를 소비합니다.
|
|
457
|
-
- **요청.** 조건에 맞는 턴마다 `POST https://api.typesafe.ai/v1/systemone`을 한 번 보내고
|
|
458
|
-
다시 시도하지 않으며 리다이렉트도 따르지 않습니다(`redirect: "error"`). 본문에는 `model`,
|
|
459
|
-
`state`, `questions`만 들어갑니다. `state`는 프롬프트 앞 8,000자에서 홈 경로를 `~`로, 이메일을
|
|
460
|
-
`[email]`로, 토큰 형태의 문자열(`password=`/`token=`/`secret=` 형식의 대입과 키 자체 포함)을
|
|
461
|
-
`[secret]`으로 바꾼 뒤 2,000자에서 자릅니다. 잘린 자리에 토큰 문자 8개 이상이 남으면 그것도
|
|
462
|
-
`[secret]`이 됩니다. 호스트 이름이나 고객 이름처럼 토큰 형태가 아닌 내용은 그대로 전송됩니다.
|
|
463
|
-
후보 목록은 runtime 스킬과 `none`입니다.
|
|
464
|
-
- **붙는 문장.** 힌트와 실패 안내는 `synthetic` text part로 붙습니다. OpenCode는 이 part를
|
|
465
|
-
모델에 그대로 보내지만, 사용자가 쓴 글로 표시하거나 복사하지 않습니다.
|
|
466
|
-
- **응답.** HTTP 200, 올바른 JSON, 후보 id와 정확히 같은 `answers.which.choice`, 기준 이상의
|
|
467
|
-
`answers.which.confidence`를 모두 만족할 때만 힌트를 붙입니다. 붙는 문장은 LitOpenCode가 정한
|
|
468
|
-
고정 문장이며 응답 본문을 옮기지 않습니다.
|
|
469
|
-
- **실패 시.** 시간 초과, 네트워크 오류, 200이 아닌 상태, 잘못된 응답, 호출 한도 도달 때는 힌트
|
|
470
|
-
없이 평소대로 진행합니다. 세션의 첫 실패에만 짧은 안내 한 줄이 붙습니다.
|
|
471
|
-
- **상태.** `litopencode doctor`의 `jevSkillHint` 값이 `Jev skill hint: off`, `on`,
|
|
472
|
-
`flag on but TYPESAFE_API_KEY missing` 가운데 하나로 나옵니다.
|
|
473
|
-
- **화면 표시.** 세션에서 조건에 맞는 첫 턴에는 `warning` 알림에 `✦ Jev skill hint is ON`이
|
|
474
|
-
뜹니다. 그 턴에 힌트도 붙으면 이 안내가 알림의 제목이 되고 `Jev → <skill> (<seconds>s)`가
|
|
475
|
-
본문이 됩니다. TUI가 알림을 한 번에 하나만 유지하기 때문입니다. 이후 힌트가 붙는 턴에는
|
|
476
|
-
`Jev → <skill> (<seconds>s)`만 담은 `info` 알림이 뜹니다. 실패 안내, `none` 응답, 건너뛴 턴에는
|
|
477
|
-
아무것도 뜨지 않습니다. 두 알림 모두 OpenCode의 기본 표시 시간을 씁니다.
|
|
478
|
-
|
|
479
|
-
| 변수 | 기본값 | 효과 |
|
|
480
|
-
| --- | --- | --- |
|
|
481
|
-
| `LITOPENCODE_JEV` | 없음 | `1`이면 키가 있을 때 힌트를 켭니다. |
|
|
482
|
-
| `TYPESAFE_API_KEY` | 없음 | 본인의 TypeSafe 키입니다. 요금은 입력 토큰 100만 개당 약 0.04달러입니다. |
|
|
483
|
-
| `LITOPENCODE_JEV_MODEL` | `jev-1.13.0` | 요청에 담는 모델 이름입니다. |
|
|
484
|
-
| `LITOPENCODE_JEV_TIMEOUT_MS` | `1500` | 제한 시간(ms)이며 최대 `3000`입니다. |
|
|
485
|
-
| `LITOPENCODE_JEV_MAX_CALLS` | `200` | 세션당 요청 수 한도입니다. |
|
|
486
|
-
| `LITOPENCODE_JEV_MIN_CONFIDENCE` | `0.35` | 힌트를 붙이는 최소 확신도입니다. |
|
|
487
|
-
| `LITOPENCODE_JEV_TRACE` | 없음 | `1`이면 요청마다 `.litopencode/logs/jev-skill-hint.jsonl`에 시각, 프롬프트 SHA-256, 고른 id, 확신도, 지연 시간, HTTP 상태, 실패 사유를 한 줄씩 남깁니다. 프롬프트 본문, 키, 응답 본문은 남기지 않습니다. |
|
|
488
|
-
|
|
489
451
|
## 패키지 경로
|
|
490
452
|
|
|
491
453
|
- 패키지와 plugin id: `litopencode`
|
package/docs/reference.md
CHANGED
|
@@ -562,6 +562,12 @@ The route writes `.litopencode/auto-handoff.json` (the switch and the percent).
|
|
|
562
562
|
install. Disable it with `--no-auto-update` or
|
|
563
563
|
`LITOPENCODE_NO_AUTO_UPDATE=1`. The cache-only notice can also be disabled with
|
|
564
564
|
`NO_UPDATE_NOTIFIER=1` or `LITOPENCODE_NO_UPDATE_CHECK=1`.
|
|
565
|
+
- An update made at startup takes effect the next time OpenCode starts; the process that ran it
|
|
566
|
+
keeps the old plugin. After a failed update that was undone cleanly, startup leaves that same
|
|
567
|
+
version alone for a day (an explicit `install` still runs it). Each start writes
|
|
568
|
+
`~/.litopencode/auto-update-check.json` (time, running version, outcome). `doctor` reads it with
|
|
569
|
+
the receipt and reports `autoUpdate.state`, `cause`, `nextStart` and a cause-to-remedy table. It
|
|
570
|
+
cannot see inside a running OpenCode, so a restart is the sure way to load a new version.
|
|
565
571
|
- Public-source retrieval uses SSRF checks, redirect validation, byte limits, and access
|
|
566
572
|
verdicts. Fetched text is treated as data, not as instructions.
|
|
567
573
|
|
|
@@ -572,54 +578,6 @@ The route writes `.litopencode/auto-handoff.json` (the switch and the percent).
|
|
|
572
578
|
Metadata-only overflow emits a bounded digest/omission rule fragment.
|
|
573
579
|
- session-scoped rule delivery deduplicates matching content and stays bounded after compaction.
|
|
574
580
|
|
|
575
|
-
## Jev skill hint (optional)
|
|
576
|
-
|
|
577
|
-
The `chat.message` hook can ask Jev, TypeSafe's hosted typed-decision model, which runtime skill
|
|
578
|
-
fits a user turn. It is off unless `LITOPENCODE_JEV=1` and `TYPESAFE_API_KEY` are both set in the
|
|
579
|
-
process environment; otherwise it makes no network call. The key is read from the environment
|
|
580
|
-
only. It is sent only in the `Authorization` header and is never written, logged or shown.
|
|
581
|
-
|
|
582
|
-
- **Eligible turns.** Root sessions only. Slash commands (including every command expanded by
|
|
583
|
-
`command.execute.before`), turns a deterministic lit route already claimed, synthetic parts
|
|
584
|
-
such as attached file text, and prompts under four non-space characters are skipped. OpenCode
|
|
585
|
-
hands `chat.message` a new parts array rather than the one `command.execute.before` saw, so a
|
|
586
|
-
command marks its session instead, and that session's next `chat.message` consumes the mark.
|
|
587
|
-
- **Request.** One `POST https://api.typesafe.ai/v1/systemone` per eligible turn, no retry and no
|
|
588
|
-
redirects (`redirect: "error"`). The body holds only `model`, `state` and `questions`. `state`
|
|
589
|
-
is built from the first 8,000 characters of the user's prompt text: home paths become `~`,
|
|
590
|
-
e-mail addresses `[email]`, and token-shaped strings (provider key prefixes, JWTs, PEM blocks,
|
|
591
|
-
`password=`/`token=`/`secret=`-style assignments, runs of 32 or more hex or base64
|
|
592
|
-
characters, and the key itself) `[secret]`. Only then is it cut to 2,000 characters, and a
|
|
593
|
-
run of eight or more token characters left at the cut also becomes `[secret]`. Anything
|
|
594
|
-
without a token shape, such as a hostname or a customer name, is sent as written. The catalog
|
|
595
|
-
is the Runtime Skills list above, each with its summary cut to 300 characters, plus `none`.
|
|
596
|
-
- **Added text.** The hint or the fallback note is a `synthetic` text part: OpenCode still sends
|
|
597
|
-
it to the model, but does not show or copy it as the user's own words.
|
|
598
|
-
- **Answer.** A hint is added only for HTTP 200, parseable JSON, an `answers.which.choice` that is
|
|
599
|
-
exactly a catalog id, and an `answers.which.confidence` at or above the threshold. The added text is
|
|
600
|
-
LitOpenCode's own fixed sentence with that id; no response text is copied into the turn.
|
|
601
|
-
- **Fallback.** On a timeout, network error, non-200 status, invalid answer or reached cap, the
|
|
602
|
-
turn continues as it would without the hint. The first failure in a session adds one short
|
|
603
|
-
`LitOpenCode skill hint unavailable (<reason>); continuing normally.` line; later failures are
|
|
604
|
-
silent.
|
|
605
|
-
- **Status.** `litopencode doctor` reports `jevSkillHint` as `Jev skill hint: off`, `on`, or
|
|
606
|
-
`flag on but TYPESAFE_API_KEY missing`.
|
|
607
|
-
- **On screen.** On the first eligible turn of a session, a `warning` toast reads
|
|
608
|
-
`✦ Jev skill hint is ON`. When that turn also gets a hint, the notice becomes the toast's title
|
|
609
|
-
and `Jev → <skill> (<seconds>s)` its message, because the TUI keeps one toast at a time. Later
|
|
610
|
-
hinted turns show an `info` toast with only `Jev → <skill> (<seconds>s)`. Fallback notes, `none`
|
|
611
|
-
answers and skipped turns show nothing. Both toasts use OpenCode's default duration.
|
|
612
|
-
|
|
613
|
-
| Variable | Default | Effect |
|
|
614
|
-
| --- | --- | --- |
|
|
615
|
-
| `LITOPENCODE_JEV` | unset | `1` turns the hint on when the key is also set. |
|
|
616
|
-
| `TYPESAFE_API_KEY` | unset | Your own TypeSafe key. TypeSafe bills about $0.04 per million input tokens. |
|
|
617
|
-
| `LITOPENCODE_JEV_MODEL` | `jev-1.13.0` | Model name sent with each request. |
|
|
618
|
-
| `LITOPENCODE_JEV_TIMEOUT_MS` | `1500` | Hard timeout in milliseconds, capped at `3000`. |
|
|
619
|
-
| `LITOPENCODE_JEV_MAX_CALLS` | `200` | Requests allowed per session. |
|
|
620
|
-
| `LITOPENCODE_JEV_MIN_CONFIDENCE` | `0.35` | Lowest confidence that produces a hint. |
|
|
621
|
-
| `LITOPENCODE_JEV_TRACE` | unset | `1` appends one record per request to `.litopencode/logs/jev-skill-hint.jsonl`: time, prompt SHA-256, chosen id, confidence, latency, HTTP status and fallback reason. It holds no prompt text, key or response body. |
|
|
622
|
-
|
|
623
581
|
## Package surface
|
|
624
582
|
|
|
625
583
|
- Package and plugin id: `litopencode`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@litfamily/litopencode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.27",
|
|
4
4
|
"description": "Workflow plugin for OpenCode: add lit to a prompt and the work is planned, built, checked and written down so the next session can pick it up.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
package/skills/lit-docx/SKILL.md
CHANGED
|
@@ -29,7 +29,7 @@ Read the user's sources and document purpose. Preserve quotations, citations, nu
|
|
|
29
29
|
|
|
30
30
|
For a bare `lit` request without facts, choose one realistic concrete example and deliver a complete document without asking. Label invented names, dates, and numbers as sample or assumption: the frontmatter `notice:` line prints the sample-data notice once (cover or first page), and the reply repeats it. Never leave bracketed placeholders or imply that example figures were verified. An explicit non-lit request may ask for clarification about content; the look is decided by the direction step, not by a question.
|
|
31
31
|
|
|
32
|
-
A `lit` report request produces `.docx` by default. Do not ask a format, font, tonality or profile question when the direction step can decide. An explicit Elsevier, ACS, IEEE, Nature or Korean publisher profile, a named tonality, or a custom template wins. Only deliver Markdown/HTML alone if requested. Keep `draft.md` next to `draft.docx`; the Markdown is an editable source, not proof the DOCX rendered correctly.
|
|
32
|
+
A `lit` report request produces `.docx` by default. Do not ask a format, font, tonality or profile question when the direction step can decide. An explicit Elsevier, ACS, IEEE, Nature or Korean publisher profile, a named tonality, or a custom template wins. Only deliver Markdown/HTML alone if requested. A requested Markdown or plain-text deliverable (`report.md`, "in Markdown", "as plain text") is a finished document for a reader, not this skill's source: write it as ordinary Markdown with headings, paragraphs, lists and tables, no frontmatter, no `:::` directive blocks and no tonality keys, and do not build a `.docx` beside it unless one was asked for. The frontmatter and component dialect below belong only to a source this skill converts to `.docx` or PDF, or to a source the user explicitly asked for. Keep `draft.md` next to `draft.docx`; the Markdown is an editable source, not proof the DOCX rendered correctly.
|
|
33
33
|
|
|
34
34
|
## #contract.mode_matrix
|
|
35
35
|
|
|
@@ -115,7 +115,7 @@ DOCX and PPTX text extraction uses `scripts/extract_office_text.py`, which relie
|
|
|
115
115
|
|
|
116
116
|
## Guard scope and limits
|
|
117
117
|
|
|
118
|
-
Scan only text being added or changed. Preserve pre-existing file content and user quotations. Skip fenced and inline code, Markdown blockquotes, and internal paths such as `plans/`, `evidence/`, `HANDOFF*`, `.lit*/`, `.hermes/`, and ledgers. The native OpenCode guard applies to reader-facing text paths, not every source or configuration file. Office and PDF checks happen after creation: a hit asks the model to fix its source and rebuild; it cannot undo the tool that created the file.
|
|
118
|
+
Scan only text being added or changed. Preserve pre-existing file content and user quotations. Skip fenced and inline code, Markdown blockquotes (except a quote opening with a bold label, which the pledge rules read as the author's callout), and internal paths such as `plans/`, `evidence/`, `HANDOFF*`, `.lit*/`, `.hermes/`, and ledgers. The native OpenCode guard applies to reader-facing text paths, not every source or configuration file. Office and PDF checks happen after creation: a hit asks the model to fix its source and rebuild; it cannot undo the tool that created the file.
|
|
119
119
|
|
|
120
120
|
Block only high-confidence patterns such as label-only evidence lines, stacked disclaimers, and chatbot residue. Word choice, contrast, triads, and rhythm are warnings. A hit is an editing signal, not proof of model authorship. Stop when another change would alter meaning, hide a real limitation, or flatten the writer's voice.
|
|
121
121
|
|
|
@@ -17,9 +17,9 @@ This original category comes from the 26 real lines in fixtures/pos-real.txt and
|
|
|
17
17
|
- ko-bracket-status-tag and en-bracket-status-tag — ko/en, block. Replace bracket keys with a sentence describing the status.
|
|
18
18
|
- ko-standalone-scope-disclaimer and en-standalone-scope-disclaimer — ko/en, block for stock disclaimers. Preserve a user-requested legal, medical or safety notice and place it once where it applies.
|
|
19
19
|
- ko-status-negation, ko-mi-status, en-epistemic-negation and en-not-performed — warnings. ko-status-negation also reads 쓰지 않음, the polite 않습니다/않았습니다 endings and a bold-wrapped ending, so a method line such as “fitting parameter는 쓰지 않음” is advised on, never refused. A negative result or unperformed action may be the finding. Write the actor, scope and verb plainly; never turn unknown into false or an estimate into a fact.
|
|
20
|
-
- ko-negative-pledge — ko, block, one line at a time. The reader meets a sentence about what the document is not for: a self-reference (이 표, 본 자료, 해당 보고서) whose verb is negated 해석/의미/주장/대표 or whose noun is a claim noun with 아님; a bare “비교로 쓰지 않음” with no subject;
|
|
20
|
+
- ko-negative-pledge — ko, block, one line at a time. The reader meets a sentence about what the document is not for: a self-reference (이 표, 본 자료, 해당 보고서) whose verb is negated 해석/의미/주장/대표 or whose noun is a claim noun with 아님; a bare “비교로 쓰지 않음” with no subject; a purpose word such as 공개용 수치가 아님; a comparison refusal about the numbers (숫자는 서로 비교할 수 없습니다 when it stands alone, 순위처럼 읽지 말아 주세요); or a ranking refusal (…만으로 순위를 정할 수 없다, 표의 순서는 순위가 아님). Move the sentence to the reply, or turn it into a fact about the data beside the number it affects (“샘플은 한 공정 조건만 포함한다”). Operating rules (사용하지 않습니다, 냉각수로 쓰지 않음), conditional method facts (합계와 비교하면 안 됩니다), type rules (숫자와 문자열은 서로 비교할 수 없습니다), data-handling rules, and legal or financial notices (대체하지, 보장하지, 권유가 아닙니다) stay outside the pattern on purpose. A quote line that opens with a bold label (`> **주의:** …`) is the author's own callout, so this rule and the two below read it with the quote marker blanked; a plain quote line stays skipped.
|
|
21
21
|
- ko-negative-pledge-repeat — ko, block, whole document. Two or more sentences ending in 해석하지/주장하지/대표하지 않음, or in a claim noun + 아님, make a list of disclaimers. One such sentence stays with the warning above; the use verbs and 의미하/간주하/인용하 are left out because definitions and processing rules end that way.
|
|
22
|
-
- en-negative-pledge — en, block, one sentence. “This is not intended as a benchmark”, “these figures do not constitute a comparison”, “not to be read as a ranking”, and “the deck makes no claim about …” say what the document is not. A comparison, benchmark or ranking noun is required, and substitute-for-advice and investment, legal or medical advice notices stay clean. Say it in the reply, or state the data fact.
|
|
22
|
+
- en-negative-pledge — en, block, one sentence. “This is not intended as a benchmark”, “these figures do not constitute a comparison”, “not to be read as a ranking”, and “the deck makes no claim about …” say what the document is not. A comparison, benchmark or ranking noun is required, and substitute-for-advice and investment, legal or medical advice notices stay clean. A deliverable subject (this table, our deck) that is not a ranking or is not meant to rank also counts, and so do “they are not a ranking.” and “do not compare these numbers.” at the end of a sentence; a refusal that goes on to describe or instruct stays clean. It reads bold-label callouts too. Say it in the reply, or state the data fact.
|
|
23
23
|
- ko-plain-meta-label-warn and en-plain-caveat-label — warnings for boundary headings. They may be useful in internal or technical packets.
|
|
24
24
|
- en-knowledge-cutoff, en-ai-self-disclaimer, en-hope-helps and ko-chatbot-signoff — block. Remove the model wrapper or generic sign-off, then retain the answer.
|
|
25
25
|
- ko-chatbot-opener and en-chatbot-opener — block stock greetings and praise at the start of a deliverable.
|