@litfamily/litopencode 1.0.34 → 1.0.36
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 +12 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/dist/activation-primary-prompts.js +7 -3
- package/dist/activation.js +5 -0
- package/dist/adhd-next.d.ts +1 -1
- package/dist/adhd-next.js +11 -0
- package/dist/cli/agent-file-command.d.ts +21 -0
- package/dist/cli/agent-file-command.js +99 -0
- package/dist/cli/agent-file.d.ts +46 -0
- package/dist/cli/agent-file.js +197 -0
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.js +25 -1
- package/dist/cli/doctor.js +10 -0
- package/dist/cli/install.d.ts +4 -1
- package/dist/cli/install.js +8 -3
- package/dist/cli/managed-block.d.ts +26 -0
- package/dist/cli/managed-block.js +80 -0
- package/dist/cli/model-routing.d.ts +5 -0
- package/dist/cli/model-routing.js +1 -1
- package/dist/cli/types.d.ts +10 -0
- package/dist/cli.js +14 -1
- package/dist/commands.js +17 -4
- package/dist/native-goal-ledger.d.ts +43 -0
- package/dist/native-goal-ledger.js +74 -0
- package/dist/v2/commands.d.ts +1 -0
- package/dist/v2/commands.js +2 -2
- package/dist/v2/context.d.ts +6 -1
- package/dist/v2/native-goal-runtime.d.ts +74 -0
- package/dist/v2/native-goal-runtime.js +193 -0
- package/dist/v2/native-goal.d.ts +49 -0
- package/dist/v2/native-goal.js +71 -0
- package/dist/v2/server.js +11 -1
- package/dist/v2/session-hooks.d.ts +4 -0
- package/dist/v2/session-hooks.js +13 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/migration.md +1 -1
- package/docs/reference-Ko-KR.md +27 -0
- package/docs/reference.md +33 -3
- package/package.json +2 -2
- package/skills/lit-adhd/SKILL.md +3 -3
- package/skills/lit-adhd/references/brief-format.md +1 -1
- package/skills/lit-adhd/references/status-taxonomy.md +1 -1
- package/skills/lit-adhd/scripts/rewind.mjs +73 -14
- package/skills/lit-init/SKILL.md +4 -0
- package/skills/litgoal/SKILL.md +2 -2
- package/skills/litwork/SKILL.md +4 -0
- package/skills/managed-skill-manifest.json +3 -3
- package/skills/native-goal-verdict/SKILL.md +34 -60
- package/skills/start-work/SKILL.md +4 -0
- package/skills/workflow-loop/SKILL.md +4 -0
package/dist/cli/install.d.ts
CHANGED
|
@@ -7,4 +7,7 @@ import { type HostChoice } from "./host-detect.ts";
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function withoutV2PluginEntries(config: Record<string, unknown> | null): Record<string, unknown> | null;
|
|
9
9
|
export declare function assertNoHostOnlyKeysAdded(before: Record<string, unknown> | null, next: Record<string, unknown> | null): void;
|
|
10
|
-
export declare function install(root: string, dryRun: boolean, modelSelection?: InstallModelSelection | undefined, modelPrompt?: ModelPromptMode, permissionMode?: InstallPermissionMode, permissionModeExplicit?: boolean, permissionPrompt?: PermissionPromptMode, progress?: InstallProgress, outputStylePrompt?: PermissionPromptMode, host?: HostChoice
|
|
10
|
+
export declare function install(root: string, dryRun: boolean, modelSelection?: InstallModelSelection | undefined, modelPrompt?: ModelPromptMode, permissionMode?: InstallPermissionMode, permissionModeExplicit?: boolean, permissionPrompt?: PermissionPromptMode, progress?: InstallProgress, outputStylePrompt?: PermissionPromptMode, host?: HostChoice, agentFileChoice?: {
|
|
11
|
+
readonly assumeYes: boolean;
|
|
12
|
+
readonly skip: boolean;
|
|
13
|
+
}): Promise<CliResult>;
|
package/dist/cli/install.js
CHANGED
|
@@ -13,6 +13,7 @@ import { renderLitOpenCodeWordmark } from "./install-tui.js";
|
|
|
13
13
|
import { promptModelSelection, promptOutputStyle, promptPermissionMode } from "./model-routing.js";
|
|
14
14
|
import { litOpenCodeConfigFileForOpenCodeRoot } from "../state.js";
|
|
15
15
|
import { describeNativeSkillMutation, ensureNativeSkills, inspectNativeSkills } from "./native-skills.js";
|
|
16
|
+
import { installAgentFile, planAgentFile } from "./agent-file-command.js";
|
|
16
17
|
import { applyPluginMutation, describePluginMutation, isLitOpenCodeEntry } from "./plugin-mutation.js";
|
|
17
18
|
import { prewarmMotionRuntime } from "./motion-runtime.js";
|
|
18
19
|
import { applyCliJsonCleanup, applyTuiWrites, backupTuiFiles, inspectTuiPlugin, planCliJsonCleanup, planTuiInstall, skippedTuiPlan } from "./tui-config.js";
|
|
@@ -257,7 +258,7 @@ async function verifyInstalledSurfaces(root, configFile, target, metadata, selec
|
|
|
257
258
|
if (failures.length > 0)
|
|
258
259
|
throw new Error("Install verification failed: " + failures.join("; "));
|
|
259
260
|
}
|
|
260
|
-
export async function install(root, dryRun, modelSelection = undefined, modelPrompt = "auto", permissionMode = "safe", permissionModeExplicit = false, permissionPrompt = "auto", progress, outputStylePrompt = "auto", host = "auto") {
|
|
261
|
+
export async function install(root, dryRun, modelSelection = undefined, modelPrompt = "auto", permissionMode = "safe", permissionModeExplicit = false, permissionPrompt = "auto", progress, outputStylePrompt = "auto", host = "auto", agentFileChoice = { assumeYes: false, skip: false }) {
|
|
261
262
|
const retiredNotice = await migrateRetiredEfforts(root, dryRun);
|
|
262
263
|
if (retiredNotice !== undefined)
|
|
263
264
|
process.stdout.write(retiredNotice + "\n");
|
|
@@ -336,10 +337,12 @@ export async function install(root, dryRun, modelSelection = undefined, modelPro
|
|
|
336
337
|
}
|
|
337
338
|
const { configFile, before, mutation, litopencodeConfig, commandAliases, nativeSkills, hostLimits, routes, tui, cliJson } = inspected;
|
|
338
339
|
const v2PluginsChanged = resolved.forV2 && withoutV2PluginEntries(before) !== before;
|
|
340
|
+
const agentFileInput = { root, version: metadata.version, ...agentFileChoice };
|
|
339
341
|
const report = {
|
|
340
342
|
dryRun,
|
|
341
343
|
path: configFile,
|
|
342
344
|
litopencodeConfig,
|
|
345
|
+
...(dryRun ? { agentFile: await planAgentFile(agentFileInput) } : {}),
|
|
343
346
|
plugin: mutation.plugin,
|
|
344
347
|
commandAliases,
|
|
345
348
|
nativeSkills,
|
|
@@ -423,12 +426,14 @@ export async function install(root, dryRun, modelSelection = undefined, modelPro
|
|
|
423
426
|
await applyCliJsonCleanup(cliJson);
|
|
424
427
|
await withProgress(activeProgress, "Verify install", async () => verifyInstalledSurfaces(root, configFile, resolved.target, metadata, selectedModel, selectedPermissionMode, selectedPermissionExplicit, selectedOutputStyle, resolved.detection));
|
|
425
428
|
await activeProgress?.complete();
|
|
429
|
+
// The block is the last step: it asks a question, and a refusal here never undoes a finished install.
|
|
430
|
+
const agentFileLine = wrapReceiptLine(await installAgentFile(agentFileInput));
|
|
426
431
|
// MO-A-42: the installer attempts the motion pre-warm; a failure never fails the install.
|
|
427
432
|
const motionReceipt = wrapReceiptLine(prewarmMotionRuntime(metadata.packageRoot));
|
|
428
433
|
return {
|
|
429
434
|
exitCode: 0,
|
|
430
435
|
stdout: activeProgress === undefined
|
|
431
|
-
? renderLitOpenCodeWordmark(pluginSpec(metadata), false) + "\n" + renderInstallReport(report) + "\n" + motionReceipt
|
|
432
|
-
: motionReceipt
|
|
436
|
+
? renderLitOpenCodeWordmark(pluginSpec(metadata), false) + "\n" + renderInstallReport(report) + "\n" + agentFileLine + "\n" + motionReceipt
|
|
437
|
+
: agentFileLine + "\n" + motionReceipt
|
|
433
438
|
};
|
|
434
439
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare class ManagedBlockError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export type ManagedBlock = {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly version: string;
|
|
6
|
+
readonly body: string;
|
|
7
|
+
};
|
|
8
|
+
export type BlockInspection = {
|
|
9
|
+
readonly state: "absent" | "present" | "malformed";
|
|
10
|
+
readonly version: string | null;
|
|
11
|
+
readonly created: boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare function inspectBlock(text: string, id: string): BlockInspection;
|
|
14
|
+
/** Adds the block, or swaps the bytes between an existing block's markers. `created` marks a file made for the block. */
|
|
15
|
+
export declare function upsertBlock(text: string, block: ManagedBlock, options?: {
|
|
16
|
+
readonly created?: boolean;
|
|
17
|
+
}): {
|
|
18
|
+
text: string;
|
|
19
|
+
changed: boolean;
|
|
20
|
+
};
|
|
21
|
+
/** Removes the block, the line ending after it and whatever was added in front of it. */
|
|
22
|
+
export declare function removeBlock(text: string, id: string): {
|
|
23
|
+
text: string;
|
|
24
|
+
changed: boolean;
|
|
25
|
+
created: boolean;
|
|
26
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// A LitFamily block inside a file the user owns (OpenCode's global AGENTS.md, a repo's AGENTS.md): the
|
|
2
|
+
// lines between `<!-- litfamily:<id>:start v=<version> -->` and `<!-- litfamily:<id>:end -->`. Only those
|
|
3
|
+
// lines are written. The start marker also records what was put in front of the block (`pre=`, hex) and
|
|
4
|
+
// whether the file was made for it (`new=1`), so removing the block takes back exactly that and an
|
|
5
|
+
// add-then-remove returns the original bytes under any line ending. A start without its end, or two of
|
|
6
|
+
// either, is malformed and refused rather than guessed at.
|
|
7
|
+
import { Buffer } from "node:buffer";
|
|
8
|
+
export class ManagedBlockError extends Error {
|
|
9
|
+
}
|
|
10
|
+
const startOf = (id) => `<!-- litfamily:${id}:start`;
|
|
11
|
+
const endOf = (id) => `<!-- litfamily:${id}:end -->`;
|
|
12
|
+
const startLine = /^<!-- litfamily:[^:\s]+:start v=(\S+)((?: [a-z]+=[0-9a-f]*)*) -->/u;
|
|
13
|
+
function lineEnding(text) {
|
|
14
|
+
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
15
|
+
}
|
|
16
|
+
function positionsOf(text, needle) {
|
|
17
|
+
const found = [];
|
|
18
|
+
for (let at = text.indexOf(needle); at !== -1; at = text.indexOf(needle, at + needle.length))
|
|
19
|
+
found.push(at);
|
|
20
|
+
return found;
|
|
21
|
+
}
|
|
22
|
+
function locate(text, id) {
|
|
23
|
+
const starts = positionsOf(text, startOf(id));
|
|
24
|
+
const ends = positionsOf(text, endOf(id));
|
|
25
|
+
if (starts.length === 0 && ends.length === 0)
|
|
26
|
+
return null;
|
|
27
|
+
if (starts.length !== 1 || ends.length !== 1 || ends[0] < starts[0]) {
|
|
28
|
+
throw new ManagedBlockError(`malformed LitFamily ${id} block: expected one start marker followed by one end marker`);
|
|
29
|
+
}
|
|
30
|
+
const head = startLine.exec(text.slice(starts[0]));
|
|
31
|
+
if (head === null)
|
|
32
|
+
throw new ManagedBlockError(`malformed LitFamily ${id} block: unreadable start marker`);
|
|
33
|
+
const attributes = new Map(head[2].trim().split(" ").filter(Boolean).map((pair) => pair.split("=")));
|
|
34
|
+
const pre = attributes.get("pre");
|
|
35
|
+
return {
|
|
36
|
+
start: starts[0],
|
|
37
|
+
end: ends[0] + endOf(id).length,
|
|
38
|
+
version: head[1],
|
|
39
|
+
pre: pre === undefined || pre === "" ? "" : Buffer.from(pre, "hex").toString("utf8"),
|
|
40
|
+
created: attributes.get("new") === "1"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function render(block, eol, added) {
|
|
44
|
+
const attributes = `${added.pre === "" ? "" : ` pre=${Buffer.from(added.pre, "utf8").toString("hex")}`}${added.created ? " new=1" : ""}`;
|
|
45
|
+
const body = block.body.replace(/\r?\n$/u, "").split(/\r?\n/u);
|
|
46
|
+
return [`${startOf(block.id)} v=${block.version}${attributes} -->`, ...body, endOf(block.id)].join(eol);
|
|
47
|
+
}
|
|
48
|
+
export function inspectBlock(text, id) {
|
|
49
|
+
try {
|
|
50
|
+
const span = locate(text, id);
|
|
51
|
+
return span === null ? { state: "absent", version: null, created: false } : { state: "present", version: span.version, created: span.created };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { state: "malformed", version: null, created: false };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Adds the block, or swaps the bytes between an existing block's markers. `created` marks a file made for the block. */
|
|
58
|
+
export function upsertBlock(text, block, options = {}) {
|
|
59
|
+
const eol = lineEnding(text);
|
|
60
|
+
const span = locate(text, block.id);
|
|
61
|
+
if (span !== null) {
|
|
62
|
+
const next = text.slice(0, span.start) + render(block, eol, span) + text.slice(span.end);
|
|
63
|
+
return { text: next, changed: next !== text };
|
|
64
|
+
}
|
|
65
|
+
const pre = text === "" ? "" : (text.endsWith("\n") ? "" : eol) + eol;
|
|
66
|
+
return { text: text + pre + render(block, eol, { pre, created: options.created === true }) + eol, changed: true };
|
|
67
|
+
}
|
|
68
|
+
/** Removes the block, the line ending after it and whatever was added in front of it. */
|
|
69
|
+
export function removeBlock(text, id) {
|
|
70
|
+
const span = locate(text, id);
|
|
71
|
+
if (span === null)
|
|
72
|
+
return { text, changed: false, created: false };
|
|
73
|
+
let before = text.slice(0, span.start);
|
|
74
|
+
let after = text.slice(span.end);
|
|
75
|
+
const eol = after.startsWith("\r\n") ? "\r\n" : after.startsWith("\n") ? "\n" : "";
|
|
76
|
+
after = after.slice(eol.length);
|
|
77
|
+
if (span.pre !== "" && before.endsWith(span.pre))
|
|
78
|
+
before = before.slice(0, -span.pre.length);
|
|
79
|
+
return { text: before + after, changed: true, created: span.created };
|
|
80
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { InstallModelSelection, InstallPermissionChoice, InstallPermissionMode, ModelConfigClassification, ModelPromptMode, PermissionPromptMode } from "./types.ts";
|
|
2
2
|
export declare function promptOutputStyle(existing: string, promptMode: PermissionPromptMode, dryRun: boolean): Promise<string>;
|
|
3
|
+
export type PromptReader = {
|
|
4
|
+
question(prompt: string): Promise<string>;
|
|
5
|
+
close(): void;
|
|
6
|
+
};
|
|
7
|
+
export declare function createPromptReader(): PromptReader;
|
|
3
8
|
export declare function promptPermissionMode(selected: InstallPermissionMode, mode: PermissionPromptMode, dryRun: boolean): Promise<InstallPermissionChoice>;
|
|
4
9
|
export declare function renderModelRouteCard(selection: InstallModelSelection, configPath: string): string;
|
|
5
10
|
export declare function promptModelSelection(selection: InstallModelSelection | undefined, mode: ModelPromptMode, dryRun: boolean, classification: ModelConfigClassification, configPath: string, currentSelection?: InstallModelSelection | undefined): Promise<InstallModelSelection | undefined>;
|
|
@@ -57,7 +57,7 @@ function formatChoice(index, label, hint) {
|
|
|
57
57
|
async function askLine(reader, prompt) {
|
|
58
58
|
return (await reader.question(prompt)).trim();
|
|
59
59
|
}
|
|
60
|
-
function createPromptReader() {
|
|
60
|
+
export function createPromptReader() {
|
|
61
61
|
if (process.stdin.isTTY === true) {
|
|
62
62
|
return readline.createInterface({ input: process.stdin, output: process.stdout, terminal: markColorMode() !== "none" });
|
|
63
63
|
}
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export type ParsedArgs = {
|
|
|
28
28
|
readonly outputStylePrompt: PermissionPromptMode;
|
|
29
29
|
readonly loop: LoopArgs;
|
|
30
30
|
readonly tuiAction?: string;
|
|
31
|
+
readonly agentFileAction?: string;
|
|
32
|
+
readonly agentFileDir?: string;
|
|
33
|
+
readonly agentFile: "auto" | "skip";
|
|
34
|
+
readonly assumeYes: boolean;
|
|
31
35
|
readonly host: HostChoice;
|
|
32
36
|
};
|
|
33
37
|
export type LoopArgs = {
|
|
@@ -158,6 +162,12 @@ export type InstallReport = {
|
|
|
158
162
|
readonly changed: boolean;
|
|
159
163
|
};
|
|
160
164
|
};
|
|
165
|
+
readonly agentFile?: {
|
|
166
|
+
readonly path: string;
|
|
167
|
+
readonly state: string;
|
|
168
|
+
readonly mode: string;
|
|
169
|
+
readonly action: string;
|
|
170
|
+
};
|
|
161
171
|
readonly plugin: PluginMutation["plugin"];
|
|
162
172
|
readonly commandAliases: {
|
|
163
173
|
readonly path: string;
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isLoopCommand, parseArgs } from "./cli/args.js";
|
|
2
2
|
import { runLoopCommand } from "./cli/loop.js";
|
|
3
3
|
import { doctor } from "./cli/doctor.js";
|
|
4
|
+
import { runAgentFileCommand } from "./cli/agent-file-command.js";
|
|
4
5
|
import { install } from "./cli/install.js";
|
|
5
6
|
import { createInstallTui, renderLitOpenCodeWordmark, shouldRenderInstallTui } from "./cli/install-tui.js";
|
|
6
7
|
import { runUpdateNotifier } from "./cli/update-notifier.js";
|
|
@@ -33,7 +34,10 @@ function helpText() {
|
|
|
33
34
|
" litopencode install [--dry-run] [--root <dir>] --provider <openai|xai> --model <id> [--effort <e>] [--subagent-model <provider/id>] [--subagent-effort <e>] [--yolo]",
|
|
34
35
|
" litopencode install [--model-prompt | --no-model-prompt | --yes]",
|
|
35
36
|
" litopencode install [--permission-prompt | --no-permission-prompt] [--permission-mode <safe|balanced|yolo>]",
|
|
37
|
+
" litopencode install [--no-agent-file]",
|
|
36
38
|
" litopencode install|doctor [--no-auto-update]",
|
|
39
|
+
" litopencode agent-file on|off|remove [--dry-run] [--root <dir>]",
|
|
40
|
+
" litopencode agent-file project [--dir <path>] [--dry-run]",
|
|
37
41
|
" litopencode doctor [--json] [--root <dir>]",
|
|
38
42
|
" litopencode tui remove [--dry-run] [--root <dir>]",
|
|
39
43
|
" litopencode motion-runtime install|status [--audio] [--word-timing] [--dry-run] (install) [--json] (status)",
|
|
@@ -54,6 +58,7 @@ function helpText() {
|
|
|
54
58
|
"Commands:",
|
|
55
59
|
" install Register litopencode with OpenCode using a branded installer UI.",
|
|
56
60
|
" doctor Report package, config, and runtime path status without writing files.",
|
|
61
|
+
" agent-file Add, remove or switch off the short LitFamily block in OpenCode's global AGENTS.md; project writes it into a repo's AGENTS.md.",
|
|
57
62
|
" tui remove Remove only LitOpenCode's TUI panel entry from tui.json/tui.jsonc (backed up first).",
|
|
58
63
|
" fetch-public Fetch a public URL with SSRF guards, verdicts, and trace output.",
|
|
59
64
|
" --allow-private-network is opt-in for reviewed local fixtures or trusted internal tests.",
|
|
@@ -121,6 +126,12 @@ function helpText() {
|
|
|
121
126
|
" --no-auto-update or LITOPENCODE_NO_AUTO_UPDATE=1 disables the barrier; NO_UPDATE_NOTIFIER and LITOPENCODE_NO_UPDATE_CHECK also disable it.",
|
|
122
127
|
" The detached update notice remains cache-refresh-only and never installs.",
|
|
123
128
|
"",
|
|
129
|
+
"LitFamily block:",
|
|
130
|
+
" An interactive install asks once whether to add a short, marker-delimited block to <root>/AGENTS.md, the global file OpenCode reads every session.",
|
|
131
|
+
" --yes adds it; --no-agent-file skips it for one run; a non-interactive install without --yes never writes it. Your own text stays byte for byte.",
|
|
132
|
+
" OpenCode reads ~/.claude/CLAUDE.md only while <root>/AGENTS.md is missing, so the installer never creates AGENTS.md over an existing CLAUDE.md.",
|
|
133
|
+
" agent-file off removes the block and stores the choice in litopencode.json; agent-file remove only removes it. doctor prints the block state.",
|
|
134
|
+
"",
|
|
124
135
|
"Default root:",
|
|
125
136
|
" ~/.config/opencode, or $XDG_CONFIG_HOME/opencode when XDG_CONFIG_HOME is set."
|
|
126
137
|
].join("\n");
|
|
@@ -146,6 +157,8 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
146
157
|
return await runLoopCommand(parsed.command, parsed.loop, parsed.dryRun);
|
|
147
158
|
if (parsed.command === "doctor")
|
|
148
159
|
return await doctor(parsed.root, parsed.host);
|
|
160
|
+
if (parsed.command === "agent-file")
|
|
161
|
+
return await runAgentFileCommand(parsed, (await readPackageMetadata()).version);
|
|
149
162
|
if (parsed.command === "tui") {
|
|
150
163
|
const removed = await removeTuiEntries(parsed.root, parsed.dryRun, { cliJson: parsed.host !== "v1" });
|
|
151
164
|
return { exitCode: 0, stdout: JSON.stringify(removed, null, 2) };
|
|
@@ -163,7 +176,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
163
176
|
return { exitCode: output.ok ? 0 : 1, stdout: renderFetchPublicText(output) };
|
|
164
177
|
}
|
|
165
178
|
if (parsed.command === "install") {
|
|
166
|
-
return await install(parsed.root, parsed.dryRun, parsed.modelSelection, parsed.modelPrompt, parsed.permissionMode, parsed.permissionModeExplicit, parsed.permissionPrompt, options.installProgress, parsed.outputStylePrompt, parsed.host);
|
|
179
|
+
return await install(parsed.root, parsed.dryRun, parsed.modelSelection, parsed.modelPrompt, parsed.permissionMode, parsed.permissionModeExplicit, parsed.permissionPrompt, options.installProgress, parsed.outputStylePrompt, parsed.host, { assumeYes: parsed.assumeYes, skip: parsed.agentFile === "skip" });
|
|
167
180
|
}
|
|
168
181
|
return { exitCode: 2, stderr: `Unknown command: ${parsed.command}` };
|
|
169
182
|
}
|
package/dist/commands.js
CHANGED
|
@@ -9,8 +9,18 @@ import { writeNext } from "./adhd-next.js";
|
|
|
9
9
|
import { autoHandoffSettingText } from "./auto-handoff.js";
|
|
10
10
|
import { formatResolvedPlanNotice, formatSavedPlanNotice, parsePlanSaveArgument, persistApprovedPlan, PlanScopeError, resolveLatestDurablePlan } from "./durable-plan.js";
|
|
11
11
|
import { isWorkflowFamilyMode, workflowFamilyCommandDefinitions } from "./workflow-families.js";
|
|
12
|
+
import { recordNativeGoalUnavailable } from "./native-goal-ledger.js";
|
|
13
|
+
import { nativeGoalRoute } from "./v2/native-goal.js";
|
|
12
14
|
import { captureKnowledgeEvent, inspectKnowledge, queryKnowledge, recoverKnowledge, reviewKnowledgeRecord } from "./knowledge.js";
|
|
13
15
|
export { litActivationBanner, startWorkPromptInjection } from "./activation.js";
|
|
16
|
+
/**
|
|
17
|
+
* The four loop commands share one prompt, and an installed alias file hands the host exactly that text,
|
|
18
|
+
* so the prompt alone cannot say which command was typed. A last line names it; the plugin reads it to tell
|
|
19
|
+
* `/lit-loop` and `/litwork` (which set a native goal) from bare `/lit` (which does not).
|
|
20
|
+
*/
|
|
21
|
+
function loopCommandText(id) {
|
|
22
|
+
return `${litLoopPromptInjection}\nCommand route: "${id}"`;
|
|
23
|
+
}
|
|
14
24
|
export const litOpenCodeCommands = Object.freeze([
|
|
15
25
|
...workflowFamilyCommandDefinitions,
|
|
16
26
|
{
|
|
@@ -35,7 +45,7 @@ export const litOpenCodeCommands = Object.freeze([
|
|
|
35
45
|
title: "LitOpenCode",
|
|
36
46
|
description: "Activate the LitOpenCode model probe line and durable goal loop.",
|
|
37
47
|
banner: activationBanner("lit"),
|
|
38
|
-
activationText:
|
|
48
|
+
activationText: loopCommandText("lit")
|
|
39
49
|
},
|
|
40
50
|
{
|
|
41
51
|
id: "lit-loop",
|
|
@@ -43,7 +53,7 @@ export const litOpenCodeCommands = Object.freeze([
|
|
|
43
53
|
title: "LitOpenCode Lit Loop",
|
|
44
54
|
description: "Activate the full LitOpenCode durable lit-loop prompt.",
|
|
45
55
|
banner: activationBanner("lit-loop"),
|
|
46
|
-
activationText:
|
|
56
|
+
activationText: loopCommandText("lit-loop")
|
|
47
57
|
},
|
|
48
58
|
{
|
|
49
59
|
id: "litwork",
|
|
@@ -51,7 +61,7 @@ export const litOpenCodeCommands = Object.freeze([
|
|
|
51
61
|
title: "LitOpenCode Work Loop",
|
|
52
62
|
description: "Resume a LitOpenCode work loop with durable ledger-backed progress.",
|
|
53
63
|
banner: activationBanner("litwork"),
|
|
54
|
-
activationText:
|
|
64
|
+
activationText: loopCommandText("litwork")
|
|
55
65
|
},
|
|
56
66
|
{
|
|
57
67
|
id: "lit-work",
|
|
@@ -59,7 +69,7 @@ export const litOpenCodeCommands = Object.freeze([
|
|
|
59
69
|
title: "LitOpenCode Work Loop",
|
|
60
70
|
description: "Hyphenated alias for /litwork.",
|
|
61
71
|
banner: activationBanner("lit-work"),
|
|
62
|
-
activationText:
|
|
72
|
+
activationText: loopCommandText("lit-work")
|
|
63
73
|
},
|
|
64
74
|
{
|
|
65
75
|
id: "start-work",
|
|
@@ -664,6 +674,9 @@ export function createCommandActivationHook(projectRoot, options = {}) {
|
|
|
664
674
|
}
|
|
665
675
|
});
|
|
666
676
|
await options.onSkillActivation?.(input.sessionID, command.id);
|
|
677
|
+
const goalRoute = nativeGoalRoute(command.id, "");
|
|
678
|
+
if (goalRoute !== undefined)
|
|
679
|
+
await recordNativeGoalUnavailable(projectRoot, input.sessionID, goalRoute, "opencode-1");
|
|
667
680
|
if (command.id === "lit-scientific-visualization") {
|
|
668
681
|
options.onScientificVisualizationActivation?.(input.sessionID);
|
|
669
682
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type NativeGoalLedgerEvent = {
|
|
2
|
+
readonly type: "native-goal.created";
|
|
3
|
+
readonly goalID: string;
|
|
4
|
+
} | {
|
|
5
|
+
readonly type: "native-goal.resumed";
|
|
6
|
+
readonly goalID: string;
|
|
7
|
+
} | {
|
|
8
|
+
readonly type: "native-goal.cleared";
|
|
9
|
+
readonly goalID: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly type: "native-goal.left-alone";
|
|
12
|
+
readonly goalID: string;
|
|
13
|
+
readonly status: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly type: "native-goal.conflict";
|
|
16
|
+
readonly goalID: string;
|
|
17
|
+
readonly reason: string;
|
|
18
|
+
} | {
|
|
19
|
+
readonly type: "native-goal.failed";
|
|
20
|
+
readonly step: string;
|
|
21
|
+
readonly detail: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly type: "native-goal.unavailable";
|
|
24
|
+
readonly host: string;
|
|
25
|
+
};
|
|
26
|
+
export type NativeGoalOwnership = {
|
|
27
|
+
readonly goalID: string | undefined;
|
|
28
|
+
readonly resumes: number;
|
|
29
|
+
readonly createdAt: string | undefined;
|
|
30
|
+
};
|
|
31
|
+
export declare function recordNativeGoal(projectRoot: string, sessionID: string, route: string | undefined, event: NativeGoalLedgerEvent): Promise<boolean>;
|
|
32
|
+
/** The goal id LitOpenCode created for this session and has not cleared, with its resume count. */
|
|
33
|
+
export declare function nativeGoalOwnership(projectRoot: string, sessionID: string): Promise<NativeGoalOwnership>;
|
|
34
|
+
/** OpenCode 1 and older OpenCode 2 hosts have no `/goal`: say so once per session and route. */
|
|
35
|
+
export declare function recordNativeGoalUnavailable(projectRoot: string, sessionID: string, route: string, host: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* True when the evidence ledger holds a goal for this session that is complete and was last written after
|
|
38
|
+
* the native goal was created. A session's first ledger goal stays complete for good, so an older one must
|
|
39
|
+
* not close a later native goal.
|
|
40
|
+
*/
|
|
41
|
+
export declare function ledgerGoalDone(projectRoot: string, sessionID: string, since?: string): Promise<boolean>;
|
|
42
|
+
/** Whether the ledger already holds an event of this type for the session (used to say a thing once). */
|
|
43
|
+
export declare function nativeGoalRecorded(projectRoot: string, sessionID: string, type: NativeGoalLedgerEvent["type"], route?: string): Promise<boolean>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The native-goal records in LitOpenCode's own ledger (`.litopencode/litgoal`). OpenCode's goal has no
|
|
2
|
+
// owner field, so the ledger is where LitOpenCode remembers which goal id it created, how often it
|
|
3
|
+
// resumed it, and that a host had no `/goal`. Every write is best-effort: a ledger failure never blocks
|
|
4
|
+
// the work, and a read failure reads as "owns nothing", which only makes LitOpenCode do less.
|
|
5
|
+
import { createLitGoalOperations, readEvidenceLedger } from "./ledger.js";
|
|
6
|
+
export async function recordNativeGoal(projectRoot, sessionID, route, event) {
|
|
7
|
+
try {
|
|
8
|
+
const operations = createLitGoalOperations(projectRoot);
|
|
9
|
+
await operations.init();
|
|
10
|
+
await operations.append({ ...event, sessionID, ...(route === undefined ? {} : { route }), timestamp: new Date().toISOString() });
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch { // no-excuse-ok: catch -- a ledger failure must never block the user's work.
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** The goal id LitOpenCode created for this session and has not cleared, with its resume count. */
|
|
18
|
+
export async function nativeGoalOwnership(projectRoot, sessionID) {
|
|
19
|
+
let goalID;
|
|
20
|
+
let createdAt;
|
|
21
|
+
let resumes = 0;
|
|
22
|
+
try {
|
|
23
|
+
for (const event of await createLitGoalOperations(projectRoot).read()) {
|
|
24
|
+
if (event["sessionID"] !== sessionID || typeof event["goalID"] !== "string")
|
|
25
|
+
continue;
|
|
26
|
+
if (event.type === "native-goal.created") {
|
|
27
|
+
goalID = event["goalID"];
|
|
28
|
+
createdAt = typeof event["timestamp"] === "string" ? event["timestamp"] : undefined;
|
|
29
|
+
resumes = 0;
|
|
30
|
+
}
|
|
31
|
+
else if (event.type === "native-goal.cleared" && event["goalID"] === goalID) {
|
|
32
|
+
goalID = undefined;
|
|
33
|
+
createdAt = undefined;
|
|
34
|
+
}
|
|
35
|
+
else if (event.type === "native-goal.resumed" && event["goalID"] === goalID) {
|
|
36
|
+
resumes += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch { // no-excuse-ok: catch -- an unreadable ledger reads as "owns nothing", so LitOpenCode touches nothing.
|
|
41
|
+
return { goalID: undefined, resumes: 0, createdAt: undefined };
|
|
42
|
+
}
|
|
43
|
+
return { goalID, resumes, createdAt };
|
|
44
|
+
}
|
|
45
|
+
/** OpenCode 1 and older OpenCode 2 hosts have no `/goal`: say so once per session and route. */
|
|
46
|
+
export async function recordNativeGoalUnavailable(projectRoot, sessionID, route, host) {
|
|
47
|
+
if (await nativeGoalRecorded(projectRoot, sessionID, "native-goal.unavailable", route))
|
|
48
|
+
return;
|
|
49
|
+
await recordNativeGoal(projectRoot, sessionID, route, { type: "native-goal.unavailable", host });
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* True when the evidence ledger holds a goal for this session that is complete and was last written after
|
|
53
|
+
* the native goal was created. A session's first ledger goal stays complete for good, so an older one must
|
|
54
|
+
* not close a later native goal.
|
|
55
|
+
*/
|
|
56
|
+
export async function ledgerGoalDone(projectRoot, sessionID, since) {
|
|
57
|
+
try {
|
|
58
|
+
const state = await readEvidenceLedger(projectRoot);
|
|
59
|
+
const floor = since === undefined ? Number.NaN : Date.parse(since);
|
|
60
|
+
return state.goals.some((goal) => goal.sessionId === sessionID && goal.status === "complete" && (Number.isNaN(floor) || Date.parse(goal.updatedAt) >= floor));
|
|
61
|
+
}
|
|
62
|
+
catch { // no-excuse-ok: catch -- an unreadable evidence ledger is not done.
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Whether the ledger already holds an event of this type for the session (used to say a thing once). */
|
|
67
|
+
export async function nativeGoalRecorded(projectRoot, sessionID, type, route) {
|
|
68
|
+
try {
|
|
69
|
+
return (await createLitGoalOperations(projectRoot).read()).some((event) => event.type === type && event["sessionID"] === sessionID && (route === undefined || event["route"] === route));
|
|
70
|
+
}
|
|
71
|
+
catch { // no-excuse-ok: catch -- an unreadable ledger cannot prove the record exists.
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
package/dist/v2/commands.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export declare function pluginOwnedCommandIds(roots: readonly string[]): readonl
|
|
|
36
36
|
export type CommandMark = {
|
|
37
37
|
readonly command: string;
|
|
38
38
|
readonly banner: string;
|
|
39
|
+
readonly work?: string;
|
|
39
40
|
};
|
|
40
41
|
export declare function createV2Commands(roots: readonly string[], session: CommandSessionLike): {
|
|
41
42
|
definitions: CommandDefinitionLike[];
|
package/dist/v2/commands.js
CHANGED
|
@@ -29,8 +29,8 @@ export function createV2Commands(roots, session) {
|
|
|
29
29
|
async execute(input) {
|
|
30
30
|
const nonce = randomUUID();
|
|
31
31
|
const mark = { command: command.id, banner: command.banner };
|
|
32
|
-
pending.set(input.sessionID, { nonce, mark });
|
|
33
32
|
const argumentsText = input.prompt.text.trim();
|
|
33
|
+
pending.set(input.sessionID, { nonce, mark, work: argumentsText });
|
|
34
34
|
if (command.agent !== undefined)
|
|
35
35
|
await session.switchAgent({ sessionID: input.sessionID, agent: command.agent });
|
|
36
36
|
await session.prompt({
|
|
@@ -52,7 +52,7 @@ export function createV2Commands(roots, session) {
|
|
|
52
52
|
if (expected === undefined || marker.nonce !== expected.nonce)
|
|
53
53
|
return undefined;
|
|
54
54
|
pending.delete(sessionID);
|
|
55
|
-
return expected.mark;
|
|
55
|
+
return { ...expected.mark, work: expected.work };
|
|
56
56
|
}
|
|
57
57
|
return {
|
|
58
58
|
definitions,
|
package/dist/v2/context.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AgentEditorLike, ModelEditorLike, PermissionEvaluationLike, Sessio
|
|
|
3
3
|
import type { CommandEditorLike, CommandSessionLike } from "./commands.ts";
|
|
4
4
|
import type { EventDomainLike, EventModelLike, EventSessionLike } from "./events.ts";
|
|
5
5
|
import type { ToolAfterEvent, ToolBeforeEvent } from "./hooks.ts";
|
|
6
|
+
import type { NativeGoalHostLike } from "./native-goal-runtime.ts";
|
|
6
7
|
import type { ContextEvent, PromptEvent } from "./session-hooks.ts";
|
|
7
8
|
import type { ToolEditorLike } from "./tools.ts";
|
|
8
9
|
export type Registration = {
|
|
@@ -28,6 +29,9 @@ type SessionHook = {
|
|
|
28
29
|
(name: "prompt", callback: Callback<PromptEvent>): Promise<Registration>;
|
|
29
30
|
(name: "context", callback: Callback<ContextEvent>): Promise<Registration>;
|
|
30
31
|
(name: "compaction", callback: Callback<ContextEvent>): Promise<Registration>;
|
|
32
|
+
(name: "experimental.idle", callback: Callback<{
|
|
33
|
+
readonly sessionID: string;
|
|
34
|
+
}>): Promise<Registration>;
|
|
31
35
|
};
|
|
32
36
|
export type SessionDomainLike = {
|
|
33
37
|
readonly get: (input: {
|
|
@@ -49,7 +53,8 @@ export type ServerContextLike = {
|
|
|
49
53
|
readonly command: {
|
|
50
54
|
readonly transform: Transform<CommandEditorLike>;
|
|
51
55
|
};
|
|
52
|
-
readonly
|
|
56
|
+
readonly rpc?: NativeGoalHostLike["rpc"];
|
|
57
|
+
readonly session: SessionDomainLike & SessionModelDomainLike & CommandSessionLike & EventSessionLike & NativeGoalHostLike["session"] & {
|
|
53
58
|
readonly hook: SessionHook;
|
|
54
59
|
};
|
|
55
60
|
readonly event: EventDomainLike;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type NativeGoalRoute } from "./native-goal.ts";
|
|
2
|
+
/** The `opencode.goal` RPC contract as JSON Schema, so the package needs no runtime import of the host's schema library. */
|
|
3
|
+
declare const goalDefinition: Readonly<{
|
|
4
|
+
id: "opencode.goal";
|
|
5
|
+
methods: Readonly<{
|
|
6
|
+
get: Readonly<{
|
|
7
|
+
input: Readonly<{
|
|
8
|
+
type: "object";
|
|
9
|
+
properties: Readonly<{
|
|
10
|
+
sessionID: Readonly<{
|
|
11
|
+
type: "string";
|
|
12
|
+
}>;
|
|
13
|
+
}>;
|
|
14
|
+
required: readonly string[];
|
|
15
|
+
}>;
|
|
16
|
+
output: Readonly<{}>;
|
|
17
|
+
}>;
|
|
18
|
+
}>;
|
|
19
|
+
events: Readonly<{
|
|
20
|
+
changed: Readonly<{
|
|
21
|
+
schema: Readonly<{
|
|
22
|
+
type: "object";
|
|
23
|
+
properties: Readonly<{
|
|
24
|
+
sessionID: Readonly<{
|
|
25
|
+
type: "string";
|
|
26
|
+
}>;
|
|
27
|
+
}>;
|
|
28
|
+
required: readonly string[];
|
|
29
|
+
}>;
|
|
30
|
+
}>;
|
|
31
|
+
}>;
|
|
32
|
+
}>;
|
|
33
|
+
export type GoalRpcClientLike = {
|
|
34
|
+
readonly get: (input: {
|
|
35
|
+
readonly sessionID: string;
|
|
36
|
+
}) => Promise<unknown>;
|
|
37
|
+
};
|
|
38
|
+
export type NativeGoalHostLike = {
|
|
39
|
+
/** `context.rpc`; absent on a host that has no RPC domain. */
|
|
40
|
+
readonly rpc?: (definition: typeof goalDefinition) => GoalRpcClientLike;
|
|
41
|
+
readonly session: {
|
|
42
|
+
readonly command: (input: {
|
|
43
|
+
readonly sessionID: string;
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly text: string;
|
|
46
|
+
readonly delivery?: "steer" | "queue";
|
|
47
|
+
}) => Promise<unknown>;
|
|
48
|
+
readonly synthetic: (input: {
|
|
49
|
+
readonly sessionID: string;
|
|
50
|
+
readonly text: string;
|
|
51
|
+
readonly description?: string;
|
|
52
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
53
|
+
readonly resume?: boolean;
|
|
54
|
+
}) => Promise<unknown>;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
/** A prompt the host expanded from an installed command alias file arrives as the command body, unmarked. */
|
|
58
|
+
export declare function aliasGoalRoute(input: string): {
|
|
59
|
+
readonly route: NativeGoalRoute;
|
|
60
|
+
readonly work: string;
|
|
61
|
+
} | undefined;
|
|
62
|
+
export declare function createNativeGoal(options: {
|
|
63
|
+
readonly projectRoot: string;
|
|
64
|
+
readonly host: NativeGoalHostLike;
|
|
65
|
+
}): {
|
|
66
|
+
/** Starts the goal step for a qualifying activation without holding up the user's prompt. */
|
|
67
|
+
activate(sessionID: string, route: NativeGoalRoute, work: string): Promise<void>;
|
|
68
|
+
/** The session finished its model work: clear a finished owned goal or report a conflict. */
|
|
69
|
+
idle(sessionID: string): Promise<void>;
|
|
70
|
+
/** Resolves when every started goal step has finished. */
|
|
71
|
+
settled(): Promise<void>;
|
|
72
|
+
};
|
|
73
|
+
export type NativeGoal = ReturnType<typeof createNativeGoal>;
|
|
74
|
+
export {};
|