@kisev/skills-opencode 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/assets/agents/architect.md +33 -0
- package/assets/agents/critic.md +26 -0
- package/assets/agents/manager.md +53 -0
- package/assets/agents/mapper.md +24 -0
- package/assets/agents/review.md +22 -0
- package/assets/agents/worker.md +65 -0
- package/assets/commands/askme.md +10 -0
- package/assets/commands/ast-grep-rewrite.md +10 -0
- package/assets/commands/ast-grep-search.md +10 -0
- package/assets/commands/attempt-cancel.md +10 -0
- package/assets/commands/attempt-list.md +10 -0
- package/assets/commands/attempt-result.md +10 -0
- package/assets/commands/attempt-show.md +10 -0
- package/assets/commands/capabilities.md +9 -0
- package/assets/commands/code-review.md +10 -0
- package/assets/commands/commit-msg.md +10 -0
- package/assets/commands/docs-prepare.md +10 -0
- package/assets/commands/docs-review.md +10 -0
- package/assets/commands/doctor.md +9 -0
- package/assets/commands/doit.md +10 -0
- package/assets/commands/goal-list.md +10 -0
- package/assets/commands/goal-pause.md +10 -0
- package/assets/commands/goal-prepare.md +10 -0
- package/assets/commands/goal-remove.md +10 -0
- package/assets/commands/goal-show.md +10 -0
- package/assets/commands/goal-start.md +10 -0
- package/assets/commands/lsp-report.md +10 -0
- package/assets/commands/mattermost.md +10 -0
- package/assets/commands/mr-prepare.md +10 -0
- package/assets/commands/mr-review.md +10 -0
- package/assets/commands/multi-run-cancel.md +10 -0
- package/assets/commands/multi-run-compare.md +10 -0
- package/assets/commands/multi-run-fusion.md +10 -0
- package/assets/commands/multi-run-start.md +10 -0
- package/assets/commands/multi-run-status.md +10 -0
- package/assets/commands/overview.md +10 -0
- package/assets/commands/release-prepare.md +10 -0
- package/assets/commands/release-review.md +10 -0
- package/assets/commands/route.md +9 -0
- package/assets/commands/schedule-add.md +10 -0
- package/assets/commands/schedule-disable.md +10 -0
- package/assets/commands/schedule-enable.md +10 -0
- package/assets/commands/schedule-list.md +10 -0
- package/assets/commands/schedule-remove.md +10 -0
- package/assets/commands/schedule-status.md +10 -0
- package/assets/commands/skill-improver.md +10 -0
- package/assets/commands/spec-audit.md +10 -0
- package/assets/commands/spec-init.md +10 -0
- package/assets/commands/spec-onboard.md +10 -0
- package/assets/commands/spec-update.md +10 -0
- package/assets/commands/stopit.md +10 -0
- package/assets/commands/summary.md +10 -0
- package/assets/commands/task-prepare-batch.md +10 -0
- package/assets/commands/task-prepare.md +10 -0
- package/assets/commands/task-review.md +10 -0
- package/assets/commands/task-triage.md +10 -0
- package/assets/commands/team-planning.md +10 -0
- package/assets/commands/team-retro.md +10 -0
- package/assets/commands/team-roadmap.md +10 -0
- package/assets/commands/team-slides-prompts.md +10 -0
- package/assets/commands/team-sprint-close.md +10 -0
- package/assets/commands/team-sprint-status.md +10 -0
- package/assets/commands/walkthrough.md +10 -0
- package/assets/plugins/autonomy-policy.js +3 -0
- package/assets/plugins/background-attempts.js +3 -0
- package/assets/plugins/goal-loop.js +3 -0
- package/assets/plugins/rtk.js +3 -0
- package/assets/plugins/rules-injector.js +3 -0
- package/assets/plugins/schedule.js +3 -0
- package/assets/plugins/zed-bell.js +3 -0
- package/assets/plugins/zed-clickable-paths.js +3 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +57 -0
- package/dist/generate-assets.d.ts +1 -0
- package/dist/generate-assets.js +10 -0
- package/dist/index.d.ts +160 -0
- package/dist/index.js +69 -0
- package/dist/installer.d.ts +25 -0
- package/dist/installer.js +332 -0
- package/dist/plugins/autonomy-policy.d.ts +21 -0
- package/dist/plugins/autonomy-policy.js +71 -0
- package/dist/plugins/background-attempts.d.ts +76 -0
- package/dist/plugins/background-attempts.js +113 -0
- package/dist/plugins/goal-loop.d.ts +51 -0
- package/dist/plugins/goal-loop.js +87 -0
- package/dist/plugins/rtk.d.ts +21 -0
- package/dist/plugins/rtk.js +48 -0
- package/dist/plugins/rules-injector.d.ts +22 -0
- package/dist/plugins/rules-injector.js +96 -0
- package/dist/plugins/schedule.d.ts +36 -0
- package/dist/plugins/schedule.js +100 -0
- package/dist/plugins/zed-bell.d.ts +13 -0
- package/dist/plugins/zed-bell.js +12 -0
- package/dist/plugins/zed-clickable-paths.d.ts +11 -0
- package/dist/plugins/zed-clickable-paths.js +10 -0
- package/dist/registry.d.ts +9 -0
- package/dist/registry.js +89 -0
- package/dist/routing.d.ts +39 -0
- package/dist/routing.js +90 -0
- package/dist/runtime/state.d.ts +7 -0
- package/dist/runtime/state.js +126 -0
- package/package.json +80 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { appendState, configRoot, stateRoot, readState, writeState } from "../runtime/state.js";
|
|
4
|
+
function match(pattern, value) { return new RegExp(`^${pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`).test(value); }
|
|
5
|
+
function rule(rules, tool, operation) { return [...rules].reverse().find((item) => match(item.tool, tool) && match(item.operation, operation)); }
|
|
6
|
+
function valid(value) {
|
|
7
|
+
const item = value;
|
|
8
|
+
return Boolean(item && item.schema_version === 1 && item.on_exhausted === "ask_pause" && Number.isInteger(item.max_mutations_per_session) && Number.isInteger(item.max_mutations_per_hour) && Array.isArray(item.always_ask) && Array.isArray(item.classifications));
|
|
9
|
+
}
|
|
10
|
+
export async function autonomyPolicy({ directory, cwd } = {}, options = {}) {
|
|
11
|
+
if (!options.enabled)
|
|
12
|
+
return {};
|
|
13
|
+
const project = resolve(directory ?? cwd ?? process.cwd());
|
|
14
|
+
const root = stateRoot("autonomy-policy");
|
|
15
|
+
const load = async () => {
|
|
16
|
+
let current = project;
|
|
17
|
+
while (true) {
|
|
18
|
+
try {
|
|
19
|
+
const value = JSON.parse(await readFile(resolve(current, ".opencode", "autonomy-policy.json"), "utf8"));
|
|
20
|
+
if (valid(value))
|
|
21
|
+
return value;
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code !== "ENOENT")
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const parent = dirname(current);
|
|
29
|
+
if (parent === current)
|
|
30
|
+
break;
|
|
31
|
+
current = parent;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const value = JSON.parse(await readFile(resolve(configRoot("autonomy-policy"), "autonomy-policy.json"), "utf8"));
|
|
35
|
+
return valid(value) ? value : undefined;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const inspect = async (input, output) => {
|
|
42
|
+
const policy = await load();
|
|
43
|
+
const tool = input.tool ?? input.permission;
|
|
44
|
+
const operation = typeof input.metadata?.operation === "string" ? input.metadata.operation : input.patterns?.[0] ?? input.permission;
|
|
45
|
+
const classification = policy && rule(policy.classifications, tool, operation);
|
|
46
|
+
if (!policy || !classification || classification.class === "destructive" || rule(policy.always_ask, tool, operation)) {
|
|
47
|
+
output.status = "ask";
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (classification.class === "read" || output.status === "deny")
|
|
51
|
+
return;
|
|
52
|
+
const path = resolve(root, `${encodeURIComponent(input.sessionID)}.json`);
|
|
53
|
+
const previous = await readState(path, root);
|
|
54
|
+
const state = previous && previous.schema_version === 1 ? previous : { schema_version: 1, session_id: input.sessionID, mutations: 0, timestamps: [], paused: false };
|
|
55
|
+
const current = options.now?.() ?? Date.now();
|
|
56
|
+
state.timestamps = state.timestamps.filter((value) => value > current - 3_600_000);
|
|
57
|
+
if ((policy.max_mutations_per_session > 0 && state.mutations >= policy.max_mutations_per_session) || (policy.max_mutations_per_hour > 0 && state.timestamps.length >= policy.max_mutations_per_hour)) {
|
|
58
|
+
state.paused = true;
|
|
59
|
+
await writeState(path, root, state);
|
|
60
|
+
await appendState(resolve(root, "receipts.jsonl"), root, { schema_version: 1, session_id: input.sessionID, tool, class: "mutate", decision: "ask", reason: "budget-exhausted", at: new Date().toISOString() });
|
|
61
|
+
output.status = "ask";
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
state.mutations += 1;
|
|
65
|
+
state.timestamps.push(current);
|
|
66
|
+
await writeState(path, root, state);
|
|
67
|
+
await appendState(resolve(root, "receipts.jsonl"), root, { schema_version: 1, session_id: input.sessionID, tool, class: "mutate", decision: "passed", reason: "budget-available", at: new Date().toISOString() });
|
|
68
|
+
};
|
|
69
|
+
return { "permission.ask": inspect };
|
|
70
|
+
}
|
|
71
|
+
export default autonomyPolicy;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
type Client = {
|
|
2
|
+
session: {
|
|
3
|
+
create?: (input: {
|
|
4
|
+
query?: Record<string, unknown>;
|
|
5
|
+
body: Record<string, unknown>;
|
|
6
|
+
}) => Promise<unknown>;
|
|
7
|
+
prompt: (input: {
|
|
8
|
+
path: {
|
|
9
|
+
id: string;
|
|
10
|
+
};
|
|
11
|
+
body: Record<string, unknown>;
|
|
12
|
+
}) => Promise<unknown>;
|
|
13
|
+
abort?: (input: {
|
|
14
|
+
path: {
|
|
15
|
+
id: string;
|
|
16
|
+
};
|
|
17
|
+
}) => Promise<unknown>;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export type BackgroundAttemptsOptions = {
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
concurrency?: number;
|
|
23
|
+
parentConcurrency?: number;
|
|
24
|
+
};
|
|
25
|
+
export declare function backgroundAttempts({ client, directory, cwd }: {
|
|
26
|
+
client: Client;
|
|
27
|
+
directory?: string;
|
|
28
|
+
cwd?: string;
|
|
29
|
+
}, options?: BackgroundAttemptsOptions): Promise<{
|
|
30
|
+
tool?: undefined;
|
|
31
|
+
"tool.execute.before"?: undefined;
|
|
32
|
+
} | {
|
|
33
|
+
tool: {
|
|
34
|
+
background_attempts: {
|
|
35
|
+
description: string;
|
|
36
|
+
args: {
|
|
37
|
+
action: import("zod").ZodEnum<{
|
|
38
|
+
cancel: "cancel";
|
|
39
|
+
list: "list";
|
|
40
|
+
result: "result";
|
|
41
|
+
status: "status";
|
|
42
|
+
start: "start";
|
|
43
|
+
retry: "retry";
|
|
44
|
+
}>;
|
|
45
|
+
task: import("zod").ZodOptional<import("zod").ZodString>;
|
|
46
|
+
category: import("zod").ZodOptional<import("zod").ZodString>;
|
|
47
|
+
decision: import("zod").ZodOptional<import("zod").ZodAny>;
|
|
48
|
+
priority: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
49
|
+
read_only: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
50
|
+
attempt_id: import("zod").ZodOptional<import("zod").ZodString>;
|
|
51
|
+
expected_revision: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
52
|
+
expected_status: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
53
|
+
queued: "queued";
|
|
54
|
+
running: "running";
|
|
55
|
+
waiting: "waiting";
|
|
56
|
+
}>>;
|
|
57
|
+
};
|
|
58
|
+
execute(args: {
|
|
59
|
+
action: "cancel" | "list" | "result" | "status" | "start" | "retry";
|
|
60
|
+
task?: string | undefined;
|
|
61
|
+
category?: string | undefined;
|
|
62
|
+
decision?: any;
|
|
63
|
+
priority?: number | undefined;
|
|
64
|
+
read_only?: boolean | undefined;
|
|
65
|
+
attempt_id?: string | undefined;
|
|
66
|
+
expected_revision?: number | undefined;
|
|
67
|
+
expected_status?: "queued" | "running" | "waiting" | undefined;
|
|
68
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
"tool.execute.before": (input: {
|
|
72
|
+
tool: string;
|
|
73
|
+
sessionID: string;
|
|
74
|
+
}) => Promise<void>;
|
|
75
|
+
}>;
|
|
76
|
+
export default backgroundAttempts;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { tool } from "@opencode-ai/plugin";
|
|
4
|
+
import { appendState, listState, readState, stateRoot, writeState } from "../runtime/state.js";
|
|
5
|
+
const TERMINAL = new Set(["completed", "failed", "cancelled", "orphaned"]);
|
|
6
|
+
const LIMIT = 2;
|
|
7
|
+
function id(value) {
|
|
8
|
+
const item = (value?.data ?? value);
|
|
9
|
+
return typeof item?.id === "string" && item.id ? item.id : undefined;
|
|
10
|
+
}
|
|
11
|
+
export async function backgroundAttempts({ client, directory, cwd }, options = {}) {
|
|
12
|
+
if (!options.enabled)
|
|
13
|
+
return {};
|
|
14
|
+
const project = resolve(directory ?? cwd ?? process.cwd());
|
|
15
|
+
const root = stateRoot("attempt");
|
|
16
|
+
const attemptsRoot = join(root, "attempts");
|
|
17
|
+
const load = async (attemptID) => readState(join(attemptsRoot, `${attemptID}.json`), root);
|
|
18
|
+
const all = async () => (await Promise.all((await listState(attemptsRoot)).map((name) => load(name.slice(0, -5))))).filter((item) => Boolean(item));
|
|
19
|
+
const save = async (item, event) => {
|
|
20
|
+
item.receipts.push({ at: new Date().toISOString(), event, status: item.status, revision: item.revision });
|
|
21
|
+
await writeState(join(attemptsRoot, `${item.attempt_id}.json`), root, item);
|
|
22
|
+
await appendState(join(root, "receipts.jsonl"), root, { attempt_id: item.attempt_id, event, status: item.status, revision: item.revision, at: new Date().toISOString() });
|
|
23
|
+
};
|
|
24
|
+
const transition = async (item, status, event) => {
|
|
25
|
+
if (!TERMINAL.has(item.status)) {
|
|
26
|
+
item.status = status;
|
|
27
|
+
item.revision += 1;
|
|
28
|
+
}
|
|
29
|
+
await save(item, event);
|
|
30
|
+
};
|
|
31
|
+
const pump = async () => {
|
|
32
|
+
const records = await all();
|
|
33
|
+
const running = records.filter((item) => item.status === "running" || item.status === "waiting");
|
|
34
|
+
const limit = options.concurrency ?? LIMIT;
|
|
35
|
+
for (const item of records.filter((candidate) => candidate.project === project && candidate.status === "queued").sort((left, right) => left.priority - right.priority || left.created_at.localeCompare(right.created_at))) {
|
|
36
|
+
if (running.length >= limit || running.filter((candidate) => candidate.parent_session_id === item.parent_session_id).length >= (options.parentConcurrency ?? 1))
|
|
37
|
+
continue;
|
|
38
|
+
if (!client.session.create) {
|
|
39
|
+
item.error = { message: "session.create unavailable", retryable: false };
|
|
40
|
+
await transition(item, "failed", "dispatch.error");
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const created = await client.session.create({ query: { directory: item.directory }, body: { title: `background-attempt:${item.attempt_id}`, parentID: item.parent_session_id, directory: item.directory } });
|
|
44
|
+
const child = id(created);
|
|
45
|
+
if (!child) {
|
|
46
|
+
item.error = { message: "OpenCode did not return child session id", retryable: false };
|
|
47
|
+
await transition(item, "failed", "dispatch.error");
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
item.child_session_id = child;
|
|
51
|
+
await transition(item, "running", "session.created");
|
|
52
|
+
running.push(item);
|
|
53
|
+
void client.session.prompt({ path: { id: child }, body: { agent: item.agent, parts: [{ type: "text", text: `Complete this bounded background attempt. Do not create recursive attempts.\nTask:\n${item.task}` }] } }).catch(async (error) => {
|
|
54
|
+
item.error = { message: String(error), retryable: false };
|
|
55
|
+
await transition(item, "failed", "session.prompt.error");
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const attempts = tool({
|
|
60
|
+
description: "Start and manage bounded routed background attempts.",
|
|
61
|
+
args: { action: tool.schema.enum(["start", "list", "status", "result", "cancel", "retry"]), task: tool.schema.string().optional(), category: tool.schema.string().optional(), decision: tool.schema.any().optional(), priority: tool.schema.number().optional(), read_only: tool.schema.boolean().optional(), attempt_id: tool.schema.string().optional(), expected_revision: tool.schema.number().optional(), expected_status: tool.schema.enum(["queued", "running", "waiting"]).optional() },
|
|
62
|
+
async execute(args, context) {
|
|
63
|
+
if (args.action === "start") {
|
|
64
|
+
const decision = args.decision;
|
|
65
|
+
if (!args.task || !args.category || !decision || typeof decision.agent !== "string" || typeof decision.decision_digest !== "string")
|
|
66
|
+
throw new Error("start requires a routed decision, task and category");
|
|
67
|
+
const item = { schema_version: 1, attempt_id: randomUUID(), parent_session_id: context.sessionID, project, directory: project, task: args.task, category: args.category, agent: decision.agent, routing_decision_digest: decision.decision_digest, status: "queued", revision: 0, priority: args.priority ?? 0, read_only: args.read_only !== false, created_at: new Date().toISOString(), receipts: [], retry_number: 0 };
|
|
68
|
+
await save(item, "attempt.queued");
|
|
69
|
+
await pump();
|
|
70
|
+
const current = await load(item.attempt_id);
|
|
71
|
+
return JSON.stringify({ attempt_id: item.attempt_id, status: current?.status ?? "queued" });
|
|
72
|
+
}
|
|
73
|
+
const candidates = (await all()).filter((item) => item.project === project && item.parent_session_id === context.sessionID);
|
|
74
|
+
if (args.action === "list")
|
|
75
|
+
return JSON.stringify(candidates.map(({ task: _task, receipts: _receipts, ...item }) => item));
|
|
76
|
+
const item = candidates.find((candidate) => candidate.attempt_id === args.attempt_id);
|
|
77
|
+
if (!item)
|
|
78
|
+
throw new Error("attempt not found in current parent session/project");
|
|
79
|
+
if (args.action === "retry") {
|
|
80
|
+
if (item.status !== "failed" || item.read_only !== true || item.error?.retryable !== true || item.retry_number >= 2)
|
|
81
|
+
throw new Error("retry requires a failed, read-only, retryable attempt within its retry limit");
|
|
82
|
+
const retry = { ...item, attempt_id: randomUUID(), parent_attempt_id: item.attempt_id, retry_number: item.retry_number + 1, status: "queued", revision: 0, created_at: new Date().toISOString(), receipts: [], child_session_id: undefined, workspace_id: undefined, result: undefined, error: undefined };
|
|
83
|
+
await save(retry, "attempt.retry.queued");
|
|
84
|
+
await pump();
|
|
85
|
+
return JSON.stringify({ attempt_id: retry.attempt_id, parent_attempt_id: item.attempt_id, status: (await load(retry.attempt_id))?.status ?? "queued" });
|
|
86
|
+
}
|
|
87
|
+
if (args.action === "status")
|
|
88
|
+
return JSON.stringify(item);
|
|
89
|
+
if (args.action === "result")
|
|
90
|
+
return JSON.stringify(TERMINAL.has(item.status) ? { attempt_id: item.attempt_id, status: item.status, terminal: true, result: item.result, session_id: item.child_session_id, workspace_id: item.workspace_id } : { attempt_id: item.attempt_id, status: item.status, terminal: false });
|
|
91
|
+
if (TERMINAL.has(item.status))
|
|
92
|
+
return JSON.stringify({ attempt_id: item.attempt_id, status: item.status, cancelled: false, reason: "terminal" });
|
|
93
|
+
if (item.revision !== args.expected_revision || item.status !== args.expected_status)
|
|
94
|
+
throw new Error("attempt changed since cancellation preview");
|
|
95
|
+
if (item.status === "queued")
|
|
96
|
+
await transition(item, "cancelled", "cancel.confirmed");
|
|
97
|
+
else if (item.child_session_id && client.session.abort) {
|
|
98
|
+
await client.session.abort({ path: { id: item.child_session_id } });
|
|
99
|
+
await transition(item, "cancelled", "cancel.confirmed");
|
|
100
|
+
}
|
|
101
|
+
else
|
|
102
|
+
await transition(item, "orphaned", "cancel.unconfirmed");
|
|
103
|
+
return JSON.stringify({ attempt_id: item.attempt_id, status: item.status });
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
return { tool: { background_attempts: attempts }, "tool.execute.before": async (input) => {
|
|
107
|
+
if (input.tool !== "background_attempts" && input.tool !== "task")
|
|
108
|
+
return;
|
|
109
|
+
if ((await all()).some((item) => item.child_session_id === input.sessionID))
|
|
110
|
+
throw new Error("child attempts cannot create recursive attempts or tasks");
|
|
111
|
+
} };
|
|
112
|
+
}
|
|
113
|
+
export default backgroundAttempts;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
type Goal = {
|
|
2
|
+
schema_version: 1;
|
|
3
|
+
goal_id: string;
|
|
4
|
+
session_id: string;
|
|
5
|
+
status: "running" | "paused" | "complete" | "blocked";
|
|
6
|
+
revision: number;
|
|
7
|
+
limits: {
|
|
8
|
+
turn_cap: number;
|
|
9
|
+
token_budget: number;
|
|
10
|
+
};
|
|
11
|
+
usage: {
|
|
12
|
+
turns: number;
|
|
13
|
+
tokens: number;
|
|
14
|
+
};
|
|
15
|
+
receipts: Array<Record<string, unknown>>;
|
|
16
|
+
updated_at: string;
|
|
17
|
+
};
|
|
18
|
+
type Client = {
|
|
19
|
+
session: {
|
|
20
|
+
messages: (input: {
|
|
21
|
+
path: {
|
|
22
|
+
id: string;
|
|
23
|
+
};
|
|
24
|
+
}) => Promise<unknown>;
|
|
25
|
+
prompt: (input: {
|
|
26
|
+
path: {
|
|
27
|
+
id: string;
|
|
28
|
+
};
|
|
29
|
+
body: Record<string, unknown>;
|
|
30
|
+
}) => Promise<unknown>;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export type GoalLoopOptions = {
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
quietWindowMs?: number;
|
|
36
|
+
now?: () => number;
|
|
37
|
+
audit?: (goal: Readonly<Goal>) => Promise<"continue" | "complete" | "blocked">;
|
|
38
|
+
};
|
|
39
|
+
export declare function goalLoop({ client }: {
|
|
40
|
+
client: Client;
|
|
41
|
+
}, options?: GoalLoopOptions): Promise<{
|
|
42
|
+
event?: undefined;
|
|
43
|
+
} | {
|
|
44
|
+
event: ({ event }: {
|
|
45
|
+
event: {
|
|
46
|
+
type?: string;
|
|
47
|
+
properties?: Record<string, unknown>;
|
|
48
|
+
};
|
|
49
|
+
}) => Promise<void>;
|
|
50
|
+
}>;
|
|
51
|
+
export default goalLoop;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { listState, readState, stateRoot, writeState } from "../runtime/state.js";
|
|
3
|
+
function sessionID(event) {
|
|
4
|
+
const value = event.properties?.sessionID ?? event.properties?.session_id;
|
|
5
|
+
return typeof value === "string" && value ? value : undefined;
|
|
6
|
+
}
|
|
7
|
+
function tokens(messages) {
|
|
8
|
+
const entries = Array.isArray(messages) ? messages : messages?.data;
|
|
9
|
+
if (!Array.isArray(entries))
|
|
10
|
+
return 0;
|
|
11
|
+
const last = [...entries].reverse().find((entry) => (entry?.info?.role === "assistant"));
|
|
12
|
+
const value = last?.info?.tokens?.total ?? last?.info?.tokens?.output;
|
|
13
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
14
|
+
}
|
|
15
|
+
export async function goalLoop({ client }, options = {}) {
|
|
16
|
+
if (!options.enabled)
|
|
17
|
+
return {};
|
|
18
|
+
const root = stateRoot("goal");
|
|
19
|
+
const timers = new Map();
|
|
20
|
+
const lookup = async (session) => {
|
|
21
|
+
for (const name of await listState(root)) {
|
|
22
|
+
const state = await readState(join(root, name), root);
|
|
23
|
+
if (state?.schema_version === 1 && state.session_id === session)
|
|
24
|
+
return { state, path: join(root, name) };
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
};
|
|
28
|
+
const settle = async (session, status, note) => {
|
|
29
|
+
const item = await lookup(session);
|
|
30
|
+
if (!item || item.state.status !== "running")
|
|
31
|
+
return;
|
|
32
|
+
item.state.status = status;
|
|
33
|
+
item.state.revision += 1;
|
|
34
|
+
item.state.updated_at = new Date().toISOString();
|
|
35
|
+
item.state.receipts.push({ revision: item.state.revision, status, note, at: item.state.updated_at });
|
|
36
|
+
await writeState(item.path, root, item.state);
|
|
37
|
+
};
|
|
38
|
+
const run = async (session) => {
|
|
39
|
+
timers.delete(session);
|
|
40
|
+
const item = await lookup(session);
|
|
41
|
+
if (!item || item.state.status !== "running")
|
|
42
|
+
return;
|
|
43
|
+
try {
|
|
44
|
+
const used = tokens(await client.session.messages({ path: { id: session } }));
|
|
45
|
+
item.state.usage.turns += 1;
|
|
46
|
+
item.state.usage.tokens += used;
|
|
47
|
+
item.state.revision += 1;
|
|
48
|
+
item.state.updated_at = new Date().toISOString();
|
|
49
|
+
item.state.receipts.push({ revision: item.state.revision, status: "running", note: "turn audited", at: item.state.updated_at });
|
|
50
|
+
await writeState(item.path, root, item.state);
|
|
51
|
+
if (item.state.usage.turns >= item.state.limits.turn_cap)
|
|
52
|
+
return settle(session, "blocked", "turn cap reached");
|
|
53
|
+
if (item.state.limits.token_budget > 0 && item.state.usage.tokens >= item.state.limits.token_budget)
|
|
54
|
+
return settle(session, "blocked", "token budget reached");
|
|
55
|
+
const verdict = await (options.audit?.(item.state) ?? Promise.resolve("continue"));
|
|
56
|
+
if (verdict === "complete" || verdict === "blocked")
|
|
57
|
+
return settle(session, verdict, "audit verdict");
|
|
58
|
+
await client.session.prompt({ path: { id: session }, body: { parts: [{ type: "text", text: "Continue the current goal within its declared constraints and boundaries." }] } });
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
await settle(session, "blocked", String(error));
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const schedule = (session) => {
|
|
65
|
+
const previous = timers.get(session);
|
|
66
|
+
if (previous)
|
|
67
|
+
clearTimeout(previous);
|
|
68
|
+
timers.set(session, setTimeout(() => void run(session), options.quietWindowMs ?? 250));
|
|
69
|
+
};
|
|
70
|
+
return { event: async ({ event }) => {
|
|
71
|
+
const session = sessionID(event);
|
|
72
|
+
if (!session)
|
|
73
|
+
return;
|
|
74
|
+
if (event.type === "session.abort" || event.type === "session.aborted") {
|
|
75
|
+
const timer = timers.get(session);
|
|
76
|
+
if (timer)
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
timers.delete(session);
|
|
79
|
+
await settle(session, "paused", "user abort");
|
|
80
|
+
}
|
|
81
|
+
else if (event.type === "session.error")
|
|
82
|
+
await settle(session, "blocked", "session error");
|
|
83
|
+
else if (event.type === "session.idle")
|
|
84
|
+
schedule(session);
|
|
85
|
+
} };
|
|
86
|
+
}
|
|
87
|
+
export default goalLoop;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type HookInput = {
|
|
2
|
+
tool: string;
|
|
3
|
+
args?: {
|
|
4
|
+
command?: unknown;
|
|
5
|
+
};
|
|
6
|
+
};
|
|
7
|
+
type HookOutput = {
|
|
8
|
+
output?: unknown;
|
|
9
|
+
};
|
|
10
|
+
export type RtkOptions = {
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
bin?: string;
|
|
13
|
+
threshold?: number;
|
|
14
|
+
run?: (filter: string, input: string) => Promise<string | undefined>;
|
|
15
|
+
};
|
|
16
|
+
export declare function rtk(options?: RtkOptions): Promise<{
|
|
17
|
+
"tool.execute.after"?: undefined;
|
|
18
|
+
} | {
|
|
19
|
+
"tool.execute.after": (input: HookInput, output: HookOutput) => Promise<void>;
|
|
20
|
+
}>;
|
|
21
|
+
export default rtk;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const THRESHOLD = 8_000;
|
|
3
|
+
const FILTERS = [
|
|
4
|
+
{ prefix: ["git", "log"], filter: "git-log" }, { prefix: ["git", "diff"], filter: "git-diff" },
|
|
5
|
+
{ prefix: ["git", "status"], filter: "git-status" }, { prefix: ["rg"], filter: "grep" },
|
|
6
|
+
{ prefix: ["grep"], filter: "grep" }, { prefix: ["pytest"], filter: "pytest" }, { prefix: ["tsc"], filter: "tsc" },
|
|
7
|
+
];
|
|
8
|
+
function filter(command) {
|
|
9
|
+
if (/[|;$&\n()`]/.test(command))
|
|
10
|
+
return undefined;
|
|
11
|
+
const values = command.trim().split(/\s+/);
|
|
12
|
+
return FILTERS.find((item) => item.prefix.every((part, index) => values[index] === part))?.filter;
|
|
13
|
+
}
|
|
14
|
+
function truncate(value) {
|
|
15
|
+
const points = Array.from(value);
|
|
16
|
+
const size = 3_200;
|
|
17
|
+
return points.length <= size * 2 ? value : `${points.slice(0, size).join("")}\n…\n${points.slice(-size).join("")}`;
|
|
18
|
+
}
|
|
19
|
+
function external(binary, selected, input) {
|
|
20
|
+
return new Promise((done) => {
|
|
21
|
+
const child = spawn(binary, ["pipe", "--filter", selected], { stdio: ["pipe", "pipe", "ignore"], timeout: 5_000 });
|
|
22
|
+
let output = "";
|
|
23
|
+
child.stdout.setEncoding("utf8");
|
|
24
|
+
child.stdout.on("data", (chunk) => { output += chunk; });
|
|
25
|
+
child.once("error", () => done(undefined));
|
|
26
|
+
child.once("close", (code) => done(code === 0 ? output : undefined));
|
|
27
|
+
child.stdin.end(input, "utf8");
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export async function rtk(options = {}) {
|
|
31
|
+
if (options.enabled === false)
|
|
32
|
+
return {};
|
|
33
|
+
const run = options.run ?? ((selected, input) => external(options.bin ?? "rtk", selected, input));
|
|
34
|
+
return { "tool.execute.after": async (input, output) => {
|
|
35
|
+
try {
|
|
36
|
+
if (input.tool === "edit" && typeof output.output === "string" && /oldString (not found|found multiple times|and newString must be different)/i.test(output.output))
|
|
37
|
+
output.output = `${output.output}\nSTOP. Read the file before retrying Edit.`;
|
|
38
|
+
if (input.tool !== "bash" || typeof output.output !== "string" || output.output.length < (options.threshold ?? THRESHOLD))
|
|
39
|
+
return;
|
|
40
|
+
const selected = typeof input.args?.command === "string" ? filter(input.args.command) : undefined;
|
|
41
|
+
const compressed = selected ? await run(selected, output.output) : undefined;
|
|
42
|
+
const result = compressed && compressed.length < output.output.length ? compressed : truncate(output.output);
|
|
43
|
+
output.output = `${result}\n[rtk: compressed ${output.output.length} chars using ${compressed ? `rtk/${selected}` : "head+tail"}]`;
|
|
44
|
+
}
|
|
45
|
+
catch { /* RTK is fail-open: preserve original tool output on plugin failure. */ }
|
|
46
|
+
} };
|
|
47
|
+
}
|
|
48
|
+
export default rtk;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type RulesInjectorOptions = {
|
|
2
|
+
budget?: number;
|
|
3
|
+
cwd?: string;
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function rulesInjector(options?: RulesInjectorOptions): Promise<{
|
|
7
|
+
"tool.execute.after"?: undefined;
|
|
8
|
+
event?: undefined;
|
|
9
|
+
"experimental.chat.system.transform"?: undefined;
|
|
10
|
+
} | {
|
|
11
|
+
"tool.execute.after": (input: unknown, result: unknown) => Promise<void>;
|
|
12
|
+
event: ({ event }: {
|
|
13
|
+
event?: {
|
|
14
|
+
type?: string;
|
|
15
|
+
properties?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
"experimental.chat.system.transform": (input: unknown, result: {
|
|
19
|
+
system?: unknown[];
|
|
20
|
+
}) => Promise<void>;
|
|
21
|
+
}>;
|
|
22
|
+
export default rulesInjector;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
const NAME = "AGENTS.md";
|
|
5
|
+
function sessionID(value) { const item = value; for (const key of ["sessionID", "sessionId", "session_id"])
|
|
6
|
+
if (typeof item?.[key] === "string" && item[key])
|
|
7
|
+
return item[key]; return undefined; }
|
|
8
|
+
function pathOf(input, output) { for (const value of [input, output]) {
|
|
9
|
+
const args = value?.args;
|
|
10
|
+
for (const key of ["filePath", "file_path", "path"])
|
|
11
|
+
if (typeof args?.[key] === "string")
|
|
12
|
+
return args[key];
|
|
13
|
+
} return undefined; }
|
|
14
|
+
function output(value, text) { const item = value; if (typeof item?.output === "string")
|
|
15
|
+
item.output = `${item.output}\n${text}`; }
|
|
16
|
+
function marker(rule) { return `[rules-injector source=${rule.path} revision=${rule.revision}]`; }
|
|
17
|
+
function block(rule) { return `${marker(rule)}\n${rule.content}\n[/rules-injector source=${rule.path}]`; }
|
|
18
|
+
export async function rulesInjector(options = {}) {
|
|
19
|
+
if (options.enabled === false)
|
|
20
|
+
return {};
|
|
21
|
+
const budget = options.budget ?? 12_000;
|
|
22
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
23
|
+
const global = resolve(process.env.XDG_CONFIG_HOME ?? resolve(homedir(), ".config"), "opencode");
|
|
24
|
+
const sessions = new Map();
|
|
25
|
+
const state = (identifier) => { let item = sessions.get(identifier); if (!item) {
|
|
26
|
+
item = { loaded: new Map(), injected: new Set(), used: 0, replay: false };
|
|
27
|
+
sessions.set(identifier, item);
|
|
28
|
+
} return item; };
|
|
29
|
+
const inject = async (input, result) => {
|
|
30
|
+
const call = input;
|
|
31
|
+
if (call?.tool !== "read" && call?.tool !== "edit")
|
|
32
|
+
return;
|
|
33
|
+
const identifier = sessionID(input);
|
|
34
|
+
const target = pathOf(input, result);
|
|
35
|
+
if (!identifier || !target)
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
const native = typeof call?.directory === "string" ? call.directory : cwd;
|
|
39
|
+
const paths = [];
|
|
40
|
+
let current = dirname(resolve(native, target));
|
|
41
|
+
while (true) {
|
|
42
|
+
const candidate = resolve(current, NAME);
|
|
43
|
+
try {
|
|
44
|
+
const info = await lstat(candidate);
|
|
45
|
+
if (info.isFile() && candidate !== resolve(native, NAME) && candidate !== resolve(global, NAME))
|
|
46
|
+
paths.push(await realpath(candidate));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error.code !== "ENOENT")
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
const parent = dirname(current);
|
|
53
|
+
if (parent === current)
|
|
54
|
+
break;
|
|
55
|
+
current = parent;
|
|
56
|
+
}
|
|
57
|
+
const currentState = state(identifier);
|
|
58
|
+
const additions = [];
|
|
59
|
+
for (const path of paths.reverse()) {
|
|
60
|
+
if (currentState.injected.has(path))
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
const info = await lstat(path);
|
|
64
|
+
if (!info.isFile())
|
|
65
|
+
continue;
|
|
66
|
+
const rule = { path, revision: `${Math.trunc(info.mtimeMs)}:${info.size}`, content: await readFile(path, "utf8") };
|
|
67
|
+
const text = block(rule);
|
|
68
|
+
if (currentState.used + text.length > budget) {
|
|
69
|
+
additions.push(`[rules-injector warning] context budget exhausted; skipped ${path}`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
currentState.loaded.set(path, rule);
|
|
73
|
+
currentState.injected.add(path);
|
|
74
|
+
currentState.used += text.length;
|
|
75
|
+
additions.push(text);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
additions.push(`[rules-injector warning] cannot read ${path}: ${String(error)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (additions.length)
|
|
82
|
+
output(result, additions.join("\n\n"));
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
output(result, `[rules-injector warning] cannot read AGENTS.md: ${String(error)}`);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
return { "tool.execute.after": inject, event: async ({ event }) => { if (event?.type?.includes("compaction")) {
|
|
89
|
+
const identifier = sessionID(event.properties);
|
|
90
|
+
if (identifier)
|
|
91
|
+
state(identifier).replay = true;
|
|
92
|
+
} }, "experimental.chat.system.transform": async (input, result) => { const identifier = sessionID(input); if (!identifier || !state(identifier).replay || !Array.isArray(result.system))
|
|
93
|
+
return; for (const rule of state(identifier).loaded.values())
|
|
94
|
+
result.system.push(block(rule)); state(identifier).replay = false; } };
|
|
95
|
+
}
|
|
96
|
+
export default rulesInjector;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type Client = {
|
|
2
|
+
session: {
|
|
3
|
+
create?: (input: {
|
|
4
|
+
body: Record<string, unknown>;
|
|
5
|
+
}) => Promise<unknown>;
|
|
6
|
+
prompt: (input: {
|
|
7
|
+
path: {
|
|
8
|
+
id: string;
|
|
9
|
+
};
|
|
10
|
+
body: Record<string, unknown>;
|
|
11
|
+
}) => Promise<unknown>;
|
|
12
|
+
abort?: (input: {
|
|
13
|
+
path: {
|
|
14
|
+
id: string;
|
|
15
|
+
};
|
|
16
|
+
}) => Promise<unknown>;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export type SchedulerOptions = {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
clock?: () => number;
|
|
22
|
+
};
|
|
23
|
+
export declare function scheduler({ client, directory, cwd }: {
|
|
24
|
+
client: Client;
|
|
25
|
+
directory?: string;
|
|
26
|
+
cwd?: string;
|
|
27
|
+
}, options?: SchedulerOptions): Promise<{
|
|
28
|
+
event?: undefined;
|
|
29
|
+
} | {
|
|
30
|
+
event: ({ event }: {
|
|
31
|
+
event: {
|
|
32
|
+
type?: string;
|
|
33
|
+
};
|
|
34
|
+
}) => Promise<void>;
|
|
35
|
+
}>;
|
|
36
|
+
export default scheduler;
|