@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.
@@ -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 { jevSkillHintDoctorLine } from "../jev-skill-hint.js";
20
- import { autoUpdateInstallLockPath, autoUpdateJournalPath, autoUpdateReceiptPath, isAutoUpdateDisabled, readAutoUpdateReceipt } from "./auto-update.js";
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
- enabled: !isAutoUpdateDisabled(process.env),
209
- installLock: autoUpdateInstallLockPath(),
210
- journal: autoUpdateJournalPath(),
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, 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.",
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
- if (Buffer.byteLength(newText, "utf8") > maxTextBytes)
208
- throw new Error("new text exceeds scan limit");
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=${findings.length} path=${displayPath(text.file)}. Review the new wording; keep any accurate, useful warning.`
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, showJevSkillHintToast } from "./activation.js";
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.25"><title>release: 1.0.25</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.25</text></g></svg>
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` | Written only when `LITOPENCODE_JEV_TRACE=1`: one record per Jev skill hint request with time, prompt SHA-256, chosen skill id, confidence, latency, HTTP status and fallback reason. No prompt text, key or response body. |
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
@@ -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.25",
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",
@@ -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; or a purpose word such as 공개용 수치가 아님. Move the sentence to the reply, or turn it into a fact about the data beside the number it affects (“샘플은 한 공정 조건만 포함한다”). Operating rules (사용하지 않습니다, 냉각수로 쓰지 않음), data-handling rules, and legal or financial notices (대체하지, 보장하지, 권유가 아닙니다) stay outside the pattern on purpose.
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.